@telemetry-dev/sdk 0.1.9 → 0.1.11

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/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, context, defaultTextMapGetter, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api";
1
+ import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, context, createContextKey, defaultTextMapGetter, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api";
2
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, DURATION_BUCKETS, OUTPUT_CHUNK_HISTOGRAM, PROPAGATED_KEY, SCOPE_NAME, SCOPE_NAME as SCOPE_NAME$1, SCOPE_VERSION, StampingSpanProcessor, TelemetrySpanProcessor, activeContext, activeContext as activeContext$1, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, sessionIdOf, sessionRootTracerProvider, sessionSampler, setLogLevel, withContext, withContext as withContext$1, withSessionParent } from "@telemetry-dev/otel";
6
+ import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, DURATION_BUCKETS, OUTPUT_CHUNK_HISTOGRAM, PROPAGATED_KEY, SCOPE_NAME, SCOPE_NAME as SCOPE_NAME$1, SCOPE_VERSION, StampingSpanProcessor, TelemetrySpanProcessor, activeContext, activeContext as activeContext$1, als, createExportFailures, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, sessionIdOf, sessionRootTracerProvider, sessionSampler, setLogLevel, sha256, withContext, withContext as withContext$1, withSessionParent } from "@telemetry-dev/otel";
7
7
  import { SeverityNumber } from "@opentelemetry/api-logs";
8
8
  //#region src/config.ts
9
9
  const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
@@ -102,9 +102,11 @@ function initInner(options, overrides) {
102
102
  "deployment.environment.name": config.environment,
103
103
  ...config.resourceAttributes
104
104
  });
105
+ const failures = createExportFailures(config.onError);
105
106
  const transport = {
106
107
  fetchImpl: config.fetchImpl,
107
- onError: config.onError
108
+ onError: config.onError,
109
+ failures
108
110
  };
109
111
  const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : void 0;
110
112
  const spanExporter = overrides?.spanExporter ?? createTraceExporter({
@@ -154,7 +156,7 @@ function initInner(options, overrides) {
154
156
  const ensureLogger = () => {
155
157
  if (!logExporter) return void 0;
156
158
  if (!logs) {
157
- const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor(logExporter) : new BatchLogRecordProcessor(logExporter);
159
+ const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor({ exporter: logExporter }) : new BatchLogRecordProcessor({ exporter: logExporter });
158
160
  const loggerProvider = new LoggerProvider({
159
161
  resource,
160
162
  logRecordLimits: { attributeValueLengthLimit: backstopLength },
@@ -173,7 +175,7 @@ function initInner(options, overrides) {
173
175
  const parts = [provider[op]()];
174
176
  if (logs) parts.push(logs[op]());
175
177
  if (metrics) parts.push(metrics[op]());
176
- return Promise.all(parts).then(() => void 0).catch((error) => reportError(config.onError, error));
178
+ return Promise.all(parts).then(() => void 0).catch((error) => failures.report(error));
177
179
  };
178
180
  const handoff = (p) => {
179
181
  if (config.waitUntil) {
@@ -356,8 +358,8 @@ function prepareCaptureValue(key, value, cfg) {
356
358
  let masked = value;
357
359
  if (cfg.mask) try {
358
360
  masked = cfg.mask(value, { key });
359
- } catch (error) {
360
- reportError(cfg.onError, error);
361
+ } catch {
362
+ reportError(cfg.onError, /* @__PURE__ */ new Error("mask hook raised; content dropped"));
361
363
  return;
362
364
  }
363
365
  const serialized = jsonAttr(masked);
@@ -544,6 +546,303 @@ const NOOP_SPAN_HANDLE = {
544
546
  end: () => {}
545
547
  };
546
548
  //#endregion
549
+ //#region src/prompts.ts
550
+ /**
551
+ * Why getPrompt failed. `code` is the code of the API error (`prompt_not_found`,
552
+ * `label_not_found`, `insufficient_scope`, `invalid_api_key`, …), or `missing_api_key`,
553
+ * `timeout`, `network_error`, `invalid_response`, or `type_mismatch`. A response with a different
554
+ * status and no API error gives `http_<status>`, for example `http_302` for a redirect: getPrompt
555
+ * does not go to the location of a redirect.
556
+ */
557
+ var PromptError = class extends Error {
558
+ name = "PromptError";
559
+ constructor(code, message, status, options) {
560
+ super(message, options);
561
+ this.code = code;
562
+ this.status = status;
563
+ }
564
+ };
565
+ const FETCH_TIMEOUT_MS = 1e4;
566
+ /** How long a failed fetch with no copy answers calls, so that they do not each fetch again. */
567
+ const ERROR_TTL_MS = 1e4;
568
+ const MAX_CACHE_ENTRIES = 1e3;
569
+ const VARIABLE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
570
+ const MODEL_CALL_OPERATIONS = new Set([
571
+ "chat",
572
+ "text_completion",
573
+ "generate_content"
574
+ ]);
575
+ const PROMPT_KEY = createContextKey("telemetry.dev prompt");
576
+ const cache = /* @__PURE__ */ new Map();
577
+ const compiledVariables = /* @__PURE__ */ new WeakMap();
578
+ async function getPrompt(name, options = {}) {
579
+ const type = options.type ?? "chat";
580
+ checkOptions(name, options, type);
581
+ const core = currentClient().core;
582
+ const env = resolveEnv();
583
+ const apiKey = options.apiKey ?? core?.config.apiKey ?? env.TELEMETRY_DEV_API_KEY;
584
+ const baseUrl = (options.baseUrl || core?.config.baseUrl || env.TELEMETRY_DEV_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
585
+ const label = options.version === void 0 ? options.label ?? "production" : null;
586
+ const onError = core?.config.onError;
587
+ let response;
588
+ try {
589
+ if (!apiKey) throw new PromptError("missing_api_key", "No API key: pass apiKey or set TELEMETRY_DEV_API_KEY");
590
+ const url = new URL(`${baseUrl}/v1/prompts`);
591
+ url.searchParams.set("name", name);
592
+ if (label === null) url.searchParams.set("version", String(options.version));
593
+ else url.searchParams.set("label", label);
594
+ response = await load([
595
+ baseUrl,
596
+ keyFingerprint(apiKey),
597
+ name,
598
+ label === null ? `version:${options.version}` : `label:${label}`
599
+ ].join("\0"), (options.cacheTtlSeconds ?? 60) * 1e3, () => fetchPrompt(url, {
600
+ authorization: `Bearer ${apiKey}`,
601
+ "x-telemetry-dev-sdk": core?.config.sdkName ?? "@telemetry-dev/sdk"
602
+ }, options.fetch ?? core?.config.fetchImpl ?? globalThis.fetch), onError);
603
+ } catch (error) {
604
+ if (options.fallback === void 0) throw error;
605
+ reportError(onError, error);
606
+ return createPrompt({
607
+ name,
608
+ type,
609
+ label,
610
+ version: null,
611
+ isFallback: true,
612
+ content: {
613
+ messages: type === "chat" ? options.fallback : [],
614
+ text: type === "text" ? options.fallback : null,
615
+ model: null,
616
+ parameters: {},
617
+ tools: [],
618
+ responseFormat: { type: "text" }
619
+ }
620
+ });
621
+ }
622
+ if (response.type !== type) throw new PromptError("type_mismatch", `${name} is a ${response.type} prompt; pass type: "${response.type}"`);
623
+ const chosen = response.rollout && inRollout(response) ? response.rollout : response;
624
+ return createPrompt({
625
+ name: response.name,
626
+ type,
627
+ label: response.label,
628
+ version: chosen.version,
629
+ isFallback: false,
630
+ content: chosen
631
+ });
632
+ }
633
+ /**
634
+ * Runs `fn` with `prompt` as the prompt of each model call (generation span) that starts inside
635
+ * it: the spans get `td.prompt.name`, `td.prompt.version`, `td.prompt.label`, and, when input
636
+ * capture is on, `td.prompt.variables`. The variables are those of the last `compile()` of this
637
+ * prompt object before withPrompt starts, or of the last `compile()` inside `fn`, so a compile in
638
+ * another task does not change them once withPrompt runs.
639
+ */
640
+ function withPrompt(prompt, fn) {
641
+ const scope = {
642
+ prompt,
643
+ variables: compiledVariables.get(prompt)
644
+ };
645
+ return withContext$1(activeContext$1().setValue(PROMPT_KEY, scope), fn);
646
+ }
647
+ /** The td.prompt.* attributes for a span with this operation that starts in `ctx`. */
648
+ function promptAttributes(ctx, operation, capture) {
649
+ if (!MODEL_CALL_OPERATIONS.has(operation)) return void 0;
650
+ const scope = ctx.getValue(PROMPT_KEY) ?? activeContext$1().getValue(PROMPT_KEY);
651
+ if (!scope) return void 0;
652
+ const { prompt, variables } = scope;
653
+ return omitUndefined({
654
+ "td.prompt.name": prompt.name,
655
+ "td.prompt.version": prompt.version ?? void 0,
656
+ "td.prompt.label": prompt.label ?? void 0,
657
+ "td.prompt.variables": variables !== void 0 && capture.captureInput ? prepareCaptureValue("td.prompt.variables", variables, capture) : void 0
658
+ });
659
+ }
660
+ function checkOptions(name, options, type) {
661
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("name must be a non-empty string");
662
+ if (type !== "chat" && type !== "text") throw new TypeError("type must be \"chat\" or \"text\"");
663
+ if (options.label !== void 0 && options.version !== void 0) throw new TypeError("Pass label or version, not both");
664
+ if (options.version !== void 0 && (!Number.isSafeInteger(options.version) || options.version < 1)) throw new RangeError("version must be a positive integer");
665
+ const ttl = options.cacheTtlSeconds;
666
+ if (ttl !== void 0 && !(Number.isFinite(ttl) && ttl >= 0)) throw new RangeError("cacheTtlSeconds must be a non-negative number");
667
+ if (options.fallback !== void 0 && (type === "chat" ? !Array.isArray(options.fallback) : typeof options.fallback !== "string")) throw new TypeError(type === "chat" ? "fallback must be a list of messages" : "fallback must be a string");
668
+ }
669
+ /**
670
+ * Returns the kept copy of a prompt, and fetches a new copy in the background when it is older
671
+ * than the TTL. Only a call with no copy waits for a fetch. A failed fetch with no copy fails the
672
+ * calls of the next ERROR_TTL_MS (or the TTL, if it is shorter) without a fetch.
673
+ */
674
+ async function load(key, ttlMs, fetchResponse, onError) {
675
+ let entry = cache.get(key);
676
+ if (!entry) {
677
+ if (cache.size >= MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value);
678
+ entry = { checkedAt: 0 };
679
+ cache.set(key, entry);
680
+ }
681
+ const fresh = Date.now() < entry.checkedAt + (entry.response ? ttlMs : Math.min(ttlMs, ERROR_TTL_MS));
682
+ if (entry.response) {
683
+ if (!fresh) entry.pending ??= refresh(entry, fetchResponse, onError);
684
+ return entry.response;
685
+ }
686
+ if (fresh) throw entry.error;
687
+ return entry.pending ??= refresh(entry, fetchResponse, onError);
688
+ }
689
+ function refresh(entry, fetchResponse, onError) {
690
+ return fetchResponse().then((response) => {
691
+ entry.response = response;
692
+ return response;
693
+ }, (error) => {
694
+ if (!entry.response) {
695
+ entry.error = error;
696
+ throw error;
697
+ }
698
+ reportError(onError, error);
699
+ return entry.response;
700
+ }).finally(() => {
701
+ entry.checkedAt = Date.now();
702
+ entry.pending = void 0;
703
+ });
704
+ }
705
+ async function fetchPrompt(url, headers, fetchImpl) {
706
+ const controller = new AbortController();
707
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
708
+ try {
709
+ const res = await fetchImpl(url, {
710
+ method: "GET",
711
+ headers,
712
+ signal: controller.signal,
713
+ redirect: "manual"
714
+ });
715
+ const body = await res.json().catch(() => void 0);
716
+ if (!res.ok) {
717
+ const error = isRecord(body) && isRecord(body.error) ? body.error : {};
718
+ throw new PromptError(typeof error.code === "string" ? error.code : `http_${res.status}`, typeof error.message === "string" ? error.message : `telemetry.dev returned ${res.status}`, res.status);
719
+ }
720
+ const response = parseResponse(body);
721
+ if (!response) throw new PromptError("invalid_response", "telemetry.dev returned a prompt it cannot read");
722
+ return response;
723
+ } catch (error) {
724
+ if (controller.signal.aborted) throw new PromptError("timeout", `Fetching the prompt took more than ${FETCH_TIMEOUT_MS} ms`);
725
+ if (error instanceof PromptError) throw error;
726
+ throw new PromptError("network_error", "Could not reach telemetry.dev", void 0, { cause: error });
727
+ } finally {
728
+ clearTimeout(timer);
729
+ }
730
+ }
731
+ function createPrompt(input) {
732
+ const { content, type } = input;
733
+ const prompt = Object.freeze({
734
+ name: input.name,
735
+ type,
736
+ version: input.version,
737
+ label: input.label,
738
+ messages: content.messages,
739
+ text: content.text,
740
+ model: content.model,
741
+ parameters: content.parameters,
742
+ tools: content.tools,
743
+ responseFormat: content.responseFormat,
744
+ isFallback: input.isFallback,
745
+ compile(variables = {}) {
746
+ compiledVariables.set(prompt, variables);
747
+ const scope = activeContext$1().getValue(PROMPT_KEY);
748
+ if (scope?.prompt === prompt) scope.variables = variables;
749
+ return type === "chat" ? compileMessages(content.messages, variables) : fill(content.text ?? "", variables);
750
+ }
751
+ });
752
+ return prompt;
753
+ }
754
+ function compileMessages(messages, variables) {
755
+ return messages.flatMap((message) => {
756
+ if (message.role !== "placeholder") return [{
757
+ role: message.role,
758
+ content: fill(message.content, variables)
759
+ }];
760
+ const value = Object.hasOwn(variables, message.name) ? variables[message.name] : void 0;
761
+ if (value == null) return [];
762
+ if (!Array.isArray(value)) throw new TypeError(`The ${message.name} variable must be a list of messages`);
763
+ return value;
764
+ });
765
+ }
766
+ function fill(text, variables) {
767
+ return text.replace(VARIABLE, (_tag, name) => {
768
+ const value = Object.hasOwn(variables, name) ? variables[name] : void 0;
769
+ if (value == null) return "";
770
+ return typeof value === "string" ? value : jsonAttr(value) ?? "";
771
+ });
772
+ }
773
+ function inRollout(response) {
774
+ const sessionId = sessionIdOf(activeContext$1()) ?? currentClient().core?.config.processSessionId;
775
+ return (sessionId === void 0 ? Math.floor(Math.random() * 100) : fnv1a(`${response.name}:${response.label}:${sessionId}`) % 100) < response.rollout.percent;
776
+ }
777
+ function fnv1a(text) {
778
+ let hash = 2166136261;
779
+ for (const byte of new TextEncoder().encode(text)) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
780
+ return hash;
781
+ }
782
+ function keyFingerprint(apiKey) {
783
+ const digest = sha256(new TextEncoder().encode(apiKey)).subarray(0, 16);
784
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
785
+ }
786
+ function parseResponse(body) {
787
+ if (!isRecord(body)) return void 0;
788
+ const { name, type, label, version, rollout } = body;
789
+ if (typeof name !== "string" || type !== "chat" && type !== "text" || label !== null && typeof label !== "string" || !isVersion(version)) return;
790
+ const content = parseContent(body, type);
791
+ if (!content) return void 0;
792
+ if (rollout == null) return deepFreeze({
793
+ name,
794
+ type,
795
+ label,
796
+ version,
797
+ ...content,
798
+ rollout: null
799
+ });
800
+ if (!isRecord(rollout) || !isVersion(rollout.version) || !Number.isInteger(rollout.percent) || rollout.percent < 0 || rollout.percent > 100) return;
801
+ const rolloutContent = parseContent(rollout, type);
802
+ if (!rolloutContent) return void 0;
803
+ return deepFreeze({
804
+ name,
805
+ type,
806
+ label,
807
+ version,
808
+ ...content,
809
+ rollout: {
810
+ percent: rollout.percent,
811
+ version: rollout.version,
812
+ ...rolloutContent
813
+ }
814
+ });
815
+ }
816
+ function parseContent(value, type) {
817
+ const { messages, text, model, parameters, tools, responseFormat } = value;
818
+ if (!(type === "chat" ? Array.isArray(messages) && messages.every(isPromptMessage) : typeof text === "string") || !isRecord(model) || typeof model.provider !== "string" || typeof model.name !== "string" || !isRecord(parameters) || !Array.isArray(tools) || !tools.every((tool) => isRecord(tool) && typeof tool.name === "string") || !isRecord(responseFormat) || typeof responseFormat.type !== "string") return;
819
+ return {
820
+ messages: type === "chat" ? messages : [],
821
+ text: type === "text" ? text : null,
822
+ model,
823
+ parameters,
824
+ tools,
825
+ responseFormat
826
+ };
827
+ }
828
+ function isPromptMessage(value) {
829
+ if (!isRecord(value)) return false;
830
+ return value.role === "placeholder" ? typeof value.name === "string" : (value.role === "system" || value.role === "user" || value.role === "assistant") && typeof value.content === "string";
831
+ }
832
+ function isVersion(value) {
833
+ return Number.isSafeInteger(value) && value >= 1;
834
+ }
835
+ function isRecord(value) {
836
+ return typeof value === "object" && value !== null && !Array.isArray(value);
837
+ }
838
+ function deepFreeze(value) {
839
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
840
+ Object.freeze(value);
841
+ for (const item of Object.values(value)) deepFreeze(item);
842
+ }
843
+ return value;
844
+ }
845
+ //#endregion
547
846
  //#region src/span.ts
548
847
  const SPAN_META = /* @__PURE__ */ new WeakMap();
549
848
  const SEVERITY_ERROR = 17;
@@ -577,16 +876,16 @@ function resolveParent(parent) {
577
876
  if (isContext(ref)) return withAmbientPropagated(ref);
578
877
  return trace.setSpanContext(base, ref);
579
878
  }
580
- function applyError(span, error) {
879
+ function applyError(span, error, capture) {
581
880
  const err = error instanceof Error ? error : void 0;
582
881
  const errorType = err === void 0 ? "Error" : err.name !== "Error" && err.name.length > 0 ? err.name : (err.constructor?.name ?? "Error") || "Error";
583
- const message = err?.message ?? String(error);
584
882
  span.setStatus({ code: SpanStatusCode.ERROR });
585
883
  span.setAttribute("error.type", errorType);
884
+ const captureContent = capture.captureInput && capture.captureOutput;
586
885
  span.addEvent("exception", omitUndefined({
587
886
  "exception.type": errorType,
588
- "exception.message": message,
589
- "exception.stacktrace": err?.stack,
887
+ "exception.message": captureContent ? prepareCaptureValue("exception.message", err?.message ?? String(error), capture) : void 0,
888
+ "exception.stacktrace": captureContent && err?.stack !== void 0 ? prepareCaptureValue("exception.stacktrace", err.stack, capture) : void 0,
590
889
  "log.severity_number": SEVERITY_ERROR
591
890
  }));
592
891
  }
@@ -594,7 +893,7 @@ function applyFields(span, meta, fields) {
594
893
  if (fields.name) span.updateName(fields.name);
595
894
  const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
596
895
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
597
- if (fields.error !== void 0) applyError(span, fields.error);
896
+ if (fields.error !== void 0) applyError(span, fields.error, meta.capture);
598
897
  }
599
898
  function metaFor(core, options) {
600
899
  const cfg = core.config;
@@ -619,6 +918,7 @@ function createSpanHandle(core, name, options = {}) {
619
918
  "gen_ai.conversation.id": sessionId
620
919
  }), sessionId, core.config.apiKey);
621
920
  const attrs = fieldsToAttributes(options, meta.type, meta.capture);
921
+ const operation = attrs["gen_ai.operation.name"] ?? SPAN_TYPE_OPERATIONS[meta.type];
622
922
  const span = core.tracer.startSpan(name, {
623
923
  kind: options.kind ?? SpanKind.INTERNAL,
624
924
  startTime: options.startTime,
@@ -626,13 +926,14 @@ function createSpanHandle(core, name, options = {}) {
626
926
  attributes: {
627
927
  "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type],
628
928
  ...propagatedFromContext(parentCtx),
929
+ ...promptAttributes(parentCtx, operation, meta.capture),
629
930
  ...attrs
630
931
  }
631
932
  }, parentCtx);
632
933
  SPAN_META.set(span, meta);
633
934
  if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
634
935
  if (options.name) span.updateName(options.name);
635
- if (options.error !== void 0) applyError(span, options.error);
936
+ if (options.error !== void 0) applyError(span, options.error, meta.capture);
636
937
  const spanContext = span.spanContext();
637
938
  let previousOutputChunkTime;
638
939
  const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
@@ -813,4 +1114,4 @@ function observe(fn, options) {
813
1114
  return wrapped;
814
1115
  }
815
1116
  //#endregion
816
- export { SCOPE_NAME, TelemetrySpanProcessor, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, captureMasked, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
1117
+ export { PromptError, SCOPE_NAME, TelemetrySpanProcessor, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, captureMasked, extractW3cContext, flush, getPrompt, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext, withPrompt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Pure TypeScript SDK for telemetry.dev: LLM traces, logs, and GenAI metrics on OpenTelemetry, for Node and edge runtimes.",
5
5
  "keywords": [
6
6
  "genai",
@@ -36,13 +36,13 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@opentelemetry/api-logs": "^0.218.0",
40
- "@opentelemetry/core": "^2.7.1",
41
- "@opentelemetry/resources": "^2.7.1",
42
- "@opentelemetry/sdk-logs": "^0.218.0",
43
- "@opentelemetry/sdk-metrics": "^2.7.1",
44
- "@opentelemetry/sdk-trace-base": "^2.7.1",
45
- "@telemetry-dev/otel": "^0.1.6"
39
+ "@opentelemetry/api-logs": "^0.223.0",
40
+ "@opentelemetry/core": "^2.12.0",
41
+ "@opentelemetry/resources": "^2.12.0",
42
+ "@opentelemetry/sdk-logs": "^0.223.0",
43
+ "@opentelemetry/sdk-metrics": "^2.12.0",
44
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
45
+ "@telemetry-dev/otel": "^0.1.7"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opentelemetry/api": "^1.9.1",
package/src/capture.ts CHANGED
@@ -270,8 +270,9 @@ export function prepareCaptureValue(
270
270
  if (cfg.mask) {
271
271
  try {
272
272
  masked = cfg.mask(value, { key });
273
- } catch (error) {
274
- reportError(cfg.onError, error);
273
+ } catch {
274
+ // Hook errors can contain the unmasked value or chain the application exception.
275
+ reportError(cfg.onError, new Error("mask hook raised; content dropped"));
275
276
 
276
277
  return undefined;
277
278
  }
package/src/client.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  als,
20
20
  AlsContextManager,
21
21
  BATCHED_METRIC_INTERVAL_MS,
22
+ createExportFailures,
22
23
  createLogExporter,
23
24
  createMetricExporter,
24
25
  createMetricsPipeline,
@@ -156,7 +157,10 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
156
157
  ...config.resourceAttributes,
157
158
  });
158
159
 
159
- const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
160
+ // A failed flush repeats the failure of its export, so the flush and the exporters share one
161
+ // failure state.
162
+ const failures = createExportFailures(config.onError);
163
+ const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError, failures };
160
164
  const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
161
165
 
162
166
  const spanExporter =
@@ -244,8 +248,8 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
244
248
  if (!logs) {
245
249
  const logProcessor =
246
250
  config.exportMode === "immediate"
247
- ? new SimpleLogRecordProcessor(logExporter)
248
- : new BatchLogRecordProcessor(logExporter);
251
+ ? new SimpleLogRecordProcessor({ exporter: logExporter })
252
+ : new BatchLogRecordProcessor({ exporter: logExporter });
249
253
 
250
254
  const loggerProvider = new LoggerProvider({
251
255
  resource,
@@ -274,7 +278,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
274
278
 
275
279
  return Promise.all(parts)
276
280
  .then(() => undefined)
277
- .catch((error: Error) => reportError(config.onError, error));
281
+ .catch((error: Error) => failures.report(error));
278
282
  };
279
283
 
280
284
  const handoff = (p: Promise<void>): Promise<void> => {
package/src/config.ts CHANGED
@@ -14,15 +14,15 @@ export type { BatchOptions, ExportMode, LogLevel, SdkLogLevel } from "@telemetry
14
14
 
15
15
  /**
16
16
  * Redaction hook applied to captured content (input/output, log messages and attributes, and
17
- * content passed to `SpanHandle.serializeContent`) before JSON stringification. Not applied to
18
- * metadata or raw attributes.
17
+ * content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
18
+ * JSON stringification. Not applied to metadata or raw attributes.
19
19
  */
20
20
  export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
21
21
 
22
22
  export interface TelemetryOptions {
23
23
  /** telemetry.dev ingest key (`td_live_…`). Falls back to `TELEMETRY_DEV_API_KEY`. When absent the SDK is a no-op. */
24
24
  apiKey?: string;
25
- /** Ingest base URL. Falls back to `TELEMETRY_DEV_BASE_URL`, then `https://ingest.telemetry.dev`. */
25
+ /** Base URL for telemetry export and getPrompt. Falls back to `TELEMETRY_DEV_BASE_URL`, then `https://ingest.telemetry.dev`. */
26
26
  baseUrl?: string;
27
27
  /** Deployment environment label. Falls back to `TELEMETRY_DEV_ENVIRONMENT`, then `production`. */
28
28
  environment?: string;
@@ -38,9 +38,9 @@ export interface TelemetryOptions {
38
38
  exportMode?: ExportMode;
39
39
  /** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
40
40
  sampler?: Sampler;
41
- /** Capture span inputs (args, prompts) by default. Default true. */
41
+ /** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
42
42
  captureInput?: boolean;
43
- /** Capture span outputs (results, completions) by default. Default true. */
43
+ /** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
44
44
  captureOutput?: boolean;
45
45
  /** Redaction hook run on captured content before stringify/truncate. */
46
46
  mask?: MaskFn;
@@ -63,7 +63,7 @@ export interface TelemetryOptions {
63
63
  fetch?: typeof fetch;
64
64
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
65
65
  waitUntil?: (p: Promise<unknown>) => void;
66
- /** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never throws into user code. */
66
+ /** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. A series of failed exports is reported once, and the next failure after an export reaches the ingest is reported again. The SDK never throws into user code. */
67
67
  onError?: (cause: Error) => void;
68
68
  /** Header value for a custom telemetry.dev integration. */
69
69
  sdkName?: string;
package/src/index.ts CHANGED
@@ -46,6 +46,25 @@ export { log, type LogOptions } from "./logs.ts";
46
46
 
47
47
  export { observe, type ObserveOptions } from "./observe.ts";
48
48
 
49
+ export {
50
+ type ChatMessage,
51
+ type ChatPrompt,
52
+ type GetChatPromptOptions,
53
+ getPrompt,
54
+ type GetPromptOptions,
55
+ type GetTextPromptOptions,
56
+ type Prompt,
57
+ PromptError,
58
+ type PromptMessage,
59
+ type PromptModel,
60
+ type PromptParameters,
61
+ type PromptResponseFormat,
62
+ type PromptTool,
63
+ type PromptVariables,
64
+ type TextPrompt,
65
+ withPrompt,
66
+ } from "./prompts.ts";
67
+
49
68
  export {
50
69
  activeContext,
51
70
  extractW3cContext,