@telemetry-dev/sdk 0.1.6 → 0.1.8

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
@@ -55,11 +55,21 @@ Without an API key every call is a silent no-op — the SDK never throws into yo
55
55
 
56
56
  Other `init()` options: `enabled` (kill switch), `registerGlobal`, `exportMode: "batched" |
57
57
  "immediate"`, `captureInput` / `captureOutput`, `mask(value, { key })` redaction hook,
58
- `maxAttributeLength` (default 65536, truncated values end with `...[truncated]`), `batch`
58
+ `maxAttributeLength` (a non-negative safe integer or `Infinity`; default 65536; `null` and
59
+ `undefined` select the default, while other invalid values disable telemetry and are reported
60
+ through `onError`; truncated values use `...[truncated]`, or its prefix when the cap is shorter than
61
+ the marker; the cap also applies, with the marker, to string metadata and log attributes, and any
62
+ other string attribute is cut at the cap without a marker, except that a cap of `0` leaves those at
63
+ 65536), `batch`
59
64
  (`maxExportBatchSize` 64, `scheduledDelayMillis` 1000, `maxQueueSize` 2048,
60
65
  `exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
61
66
  default `"warn"`), `fetch`, `waitUntil`, `onError`.
62
67
 
68
+ `captureInput: false` and `captureOutput: false` omit captured content. Provider integrations may
69
+ 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.
72
+
63
73
  Root spans remain independent by default. `init({ sessionMode: "process" })` groups otherwise
64
74
  uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK initialization.
65
75
  Explicit and propagated session IDs take precedence, and valid parent trace context is preserved.
@@ -92,6 +102,8 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
92
102
  or inject W3C trace context. Baggage is omitted unless `includeBaggage` is true.
93
103
  - `captureEnabled("input" | "output")` — report whether the active initialized client permits that
94
104
  capture direction. Returns `false` when no client is initialized.
105
+ - `captureMasked()` — report whether the active initialized client has a `mask` configured.
106
+ Returns `false` when no client is initialized.
95
107
  - `boundedCapture(value, options?)` — copy a JSON-compatible value within bounded work and retained
96
108
  size. Defaults to 48 KiB, depth 32, and 1,000 traversal items. Returns `{ value, truncated }`;
97
109
  `value` can be partial or `undefined` when a limit, cycle, getter, or skip rule prevents capture.
@@ -115,9 +127,13 @@ after the ended span passes `spanFilter`, including interrupted streams. Chunk t
115
127
  histogram state and does not retain individual intervals. Pull-based timing includes consumer delay
116
128
  between reads. An explicit timestamp uses the same monotonic millisecond clock for every chunk.
117
129
 
118
- Provider integrations feature-detect chunk recording. With an older core SDK that lacks
119
- `recordOutputChunk`, streams and existing tracing continue normally, but chunk-interval metrics are
120
- unavailable. Upgrade the core SDK with provider integrations to enable the new metric.
130
+ Provider integrations feature-detect stream APIs. With an older core SDK that lacks the stream
131
+ capture policy, streams are still traced with usage, model, finish reason, and timing. Output is
132
+ retained within the same fixed bounds as with a current core, but a mask is assumed to be
133
+ configured, so incomplete output is withheld. Instrumentation failures are not reported to `onError` with such a core. If only
134
+ `recordOutputChunk` is unavailable, tracing and reconstruction continue but chunk-interval metrics
135
+ are unavailable. Upgrade the core SDK with provider integrations to enable the full stream
136
+ telemetry.
121
137
 
122
138
  ## Serverless
123
139
 
package/dist/index.d.mts CHANGED
@@ -34,7 +34,7 @@ interface TelemetryOptions {
34
34
  captureOutput?: boolean;
35
35
  /** Redaction hook run on captured content before stringify/truncate. */
36
36
  mask?: MaskFn;
37
- /** Truncation cap (chars) for content attributes; also set as the provider's attributeValueLengthLimit. Default 65536. */
37
+ /** Non-negative safe-integer truncation cap for content attributes; Infinity disables truncation. The cap also applies, with the marker, to string metadata and log attributes; any other string attribute is cut at the cap without a marker, except that a cap of 0 leaves those at 65536. init() reports invalid values through onError and returns a disabled client. Default 65536. */
38
38
  maxAttributeLength?: number;
39
39
  batch?: BatchOptions$1;
40
40
  /** Export filter. Default when registerGlobal: only spans from this SDK's instrumentation scope. */
@@ -53,7 +53,7 @@ interface TelemetryOptions {
53
53
  fetch?: typeof fetch;
54
54
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
55
55
  waitUntil?: (p: Promise<unknown>) => void;
56
- /** Receives any error raised while emitting telemetry; the SDK never throws into user code. */
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. */
57
57
  onError?: (cause: Error) => void;
58
58
  /** Header value for a custom telemetry.dev integration. */
59
59
  sdkName?: string;
@@ -154,6 +154,8 @@ interface TelemetryClient {
154
154
  shutdown(): Promise<void>;
155
155
  }
156
156
  declare function captureEnabled(kind: "input" | "output"): boolean;
157
+ /** Whether the active client runs captured values through a mask; false before `init`. */
158
+ declare function captureMasked(): boolean;
157
159
  declare function flush(): Promise<void>;
158
160
  declare function shutdown(): Promise<void>;
159
161
  declare function init(options?: TelemetryOptions, overrides?: ClientOverrides): TelemetryClient;
@@ -189,6 +191,11 @@ interface StartSpanOptions extends SpanFields {
189
191
  /** Per-call override of the global captureOutput. */
190
192
  captureOutput?: boolean;
191
193
  }
194
+ interface SpanCapturePolicy {
195
+ readonly output: boolean;
196
+ readonly mask: boolean;
197
+ readonly maxAttributeLength: number;
198
+ }
192
199
  interface SpanHandle {
193
200
  /** Raw OTel span (interop escape hatch). */
194
201
  readonly span: Span;
@@ -199,9 +206,11 @@ interface SpanHandle {
199
206
  /** W3C traceparent for this span; null for the no-op handle. */
200
207
  readonly traceparent: string | null;
201
208
  readonly isRecording: boolean;
209
+ readonly capturePolicy?: SpanCapturePolicy;
202
210
  update(fields: SpanFields): SpanHandle;
203
211
  /** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
204
212
  recordOutputChunk(timestampMs?: number): void;
213
+ reportError?(cause: unknown): void;
205
214
  end(fields?: SpanFields & {
206
215
  endTime?: Date | number;
207
216
  }): void;
@@ -235,4 +244,4 @@ interface ObserveOptions extends StartSpanOptions {
235
244
  */
236
245
  declare function observe<TThis, TArgs extends unknown[], TReturn>(fn: (this: TThis, ...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
237
246
  //#endregion
238
- export { type AgentFields, type BatchOptions, type BoundedCaptureDetails, type BoundedCaptureOptions, type BoundedCaptureResult, type ClientOverrides, type ExportMode, type GenerationFields, type LogLevel, type LogOptions, type MaskFn, type ObserveOptions, type ParentRef, type PropagatedAttributes, SCOPE_NAME, type SdkLogLevel, type SpanFields, type SpanHandle, type SpanType, type StartSpanOptions, type TelemetryClient, type TelemetryOptions, TelemetrySpanProcessor, type TelemetrySpanProcessorOptions, type TokenUsage, type ToolFields, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
247
+ export { type AgentFields, type BatchOptions, type BoundedCaptureDetails, type BoundedCaptureOptions, type BoundedCaptureResult, type ClientOverrides, type ExportMode, type GenerationFields, type LogLevel, type LogOptions, type MaskFn, type ObserveOptions, type ParentRef, type PropagatedAttributes, SCOPE_NAME, type SdkLogLevel, type SpanCapturePolicy, type SpanFields, type SpanHandle, type SpanType, type StartSpanOptions, type TelemetryClient, type TelemetryOptions, TelemetrySpanProcessor, type TelemetrySpanProcessorOptions, type TokenUsage, type ToolFields, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, captureMasked, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
package/dist/index.mjs CHANGED
@@ -5,9 +5,13 @@ import { BatchLogRecordProcessor, LoggerProvider, SimpleLogRecordProcessor } fro
5
5
  import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
6
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";
7
7
  import { SeverityNumber } from "@opentelemetry/api-logs";
8
+ //#region src/config.ts
9
+ const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
8
10
  function resolveConfig(options = {}) {
9
11
  const env = resolveEnv();
10
12
  if (options.sessionMode !== void 0 && options.sessionMode !== "explicit" && options.sessionMode !== "process") throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
13
+ const maxAttributeLength = options.maxAttributeLength == null ? DEFAULT_MAX_ATTRIBUTE_LENGTH : options.maxAttributeLength;
14
+ if (maxAttributeLength !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(maxAttributeLength) || maxAttributeLength < 0)) throw new RangeError("maxAttributeLength must be a non-negative safe integer or Infinity");
11
15
  const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
12
16
  return {
13
17
  apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
@@ -22,7 +26,7 @@ function resolveConfig(options = {}) {
22
26
  captureInput: options.captureInput ?? true,
23
27
  captureOutput: options.captureOutput ?? true,
24
28
  mask: options.mask,
25
- maxAttributeLength: options.maxAttributeLength ?? 65536,
29
+ maxAttributeLength,
26
30
  batch: {
27
31
  ...DEFAULT_BATCH,
28
32
  ...options.batch
@@ -45,6 +49,13 @@ const NOOP_CLIENT = {
45
49
  shutdown: () => Promise.resolve()
46
50
  };
47
51
  let activeClient;
52
+ function shutdownClient(client, onError) {
53
+ try {
54
+ client.shutdown().catch((error) => reportError(onError, error));
55
+ } catch (error) {
56
+ reportError(onError, error instanceof Error ? error : new Error(String(error)));
57
+ }
58
+ }
48
59
  function currentClient() {
49
60
  return activeClient ?? NOOP_CLIENT;
50
61
  }
@@ -52,6 +63,10 @@ function captureEnabled(kind) {
52
63
  const config = currentClient().core?.config;
53
64
  return kind === "input" ? config?.captureInput ?? false : config?.captureOutput ?? false;
54
65
  }
66
+ /** Whether the active client runs captured values through a mask; false before `init`. */
67
+ function captureMasked() {
68
+ return currentClient().core?.config.mask !== void 0;
69
+ }
55
70
  function flush() {
56
71
  return currentClient().flush();
57
72
  }
@@ -59,20 +74,22 @@ function shutdown() {
59
74
  return currentClient().shutdown();
60
75
  }
61
76
  function init(options, overrides) {
77
+ const previousClient = activeClient;
62
78
  try {
79
+ setLogLevel(options?.logLevel ?? "warn");
63
80
  return initInner(options, overrides);
64
81
  } catch (error) {
65
82
  reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
66
- activeClient = NOOP_CLIENT;
83
+ if (previousClient && previousClient !== NOOP_CLIENT) shutdownClient(previousClient, options?.onError);
84
+ if (activeClient === previousClient) activeClient = NOOP_CLIENT;
67
85
  return NOOP_CLIENT;
68
86
  }
69
87
  }
70
88
  function initInner(options, overrides) {
71
89
  const config = resolveConfig(options);
72
- setLogLevel(config.logLevel);
73
90
  if (activeClient && activeClient !== NOOP_CLIENT) {
74
91
  diag.warn("init() called again; replacing the previous client");
75
- activeClient.shutdown().catch((error) => reportError(config.onError, error));
92
+ shutdownClient(activeClient, config.onError);
76
93
  }
77
94
  if (!(config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter))) {
78
95
  if (config.enabled && !config.apiKey) diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); telemetry disabled");
@@ -112,11 +129,12 @@ function initInner(options, overrides) {
112
129
  recordMetrics: metrics ? (span) => metrics.record(span) : void 0,
113
130
  onError: config.onError
114
131
  });
132
+ const backstopLength = config.maxAttributeLength || 65536;
115
133
  const provider = new BasicTracerProvider({
116
134
  resource,
117
135
  sampler: sessionSampler(config.sampler),
118
136
  spanProcessors: [processor],
119
- spanLimits: { attributeValueLengthLimit: config.maxAttributeLength }
137
+ spanLimits: { attributeValueLengthLimit: backstopLength }
120
138
  });
121
139
  const tracer = provider.getTracer(SCOPE_NAME$1, SCOPE_VERSION);
122
140
  let registeredTrace = false;
@@ -139,7 +157,7 @@ function initInner(options, overrides) {
139
157
  const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor(logExporter) : new BatchLogRecordProcessor(logExporter);
140
158
  const loggerProvider = new LoggerProvider({
141
159
  resource,
142
- logRecordLimits: { attributeValueLengthLimit: config.maxAttributeLength },
160
+ logRecordLimits: { attributeValueLengthLimit: backstopLength },
143
161
  processors: [logProcessor]
144
162
  });
145
163
  logs = {
@@ -295,7 +313,13 @@ function boundedCaptureDetails(input, options = {}) {
295
313
  const converted = visit(item, depth + 1, [...path, key]);
296
314
  if (converted === void 0) bytes = previousBytes;
297
315
  else {
298
- result[key] = converted;
316
+ if (key === "__proto__") Object.defineProperty(result, key, {
317
+ configurable: true,
318
+ enumerable: true,
319
+ value: converted,
320
+ writable: true
321
+ });
322
+ else result[key] = converted;
299
323
  emitted = true;
300
324
  }
301
325
  }
@@ -323,7 +347,9 @@ function boundedCapture(input, options = {}) {
323
347
  }
324
348
  function truncate(value, maxLength) {
325
349
  if (value.length <= maxLength) return value;
326
- return value.slice(0, Math.max(maxLength - 14, 0)) + TRUNCATION_MARKER;
350
+ const limit = Math.max(maxLength, 0);
351
+ const marker = TRUNCATION_MARKER.slice(0, limit);
352
+ return value.slice(0, limit - marker.length) + marker;
327
353
  }
328
354
  /** The single content funnel: mask → JSON stringify → truncate. */
329
355
  function prepareCaptureValue(key, value, cfg) {
@@ -506,8 +532,14 @@ const NOOP_SPAN_HANDLE = {
506
532
  spanId: INVALID_SPAN_CONTEXT.spanId,
507
533
  traceparent: null,
508
534
  isRecording: false,
535
+ capturePolicy: {
536
+ output: false,
537
+ mask: false,
538
+ maxAttributeLength: 0
539
+ },
509
540
  update: () => NOOP_SPAN_HANDLE,
510
541
  recordOutputChunk: () => {},
542
+ reportError: () => {},
511
543
  end: () => {}
512
544
  };
513
545
  //#endregion
@@ -609,6 +641,11 @@ function createSpanHandle(core, name, options = {}) {
609
641
  traceId: spanContext.traceId,
610
642
  spanId: spanContext.spanId,
611
643
  traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`,
644
+ capturePolicy: {
645
+ output: meta.capture.captureOutput,
646
+ mask: meta.capture.mask !== void 0,
647
+ maxAttributeLength: meta.capture.maxAttributeLength
648
+ },
612
649
  get isRecording() {
613
650
  return span.isRecording();
614
651
  },
@@ -642,6 +679,9 @@ function createSpanHandle(core, name, options = {}) {
642
679
  }
643
680
  previousOutputChunkTime = now;
644
681
  },
682
+ reportError(cause) {
683
+ reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
684
+ },
645
685
  end(fields) {
646
686
  try {
647
687
  if (fields) applyFields(span, meta, fields);
@@ -769,4 +809,4 @@ function observe(fn, options) {
769
809
  return wrapped;
770
810
  }
771
811
  //#endregion
772
- export { SCOPE_NAME, TelemetrySpanProcessor, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
812
+ export { SCOPE_NAME, TelemetrySpanProcessor, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, captureMasked, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
package/src/capture.ts CHANGED
@@ -168,7 +168,7 @@ export function boundedCaptureDetails(
168
168
  return result;
169
169
  }
170
170
 
171
- const result: CapturedObject = {};
171
+ const result = {} as CapturedObject;
172
172
  let emitted = false;
173
173
 
174
174
  for (const key in value) {
@@ -213,7 +213,17 @@ export function boundedCaptureDetails(
213
213
  if (converted === undefined) {
214
214
  bytes = previousBytes;
215
215
  } else {
216
- result[key] = converted;
216
+ if (key === "__proto__") {
217
+ Object.defineProperty(result, key, {
218
+ configurable: true,
219
+ enumerable: true,
220
+ value: converted,
221
+ writable: true,
222
+ });
223
+ } else {
224
+ result[key] = converted;
225
+ }
226
+
217
227
  emitted = true;
218
228
  }
219
229
  }
@@ -243,9 +253,10 @@ export function boundedCapture(
243
253
  export function truncate(value: string, maxLength: number): string {
244
254
  if (value.length <= maxLength) return value;
245
255
 
246
- // Total stays within maxLength so the provider's attributeValueLengthLimit backstop
247
- // (set to the same cap) never slices the marker off.
248
- return value.slice(0, Math.max(maxLength - TRUNCATION_MARKER.length, 0)) + TRUNCATION_MARKER;
256
+ const limit = Math.max(maxLength, 0);
257
+ const marker = TRUNCATION_MARKER.slice(0, limit);
258
+
259
+ return value.slice(0, limit - marker.length) + marker;
249
260
  }
250
261
 
251
262
  /** The single content funnel: mask → JSON stringify → truncate. */
package/src/client.ts CHANGED
@@ -36,7 +36,12 @@ import {
36
36
  type Transport,
37
37
  } from "@telemetry-dev/otel";
38
38
 
39
- import { type ResolvedConfig, resolveConfig, type TelemetryOptions } from "./config.ts";
39
+ import {
40
+ DEFAULT_MAX_ATTRIBUTE_LENGTH,
41
+ type ResolvedConfig,
42
+ resolveConfig,
43
+ type TelemetryOptions,
44
+ } from "./config.ts";
40
45
 
41
46
  /** Test seam: inject in-memory exporters instead of the OTLP fetch transport. */
42
47
  export interface ClientOverrides {
@@ -71,6 +76,14 @@ const NOOP_CLIENT: ClientHandle = {
71
76
 
72
77
  let activeClient: ClientHandle | undefined;
73
78
 
79
+ function shutdownClient(client: ClientHandle, onError?: (cause: Error) => void): void {
80
+ try {
81
+ void client.shutdown().catch((error: Error) => reportError(onError, error));
82
+ } catch (error) {
83
+ reportError(onError, error instanceof Error ? error : new Error(String(error)));
84
+ }
85
+ }
86
+
74
87
  export function currentClient(): ClientHandle {
75
88
  return activeClient ?? NOOP_CLIENT;
76
89
  }
@@ -81,6 +94,11 @@ export function captureEnabled(kind: "input" | "output"): boolean {
81
94
  return kind === "input" ? (config?.captureInput ?? false) : (config?.captureOutput ?? false);
82
95
  }
83
96
 
97
+ /** Whether the active client runs captured values through a mask; false before `init`. */
98
+ export function captureMasked(): boolean {
99
+ return currentClient().core?.config.mask !== undefined;
100
+ }
101
+
84
102
  export function flush(): Promise<void> {
85
103
  return currentClient().flush();
86
104
  }
@@ -90,11 +108,20 @@ export function shutdown(): Promise<void> {
90
108
  }
91
109
 
92
110
  export function init(options?: TelemetryOptions, overrides?: ClientOverrides): TelemetryClient {
111
+ const previousClient = activeClient;
112
+
93
113
  try {
114
+ setLogLevel(options?.logLevel ?? "warn");
115
+
94
116
  return initInner(options, overrides);
95
117
  } catch (error) {
96
118
  reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
97
- activeClient = NOOP_CLIENT;
119
+
120
+ if (previousClient && previousClient !== NOOP_CLIENT) {
121
+ shutdownClient(previousClient, options?.onError);
122
+ }
123
+
124
+ if (activeClient === previousClient) activeClient = NOOP_CLIENT;
98
125
 
99
126
  return NOOP_CLIENT;
100
127
  }
@@ -102,12 +129,11 @@ export function init(options?: TelemetryOptions, overrides?: ClientOverrides): T
102
129
 
103
130
  function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): TelemetryClient {
104
131
  const config = resolveConfig(options);
105
- setLogLevel(config.logLevel);
106
132
 
107
133
  if (activeClient && activeClient !== NOOP_CLIENT) {
108
134
  diag.warn("init() called again; replacing the previous client");
109
135
  // shutdown() synchronously releases global registrations before this init claims them.
110
- void activeClient.shutdown().catch((error: Error) => reportError(config.onError, error));
136
+ shutdownClient(activeClient, config.onError);
111
137
  }
112
138
 
113
139
  const enabled = config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter);
@@ -167,11 +193,14 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
167
193
  onError: config.onError,
168
194
  });
169
195
 
196
+ // A zero cap only drops content; OpenTelemetry rejects it as a provider limit.
197
+ const backstopLength = config.maxAttributeLength || DEFAULT_MAX_ATTRIBUTE_LENGTH;
198
+
170
199
  const provider = new BasicTracerProvider({
171
200
  resource,
172
201
  sampler: sessionSampler(config.sampler),
173
202
  spanProcessors: [processor],
174
- spanLimits: { attributeValueLengthLimit: config.maxAttributeLength },
203
+ spanLimits: { attributeValueLengthLimit: backstopLength },
175
204
  });
176
205
 
177
206
  const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
@@ -220,7 +249,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
220
249
 
221
250
  const loggerProvider = new LoggerProvider({
222
251
  resource,
223
- logRecordLimits: { attributeValueLengthLimit: config.maxAttributeLength },
252
+ logRecordLimits: { attributeValueLengthLimit: backstopLength },
224
253
  processors: [logProcessor],
225
254
  });
226
255
 
package/src/config.ts CHANGED
@@ -40,7 +40,7 @@ export interface TelemetryOptions {
40
40
  captureOutput?: boolean;
41
41
  /** Redaction hook run on captured content before stringify/truncate. */
42
42
  mask?: MaskFn;
43
- /** Truncation cap (chars) for content attributes; also set as the provider's attributeValueLengthLimit. Default 65536. */
43
+ /** Non-negative safe-integer truncation cap for content attributes; Infinity disables truncation. The cap also applies, with the marker, to string metadata and log attributes; any other string attribute is cut at the cap without a marker, except that a cap of 0 leaves those at 65536. init() reports invalid values through onError and returns a disabled client. Default 65536. */
44
44
  maxAttributeLength?: number;
45
45
  batch?: BatchOptions;
46
46
  /** Export filter. Default when registerGlobal: only spans from this SDK's instrumentation scope. */
@@ -59,7 +59,7 @@ export interface TelemetryOptions {
59
59
  fetch?: typeof fetch;
60
60
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
61
61
  waitUntil?: (p: Promise<unknown>) => void;
62
- /** Receives any error raised while emitting telemetry; the SDK never throws into user code. */
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. */
63
63
  onError?: (cause: Error) => void;
64
64
  /** Header value for a custom telemetry.dev integration. */
65
65
  sdkName?: string;
@@ -104,6 +104,16 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
104
104
  throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
105
105
  }
106
106
 
107
+ const maxAttributeLength =
108
+ options.maxAttributeLength == null ? DEFAULT_MAX_ATTRIBUTE_LENGTH : options.maxAttributeLength;
109
+
110
+ if (
111
+ maxAttributeLength !== Number.POSITIVE_INFINITY &&
112
+ (!Number.isSafeInteger(maxAttributeLength) || maxAttributeLength < 0)
113
+ ) {
114
+ throw new RangeError("maxAttributeLength must be a non-negative safe integer or Infinity");
115
+ }
116
+
107
117
  const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(
108
118
  /\/+$/,
109
119
  "",
@@ -122,7 +132,7 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
122
132
  captureInput: options.captureInput ?? true,
123
133
  captureOutput: options.captureOutput ?? true,
124
134
  mask: options.mask,
125
- maxAttributeLength: options.maxAttributeLength ?? DEFAULT_MAX_ATTRIBUTE_LENGTH,
135
+ maxAttributeLength,
126
136
  batch: { ...DEFAULT_BATCH, ...options.batch },
127
137
  spanFilter: options.spanFilter,
128
138
  sessionRootOf: options.sessionRootOf,
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
 
10
10
  export {
11
11
  captureEnabled,
12
+ captureMasked,
12
13
  type ClientOverrides,
13
14
  flush,
14
15
  init,
@@ -51,6 +52,7 @@ export {
51
52
  getTraceparent,
52
53
  injectW3cContext,
53
54
  type ParentRef,
55
+ type SpanCapturePolicy,
54
56
  type SpanHandle,
55
57
  startActiveSpan,
56
58
  startSpan,
package/src/noop.ts CHANGED
@@ -12,7 +12,9 @@ export const NOOP_SPAN_HANDLE: SpanHandle = {
12
12
  spanId: INVALID_SPAN_CONTEXT.spanId,
13
13
  traceparent: null,
14
14
  isRecording: false,
15
+ capturePolicy: { output: false, mask: false, maxAttributeLength: 0 },
15
16
  update: () => NOOP_SPAN_HANDLE,
16
17
  recordOutputChunk: () => {},
18
+ reportError: () => {},
17
19
  end: () => {},
18
20
  };
package/src/span.ts CHANGED
@@ -59,6 +59,12 @@ export interface StartSpanOptions extends SpanFields {
59
59
  captureOutput?: boolean;
60
60
  }
61
61
 
62
+ export interface SpanCapturePolicy {
63
+ readonly output: boolean;
64
+ readonly mask: boolean;
65
+ readonly maxAttributeLength: number;
66
+ }
67
+
62
68
  export interface SpanHandle {
63
69
  /** Raw OTel span (interop escape hatch). */
64
70
  readonly span: Span;
@@ -69,9 +75,11 @@ export interface SpanHandle {
69
75
  /** W3C traceparent for this span; null for the no-op handle. */
70
76
  readonly traceparent: string | null;
71
77
  readonly isRecording: boolean;
78
+ readonly capturePolicy?: SpanCapturePolicy;
72
79
  update(fields: SpanFields): SpanHandle;
73
80
  /** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
74
81
  recordOutputChunk(timestampMs?: number): void;
82
+ reportError?(cause: unknown): void;
75
83
  end(fields?: SpanFields & { endTime?: Date | number }): void;
76
84
  }
77
85
 
@@ -240,6 +248,11 @@ export function createSpanHandle(
240
248
  traceId: spanContext.traceId,
241
249
  spanId: spanContext.spanId,
242
250
  traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`,
251
+ capturePolicy: {
252
+ output: meta.capture.captureOutput,
253
+ mask: meta.capture.mask !== undefined,
254
+ maxAttributeLength: meta.capture.maxAttributeLength,
255
+ },
243
256
  get isRecording() {
244
257
  return span.isRecording();
245
258
  },
@@ -287,6 +300,9 @@ export function createSpanHandle(
287
300
 
288
301
  previousOutputChunkTime = now;
289
302
  },
303
+ reportError(cause) {
304
+ reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
305
+ },
290
306
  end(fields) {
291
307
  try {
292
308
  if (fields) applyFields(span, meta, fields);