From 44ef057b0c604a7bdcc2a315838b31845ef1ba1b Mon Sep 17 00:00:00 2001 From: Aikins Laryea Date: Wed, 4 Nov 2026 00:11:06 +0000 Subject: [PATCH] docs(sdk): document the overrides argument for capturing telemetry in tests --- packages/ai/README.md | 32 +++++++++++++++++++ packages/ai/tests/integration.test.ts | 2 -- packages/otel/src/generation.ts | 6 +++- packages/tanstack-ai/README.md | 31 ++++++++++++++++++ packages/tanstack-ai/tests/middleware.test.ts | 2 -- 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index b478df46..3c9f0c53 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -257,6 +257,38 @@ Alongside spans, each run emits structured logs (shown on the trace and the Logg Logs are emitted as OTLP span events on the owning span (sent on the same `/v1/traces` request), so they correlate to the turn/tool they describe. +## Testing + +To check your telemetry in tests without a network, pass a second argument to `telemetryDev()`. +`sendSpans` receives each flushed batch of sampled spans instead of posting it, and +`recordDuration` and `recordTokens` receive each metric point instead of recording it. The media +and batch wrappers, and the `/v6` entry, take the same object as their last argument. + +```ts +import { telemetryDev } from "@telemetry-dev/ai-sdk"; + +const spanNames: string[] = []; +const tokens: number[] = []; + +const telemetry = telemetryDev( + { apiKey: "td_live_test" }, + { + sendSpans: async (batch) => { + spanNames.push(...batch.map((span) => span.name)); + }, + recordDuration: () => {}, + recordTokens: (_type, count) => { + tokens.push(count); + }, + }, +); +``` + +The integration still needs an API key, or it does nothing; any non-empty value works. Each +function you leave out keeps its default, so pass all three to keep the test offline. An error +thrown by `sendSpans` goes to `onError`. Each span in a batch is an OpenTelemetry `ReadableSpan`, so +a test can also read its `attributes` and `events`. + ## Limitations On `ai@7` (root entry): diff --git a/packages/ai/tests/integration.test.ts b/packages/ai/tests/integration.test.ts index e88a3e4c..491744f7 100644 --- a/packages/ai/tests/integration.test.ts +++ b/packages/ai/tests/integration.test.ts @@ -24,8 +24,6 @@ interface MetricRecord { const requestUrl = (input: string | Request | URL): string => input instanceof Request ? input.url : input.toString(); -// Inject a capturing transport via the otel.ts test seam: spans are captured as the real -// ReadableSpans the emitter would serialize, and metric points are captured in-process. function makeCapture() { const spanBatches: ReadableSpan[][] = []; const metrics: MetricRecord[] = []; diff --git a/packages/otel/src/generation.ts b/packages/otel/src/generation.ts index 8118929f..388469ae 100644 --- a/packages/otel/src/generation.ts +++ b/packages/otel/src/generation.ts @@ -31,7 +31,11 @@ export interface GenerationEmitter { flush(spans: Span[]): Promise; } -/** Replaces span export or metric collection without changes to sampling and flush behavior. */ +/** + * Replaces span export or metric collection without changes to sampling and flush behavior. + * `sendSpans` receives each flushed batch of sampled spans, and an error it throws goes to + * `onError`. A function left out keeps exporting to telemetry.dev. + */ export interface GenerationEmitterOverrides { sendSpans?: (spans: ReadableSpan[]) => Promise; recordDuration?: (seconds: number, attributes: Attributes) => void; diff --git a/packages/tanstack-ai/README.md b/packages/tanstack-ai/README.md index bb46ee8c..ca60272f 100644 --- a/packages/tanstack-ai/README.md +++ b/packages/tanstack-ai/README.md @@ -164,6 +164,37 @@ telemetryDev({ waitUntil: (p) => ctx.waitUntil(p) }); - **Metrics:** `gen_ai.client.operation.duration` per iteration and tool call, and `gen_ai.client.token.usage` per iteration, both following the OTel GenAI semantic conventions. +## Testing + +To check your telemetry in tests without a network, pass a second argument to `telemetryDev()`. +`sendSpans` receives each flushed batch of sampled spans instead of posting it, and +`recordDuration` and `recordTokens` receive each metric point instead of recording it. + +```ts +import { telemetryDev } from "@telemetry-dev/tanstack-ai"; + +const spanNames: string[] = []; +const tokens: number[] = []; + +const telemetry = telemetryDev( + { apiKey: "td_live_test" }, + { + sendSpans: async (batch) => { + spanNames.push(...batch.map((span) => span.name)); + }, + recordDuration: () => {}, + recordTokens: (_type, count) => { + tokens.push(count); + }, + }, +); +``` + +The middleware still needs an API key, or it does nothing; any non-empty value works. Each +function you leave out keeps its default, so pass all three to keep the test offline. An error +thrown by `sendSpans` goes to `onError`. Each span in a batch is an OpenTelemetry `ReadableSpan`, so +a test can also read its `attributes` and `events`. + ## Limitations - **TTFT:** time-to-first-chunk is not currently reported. diff --git a/packages/tanstack-ai/tests/middleware.test.ts b/packages/tanstack-ai/tests/middleware.test.ts index 37783690..b9582c1c 100644 --- a/packages/tanstack-ai/tests/middleware.test.ts +++ b/packages/tanstack-ai/tests/middleware.test.ts @@ -30,8 +30,6 @@ interface MetricRecord { attributes: Attributes; } -// Inject a capturing transport via the otel.ts test seam: spans are captured as the real -// ReadableSpans the emitter would serialize, and metric points are captured in-process. function makeCapture() { const spanBatches: ReadableSpan[][] = []; const metrics: MetricRecord[] = [];