# Telemetry & Analytics

Measure Droid adoption, activity, and cost by exporting OpenTelemetry metrics to your own collector or reading Factory's hosted Analytics API.

Enterprise adoption requires more than a good developer experience. You need to understand **who is using Droid, on what, and at what cost**. There are two complementary ways to measure that, and you can use either or both.

This page covers setting up the self-hosted export. The rest of the story lives on its own pages:

<CardGroup cols={2}>
  <Card title="Self-host via OTEL export" icon="chart-line" href="#self-hosted-otel-metrics-export">
    Export Droid's OpenTelemetry **metrics** to your own OTLP-compatible collector. Configuration, examples, and troubleshooting, on this page.
  </Card>
  <Card title="Exported data reference" icon="database" href="/enterprise/telemetry/data-reference">
    Every metric, span, and attribute Droid exports, in both the `droid.*` and GenAI semantic convention formats.
  </Card>
  <Card title="Telemetry privacy" icon="shield-check" href="/enterprise/telemetry/privacy">
    Data granularity, message content logging, and what reaches Factory versus your collector.
  </Card>
  <Card title="Factory-hosted Analytics API" icon="cloud" href="/api-reference/analytics">
    Read aggregated usage, cost, and productivity data from Factory's cloud via the [Analytics API](/api-reference/analytics).
  </Card>
</CardGroup>

Customer-side export is **metrics only by default**; message content can optionally be exported as trace spans to your own endpoint (see [Message content logging](/enterprise/telemetry/privacy#message-content-logging)). Factory's internal tracing is what powers the hosted Analytics API; it is never sent to your collectors.

<LabeledDivider label='Setup' />

## Self-hosted OTEL metrics export

Droid can export OpenTelemetry (OTEL) metrics to your own OTLP-compatible collector, giving you visibility into Droid activity within your existing observability stack. In connected deployments, metrics are sent to both Factory's collector and yours in the same export cycle. If your collector is unreachable, Factory's own export is not affected.

**Spans are yours alone.** Factory's collector never receives a span, in any configuration. Droid builds a trace pipeline only where you have configured a collector of your own that is distinct from Factory's, so with no endpoint set no span is recorded anywhere.

## Configuration

There are two places the sink can be configured, and they take precedence in this order:

1. **Org-managed settings.** A `telemetry` block in org-managed settings pins the sink for every member of the organization. See [Org-managed telemetry settings](#org-managed-telemetry-settings).
2. **Environment variables** on the developer's machine, used for any value the organization has not pinned. See [Machine environment variables](#machine-environment-variables).

## Org-managed telemetry settings

Environment variables live on the developer's machine, which makes them advisory: anyone can `export OTEL_TELEMETRY_ENDPOINT=...` to redirect the stream or switch it off. Organizations that need the sink enforced can pin it centrally with a `telemetry` block in org-managed settings, edited under **Settings → Enterprise Controls → Raw Configuration**:

```json
{
  "telemetry": {
    "enabled": true,
    "endpoint": "https://collector.example.com:4318",
    "headers": { "Authorization": "Bearer ${OTEL_COLLECTOR_TOKEN}" },
    "metricExportIntervalMs": 60000,
    "logMessageContent": false,
    "granularity": "user",
    "format": "legacy"
  }
}
```

<PropertyList>
  <Property name='enabled' type='boolean'>
    Master switch for the customer telemetry pipeline. `true` keeps it on even
    if the machine sets `OTEL_CUSTOMER_ENABLED=false`; `false` keeps it off even
    if the machine sets `OTEL_CUSTOMER_ENABLED=true`. Omit it to fall back to
    `OTEL_CUSTOMER_ENABLED`. In airgapped deployments the pipeline runs only
    when you have configured a collector of your own; see
    [Airgapped deployments](#airgapped-deployments).
  </Property>
  <Property name='endpoint' type='string'>
    Base URL of your OTLP HTTP collector, taking precedence over
    `OTEL_TELEMETRY_ENDPOINT`. Must be an `http`/`https` URL with no query
    string or fragment, or a `${VAR_NAME}` reference standing in for the whole
    URL. Invalid values are rejected when you save, not on member machines.
    Because it overrides `OTEL_TELEMETRY_ENDPOINT` on every machine, use a
    `${VAR_NAME}` reference if machines legitimately need to point at different
    collectors.
  </Property>
  <Property name='headers' type='object'>
    Static OTLP headers sent on every export, as a map of header name to value.
    Values may contain `${VAR_NAME}` references. Headers are transport-only —
    used to authenticate and route the HTTP export — and are never attached to
    exported spans or metrics as attributes.
  </Property>
  <Property name='metricExportIntervalMs' type='integer'>
    Metric flush interval in milliseconds, taking precedence over
    `OTEL_METRIC_EXPORT_INTERVAL`. Capped at `2147483647`.
  </Property>
  <Property name='logMessageContent' type='boolean'>
    Whether message content is exported. See
    [Message content logging](/enterprise/telemetry/privacy#message-content-logging).
  </Property>
  <Property name='granularity' type='"user" | "aggregate"'>
    Whether exported data carries per-individual identity. Defaults to `user`.
    Set `aggregate` where a works council agreement or a jurisdictional rule
    forbids per-individual analytics. See
    [Data granularity](/enterprise/telemetry/privacy#data-granularity).
  </Property>
  <Property name='format' type='"legacy" | "genai"'>
    Which convention your collector receives. Defaults to `legacy`. See
    [Export formats](/enterprise/telemetry/data-reference#export-formats).
    Factory's own collector is unaffected by this field.
  </Property>
</PropertyList>

Three properties of the block are worth calling out:

- **It is read from the organization level only.** A `telemetry` block in project, folder, or user settings is ignored with a warning, so a checked-in repository config or a developer's own `settings.json` cannot redirect the stream.
- **It is distributed to every member**, not just Managers and Owners, because the CLI reads it on each member's machine. Treat every value in it as readable by the whole organization.
- **It configures your sink only.** Factory's own collector is not affected by it.

<Warning>
  Prefer a `${VAR_NAME}` reference over a literal collector credential in `headers`, so the secret is distributed to machines out of band (MDM, a secret manager) and never stored in Factory's control plane.

  If a referenced variable is not set on a machine, Droid disables the customer sink for that session and logs an error naming the missing variable, rather than shipping the literal `${VAR_NAME}` to your collector.
</Warning>

<Note>
  When `endpoint` is set in the block, the `OTEL_*` header environment variables never apply to it, because a credential sitting in a developer's shell for unrelated tooling must not be shipped to the organization's collector. Omitting `headers` therefore means the org sink is called with none; put machine-local values in `${VAR_NAME}` references inside `headers` instead.

  The block is also read once, at startup. A session that begins while logged out has no org settings to read and runs on the `OTEL_*` environment for the life of that process.
</Note>

## Machine environment variables

Set these before launching Droid. Each one configures the sink only where the organization has left that value unpinned; a `telemetry` block in org-managed settings overrides the matching variable.

```bash
export OTEL_TELEMETRY_ENDPOINT="https://your-collector.example.com:4318"
export OTEL_TELEMETRY_HEADERS="Authorization=Bearer <your-token>"
```

| Variable                  | Required | Description                                                                                                          |
| :------------------------ | :------- | :------------------------------------------------------------------------------------------------------------------- |
| `OTEL_TELEMETRY_ENDPOINT` | Yes      | Base URL of your OTLP HTTP collector. Metrics are sent to `{endpoint}/v1/metrics`. Falls back to `OTEL_EXPORTER_OTLP_ENDPOINT`. Not required when the organization pins `telemetry.endpoint`. |
| `OTEL_TELEMETRY_HEADERS`  | No       | Comma-separated `key=value` pairs sent as HTTP headers on every export. Values may contain `=` (e.g. base64 tokens). Falls back to `OTEL_EXPORTER_OTLP_HEADERS`. Never applied to an org-pinned endpoint. |
| `OTEL_LOG_MESSAGE_CONTENT` | No      | Set to `true` or `1` to export message content (user/assistant messages, tool input/results) as trace spans to `OTEL_TELEMETRY_ENDPOINT`. Content is dropped if no customer endpoint is set. |
| `OTEL_METRIC_EXPORT_INTERVAL` | No   | Metric flush interval in milliseconds. Defaults to `60000`. |
| `OTEL_CUSTOMER_ENABLED`   | No       | Set to `false` to opt this machine out of customer telemetry entirely (useful in tests and local development). This is the same pipeline pinned by `telemetry.enabled`, not a separate stream. |
| `OTEL_TELEMETRY_FORMAT`   | No       | `legacy` or `genai`. Applies only where the organization has left `telemetry.format` unset. Anything unrecognized resolves to `legacy`. See [Export formats](/enterprise/telemetry/data-reference#export-formats). |
| `OTEL_TELEMETRY_GRANULARITY` | No    | `user` or `aggregate`. Resolved most-restrictive-wins against `telemetry.granularity`, not by precedence. See [Data granularity](/enterprise/telemetry/privacy#data-granularity). |
| `OTEL_RESOURCE_ATTRIBUTES` | No      | Standard OTEL `key=value,key=value` attributes, applied to your collector only (see [Resource attributes](/enterprise/telemetry/data-reference#resource-attributes)). |
| `DROID_PARENT_SESSION_ID` | No       | Adds `parent_session.id` to metrics and content spans, for linking headless/CI sub-sessions to a parent. |

## How it works

- Once an endpoint is resolved, metrics are sent to it in the same export cycle via a fan-out exporter: no extra timers, no duplication of metric readers.
- Failures to your collector do not affect Factory's own export. Each endpoint is isolated.
- Metrics use **delta temporality**: each export contains only new values since the last flush (60-second intervals by default). Sum the deltas; do not read a last value.

For what arrives on the wire, including the metric catalogs and every attribute, see the [exported data reference](/enterprise/telemetry/data-reference).

## Example configurations

### Generic OTEL collector / Grafana Alloy

```bash
export OTEL_TELEMETRY_ENDPOINT="https://collector.example.com:4318"
```

### Datadog via OTLP ingestion

```bash
export OTEL_TELEMETRY_ENDPOINT="http://localhost:4318"
```

### Datadog direct OTLP intake

```bash
export OTEL_TELEMETRY_ENDPOINT="https://otlp.datadoghq.com"
export OTEL_TELEMETRY_HEADERS="dd-api-key=<your-datadog-api-key>"
```

### New Relic

```bash
export OTEL_TELEMETRY_ENDPOINT="https://otlp.nr-data.net:4318"
export OTEL_TELEMETRY_HEADERS="api-key=<your-new-relic-license-key>"
```

### Honeycomb

```bash
export OTEL_TELEMETRY_ENDPOINT="https://api.honeycomb.io"
export OTEL_TELEMETRY_HEADERS="x-honeycomb-team=<your-api-key>,x-honeycomb-dataset=droid-metrics"
```

## Troubleshooting

<Troubleshooting>
  <TroubleshootingItem title="Metrics are not reaching the collector">
    Verify your collector accepts OTLP HTTP on the `/v1/metrics` path.
  </TroubleshootingItem>

  <TroubleshootingItem title="Collector rejects telemetry">
    Confirm `OTEL_TELEMETRY_HEADERS` includes valid auth credentials.
  </TroubleshootingItem>

  <TroubleshootingItem title="Droid cannot reach the collector">
    Ensure network connectivity from the machine running Droid to your collector.
  </TroubleshootingItem>

  <TroubleshootingItem title="A telemetry block in settings is being ignored">
    The `telemetry` block is only honored at the organization level. A copy in project, folder, or user settings is ignored, and Droid logs a warning naming the misconfiguration.
  </TroubleshootingItem>

  <TroubleshootingItem title="Telemetry stopped after an org settings change">
    An `endpoint` or `headers` value that references an environment variable the machine does not set disables the customer sink for that session. Droid logs an error naming the missing variable; set it on the machine or remove the reference.
  </TroubleshootingItem>

  <TroubleshootingItem title="Custom OTEL_RESOURCE_ATTRIBUTES are missing from a metric datapoint">
    Six resource keys and five identity keys are reserved and cannot be set from the variable. See [Resource attributes](/enterprise/telemetry/data-reference#resource-attributes). There is no org-managed field for resource attributes; set the variable per machine or per runner.
  </TroubleshootingItem>

  <TroubleshootingItem title="Metrics arrive but no spans do">
    Spans require a collector of your own that is distinct from Factory's, so confirm `telemetry.endpoint` or `OTEL_TELEMETRY_ENDPOINT` is set and does not resolve to Factory's collector. If it is set, check whether the organization runs `telemetry.granularity: aggregate` under the `legacy` format, which exports metrics only.
  </TroubleshootingItem>

  <TroubleshootingItem title="droid.* metrics stopped after a settings change">
    Check whether `telemetry.format` was set to `genai`. The two formats replace each other, so your collector now receives `gen_ai.*` instead. See [Export formats](/enterprise/telemetry/data-reference#export-formats).
  </TroubleshootingItem>

  <TroubleshootingItem title="No telemetry at all in an airgapped deployment">
    Airgapped deployments export to your collector only, so an unset or unusable `telemetry.endpoint` disables the pipeline entirely rather than falling back to Factory. See [Airgapped deployments](#airgapped-deployments).
  </TroubleshootingItem>
</Troubleshooting>

## Airgapped deployments

Airgapped deployments export to **your collector only**. Droid builds no Factory-bound leg at all, so nothing is sent outward and nothing is queued waiting for a network that is not there.

This makes `telemetry.endpoint` (or `OTEL_TELEMETRY_ENDPOINT`) the single telemetry surface in an airgap, and the only one. Without a usable collector of your own, the pipeline does not run: there is no Factory fallback to degrade to. An endpoint that Droid rejects, such as one with an unresolved `${VAR_NAME}` reference or a query string, counts as no collector for this purpose.

The `telemetry.enabled` switch still applies on top, and airgapped deployments do not use Factory-hosted analytics.

## Factory-hosted Analytics API

In cloud-managed deployments, Factory provides a **hosted analytics view** for platform and leadership teams. It is backed by Factory's internal tracing, which feeds an aggregation pipeline; this internal tracing is separate from the metrics you export above and is never sent to your collectors.

The [Analytics API](/api-reference/analytics) exposes aggregated, org-level data, including:

- Adoption metrics by org, team, and user.
- Model usage and performance trends.
- Token consumption and cost estimates for LLM usage.
- Tool usage and productivity signals.

Use the Analytics API when you want a hosted, aggregated view, particularly for **token and cost data**, which is not part of the customer OTEL metric set.

<RelatedLinks>
  <RelatedLink href="/enterprise/telemetry/data-reference" title="Telemetry Data Reference">
    Every metric, span, and attribute Droid exports, in both formats.
  </RelatedLink>
  <RelatedLink href="/enterprise/telemetry/privacy" title="Telemetry Privacy Controls">
    Data granularity, message content logging, and what reaches Factory.
  </RelatedLink>
  <RelatedLink href="/enterprise/compliance-audit-and-monitoring" title="Compliance & Audit">
    Map telemetry signals to audit trails and regulatory workflows.
  </RelatedLink>
  <RelatedLink href="/enterprise/hierarchical-settings-and-org-control" title="Enterprise Controls & Managed Settings">
    The org-managed settings surface that pins the telemetry block.
  </RelatedLink>
</RelatedLinks>
