retrace-sdk 0.4.0 → 0.5.0
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/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interceptors/openai.js +104 -3
- package/dist/recorder.js +3 -2
- package/dist/replay.d.ts +5 -2
- package/dist/replay.js +21 -25
- package/dist/traceparent.d.ts +28 -0
- package/dist/traceparent.js +56 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +43 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface Config {
|
|
|
5
5
|
projectId: string | undefined;
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
sampleRate: number;
|
|
8
|
+
/** Optional seed for deterministic sampling. When set, the same trace name always produces the same sample decision. */
|
|
9
|
+
sampleSeed: string | undefined;
|
|
8
10
|
}
|
|
9
11
|
export declare function configure(opts: Partial<Config>): Config;
|
|
10
12
|
export declare function requireApiKey(): string;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ const config = {
|
|
|
5
5
|
projectId: process.env.RETRACE_PROJECT_ID || undefined,
|
|
6
6
|
enabled: !["false", "0"].includes((process.env.RETRACE_ENABLED || "true").toLowerCase()),
|
|
7
7
|
sampleRate: parseFloat(process.env.RETRACE_SAMPLE_RATE || "1"),
|
|
8
|
+
sampleSeed: process.env.RETRACE_SAMPLE_SEED || undefined,
|
|
8
9
|
};
|
|
9
10
|
config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
10
11
|
export function configure(opts) {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,3 +11,4 @@ export { registerResumable, handleResume } from "./resume.js";
|
|
|
11
11
|
export type { ResumeCommand } from "./resume.js";
|
|
12
12
|
export { isReplaying, consumeCassetteEntry, handleReplay } from "./replay.js";
|
|
13
13
|
export type { CassetteEntry, ReplayCommand } from "./replay.js";
|
|
14
|
+
export { setTraceContext, clearTraceContext, getTraceparent, injectTraceparent, parseTraceparent } from "./traceparent.js";
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./in
|
|
|
8
8
|
export { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceConnectionError, RetraceRateLimitError } from "./errors.js";
|
|
9
9
|
export { registerResumable, handleResume } from "./resume.js";
|
|
10
10
|
export { isReplaying, consumeCassetteEntry, handleReplay } from "./replay.js";
|
|
11
|
+
export { setTraceContext, clearTraceContext, getTraceparent, injectTraceparent, parseTraceparent } from "./traceparent.js";
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { SpanType } from "../trace.js";
|
|
2
2
|
import { genId, nowIso, truncateJson } from "../utils.js";
|
|
3
3
|
import { isReplaying, consumeCassetteEntry } from "../replay.js";
|
|
4
|
-
|
|
4
|
+
import { getConfig } from "../config.js";
|
|
5
|
+
import { RetraceRateLimitError, RetraceAuthError, RetraceConnectionError } from "../errors.js";
|
|
6
|
+
/** Hardcoded fallback pricing ($/1M tokens: [input, output]). Updated periodically. */
|
|
7
|
+
const FALLBACK_PRICING = {
|
|
5
8
|
"gpt-5.5-pro": [30.0, 180.0],
|
|
6
9
|
"gpt-5.5": [5.0, 30.0],
|
|
7
10
|
"gpt-5.4-pro": [15.0, 90.0],
|
|
@@ -20,8 +23,35 @@ const PRICING = {
|
|
|
20
23
|
"o4-mini": [1.10, 4.40],
|
|
21
24
|
"o3-mini": [1.10, 4.40],
|
|
22
25
|
};
|
|
26
|
+
let livePricing = null;
|
|
27
|
+
let pricingFetchedAt = 0;
|
|
28
|
+
const PRICING_TTL_MS = 3600_000; // Refresh every hour
|
|
29
|
+
/** Fetch live pricing from Retrace API. Falls back to hardcoded on failure. */
|
|
30
|
+
async function fetchPricing() {
|
|
31
|
+
if (livePricing && Date.now() - pricingFetchedAt < PRICING_TTL_MS)
|
|
32
|
+
return livePricing;
|
|
33
|
+
try {
|
|
34
|
+
const cfg = getConfig();
|
|
35
|
+
const res = await fetch(`${cfg.baseUrl}/api/v1/pricing/models`, {
|
|
36
|
+
signal: AbortSignal.timeout(3000),
|
|
37
|
+
});
|
|
38
|
+
if (res.ok) {
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
if (data && typeof data === "object") {
|
|
41
|
+
livePricing = data;
|
|
42
|
+
pricingFetchedAt = Date.now();
|
|
43
|
+
return livePricing;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch { /* silent — use fallback */ }
|
|
48
|
+
return FALLBACK_PRICING;
|
|
49
|
+
}
|
|
50
|
+
// Kick off initial fetch (non-blocking)
|
|
51
|
+
fetchPricing().catch(() => { });
|
|
23
52
|
function calcCost(model, inputTokens, outputTokens) {
|
|
24
|
-
|
|
53
|
+
const pricing = livePricing || FALLBACK_PRICING;
|
|
54
|
+
for (const [key, p] of Object.entries(pricing)) {
|
|
25
55
|
if (model.includes(key))
|
|
26
56
|
return (inputTokens * p[0] + outputTokens * p[1]) / 1_000_000;
|
|
27
57
|
}
|
|
@@ -64,9 +94,18 @@ function createPatchedCreate() {
|
|
|
64
94
|
const opts = args[0] || {};
|
|
65
95
|
const model = opts.model || "unknown";
|
|
66
96
|
const messages = opts.messages || [];
|
|
97
|
+
const isStreaming = !!opts.stream;
|
|
67
98
|
const spanId = genId();
|
|
68
99
|
const startedAt = nowIso();
|
|
69
100
|
const startMs = Date.now();
|
|
101
|
+
// Capture vision (image_url) and structured output (response_format) metadata
|
|
102
|
+
const hasVision = messages.some((m) => Array.isArray(m.content) && m.content.some((p) => p.type === "image_url"));
|
|
103
|
+
const responseFormat = opts.response_format;
|
|
104
|
+
const spanMetadata = {};
|
|
105
|
+
if (hasVision)
|
|
106
|
+
spanMetadata.vision = true;
|
|
107
|
+
if (responseFormat)
|
|
108
|
+
spanMetadata.structured_output = typeof responseFormat === "object" ? responseFormat.type || "json_schema" : responseFormat;
|
|
70
109
|
// During replay, return mocked response from cassette instead of calling the real API
|
|
71
110
|
if (isReplaying()) {
|
|
72
111
|
const entry = consumeCassetteEntry("openai.chat.completions.create", "llm_call");
|
|
@@ -92,6 +131,57 @@ function createPatchedCreate() {
|
|
|
92
131
|
}
|
|
93
132
|
try {
|
|
94
133
|
const result = await originalCreate.apply(this, args);
|
|
134
|
+
// Streaming response: wrap the async iterator to collect chunks
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
if (isStreaming && result && typeof result[Symbol.asyncIterator] === "function") {
|
|
137
|
+
const chunks = [];
|
|
138
|
+
let inputTokens = 0;
|
|
139
|
+
let outputTokens = 0;
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
|
+
const originalIterator = result[Symbol.asyncIterator]();
|
|
142
|
+
const wrappedStream = {
|
|
143
|
+
[Symbol.asyncIterator]() {
|
|
144
|
+
return {
|
|
145
|
+
async next() {
|
|
146
|
+
const { value, done } = await originalIterator.next();
|
|
147
|
+
if (done) {
|
|
148
|
+
// Stream complete — emit span
|
|
149
|
+
const durationMs = Date.now() - startMs;
|
|
150
|
+
const output = chunks.join("");
|
|
151
|
+
const span = {
|
|
152
|
+
id: spanId, trace_id: "", parent_id: null,
|
|
153
|
+
span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
|
|
154
|
+
input: truncateJson({ messages: messages.slice(0, 10) }),
|
|
155
|
+
output: truncateJson(output),
|
|
156
|
+
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
157
|
+
cost: calcCost(model, inputTokens, outputTokens),
|
|
158
|
+
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
159
|
+
metadata: { streaming: true },
|
|
160
|
+
};
|
|
161
|
+
onSpanCallback?.(span);
|
|
162
|
+
return { value: undefined, done: true };
|
|
163
|
+
}
|
|
164
|
+
// Collect content delta
|
|
165
|
+
const delta = value?.choices?.[0]?.delta?.content;
|
|
166
|
+
if (delta)
|
|
167
|
+
chunks.push(delta);
|
|
168
|
+
// Collect usage from final chunk
|
|
169
|
+
if (value?.usage) {
|
|
170
|
+
inputTokens = value.usage.prompt_tokens || 0;
|
|
171
|
+
outputTokens = value.usage.completion_tokens || 0;
|
|
172
|
+
}
|
|
173
|
+
return { value, done: false };
|
|
174
|
+
},
|
|
175
|
+
return() { return originalIterator.return?.() ?? Promise.resolve({ value: undefined, done: true }); },
|
|
176
|
+
throw(e) { return originalIterator.throw?.(e) ?? Promise.reject(e); },
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
// Preserve tee/controller methods if present
|
|
180
|
+
...(result.controller ? { controller: result.controller } : {}),
|
|
181
|
+
};
|
|
182
|
+
return wrappedStream;
|
|
183
|
+
}
|
|
184
|
+
// Non-streaming response
|
|
95
185
|
const durationMs = Date.now() - startMs;
|
|
96
186
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
187
|
const res = result;
|
|
@@ -101,11 +191,12 @@ function createPatchedCreate() {
|
|
|
101
191
|
const span = {
|
|
102
192
|
id: spanId, trace_id: "", parent_id: null,
|
|
103
193
|
span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
|
|
104
|
-
input: truncateJson({ messages: messages.slice(0, 10) }),
|
|
194
|
+
input: truncateJson({ messages: messages.slice(0, 10), ...(responseFormat ? { response_format: responseFormat } : {}) }),
|
|
105
195
|
output: truncateJson(output),
|
|
106
196
|
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
107
197
|
cost: calcCost(model, inputTokens, outputTokens),
|
|
108
198
|
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
199
|
+
...(Object.keys(spanMetadata).length ? { metadata: spanMetadata } : {}),
|
|
109
200
|
};
|
|
110
201
|
onSpanCallback?.(span);
|
|
111
202
|
return result;
|
|
@@ -120,6 +211,16 @@ function createPatchedCreate() {
|
|
|
120
211
|
error: err instanceof Error ? err.message : String(err),
|
|
121
212
|
};
|
|
122
213
|
onSpanCallback?.(span);
|
|
214
|
+
// Wrap provider errors in typed Retrace exceptions for user-facing clarity
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
+
const status = err?.status || err?.response?.status;
|
|
217
|
+
if (status === 429)
|
|
218
|
+
throw new RetraceRateLimitError(parseInt(err?.headers?.["retry-after"] || "60", 10));
|
|
219
|
+
if (status === 401 || status === 403)
|
|
220
|
+
throw new RetraceAuthError(`OpenAI auth failed: ${err.message}`);
|
|
221
|
+
if (err?.message?.includes("ECONNREFUSED") || err?.message?.includes("fetch failed")) {
|
|
222
|
+
throw new RetraceConnectionError(`OpenAI connection failed: ${err.message}`);
|
|
223
|
+
}
|
|
123
224
|
throw err;
|
|
124
225
|
}
|
|
125
226
|
};
|
package/dist/recorder.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getConfig, requireApiKey } from "./config.js";
|
|
2
2
|
import { SpanBuilder, SpanType, TraceBuilder, TraceStatus } from "./trace.js";
|
|
3
3
|
import { createTransport } from "./transport.js";
|
|
4
|
+
import { shouldSample } from "./utils.js";
|
|
4
5
|
import { installGeminiInterceptor } from "./interceptors/gemini.js";
|
|
5
6
|
import { installOpenAIInterceptor } from "./interceptors/openai.js";
|
|
6
7
|
import { installAnthropicInterceptor } from "./interceptors/anthropic.js";
|
|
@@ -108,7 +109,7 @@ export class TraceRecorder {
|
|
|
108
109
|
}
|
|
109
110
|
export function record(opts) {
|
|
110
111
|
const cfg = getConfig();
|
|
111
|
-
if (!cfg.enabled ||
|
|
112
|
+
if (!cfg.enabled || !shouldSample(cfg.sampleRate, cfg.sampleSeed, opts?.name)) {
|
|
112
113
|
// Return a typed no-op stub (preserves type safety unlike Proxy)
|
|
113
114
|
return {
|
|
114
115
|
get traceId() { return ""; },
|
|
@@ -131,7 +132,7 @@ export function trace(fn, opts) {
|
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
return (...args) => {
|
|
134
|
-
if (!cfg.enabled ||
|
|
135
|
+
if (!cfg.enabled || !shouldSample(cfg.sampleRate, cfg.sampleSeed, opts?.name || fn.name))
|
|
135
136
|
return fn(...args);
|
|
136
137
|
const recorder = new TraceRecorder({
|
|
137
138
|
name: opts?.name || fn.name || "anonymous",
|
package/dist/replay.d.ts
CHANGED
|
@@ -25,18 +25,21 @@ export interface ReplayCommand {
|
|
|
25
25
|
cassette: CassetteEntry[];
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Check if a replay is currently active.
|
|
28
|
+
* Check if a replay is currently active in this async context.
|
|
29
29
|
*/
|
|
30
30
|
export declare function isReplaying(): boolean;
|
|
31
31
|
/**
|
|
32
32
|
* Get the next cassette entry matching a span name and type.
|
|
33
33
|
* Uses sequential matching with name-based fallback for deterministic replay.
|
|
34
|
+
* Thread-safe: each async context has its own pointer.
|
|
34
35
|
*/
|
|
35
36
|
export declare function consumeCassetteEntry(name: string, spanType: string): CassetteEntry | null;
|
|
36
37
|
/**
|
|
37
38
|
* Handle a replay command from the server.
|
|
39
|
+
* Returns a Promise that resolves when replay completes or rejects on failure.
|
|
40
|
+
* Uses AsyncLocalStorage for per-context cassette isolation.
|
|
38
41
|
*/
|
|
39
|
-
export declare function handleReplay(command: ReplayCommand): boolean
|
|
42
|
+
export declare function handleReplay(command: ReplayCommand): Promise<boolean>;
|
|
40
43
|
export declare function parseReplayMessage(msg: {
|
|
41
44
|
type: string;
|
|
42
45
|
data?: unknown;
|
package/dist/replay.js
CHANGED
|
@@ -10,50 +10,50 @@
|
|
|
10
10
|
* This enables one-click reproduction of any production trace locally.
|
|
11
11
|
*/
|
|
12
12
|
import { getResumable } from "./resume.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
let cassettePointer = 0;
|
|
13
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
14
|
+
const cassetteStorage = new AsyncLocalStorage();
|
|
16
15
|
/**
|
|
17
|
-
* Check if a replay is currently active.
|
|
16
|
+
* Check if a replay is currently active in this async context.
|
|
18
17
|
*/
|
|
19
18
|
export function isReplaying() {
|
|
20
|
-
return
|
|
19
|
+
return cassetteStorage.getStore() !== undefined;
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
22
|
* Get the next cassette entry matching a span name and type.
|
|
24
23
|
* Uses sequential matching with name-based fallback for deterministic replay.
|
|
24
|
+
* Thread-safe: each async context has its own pointer.
|
|
25
25
|
*/
|
|
26
26
|
export function consumeCassetteEntry(name, spanType) {
|
|
27
|
-
|
|
27
|
+
const ctx = cassetteStorage.getStore();
|
|
28
|
+
if (!ctx)
|
|
28
29
|
return null;
|
|
29
30
|
// Primary: sequential pointer (deterministic order)
|
|
30
|
-
if (
|
|
31
|
-
const entry =
|
|
31
|
+
if (ctx.pointer < ctx.cassette.length) {
|
|
32
|
+
const entry = ctx.cassette[ctx.pointer];
|
|
32
33
|
if (entry.name === name && entry.span_type === spanType) {
|
|
33
|
-
|
|
34
|
+
ctx.pointer++;
|
|
34
35
|
return entry;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
// Fallback: search by name + type from current pointer forward
|
|
38
|
-
for (let i =
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
return
|
|
39
|
+
for (let i = ctx.pointer; i < ctx.cassette.length; i++) {
|
|
40
|
+
if (ctx.cassette[i].name === name && ctx.cassette[i].span_type === spanType) {
|
|
41
|
+
ctx.pointer = i + 1;
|
|
42
|
+
return ctx.cassette[i];
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
48
|
* Handle a replay command from the server.
|
|
49
|
+
* Returns a Promise that resolves when replay completes or rejects on failure.
|
|
50
|
+
* Uses AsyncLocalStorage for per-context cassette isolation.
|
|
48
51
|
*/
|
|
49
|
-
export function handleReplay(command) {
|
|
52
|
+
export async function handleReplay(command) {
|
|
50
53
|
const fn = getResumable(command.traceName);
|
|
51
54
|
if (!fn)
|
|
52
55
|
return false;
|
|
53
|
-
|
|
54
|
-
activeCassette = command.cassette;
|
|
55
|
-
cassettePointer = 0;
|
|
56
|
-
(async () => {
|
|
56
|
+
return cassetteStorage.run({ cassette: command.cassette, pointer: 0 }, async () => {
|
|
57
57
|
try {
|
|
58
58
|
const { TraceRecorder } = await import("./recorder.js");
|
|
59
59
|
const { TraceStatus } = await import("./trace.js");
|
|
@@ -72,17 +72,13 @@ export function handleReplay(command) {
|
|
|
72
72
|
: Array.isArray(command.input) ? command.input : [command.input];
|
|
73
73
|
const result = await Promise.resolve(fn(...args));
|
|
74
74
|
recorder.end(result, TraceStatus.COMPLETED);
|
|
75
|
+
return true;
|
|
75
76
|
}
|
|
76
77
|
catch (err) {
|
|
77
78
|
console.error("[retrace] Deterministic replay failed:", err);
|
|
79
|
+
return false;
|
|
78
80
|
}
|
|
79
|
-
|
|
80
|
-
// Clean up cassette state
|
|
81
|
-
activeCassette = null;
|
|
82
|
-
cassettePointer = 0;
|
|
83
|
-
}
|
|
84
|
-
})();
|
|
85
|
-
return true;
|
|
81
|
+
});
|
|
86
82
|
}
|
|
87
83
|
export function parseReplayMessage(msg) {
|
|
88
84
|
if (msg.type !== "replay" || !msg.data)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C Trace Context (traceparent) propagation.
|
|
3
|
+
* Format: 00-{trace_id_32hex}-{parent_id_16hex}-{flags_2hex}
|
|
4
|
+
*
|
|
5
|
+
* This enables distributed tracing across service boundaries.
|
|
6
|
+
* When a traced function makes HTTP calls, the traceparent header
|
|
7
|
+
* is injected so downstream services can correlate their spans.
|
|
8
|
+
*/
|
|
9
|
+
/** Set the active trace context for outgoing requests. */
|
|
10
|
+
export declare function setTraceContext(traceId: string, spanId: string): void;
|
|
11
|
+
/** Clear the active trace context. */
|
|
12
|
+
export declare function clearTraceContext(): void;
|
|
13
|
+
/** Get the current traceparent header value, or null if no active trace. */
|
|
14
|
+
export declare function getTraceparent(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Inject traceparent into a headers object (for fetch/axios/http calls).
|
|
17
|
+
* Returns the headers with traceparent added if a trace is active.
|
|
18
|
+
*/
|
|
19
|
+
export declare function injectTraceparent(headers?: Record<string, string>): Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Parse an incoming traceparent header.
|
|
22
|
+
* Returns { traceId, parentId, sampled } or null if invalid.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseTraceparent(header: string): {
|
|
25
|
+
traceId: string;
|
|
26
|
+
parentId: string;
|
|
27
|
+
sampled: boolean;
|
|
28
|
+
} | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C Trace Context (traceparent) propagation.
|
|
3
|
+
* Format: 00-{trace_id_32hex}-{parent_id_16hex}-{flags_2hex}
|
|
4
|
+
*
|
|
5
|
+
* This enables distributed tracing across service boundaries.
|
|
6
|
+
* When a traced function makes HTTP calls, the traceparent header
|
|
7
|
+
* is injected so downstream services can correlate their spans.
|
|
8
|
+
*/
|
|
9
|
+
let _currentTraceId = null;
|
|
10
|
+
let _currentSpanId = null;
|
|
11
|
+
/** Set the active trace context for outgoing requests. */
|
|
12
|
+
export function setTraceContext(traceId, spanId) {
|
|
13
|
+
// Convert UUID format to 32-hex (remove dashes)
|
|
14
|
+
_currentTraceId = traceId.replace(/-/g, "");
|
|
15
|
+
// Take first 16 chars of span ID as parent span
|
|
16
|
+
_currentSpanId = spanId.replace(/-/g, "").slice(0, 16);
|
|
17
|
+
}
|
|
18
|
+
/** Clear the active trace context. */
|
|
19
|
+
export function clearTraceContext() {
|
|
20
|
+
_currentTraceId = null;
|
|
21
|
+
_currentSpanId = null;
|
|
22
|
+
}
|
|
23
|
+
/** Get the current traceparent header value, or null if no active trace. */
|
|
24
|
+
export function getTraceparent() {
|
|
25
|
+
if (!_currentTraceId || !_currentSpanId)
|
|
26
|
+
return null;
|
|
27
|
+
// version-trace_id-parent_id-flags (01 = sampled)
|
|
28
|
+
return `00-${_currentTraceId}-${_currentSpanId}-01`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Inject traceparent into a headers object (for fetch/axios/http calls).
|
|
32
|
+
* Returns the headers with traceparent added if a trace is active.
|
|
33
|
+
*/
|
|
34
|
+
export function injectTraceparent(headers = {}) {
|
|
35
|
+
const tp = getTraceparent();
|
|
36
|
+
if (tp) {
|
|
37
|
+
headers["traceparent"] = tp;
|
|
38
|
+
}
|
|
39
|
+
return headers;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse an incoming traceparent header.
|
|
43
|
+
* Returns { traceId, parentId, sampled } or null if invalid.
|
|
44
|
+
*/
|
|
45
|
+
export function parseTraceparent(header) {
|
|
46
|
+
const parts = header.split("-");
|
|
47
|
+
if (parts.length !== 4 || parts[0] !== "00")
|
|
48
|
+
return null;
|
|
49
|
+
if (parts[1].length !== 32 || parts[2].length !== 16)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
traceId: parts[1],
|
|
53
|
+
parentId: parts[2],
|
|
54
|
+
sampled: parts[3] === "01",
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
export declare function genId(): string;
|
|
2
2
|
export declare function nowIso(): string;
|
|
3
3
|
export declare function utcNow(): Date;
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic sampling decision using FNV-1a hash.
|
|
6
|
+
* When a seed is provided, the same (seed + key) always produces the same decision.
|
|
7
|
+
* Without a seed, falls back to Math.random() for backward compatibility.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldSample(rate: number, seed?: string, key?: string): boolean;
|
|
4
10
|
export declare function truncateJson(obj: unknown, maxBytes?: number): unknown;
|
|
11
|
+
/** Configure per-span-type truncation limits. */
|
|
12
|
+
export declare function setTruncationLimits(limits: Record<string, number>): void;
|
|
13
|
+
/** Get the truncation limit for a given span type. */
|
|
14
|
+
export declare function getTruncationLimit(spanType: string): number;
|
|
15
|
+
/** Truncate payload based on span type. */
|
|
16
|
+
export declare function truncateForSpanType(obj: unknown, spanType: string): unknown;
|
package/dist/utils.js
CHANGED
|
@@ -8,6 +8,27 @@ export function nowIso() {
|
|
|
8
8
|
export function utcNow() {
|
|
9
9
|
return new Date();
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Deterministic sampling decision using FNV-1a hash.
|
|
13
|
+
* When a seed is provided, the same (seed + key) always produces the same decision.
|
|
14
|
+
* Without a seed, falls back to Math.random() for backward compatibility.
|
|
15
|
+
*/
|
|
16
|
+
export function shouldSample(rate, seed, key) {
|
|
17
|
+
if (rate >= 1)
|
|
18
|
+
return true;
|
|
19
|
+
if (rate <= 0)
|
|
20
|
+
return false;
|
|
21
|
+
if (!seed)
|
|
22
|
+
return Math.random() < rate;
|
|
23
|
+
// FNV-1a hash of seed+key → deterministic float in [0,1)
|
|
24
|
+
const input = `${seed}:${key || Date.now()}`;
|
|
25
|
+
let hash = 2166136261;
|
|
26
|
+
for (let i = 0; i < input.length; i++) {
|
|
27
|
+
hash ^= input.charCodeAt(i);
|
|
28
|
+
hash = Math.imul(hash, 16777619);
|
|
29
|
+
}
|
|
30
|
+
return ((hash >>> 0) / 4294967296) < rate;
|
|
31
|
+
}
|
|
11
32
|
export function truncateJson(obj, maxBytes = 10240) {
|
|
12
33
|
try {
|
|
13
34
|
const s = JSON.stringify(obj);
|
|
@@ -19,3 +40,25 @@ export function truncateJson(obj, maxBytes = 10240) {
|
|
|
19
40
|
return String(obj).slice(0, maxBytes);
|
|
20
41
|
}
|
|
21
42
|
}
|
|
43
|
+
/** Default per-span-type truncation limits (bytes). */
|
|
44
|
+
const DEFAULT_TRUNCATION_LIMITS = {
|
|
45
|
+
llm_call: 51200, // 50KB — LLM prompts can be large
|
|
46
|
+
tool_call: 10240, // 10KB
|
|
47
|
+
tool_result: 10240, // 10KB
|
|
48
|
+
reasoning: 20480, // 20KB
|
|
49
|
+
action: 5120, // 5KB
|
|
50
|
+
error: 5120, // 5KB
|
|
51
|
+
};
|
|
52
|
+
let customTruncationLimits = {};
|
|
53
|
+
/** Configure per-span-type truncation limits. */
|
|
54
|
+
export function setTruncationLimits(limits) {
|
|
55
|
+
customTruncationLimits = limits;
|
|
56
|
+
}
|
|
57
|
+
/** Get the truncation limit for a given span type. */
|
|
58
|
+
export function getTruncationLimit(spanType) {
|
|
59
|
+
return customTruncationLimits[spanType] || DEFAULT_TRUNCATION_LIMITS[spanType] || 10240;
|
|
60
|
+
}
|
|
61
|
+
/** Truncate payload based on span type. */
|
|
62
|
+
export function truncateForSpanType(obj, spanType) {
|
|
63
|
+
return truncateJson(obj, getTruncationLimit(spanType));
|
|
64
|
+
}
|