VS Code extension analytics
Last updated: Oct-26-2026
Overview
The Cloudinary VS Code extension reports usage data through three separate mechanisms:
-
UI events. The extension explicitly reports named events, such as
library_openedorupload_succeeded, to the Cloudinary analytics endpoint. These events describe what a developer did inside the editor. -
API call attribution. Every Admin API and Upload API call the extension makes carries a
CloudinaryVSCode/<version>user agent, so extension traffic is identifiable in the standard API logs without any separate reporting. -
Delivery URL analytics. Delivery URLs the extension generates carry an
_aparameter that identifies the Node SDK rather than the extension, so it can't be used for extension attribution. See Delivery URL analytics.
The first two land in the same Snowflake data source, CLOUDINARY.LOGS.UDR_RAW, because the analytics endpoint is itself a Cloudinary API controller. The third rides on delivery URLs and is recorded in the CDN delivery logs.
How UI events are reported
UI events are sent by AnalyticsService in src/analytics/analyticsService.ts. A single instance is created during activation in src/extension.ts and passed to every command and webview provider that needs it.
Each event is one HTTP POST request, with the whole payload in the query string:
Requests are fire and forget. track() doesn't await the response, and any network failure is swallowed, so analytics can never block or break an extension workflow.
Parameters sent with every event
The analytics service attaches these parameters to every event, regardless of the event name.
| Parameter | Description |
|---|---|
source |
Always vscode_extension. Also used as the endpoint path segment. |
event |
The event name. See the UI event catalog. |
extension_version |
The version field from the extension's package.json, for example 1.0.1. |
ide_platform |
The detected editor: cursor, windsurf, antigravity, or github-copilot. |
session_id |
A random UUID created on first use and stored in VS Code global state under cloudinary.analyticsSessionId. |
event_time |
The client clock at report time, as an ISO 8601 timestamp. |
cloud_name |
The active product environment, or the literal (not configured) when no cloud is configured. |
debug_id |
Only present when the CLOUDINARY_ANALYTICS_DEBUG_ID environment variable is set. Used to isolate a specific developer's events during testing. |
ide_platform reports github-copilot for plain VS Code, and also for any editor the extension fails to recognize. It's a label for the AI tooling associated with the editor, not the editor name, so don't read it as a clean editor breakdown. The user agent on API calls carries the real editor name and version.Payload rules
The analytics service normalizes and filters every event payload before sending it:
-
Event names must match
^[a-z][a-z0-9_]*$. An event with any other name is silently dropped, so it never reaches the endpoint. - Keys keep the spelling supplied by the caller. Reserved and sensitive key checks are case-insensitive and treat hyphens, dots, and whitespace as underscores.
-
Values are converted to strings. Only strings, finite numbers, and
trueorfalsevalues survive. Empty strings,null,undefined,NaN,Infinity, objects, and arrays are dropped. -
Reserved keys are dropped so an event payload can't overwrite a common parameter:
source,extension_version,ide_platform,session_id,event_time, andevent. -
Sensitive keys are dropped. Blocked names include credentials and tokens (
api_key,api_secret,access_token,authorization,client_secret,password,refresh_token,secret,token,id_token), contact details (email,user_email,phone), and message content (prompt,message,message_content,user_message). Equivalent camel case and punctuation variants are also blocked.
Current analytics events don't include search text, Docs AI prompts, feedback comments, public IDs, URLs, or file paths. asset_search and message_sent report character counts instead. Docs AI sends conversation content to the Docs AI API as part of chat and feedback workflows; that traffic is separate from analytics reporting.
cloud_name and debug_id aren't reserved. An event-specific payload with either name replaces the corresponding common value.
Events reported from the Docs AI webview
The Docs AI view runs in a webview, which has no direct access to the analytics service. Its client code calls a local trackAnalytics() helper that posts an analyticsEvent message to the extension host, and DocsAiViewProvider._handleAnalyticsEvent forwards the name and payload to AnalyticsService.track().
The forwarding path doesn't use an allowlist of event names. The event name regex in the analytics service is the only guard, so any renamed event in the webview client reaches the endpoint under its new name without a matching change on the extension host side.
UI event catalog
Lifecycle and configuration
| Event | Reported when | Event-specific parameters |
|---|---|---|
extension_activated |
VS Code activates the extension. | None. |
config_validation_succeeded |
A credential check against GET /v1_1/<cloud>/config?settings=true returns 200 with a recognized folder mode. |
folder_mode: dynamic or fixed.entry_point: activation, config_change, manual_refresh, or switch. |
config_validation_failed |
The same credential check is rejected or unreachable. |
error_reason: unauthorized, unexpected_response, unexpected_status, or network_error.status: the HTTP status code. Absent for network_error.entry_point: as above. |
config_opened |
The developer opens the global configuration file from the home screen. |
entry_point: homescreen. |
config_validation_skipped event, so config_validation_succeeded plus config_validation_failed doesn't add up to the number of activations.Navigation
| Event | Reported when | Event-specific parameters |
|---|---|---|
home_opened |
The home screen is shown. |
entry_point: command for the cloudinary.showHomescreen command, or webview_ready when the webview reports that it finished loading. |
library_opened |
The Library view is shown. |
entry_point: command, search when the command carried a search query, or webview_ready. |
library_refreshed |
The developer runs the cloudinary.refresh command. |
entry_point: command. |
docs_ai_opened |
The Docs AI view is shown. |
entry_point: command or webview_ready.has_initial_prompt: true or false. Sent by the cloudinary.showDocsAI command and on webview ready.has_initial_conversation: true or false. Sent by the cloudinary.showDocsAIConversation command and on webview ready. |
entry_point = 'webview_ready' separately from the command entry points.Library
| Event | Reported when | Event-specific parameters |
|---|---|---|
asset_search |
A search is submitted. |
entry_point: library or homescreen.query_length: the character count of the query. The query text itself is never sent.resource_type: the active resource type filter. Sent from the library only. |
library_view_changed |
The resource type filter or the sort direction changes. |
resource_type: the new filter.sort_direction: the new sort direction. |
asset_opened |
An asset preview is opened. |
entry_point: library, preview_command, or upload_result.resource_type: image, video, or raw.asset_type: the delivery type, for example upload, private, or authenticated. |
Upload
| Event | Reported when | Event-specific parameters |
|---|---|---|
upload_widget_opened |
The upload panel is opened. |
entry_point: command, folder_context for the folder context menu, or library_context for the library folder tree.folder_selected: true or false. Not sent for entry_point = 'command'. |
upload_started |
An upload begins. |
upload_source: file or url.folder_selected: true or false.preset_selected: true or false. |
upload_succeeded |
An upload returns successfully. |
upload_source, folder_selected, and preset_selected as above.resource_type: the resource type Cloudinary assigned.asset_type: the delivery type Cloudinary assigned. |
upload_failed |
An upload throws. |
upload_source, folder_selected, and preset_selected as above. |
upload_failed carries no error detail, because the underlying error message could contain a file path or a signed URL. To investigate failures, join on the Upload API calls themselves, which do record status codes and error classes. See Attributing API calls to the extension.Clipboard
| Event | Reported when | Event-specific parameters |
|---|---|---|
copy_asset_url |
An asset URL is copied. |
copy_type: secure_url, optimized_url, preview from the preview panel, or upload_result from the upload panel. |
copy_asset_public_id |
An asset public ID is copied. |
copy_type: public_id. |
Docs AI
These events originate in the webview client, src/webview/client/docs-ai/, and are relayed through the extension host.
| Event | Reported when | Event-specific parameters |
|---|---|---|
chat_started |
The first message of a new conversation is sent. |
conversation_id.language: the browser locale of the webview, for example en-US. |
message_sent |
Any message is sent. Fires alongside chat_started for the first message. |
conversation_id.language.message_length: the character count of the prompt. The prompt text is never sent.is_first_message: true or false. |
conversation_resumed |
A saved conversation is opened from the recent conversations list, chat history, or a deep link. Switching between open tabs doesn't report this event. |
conversation_id.age_since_created_hours: hours since the conversation was created, to two decimal places.age_since_last_message_hours: hours since the last message. |
conversation_rated |
The developer rates an answer. |
rating: up or down.star_rating: 1 to 5. Only sent with a down rating, and only when a star is chosen.has_comment: true or false. Only sent with a down rating.conversation_id. |
copy_message |
A copy button on a code block or a message is clicked. |
copy_type: code or message.code_length: sent for copy_type = 'code'.copied_role: user or assistant. Sent for copy_type = 'message'.copied_length: sent for copy_type = 'message'.conversation_id. |
conversation_rated event, so ratings can't be netted off against retractions.Attributing API calls to the extension
The extension doesn't report a separate analytics event for each Cloudinary API call. Instead, it identifies itself on the calls themselves, so API traffic is attributable using the standard API logs.
generateUserAgent() in src/utils/userAgent.ts builds the identifier:
For example, CloudinaryVSCode/1.0.1 (Cursor 1.99.3). The editor name is normalized to VSCode, Cursor, Windsurf, VSCodeInsiders, or VSCodeOSS, with a camel case fallback for anything unrecognized.
This identifier is applied in two places:
-
SDK calls. Activation and environment switching set
cloudinary.utils.userPlatform. The Node SDK adds that value in front of its own user agent, so the header becomesCloudinaryVSCode/1.0.1 (Cursor 1.99.3) CloudinaryNodeJS/<sdk version> (Node <node version>). -
Direct calls. The folder mode check in
src/config/detectFolderMode.tscallsfetchdirectly and sets theUser-Agentheader itself.
In Snowflake, the header is available as HTTP_REQUEST_HEADERS:user_agent. The key uses an underscore, not the hyphen used on the wire.
The endpoints the extension currently calls are FoldersApiController/show, FoldersApiController/index, ResourceSearchApiController/index, ResourcesApiController/customer_configuration, UploadPresetsApiController/index, and UploadController/upload.
Delivery URL analytics
Delivery URLs the extension builds with cloudinary.url() carry an _a query parameter that the Node SDK appends automatically. The SDK's urlAnalytics option defaults to true and the extension doesn't disable it, so the optimized URLs and signed original URLs shown in the library and preview panels all include it.
_a encodes the SDK identity only: the SDK code, the SDK version, the language runtime version, and the product. It doesn't carry the extension version or the userPlatform string, so it can't be used to attribute delivery traffic to the VS Code extension. Extension attribution relies on the user agent described above.
Two consequences worth knowing:
- A URL a developer copies out of the extension keeps its
_avalue, so once that URL is pasted into application code the delivery traffic is reported against the Node SDK rather than the extension. - Requests for these URLs are delivery traffic and are recorded in the CDN delivery logs, not in the API records that the rest of this page queries.
Where the data lands in Snowflake
The analytics endpoint is a Cloudinary API controller, so UI events flow through the standard usage data record (UDR) pipeline rather than a dedicated events table.
| Source | Contents | Use it for |
|---|---|---|
CLOUDINARY.LOGS.UDR_RAW |
One row per request, with the full query string in the HTTP_QUERY_PARAMS variant column. |
Anything that needs the event name or a payload value. This is the only source that retains them. |
CLOUDINARY.AGG.DAILY_UDR_PARAMS_FINAL |
Daily aggregate, filtered with FEATURE = 'vscode_extension'. |
Cheap long-range request volume only. |
UI events in UDR_RAW are identified by HTTP_QUERY_PARAMS:source = 'vscode_extension'. They resolve to ENDPOINT = 'AnalyticsController/collect', SERVICE_NAME = 'core', and, in the aggregate table, FEATURE = 'vscode_extension' with SOURCE = 'analytics'.
PARAM_KEYS records only which parameters were present, so event appears as a bare key with no value, and true or false parameters are folded into the key itself as folder_selected:true. PARAM_VALUES holds a small allowlisted subset of values, currently source, resource_type, language, and status. Any analysis that needs to distinguish one event from another must query LOGS.UDR_RAW.Known data quality caveats
-
Version 0.1.6 double reports. Version 0.1.6 sent a
GETrequest with noeventparameter alongside itsPOST. Filter onHTTP_REQUEST_METHOD = 'POST', or onHTTP_QUERY_PARAMS:event IS NOT NULL, or those rows inflate volume without being attributable to any event. Versions 1.0.0 and later send only thePOST. -
Test traffic. Events reported during development carry
extension_version = 'local_test', or adebug_idparameter. Exclude both from adoption figures. -
endpoint_checkisn't a product event. It appears in historical data from the endpoint's initial verification and isn't reported by any current code path. -
Client clocks.
event_timecomes from the developer's machine and can be wrong.UDR_RAW.TIMESTAMPis the server clock and is the safer choice for time bucketing. -
Sessions aren't users.
session_idis created once per installation and persists in global state, so it approximates an installation rather than either a user or an editor session. Cloudinary user IDs aren't reported with UI events, because the extension authenticates with API key and secret rather than as a Console user. -
Asset previews can report twice. Opening an asset from Library or an upload result first reports
asset_openedwith its source entry point, then reports the same event withentry_point = 'preview_command'when the shared preview command runs. -
Library folder uploads report twice. Opening the upload panel from the Library folder tree reports
upload_widget_openedwithentry_point = 'library_context', then reports it again withentry_point = 'folder_context'when the upload command runs.
Sample queries
These queries run against Snowflake directly and work unchanged in Redash.
Daily event volume by event name. The starting point for most questions:
Adoption by editor and version. Distinct installations and product environments per week:
View events by entry point. The command value covers both Command Palette actions and in-editor actions that invoke the same command. The webview_ready value identifies webview initialization:
Upload outcomes. Success rate split by upload source:
Configuration failure reasons. Useful when investigating setup friction:
API calls made by the extension. Attributed by user agent rather than by a reported event:
LOGS.UDR_RAW is a view over the full request log, so always include a TIMESTAMP filter. Queries that scan more than a few weeks without one are slow and expensive.Long-range request volume from the aggregate. Cheaper than UDR_RAW, but with no event breakdown:
Adding a new event
- Pick a name that matches
^[a-z][a-z0-9_]*$and follows the existingobject_verbpattern, for exampleasset_opened, notopenAsset. - Report it through the shared analytics service. In commands and providers, call
analytics?.track(). In the Docs AI webview, call the localtrackAnalytics()helper. - Keep the payload low cardinality and free of content. Report lengths, counts, flags, and enumerated values. Never report public IDs, URLs, file paths, search text, or prompts.
- Reuse existing parameter names.
entry_point,resource_type,asset_type, andcopy_typealready have established value sets, and reusing them keeps queries simple. - Add the event to the UI event catalog in the same pull request.
- Verify the event end to end by setting
CLOUDINARY_ANALYTICS_DEBUG_IDto a unique value before launching the extension, then queryingLOGS.UDR_RAWfor thatdebug_id.
Resources
- Cloudinary VS Code Extension for development and publishing
- Analytics service for the reporting implementation
- Configuration validation tracking for the configuration validation events
- User agent utility for the API call identifier