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