# Telemetry

Canonical page: https://docs.ovok.com/native-sdk/guide/components/helpers/telemetry

The SDK exposes vendor-neutral helpers for OpenTelemetry traces, metrics, and
logs. Import them from `@ovok/native/helpers` or the package root
`@ovok/native`. The SDK does not configure an exporter or choose a telemetry
backend; the host app owns provider registration, export, retention, and access
policy.

~~~ts
import {
  configureOvokTelemetry,
  withOvokSpan,
  recordOvokMetric,
} from "@ovok/native/helpers";
~~~

## Configure providers [#configure-providers]

If the app has registered OpenTelemetry providers globally, the SDK uses those
providers when the optional API packages are installed. Otherwise, pass the
providers explicitly:

~~~ts
configureOvokTelemetry({
  tracerProvider: appTracerProvider,
  meterProvider: appMeterProvider,
  loggerProvider: appLoggerProvider,
});
~~~

Each provider is optional. `@opentelemetry/api` and `@opentelemetry/api-logs` are
optional peer dependencies; an app that does not use OpenTelemetry can omit them.
With no provider available, telemetry helpers are no-ops and SDK operations
continue. Configure providers during startup before SDK features run.

`OvokTelemetryOptions` accepts three provider adapters:

| Option | Provider method | Used for |
| --- | --- | --- |
| `tracerProvider` | `getTracer(name, version?)` and optional `getActiveSpan()` | Spans and span events |
| `meterProvider` | `getMeter(name, version?)` | Counters and histograms |
| `loggerProvider` | `getLogger(name, version?)` | OpenTelemetry log records |

The provider interfaces accept the SDK's minimal OpenTelemetry-compatible surface;
the app still chooses the concrete SDK, processors, exporters, and destinations.

## Public helpers [#public-helpers]

| Export | Signature | Behavior |
| --- | --- | --- |
| `configureOvokTelemetry` | `(options?: OvokTelemetryOptions) => void` | Set explicit providers; an empty options object clears explicit providers. |
| `sanitizeOvokTelemetryAttributes` | `(attributes?: Record<string, unknown>) => OvokTelemetryAttributes` | Keep allowlisted keys with string, number, or boolean values. |
| `startOvokSpan` | `(name: string, attributes?: Record<string, unknown>) => OvokSpan \| undefined` | Start a span when a tracer exists; end it with `span.end()` in `finally`. |
| `withOvokSpan` | `<T>(name, attributes, operation) => Promise<T>` | Run an operation in a span when available, preserving its return value or error. |
| `recordOvokMetric` | `(name: string, value: number, attributes?: Record<string, unknown>) => void` | Add a value to a named counter. |
| `recordOvokDuration` | `(name: string, durationMs: number, attributes?: Record<string, unknown>) => void` | Record milliseconds in a named histogram. |
| `addOvokSpanEvent` | `(name: string, attributes?: Record<string, unknown>, span?: OvokSpan) => void` | Add an event to the supplied span or current active span. |
| `emitOvokLog` | `(severityText: string, body: string, attributes?: Record<string, unknown>) => void` | Emit an OpenTelemetry log record when a logger provider exists. |

The public telemetry types are `OvokTelemetryAttribute` (`string | number |
boolean`), `OvokTelemetryAttributes` (a string-keyed record of those values),
`OvokSpan`, `OvokTracerProvider`, `OvokMeterProvider`, `OvokLoggerProvider`, and
`OvokTelemetryOptions`. `OvokSpan` exposes optional methods for setting attributes,
adding events, recording exceptions, and setting status; `end()` is required.

~~~ts
type OvokTelemetryAttribute = string | number | boolean;
type OvokTelemetryAttributes = Record<string, OvokTelemetryAttribute>;

interface OvokSpan {
  setAttribute?: (name: string, value: OvokTelemetryAttribute) => void;
  setAttributes?: (attributes: OvokTelemetryAttributes) => void;
  addEvent?: (name: string, attributes?: OvokTelemetryAttributes) => void;
  recordException?: (error: Error | string) => void;
  setStatus?: (status: { code: number; message?: string }) => void;
  end: () => void;
}

interface OvokTelemetryOptions {
  tracerProvider?: OvokTracerProvider;
  meterProvider?: OvokMeterProvider;
  loggerProvider?: OvokLoggerProvider;
}
~~~

When a span is available, `withOvokSpan` sets success or error status, records
exceptions, ends the span, and adds one to `ovok.errors` when the operation throws.
It rethrows the original error. If no tracer is available, it still invokes and
awaits the operation. Provider failures do not change SDK control flow.

For direct span management, always end the returned span:

~~~ts
const span = startOvokSpan("app.measurement.persist", {
  provider: "bluetooth",
  measurementTypeKey: "body-weight",
});

try {
  await persistMeasurement(measurement);
} finally {
  span?.end();
}
~~~

Prefer `withOvokSpan` for app operations because it handles lifecycle and errors.

## Attribute allowlist [#attribute-allowlist]

The SDK filters attributes passed through its span, metric, duration, and span-event
helpers. It keeps only these keys and only primitive string, number, or boolean
values:

`deviceId`, `deviceName`, `model`, `manufacturer`, `measurementTypeKey`, `platform`,
`status`, `reason`, `errorCode`, `retryCount`, `attempt`, `queueDepth`, `durationMs`,
`success`, `exhausted`, `willRetry`, `requestedMtu`, `provider`, and `sampleCount`.

Unknown keys and object or array values are dropped. This is an operational
allowlist, not a guarantee that every permitted value is safe for every app. Review
device identifiers and other context under your data policy.

`emitOvokLog` does **not** call this sanitizer. The logger helpers also pass log
messages and `extra` values as supplied. Do not put patient identifiers, credentials,
measurement values, or raw device payloads in log records. See [Logging](/native-sdk/guide/components/helpers/logging)
for the logger callback and `slog` behavior.

## Built-in SDK signals [#built-in-sdk-signals]

The SDK currently emits these signals through the helpers above:

| Signal | Instrument | Meaning |
| --- | --- | --- |
| Span | `bluetooth.connect` | One Bluetooth connection attempt. |
| Span | `health.import` | One HealthKit or Health Connect import run. |
| Event | `bluetooth.scan.started` | Continuous Bluetooth scanning started. |
| Event | `device.status_changed` | A device status callback ran. |
| Event | `reading.received` | A measurement reading was received. |
| Event | `pairing.failed` | The device connection reports that pairing requires a code. |
| Counter | `ovok.devices.found` | A supported device was found. |
| Counter | `ovok.readings.received` | A reading was delivered by a device. |
| Counter | `ovok.pairing.failures` | A pairing-required event occurred. |
| Counter | `ovok.queue.delivered` | A queued result was delivered. |
| Counter | `ovok.queue.retries` | A queued delivery attempt failed and will be retried. |
| Counter | `ovok.queue.exhausted` | A queued result reached the retry limit. |
| Counter | `ovok.health.samples.imported` | Count of newly created observations in a health-import batch. |
| Counter | `ovok.errors` | A traced operation failed, or the logger received `error`/`fatal`. |
| Counter | `ovok.queue.depth` | The current queue length is added as a counter value when the queue is written; this is not a gauge. |
| Histogram | `ovok.device.connection.duration` | Bluetooth connection duration in milliseconds. |
| Histogram | `ovok.measurement.save.duration` | Health-import batch save duration in milliseconds. |

Metric attributes are limited by the sanitizer. Signal availability also depends on
the feature running in the foreground or background; the SDK does not guarantee that
an OS-scheduled task will run long enough to emit telemetry.

See [Logs and telemetry](/native-sdk/guide/how-to/observability) for setup and integration
guidance, or return to the [Helpers reference](/native-sdk/guide/components/helpers/).
