@telemetry-dev/sdk 0.1.9 → 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
@@ -78,16 +78,27 @@ other string attribute is cut at the cap without a marker, except that a cap of
78
78
  `exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
79
79
  default `"warn"`), `fetch`, `waitUntil`, `onError`.
80
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
+
81
91
  `captureInput: false` and `captureOutput: false` omit captured content. Provider integrations may
82
92
  still record non-content metadata such as response IDs and models, finish reasons, usage, timing,
83
- 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,
84
- 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.
85
96
 
86
97
  `mask` receives input and output content, system instructions, log messages, log attributes
87
98
  other than numbers and booleans, and content that integrations pass to
88
- `handle.serializeContent`. `ctx.key` names the attribute being written. It does not receive
89
- span or propagated `metadata`, raw `attributes`, or exception messages and stack traces; redact
90
- those separately.
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.
91
102
 
92
103
  Root spans remain independent by default. `init({ sessionMode: "process" })` groups otherwise
93
104
  uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK initialization.
package/dist/index.d.mts CHANGED
@@ -7,8 +7,8 @@ import { PushMetricExporter } from "@opentelemetry/sdk-metrics";
7
7
  //#region src/config.d.ts
8
8
  /**
9
9
  * Redaction hook applied to captured content (input/output, log messages and attributes, and
10
- * content passed to `SpanHandle.serializeContent`) before JSON stringification. Not applied to
11
- * metadata or raw attributes.
10
+ * content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
11
+ * JSON stringification. Not applied to metadata or raw attributes.
12
12
  */
13
13
  type MaskFn<T = unknown> = (value: T, ctx: {
14
14
  key: string;
@@ -32,9 +32,9 @@ interface TelemetryOptions {
32
32
  exportMode?: ExportMode$1;
33
33
  /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
34
34
  sampler?: Sampler;
35
- /** Capture span inputs (args, prompts) by default. Default true. */
35
+ /** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
36
36
  captureInput?: boolean;
37
- /** Capture span outputs (results, completions) by default. Default true. */
37
+ /** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
38
38
  captureOutput?: boolean;
39
39
  /** Redaction hook run on captured content before stringify/truncate. */
40
40
  mask?: MaskFn;
@@ -57,7 +57,7 @@ interface TelemetryOptions {
57
57
  fetch?: typeof fetch;
58
58
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
59
59
  waitUntil?: (p: Promise<unknown>) => void;
60
- /** 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. */
61
61
  onError?: (cause: Error) => void;
62
62
  /** Header value for a custom telemetry.dev integration. */
63
63
  sdkName?: string;
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);
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);
@@ -577,16 +579,16 @@ function resolveParent(parent) {
577
579
  if (isContext(ref)) return withAmbientPropagated(ref);
578
580
  return trace.setSpanContext(base, ref);
579
581
  }
580
- function applyError(span, error) {
582
+ function applyError(span, error, capture) {
581
583
  const err = error instanceof Error ? error : void 0;
582
584
  const errorType = err === void 0 ? "Error" : err.name !== "Error" && err.name.length > 0 ? err.name : (err.constructor?.name ?? "Error") || "Error";
583
- const message = err?.message ?? String(error);
584
585
  span.setStatus({ code: SpanStatusCode.ERROR });
585
586
  span.setAttribute("error.type", errorType);
587
+ const captureContent = capture.captureInput && capture.captureOutput;
586
588
  span.addEvent("exception", omitUndefined({
587
589
  "exception.type": errorType,
588
- "exception.message": message,
589
- "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,
590
592
  "log.severity_number": SEVERITY_ERROR
591
593
  }));
592
594
  }
@@ -594,7 +596,7 @@ function applyFields(span, meta, fields) {
594
596
  if (fields.name) span.updateName(fields.name);
595
597
  const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
596
598
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
597
- if (fields.error !== void 0) applyError(span, fields.error);
599
+ if (fields.error !== void 0) applyError(span, fields.error, meta.capture);
598
600
  }
599
601
  function metaFor(core, options) {
600
602
  const cfg = core.config;
@@ -632,7 +634,7 @@ function createSpanHandle(core, name, options = {}) {
632
634
  SPAN_META.set(span, meta);
633
635
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
634
636
  if (options.name) span.updateName(options.name);
635
- if (options.error !== void 0) applyError(span, options.error);
637
+ if (options.error !== void 0) applyError(span, options.error, meta.capture);
636
638
  const spanContext = span.spanContext();
637
639
  let previousOutputChunkTime;
638
640
  const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/sdk",
3
- "version": "0.1.9",
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.6"
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);
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
@@ -14,8 +14,8 @@ export type { BatchOptions, ExportMode, LogLevel, SdkLogLevel } from "@telemetry
14
14
 
15
15
  /**
16
16
  * Redaction hook applied to captured content (input/output, log messages and attributes, and
17
- * content passed to `SpanHandle.serializeContent`) before JSON stringification. Not applied to
18
- * metadata or raw attributes.
17
+ * content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
18
+ * JSON stringification. Not applied to metadata or raw attributes.
19
19
  */
20
20
  export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
21
21
 
@@ -38,9 +38,9 @@ export interface TelemetryOptions {
38
38
  exportMode?: ExportMode;
39
39
  /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
40
40
  sampler?: Sampler;
41
- /** Capture span inputs (args, prompts) by default. Default true. */
41
+ /** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
42
42
  captureInput?: boolean;
43
- /** Capture span outputs (results, completions) by default. Default true. */
43
+ /** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
44
44
  captureOutput?: boolean;
45
45
  /** Redaction hook run on captured content before stringify/truncate. */
46
46
  mask?: MaskFn;
@@ -63,7 +63,7 @@ export interface TelemetryOptions {
63
63
  fetch?: typeof fetch;
64
64
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
65
65
  waitUntil?: (p: Promise<unknown>) => void;
66
- /** 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. */
67
67
  onError?: (cause: Error) => void;
68
68
  /** Header value for a custom telemetry.dev integration. */
69
69
  sdkName?: string;
package/src/span.ts CHANGED
@@ -153,7 +153,7 @@ function resolveParent(parent: ParentRef | undefined): Context {
153
153
  return trace.setSpanContext(base, ref);
154
154
  }
155
155
 
156
- function applyError(span: Span, error: unknown): void {
156
+ function applyError(span: Span, error: unknown, capture: FieldCaptureConfig): void {
157
157
  const err = error instanceof Error ? error : undefined;
158
158
 
159
159
  // Prefer the specific subclass name (Python parity: type(error).__name__) — many SDK
@@ -165,15 +165,21 @@ function applyError(span: Span, error: unknown): void {
165
165
  ? err.name
166
166
  : (err.constructor?.name ?? "Error") || "Error";
167
167
 
168
- const message = err?.message ?? String(error);
169
168
  span.setStatus({ code: SpanStatusCode.ERROR });
170
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;
171
172
  span.addEvent(
172
173
  "exception",
173
174
  omitUndefined({
174
175
  "exception.type": errorType,
175
- "exception.message": message,
176
- "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,
177
183
  "log.severity_number": SEVERITY_ERROR,
178
184
  }),
179
185
  );
@@ -185,7 +191,7 @@ function applyFields(span: Span, meta: SpanMeta, fields: SpanFields): void {
185
191
 
186
192
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
187
193
 
188
- if (fields.error !== undefined) applyError(span, fields.error);
194
+ if (fields.error !== undefined) applyError(span, fields.error, meta.capture);
189
195
  }
190
196
 
191
197
  function metaFor(core: ClientCore, options?: StartSpanOptions): SpanMeta {
@@ -246,7 +252,7 @@ export function createSpanHandle(
246
252
 
247
253
  if (options.name) span.updateName(options.name);
248
254
 
249
- if (options.error !== undefined) applyError(span, options.error);
255
+ if (options.error !== undefined) applyError(span, options.error, meta.capture);
250
256
 
251
257
  const spanContext = span.spanContext();
252
258
  let previousOutputChunkTime: number | undefined;