@telemetry-dev/sdk 0.1.0 → 0.1.1

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
@@ -74,6 +74,10 @@ default `"warn"`), `fetch`, `waitUntil`, `onError`.
74
74
  - `log(message, { level, eventName, attributes }?)` — log record correlated to the active trace.
75
75
  - `getTraceparent()` — W3C traceparent of the active span (pass to other services; accept it via
76
76
  `startSpan(name, { parent })`).
77
+ - `activeContext()` / `withContext(context, fn)` — read or temporarily activate an OpenTelemetry
78
+ context.
79
+ - `extractW3cContext(carrier, options?)` / `injectW3cContext(context, carrier, options?)` — extract
80
+ or inject W3C trace context. Baggage is omitted unless `includeBaggage` is true.
77
81
  - `flush()` / `shutdown()` — export pending data; required before serverless freeze/exit.
78
82
 
79
83
  Span types: `span` (default) | `generation` | `tool` | `agent` | `embedding` — mapped to
package/dist/index.d.mts CHANGED
@@ -1,14 +1,14 @@
1
- import { AttributeValue, Context, Span, SpanContext } from "@opentelemetry/api";
1
+ import { AttributeValue, Context, Link, Span, SpanContext, SpanKind } from "@opentelemetry/api";
2
2
  import { LogRecordExporter } from "@opentelemetry/sdk-logs";
3
3
  import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
4
- import { BatchOptions, BatchOptions as BatchOptions$1, ExportMode, ExportMode as ExportMode$1, LogLevel, PropagatedAttributes, SdkLogLevel, SdkLogLevel as SdkLogLevel$1, propagateAttributes } from "@telemetry-dev/otel";
4
+ import { BatchOptions, BatchOptions as BatchOptions$1, ExportMode, ExportMode as ExportMode$1, LogLevel, PropagatedAttributes, SdkLogLevel, SdkLogLevel as SdkLogLevel$1, activeContext, propagateAttributes, withContext } from "@telemetry-dev/otel";
5
5
  import { PushMetricExporter } from "@opentelemetry/sdk-metrics";
6
6
 
7
7
  //#region src/config.d.ts
8
8
  /** Redaction hook applied to captured content (input/output/log message) before JSON stringification. */
9
- type MaskFn = (value: unknown, ctx: {
9
+ type MaskFn<T = unknown> = (value: T, ctx: {
10
10
  key: string;
11
- }) => unknown;
11
+ }) => T;
12
12
  interface TelemetryOptions {
13
13
  /** telemetry.dev ingest key (`td_live_…`). Falls back to `TELEMETRY_DEV_API_KEY`. When absent the SDK is a no-op. */
14
14
  apiKey?: string;
@@ -44,7 +44,9 @@ interface TelemetryOptions {
44
44
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
45
45
  waitUntil?: (p: Promise<unknown>) => void;
46
46
  /** Receives any error raised while emitting telemetry; the SDK never throws into user code. */
47
- onError?: (error: unknown) => void;
47
+ onError?: (cause: Error) => void;
48
+ /** Header value for a custom telemetry.dev integration. */
49
+ sdkName?: string;
48
50
  }
49
51
  //#endregion
50
52
  //#region src/attrs.d.ts
@@ -60,7 +62,7 @@ interface TokenUsage {
60
62
  interface GenerationFields {
61
63
  model?: string;
62
64
  provider?: string;
63
- systemInstructions?: unknown;
65
+ systemInstructions?: Parameters<MaskFn>[0];
64
66
  responseModel?: string;
65
67
  responseId?: string;
66
68
  usage?: TokenUsage;
@@ -90,12 +92,12 @@ interface AgentFields {
90
92
  interface SpanFields extends GenerationFields, ToolFields, AgentFields {
91
93
  /** Rename the span (update only). */
92
94
  name?: string;
93
- input?: unknown;
94
- output?: unknown;
95
+ input?: Parameters<MaskFn>[0];
96
+ output?: Parameters<MaskFn>[0];
95
97
  /** Marks the span failed: status ERROR + error.type + an "exception" event. */
96
98
  error?: unknown;
97
99
  /** td.metadata.<key> on this span only; use propagateAttributes for scope-wide metadata. */
98
- metadata?: Record<string, unknown>;
100
+ metadata?: Record<string, Parameters<MaskFn>[0]>;
99
101
  /** Raw attribute escape hatch, merged last. */
100
102
  attributes?: Record<string, AttributeValue>;
101
103
  }
@@ -122,7 +124,7 @@ declare function init(options?: TelemetryOptions, overrides?: ClientOverrides):
122
124
  interface LogOptions {
123
125
  /** Default "info". */
124
126
  level?: LogLevel;
125
- attributes?: Record<string, unknown>;
127
+ attributes?: Record<string, Parameters<MaskFn>[0]>;
126
128
  eventName?: string;
127
129
  timestamp?: Date | number;
128
130
  }
@@ -137,8 +139,12 @@ type ParentRef = string | SpanHandle | SpanContext | Context;
137
139
  interface StartSpanOptions extends SpanFields {
138
140
  /** Default "span" → operation "function". */
139
141
  type?: SpanType;
142
+ /** OpenTelemetry span kind; defaults to INTERNAL. */
143
+ kind?: SpanKind;
140
144
  /** Parent override; defaults to the active context. */
141
145
  parent?: ParentRef;
146
+ /** Links to associate with the span. */
147
+ links?: Link[];
142
148
  startTime?: Date | number;
143
149
  /** Per-call override of the global captureInput. */
144
150
  captureInput?: boolean;
@@ -166,6 +172,14 @@ declare function startActiveSpan<T>(name: string, fn: (span: SpanHandle) => T):
166
172
  declare function startActiveSpan<T>(name: string, options: StartSpanOptions, fn: (span: SpanHandle) => T): T;
167
173
  /** Update the innermost active span; no-op when none is recording. */
168
174
  declare function updateActiveSpan(fields: SpanFields): void;
175
+ /** Extract W3C trace context; baggage is omitted unless includeBaggage is true. */
176
+ declare function extractW3cContext(carrier: Record<string, unknown>, options?: {
177
+ includeBaggage?: boolean;
178
+ }): Context;
179
+ /** Inject W3C trace context; baggage is omitted unless includeBaggage is true. */
180
+ declare function injectW3cContext(context: Context, carrier: Record<string, unknown>, options?: {
181
+ includeBaggage?: boolean;
182
+ }): void;
169
183
  /** Serialize the active span context as a W3C traceparent; null when there is none. */
170
184
  declare function getTraceparent(): string | null;
171
185
  //#endregion
@@ -179,6 +193,6 @@ interface ObserveOptions extends StartSpanOptions {
179
193
  * output, thrown/rejected errors captured and rethrown. Activates context so nested SDK calls
180
194
  * parent to it. Resolves the client at call time, so wrapping at module load before init() works.
181
195
  */
182
- declare function observe<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
196
+ declare function observe<TThis, TArgs extends unknown[], TReturn>(fn: (this: TThis, ...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
183
197
  //#endregion
184
- export { type AgentFields, type BatchOptions, type ClientOverrides, type ExportMode, type GenerationFields, type LogLevel, type LogOptions, type MaskFn, type ObserveOptions, type ParentRef, type PropagatedAttributes, type SdkLogLevel, type SpanFields, type SpanHandle, type SpanType, type StartSpanOptions, type TelemetryClient, type TelemetryOptions, type TokenUsage, type ToolFields, flush, getTraceparent, init, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan };
198
+ export { type AgentFields, type BatchOptions, type ClientOverrides, type ExportMode, type GenerationFields, type LogLevel, type LogOptions, type MaskFn, type ObserveOptions, type ParentRef, type PropagatedAttributes, type SdkLogLevel, type SpanFields, type SpanHandle, type SpanType, type StartSpanOptions, type TelemetryClient, type TelemetryOptions, type TokenUsage, type ToolFields, activeContext, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, context, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api";
2
- import { W3CTraceContextPropagator, parseTraceParent } from "@opentelemetry/core";
1
+ import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, context, defaultTextMapGetter, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api";
2
+ import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator, parseTraceParent } from "@opentelemetry/core";
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, PROPAGATED_KEY, SCOPE_NAME, SCOPE_VERSION, StampingSpanProcessor, activeContext, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, setLogLevel, withContext } from "@telemetry-dev/otel";
6
+ import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, PROPAGATED_KEY, SCOPE_NAME, SCOPE_VERSION, StampingSpanProcessor, activeContext, activeContext as activeContext$1, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, setLogLevel, withContext, withContext as withContext$1 } from "@telemetry-dev/otel";
7
7
  import { SeverityNumber } from "@opentelemetry/api-logs";
8
8
  function resolveConfig(options = {}) {
9
9
  const env = resolveEnv();
@@ -29,7 +29,8 @@ function resolveConfig(options = {}) {
29
29
  logLevel: options.logLevel ?? "warn",
30
30
  fetchImpl: options.fetch ?? globalThis.fetch,
31
31
  waitUntil: options.waitUntil,
32
- onError: options.onError
32
+ onError: options.onError,
33
+ sdkName: options.sdkName ?? "@telemetry-dev/sdk"
33
34
  };
34
35
  }
35
36
  //#endregion
@@ -53,7 +54,7 @@ function init(options, overrides) {
53
54
  try {
54
55
  return initInner(options, overrides);
55
56
  } catch (error) {
56
- reportError(options?.onError, error);
57
+ reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
57
58
  activeClient = NOOP_CLIENT;
58
59
  return NOOP_CLIENT;
59
60
  }
@@ -79,7 +80,7 @@ function initInner(options, overrides) {
79
80
  fetchImpl: config.fetchImpl,
80
81
  onError: config.onError
81
82
  };
82
- const headers = config.apiKey ? otlpHeaders(config.apiKey) : void 0;
83
+ const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : void 0;
83
84
  const spanExporter = overrides?.spanExporter ?? createTraceExporter({
84
85
  url: `${config.baseUrl}/v1/traces`,
85
86
  headers
@@ -188,7 +189,7 @@ function prepareCaptureValue(key, value, cfg) {
188
189
  if (cfg.mask) try {
189
190
  masked = cfg.mask(value, { key });
190
191
  } catch (error) {
191
- reportError(cfg.onError, error);
192
+ reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
192
193
  return;
193
194
  }
194
195
  const serialized = jsonAttr(masked);
@@ -232,7 +233,7 @@ function log(message, options) {
232
233
  onError: cfg.onError
233
234
  };
234
235
  const severity = SEVERITIES[options?.level ?? "info"];
235
- const ctx = activeContext();
236
+ const ctx = activeContext$1();
236
237
  const attributes = {};
237
238
  for (const [key, value] of Object.entries(options?.attributes ?? {})) {
238
239
  if (value === void 0) continue;
@@ -245,7 +246,7 @@ function log(message, options) {
245
246
  }
246
247
  const propagated = propagatedFromContext(ctx);
247
248
  if (propagated) {
248
- for (const [key, value] of Object.entries(propagated)) if (value !== void 0) attributes[key] = typeof value === "string" ? truncate(value, cfg.maxAttributeLength) : value;
249
+ for (const [key, value] of Object.entries(propagated)) if (value !== void 0) attributes[key] = value.constructor === String ? truncate(String(value), cfg.maxAttributeLength) : value;
249
250
  }
250
251
  logger.emit({
251
252
  body: prepareCaptureValue("log.message", message, captureCfg) ?? "",
@@ -257,7 +258,7 @@ function log(message, options) {
257
258
  attributes
258
259
  });
259
260
  } catch (error) {
260
- reportError(core.config.onError, error);
261
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
261
262
  }
262
263
  }
263
264
  //#endregion
@@ -379,13 +380,14 @@ function resolveParent(parent) {
379
380
  isRemote: true
380
381
  });
381
382
  }
382
- if (isSpanHandle(parent)) return withAmbientPropagated(parent.context);
383
- if (isContext(parent)) return withAmbientPropagated(parent);
384
- return trace.setSpanContext(base, parent);
383
+ const ref = parent;
384
+ if (isSpanHandle(ref)) return withAmbientPropagated(ref.context);
385
+ if (isContext(ref)) return withAmbientPropagated(ref);
386
+ return trace.setSpanContext(base, ref);
385
387
  }
386
388
  function applyError(span, error) {
387
389
  const err = error instanceof Error ? error : void 0;
388
- const errorType = err?.name ?? "Error";
390
+ const errorType = err === void 0 ? "Error" : err.name !== "Error" && err.name.length > 0 ? err.name : (err.constructor?.name ?? "Error") || "Error";
389
391
  const message = err?.message ?? String(error);
390
392
  span.setStatus({ code: SpanStatusCode.ERROR });
391
393
  span.setAttribute("error.type", errorType);
@@ -420,8 +422,9 @@ function createSpanHandle(core, name, options = {}) {
420
422
  const meta = metaFor(core, options);
421
423
  const parentCtx = resolveParent(options.parent);
422
424
  const span = core.tracer.startSpan(name, {
423
- kind: SpanKind.INTERNAL,
425
+ kind: options.kind ?? SpanKind.INTERNAL,
424
426
  startTime: options.startTime,
427
+ links: options.links,
425
428
  attributes: { "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type] }
426
429
  }, parentCtx);
427
430
  SPAN_META.set(span, meta);
@@ -441,7 +444,7 @@ function createSpanHandle(core, name, options = {}) {
441
444
  try {
442
445
  applyFields(span, meta, fields);
443
446
  } catch (error) {
444
- reportError(meta.onError, error);
447
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
445
448
  }
446
449
  return handle;
447
450
  },
@@ -449,7 +452,7 @@ function createSpanHandle(core, name, options = {}) {
449
452
  try {
450
453
  if (fields) applyFields(span, meta, fields);
451
454
  } catch (error) {
452
- reportError(meta.onError, error);
455
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
453
456
  }
454
457
  span.end(fields?.endTime);
455
458
  }
@@ -463,7 +466,7 @@ function startSpan(name, options) {
463
466
  try {
464
467
  return createSpanHandle(core, name, options);
465
468
  } catch (error) {
466
- reportError(core.config.onError, error);
469
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
467
470
  return NOOP_SPAN_HANDLE;
468
471
  }
469
472
  }
@@ -485,8 +488,9 @@ function runWithHandle(handle, fn) {
485
488
  }
486
489
  }
487
490
  function startActiveSpan(name, optionsOrFn, maybeFn) {
488
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
489
- const handle = startSpan(name, typeof optionsOrFn === "function" ? void 0 : optionsOrFn);
491
+ const isFn = typeof optionsOrFn === "function";
492
+ const fn = isFn ? optionsOrFn : maybeFn;
493
+ const handle = startSpan(name, isFn ? void 0 : optionsOrFn);
490
494
  if (!handle.isRecording) return fn(handle);
491
495
  return runWithHandle(handle, fn);
492
496
  }
@@ -499,14 +503,28 @@ function updateActiveSpan(fields) {
499
503
  if (!span?.isRecording()) return;
500
504
  applyFields(span, SPAN_META.get(span) ?? metaFor(core), fields);
501
505
  } catch (error) {
502
- reportError(core.config.onError, error);
506
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
503
507
  }
504
508
  }
505
- const PROPAGATOR = new W3CTraceContextPropagator();
509
+ const TRACE_CONTEXT_PROPAGATOR = new W3CTraceContextPropagator();
510
+ const W3C_PROPAGATOR = new CompositePropagator({ propagators: [TRACE_CONTEXT_PROPAGATOR, new W3CBaggagePropagator()] });
511
+ /** Extract W3C trace context; baggage is omitted unless includeBaggage is true. */
512
+ function extractW3cContext(carrier, options = {}) {
513
+ return (options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR).extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
514
+ }
515
+ /** Inject W3C trace context; baggage is omitted unless includeBaggage is true. */
516
+ function injectW3cContext(context, carrier, options = {}) {
517
+ const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
518
+ for (const key of Object.keys(carrier)) {
519
+ const normalized = key.toLowerCase();
520
+ if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") delete carrier[key];
521
+ }
522
+ propagator.inject(context, carrier, defaultTextMapSetter);
523
+ }
506
524
  /** Serialize the active span context as a W3C traceparent; null when there is none. */
507
525
  function getTraceparent() {
508
- const carrier = {};
509
- PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
526
+ const carrier = { traceparent: void 0 };
527
+ TRACE_CONTEXT_PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
510
528
  return carrier.traceparent ?? null;
511
529
  }
512
530
  //#endregion
@@ -529,22 +547,22 @@ function observe(fn, options) {
529
547
  input
530
548
  });
531
549
  } catch (error) {
532
- reportError(core.config.onError, error);
550
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
533
551
  return fn.apply(this, args);
534
552
  }
535
553
  try {
536
- const result = withContext(handle.context, () => fn.apply(this, args));
554
+ const result = withContext$1(handle.context, () => fn.apply(this, args));
537
555
  if (isThenable(result)) return result.then((value) => {
538
556
  handle.end({ output: value });
539
557
  return value;
540
558
  }, (error) => {
541
- handle.end({ error });
559
+ handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
542
560
  throw error;
543
561
  });
544
562
  handle.end({ output: result });
545
563
  return result;
546
564
  } catch (error) {
547
- handle.end({ error });
565
+ handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
548
566
  throw error;
549
567
  }
550
568
  };
@@ -555,4 +573,4 @@ function observe(fn, options) {
555
573
  return wrapped;
556
574
  }
557
575
  //#endregion
558
- export { flush, getTraceparent, init, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan };
576
+ export { activeContext, 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.0",
3
+ "version": "0.1.1",
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",
@@ -42,7 +42,7 @@
42
42
  "@opentelemetry/sdk-logs": "^0.218.0",
43
43
  "@opentelemetry/sdk-metrics": "^2.7.1",
44
44
  "@opentelemetry/sdk-trace-base": "^2.7.1",
45
- "@telemetry-dev/otel": "^0.1.0"
45
+ "@telemetry-dev/otel": "^0.1.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opentelemetry/api": "^1.9.1",
package/src/attrs.ts CHANGED
@@ -9,13 +9,13 @@ export type SpanType = "span" | "generation" | "tool" | "agent" | "embedding";
9
9
  // Operation names are taken verbatim by the ingest's deriveOperation. Plain spans always get an
10
10
  // explicit "function": without one, the propagated gen_ai.conversation.id would trip the ingest's
11
11
  // hasGenAi heuristic and misclassify them as "llm".
12
- export const SPAN_TYPE_OPERATIONS: Record<SpanType, string> = {
12
+ export const SPAN_TYPE_OPERATIONS = {
13
13
  span: "function",
14
14
  generation: "chat",
15
15
  tool: "execute_tool",
16
16
  agent: "invoke_agent",
17
17
  embedding: "embeddings",
18
- };
18
+ } satisfies Record<SpanType, string>;
19
19
 
20
20
  export function inputKeyFor(type: SpanType): string {
21
21
  return type === "tool" ? "gen_ai.tool.call.arguments" : "gen_ai.input.messages";
@@ -37,7 +37,7 @@ export interface TokenUsage {
37
37
  export interface GenerationFields {
38
38
  model?: string;
39
39
  provider?: string;
40
- systemInstructions?: unknown;
40
+ systemInstructions?: Parameters<import("./config.ts").MaskFn>[0];
41
41
  responseModel?: string;
42
42
  responseId?: string;
43
43
  usage?: TokenUsage;
@@ -70,12 +70,12 @@ export interface AgentFields {
70
70
  export interface SpanFields extends GenerationFields, ToolFields, AgentFields {
71
71
  /** Rename the span (update only). */
72
72
  name?: string;
73
- input?: unknown;
74
- output?: unknown;
73
+ input?: Parameters<import("./config.ts").MaskFn>[0];
74
+ output?: Parameters<import("./config.ts").MaskFn>[0];
75
75
  /** Marks the span failed: status ERROR + error.type + an "exception" event. */
76
76
  error?: unknown;
77
77
  /** td.metadata.<key> on this span only; use propagateAttributes for scope-wide metadata. */
78
- metadata?: Record<string, unknown>;
78
+ metadata?: Record<string, Parameters<import("./config.ts").MaskFn>[0]>;
79
79
  /** Raw attribute escape hatch, merged last. */
80
80
  attributes?: Record<string, AttributeValue>;
81
81
  }
package/src/capture.ts CHANGED
@@ -8,7 +8,7 @@ export const TRUNCATION_MARKER = "...[truncated]";
8
8
  export interface CaptureConfig {
9
9
  mask?: MaskFn;
10
10
  maxAttributeLength: number;
11
- onError?: (error: unknown) => void;
11
+ onError?: (cause: Error) => void;
12
12
  }
13
13
 
14
14
  export function truncate(value: string, maxLength: number): string {
@@ -21,7 +21,7 @@ export function truncate(value: string, maxLength: number): string {
21
21
  /** The single content funnel: mask → JSON stringify → truncate. */
22
22
  export function prepareCaptureValue(
23
23
  key: string,
24
- value: unknown,
24
+ value: Parameters<MaskFn>[0],
25
25
  cfg: CaptureConfig,
26
26
  ): string | undefined {
27
27
  let masked = value;
@@ -29,7 +29,7 @@ export function prepareCaptureValue(
29
29
  try {
30
30
  masked = cfg.mask(value, { key });
31
31
  } catch (error) {
32
- reportError(cfg.onError, error);
32
+ reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
33
33
  return undefined;
34
34
  }
35
35
  }
package/src/client.ts CHANGED
@@ -85,7 +85,7 @@ export function init(options?: TelemetryOptions, overrides?: ClientOverrides): T
85
85
  try {
86
86
  return initInner(options, overrides);
87
87
  } catch (error) {
88
- reportError(options?.onError, error);
88
+ reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
89
89
  activeClient = NOOP_CLIENT;
90
90
  return NOOP_CLIENT;
91
91
  }
@@ -98,7 +98,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
98
98
  if (activeClient && activeClient !== NOOP_CLIENT) {
99
99
  diag.warn("init() called again; replacing the previous client");
100
100
  // shutdown() synchronously releases global registrations before this init claims them.
101
- void activeClient.shutdown().catch((error: unknown) => reportError(config.onError, error));
101
+ void activeClient.shutdown().catch((error: Error) => reportError(config.onError, error));
102
102
  }
103
103
 
104
104
  const enabled = config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter);
@@ -116,7 +116,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
116
116
  ...config.resourceAttributes,
117
117
  });
118
118
  const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
119
- const headers = config.apiKey ? otlpHeaders(config.apiKey) : undefined;
119
+ const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
120
120
 
121
121
  const spanExporter =
122
122
  overrides?.spanExporter ??
@@ -204,12 +204,12 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
204
204
 
205
205
  let torn = false;
206
206
  const collect = (op: "forceFlush" | "shutdown"): Promise<void> => {
207
- const parts: Promise<unknown>[] = [provider[op]()];
207
+ const parts: Promise<void>[] = [provider[op]()];
208
208
  if (logs) parts.push(logs[op]());
209
209
  if (metrics) parts.push(metrics[op]());
210
210
  return Promise.all(parts)
211
211
  .then(() => undefined)
212
- .catch((error: unknown) => reportError(config.onError, error));
212
+ .catch((error: Error) => reportError(config.onError, error));
213
213
  };
214
214
  const handoff = (p: Promise<void>): Promise<void> => {
215
215
  if (config.waitUntil) {
package/src/config.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  export type { BatchOptions, ExportMode, LogLevel, SdkLogLevel } from "@telemetry-dev/otel";
13
13
 
14
14
  /** Redaction hook applied to captured content (input/output/log message) before JSON stringification. */
15
- export type MaskFn = (value: unknown, ctx: { key: string }) => unknown;
15
+ export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
16
16
 
17
17
  export interface TelemetryOptions {
18
18
  /** telemetry.dev ingest key (`td_live_…`). Falls back to `TELEMETRY_DEV_API_KEY`. When absent the SDK is a no-op. */
@@ -49,7 +49,9 @@ export interface TelemetryOptions {
49
49
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
50
50
  waitUntil?: (p: Promise<unknown>) => void;
51
51
  /** Receives any error raised while emitting telemetry; the SDK never throws into user code. */
52
- onError?: (error: unknown) => void;
52
+ onError?: (cause: Error) => void;
53
+ /** Header value for a custom telemetry.dev integration. */
54
+ sdkName?: string;
53
55
  }
54
56
 
55
57
  export interface ResolvedConfig {
@@ -70,7 +72,8 @@ export interface ResolvedConfig {
70
72
  logLevel: SdkLogLevel;
71
73
  fetchImpl: typeof fetch;
72
74
  waitUntil?: (p: Promise<unknown>) => void;
73
- onError?: (error: unknown) => void;
75
+ onError?: (cause: Error) => void;
76
+ sdkName: string;
74
77
  }
75
78
 
76
79
  export const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
@@ -100,5 +103,6 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
100
103
  fetchImpl: options.fetch ?? globalThis.fetch,
101
104
  waitUntil: options.waitUntil,
102
105
  onError: options.onError,
106
+ sdkName: options.sdkName ?? "@telemetry-dev/sdk",
103
107
  };
104
108
  }
package/src/index.ts CHANGED
@@ -19,11 +19,15 @@ export { propagateAttributes, type PropagatedAttributes } from "@telemetry-dev/o
19
19
  export { log, type LogOptions } from "./logs.ts";
20
20
  export { observe, type ObserveOptions } from "./observe.ts";
21
21
  export {
22
+ activeContext,
23
+ extractW3cContext,
22
24
  getTraceparent,
25
+ injectW3cContext,
23
26
  type ParentRef,
24
27
  type SpanHandle,
25
28
  startActiveSpan,
26
29
  startSpan,
27
30
  type StartSpanOptions,
28
31
  updateActiveSpan,
32
+ withContext,
29
33
  } from "./span.ts";
package/src/logs.ts CHANGED
@@ -8,17 +8,17 @@ import { currentClient } from "./client.ts";
8
8
  import type { LogLevel } from "./config.ts";
9
9
 
10
10
  // Severity numbers match the ingest's severityFromNumber buckets exactly.
11
- const SEVERITIES: Record<LogLevel, { number: SeverityNumber; text: string }> = {
11
+ const SEVERITIES = {
12
12
  debug: { number: SeverityNumber.DEBUG, text: "DEBUG" },
13
13
  info: { number: SeverityNumber.INFO, text: "INFO" },
14
14
  warn: { number: SeverityNumber.WARN, text: "WARN" },
15
15
  error: { number: SeverityNumber.ERROR, text: "ERROR" },
16
- };
16
+ } satisfies Record<LogLevel, { number: SeverityNumber; text: string }>;
17
17
 
18
18
  export interface LogOptions {
19
19
  /** Default "info". */
20
20
  level?: LogLevel;
21
- attributes?: Record<string, unknown>;
21
+ attributes?: Record<string, Parameters<import("./config.ts").MaskFn>[0]>;
22
22
  eventName?: string;
23
23
  timestamp?: Date | number;
24
24
  }
@@ -57,7 +57,7 @@ export function log(message: string, options?: LogOptions): void {
57
57
  for (const [key, value] of Object.entries(propagated)) {
58
58
  if (value !== undefined) {
59
59
  attributes[key] =
60
- typeof value === "string" ? truncate(value, cfg.maxAttributeLength) : value;
60
+ value.constructor === String ? truncate(String(value), cfg.maxAttributeLength) : value;
61
61
  }
62
62
  }
63
63
  }
@@ -72,6 +72,6 @@ export function log(message: string, options?: LogOptions): void {
72
72
  attributes,
73
73
  });
74
74
  } catch (error) {
75
- reportError(core.config.onError, error);
75
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
76
76
  }
77
77
  }
package/src/observe.ts CHANGED
@@ -13,11 +13,11 @@ export interface ObserveOptions extends StartSpanOptions {
13
13
  * output, thrown/rejected errors captured and rethrown. Activates context so nested SDK calls
14
14
  * parent to it. Resolves the client at call time, so wrapping at module load before init() works.
15
15
  */
16
- export function observe<TArgs extends unknown[], TReturn>(
17
- fn: (...args: TArgs) => TReturn,
16
+ export function observe<TThis, TArgs extends unknown[], TReturn>(
17
+ fn: (this: TThis, ...args: TArgs) => TReturn,
18
18
  options?: ObserveOptions,
19
19
  ): (...args: TArgs) => TReturn {
20
- const wrapped = function (this: unknown, ...args: TArgs): TReturn {
20
+ const wrapped = function (this: TThis, ...args: TArgs): TReturn {
21
21
  const core = currentClient().core;
22
22
  if (!core) return fn.apply(this, args);
23
23
 
@@ -34,20 +34,20 @@ export function observe<TArgs extends unknown[], TReturn>(
34
34
  : args;
35
35
  handle = createSpanHandle(core, name, { ...options, input });
36
36
  } catch (error) {
37
- reportError(core.config.onError, error);
37
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
38
38
  return fn.apply(this, args);
39
39
  }
40
40
 
41
41
  try {
42
42
  const result = withContext(handle.context, () => fn.apply(this, args));
43
43
  if (isThenable(result)) {
44
- return (result as unknown as Promise<unknown>).then(
44
+ return result.then(
45
45
  (value) => {
46
46
  handle.end({ output: value });
47
47
  return value;
48
48
  },
49
- (error: unknown) => {
50
- handle.end({ error });
49
+ (error: Error) => {
50
+ handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
51
51
  throw error;
52
52
  },
53
53
  ) as TReturn;
@@ -55,7 +55,7 @@ export function observe<TArgs extends unknown[], TReturn>(
55
55
  handle.end({ output: result });
56
56
  return result;
57
57
  } catch (error) {
58
- handle.end({ error });
58
+ handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
59
59
  throw error;
60
60
  }
61
61
  };
package/src/span.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  import {
2
2
  type Context,
3
+ defaultTextMapGetter,
3
4
  defaultTextMapSetter,
5
+ type Link,
6
+ ROOT_CONTEXT,
4
7
  type Span,
5
8
  type SpanContext,
6
9
  SpanKind,
7
10
  SpanStatusCode,
8
11
  trace,
9
12
  } from "@opentelemetry/api";
10
- import { parseTraceParent, W3CTraceContextPropagator } from "@opentelemetry/core";
13
+ import {
14
+ CompositePropagator,
15
+ parseTraceParent,
16
+ W3CBaggagePropagator,
17
+ W3CTraceContextPropagator,
18
+ } from "@opentelemetry/core";
11
19
 
12
20
  import {
13
21
  activeContext,
@@ -33,8 +41,12 @@ export type ParentRef = string /* W3C traceparent */ | SpanHandle | SpanContext
33
41
  export interface StartSpanOptions extends SpanFields {
34
42
  /** Default "span" → operation "function". */
35
43
  type?: SpanType;
44
+ /** OpenTelemetry span kind; defaults to INTERNAL. */
45
+ kind?: SpanKind;
36
46
  /** Parent override; defaults to the active context. */
37
47
  parent?: ParentRef;
48
+ /** Links to associate with the span. */
49
+ links?: Link[];
38
50
  startTime?: Date | number;
39
51
  /** Per-call override of the global captureInput. */
40
52
  captureInput?: boolean;
@@ -59,7 +71,7 @@ export interface SpanHandle {
59
71
  interface SpanMeta {
60
72
  type: SpanType;
61
73
  capture: FieldCaptureConfig;
62
- onError?: (error: unknown) => void;
74
+ onError?: (cause: Error) => void;
63
75
  }
64
76
 
65
77
  // Remembers each span's type + capture flags so update()/end()/updateActiveSpan map
@@ -68,7 +80,7 @@ const SPAN_META = new WeakMap<Span, SpanMeta>();
68
80
 
69
81
  const SEVERITY_ERROR = 17;
70
82
 
71
- export function isThenable(value: unknown): value is PromiseLike<unknown> {
83
+ export function isThenable<T>(value: T): value is T & PromiseLike<Awaited<T>> {
72
84
  return (
73
85
  value != null &&
74
86
  (typeof value === "object" || typeof value === "function") &&
@@ -77,11 +89,11 @@ export function isThenable(value: unknown): value is PromiseLike<unknown> {
77
89
  );
78
90
  }
79
91
 
80
- function isContext(value: object): value is Context {
92
+ function isContext(value: Exclude<ParentRef, string>): value is Context {
81
93
  return "getValue" in value && typeof value.getValue === "function";
82
94
  }
83
95
 
84
- function isSpanHandle(value: object): value is SpanHandle {
96
+ function isSpanHandle(value: Exclude<ParentRef, string>): value is SpanHandle {
85
97
  return "span" in value && "context" in value && "update" in value;
86
98
  }
87
99
 
@@ -101,14 +113,22 @@ function resolveParent(parent: ParentRef | undefined): Context {
101
113
  if (!parsed) return base;
102
114
  return trace.setSpanContext(base, { ...parsed, isRemote: true });
103
115
  }
104
- if (isSpanHandle(parent)) return withAmbientPropagated(parent.context);
105
- if (isContext(parent)) return withAmbientPropagated(parent);
106
- return trace.setSpanContext(base, parent);
116
+ const ref = parent as Exclude<ParentRef, string>;
117
+ if (isSpanHandle(ref)) return withAmbientPropagated(ref.context);
118
+ if (isContext(ref)) return withAmbientPropagated(ref);
119
+ return trace.setSpanContext(base, ref);
107
120
  }
108
121
 
109
122
  function applyError(span: Span, error: unknown): void {
110
123
  const err = error instanceof Error ? error : undefined;
111
- const errorType = err?.name ?? "Error";
124
+ // Prefer the specific subclass name (Python parity: type(error).__name__) — many SDK
125
+ // error classes (e.g. openai's BadRequestError) never override the inherited "Error" name.
126
+ const errorType =
127
+ err === undefined
128
+ ? "Error"
129
+ : err.name !== "Error" && err.name.length > 0
130
+ ? err.name
131
+ : (err.constructor?.name ?? "Error") || "Error";
112
132
  const message = err?.message ?? String(error);
113
133
  span.setStatus({ code: SpanStatusCode.ERROR });
114
134
  span.setAttribute("error.type", errorType);
@@ -155,8 +175,9 @@ export function createSpanHandle(
155
175
  const span = core.tracer.startSpan(
156
176
  name,
157
177
  {
158
- kind: SpanKind.INTERNAL,
178
+ kind: options.kind ?? SpanKind.INTERNAL,
159
179
  startTime: options.startTime,
180
+ links: options.links,
160
181
  attributes: { "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type] },
161
182
  },
162
183
  parentCtx,
@@ -179,7 +200,7 @@ export function createSpanHandle(
179
200
  try {
180
201
  applyFields(span, meta, fields);
181
202
  } catch (error) {
182
- reportError(meta.onError, error);
203
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
183
204
  }
184
205
  return handle;
185
206
  },
@@ -187,7 +208,7 @@ export function createSpanHandle(
187
208
  try {
188
209
  if (fields) applyFields(span, meta, fields);
189
210
  } catch (error) {
190
- reportError(meta.onError, error);
211
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
191
212
  }
192
213
  span.end(fields?.endTime);
193
214
  },
@@ -202,7 +223,7 @@ export function startSpan(name: string, options?: StartSpanOptions): SpanHandle
202
223
  try {
203
224
  return createSpanHandle(core, name, options);
204
225
  } catch (error) {
205
- reportError(core.config.onError, error);
226
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
206
227
  return NOOP_SPAN_HANDLE;
207
228
  }
208
229
  }
@@ -211,7 +232,7 @@ export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T
211
232
  try {
212
233
  const result = withContext(handle.context, () => fn(handle));
213
234
  if (isThenable(result)) {
214
- return (result as unknown as Promise<unknown>).then(
235
+ return result.then(
215
236
  (value) => {
216
237
  handle.end();
217
238
  return value;
@@ -241,8 +262,9 @@ export function startActiveSpan<T>(
241
262
  optionsOrFn: StartSpanOptions | ((span: SpanHandle) => T),
242
263
  maybeFn?: (span: SpanHandle) => T,
243
264
  ): T {
244
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn!;
245
- const options = typeof optionsOrFn === "function" ? undefined : optionsOrFn;
265
+ const isFn = typeof optionsOrFn === "function";
266
+ const fn = isFn ? optionsOrFn : maybeFn!;
267
+ const options = isFn ? undefined : optionsOrFn;
246
268
  const handle = startSpan(name, options);
247
269
  if (!handle.isRecording) return fn(handle);
248
270
  return runWithHandle(handle, fn);
@@ -258,15 +280,45 @@ export function updateActiveSpan(fields: SpanFields): void {
258
280
  const meta = SPAN_META.get(span) ?? metaFor(core);
259
281
  applyFields(span, meta, fields);
260
282
  } catch (error) {
261
- reportError(core.config.onError, error);
283
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
262
284
  }
263
285
  }
264
286
 
265
- const PROPAGATOR = new W3CTraceContextPropagator();
287
+ const TRACE_CONTEXT_PROPAGATOR = new W3CTraceContextPropagator();
288
+ const W3C_PROPAGATOR = new CompositePropagator({
289
+ propagators: [TRACE_CONTEXT_PROPAGATOR, new W3CBaggagePropagator()],
290
+ });
291
+
292
+ export { activeContext, withContext };
293
+
294
+ /** Extract W3C trace context; baggage is omitted unless includeBaggage is true. */
295
+ export function extractW3cContext(
296
+ carrier: Record<string, unknown>,
297
+ options: { includeBaggage?: boolean } = {},
298
+ ): Context {
299
+ const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
300
+ return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
301
+ }
302
+
303
+ /** Inject W3C trace context; baggage is omitted unless includeBaggage is true. */
304
+ export function injectW3cContext(
305
+ context: Context,
306
+ carrier: Record<string, unknown>,
307
+ options: { includeBaggage?: boolean } = {},
308
+ ): void {
309
+ const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
310
+ for (const key of Object.keys(carrier)) {
311
+ const normalized = key.toLowerCase();
312
+ if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") {
313
+ delete carrier[key];
314
+ }
315
+ }
316
+ propagator.inject(context, carrier, defaultTextMapSetter);
317
+ }
266
318
 
267
319
  /** Serialize the active span context as a W3C traceparent; null when there is none. */
268
320
  export function getTraceparent(): string | null {
269
- const carrier: { traceparent?: string } = {};
270
- PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
321
+ const carrier = { traceparent: undefined };
322
+ TRACE_CONTEXT_PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
271
323
  return carrier.traceparent ?? null;
272
324
  }