@scope-analytics/node 0.1.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/README.md +129 -0
- package/dist/index.cjs +921 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +211 -0
- package/dist/index.d.ts +211 -0
- package/dist/index.js +893 -0
- package/dist/index.js.map +1 -0
- package/dist/register.cjs +895 -0
- package/dist/register.cjs.map +1 -0
- package/dist/register.d.cts +2 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +892 -0
- package/dist/register.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
3
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
4
|
+
import { OpenAIInstrumentation } from "@arizeai/openinference-instrumentation-openai";
|
|
5
|
+
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
|
|
6
|
+
import { OpenInferenceBatchSpanProcessor } from "@arizeai/openinference-vercel";
|
|
7
|
+
|
|
8
|
+
// src/google-genai-instrumentation.ts
|
|
9
|
+
import {
|
|
10
|
+
context,
|
|
11
|
+
diag,
|
|
12
|
+
SpanKind,
|
|
13
|
+
SpanStatusCode,
|
|
14
|
+
trace
|
|
15
|
+
} from "@opentelemetry/api";
|
|
16
|
+
import { isTracingSuppressed } from "@opentelemetry/core";
|
|
17
|
+
import {
|
|
18
|
+
InstrumentationBase,
|
|
19
|
+
InstrumentationNodeModuleDefinition,
|
|
20
|
+
isWrapped
|
|
21
|
+
} from "@opentelemetry/instrumentation";
|
|
22
|
+
import {
|
|
23
|
+
LLMProvider,
|
|
24
|
+
MimeType,
|
|
25
|
+
OpenInferenceSpanKind,
|
|
26
|
+
SemanticConventions as SC
|
|
27
|
+
} from "@arizeai/openinference-semantic-conventions";
|
|
28
|
+
|
|
29
|
+
// src/version.ts
|
|
30
|
+
var SDK_VERSION = "0.1.0";
|
|
31
|
+
var SDK_SOURCE = "backend_sdk";
|
|
32
|
+
var SDK_USER_AGENT = `scope-tracer-node/${SDK_VERSION}`;
|
|
33
|
+
|
|
34
|
+
// src/google-genai-instrumentation.ts
|
|
35
|
+
var MODULE_NAME = "@google/genai";
|
|
36
|
+
var INSTRUMENTATION_NAME = "@scope-analytics/node/instrumentation-google-genai";
|
|
37
|
+
var _isPatched = false;
|
|
38
|
+
function isGoogleGenAIPatched() {
|
|
39
|
+
return _isPatched;
|
|
40
|
+
}
|
|
41
|
+
function asFiniteNumber(v) {
|
|
42
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
43
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
function safeJsonStringify(v) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(v) ?? String(v);
|
|
49
|
+
} catch {
|
|
50
|
+
return "[unserializable]";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function errMessage(err) {
|
|
54
|
+
const m = err?.message;
|
|
55
|
+
return m != null ? String(m) : "error";
|
|
56
|
+
}
|
|
57
|
+
function isPromiseLike(v) {
|
|
58
|
+
return v != null && typeof v.then === "function";
|
|
59
|
+
}
|
|
60
|
+
function modelName(params) {
|
|
61
|
+
const m = typeof params.model === "string" ? params.model : "gemini";
|
|
62
|
+
return m.startsWith("models/") ? m.slice("models/".length) : m;
|
|
63
|
+
}
|
|
64
|
+
function normalizeContents(contents) {
|
|
65
|
+
if (typeof contents === "string") return [{ role: "user", content: contents }];
|
|
66
|
+
if (Array.isArray(contents)) {
|
|
67
|
+
const msgs = [];
|
|
68
|
+
for (const item of contents) {
|
|
69
|
+
if (typeof item === "string") {
|
|
70
|
+
msgs.push({ role: "user", content: item });
|
|
71
|
+
} else if (item && typeof item === "object") {
|
|
72
|
+
const obj = item;
|
|
73
|
+
if ("content" in obj) {
|
|
74
|
+
msgs.push({
|
|
75
|
+
role: typeof obj.role === "string" ? obj.role : "user",
|
|
76
|
+
content: typeof obj.content === "string" ? obj.content : safeJsonStringify(obj.content)
|
|
77
|
+
});
|
|
78
|
+
} else if ("parts" in obj) {
|
|
79
|
+
const parts = Array.isArray(obj.parts) ? obj.parts : [];
|
|
80
|
+
const content = parts.filter((p) => p && typeof p.text === "string").map((p) => p.text).join("");
|
|
81
|
+
msgs.push({ role: typeof obj.role === "string" ? obj.role : "user", content });
|
|
82
|
+
} else if (typeof obj.text === "string") {
|
|
83
|
+
msgs.push({ role: "user", content: obj.text });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return msgs.length ? msgs : [{ role: "user", content: safeJsonStringify(contents) }];
|
|
88
|
+
}
|
|
89
|
+
if (contents == null) return [];
|
|
90
|
+
return [{ role: "user", content: safeJsonStringify(contents) }];
|
|
91
|
+
}
|
|
92
|
+
function responseText(resp) {
|
|
93
|
+
let text;
|
|
94
|
+
try {
|
|
95
|
+
text = resp?.text;
|
|
96
|
+
} catch {
|
|
97
|
+
text = void 0;
|
|
98
|
+
}
|
|
99
|
+
if (typeof text === "string" && text) return text;
|
|
100
|
+
let out = "";
|
|
101
|
+
const candidates = Array.isArray(resp?.candidates) ? resp?.candidates : [];
|
|
102
|
+
for (const cand of candidates) {
|
|
103
|
+
const parts = Array.isArray(cand?.content?.parts) ? cand?.content?.parts : [];
|
|
104
|
+
for (const part of parts) {
|
|
105
|
+
if (typeof part?.text === "string") out += part.text;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function inputAttributes(params) {
|
|
111
|
+
const attrs = {
|
|
112
|
+
[SC.OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.LLM,
|
|
113
|
+
[SC.LLM_PROVIDER]: LLMProvider.GOOGLE,
|
|
114
|
+
[SC.LLM_MODEL_NAME]: modelName(params),
|
|
115
|
+
[SC.INPUT_VALUE]: safeJsonStringify(params),
|
|
116
|
+
[SC.INPUT_MIME_TYPE]: MimeType.JSON
|
|
117
|
+
};
|
|
118
|
+
normalizeContents(params.contents).forEach((msg, i) => {
|
|
119
|
+
const prefix = `${SC.LLM_INPUT_MESSAGES}.${i}.`;
|
|
120
|
+
if (msg.role !== void 0) attrs[`${prefix}${SC.MESSAGE_ROLE}`] = msg.role;
|
|
121
|
+
if (msg.content !== void 0) attrs[`${prefix}${SC.MESSAGE_CONTENT}`] = msg.content;
|
|
122
|
+
});
|
|
123
|
+
return attrs;
|
|
124
|
+
}
|
|
125
|
+
function setOutputAttributes(span, text, usage) {
|
|
126
|
+
const attrs = {
|
|
127
|
+
[SC.OUTPUT_VALUE]: text,
|
|
128
|
+
[SC.OUTPUT_MIME_TYPE]: MimeType.TEXT,
|
|
129
|
+
[`${SC.LLM_OUTPUT_MESSAGES}.0.${SC.MESSAGE_ROLE}`]: "model",
|
|
130
|
+
[`${SC.LLM_OUTPUT_MESSAGES}.0.${SC.MESSAGE_CONTENT}`]: text
|
|
131
|
+
};
|
|
132
|
+
if (usage) {
|
|
133
|
+
attrs[SC.LLM_TOKEN_COUNT_PROMPT] = asFiniteNumber(usage.promptTokenCount) ?? 0;
|
|
134
|
+
attrs[SC.LLM_TOKEN_COUNT_COMPLETION] = asFiniteNumber(usage.candidatesTokenCount) ?? 0;
|
|
135
|
+
attrs[SC.LLM_TOKEN_COUNT_TOTAL] = asFiniteNumber(usage.totalTokenCount) ?? 0;
|
|
136
|
+
}
|
|
137
|
+
span.setAttributes(attrs);
|
|
138
|
+
}
|
|
139
|
+
function endOk(span, resp) {
|
|
140
|
+
try {
|
|
141
|
+
setOutputAttributes(span, responseText(resp), resp?.usageMetadata);
|
|
142
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
span.end();
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function endWithError(span, err) {
|
|
151
|
+
try {
|
|
152
|
+
span.recordException(err);
|
|
153
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errMessage(err) });
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
span.end();
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
var GoogleGenAIInstrumentation = class extends InstrumentationBase {
|
|
162
|
+
constructor(config = {}) {
|
|
163
|
+
super(INSTRUMENTATION_NAME, SDK_VERSION, config);
|
|
164
|
+
}
|
|
165
|
+
init() {
|
|
166
|
+
return new InstrumentationNodeModuleDefinition(
|
|
167
|
+
MODULE_NAME,
|
|
168
|
+
["*"],
|
|
169
|
+
(moduleExports) => this.patch(moduleExports),
|
|
170
|
+
(moduleExports) => this.unpatch(moduleExports)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
/** ESM escape hatch — the require-hook doesn't fire for `import` (mirrors the Arize instrumentations). */
|
|
174
|
+
manuallyInstrument(moduleExports) {
|
|
175
|
+
diag.debug(`Manually instrumenting ${MODULE_NAME}`);
|
|
176
|
+
this.patch(moduleExports);
|
|
177
|
+
}
|
|
178
|
+
patch(moduleExports) {
|
|
179
|
+
if (!moduleExports || moduleExports.scopePatched || _isPatched) return moduleExports;
|
|
180
|
+
const m = moduleExports.default || moduleExports;
|
|
181
|
+
const proto = m?.Models?.prototype;
|
|
182
|
+
if (!proto || typeof proto.generateContentInternal !== "function") {
|
|
183
|
+
diag.warn(`[scope] cannot find Models.prototype.generateContentInternal in ${MODULE_NAME}`);
|
|
184
|
+
return moduleExports;
|
|
185
|
+
}
|
|
186
|
+
const wrappable = proto;
|
|
187
|
+
this._wrap(wrappable, "generateContentInternal", this._wrapGenerate(false));
|
|
188
|
+
if (typeof proto.generateContentStreamInternal === "function") {
|
|
189
|
+
this._wrap(wrappable, "generateContentStreamInternal", this._wrapGenerate(true));
|
|
190
|
+
}
|
|
191
|
+
_isPatched = true;
|
|
192
|
+
try {
|
|
193
|
+
moduleExports.scopePatched = true;
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
return moduleExports;
|
|
197
|
+
}
|
|
198
|
+
unpatch(moduleExports) {
|
|
199
|
+
const m = moduleExports?.default || moduleExports;
|
|
200
|
+
const proto = m?.Models?.prototype;
|
|
201
|
+
if (proto) {
|
|
202
|
+
if (isWrapped(proto.generateContentInternal)) {
|
|
203
|
+
this._unwrap(proto, "generateContentInternal");
|
|
204
|
+
}
|
|
205
|
+
if (isWrapped(proto.generateContentStreamInternal)) {
|
|
206
|
+
this._unwrap(proto, "generateContentStreamInternal");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
_isPatched = false;
|
|
210
|
+
try {
|
|
211
|
+
if (moduleExports) moduleExports.scopePatched = false;
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
_wrapGenerate(streaming) {
|
|
216
|
+
const instrumentation = this;
|
|
217
|
+
return (original) => function patched(...args) {
|
|
218
|
+
if (isTracingSuppressed(context.active())) {
|
|
219
|
+
return original.apply(this, args);
|
|
220
|
+
}
|
|
221
|
+
let span;
|
|
222
|
+
try {
|
|
223
|
+
const params = args[0] ?? {};
|
|
224
|
+
span = instrumentation.tracer.startSpan("GoogleGenAI.generateContent", {
|
|
225
|
+
kind: SpanKind.INTERNAL,
|
|
226
|
+
attributes: inputAttributes(params)
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
span = void 0;
|
|
230
|
+
}
|
|
231
|
+
if (!span) return original.apply(this, args);
|
|
232
|
+
const activeSpan = span;
|
|
233
|
+
const activeContext = trace.setSpan(context.active(), activeSpan);
|
|
234
|
+
let result;
|
|
235
|
+
try {
|
|
236
|
+
result = context.with(activeContext, () => original.apply(this, args));
|
|
237
|
+
} catch (err) {
|
|
238
|
+
endWithError(activeSpan, err);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
if (!isPromiseLike(result)) {
|
|
242
|
+
endOk(activeSpan, void 0);
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
if (streaming) {
|
|
246
|
+
return result.then(
|
|
247
|
+
(gen) => instrumentation._wrapStream(gen, activeSpan),
|
|
248
|
+
(err) => {
|
|
249
|
+
endWithError(activeSpan, err);
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return result.then(
|
|
255
|
+
(resp) => {
|
|
256
|
+
endOk(activeSpan, resp);
|
|
257
|
+
return resp;
|
|
258
|
+
},
|
|
259
|
+
(err) => {
|
|
260
|
+
endWithError(activeSpan, err);
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Wrap the streaming async-generator: yield every chunk through to the caller while accumulating the
|
|
268
|
+
* full text + final usage, then end the span. Capture is fully isolated from the caller's stream:
|
|
269
|
+
* - per-chunk extraction is swallowed, so a malformed chunk never aborts the user's iteration;
|
|
270
|
+
* - the catch handles ONLY a real error thrown by the underlying stream (records it, rethrows);
|
|
271
|
+
* - the finally ends the span exactly once, even on early abandonment (the generator's .return()
|
|
272
|
+
* runs `finally`) — capturing whatever text/usage arrived so far.
|
|
273
|
+
* Mirrors the Python _wrap_stream_iter + _capture_streaming_response (chunks carry text; the final
|
|
274
|
+
* chunk carries usage).
|
|
275
|
+
*/
|
|
276
|
+
async *_wrapStream(gen, span) {
|
|
277
|
+
const texts = [];
|
|
278
|
+
let usage;
|
|
279
|
+
let errored = false;
|
|
280
|
+
try {
|
|
281
|
+
for await (const chunk of gen) {
|
|
282
|
+
try {
|
|
283
|
+
texts.push(responseText(chunk));
|
|
284
|
+
if (chunk?.usageMetadata) usage = chunk.usageMetadata;
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
yield chunk;
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
errored = true;
|
|
291
|
+
try {
|
|
292
|
+
span.recordException(err);
|
|
293
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errMessage(err) });
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
throw err;
|
|
297
|
+
} finally {
|
|
298
|
+
if (!errored) {
|
|
299
|
+
try {
|
|
300
|
+
setOutputAttributes(span, texts.join(""), usage);
|
|
301
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
span.end();
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/vercel-ai-instrumentation.ts
|
|
314
|
+
import { diag as diag2 } from "@opentelemetry/api";
|
|
315
|
+
import {
|
|
316
|
+
InstrumentationBase as InstrumentationBase2,
|
|
317
|
+
InstrumentationNodeModuleDefinition as InstrumentationNodeModuleDefinition2,
|
|
318
|
+
isWrapped as isWrapped2
|
|
319
|
+
} from "@opentelemetry/instrumentation";
|
|
320
|
+
var MODULE_NAME2 = "ai";
|
|
321
|
+
var INSTRUMENTATION_NAME2 = "@scope-analytics/node/instrumentation-vercel-ai";
|
|
322
|
+
var TARGET_FUNCTIONS = ["generateText", "streamText", "generateObject", "streamObject"];
|
|
323
|
+
var _isPatched2 = false;
|
|
324
|
+
function isVercelAIPatched() {
|
|
325
|
+
return _isPatched2;
|
|
326
|
+
}
|
|
327
|
+
function injectTelemetryArgs(args) {
|
|
328
|
+
try {
|
|
329
|
+
const opts = args[0];
|
|
330
|
+
if (!opts || typeof opts !== "object" || Array.isArray(opts)) return args;
|
|
331
|
+
const o = opts;
|
|
332
|
+
const existing = o.experimental_telemetry;
|
|
333
|
+
const hasExisting = !!existing && typeof existing === "object" && !Array.isArray(existing);
|
|
334
|
+
if (hasExisting && existing.isEnabled === false) return args;
|
|
335
|
+
if (hasExisting && existing.isEnabled === true) return args;
|
|
336
|
+
const mergedTelemetry = {
|
|
337
|
+
...hasExisting ? existing : {},
|
|
338
|
+
isEnabled: true
|
|
339
|
+
};
|
|
340
|
+
return [{ ...o, experimental_telemetry: mergedTelemetry }, ...args.slice(1)];
|
|
341
|
+
} catch {
|
|
342
|
+
return args;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function makeTelemetryWrapper(original) {
|
|
346
|
+
return function scopeVercelTelemetryWrapper(...args) {
|
|
347
|
+
return original.apply(this, injectTelemetryArgs(args));
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function settableInPlace(obj, name) {
|
|
351
|
+
const d = Object.getOwnPropertyDescriptor(obj, name);
|
|
352
|
+
if (!d) return false;
|
|
353
|
+
if (typeof d.set === "function") return true;
|
|
354
|
+
if (d.writable === true) return true;
|
|
355
|
+
if (d.configurable === true) return true;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
var VercelAIInstrumentation = class extends InstrumentationBase2 {
|
|
359
|
+
// Same original-exports → Proxy, so repeated patch() calls (re-entrancy) return one stable Proxy.
|
|
360
|
+
_proxyCache = /* @__PURE__ */ new WeakMap();
|
|
361
|
+
constructor(config = {}) {
|
|
362
|
+
super(INSTRUMENTATION_NAME2, SDK_VERSION, config);
|
|
363
|
+
}
|
|
364
|
+
init() {
|
|
365
|
+
return new InstrumentationNodeModuleDefinition2(
|
|
366
|
+
MODULE_NAME2,
|
|
367
|
+
["*"],
|
|
368
|
+
(moduleExports) => this.patch(moduleExports),
|
|
369
|
+
(moduleExports) => this.unpatch(moduleExports)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
/** ESM escape hatch — the require-hook doesn't fire for `import` (mirrors the Arize instrumentations). */
|
|
373
|
+
manuallyInstrument(moduleExports) {
|
|
374
|
+
diag2.debug(`Manually instrumenting ${MODULE_NAME2}`);
|
|
375
|
+
return this.patch(moduleExports);
|
|
376
|
+
}
|
|
377
|
+
patch(moduleExports) {
|
|
378
|
+
if (!moduleExports) return moduleExports;
|
|
379
|
+
const present = TARGET_FUNCTIONS.filter((n) => typeof moduleExports[n] === "function");
|
|
380
|
+
if (present.length === 0) {
|
|
381
|
+
diag2.warn(`[scope] no target functions found in '${MODULE_NAME2}'; AI SDK telemetry not auto-enabled`);
|
|
382
|
+
return moduleExports;
|
|
383
|
+
}
|
|
384
|
+
if (present.every((n) => settableInPlace(moduleExports, n))) {
|
|
385
|
+
for (const n of present) {
|
|
386
|
+
if (!isWrapped2(moduleExports[n])) {
|
|
387
|
+
this._wrap(moduleExports, n, (original) => makeTelemetryWrapper(original));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
_isPatched2 = true;
|
|
391
|
+
return moduleExports;
|
|
392
|
+
}
|
|
393
|
+
const cached = this._proxyCache.get(moduleExports);
|
|
394
|
+
if (cached) {
|
|
395
|
+
_isPatched2 = true;
|
|
396
|
+
return cached;
|
|
397
|
+
}
|
|
398
|
+
const targets = new Set(present);
|
|
399
|
+
const fnCache = /* @__PURE__ */ new Map();
|
|
400
|
+
const proxy = new Proxy(moduleExports, {
|
|
401
|
+
get(target, prop, receiver) {
|
|
402
|
+
const value = Reflect.get(target, prop, receiver);
|
|
403
|
+
if (typeof prop === "string" && targets.has(prop) && typeof value === "function") {
|
|
404
|
+
let wrapped = fnCache.get(prop);
|
|
405
|
+
if (!wrapped) {
|
|
406
|
+
wrapped = makeTelemetryWrapper(value);
|
|
407
|
+
fnCache.set(prop, wrapped);
|
|
408
|
+
}
|
|
409
|
+
return wrapped;
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
this._proxyCache.set(moduleExports, proxy);
|
|
415
|
+
_isPatched2 = true;
|
|
416
|
+
return proxy;
|
|
417
|
+
}
|
|
418
|
+
unpatch(moduleExports) {
|
|
419
|
+
if (moduleExports) {
|
|
420
|
+
for (const n of TARGET_FUNCTIONS) {
|
|
421
|
+
if (typeof moduleExports[n] === "function" && isWrapped2(moduleExports[n])) {
|
|
422
|
+
this._unwrap(moduleExports, n);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
this._proxyCache.delete(moduleExports);
|
|
426
|
+
}
|
|
427
|
+
_isPatched2 = false;
|
|
428
|
+
return moduleExports;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/esm-hook.ts
|
|
433
|
+
import * as nodeModule from "module";
|
|
434
|
+
import { diag as diag3 } from "@opentelemetry/api";
|
|
435
|
+
import { Hook } from "import-in-the-middle";
|
|
436
|
+
var _loaderRegistered = false;
|
|
437
|
+
function registerEsmManualInstrument(entries) {
|
|
438
|
+
if (!_loaderRegistered) return;
|
|
439
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
440
|
+
for (const { modules, instrumentation } of entries) {
|
|
441
|
+
for (const m of modules) byModule.set(m, instrumentation);
|
|
442
|
+
}
|
|
443
|
+
if (byModule.size === 0) return;
|
|
444
|
+
try {
|
|
445
|
+
new Hook([...byModule.keys()], (exported, name) => {
|
|
446
|
+
const instrumentation = byModule.get(name);
|
|
447
|
+
if (!instrumentation) return exported;
|
|
448
|
+
try {
|
|
449
|
+
instrumentation.manuallyInstrument(exported);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
diag3.debug(
|
|
452
|
+
`[scope] ESM manual instrument of '${name}' failed (${err instanceof Error ? err.message : String(err)}).`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return exported;
|
|
456
|
+
});
|
|
457
|
+
diag3.debug(`[scope] ESM manual instrumentation registered for: ${[...byModule.keys()].join(", ")}.`);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
diag3.debug(
|
|
460
|
+
`[scope] ESM manual instrumentation not registered (${err instanceof Error ? err.message : String(err)}).`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/config.ts
|
|
466
|
+
var ScopeConfigError = class extends Error {
|
|
467
|
+
constructor(message) {
|
|
468
|
+
super(message);
|
|
469
|
+
this.name = "ScopeConfigError";
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
var DEFAULT_ENDPOINT = "https://api.scopeai.dev";
|
|
473
|
+
function resolveConfig(options = {}, env = process.env) {
|
|
474
|
+
const apiKey = (options.apiKey ?? env.SCOPE_API_KEY ?? "").trim();
|
|
475
|
+
if (!apiKey) {
|
|
476
|
+
throw new ScopeConfigError(
|
|
477
|
+
"Scope: missing API key. Set SCOPE_API_KEY (your project secret key, sk_live_\u2026 or sk_test_\u2026) or pass { apiKey } to init()."
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (!(apiKey.startsWith("sk_live_") || apiKey.startsWith("sk_test_"))) {
|
|
481
|
+
throw new ScopeConfigError(
|
|
482
|
+
'Scope: invalid API key. The backend SDK needs a project SECRET key starting with "sk_live_" or "sk_test_" (a public "pk_\u2026" key will not work for server-side ingest).'
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const rawEndpoint = (options.endpoint ?? env.SCOPE_ENDPOINT ?? DEFAULT_ENDPOINT).trim();
|
|
486
|
+
const endpoint = (rawEndpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
487
|
+
const environment = (options.environment ?? env.SCOPE_ENVIRONMENT ?? "production").trim() || "production";
|
|
488
|
+
const debug = options.debug ?? (env.SCOPE_DEBUG ?? "").toLowerCase() === "true";
|
|
489
|
+
const serviceName = (options.serviceName ?? env.SCOPE_SERVICE_NAME ?? env.OTEL_SERVICE_NAME ?? "scope-app").trim() || "scope-app";
|
|
490
|
+
return {
|
|
491
|
+
apiKey,
|
|
492
|
+
endpoint,
|
|
493
|
+
eventsUrl: `${endpoint}/api/events`,
|
|
494
|
+
environment,
|
|
495
|
+
debug,
|
|
496
|
+
serviceName,
|
|
497
|
+
sdkVersion: SDK_VERSION,
|
|
498
|
+
redactPatterns: options.redactPatterns ?? []
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/deployment.ts
|
|
503
|
+
var GIT_SHA_ENV_VARS = [
|
|
504
|
+
"RAILWAY_GIT_COMMIT_SHA",
|
|
505
|
+
// Railway
|
|
506
|
+
"VERCEL_GIT_COMMIT_SHA",
|
|
507
|
+
// Vercel
|
|
508
|
+
"RENDER_GIT_COMMIT",
|
|
509
|
+
// Render
|
|
510
|
+
"HEROKU_RELEASE_VERSION",
|
|
511
|
+
// Heroku
|
|
512
|
+
"K_REVISION",
|
|
513
|
+
// Cloud Run / Cloud Functions / Firebase
|
|
514
|
+
"AWS_LAMBDA_FUNCTION_VERSION",
|
|
515
|
+
// AWS Lambda
|
|
516
|
+
"GIT_COMMIT_SHA"
|
|
517
|
+
// Generic fallback
|
|
518
|
+
];
|
|
519
|
+
function detectGitSha(env = process.env) {
|
|
520
|
+
for (const name of GIT_SHA_ENV_VARS) {
|
|
521
|
+
const value = env[name];
|
|
522
|
+
if (value && value.trim()) return value.trim();
|
|
523
|
+
}
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
function resolveDeployment(startedAt, env = process.env) {
|
|
527
|
+
const gitSha = detectGitSha(env);
|
|
528
|
+
if (!gitSha) return {};
|
|
529
|
+
const startIso = startedAt.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
530
|
+
return { gitSha, deploymentId: `${startIso}-${gitSha.slice(0, 7)}` };
|
|
531
|
+
}
|
|
532
|
+
function stampDeployment(event, deployment) {
|
|
533
|
+
if (deployment.gitSha && event.git_sha === void 0) {
|
|
534
|
+
event.git_sha = deployment.gitSha;
|
|
535
|
+
if (deployment.deploymentId && event.deployment_id === void 0) {
|
|
536
|
+
event.deployment_id = deployment.deploymentId;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return event;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/exporter.ts
|
|
543
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
544
|
+
|
|
545
|
+
// src/event.ts
|
|
546
|
+
import {
|
|
547
|
+
SemanticConventions as SC2,
|
|
548
|
+
OpenInferenceSpanKind as OpenInferenceSpanKind2
|
|
549
|
+
} from "@arizeai/openinference-semantic-conventions";
|
|
550
|
+
import { SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
|
|
551
|
+
|
|
552
|
+
// src/attributes.ts
|
|
553
|
+
var SCOPE_THREAD_ID_ATTR = "scope.thread_id";
|
|
554
|
+
var SCOPE_URL_ATTR = "scope.url";
|
|
555
|
+
|
|
556
|
+
// src/session.ts
|
|
557
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
558
|
+
import { randomBytes } from "crypto";
|
|
559
|
+
var storage = new AsyncLocalStorage();
|
|
560
|
+
function runWithContext(ctx, fn) {
|
|
561
|
+
return storage.run({ ...ctx }, fn);
|
|
562
|
+
}
|
|
563
|
+
function getContext() {
|
|
564
|
+
return storage.getStore();
|
|
565
|
+
}
|
|
566
|
+
function setUser(userId) {
|
|
567
|
+
const store = storage.getStore();
|
|
568
|
+
if (store) store.userId = userId;
|
|
569
|
+
}
|
|
570
|
+
function generateTempSessionId() {
|
|
571
|
+
return `temp_${randomBytes(8).toString("hex")}`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/event.ts
|
|
575
|
+
function asString(v) {
|
|
576
|
+
if (typeof v === "string") return v;
|
|
577
|
+
if (v === void 0 || v === null) return void 0;
|
|
578
|
+
return String(v);
|
|
579
|
+
}
|
|
580
|
+
function asNumber(v) {
|
|
581
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
582
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
583
|
+
return void 0;
|
|
584
|
+
}
|
|
585
|
+
var GEN_AI_SYSTEM_ATTR = "gen_ai.system";
|
|
586
|
+
var VERCEL_MODEL_PROVIDER_ATTR = "ai.model.provider";
|
|
587
|
+
var CREDENTIAL_REDACT_PATTERNS = [
|
|
588
|
+
/(password\s*=\s*['"]?)[^'"\s]+/gi,
|
|
589
|
+
/(api[_-]?key\s*=\s*['"]?)[^'"\s]+/gi,
|
|
590
|
+
/(token\s*=\s*['"]?)[^'"\s]+/gi,
|
|
591
|
+
/(secret\s*=\s*['"]?)[^'"\s]+/gi
|
|
592
|
+
];
|
|
593
|
+
function redactText(text, patterns) {
|
|
594
|
+
if (text === void 0 || patterns.length === 0) return text;
|
|
595
|
+
let out = text;
|
|
596
|
+
for (const re of patterns) out = out.replace(re, "$1***REDACTED***");
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
function hrTimeToMs(t) {
|
|
600
|
+
return t[0] * 1e3 + t[1] / 1e6;
|
|
601
|
+
}
|
|
602
|
+
function escapeRe(s) {
|
|
603
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
604
|
+
}
|
|
605
|
+
function collectMessages(attrs, prefix) {
|
|
606
|
+
const p = escapeRe(prefix);
|
|
607
|
+
const roleRe = new RegExp(`^${p}\\.(\\d+)\\.${escapeRe(SC2.MESSAGE_ROLE)}$`);
|
|
608
|
+
const contentRe = new RegExp(`^${p}\\.(\\d+)\\.${escapeRe(SC2.MESSAGE_CONTENT)}$`);
|
|
609
|
+
const partRe = new RegExp(
|
|
610
|
+
`^${p}\\.(\\d+)\\.${escapeRe(SC2.MESSAGE_CONTENTS)}\\.(\\d+)\\.${escapeRe(SC2.MESSAGE_CONTENT_TEXT)}$`
|
|
611
|
+
);
|
|
612
|
+
const roles = /* @__PURE__ */ new Map();
|
|
613
|
+
const scalarContent = /* @__PURE__ */ new Map();
|
|
614
|
+
const parts = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
616
|
+
let m;
|
|
617
|
+
if (m = roleRe.exec(key)) {
|
|
618
|
+
roles.set(Number(m[1]), asString(value));
|
|
619
|
+
} else if (m = contentRe.exec(key)) {
|
|
620
|
+
scalarContent.set(Number(m[1]), asString(value));
|
|
621
|
+
} else if (m = partRe.exec(key)) {
|
|
622
|
+
const i = Number(m[1]);
|
|
623
|
+
const text = asString(value);
|
|
624
|
+
if (text !== void 0) {
|
|
625
|
+
const byPart = parts.get(i) ?? /* @__PURE__ */ new Map();
|
|
626
|
+
byPart.set(Number(m[2]), text);
|
|
627
|
+
parts.set(i, byPart);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const indices = [.../* @__PURE__ */ new Set([...roles.keys(), ...scalarContent.keys(), ...parts.keys()])].sort(
|
|
632
|
+
(a, b) => a - b
|
|
633
|
+
);
|
|
634
|
+
return indices.map((i) => {
|
|
635
|
+
const message = {};
|
|
636
|
+
const role = roles.get(i);
|
|
637
|
+
if (role !== void 0) message.role = role;
|
|
638
|
+
let content = scalarContent.get(i);
|
|
639
|
+
if (content === void 0 && parts.has(i)) {
|
|
640
|
+
const byPart = parts.get(i);
|
|
641
|
+
content = [...byPart.keys()].sort((a, b) => a - b).map((j) => byPart.get(j)).join("");
|
|
642
|
+
}
|
|
643
|
+
if (content !== void 0) message.content = content;
|
|
644
|
+
return message;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function isLlmSpan(span) {
|
|
648
|
+
return span.attributes[SC2.OPENINFERENCE_SPAN_KIND] === OpenInferenceSpanKind2.LLM;
|
|
649
|
+
}
|
|
650
|
+
function spanToScopeEvent(span, ctx) {
|
|
651
|
+
if (!isLlmSpan(span)) return null;
|
|
652
|
+
const a = span.attributes;
|
|
653
|
+
const inputMessages = collectMessages(a, SC2.LLM_INPUT_MESSAGES);
|
|
654
|
+
const outputMessages = collectMessages(a, SC2.LLM_OUTPUT_MESSAGES);
|
|
655
|
+
const lastUser = [...inputMessages].reverse().find((m) => (m.role ?? "").toLowerCase() === "user");
|
|
656
|
+
const patterns = ctx.config.redactPatterns ?? [];
|
|
657
|
+
const prompt = redactText(
|
|
658
|
+
lastUser?.content ?? inputMessages.at(-1)?.content ?? asString(a[SC2.INPUT_VALUE]),
|
|
659
|
+
patterns
|
|
660
|
+
);
|
|
661
|
+
const response = redactText(outputMessages[0]?.content ?? asString(a[SC2.OUTPUT_VALUE]), patterns);
|
|
662
|
+
const tokens = {};
|
|
663
|
+
const promptTokens = asNumber(a[SC2.LLM_TOKEN_COUNT_PROMPT]);
|
|
664
|
+
if (promptTokens !== void 0) tokens.prompt_tokens = promptTokens;
|
|
665
|
+
const completionTokens = asNumber(a[SC2.LLM_TOKEN_COUNT_COMPLETION]);
|
|
666
|
+
if (completionTokens !== void 0) tokens.completion_tokens = completionTokens;
|
|
667
|
+
const totalTokens = asNumber(a[SC2.LLM_TOKEN_COUNT_TOTAL]);
|
|
668
|
+
if (totalTokens !== void 0) tokens.total_tokens = totalTokens;
|
|
669
|
+
const sessionId = asString(a[SC2.SESSION_ID]) ?? generateTempSessionId();
|
|
670
|
+
const userId = asString(a[SC2.USER_ID]) ?? null;
|
|
671
|
+
const isError = span.status?.code === SpanStatusCode2.ERROR;
|
|
672
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
673
|
+
const nowIso = now.toISOString();
|
|
674
|
+
const event = {
|
|
675
|
+
event_type: "llm_call",
|
|
676
|
+
source: SDK_SOURCE,
|
|
677
|
+
timestamp: nowIso,
|
|
678
|
+
session_id: sessionId,
|
|
679
|
+
user_id: userId,
|
|
680
|
+
is_user_facing: !sessionId.startsWith("temp_"),
|
|
681
|
+
provider: asString(a[SC2.LLM_PROVIDER]) ?? asString(a[SC2.LLM_SYSTEM]) ?? asString(a[GEN_AI_SYSTEM_ATTR]) ?? asString(a[VERCEL_MODEL_PROVIDER_ATTR]),
|
|
682
|
+
model: asString(a[SC2.LLM_MODEL_NAME]),
|
|
683
|
+
prompt,
|
|
684
|
+
response,
|
|
685
|
+
messages: patterns.length ? inputMessages.map(
|
|
686
|
+
(m) => m.content !== void 0 ? { ...m, content: redactText(m.content, patterns) } : m
|
|
687
|
+
) : inputMessages,
|
|
688
|
+
tokens,
|
|
689
|
+
latency_ms: Math.max(0, hrTimeToMs(span.endTime) - hrTimeToMs(span.startTime)),
|
|
690
|
+
error: isError ? span.status?.message ?? "error" : null,
|
|
691
|
+
success: !isError,
|
|
692
|
+
environment: ctx.config.environment,
|
|
693
|
+
sdk_version: ctx.config.sdkVersion,
|
|
694
|
+
server_timestamp: nowIso
|
|
695
|
+
};
|
|
696
|
+
const threadId = asString(a[SCOPE_THREAD_ID_ATTR]);
|
|
697
|
+
if (threadId !== void 0) event.thread_id = threadId;
|
|
698
|
+
const url = asString(a[SCOPE_URL_ATTR]);
|
|
699
|
+
if (url !== void 0) event.url = url;
|
|
700
|
+
return stampDeployment(event, ctx.deployment);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/transport.ts
|
|
704
|
+
async function shipEvents(events, config) {
|
|
705
|
+
if (events.length === 0) return true;
|
|
706
|
+
try {
|
|
707
|
+
const response = await fetch(config.eventsUrl, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
711
|
+
"Content-Type": "application/json",
|
|
712
|
+
"User-Agent": SDK_USER_AGENT
|
|
713
|
+
},
|
|
714
|
+
body: JSON.stringify({ events, source: SDK_SOURCE })
|
|
715
|
+
});
|
|
716
|
+
if (response.ok) {
|
|
717
|
+
if (config.debug) console.log(`[scope] shipped ${events.length} event(s)`);
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
if (config.debug) console.warn(`[scope] ship failed: HTTP ${response.status}`);
|
|
721
|
+
return false;
|
|
722
|
+
} catch (err) {
|
|
723
|
+
if (config.debug) {
|
|
724
|
+
console.warn(`[scope] ship error: ${err instanceof Error ? err.message : String(err)}`);
|
|
725
|
+
}
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/exporter.ts
|
|
731
|
+
var ScopeSpanExporter = class {
|
|
732
|
+
constructor(config, deployment) {
|
|
733
|
+
this.config = config;
|
|
734
|
+
this.deployment = deployment;
|
|
735
|
+
}
|
|
736
|
+
config;
|
|
737
|
+
deployment;
|
|
738
|
+
export(spans, resultCallback) {
|
|
739
|
+
const events = [];
|
|
740
|
+
for (const span of spans) {
|
|
741
|
+
const event = spanToScopeEvent(span, { config: this.config, deployment: this.deployment });
|
|
742
|
+
if (event) events.push(event);
|
|
743
|
+
}
|
|
744
|
+
if (events.length === 0) {
|
|
745
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
shipEvents(events, this.config).then((ok) => resultCallback({ code: ok ? ExportResultCode.SUCCESS : ExportResultCode.FAILED })).catch(() => resultCallback({ code: ExportResultCode.FAILED }));
|
|
749
|
+
}
|
|
750
|
+
shutdown() {
|
|
751
|
+
return Promise.resolve();
|
|
752
|
+
}
|
|
753
|
+
forceFlush() {
|
|
754
|
+
return Promise.resolve();
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// src/processor.ts
|
|
759
|
+
import { SemanticConventions as SC3 } from "@arizeai/openinference-semantic-conventions";
|
|
760
|
+
var ScopeContextSpanProcessor = class {
|
|
761
|
+
onStart(span, _parentContext) {
|
|
762
|
+
const ctx = getContext();
|
|
763
|
+
if (!ctx) return;
|
|
764
|
+
if (ctx.sessionId && span.attributes[SC3.SESSION_ID] === void 0) {
|
|
765
|
+
span.setAttribute(SC3.SESSION_ID, ctx.sessionId);
|
|
766
|
+
}
|
|
767
|
+
if (ctx.userId && span.attributes[SC3.USER_ID] === void 0) {
|
|
768
|
+
span.setAttribute(SC3.USER_ID, ctx.userId);
|
|
769
|
+
}
|
|
770
|
+
if (ctx.threadId && span.attributes[SCOPE_THREAD_ID_ATTR] === void 0) {
|
|
771
|
+
span.setAttribute(SCOPE_THREAD_ID_ATTR, ctx.threadId);
|
|
772
|
+
}
|
|
773
|
+
if (ctx.url && span.attributes[SCOPE_URL_ATTR] === void 0) {
|
|
774
|
+
span.setAttribute(SCOPE_URL_ATTR, ctx.url);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
onEnd(_span) {
|
|
778
|
+
}
|
|
779
|
+
shutdown() {
|
|
780
|
+
return Promise.resolve();
|
|
781
|
+
}
|
|
782
|
+
forceFlush() {
|
|
783
|
+
return Promise.resolve();
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// src/index.ts
|
|
788
|
+
var STARTED_AT = /* @__PURE__ */ new Date();
|
|
789
|
+
var active = null;
|
|
790
|
+
function init(options = {}) {
|
|
791
|
+
if (active) {
|
|
792
|
+
if (active.config.debug) console.warn("[scope] init() called again; reusing the running tracer.");
|
|
793
|
+
return active;
|
|
794
|
+
}
|
|
795
|
+
const config = resolveConfig(options);
|
|
796
|
+
const deployment = resolveDeployment(STARTED_AT);
|
|
797
|
+
const provider = new NodeTracerProvider({
|
|
798
|
+
spanProcessors: [
|
|
799
|
+
// Stamp session/user from AsyncLocalStorage onto each span before it's read at export.
|
|
800
|
+
new ScopeContextSpanProcessor(),
|
|
801
|
+
// Batch spans, then translate + ship them to Scope. We route through the OpenInference Vercel
|
|
802
|
+
// span processor (rather than a plain BatchSpanProcessor) so the Vercel AI SDK's own
|
|
803
|
+
// `ai.*`/`gen_ai.*` spans are translated to OpenInference `llm.*` attributes before our exporter
|
|
804
|
+
// reads them — the only way to capture the Vercel AI SDK, which emits its own spans instead of
|
|
805
|
+
// calling a provider SDK we instrument. On spans that are ALREADY OpenInference
|
|
806
|
+
// (OpenAI/Anthropic/Gemini) the translation is additive-only (it just re-affirms their span
|
|
807
|
+
// kind), so those keep flowing through unchanged. NB: the adapter tags EVERY span the process
|
|
808
|
+
// ends (a non-LLM span just gets an OpenInference span-kind attribute), but ScopeSpanExporter
|
|
809
|
+
// stays the single gate for which spans become events (LLM only) — so no spanFilter is needed,
|
|
810
|
+
// and any non-LLM span (Vercel's AGENT/TOOL spans, or a host's own HTTP/DB spans) is dropped
|
|
811
|
+
// there. Vercel emits a parent (AGENT) + child (LLM) span per call; the AGENT span is dropped,
|
|
812
|
+
// leaving exactly one llm_call per call.
|
|
813
|
+
new OpenInferenceBatchSpanProcessor({ exporter: new ScopeSpanExporter(config, deployment) })
|
|
814
|
+
]
|
|
815
|
+
});
|
|
816
|
+
provider.register();
|
|
817
|
+
const openaiInstrumentation = new OpenAIInstrumentation();
|
|
818
|
+
const anthropicInstrumentation = new AnthropicInstrumentation();
|
|
819
|
+
const googleGenAIInstrumentation = new GoogleGenAIInstrumentation();
|
|
820
|
+
const vercelAIInstrumentation = new VercelAIInstrumentation();
|
|
821
|
+
registerInstrumentations({
|
|
822
|
+
tracerProvider: provider,
|
|
823
|
+
instrumentations: [
|
|
824
|
+
openaiInstrumentation,
|
|
825
|
+
anthropicInstrumentation,
|
|
826
|
+
googleGenAIInstrumentation,
|
|
827
|
+
vercelAIInstrumentation
|
|
828
|
+
]
|
|
829
|
+
});
|
|
830
|
+
registerEsmManualInstrument([
|
|
831
|
+
{ modules: ["openai"], instrumentation: openaiInstrumentation },
|
|
832
|
+
{ modules: ["@anthropic-ai/sdk"], instrumentation: anthropicInstrumentation },
|
|
833
|
+
{ modules: ["@google/genai"], instrumentation: googleGenAIInstrumentation }
|
|
834
|
+
]);
|
|
835
|
+
active = {
|
|
836
|
+
config,
|
|
837
|
+
deployment,
|
|
838
|
+
shutdown: async () => {
|
|
839
|
+
try {
|
|
840
|
+
await provider.shutdown();
|
|
841
|
+
} finally {
|
|
842
|
+
active = null;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
if (deployment.gitSha) void emitDeploymentDetected(config, deployment);
|
|
847
|
+
if (config.debug) {
|
|
848
|
+
console.log(
|
|
849
|
+
`[scope] tracer started (endpoint=${config.endpoint}, env=${config.environment}` + (deployment.gitSha ? `, git_sha=${deployment.gitSha.slice(0, 7)}` : "") + ")."
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return active;
|
|
853
|
+
}
|
|
854
|
+
async function emitDeploymentDetected(config, deployment) {
|
|
855
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
856
|
+
await shipEvents(
|
|
857
|
+
[
|
|
858
|
+
{
|
|
859
|
+
event_type: "deployment_detected",
|
|
860
|
+
source: SDK_SOURCE,
|
|
861
|
+
timestamp: nowIso,
|
|
862
|
+
server_timestamp: nowIso,
|
|
863
|
+
session_id: `deploy_${deployment.deploymentId}`,
|
|
864
|
+
git_sha: deployment.gitSha,
|
|
865
|
+
deployment_id: deployment.deploymentId,
|
|
866
|
+
is_user_facing: false,
|
|
867
|
+
success: true,
|
|
868
|
+
environment: config.environment,
|
|
869
|
+
sdk_version: config.sdkVersion
|
|
870
|
+
}
|
|
871
|
+
],
|
|
872
|
+
config
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
export {
|
|
876
|
+
CREDENTIAL_REDACT_PATTERNS,
|
|
877
|
+
GoogleGenAIInstrumentation,
|
|
878
|
+
ScopeConfigError,
|
|
879
|
+
ScopeSpanExporter,
|
|
880
|
+
VercelAIInstrumentation,
|
|
881
|
+
detectGitSha,
|
|
882
|
+
getContext,
|
|
883
|
+
init,
|
|
884
|
+
isGoogleGenAIPatched,
|
|
885
|
+
isLlmSpan,
|
|
886
|
+
isVercelAIPatched,
|
|
887
|
+
resolveConfig,
|
|
888
|
+
resolveDeployment,
|
|
889
|
+
runWithContext,
|
|
890
|
+
setUser,
|
|
891
|
+
spanToScopeEvent
|
|
892
|
+
};
|
|
893
|
+
//# sourceMappingURL=index.js.map
|