Extension Telemetry

September 25, 2026 · View on GitHub

This guide is for extension authors whose telemetry has passed privacy review and whose extension is published to the official azd registry. It explains how to ask azd to record a usage signal on the extension's behalf — for example, which deployment mode a user picked.

Important

Any telemetry must go through a privacy review to ensure that it respects user privacy before you start collecting it. See Your responsibility for content below.

Telemetry is a preview service azd offers to eligible extensions installed from the official azd registry. Import github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta for its request and response types. Call ReportUsage with an event name and the attributes you care about. azd core owns the identity fields, namespaces your attributes, and bounds their size and number. It does not inspect what they mean.

First-party extensions in this repository must declare every concrete ext.* field in extensions/telemetry/fields.go. A PR check verifies telemetry payload construction and checks every attribute against that inventory before the extension is released. This keeps classification failures in development and out of the user's command path.

Runtime recording is limited to eligible official-registry installations.

See ADR-001 for the reasoning behind this design.

Require a host with telemetry support

A published extension version that depends on this service should declare the first azd release that includes TelemetryService in its extension.yaml:

requiredAzdVersion: ">=1.33.0"

Use the first released azd version containing the service as the lower bound. azd uses this field while resolving extension installs and updates, so new installations do not select the extension on an older host. See Extension Resolution and Versioning for the compatibility behavior.

This version requirement is the normal compatibility mechanism. Keep the RPC call best-effort for already-installed versions and non-registry sources, which may still run on an older host and receive Unimplemented.

What azd records

Attribute nameAttribute value
extension.idID of the current extension, added automatically
extension.versionVersion of the current extension, added automatically
extension.sourceSource of the current extension, added automatically
extension.eventEvent name chosen by the extension
ext.*Extension-chosen keys and values; first-party fields are declared and classified in extensions/telemetry/fields.go

The extension chooses the event name and every ext.* key and value. azd enforces the ext. prefix, the bounds below, and the eligibility requirement at runtime. The repository source test separately enforces the first-party field inventory during development.

azd cannot write your attribute keys unprefixed, and you cannot overwrite the identity fields — a key of extension.id is recorded as ext.extension.id.

Reporting an event

Microsoft Foundry extensions should use the shared reporter in pkg/foundry/telemetry instead of calling the generated gRPC client directly:

import "github.com/azure/azure-dev/cli/azd/pkg/foundry/telemetry"

reporter := telemetry.NewReporter(client.Telemetry(), nil)
reporter.Report(ctx, telemetry.Event{
  Name: "deploy.completed",
  Attributes: map[string]string{
    "deploy.mode": "container",
    "retries":     "2",
  },
})

Report is best effort and has no return value. It applies a one-second timeout by default, does not retry, treats Accepted: false as a normal result, and writes only the event name and gRPC status code to the debug log when reporting fails. It never logs attribute values or raw transport error details. Each Foundry extension should keep its approved event builders and bounded value types in the extension that owns those product semantics, and declare each attribute in the shared field inventory.

Other extension families can call the generated client directly until they have an appropriate shared package:

if _, err := client.Telemetry().ReportUsage(
    ctx,
    &v1beta.ReportUsageRequest{
        EventName: "deploy.completed",
        Attributes: map[string]string{
            "deploy.mode": "container",
            "retries":     "2",
        },
    },
); err != nil {
    log.Printf("telemetry unavailable: %v", err)
}

Treat the call as best-effort. A malformed request is a plain error, and an older host can return Unimplemented when the extension was already installed or came from a non-registry source. Log the error, but never let it change command behavior or retry. Report the event immediately after the fact it represents is known, rather than waiting until the command completes, so a later unrelated failure does not lose the signal.

An event with no attributes is valid — "this happened" is a legitimate signal.

There is a complete working example in the demo extension: extensions/microsoft.azd.demo/internal/cmd/telemetry.go, runnable with azd demo telemetry.

Declare and validate attributes

Every first-party attribute must have one exported, package-level fields.AttributeKey declaration in extensions/telemetry/fields.go. The declaration uses the final property name recorded by the host:

var DeployMode = fields.AttributeKey{
	Key:            attribute.Key("ext.deploy.mode"),
	Classification: fields.SystemMetadata,
	Purpose:        fields.FeatureInsight,
	Endpoint:       "N/A",
}

Declarations form a shared first-party field schema and are not exclusive to the extension that introduced them. Another first-party extension may reuse an existing key when its meaning, allowed values, classification, and purpose are identical. If any of those differ, declare a distinct key.

The extension still sends the suffix:

Attributes: map[string]string{
	"deploy.mode": "container",
}

Choose classification, purpose, and endpoint from what the property actually contains and why it is collected. Do not copy SystemMetadata or FeatureInsight merely because another extension field uses them. SystemMetadata must use N/A; every other classification must use a non-N/A endpoint.

Set Attributes only in the telemetry payload literal, using nil or an inline map[string]string literal. Reading or assigning Attributes or GetAttributes through a selector anywhere in extension code is rejected, so aliases and helper mutations cannot quietly hide fields; the inline-literal key scan remains the guarantee that every key is declared. Because the check is syntactic rather than type-aware, an unrelated struct field named Attributes is rejected too and must be renamed. Payload literals must use keyed fields. Attribute keys must be string literals or same-package compile-time string constants so repository validation can resolve them. Generic container literals, type-elided literals that carry an Attributes entry, and telemetry payload type names — aliases or defined types, including cross-package re-exports — are not supported in packages that define extension telemetry; a payload type name is rejected at its declaration. Use a concrete keyed telemetry payload literal instead. Conversions from another struct to a telemetry payload type are also rejected because their attribute keys are not visible in the concrete payload literal. Pass the payload to ReportUsage as an inline literal built at the call, for example telemetry.ReportUsage(ctx, &azdext.ReportUsageRequest{Attributes: ...}). A payload reaching the sink as a variable, parameter, or decoded value is rejected, because the host emits every entry in its Attributes map and only an inline literal has its keys scanned. Run the validation from cli/azd:

go test ./extensions/telemetry

The test scans production Go source under extensions/, verifies every final ext.* key has one valid declaration, and fails with the extension, file, and line for an undeclared key. It analyzes repository source only and does not run extensions. The extension CI workflow runs the same test.

The inventory is not a runtime allowlist. Metadata changes do not require a new azd release, while released extensions continue to use the existing best-effort ReportUsage behavior.

Bounds

RuleLimit
Attributes per event32
Event name length1–128 UTF-8 bytes
Attribute key length1–128 UTF-8 bytes
Attribute value length512 UTF-8 bytes
Recorded events per azd invocation100

There are no charset rules. Exceeding a per-event bound rejects the whole call and records nothing, so a partially-valid event never lands as a complete-looking one. The per-invocation budget behaves differently: see When your event is not recorded.

Your responsibility for content

azd does not inspect your values at runtime. Privacy review and the content rules below remain your responsibility as the extension author:

  • Never send customer content. No file paths, resource names, connection strings, prompts, URLs, or anything a user typed. If you are unsure whether a value qualifies as customer content, always assume that it does to be safe.
  • Keep values low cardinality. Prefer a small enum such as code | container | unknown. Unbounded values make the data expensive and unusable for aggregation.
  • Document your events the way azd core documents its own fields: what each event and attribute means and why you need it.
  • Declare every attribute in extensions/telemetry/fields.go with the classification, purpose, and endpoint that match its actual semantics.
  • Get a privacy review as part of reviewing your extension, following the telemetry privacy review checklist.

When the call returns an error

StatusCause
UnauthenticatedThe request did not carry the host-issued extension token
PermissionDeniedThe calling extension is not installed
InvalidArgumentThe event name is missing, or a per-event bound was exceeded
UnimplementedThe azd host predates this service

Error messages do not echo attribute values. When a valid key has an oversized value, the key is included so you can identify the failing field; invalid keys are not echoed. Use the status code plus your own call site to diagnose.

When your event is not recorded

Two outcomes are deliberately not errors. The call succeeds and ReportUsageResponse.Accepted is false:

CauseWhy
The extension is not eligible for telemetry recordingRecording is limited to reviewed official-registry installations
The per-invocation event budget is spentReportUsage can be called in a loop, and the per-event bounds do not limit how many events arrive

Run azd with --debug to see which one applied.

This means your events are not recorded while you develop locally, because an extension installed with --source dev or from a file path does not pass the gate. You can still verify your integration end to end: the call succeeds and Accepted comes back false. Because it is not an error, your code runs the same path in development as in production — do not branch on Accepted.

Where the data lands

Each accepted event becomes an ext.usage span carrying extension.id, extension.version, extension.source, extension.event, and one ext.<key> attribute per entry in your map. The span shares the command's trace, so it joins to the originating command on operation_Id in Application Insights:

requests
| where name == "ext.usage"
| where customDimensions["extension.id"] == "contoso.tools"
| where customDimensions["extension.event"] == "deploy.completed"
| summarize count() by tostring(customDimensions["ext.deploy.mode"])