microsoft/telemetry-instrumentation
Instrumentation patterns for telemetry in the vscode-documentdb extension. Use when adding telemetry to a feature, enriching existing telemetry with properties or measurements, linking related events with correlation IDs, instrumenting commands or tree views, suppressing noisy events, or deciding what data points to capture. Also use when asked to "add telemetry" or "improve telemetry" for any feature.
npx skills add https://github.com/microsoft/vscode-documentdb --skill telemetry-instrumentation
How to instrument code in vscode-documentdb to produce actionable telemetry.
The @microsoft/vscode-azext-utils library automatically captures these — never add them manually:
| Auto-captured | Details |
| ----------------------- | ------------------------------------------------------------------ |
| Duration | measurements.duration — wall-clock ms from callback start to end |
| Result | properties.result — "Succeeded", "Failed", or "Canceled" |
| Error info | properties.error (name), properties.errorMessage (message) |
| Cancel detection | UserCancelledError sets result to "Canceled" automatically |
| Machine/session IDs | VSCodeMachineId, VSCodeSessionId — always present |
| Extension metadata | Extension name, version, VS Code version |
Do not re-measure duration, set result, or catch errors just for telemetry — the wrapper handles it.
callWithTelemetryAndErrorHandling(eventId, callback)The primary wrapper. Creates a telemetry event, runs the callback, reports result + duration + errors. The callback's return value is passed through, so you can wrap any function call directly:
import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
// Simple — wrap logic, telemetry is automatic
await callWithTelemetryAndErrorHandling('myFeature.doSomething', async (context) => {
context.telemetry.properties.someProperty = 'value';
context.telemetry.measurements.itemCount = items.length;
// ... your logic — errors are caught and reported automatically
});
// With return value — no need for intermediate variables
const result = await callWithTelemetryAndErrorHandling('myFeature.query', async (context) => {
context.telemetry.properties.queryType = 'find';
return doWork(); // return value passes through to the caller
});
Event name convention: use dot-separated hierarchy: connect, connect.getmetadata, connect.promptForCredentials.
Every registered command gets a telemetry event automatically. Prefer the correlation-wrapped variants:
import { registerCommandWithTreeNodeUnwrapping } from '@microsoft/vscode-azext-utils';
import { withTreeNodeCommandCorrelation, withCommandCorrelation } from '../../utils/commandTelemetry';
// Tree node commands — unwraps tree item + tracks journeyCorrelationId
registerCommandWithTreeNodeUnwrapping(
'vscode-documentdb.command.connectionsView.myCommand',
withTreeNodeCommandCorrelation(myCommandHandler),
);
// Plain commands — tracks journeyCorrelationId
registerCommand('vscode-documentdb.command.myCommand', withCommandCorrelation(myCommandHandler));
For critical commands where errors need modal dialogs:
import { registerCommandWithTreeNodeUnwrappingAndModalErrors } from '../../utils/commandErrorHandling';
Webview tRPC calls get telemetry automatically via publicProcedureWithTelemetry. Each call emits documentDB.rpc.{type}.{path} with result, error, abort tracking. The DocumentDB TelemetryRunner contributes the full IActionContext to ctx.actionContext (narrow ctx to WithTelemetry<RouterContext> to read it):
myProcedure: publicProcedureWithTelemetry.input(schema).query(async ({ ctx, input }) => {
const myCtx = ctx as WithTelemetry<RouterContext>;
myCtx.actionContext.telemetry.properties.someDetail = 'value';
myCtx.actionContext.telemetry.measurements.resultCount = results.length;
return results;
}),
authMethod, connectionMode, discoveryProviderIddomainInfo_isAzure, serverInfo_version. The prefix acts as a namespace. This pattern exists in getClusterMetadata.ts and is matched by dashboard queries.task_initializationCompleted (mixed snake + camelCase) is wrong — use task_initializationCompleted → taskInitializationCompleted or task_initialization_completed consistently.journeyCorrelationId, connectionCorrelationId.Always use the ternary pattern — it makes the intent explicit:
// ✅ Correct — clear intent, consistent
context.telemetry.properties.hasFolders = totalFolders > 0 ? 'true' : 'false';
context.telemetry.properties.isFirstConnection = isFirst ? 'true' : 'false';
// ❌ Avoid — works but less readable, can accidentally stringify non-booleans
context.telemetry.properties.isWindows = isWindows.toString();
Rule: if you'd ever want to compute P50, average, sum, or max over it, use a measurement. Numbers stored as string properties can't be aggregated in analytics without casting.
// ✅ Measurement — enables P50(pageNumber), avg(itemCount)
context.telemetry.measurements.pageNumber = input.pageNumber;
context.telemetry.measurements.itemCount = items.length;
// ❌ Wrong — forces dashboard to todouble(Properties.pageNumber)
context.telemetry.properties.pageNumber = input.pageNumber.toString();
Exception: numeric values used purely as categories (e.g., an error code used for grouping, not aggregation) can be properties.
| Use | Type | Example |
| ------------------------- | ------------------------------------ | ------------------------------------------------ |
| Properties (string) | Categorical data, flags, identifiers | serverType, view, authMethod, hasFolders |
| Measurements (number) | Counts, sizes, durations, rates | itemCount, loadTimeMs, retryAttempts |
// Feature context — what variant/mode is being used
context.telemetry.properties.view = 'connectionsView';
context.telemetry.properties.experience = node.experience.api; // 'vCore' | 'RU'
context.telemetry.properties.authMethod = 'connectionString';
// Boolean flags — always use 'true'/'false' strings
context.telemetry.properties.hasFolders = totalFolders > 0 ? 'true' : 'false';
context.telemetry.properties.isFirstConnection = isFirst ? 'true' : 'false';
// Classification of inputs or results
context.telemetry.properties.connectionMode = 'documentdb'; // what was selected
context.telemetry.properties.terminalType = 'PowerShell'; // what environment
// Error classification beyond the auto-captured error
context.telemetry.properties.connectionErrorType = 'auth'; // error category
context.telemetry.properties.parseError = 'SyntaxError'; // specific error type
// View identification — set for user-initiated or view-specific operations
// Skip for background tasks, activation events, or cross-cutting concerns
context.telemetry.properties.view = Views.ConnectionsView;
// Counts — how many items were involved
context.telemetry.measurements.itemCount = results.length;
context.telemetry.measurements.totalConnections = connections.length;
context.telemetry.measurements.savedConnections = rootItems.length;
// Requested vs returned — reveals pagination, limits, empty results
context.telemetry.measurements.pageSize = input.pageSize;
context.telemetry.measurements.documentCount = docs.length;
// Sub-durations — where time is spent (the total is auto-captured)
context.telemetry.measurements.loadTimeMs = Date.now() - startTime;
context.telemetry.measurements.mainFileLoad = (loadEnd - loadStart) / 1000;
// Attempts/retries
context.telemetry.measurements.connectionViewActivationAttempts = attempt + 1;
context.telemetry.measurements.cleanupIterations = iteration;
// Breakdown counts — for multi-category results
context.telemetry.measurements.createRecommendationCount = createCount;
context.telemetry.measurements.dropRecommendationCount = dropCount;
// Structural complexity — depth, nesting
context.telemetry.measurements.maxFolderDepth = maxDepth;
When the set of categories is data-driven, use a prefix:
context.telemetry.measurements[`${zonePrefix}_Connections`] = connectionsInZone;
context.telemetry.measurements[`${zonePrefix}_Folders`] = foldersInZone;
Use nested callWithTelemetryAndErrorHandling inside a parent event to get fine-grained timing and success/failure per step:
await callWithTelemetryAndErrorHandling('connect', async (context) => {
context.telemetry.properties.view = 'discoveryView';
// Sub-step: prompting — gets its own duration, result, errors
await callWithTelemetryAndErrorHandling('connect.promptForCredentials', async (subCtx) => {
subCtx.errorHandling.rethrow = true; // let parent see failures
await wizard.prompt();
});
// Sub-step: metadata collection (fire-and-forget, non-blocking)
void callWithTelemetryAndErrorHandling('connect.getmetadata', async (metaCtx) => {
const metadata = await collectMetadata();
metaCtx.telemetry.properties = { ...metaCtx.telemetry.properties, ...metadata };
});
});
When to use sub-steps:
Naming: use parent event as prefix: connect.promptForCredentials, connect.getmetadata, connect.staticmetadata.
Links a chain of user actions that form a logical journey (e.g., discover → select → connect → browse). Generated once at the start and propagated through tree items:
// Generate at the journey start (e.g., when a discovery tree root is created)
this.journeyCorrelationId = randomUUID();
// Tree items carry it to children
new MongoRUResourceItem(this, account, this.journeyCorrelationId);
// Commands pick it up automatically via withTreeNodeCommandCorrelation/withCommandCorrelation
// OR manually in a callWithTelemetryAndErrorHandling:
if (this.journeyCorrelationId) {
context.telemetry.properties.journeyCorrelationId = this.journeyCorrelationId;
}
Use journeyCorrelationId when: a user flow spans multiple command invocations that should be analyzed together (discovery → connect → browse, folder operations in sequence).
Links the connect, connect.staticmetadata, and connect.getmetadata events for a single connection attempt:
this.connectionCorrelationId = randomUUID();
void callWithTelemetryAndErrorHandling('connect.staticmetadata', async (context) => {
context.telemetry.properties.connectionCorrelationId = this.connectionCorrelationId;
// ...domain metadata
});
void callWithTelemetryAndErrorHandling('connect.getmetadata', async (context) => {
context.telemetry.properties.connectionCorrelationId = this.connectionCorrelationId;
// ...server metadata
});
Use connectionCorrelationId when: an operation emits multiple independent events that must be joined for analysis (especially fire-and-forget sub-events).
For features where a user session produces many events (e.g., shell, REPL, interactive query editing), generate a session-scoped correlation ID and attach it to every event in that session. This enables aggregation (total commands per session, session duration as max-min timestamps):
const sessionId = randomUUID();
// Each action within the session carries the same ID
context.telemetry.properties.shellSessionId = sessionId;
context.telemetry.measurements.commandIndex = commandCount++;
// High-frequency events (keystroke handlers, document change listeners)
context.telemetry.suppressIfSuccessful = true; // only emit on error/cancel
// Fully silent (internal bookkeeping)
context.telemetry.suppressAll = true;
// Don't show error notification to user (background/internal operations)
context.errorHandling.suppressDisplay = true;
// Re-throw so the parent caller sees the error
context.errorHandling.rethrow = true;
// Don't offer "Report Issue" button
context.errorHandling.suppressReportIssue = true;
Mark startup/initialization telemetry so it can be filtered out of active-usage analysis:
context.telemetry.properties.isActivationEvent = 'true';
The framework auto-captures measurements.duration for every callWithTelemetryAndErrorHandling event. Never add Date.now() tracking for the same purpose:
// ❌ Wrong — redundant manual timing
const startTime = Date.now();
await callWithTelemetryAndErrorHandling('myEvent', async (context) => {
await doWork();
context.telemetry.measurements.durationMs = Date.now() - startTime; // duplicates framework
});
// ✅ Correct — framework captures duration automatically
await callWithTelemetryAndErrorHandling('myEvent', async (context) => {
await doWork();
// duration is auto-captured — no manual timing needed
});
Exception: sub-durations within a single event (e.g., "time spent on init vs. time on eval") are valid because they measure a _phase_, not the total.
Wrap the operation in a single callWithTelemetryAndErrorHandling. The framework sets result=Succeeded or result=Failed automatically:
// ❌ Wrong — two fire-and-forget blocks, error rethrow inside void is swallowed
try {
const result = await doWork();
void callWithTelemetryAndErrorHandling('myEvent', async (ctx) => {
ctx.telemetry.properties.isError = 'false';
});
} catch (error) {
void callWithTelemetryAndErrorHandling('myEvent', async (ctx) => {
ctx.telemetry.properties.isError = 'true';
throw error; // ⚠️ swallowed — void discards the promise
});
}
// ✅ Correct — single wrapper, framework handles success/failure
await callWithTelemetryAndErrorHandling('myEvent', async (context) => {
context.errorHandling.suppressDisplay = true;
context.errorHandling.rethrow = true; // let caller handle display
context.telemetry.properties.someProperty = 'value';
const result = await doWork(); // errors auto-set result=Failed
context.telemetry.properties.resultType = result.type;
});
suppressIfSuccessful on events you want to measuresuppressIfSuccessful = true means the event is only emitted on error. Never use it on events where you need to count successful occurrences:
// ❌ Wrong — successful completions are silently dropped
registerCommand('completionAccepted', (context) => {
context.telemetry.properties.category = category;
context.telemetry.suppressIfSuccessful = true; // BUG: won't emit on success
});
// ✅ Correct — every acceptance is tracked
registerCommand('completionAccepted', (context) => {
context.telemetry.properties.category = category;
// no suppress — we want to count every acceptance
});
When suppressIfSuccessful IS correct: high-frequency background operations where you only care about failures (e.g., keystroke handlers, document change listeners, periodic health checks).
void callWithTelemetryAndErrorHandlingWhen using fire-and-forget (void) telemetry, thrown errors are swallowed because nobody awaits the promise. The framework captures the error in telemetry properties but the throw has no effect on the calling code:
// ❌ Wrong — throw inside void is swallowed, no effect on caller
void callWithTelemetryAndErrorHandling('myEvent', async (context) => {
throw error; // captured in telemetry but NOT re-thrown to caller
});
// code continues here regardless
// ✅ Correct for fire-and-forget — just set properties, don't throw
void callWithTelemetryAndErrorHandling('myEvent', async (context) => {
context.telemetry.properties.shellSessionId = sessionId;
// fire-and-forget: no throw, no await needed
});
// ✅ Correct when you need error propagation — use await
await callWithTelemetryAndErrorHandling('myEvent', async (context) => {
context.errorHandling.rethrow = true;
await riskyOperation(); // error propagates to caller via rethrow
});
Never let connection strings, passwords, tokens, or hostnames appear in telemetry.
import { maskSensitiveValuesInTelemetry } from '../../documentdb/utils/connectionStringHelpers';
// Mask all sensitive parts of a parsed connection string
maskSensitiveValuesInTelemetry(context, parsedConnectionString);
// Or mask individual values
context.valuesToMask.push(password, token);
Tree getChildren calls emit telemetry. Always set context so analytics can distinguish navigation levels:
return callWithTelemetryAndErrorHandling('getChildren', async (context) => {
context.telemetry.properties.parentNodeContext = element ? (await element.getTreeItem()).contextValue : 'root';
context.telemetry.measurements.childrenCount = children.length;
return children;
});
Note: Root-level getChildren fires automatically when panels become visible (passive rendering). Analytics filters these with parentNodeContext != "root" — always set this property.
When instrumenting a feature, consider these categories:
lastStep property)reconnected = 'true')result ManuallyThe wrapper auto-sets result to "Succeeded" / "Failed" / "Canceled". Manual override is required in two cases:
if (!isValidFormat) {
context.telemetry.properties.result = 'Failed';
context.telemetry.properties.errorReason = 'invalidNodeIdFormat';
return; // no throw → wrapper would report "Succeeded" without the override
}
{ ok: false } instead of throwing:if (!result.ok) {
context.telemetry.properties.result = 'Failed';
context.telemetry.properties.error = result.error.name;
}
Do NOT override result when you also throw — the wrapper handles that automatically.
callWithTelemetryAndErrorHandling does this alreadyresult when throwing — the wrapper sets "Succeeded" / "Failed" / "Canceled" automatically from the throw. Only override result on early-return failures or non-throwing error paths (see above)maskSensitiveValuesInTelemetry or context.valuesToMaskcallWithTelemetryAndErrorHandling for every minor step — only when you need separate duration/result tracking for a stepcontext.errorHandling.rethrow = true in sub-steps that should propagate failures to the parentsuppressIfSuccessful on events you need to count — it drops successful events entirelymeasurements for anything you'd want to aggregate (P50, sum, avg). Use .toString() only for numbers used as categorical grouping keys.toString() for booleans — use the explicit ternary: value ? 'true' : 'false'Take microsoft/telemetry-instrumentation 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.