@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/README.md +81 -5
- package/dist/index.d.mts +160 -7
- package/dist/index.mjs +315 -14
- package/package.json +8 -8
- package/src/capture.ts +3 -2
- package/src/client.ts +8 -4
- package/src/config.ts +6 -6
- package/src/index.ts +19 -0
- package/src/prompts.ts +701 -0
- package/src/span.ts +15 -6
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) =>
|
|
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
|
|
360
|
-
reportError(cfg.onError,
|
|
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.
|
|
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.
|
|
40
|
-
"@opentelemetry/core": "^2.
|
|
41
|
-
"@opentelemetry/resources": "^2.
|
|
42
|
-
"@opentelemetry/sdk-logs": "^0.
|
|
43
|
-
"@opentelemetry/sdk-metrics": "^2.
|
|
44
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
45
|
-
"@telemetry-dev/otel": "^0.1.
|
|
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
|
|
274
|
-
|
|
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
|
-
|
|
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) =>
|
|
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`)
|
|
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
|
-
/**
|
|
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)
|
|
41
|
+
/** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
|
|
42
42
|
captureInput?: boolean;
|
|
43
|
-
/** Capture span outputs (results, completions)
|
|
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,
|