@telemetry-dev/sdk 0.1.0 → 0.1.2

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
@@ -60,6 +60,11 @@ Other `init()` options: `enabled` (kill switch), `registerGlobal`, `exportMode:
60
60
  `exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
61
61
  default `"warn"`), `fetch`, `waitUntil`, `onError`.
62
62
 
63
+ `sampler` accepts an OpenTelemetry `Sampler` and overrides `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG`.
64
+ The default is parent-based always-on. Session roots keep the configured root policy.
65
+ Nested spans and explicitly parented spans keep the real parent's policy.
66
+ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-attributes) on its provider. This includes the global provider that Eve uses.
67
+
63
68
  ## API
64
69
 
65
70
  - `init(options?)` — create the client (isolated OTel TracerProvider by default).
@@ -74,6 +79,10 @@ default `"warn"`), `fetch`, `waitUntil`, `onError`.
74
79
  - `log(message, { level, eventName, attributes }?)` — log record correlated to the active trace.
75
80
  - `getTraceparent()` — W3C traceparent of the active span (pass to other services; accept it via
76
81
  `startSpan(name, { parent })`).
82
+ - `activeContext()` / `withContext(context, fn)` — read or temporarily activate an OpenTelemetry
83
+ context.
84
+ - `extractW3cContext(carrier, options?)` / `injectW3cContext(context, carrier, options?)` — extract
85
+ or inject W3C trace context. Baggage is omitted unless `includeBaggage` is true.
77
86
  - `flush()` / `shutdown()` — export pending data; required before serverless freeze/exit.
78
87
 
79
88
  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
- 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";
3
+ import { ReadableSpan, Sampler, SpanExporter } from "@opentelemetry/sdk-trace-base";
4
+ import { BatchOptions, BatchOptions as BatchOptions$1, ExportMode, ExportMode as ExportMode$1, LogLevel, PropagatedAttributes, SCOPE_NAME, SdkLogLevel, SdkLogLevel as SdkLogLevel$1, SessionRootOf, TelemetrySpanProcessor, TelemetrySpanProcessorOptions, 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;
@@ -24,6 +24,8 @@ interface TelemetryOptions {
24
24
  registerGlobal?: boolean;
25
25
  /** "batched" (default) buffers spans; "immediate" exports per span (serverless). */
26
26
  exportMode?: ExportMode$1;
27
+ /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
28
+ sampler?: Sampler;
27
29
  /** Capture span inputs (args, prompts) by default. Default true. */
28
30
  captureInput?: boolean;
29
31
  /** Capture span outputs (results, completions) by default. Default true. */
@@ -35,6 +37,12 @@ interface TelemetryOptions {
35
37
  batch?: BatchOptions$1;
36
38
  /** Export filter. Default when registerGlobal: only spans from this SDK's instrumentation scope. */
37
39
  spanFilter?: (span: ReadableSpan) => boolean;
40
+ /**
41
+ * With registerGlobal: spans from other tracers that this returns a session id for are
42
+ * parented under the session trace even when a parent span is active. Use it for frameworks
43
+ * that start each turn inside their own engine span.
44
+ */
45
+ sessionRootOf?: SessionRootOf;
38
46
  /** Extra resource attributes merged onto service.name / deployment.environment.name. */
39
47
  resourceAttributes?: Record<string, AttributeValue>;
40
48
  /** SDK self-diagnostics console level. Default "warn"; "silent" disables. */
@@ -44,7 +52,9 @@ interface TelemetryOptions {
44
52
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
45
53
  waitUntil?: (p: Promise<unknown>) => void;
46
54
  /** Receives any error raised while emitting telemetry; the SDK never throws into user code. */
47
- onError?: (error: unknown) => void;
55
+ onError?: (cause: Error) => void;
56
+ /** Header value for a custom telemetry.dev integration. */
57
+ sdkName?: string;
48
58
  }
49
59
  //#endregion
50
60
  //#region src/attrs.d.ts
@@ -60,7 +70,7 @@ interface TokenUsage {
60
70
  interface GenerationFields {
61
71
  model?: string;
62
72
  provider?: string;
63
- systemInstructions?: unknown;
73
+ systemInstructions?: Parameters<MaskFn>[0];
64
74
  responseModel?: string;
65
75
  responseId?: string;
66
76
  usage?: TokenUsage;
@@ -90,12 +100,12 @@ interface AgentFields {
90
100
  interface SpanFields extends GenerationFields, ToolFields, AgentFields {
91
101
  /** Rename the span (update only). */
92
102
  name?: string;
93
- input?: unknown;
94
- output?: unknown;
103
+ input?: Parameters<MaskFn>[0];
104
+ output?: Parameters<MaskFn>[0];
95
105
  /** Marks the span failed: status ERROR + error.type + an "exception" event. */
96
106
  error?: unknown;
97
107
  /** td.metadata.<key> on this span only; use propagateAttributes for scope-wide metadata. */
98
- metadata?: Record<string, unknown>;
108
+ metadata?: Record<string, Parameters<MaskFn>[0]>;
99
109
  /** Raw attribute escape hatch, merged last. */
100
110
  attributes?: Record<string, AttributeValue>;
101
111
  }
@@ -122,7 +132,7 @@ declare function init(options?: TelemetryOptions, overrides?: ClientOverrides):
122
132
  interface LogOptions {
123
133
  /** Default "info". */
124
134
  level?: LogLevel;
125
- attributes?: Record<string, unknown>;
135
+ attributes?: Record<string, Parameters<MaskFn>[0]>;
126
136
  eventName?: string;
127
137
  timestamp?: Date | number;
128
138
  }
@@ -137,8 +147,12 @@ type ParentRef = string | SpanHandle | SpanContext | Context;
137
147
  interface StartSpanOptions extends SpanFields {
138
148
  /** Default "span" → operation "function". */
139
149
  type?: SpanType;
150
+ /** OpenTelemetry span kind; defaults to INTERNAL. */
151
+ kind?: SpanKind;
140
152
  /** Parent override; defaults to the active context. */
141
153
  parent?: ParentRef;
154
+ /** Links to associate with the span. */
155
+ links?: Link[];
142
156
  startTime?: Date | number;
143
157
  /** Per-call override of the global captureInput. */
144
158
  captureInput?: boolean;
@@ -166,6 +180,14 @@ declare function startActiveSpan<T>(name: string, fn: (span: SpanHandle) => T):
166
180
  declare function startActiveSpan<T>(name: string, options: StartSpanOptions, fn: (span: SpanHandle) => T): T;
167
181
  /** Update the innermost active span; no-op when none is recording. */
168
182
  declare function updateActiveSpan(fields: SpanFields): void;
183
+ /** Extract W3C trace context; baggage is omitted unless includeBaggage is true. */
184
+ declare function extractW3cContext(carrier: Record<string, unknown>, options?: {
185
+ includeBaggage?: boolean;
186
+ }): Context;
187
+ /** Inject W3C trace context; baggage is omitted unless includeBaggage is true. */
188
+ declare function injectW3cContext(context: Context, carrier: Record<string, unknown>, options?: {
189
+ includeBaggage?: boolean;
190
+ }): void;
169
191
  /** Serialize the active span context as a W3C traceparent; null when there is none. */
170
192
  declare function getTraceparent(): string | null;
171
193
  //#endregion
@@ -179,6 +201,6 @@ interface ObserveOptions extends StartSpanOptions {
179
201
  * output, thrown/rejected errors captured and rethrown. Activates context so nested SDK calls
180
202
  * parent to it. Resolves the client at call time, so wrapping at module load before init() works.
181
203
  */
182
- declare function observe<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
204
+ declare function observe<TThis, TArgs extends unknown[], TReturn>(fn: (this: TThis, ...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
183
205
  //#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 };
206
+ export { type AgentFields, type BatchOptions, 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, 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_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
8
  function resolveConfig(options = {}) {
9
9
  const env = resolveEnv();
@@ -16,6 +16,7 @@ function resolveConfig(options = {}) {
16
16
  enabled: options.enabled ?? true,
17
17
  registerGlobal: options.registerGlobal ?? false,
18
18
  exportMode: options.exportMode ?? "batched",
19
+ sampler: options.sampler,
19
20
  captureInput: options.captureInput ?? true,
20
21
  captureOutput: options.captureOutput ?? true,
21
22
  mask: options.mask,
@@ -25,11 +26,13 @@ function resolveConfig(options = {}) {
25
26
  ...options.batch
26
27
  },
27
28
  spanFilter: options.spanFilter,
29
+ sessionRootOf: options.sessionRootOf,
28
30
  resourceAttributes: options.resourceAttributes,
29
31
  logLevel: options.logLevel ?? "warn",
30
32
  fetchImpl: options.fetch ?? globalThis.fetch,
31
33
  waitUntil: options.waitUntil,
32
- onError: options.onError
34
+ onError: options.onError,
35
+ sdkName: options.sdkName ?? "@telemetry-dev/sdk"
33
36
  };
34
37
  }
35
38
  //#endregion
@@ -53,7 +56,7 @@ function init(options, overrides) {
53
56
  try {
54
57
  return initInner(options, overrides);
55
58
  } catch (error) {
56
- reportError(options?.onError, error);
59
+ reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
57
60
  activeClient = NOOP_CLIENT;
58
61
  return NOOP_CLIENT;
59
62
  }
@@ -79,7 +82,7 @@ function initInner(options, overrides) {
79
82
  fetchImpl: config.fetchImpl,
80
83
  onError: config.onError
81
84
  };
82
- const headers = config.apiKey ? otlpHeaders(config.apiKey) : void 0;
85
+ const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : void 0;
83
86
  const spanExporter = overrides?.spanExporter ?? createTraceExporter({
84
87
  url: `${config.baseUrl}/v1/traces`,
85
88
  headers
@@ -93,25 +96,27 @@ function initInner(options, overrides) {
93
96
  exporter: metricExporter,
94
97
  exportIntervalMillis: config.exportMode === "batched" ? BATCHED_METRIC_INTERVAL_MS : DORMANT_INTERVAL_MS
95
98
  }) : void 0;
96
- const spanFilter = config.spanFilter ?? (config.registerGlobal ? (span) => span.instrumentationScope.name === SCOPE_NAME : void 0);
99
+ const spanFilter = config.spanFilter ?? (config.registerGlobal ? (span) => span.instrumentationScope.name === SCOPE_NAME$1 : void 0);
100
+ const processor = new StampingSpanProcessor({
101
+ exporter: spanExporter,
102
+ exportMode: config.exportMode,
103
+ batch: config.batch,
104
+ spanFilter,
105
+ recordMetrics: metrics ? (span) => metrics.record(span) : void 0,
106
+ onError: config.onError
107
+ });
97
108
  const provider = new BasicTracerProvider({
98
109
  resource,
99
- spanProcessors: [new StampingSpanProcessor({
100
- exporter: spanExporter,
101
- exportMode: config.exportMode,
102
- batch: config.batch,
103
- spanFilter,
104
- recordMetrics: metrics ? (span) => metrics.record(span) : void 0,
105
- onError: config.onError
106
- })],
110
+ sampler: sessionSampler(config.sampler),
111
+ spanProcessors: [processor],
107
112
  spanLimits: { attributeValueLengthLimit: config.maxAttributeLength }
108
113
  });
109
- const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
114
+ const tracer = provider.getTracer(SCOPE_NAME$1, SCOPE_VERSION);
110
115
  let registeredTrace = false;
111
116
  let registeredContext = false;
112
117
  let registeredPropagation = false;
113
118
  if (config.registerGlobal) {
114
- registeredTrace = trace.setGlobalTracerProvider(provider);
119
+ registeredTrace = trace.setGlobalTracerProvider(config.sessionRootOf ? sessionRootTracerProvider(provider, config.apiKey, config.sessionRootOf, config.onError) : provider);
115
120
  if (als) registeredContext = context.setGlobalContextManager(new AlsContextManager(als));
116
121
  registeredPropagation = propagation.setGlobalPropagator(new W3CTraceContextPropagator());
117
122
  if (!registeredTrace) diag.warn("a global TracerProvider is already registered; attach the TelemetrySpanProcessor from '@telemetry-dev/otel' to your own provider instead");
@@ -131,7 +136,7 @@ function initInner(options, overrides) {
131
136
  processors: [logProcessor]
132
137
  });
133
138
  logs = {
134
- logger: loggerProvider.getLogger(SCOPE_NAME, SCOPE_VERSION),
139
+ logger: loggerProvider.getLogger(SCOPE_NAME$1, SCOPE_VERSION),
135
140
  forceFlush: () => loggerProvider.forceFlush(),
136
141
  shutdown: () => loggerProvider.shutdown()
137
142
  };
@@ -188,7 +193,7 @@ function prepareCaptureValue(key, value, cfg) {
188
193
  if (cfg.mask) try {
189
194
  masked = cfg.mask(value, { key });
190
195
  } catch (error) {
191
- reportError(cfg.onError, error);
196
+ reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
192
197
  return;
193
198
  }
194
199
  const serialized = jsonAttr(masked);
@@ -232,7 +237,7 @@ function log(message, options) {
232
237
  onError: cfg.onError
233
238
  };
234
239
  const severity = SEVERITIES[options?.level ?? "info"];
235
- const ctx = activeContext();
240
+ const ctx = activeContext$1();
236
241
  const attributes = {};
237
242
  for (const [key, value] of Object.entries(options?.attributes ?? {})) {
238
243
  if (value === void 0) continue;
@@ -245,7 +250,7 @@ function log(message, options) {
245
250
  }
246
251
  const propagated = propagatedFromContext(ctx);
247
252
  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;
253
+ for (const [key, value] of Object.entries(propagated)) if (value !== void 0) attributes[key] = value.constructor === String ? truncate(String(value), cfg.maxAttributeLength) : value;
249
254
  }
250
255
  logger.emit({
251
256
  body: prepareCaptureValue("log.message", message, captureCfg) ?? "",
@@ -257,7 +262,7 @@ function log(message, options) {
257
262
  attributes
258
263
  });
259
264
  } catch (error) {
260
- reportError(core.config.onError, error);
265
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
261
266
  }
262
267
  }
263
268
  //#endregion
@@ -379,13 +384,14 @@ function resolveParent(parent) {
379
384
  isRemote: true
380
385
  });
381
386
  }
382
- if (isSpanHandle(parent)) return withAmbientPropagated(parent.context);
383
- if (isContext(parent)) return withAmbientPropagated(parent);
384
- return trace.setSpanContext(base, parent);
387
+ const ref = parent;
388
+ if (isSpanHandle(ref)) return withAmbientPropagated(ref.context);
389
+ if (isContext(ref)) return withAmbientPropagated(ref);
390
+ return trace.setSpanContext(base, ref);
385
391
  }
386
392
  function applyError(span, error) {
387
393
  const err = error instanceof Error ? error : void 0;
388
- const errorType = err?.name ?? "Error";
394
+ const errorType = err === void 0 ? "Error" : err.name !== "Error" && err.name.length > 0 ? err.name : (err.constructor?.name ?? "Error") || "Error";
389
395
  const message = err?.message ?? String(error);
390
396
  span.setStatus({ code: SpanStatusCode.ERROR });
391
397
  span.setAttribute("error.type", errorType);
@@ -418,14 +424,23 @@ function metaFor(core, options) {
418
424
  }
419
425
  function createSpanHandle(core, name, options = {}) {
420
426
  const meta = metaFor(core, options);
421
- const parentCtx = resolveParent(options.parent);
427
+ const resolved = resolveParent(options.parent);
428
+ const parentCtx = withSessionParent(resolved, sessionIdOf(resolved, options.attributes), core.config.apiKey);
429
+ const attrs = fieldsToAttributes(options, meta.type, meta.capture);
422
430
  const span = core.tracer.startSpan(name, {
423
- kind: SpanKind.INTERNAL,
431
+ kind: options.kind ?? SpanKind.INTERNAL,
424
432
  startTime: options.startTime,
425
- attributes: { "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type] }
433
+ links: options.links,
434
+ attributes: {
435
+ "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type],
436
+ ...propagatedFromContext(parentCtx),
437
+ ...attrs
438
+ }
426
439
  }, parentCtx);
427
440
  SPAN_META.set(span, meta);
428
- applyFields(span, meta, options);
441
+ if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
442
+ if (options.name) span.updateName(options.name);
443
+ if (options.error !== void 0) applyError(span, options.error);
429
444
  const spanContext = span.spanContext();
430
445
  const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
431
446
  const handle = {
@@ -441,7 +456,7 @@ function createSpanHandle(core, name, options = {}) {
441
456
  try {
442
457
  applyFields(span, meta, fields);
443
458
  } catch (error) {
444
- reportError(meta.onError, error);
459
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
445
460
  }
446
461
  return handle;
447
462
  },
@@ -449,7 +464,7 @@ function createSpanHandle(core, name, options = {}) {
449
464
  try {
450
465
  if (fields) applyFields(span, meta, fields);
451
466
  } catch (error) {
452
- reportError(meta.onError, error);
467
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
453
468
  }
454
469
  span.end(fields?.endTime);
455
470
  }
@@ -463,7 +478,7 @@ function startSpan(name, options) {
463
478
  try {
464
479
  return createSpanHandle(core, name, options);
465
480
  } catch (error) {
466
- reportError(core.config.onError, error);
481
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
467
482
  return NOOP_SPAN_HANDLE;
468
483
  }
469
484
  }
@@ -485,9 +500,10 @@ function runWithHandle(handle, fn) {
485
500
  }
486
501
  }
487
502
  function startActiveSpan(name, optionsOrFn, maybeFn) {
488
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
489
- const handle = startSpan(name, typeof optionsOrFn === "function" ? void 0 : optionsOrFn);
490
- if (!handle.isRecording) return fn(handle);
503
+ const isFn = typeof optionsOrFn === "function";
504
+ const fn = isFn ? optionsOrFn : maybeFn;
505
+ const handle = startSpan(name, isFn ? void 0 : optionsOrFn);
506
+ if (handle === NOOP_SPAN_HANDLE) return fn(handle);
491
507
  return runWithHandle(handle, fn);
492
508
  }
493
509
  /** Update the innermost active span; no-op when none is recording. */
@@ -499,14 +515,28 @@ function updateActiveSpan(fields) {
499
515
  if (!span?.isRecording()) return;
500
516
  applyFields(span, SPAN_META.get(span) ?? metaFor(core), fields);
501
517
  } catch (error) {
502
- reportError(core.config.onError, error);
518
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
503
519
  }
504
520
  }
505
- const PROPAGATOR = new W3CTraceContextPropagator();
521
+ const TRACE_CONTEXT_PROPAGATOR = new W3CTraceContextPropagator();
522
+ const W3C_PROPAGATOR = new CompositePropagator({ propagators: [TRACE_CONTEXT_PROPAGATOR, new W3CBaggagePropagator()] });
523
+ /** Extract W3C trace context; baggage is omitted unless includeBaggage is true. */
524
+ function extractW3cContext(carrier, options = {}) {
525
+ return (options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR).extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
526
+ }
527
+ /** Inject W3C trace context; baggage is omitted unless includeBaggage is true. */
528
+ function injectW3cContext(context, carrier, options = {}) {
529
+ const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
530
+ for (const key of Object.keys(carrier)) {
531
+ const normalized = key.toLowerCase();
532
+ if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") delete carrier[key];
533
+ }
534
+ propagator.inject(context, carrier, defaultTextMapSetter);
535
+ }
506
536
  /** Serialize the active span context as a W3C traceparent; null when there is none. */
507
537
  function getTraceparent() {
508
- const carrier = {};
509
- PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
538
+ const carrier = { traceparent: void 0 };
539
+ TRACE_CONTEXT_PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
510
540
  return carrier.traceparent ?? null;
511
541
  }
512
542
  //#endregion
@@ -529,22 +559,22 @@ function observe(fn, options) {
529
559
  input
530
560
  });
531
561
  } catch (error) {
532
- reportError(core.config.onError, error);
562
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
533
563
  return fn.apply(this, args);
534
564
  }
535
565
  try {
536
- const result = withContext(handle.context, () => fn.apply(this, args));
566
+ const result = withContext$1(handle.context, () => fn.apply(this, args));
537
567
  if (isThenable(result)) return result.then((value) => {
538
568
  handle.end({ output: value });
539
569
  return value;
540
570
  }, (error) => {
541
- handle.end({ error });
571
+ handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
542
572
  throw error;
543
573
  });
544
574
  handle.end({ output: result });
545
575
  return result;
546
576
  } catch (error) {
547
- handle.end({ error });
577
+ handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
548
578
  throw error;
549
579
  }
550
580
  };
@@ -555,4 +585,4 @@ function observe(fn, options) {
555
585
  return wrapped;
556
586
  }
557
587
  //#endregion
558
- export { flush, getTraceparent, init, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan };
588
+ export { SCOPE_NAME, TelemetrySpanProcessor, 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.2",
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.2"
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
@@ -29,6 +29,8 @@ import {
29
29
  reportError,
30
30
  SCOPE_NAME,
31
31
  SCOPE_VERSION,
32
+ sessionRootTracerProvider,
33
+ sessionSampler,
32
34
  setLogLevel,
33
35
  StampingSpanProcessor,
34
36
  type Transport,
@@ -85,7 +87,7 @@ export function init(options?: TelemetryOptions, overrides?: ClientOverrides): T
85
87
  try {
86
88
  return initInner(options, overrides);
87
89
  } catch (error) {
88
- reportError(options?.onError, error);
90
+ reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
89
91
  activeClient = NOOP_CLIENT;
90
92
  return NOOP_CLIENT;
91
93
  }
@@ -98,7 +100,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
98
100
  if (activeClient && activeClient !== NOOP_CLIENT) {
99
101
  diag.warn("init() called again; replacing the previous client");
100
102
  // shutdown() synchronously releases global registrations before this init claims them.
101
- void activeClient.shutdown().catch((error: unknown) => reportError(config.onError, error));
103
+ void activeClient.shutdown().catch((error: Error) => reportError(config.onError, error));
102
104
  }
103
105
 
104
106
  const enabled = config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter);
@@ -116,7 +118,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
116
118
  ...config.resourceAttributes,
117
119
  });
118
120
  const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
119
- const headers = config.apiKey ? otlpHeaders(config.apiKey) : undefined;
121
+ const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
120
122
 
121
123
  const spanExporter =
122
124
  overrides?.spanExporter ??
@@ -153,6 +155,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
153
155
 
154
156
  const provider = new BasicTracerProvider({
155
157
  resource,
158
+ sampler: sessionSampler(config.sampler),
156
159
  spanProcessors: [processor],
157
160
  spanLimits: { attributeValueLengthLimit: config.maxAttributeLength },
158
161
  });
@@ -162,7 +165,11 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
162
165
  let registeredContext = false;
163
166
  let registeredPropagation = false;
164
167
  if (config.registerGlobal) {
165
- registeredTrace = trace.setGlobalTracerProvider(provider);
168
+ registeredTrace = trace.setGlobalTracerProvider(
169
+ config.sessionRootOf
170
+ ? sessionRootTracerProvider(provider, config.apiKey, config.sessionRootOf, config.onError)
171
+ : provider,
172
+ );
166
173
  if (als) {
167
174
  registeredContext = apiContext.setGlobalContextManager(new AlsContextManager(als));
168
175
  }
@@ -204,12 +211,12 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
204
211
 
205
212
  let torn = false;
206
213
  const collect = (op: "forceFlush" | "shutdown"): Promise<void> => {
207
- const parts: Promise<unknown>[] = [provider[op]()];
214
+ const parts: Promise<void>[] = [provider[op]()];
208
215
  if (logs) parts.push(logs[op]());
209
216
  if (metrics) parts.push(metrics[op]());
210
217
  return Promise.all(parts)
211
218
  .then(() => undefined)
212
- .catch((error: unknown) => reportError(config.onError, error));
219
+ .catch((error: Error) => reportError(config.onError, error));
213
220
  };
214
221
  const handoff = (p: Promise<void>): Promise<void> => {
215
222
  if (config.waitUntil) {
package/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AttributeValue } from "@opentelemetry/api";
2
- import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
2
+ import type { ReadableSpan, Sampler } from "@opentelemetry/sdk-trace-base";
3
3
  import {
4
4
  type BatchOptions,
5
5
  DEFAULT_BASE_URL,
@@ -7,12 +7,13 @@ import {
7
7
  type ExportMode,
8
8
  resolveEnv,
9
9
  type SdkLogLevel,
10
+ type SessionRootOf,
10
11
  } from "@telemetry-dev/otel";
11
12
 
12
13
  export type { BatchOptions, ExportMode, LogLevel, SdkLogLevel } from "@telemetry-dev/otel";
13
14
 
14
15
  /** Redaction hook applied to captured content (input/output/log message) before JSON stringification. */
15
- export type MaskFn = (value: unknown, ctx: { key: string }) => unknown;
16
+ export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
16
17
 
17
18
  export interface TelemetryOptions {
18
19
  /** telemetry.dev ingest key (`td_live_…`). Falls back to `TELEMETRY_DEV_API_KEY`. When absent the SDK is a no-op. */
@@ -29,6 +30,8 @@ export interface TelemetryOptions {
29
30
  registerGlobal?: boolean;
30
31
  /** "batched" (default) buffers spans; "immediate" exports per span (serverless). */
31
32
  exportMode?: ExportMode;
33
+ /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
34
+ sampler?: Sampler;
32
35
  /** Capture span inputs (args, prompts) by default. Default true. */
33
36
  captureInput?: boolean;
34
37
  /** Capture span outputs (results, completions) by default. Default true. */
@@ -40,6 +43,12 @@ export interface TelemetryOptions {
40
43
  batch?: BatchOptions;
41
44
  /** Export filter. Default when registerGlobal: only spans from this SDK's instrumentation scope. */
42
45
  spanFilter?: (span: ReadableSpan) => boolean;
46
+ /**
47
+ * With registerGlobal: spans from other tracers that this returns a session id for are
48
+ * parented under the session trace even when a parent span is active. Use it for frameworks
49
+ * that start each turn inside their own engine span.
50
+ */
51
+ sessionRootOf?: SessionRootOf;
43
52
  /** Extra resource attributes merged onto service.name / deployment.environment.name. */
44
53
  resourceAttributes?: Record<string, AttributeValue>;
45
54
  /** SDK self-diagnostics console level. Default "warn"; "silent" disables. */
@@ -49,7 +58,9 @@ export interface TelemetryOptions {
49
58
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
50
59
  waitUntil?: (p: Promise<unknown>) => void;
51
60
  /** Receives any error raised while emitting telemetry; the SDK never throws into user code. */
52
- onError?: (error: unknown) => void;
61
+ onError?: (cause: Error) => void;
62
+ /** Header value for a custom telemetry.dev integration. */
63
+ sdkName?: string;
53
64
  }
54
65
 
55
66
  export interface ResolvedConfig {
@@ -60,17 +71,20 @@ export interface ResolvedConfig {
60
71
  enabled: boolean;
61
72
  registerGlobal: boolean;
62
73
  exportMode: ExportMode;
74
+ sampler?: Sampler;
63
75
  captureInput: boolean;
64
76
  captureOutput: boolean;
65
77
  mask?: MaskFn;
66
78
  maxAttributeLength: number;
67
79
  batch: Required<BatchOptions>;
68
80
  spanFilter?: (span: ReadableSpan) => boolean;
81
+ sessionRootOf?: SessionRootOf;
69
82
  resourceAttributes?: Record<string, AttributeValue>;
70
83
  logLevel: SdkLogLevel;
71
84
  fetchImpl: typeof fetch;
72
85
  waitUntil?: (p: Promise<unknown>) => void;
73
- onError?: (error: unknown) => void;
86
+ onError?: (cause: Error) => void;
87
+ sdkName: string;
74
88
  }
75
89
 
76
90
  export const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
@@ -89,16 +103,19 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
89
103
  enabled: options.enabled ?? true,
90
104
  registerGlobal: options.registerGlobal ?? false,
91
105
  exportMode: options.exportMode ?? "batched",
106
+ sampler: options.sampler,
92
107
  captureInput: options.captureInput ?? true,
93
108
  captureOutput: options.captureOutput ?? true,
94
109
  mask: options.mask,
95
110
  maxAttributeLength: options.maxAttributeLength ?? DEFAULT_MAX_ATTRIBUTE_LENGTH,
96
111
  batch: { ...DEFAULT_BATCH, ...options.batch },
97
112
  spanFilter: options.spanFilter,
113
+ sessionRootOf: options.sessionRootOf,
98
114
  resourceAttributes: options.resourceAttributes,
99
115
  logLevel: options.logLevel ?? "warn",
100
116
  fetchImpl: options.fetch ?? globalThis.fetch,
101
117
  waitUntil: options.waitUntil,
102
118
  onError: options.onError,
119
+ sdkName: options.sdkName ?? "@telemetry-dev/sdk",
103
120
  };
104
121
  }
package/src/index.ts CHANGED
@@ -15,15 +15,25 @@ export type {
15
15
  SdkLogLevel,
16
16
  TelemetryOptions,
17
17
  } from "./config.ts";
18
- export { propagateAttributes, type PropagatedAttributes } from "@telemetry-dev/otel";
18
+ export {
19
+ propagateAttributes,
20
+ type PropagatedAttributes,
21
+ SCOPE_NAME,
22
+ TelemetrySpanProcessor,
23
+ type TelemetrySpanProcessorOptions,
24
+ } from "@telemetry-dev/otel";
19
25
  export { log, type LogOptions } from "./logs.ts";
20
26
  export { observe, type ObserveOptions } from "./observe.ts";
21
27
  export {
28
+ activeContext,
29
+ extractW3cContext,
22
30
  getTraceparent,
31
+ injectW3cContext,
23
32
  type ParentRef,
24
33
  type SpanHandle,
25
34
  startActiveSpan,
26
35
  startSpan,
27
36
  type StartSpanOptions,
28
37
  updateActiveSpan,
38
+ withContext,
29
39
  } 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,
@@ -15,7 +23,9 @@ import {
15
23
  PROPAGATED_KEY,
16
24
  propagatedFromContext,
17
25
  reportError,
26
+ sessionIdOf,
18
27
  withContext,
28
+ withSessionParent,
19
29
  } from "@telemetry-dev/otel";
20
30
 
21
31
  import {
@@ -33,8 +43,12 @@ export type ParentRef = string /* W3C traceparent */ | SpanHandle | SpanContext
33
43
  export interface StartSpanOptions extends SpanFields {
34
44
  /** Default "span" → operation "function". */
35
45
  type?: SpanType;
46
+ /** OpenTelemetry span kind; defaults to INTERNAL. */
47
+ kind?: SpanKind;
36
48
  /** Parent override; defaults to the active context. */
37
49
  parent?: ParentRef;
50
+ /** Links to associate with the span. */
51
+ links?: Link[];
38
52
  startTime?: Date | number;
39
53
  /** Per-call override of the global captureInput. */
40
54
  captureInput?: boolean;
@@ -59,7 +73,7 @@ export interface SpanHandle {
59
73
  interface SpanMeta {
60
74
  type: SpanType;
61
75
  capture: FieldCaptureConfig;
62
- onError?: (error: unknown) => void;
76
+ onError?: (cause: Error) => void;
63
77
  }
64
78
 
65
79
  // Remembers each span's type + capture flags so update()/end()/updateActiveSpan map
@@ -68,7 +82,7 @@ const SPAN_META = new WeakMap<Span, SpanMeta>();
68
82
 
69
83
  const SEVERITY_ERROR = 17;
70
84
 
71
- export function isThenable(value: unknown): value is PromiseLike<unknown> {
85
+ export function isThenable<T>(value: T): value is T & PromiseLike<Awaited<T>> {
72
86
  return (
73
87
  value != null &&
74
88
  (typeof value === "object" || typeof value === "function") &&
@@ -77,11 +91,11 @@ export function isThenable(value: unknown): value is PromiseLike<unknown> {
77
91
  );
78
92
  }
79
93
 
80
- function isContext(value: object): value is Context {
94
+ function isContext(value: Exclude<ParentRef, string>): value is Context {
81
95
  return "getValue" in value && typeof value.getValue === "function";
82
96
  }
83
97
 
84
- function isSpanHandle(value: object): value is SpanHandle {
98
+ function isSpanHandle(value: Exclude<ParentRef, string>): value is SpanHandle {
85
99
  return "span" in value && "context" in value && "update" in value;
86
100
  }
87
101
 
@@ -101,14 +115,22 @@ function resolveParent(parent: ParentRef | undefined): Context {
101
115
  if (!parsed) return base;
102
116
  return trace.setSpanContext(base, { ...parsed, isRemote: true });
103
117
  }
104
- if (isSpanHandle(parent)) return withAmbientPropagated(parent.context);
105
- if (isContext(parent)) return withAmbientPropagated(parent);
106
- return trace.setSpanContext(base, parent);
118
+ const ref = parent as Exclude<ParentRef, string>;
119
+ if (isSpanHandle(ref)) return withAmbientPropagated(ref.context);
120
+ if (isContext(ref)) return withAmbientPropagated(ref);
121
+ return trace.setSpanContext(base, ref);
107
122
  }
108
123
 
109
124
  function applyError(span: Span, error: unknown): void {
110
125
  const err = error instanceof Error ? error : undefined;
111
- const errorType = err?.name ?? "Error";
126
+ // Prefer the specific subclass name (Python parity: type(error).__name__) — many SDK
127
+ // error classes (e.g. openai's BadRequestError) never override the inherited "Error" name.
128
+ const errorType =
129
+ err === undefined
130
+ ? "Error"
131
+ : err.name !== "Error" && err.name.length > 0
132
+ ? err.name
133
+ : (err.constructor?.name ?? "Error") || "Error";
112
134
  const message = err?.message ?? String(error);
113
135
  span.setStatus({ code: SpanStatusCode.ERROR });
114
136
  span.setAttribute("error.type", errorType);
@@ -151,18 +173,32 @@ export function createSpanHandle(
151
173
  options: StartSpanOptions = {},
152
174
  ): SpanHandle {
153
175
  const meta = metaFor(core, options);
154
- const parentCtx = resolveParent(options.parent);
176
+ const resolved = resolveParent(options.parent);
177
+ const parentCtx = withSessionParent(
178
+ resolved,
179
+ sessionIdOf(resolved, options.attributes),
180
+ core.config.apiKey,
181
+ );
182
+ const attrs = fieldsToAttributes(options, meta.type, meta.capture);
155
183
  const span = core.tracer.startSpan(
156
184
  name,
157
185
  {
158
- kind: SpanKind.INTERNAL,
186
+ kind: options.kind ?? SpanKind.INTERNAL,
159
187
  startTime: options.startTime,
160
- attributes: { "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type] },
188
+ links: options.links,
189
+ attributes: {
190
+ "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type],
191
+ ...propagatedFromContext(parentCtx),
192
+ ...attrs,
193
+ },
161
194
  },
162
195
  parentCtx,
163
196
  );
164
197
  SPAN_META.set(span, meta);
165
- applyFields(span, meta, options);
198
+ // The processor stamps propagation on start; explicit fields still win.
199
+ if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
200
+ if (options.name) span.updateName(options.name);
201
+ if (options.error !== undefined) applyError(span, options.error);
166
202
 
167
203
  const spanContext = span.spanContext();
168
204
  const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, "0");
@@ -179,7 +215,7 @@ export function createSpanHandle(
179
215
  try {
180
216
  applyFields(span, meta, fields);
181
217
  } catch (error) {
182
- reportError(meta.onError, error);
218
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
183
219
  }
184
220
  return handle;
185
221
  },
@@ -187,7 +223,7 @@ export function createSpanHandle(
187
223
  try {
188
224
  if (fields) applyFields(span, meta, fields);
189
225
  } catch (error) {
190
- reportError(meta.onError, error);
226
+ reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
191
227
  }
192
228
  span.end(fields?.endTime);
193
229
  },
@@ -202,7 +238,7 @@ export function startSpan(name: string, options?: StartSpanOptions): SpanHandle
202
238
  try {
203
239
  return createSpanHandle(core, name, options);
204
240
  } catch (error) {
205
- reportError(core.config.onError, error);
241
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
206
242
  return NOOP_SPAN_HANDLE;
207
243
  }
208
244
  }
@@ -211,7 +247,7 @@ export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T
211
247
  try {
212
248
  const result = withContext(handle.context, () => fn(handle));
213
249
  if (isThenable(result)) {
214
- return (result as unknown as Promise<unknown>).then(
250
+ return result.then(
215
251
  (value) => {
216
252
  handle.end();
217
253
  return value;
@@ -241,10 +277,11 @@ export function startActiveSpan<T>(
241
277
  optionsOrFn: StartSpanOptions | ((span: SpanHandle) => T),
242
278
  maybeFn?: (span: SpanHandle) => T,
243
279
  ): T {
244
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn!;
245
- const options = typeof optionsOrFn === "function" ? undefined : optionsOrFn;
280
+ const isFn = typeof optionsOrFn === "function";
281
+ const fn = isFn ? optionsOrFn : maybeFn!;
282
+ const options = isFn ? undefined : optionsOrFn;
246
283
  const handle = startSpan(name, options);
247
- if (!handle.isRecording) return fn(handle);
284
+ if (handle === NOOP_SPAN_HANDLE) return fn(handle);
248
285
  return runWithHandle(handle, fn);
249
286
  }
250
287
 
@@ -258,15 +295,45 @@ export function updateActiveSpan(fields: SpanFields): void {
258
295
  const meta = SPAN_META.get(span) ?? metaFor(core);
259
296
  applyFields(span, meta, fields);
260
297
  } catch (error) {
261
- reportError(core.config.onError, error);
298
+ reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
262
299
  }
263
300
  }
264
301
 
265
- const PROPAGATOR = new W3CTraceContextPropagator();
302
+ const TRACE_CONTEXT_PROPAGATOR = new W3CTraceContextPropagator();
303
+ const W3C_PROPAGATOR = new CompositePropagator({
304
+ propagators: [TRACE_CONTEXT_PROPAGATOR, new W3CBaggagePropagator()],
305
+ });
306
+
307
+ export { activeContext, withContext };
308
+
309
+ /** Extract W3C trace context; baggage is omitted unless includeBaggage is true. */
310
+ export function extractW3cContext(
311
+ carrier: Record<string, unknown>,
312
+ options: { includeBaggage?: boolean } = {},
313
+ ): Context {
314
+ const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
315
+ return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
316
+ }
317
+
318
+ /** Inject W3C trace context; baggage is omitted unless includeBaggage is true. */
319
+ export function injectW3cContext(
320
+ context: Context,
321
+ carrier: Record<string, unknown>,
322
+ options: { includeBaggage?: boolean } = {},
323
+ ): void {
324
+ const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
325
+ for (const key of Object.keys(carrier)) {
326
+ const normalized = key.toLowerCase();
327
+ if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") {
328
+ delete carrier[key];
329
+ }
330
+ }
331
+ propagator.inject(context, carrier, defaultTextMapSetter);
332
+ }
266
333
 
267
334
  /** Serialize the active span context as a W3C traceparent; null when there is none. */
268
335
  export function getTraceparent(): string | null {
269
- const carrier: { traceparent?: string } = {};
270
- PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
336
+ const carrier = { traceparent: undefined };
337
+ TRACE_CONTEXT_PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
271
338
  return carrier.traceparent ?? null;
272
339
  }