@telemetry-dev/sdk 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,8 +3,11 @@
3
3
  Pure TypeScript SDK for [telemetry.dev](https://telemetry.dev): add LLM traces, logs, and GenAI
4
4
  metrics to any app. Built on OpenTelemetry — spans use the `gen_ai.*` semantic conventions and
5
5
  ship as OTLP protobuf to `https://ingest.telemetry.dev`. Works on Node >= 20.19 and edge runtimes
6
- (Vercel Edge, Cloudflare Workers with `nodejs_compat`). ESM-only; CJS consumers can `require()` it
7
- on Node >= 20.19.
6
+ (Vercel Edge, and Cloudflare Workers with Node.js compatibility, which is on by default for
7
+ compatibility dates of 2026-08-04 or later; earlier dates need the `nodejs_compat` flag). ESM-only;
8
+ CJS consumers can `require()` it on Node >= 20.19.
9
+
10
+ Docs: https://telemetry.dev/docs/sdk/typescript
8
11
 
9
12
  ```sh
10
13
  npm install @telemetry-dev/sdk
@@ -12,10 +15,20 @@ npm install @telemetry-dev/sdk
12
15
 
13
16
  ## Quickstart
14
17
 
18
+ Set `TELEMETRY_DEV_API_KEY` to your project API key. `init()` reads it from the environment.
19
+
15
20
  ```ts
16
21
  import { init, observe, startSpan, propagateAttributes, log, flush } from "@telemetry-dev/sdk";
17
22
 
18
- init({ apiKey: "td_live_..." }); // or TELEMETRY_DEV_API_KEY
23
+ init();
24
+
25
+ // Stand-in for your model client, so the example runs on its own. Replace it with a real call.
26
+ async function callModel(question: string) {
27
+ return {
28
+ message: { role: "assistant", content: `You asked: ${question}` },
29
+ usage: { input: 12, output: 9 },
30
+ };
31
+ }
19
32
 
20
33
  const answer = observe(
21
34
  async function answer(question: string) {
@@ -25,13 +38,13 @@ const answer = observe(
25
38
  provider: "openai",
26
39
  input: [{ role: "user", content: question }],
27
40
  });
28
- const res = await llm.chat(question);
41
+ const res = await callModel(question);
29
42
  generation.end({
30
- output: res.message,
43
+ output: [res.message],
31
44
  usage: { inputTokens: res.usage.input, outputTokens: res.usage.output },
32
45
  });
33
46
  log("generation finished", { attributes: { cached: false } });
34
- return res.message;
47
+ return res.message.content;
35
48
  },
36
49
  { type: "agent" },
37
50
  );
@@ -65,10 +78,27 @@ other string attribute is cut at the cap without a marker, except that a cap of
65
78
  `exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
66
79
  default `"warn"`), `fetch`, `waitUntil`, `onError`.
67
80
 
81
+ Core SDK error recording keeps failure status, error type, and an exception event. It includes
82
+ `exception.message` and `exception.stacktrace` only when both effective `captureInput` and
83
+ `captureOutput` are true, including per-span overrides: exceptions can echo either inputs or
84
+ outputs. The policy is per span: when an error propagates to an enclosing span whose capture is on,
85
+ that span records its message and stack. Included fields pass through `mask` with their exception
86
+ attribute keys, then serialization and truncation. A throwing mask drops the affected field and reports a generic error to `onError`
87
+ and SDK diagnostics without the hook's original message or cause. The original application error
88
+ is rethrown unchanged. These controls do not scrub error type names, metadata, identifiers, resource
89
+ attributes, or events written directly through raw OpenTelemetry APIs.
90
+
68
91
  `captureInput: false` and `captureOutput: false` omit captured content. Provider integrations may
69
92
  still record non-content metadata such as response IDs and models, finish reasons, usage, timing,
70
- and errors. Capture flags do not gate stop sequences, tool descriptions and definitions (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes,
71
- or exception messages and stack traces; redact those separately when needed.
93
+ and error types. Capture flags do not gate stop sequences, tool descriptions and definitions
94
+ (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes;
95
+ redact those separately when needed.
96
+
97
+ `mask` receives input and output content, system instructions, log messages, log attributes
98
+ other than numbers and booleans, and content that integrations pass to
99
+ `handle.serializeContent`, including captured exception messages and stack traces. `ctx.key` names
100
+ the attribute being written. It does not receive span or propagated `metadata` or raw `attributes`;
101
+ redact those separately. Log masking is independent of span capture flags.
72
102
 
73
103
  Root spans remain independent by default. `init({ sessionMode: "process" })` groups otherwise
74
104
  uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK initialization.
@@ -79,7 +109,7 @@ new ID. It is not persisted across processes.
79
109
  `sampler` accepts an OpenTelemetry `Sampler` and overrides `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG`.
80
110
  The default is parent-based always-on. Session roots keep the configured root policy.
81
111
  Nested spans and explicitly parented spans keep the real parent's policy.
82
- The SDK installs the [session sampling wrapper](../otel/README.md#correlation-attributes) on its provider. This includes the global provider that Eve uses.
112
+ The SDK installs the [session sampling wrapper](https://github.com/telemetry-dev/telemetry/blob/main/packages/otel/README.md#correlation-attributes) on its provider. This includes the global provider that Eve uses.
83
113
 
84
114
  ## API
85
115
 
@@ -89,6 +119,13 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
89
119
  - `startSpan(name, options?)` — manual handle; does **not** activate context. `handle.update()`,
90
120
  `handle.end()`, `handle.recordOutputChunk(timestampMs?)`, `handle.traceparent`, `handle.span`
91
121
  (raw OTel span).
122
+ - `handle.serializeContent(value, key)` — prepare content that an integration writes outside
123
+ `input` and `output`, such as an error message or a raw attribute, the way `input` and `output`
124
+ are prepared: the configured `mask` with `key`, JSON stringify (strings pass through), then
125
+ truncation to `maxAttributeLength`. Returns `undefined` when the mask throws (reported through
126
+ `onError`) or drops the value, or the value cannot be serialized; write nothing then. It does not
127
+ check `captureInput` / `captureOutput`. Integrations that support older cores should treat a
128
+ missing method as a configured mask.
92
129
  - `startActiveSpan(name, options?, fn)` — callback-scoped; activates context, auto-ends.
93
130
  - `updateActiveSpan(fields)` — update the innermost active span.
94
131
  - `propagateAttributes({ userId, sessionId, metadata }, fn)` — stamps `user.id`,
@@ -165,10 +202,21 @@ that mode only this SDK's spans are exported by default.
165
202
  Any vanilla OTel app can also skip the SDK entirely: point a stock OTLP/HTTP protobuf exporter at
166
203
  `https://ingest.telemetry.dev/v1/traces` with an `Authorization: Bearer td_live_...` header.
167
204
 
205
+ ### Native Claude SDK spans
206
+
207
+ `@anthropic-ai/sdk` 0.134.0 and later emits OpenTelemetry spans natively through the global tracer provider.
208
+ To send these spans to telemetry.dev, refer to [Claude SDK](https://telemetry.dev/docs/integrations/claude-sdk).
209
+ If you use `init({ registerGlobal: true })`, also set `spanFilter: () => true`. The default filter of that mode exports only the spans of this SDK.
210
+ Do not use the native spans and `@telemetry-dev/anthropic` together. If you use the two, each request has two spans.
211
+
168
212
  ## Notes
169
213
 
170
- - AsyncLocalStorage powers context propagation. On runtimes without it (Workers without
171
- `nodejs_compat`) automatic nesting is unavailable entirely unless the host app registered its
214
+ - AsyncLocalStorage powers context propagation. On runtimes without it (Cloudflare Workers without
215
+ Node.js compatibility) automatic nesting is unavailable entirely unless the host app registered its
172
216
  own global OTel context manager — pass `parent` explicitly there.
173
217
  - `@opentelemetry/api` is a peer dependency (installed automatically by npm/pnpm) so the SDK
174
218
  shares the host app's OTel API singleton.
219
+
220
+ ## License
221
+
222
+ MIT. See [LICENSE](https://github.com/telemetry-dev/telemetry/blob/main/packages/sdk/LICENSE).
package/dist/index.d.mts CHANGED
@@ -5,7 +5,11 @@ import { BatchOptions, BatchOptions as BatchOptions$1, ExportMode, ExportMode as
5
5
  import { PushMetricExporter } from "@opentelemetry/sdk-metrics";
6
6
 
7
7
  //#region src/config.d.ts
8
- /** Redaction hook applied to captured content (input/output/log message) before JSON stringification. */
8
+ /**
9
+ * Redaction hook applied to captured content (input/output, log messages and attributes, and
10
+ * content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
11
+ * JSON stringification. Not applied to metadata or raw attributes.
12
+ */
9
13
  type MaskFn<T = unknown> = (value: T, ctx: {
10
14
  key: string;
11
15
  }) => T;
@@ -28,9 +32,9 @@ interface TelemetryOptions {
28
32
  exportMode?: ExportMode$1;
29
33
  /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
30
34
  sampler?: Sampler;
31
- /** Capture span inputs (args, prompts) by default. Default true. */
35
+ /** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
32
36
  captureInput?: boolean;
33
- /** Capture span outputs (results, completions) by default. Default true. */
37
+ /** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
34
38
  captureOutput?: boolean;
35
39
  /** Redaction hook run on captured content before stringify/truncate. */
36
40
  mask?: MaskFn;
@@ -53,7 +57,7 @@ interface TelemetryOptions {
53
57
  fetch?: typeof fetch;
54
58
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
55
59
  waitUntil?: (p: Promise<unknown>) => void;
56
- /** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never throws into user code. */
60
+ /** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. A series of failed exports is reported once, and the next failure after an export reaches the ingest is reported again. The SDK never throws into user code. */
57
61
  onError?: (cause: Error) => void;
58
62
  /** Header value for a custom telemetry.dev integration. */
59
63
  sdkName?: string;
@@ -211,6 +215,15 @@ interface SpanHandle {
211
215
  /** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
212
216
  recordOutputChunk(timestampMs?: number): void;
213
217
  reportError?(cause: unknown): void;
218
+ /**
219
+ * Prepares content that the span writes outside `input` and `output` the way those are
220
+ * prepared: the configured mask with `key` (the attribute the result is written to), JSON
221
+ * stringify (strings pass through), then truncation to `maxAttributeLength`. Returns undefined
222
+ * when the mask throws (reported to `onError`) or drops the value, or the value cannot be
223
+ * serialized. Capture flags are not checked. Absent on cores that predate it; treat a missing
224
+ * method as masked.
225
+ */
226
+ serializeContent?(value: unknown, key: string): string | undefined;
214
227
  end(fields?: SpanFields & {
215
228
  endTime?: Date | number;
216
229
  }): void;
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator, p
3
3
  import { resourceFromAttributes } from "@opentelemetry/resources";
4
4
  import { BatchLogRecordProcessor, LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
5
5
  import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
6
- import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, DURATION_BUCKETS, OUTPUT_CHUNK_HISTOGRAM, PROPAGATED_KEY, SCOPE_NAME, SCOPE_NAME as SCOPE_NAME$1, SCOPE_VERSION, StampingSpanProcessor, TelemetrySpanProcessor, activeContext, activeContext as activeContext$1, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, sessionIdOf, sessionRootTracerProvider, sessionSampler, setLogLevel, withContext, withContext as withContext$1, withSessionParent } from "@telemetry-dev/otel";
6
+ import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, DURATION_BUCKETS, OUTPUT_CHUNK_HISTOGRAM, PROPAGATED_KEY, SCOPE_NAME, SCOPE_NAME as SCOPE_NAME$1, SCOPE_VERSION, StampingSpanProcessor, TelemetrySpanProcessor, activeContext, activeContext as activeContext$1, als, createExportFailures, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, sessionIdOf, sessionRootTracerProvider, sessionSampler, setLogLevel, withContext, withContext as withContext$1, withSessionParent } from "@telemetry-dev/otel";
7
7
  import { SeverityNumber } from "@opentelemetry/api-logs";
8
8
  //#region src/config.ts
9
9
  const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
@@ -102,9 +102,11 @@ function initInner(options, overrides) {
102
102
  "deployment.environment.name": config.environment,
103
103
  ...config.resourceAttributes
104
104
  });
105
+ const failures = createExportFailures(config.onError);
105
106
  const transport = {
106
107
  fetchImpl: config.fetchImpl,
107
- onError: config.onError
108
+ onError: config.onError,
109
+ failures
108
110
  };
109
111
  const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : void 0;
110
112
  const spanExporter = overrides?.spanExporter ?? createTraceExporter({
@@ -154,7 +156,7 @@ function initInner(options, overrides) {
154
156
  const ensureLogger = () => {
155
157
  if (!logExporter) return void 0;
156
158
  if (!logs) {
157
- const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor(logExporter) : new BatchLogRecordProcessor(logExporter);
159
+ const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor({ exporter: logExporter }) : new BatchLogRecordProcessor({ exporter: logExporter });
158
160
  const loggerProvider = new LoggerProvider({
159
161
  resource,
160
162
  logRecordLimits: { attributeValueLengthLimit: backstopLength },
@@ -173,7 +175,7 @@ function initInner(options, overrides) {
173
175
  const parts = [provider[op]()];
174
176
  if (logs) parts.push(logs[op]());
175
177
  if (metrics) parts.push(metrics[op]());
176
- return Promise.all(parts).then(() => void 0).catch((error) => reportError(config.onError, error));
178
+ return Promise.all(parts).then(() => void 0).catch((error) => failures.report(error));
177
179
  };
178
180
  const handoff = (p) => {
179
181
  if (config.waitUntil) {
@@ -356,8 +358,8 @@ function prepareCaptureValue(key, value, cfg) {
356
358
  let masked = value;
357
359
  if (cfg.mask) try {
358
360
  masked = cfg.mask(value, { key });
359
- } catch (error) {
360
- reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
361
+ } catch {
362
+ reportError(cfg.onError, /* @__PURE__ */ new Error("mask hook raised; content dropped"));
361
363
  return;
362
364
  }
363
365
  const serialized = jsonAttr(masked);
@@ -540,6 +542,7 @@ const NOOP_SPAN_HANDLE = {
540
542
  update: () => NOOP_SPAN_HANDLE,
541
543
  recordOutputChunk: () => {},
542
544
  reportError: () => {},
545
+ serializeContent: () => void 0,
543
546
  end: () => {}
544
547
  };
545
548
  //#endregion
@@ -576,16 +579,16 @@ function resolveParent(parent) {
576
579
  if (isContext(ref)) return withAmbientPropagated(ref);
577
580
  return trace.setSpanContext(base, ref);
578
581
  }
579
- function applyError(span, error) {
582
+ function applyError(span, error, capture) {
580
583
  const err = error instanceof Error ? error : void 0;
581
584
  const errorType = err === void 0 ? "Error" : err.name !== "Error" && err.name.length > 0 ? err.name : (err.constructor?.name ?? "Error") || "Error";
582
- const message = err?.message ?? String(error);
583
585
  span.setStatus({ code: SpanStatusCode.ERROR });
584
586
  span.setAttribute("error.type", errorType);
587
+ const captureContent = capture.captureInput && capture.captureOutput;
585
588
  span.addEvent("exception", omitUndefined({
586
589
  "exception.type": errorType,
587
- "exception.message": message,
588
- "exception.stacktrace": err?.stack,
590
+ "exception.message": captureContent ? prepareCaptureValue("exception.message", err?.message ?? String(error), capture) : void 0,
591
+ "exception.stacktrace": captureContent && err?.stack !== void 0 ? prepareCaptureValue("exception.stacktrace", err.stack, capture) : void 0,
589
592
  "log.severity_number": SEVERITY_ERROR
590
593
  }));
591
594
  }
@@ -593,7 +596,7 @@ function applyFields(span, meta, fields) {
593
596
  if (fields.name) span.updateName(fields.name);
594
597
  const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
595
598
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
596
- if (fields.error !== void 0) applyError(span, fields.error);
599
+ if (fields.error !== void 0) applyError(span, fields.error, meta.capture);
597
600
  }
598
601
  function metaFor(core, options) {
599
602
  const cfg = core.config;
@@ -631,7 +634,7 @@ function createSpanHandle(core, name, options = {}) {
631
634
  SPAN_META.set(span, meta);
632
635
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
633
636
  if (options.name) span.updateName(options.name);
634
- if (options.error !== void 0) applyError(span, options.error);
637
+ if (options.error !== void 0) applyError(span, options.error, meta.capture);
635
638
  const spanContext = span.spanContext();
636
639
  let previousOutputChunkTime;
637
640
  const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
@@ -682,6 +685,9 @@ function createSpanHandle(core, name, options = {}) {
682
685
  reportError(cause) {
683
686
  reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
684
687
  },
688
+ serializeContent(value, key) {
689
+ return prepareCaptureValue(key, value, meta.capture);
690
+ },
685
691
  end(fields) {
686
692
  try {
687
693
  if (fields) applyFields(span, meta, fields);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Pure TypeScript SDK for telemetry.dev: LLM traces, logs, and GenAI metrics on OpenTelemetry, for Node and edge runtimes.",
5
5
  "keywords": [
6
6
  "genai",
@@ -36,13 +36,13 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@opentelemetry/api-logs": "^0.218.0",
40
- "@opentelemetry/core": "^2.7.1",
41
- "@opentelemetry/resources": "^2.7.1",
42
- "@opentelemetry/sdk-logs": "^0.218.0",
43
- "@opentelemetry/sdk-metrics": "^2.7.1",
44
- "@opentelemetry/sdk-trace-base": "^2.7.1",
45
- "@telemetry-dev/otel": "^0.1.5"
39
+ "@opentelemetry/api-logs": "^0.223.0",
40
+ "@opentelemetry/core": "^2.12.0",
41
+ "@opentelemetry/resources": "^2.12.0",
42
+ "@opentelemetry/sdk-logs": "^0.223.0",
43
+ "@opentelemetry/sdk-metrics": "^2.12.0",
44
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
45
+ "@telemetry-dev/otel": "^0.1.7"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opentelemetry/api": "^1.9.1",
package/src/capture.ts CHANGED
@@ -270,8 +270,9 @@ export function prepareCaptureValue(
270
270
  if (cfg.mask) {
271
271
  try {
272
272
  masked = cfg.mask(value, { key });
273
- } catch (error) {
274
- reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
273
+ } catch {
274
+ // Hook errors can contain the unmasked value or chain the application exception.
275
+ reportError(cfg.onError, new Error("mask hook raised; content dropped"));
275
276
 
276
277
  return undefined;
277
278
  }
package/src/client.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  als,
20
20
  AlsContextManager,
21
21
  BATCHED_METRIC_INTERVAL_MS,
22
+ createExportFailures,
22
23
  createLogExporter,
23
24
  createMetricExporter,
24
25
  createMetricsPipeline,
@@ -156,7 +157,10 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
156
157
  ...config.resourceAttributes,
157
158
  });
158
159
 
159
- const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
160
+ // A failed flush repeats the failure of its export, so the flush and the exporters share one
161
+ // failure state.
162
+ const failures = createExportFailures(config.onError);
163
+ const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError, failures };
160
164
  const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
161
165
 
162
166
  const spanExporter =
@@ -244,8 +248,8 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
244
248
  if (!logs) {
245
249
  const logProcessor =
246
250
  config.exportMode === "immediate"
247
- ? new SimpleLogRecordProcessor(logExporter)
248
- : new BatchLogRecordProcessor(logExporter);
251
+ ? new SimpleLogRecordProcessor({ exporter: logExporter })
252
+ : new BatchLogRecordProcessor({ exporter: logExporter });
249
253
 
250
254
  const loggerProvider = new LoggerProvider({
251
255
  resource,
@@ -274,7 +278,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
274
278
 
275
279
  return Promise.all(parts)
276
280
  .then(() => undefined)
277
- .catch((error: Error) => reportError(config.onError, error));
281
+ .catch((error: Error) => failures.report(error));
278
282
  };
279
283
 
280
284
  const handoff = (p: Promise<void>): Promise<void> => {
package/src/config.ts CHANGED
@@ -12,7 +12,11 @@ import {
12
12
 
13
13
  export type { BatchOptions, ExportMode, LogLevel, SdkLogLevel } from "@telemetry-dev/otel";
14
14
 
15
- /** Redaction hook applied to captured content (input/output/log message) before JSON stringification. */
15
+ /**
16
+ * Redaction hook applied to captured content (input/output, log messages and attributes, and
17
+ * content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
18
+ * JSON stringification. Not applied to metadata or raw attributes.
19
+ */
16
20
  export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
17
21
 
18
22
  export interface TelemetryOptions {
@@ -34,9 +38,9 @@ export interface TelemetryOptions {
34
38
  exportMode?: ExportMode;
35
39
  /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
36
40
  sampler?: Sampler;
37
- /** Capture span inputs (args, prompts) by default. Default true. */
41
+ /** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
38
42
  captureInput?: boolean;
39
- /** Capture span outputs (results, completions) by default. Default true. */
43
+ /** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
40
44
  captureOutput?: boolean;
41
45
  /** Redaction hook run on captured content before stringify/truncate. */
42
46
  mask?: MaskFn;
@@ -59,7 +63,7 @@ export interface TelemetryOptions {
59
63
  fetch?: typeof fetch;
60
64
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
61
65
  waitUntil?: (p: Promise<unknown>) => void;
62
- /** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never throws into user code. */
66
+ /** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. A series of failed exports is reported once, and the next failure after an export reaches the ingest is reported again. The SDK never throws into user code. */
63
67
  onError?: (cause: Error) => void;
64
68
  /** Header value for a custom telemetry.dev integration. */
65
69
  sdkName?: string;
package/src/noop.ts CHANGED
@@ -16,5 +16,6 @@ export const NOOP_SPAN_HANDLE: SpanHandle = {
16
16
  update: () => NOOP_SPAN_HANDLE,
17
17
  recordOutputChunk: () => {},
18
18
  reportError: () => {},
19
+ serializeContent: () => undefined,
19
20
  end: () => {},
20
21
  };
package/src/span.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  type SpanFields,
39
39
  type SpanType,
40
40
  } from "./attrs.ts";
41
+ import { prepareCaptureValue } from "./capture.ts";
41
42
  import { type ClientCore, currentClient } from "./client.ts";
42
43
  import { NOOP_SPAN_HANDLE } from "./noop.ts";
43
44
 
@@ -80,6 +81,15 @@ export interface SpanHandle {
80
81
  /** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
81
82
  recordOutputChunk(timestampMs?: number): void;
82
83
  reportError?(cause: unknown): void;
84
+ /**
85
+ * Prepares content that the span writes outside `input` and `output` the way those are
86
+ * prepared: the configured mask with `key` (the attribute the result is written to), JSON
87
+ * stringify (strings pass through), then truncation to `maxAttributeLength`. Returns undefined
88
+ * when the mask throws (reported to `onError`) or drops the value, or the value cannot be
89
+ * serialized. Capture flags are not checked. Absent on cores that predate it; treat a missing
90
+ * method as masked.
91
+ */
92
+ serializeContent?(value: unknown, key: string): string | undefined;
83
93
  end(fields?: SpanFields & { endTime?: Date | number }): void;
84
94
  }
85
95
 
@@ -143,7 +153,7 @@ function resolveParent(parent: ParentRef | undefined): Context {
143
153
  return trace.setSpanContext(base, ref);
144
154
  }
145
155
 
146
- function applyError(span: Span, error: unknown): void {
156
+ function applyError(span: Span, error: unknown, capture: FieldCaptureConfig): void {
147
157
  const err = error instanceof Error ? error : undefined;
148
158
 
149
159
  // Prefer the specific subclass name (Python parity: type(error).__name__) — many SDK
@@ -155,15 +165,21 @@ function applyError(span: Span, error: unknown): void {
155
165
  ? err.name
156
166
  : (err.constructor?.name ?? "Error") || "Error";
157
167
 
158
- const message = err?.message ?? String(error);
159
168
  span.setStatus({ code: SpanStatusCode.ERROR });
160
169
  span.setAttribute("error.type", errorType);
170
+ // Exceptions can echo either inputs or outputs; both must allow content capture.
171
+ const captureContent = capture.captureInput && capture.captureOutput;
161
172
  span.addEvent(
162
173
  "exception",
163
174
  omitUndefined({
164
175
  "exception.type": errorType,
165
- "exception.message": message,
166
- "exception.stacktrace": err?.stack,
176
+ "exception.message": captureContent
177
+ ? prepareCaptureValue("exception.message", err?.message ?? String(error), capture)
178
+ : undefined,
179
+ "exception.stacktrace":
180
+ captureContent && err?.stack !== undefined
181
+ ? prepareCaptureValue("exception.stacktrace", err.stack, capture)
182
+ : undefined,
167
183
  "log.severity_number": SEVERITY_ERROR,
168
184
  }),
169
185
  );
@@ -175,7 +191,7 @@ function applyFields(span: Span, meta: SpanMeta, fields: SpanFields): void {
175
191
 
176
192
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
177
193
 
178
- if (fields.error !== undefined) applyError(span, fields.error);
194
+ if (fields.error !== undefined) applyError(span, fields.error, meta.capture);
179
195
  }
180
196
 
181
197
  function metaFor(core: ClientCore, options?: StartSpanOptions): SpanMeta {
@@ -236,7 +252,7 @@ export function createSpanHandle(
236
252
 
237
253
  if (options.name) span.updateName(options.name);
238
254
 
239
- if (options.error !== undefined) applyError(span, options.error);
255
+ if (options.error !== undefined) applyError(span, options.error, meta.capture);
240
256
 
241
257
  const spanContext = span.spanContext();
242
258
  let previousOutputChunkTime: number | undefined;
@@ -303,6 +319,9 @@ export function createSpanHandle(
303
319
  reportError(cause) {
304
320
  reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
305
321
  },
322
+ serializeContent(value, key) {
323
+ return prepareCaptureValue(key, value, meta.capture);
324
+ },
306
325
  end(fields) {
307
326
  try {
308
327
  if (fields) applyFields(span, meta, fields);