microsoft/telemetry-best-practices
Reviews and authors telemetry code in this extension. Use when adding, modifying, or reviewing any `callWithTelemetryAndErrorHandling` call, any `context.telemetry.properties`/`measurements` assignment, or any helper that reports stats to telemetry. Ensures no PII/EUII is emitted, that property vs. measurement usage is correct, and that event/property names are consistent.
npx skills add https://github.com/microsoft/vscode-cosmosdb --skill telemetry-best-practices
This extension uses @microsoft/vscode-azext-utils (callWithTelemetryAndErrorHandling, IActionContext.telemetry) to emit telemetry events. Telemetry is public — assume every property and measurement value is shipped to a backend and visible to whoever has access to the dashboards. Treat every new property as a privacy decision.
These rules apply to the entire codebase (extension host, webviews, language services, helper packages), not to any single feature area.
Apply it whenever code:
callWithTelemetryAndErrorHandling(...), callWithTelemetryAndErrorHandlingSync(...), or registers a command (which wraps the same).context.telemetry.properties.* or context.telemetry.measurements.*.report*Stats, track*, *Telemetry) that mutates an IActionContext.callWithTelemetryAndErrorHandling).The following must never appear in telemetry.properties or telemetry.measurements, and never as part of an event name:
Not PII (always allowed): the AI model identifiers modelId, modelFamily, modelVendor (vendor-published values from vscode.LanguageModelChat), bounded enums you control, durations, counts, and ratios.
A small set of Azure identifiers are OII (Organization Identifiable Information), not PII, and may be emitted as-is — but only under the predefined property names listed below. The telemetry pipeline (and @microsoft/vscode-azext-utils) recognizes these names and handles them accordingly (subscription scoping, sanitization of the resource path, organization-tier classification, etc.).
| Key | Type | Notes |
| --- | --- | --- |
| subscriptionId | string | Azure subscription GUID. |
| tenantId | string | Azure / Entra tenant GUID. |
| resourceId | vscode.TelemetryTrustedValue | Full ARM resource id. Must be wrapped so VS Code's telemetry layer does not re-sanitize the path. |
| accountName | string | Azure resource (account) name, e.g. the Cosmos DB account name. |
context.telemetry.properties.subscriptionId = subscriptionId;
context.telemetry.properties.tenantId = tenantId;
context.telemetry.properties.accountName = account.name;
context.telemetry.properties.resourceId = new vscode.TelemetryTrustedValue(resourceId.rawId);
Rules:
subId, accountId, armId, cosmosAccount, …) bypasses the special handling and counts as PII.resourceId must be wrapped in new vscode.TelemetryTrustedValue(...).resourceGroup, databaseName, containerName, …) under separate properties — only the four keys above are allowed; everything else is PII.context.valuesToMask so they are redacted from any error messages emitted alongside the event.crypto.randomUUID() per session/operation.'mongo' | 'postgres' | 'sqlserver'), never a free-form string.hasCustomInstructions) instead of the actual value.| Pattern | Action |
| --- | --- |
| properties.fileName, properties.path, properties.basename, properties.fileBaseName | Remove. |
| properties.error = err.message (raw) | Remove or replace with a category enum + sanitized code. |
| properties.query, properties.sql, properties.ddl, properties.prompt, properties.response | Remove. |
| properties.<anything> = someUserInput | Remove unless it is a validated bounded enum. |
| OII value (subscriptionId, tenantId, resourceId, accountName) emitted under a different key | Rename to the predefined key, or remove. |
| resourceId not wrapped in vscode.TelemetryTrustedValue | Wrap it. |
| Event name containing a user value (e.g. cosmosDB.${dbName}.start ) | Replace with a static event name; move the value to a bounded enum property only if safe. |
context.valuesToMask — Defense in Depth, Not a SubstituteIActionContext.valuesToMask is a list of strings that the telemetry pipeline replaces with --- in any error message that would otherwise be reported (stack traces, error.message, the GitHub issue body produced by reportIssue). It does not redact values from telemetry.properties / telemetry.measurements you set yourself — those are sent verbatim.
Use it as a safety net for sensitive values that your code touches and might end up in a thrown error or log line you don't fully control:
// Extension host: push directly to the action context
context.valuesToMask.push(connectionString);
context.valuesToMask.push(masterKey, endpoint, databaseId, containerId);
context.valuesToMask.push(account.subscription.subscriptionId);
context.valuesToMask.push(userProvidedName); // database/container/resource names entered in a wizard
// Webview-bridged events: register on the per-webview TelemetryContext instead
telemetryContext.addMaskedValue(connectionString);
telemetryContext.addMaskedValue([endpoint, databaseId, containerId]);
valuesToMask as soon as you obtain it if it could end up in an error path. This includes:subscriptionId, tenantId, resourceId, accountName) — even though they are emitted as-is under their predefined keys, they should still be masked from error messages.properties.connectionString = cs and rely on masking — only error-path strings are masked.Telemetry.ts filter already drops falsy values; do not bypass it)./), push all forms: context.valuesToMask.push(partitionKey, partitionKey.slice(1));prompt/validateInput steps that capture user input should push the captured value before the step returns. See CosmosDBContainerNameStep, CosmosDBConnectionStringStep, CosmosDBPartitionKeyStep for the pattern.CosmosDBBranchDataProvider, AccountInfo).TelemetryContext via addMaskedValue(value) (see src/Telemetry.ts); the mask list is then applied automatically to every reportWebviewEvent / reportWebviewError call from that webview.telemetry.properties are strings, telemetry.measurements are numbers. Use them correctly:
properties. Stringify booleans (String(value)), keep enums short and bounded.measurements. Never put a number in properties just because it is convenient.NaN or Infinity — guard with a check before assignment.null/undefined. Skip the assignment if the value is missing.cosmosDB.<area>[.<subarea>].<action> (two to four camelCase segments). Keep them static — no interpolated user data. Examples currently in use: cosmosDB.nosql.queryEditor.executeQuery, cosmosDB.migration.ddlExtractor.extract. Match an existing prefix if you are adding to an existing area instead of inventing a new top-level name.camelCase, stable across versions. Renaming a key breaks dashboards and queries; prefer adding a new key over renaming.sessionId, durationMs, errorCategory).await callWithTelemetryAndErrorHandling('cosmosDB.<area>.<action>', async (ctx) => {
ctx.errorHandling.suppressDisplay = true;
ctx.errorHandling.rethrow = false;
// ... record measurements/properties (sync or async) ...
});
The callback may be sync or async; keep it async whenever the work inside is async. Use this for fire-and-forget instrumentation that must never affect the user-visible flow.
IActionContextWhen per-call events would be too chatty, accumulate counters on a longer-lived context's measurements and emit a single summary event at the end. Pass that context as an extra parameter (commonly named phaseContext / rollupContext) to the reporting helper.
When the same set of properties (session id, mode, source type, etc.) appears at many call sites, put it in a single helper (enrichWithMigrationContext, enrichWithQueryEditorContext, …; <Area> is a placeholder for the actual feature name) and call that at the top of every event. Extend the helper instead of duplicating assignments.
properties.errorCategory to a bounded enum. Reuse values already used in the codebase ('ai', 'infrastructure' in migrationTelemetry.ts) before introducing new ones.error.message into properties — it will likely contain user values.ctx.errorHandling.issueProperties is the data appended to the body of the GitHub issue created when the user clicks Report an Issue. It is not a sanctioned PII channel — the same PII rules apply. Use it for diagnostic context (model id, error category, sanitized codes) that helps maintainers debug a reported issue.When reviewing a diff that touches telemetry, confirm each item:
subscriptionId, tenantId, accountName, and resourceId under those exact key names; resourceId wrapped in vscode.TelemetryTrustedValue.)measurements, strings/booleans/enums go to properties.String(value)).NaN/Infinity reach the assignment.camelCase names where the meaning matches an existing key.suppressDisplay = true and rethrow = false.context.valuesToMask as soon as it is obtained.report* / track* helper explicitly states "no file contents, paths, or names are emitted".Take microsoft/telemetry-best-practices from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.