@observyze/sdk 0.1.0 → 0.1.3
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/chunk-FQBYUOJB.mjs +316 -0
- package/dist/chunk-YSYKPKKX.mjs +357 -0
- package/dist/index--b41_E-1.d.mts +440 -0
- package/dist/index--b41_E-1.d.ts +440 -0
- package/dist/index-Cr-FN-y5.d.mts +405 -0
- package/dist/index-Cr-FN-y5.d.ts +405 -0
- package/dist/index-D4UXMom5.d.mts +429 -0
- package/dist/index-D4UXMom5.d.ts +429 -0
- package/dist/index.d.mts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1282 -0
- package/dist/index.mjs +936 -0
- package/dist/opentelemetry/index.d.mts +2 -0
- package/dist/opentelemetry/index.d.ts +2 -0
- package/dist/opentelemetry/index.js +338 -0
- package/dist/opentelemetry/index.mjs +6 -0
- package/package.json +17 -3
- package/INSTRUMENTATION_SUMMARY.md +0 -184
- package/examples/auto-instrumentation.ts +0 -210
- package/src/client.ts +0 -578
- package/src/index.ts +0 -21
- package/src/instrumentation/README.md +0 -227
- package/src/instrumentation/anthropic.ts +0 -233
- package/src/instrumentation/index.ts +0 -43
- package/src/instrumentation/openai.ts +0 -193
- package/src/trace.ts +0 -242
- package/src/types.ts +0 -102
- package/tsconfig.json +0 -14
package/dist/index.js
ADDED
|
@@ -0,0 +1,1282 @@
|
|
|
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 __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ObservyzeClient: () => ObservyzeClient,
|
|
34
|
+
ObservyzeSpanExporter: () => ObservyzeSpanExporter,
|
|
35
|
+
Span: () => Span,
|
|
36
|
+
SpanType: () => import_types.SpanType,
|
|
37
|
+
Trace: () => Trace,
|
|
38
|
+
TraceStatus: () => import_types.TraceStatus,
|
|
39
|
+
wrap: () => wrap,
|
|
40
|
+
wrapAnthropic: () => wrapAnthropic,
|
|
41
|
+
wrapOpenAI: () => wrapOpenAI
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/client.ts
|
|
46
|
+
var import_debug3 = __toESM(require("debug"));
|
|
47
|
+
|
|
48
|
+
// src/types.ts
|
|
49
|
+
var import_types = require("@observyze/types");
|
|
50
|
+
|
|
51
|
+
// src/trace.ts
|
|
52
|
+
var import_crypto = require("crypto");
|
|
53
|
+
function generateId() {
|
|
54
|
+
return `${Date.now()}-${(0, import_crypto.randomUUID)().substring(0, 8)}`;
|
|
55
|
+
}
|
|
56
|
+
var Span = class {
|
|
57
|
+
data;
|
|
58
|
+
startTime;
|
|
59
|
+
constructor(name, type, parentSpanId) {
|
|
60
|
+
this.startTime = Date.now();
|
|
61
|
+
this.data = {
|
|
62
|
+
span_id: generateId(),
|
|
63
|
+
parent_span_id: parentSpanId,
|
|
64
|
+
name,
|
|
65
|
+
type,
|
|
66
|
+
start_time: new Date(this.startTime),
|
|
67
|
+
end_time: new Date(this.startTime),
|
|
68
|
+
// Will be updated on end()
|
|
69
|
+
duration_ms: 0,
|
|
70
|
+
input: null,
|
|
71
|
+
output: null,
|
|
72
|
+
metadata: {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set the input data for this span
|
|
77
|
+
*/
|
|
78
|
+
setInput(input) {
|
|
79
|
+
this.data.input = input;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Set the output data for this span
|
|
84
|
+
*/
|
|
85
|
+
setOutput(output) {
|
|
86
|
+
this.data.output = output;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Record an error that occurred during span execution
|
|
91
|
+
*/
|
|
92
|
+
setError(error) {
|
|
93
|
+
this.data.error = {
|
|
94
|
+
message: error.message,
|
|
95
|
+
stack: error.stack,
|
|
96
|
+
code: error.code
|
|
97
|
+
};
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Set metadata for this span
|
|
102
|
+
*/
|
|
103
|
+
setMetadata(key, value) {
|
|
104
|
+
this.data.metadata[key] = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Set multiple metadata fields at once
|
|
109
|
+
*/
|
|
110
|
+
setMetadataAll(metadata) {
|
|
111
|
+
this.data.metadata = { ...this.data.metadata, ...metadata };
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Set token usage information
|
|
116
|
+
*/
|
|
117
|
+
setTokens(tokens) {
|
|
118
|
+
this.data.tokens = tokens;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* End the span and calculate duration
|
|
123
|
+
*/
|
|
124
|
+
end() {
|
|
125
|
+
const endTime = Date.now();
|
|
126
|
+
this.data.end_time = new Date(endTime);
|
|
127
|
+
this.data.duration_ms = endTime - this.startTime;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the span ID
|
|
131
|
+
*/
|
|
132
|
+
get id() {
|
|
133
|
+
return this.data.span_id;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get the span data for serialization
|
|
137
|
+
*/
|
|
138
|
+
toJSON() {
|
|
139
|
+
return { ...this.data };
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var Trace = class {
|
|
143
|
+
data;
|
|
144
|
+
startTime;
|
|
145
|
+
spans = [];
|
|
146
|
+
ended = false;
|
|
147
|
+
constructor(name, organizationId, projectId) {
|
|
148
|
+
this.startTime = Date.now();
|
|
149
|
+
this.data = {
|
|
150
|
+
trace_id: generateId(),
|
|
151
|
+
organization_id: organizationId,
|
|
152
|
+
project_id: projectId,
|
|
153
|
+
name,
|
|
154
|
+
status: import_types.TraceStatus.RUNNING,
|
|
155
|
+
start_time: new Date(this.startTime),
|
|
156
|
+
end_time: new Date(this.startTime),
|
|
157
|
+
// Will be updated on end()
|
|
158
|
+
duration_ms: 0,
|
|
159
|
+
metadata: {},
|
|
160
|
+
spans: [],
|
|
161
|
+
tags: []
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Start a new span within this trace
|
|
166
|
+
*/
|
|
167
|
+
startSpan(name, type, parentSpanId) {
|
|
168
|
+
if (this.ended) {
|
|
169
|
+
throw new Error("Cannot start span on an ended trace");
|
|
170
|
+
}
|
|
171
|
+
const span = new Span(name, type, parentSpanId);
|
|
172
|
+
this.spans.push(span);
|
|
173
|
+
return span;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Add metadata to the trace
|
|
177
|
+
*/
|
|
178
|
+
setMetadata(key, value) {
|
|
179
|
+
this.data.metadata[key] = value;
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Set multiple metadata fields at once
|
|
184
|
+
*/
|
|
185
|
+
setMetadataAll(metadata) {
|
|
186
|
+
this.data.metadata = { ...this.data.metadata, ...metadata };
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Add tags to the trace
|
|
191
|
+
*/
|
|
192
|
+
addTag(tag) {
|
|
193
|
+
if (!this.data.tags.includes(tag)) {
|
|
194
|
+
this.data.tags.push(tag);
|
|
195
|
+
}
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Add multiple tags at once
|
|
200
|
+
*/
|
|
201
|
+
addTags(tags) {
|
|
202
|
+
tags.forEach((tag) => this.addTag(tag));
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Set the user ID associated with this trace
|
|
207
|
+
*/
|
|
208
|
+
setUserId(userId) {
|
|
209
|
+
this.data.user_id = userId;
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Set the session ID associated with this trace
|
|
214
|
+
*/
|
|
215
|
+
setSessionId(sessionId) {
|
|
216
|
+
this.data.session_id = sessionId;
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* End the trace with a final status
|
|
221
|
+
*/
|
|
222
|
+
end(status = import_types.TraceStatus.SUCCESS) {
|
|
223
|
+
if (this.ended) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const endTime = Date.now();
|
|
227
|
+
this.data.end_time = new Date(endTime);
|
|
228
|
+
this.data.duration_ms = endTime - this.startTime;
|
|
229
|
+
this.data.status = status;
|
|
230
|
+
this.data.spans = this.spans.map((span) => span.toJSON());
|
|
231
|
+
this.ended = true;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get the trace ID
|
|
235
|
+
*/
|
|
236
|
+
get id() {
|
|
237
|
+
return this.data.trace_id;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Check if the trace has ended
|
|
241
|
+
*/
|
|
242
|
+
get isEnded() {
|
|
243
|
+
return this.ended;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get the trace data for serialization
|
|
247
|
+
*/
|
|
248
|
+
toJSON() {
|
|
249
|
+
return { ...this.data };
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/instrumentation/openai.ts
|
|
254
|
+
var import_debug = __toESM(require("debug"));
|
|
255
|
+
var log = (0, import_debug.default)("observyze:sdk");
|
|
256
|
+
function wrapOpenAI(client, nwClient) {
|
|
257
|
+
const anyClient = client;
|
|
258
|
+
if (nwClient.getConfig().enableProxyRedirect && anyClient.baseURL && anyClient.apiKey) {
|
|
259
|
+
const isAlreadyRedirected = anyClient.baseURL.includes("/api/v1/proxy/openai");
|
|
260
|
+
if (!isAlreadyRedirected) {
|
|
261
|
+
const originalApiKey = anyClient.apiKey;
|
|
262
|
+
anyClient.baseURL = `${nwClient.getConfig().endpoint}/api/v1/proxy/openai/v1`;
|
|
263
|
+
anyClient.apiKey = nwClient.getConfig().apiKey;
|
|
264
|
+
anyClient.defaultHeaders = {
|
|
265
|
+
...anyClient.defaultHeaders,
|
|
266
|
+
"x-provider-key": originalApiKey
|
|
267
|
+
};
|
|
268
|
+
if (nwClient.getConfig().debug) {
|
|
269
|
+
log("[Observyze SDK] Transparently redirected OpenAI client to proxy gateway:", anyClient.baseURL);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
274
|
+
client.chat.completions.create = async function(params, options) {
|
|
275
|
+
const isProxyRedirected = nwClient.getConfig().enableProxyRedirect && anyClient.baseURL?.includes("/api/v1/proxy/openai");
|
|
276
|
+
if (isProxyRedirected) {
|
|
277
|
+
return originalCreate(params, options);
|
|
278
|
+
}
|
|
279
|
+
const trace = nwClient.startTrace(`openai.chat.completions.create`, {
|
|
280
|
+
provider: "openai",
|
|
281
|
+
model: params.model
|
|
282
|
+
});
|
|
283
|
+
const span = trace.startSpan("chat.completions.create", import_types.SpanType.LLM);
|
|
284
|
+
span.setMetadata("model", params.model);
|
|
285
|
+
span.setMetadata("provider", "openai");
|
|
286
|
+
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
287
|
+
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
288
|
+
span.setInput({
|
|
289
|
+
model: params.model,
|
|
290
|
+
messages: params.messages,
|
|
291
|
+
temperature: params.temperature,
|
|
292
|
+
max_tokens: params.max_tokens
|
|
293
|
+
});
|
|
294
|
+
const startTime = Date.now();
|
|
295
|
+
try {
|
|
296
|
+
const response = await originalCreate(params, options);
|
|
297
|
+
if (params.stream) {
|
|
298
|
+
return wrapOpenAIStream(response, span, trace, startTime);
|
|
299
|
+
}
|
|
300
|
+
const completionResponse = response;
|
|
301
|
+
const latency = Date.now() - startTime;
|
|
302
|
+
span.setOutput({
|
|
303
|
+
id: completionResponse.id,
|
|
304
|
+
model: completionResponse.model,
|
|
305
|
+
choices: completionResponse.choices
|
|
306
|
+
});
|
|
307
|
+
if (completionResponse.usage) {
|
|
308
|
+
span.setTokens({
|
|
309
|
+
input: completionResponse.usage.prompt_tokens,
|
|
310
|
+
output: completionResponse.usage.completion_tokens,
|
|
311
|
+
total: completionResponse.usage.total_tokens
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
span.setMetadata("latency_ms", latency);
|
|
315
|
+
span.end();
|
|
316
|
+
trace.end();
|
|
317
|
+
return response;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const latency = Date.now() - startTime;
|
|
320
|
+
span.setMetadata("latency_ms", latency);
|
|
321
|
+
span.setError(error);
|
|
322
|
+
span.end();
|
|
323
|
+
trace.end();
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
return client;
|
|
328
|
+
}
|
|
329
|
+
function wrapOpenAIStream(stream, span, trace, startTime) {
|
|
330
|
+
const bufferedChunks = [];
|
|
331
|
+
let streamId = "";
|
|
332
|
+
let streamModel = "";
|
|
333
|
+
return {
|
|
334
|
+
[Symbol.asyncIterator]: async function* () {
|
|
335
|
+
try {
|
|
336
|
+
for await (const chunk of stream) {
|
|
337
|
+
if (chunk.id) streamId = chunk.id;
|
|
338
|
+
if (chunk.model) streamModel = chunk.model;
|
|
339
|
+
const delta = chunk.choices[0]?.delta;
|
|
340
|
+
if (delta?.content) {
|
|
341
|
+
bufferedChunks.push(delta.content);
|
|
342
|
+
}
|
|
343
|
+
yield chunk;
|
|
344
|
+
}
|
|
345
|
+
const latency = Date.now() - startTime;
|
|
346
|
+
const completeOutput = bufferedChunks.join("");
|
|
347
|
+
span.setOutput({
|
|
348
|
+
id: streamId,
|
|
349
|
+
model: streamModel,
|
|
350
|
+
content: completeOutput
|
|
351
|
+
});
|
|
352
|
+
span.setMetadata("latency_ms", latency);
|
|
353
|
+
span.setMetadata("streaming", true);
|
|
354
|
+
span.end();
|
|
355
|
+
trace.end();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const latency = Date.now() - startTime;
|
|
358
|
+
span.setMetadata("latency_ms", latency);
|
|
359
|
+
span.setError(error);
|
|
360
|
+
span.end();
|
|
361
|
+
trace.end();
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/instrumentation/anthropic.ts
|
|
369
|
+
var import_debug2 = __toESM(require("debug"));
|
|
370
|
+
var log2 = (0, import_debug2.default)("observyze:sdk");
|
|
371
|
+
function wrapAnthropic(client, nwClient) {
|
|
372
|
+
const anyClient = client;
|
|
373
|
+
if (nwClient.getConfig().enableProxyRedirect && anyClient.baseURL && anyClient.apiKey) {
|
|
374
|
+
const isAlreadyRedirected = anyClient.baseURL.includes("/api/v1/proxy/anthropic");
|
|
375
|
+
if (!isAlreadyRedirected) {
|
|
376
|
+
const originalApiKey = anyClient.apiKey;
|
|
377
|
+
anyClient.baseURL = `${nwClient.getConfig().endpoint}/api/v1/proxy/anthropic/v1`;
|
|
378
|
+
anyClient.apiKey = nwClient.getConfig().apiKey;
|
|
379
|
+
anyClient.defaultHeaders = {
|
|
380
|
+
...anyClient.defaultHeaders,
|
|
381
|
+
"x-provider-key": originalApiKey
|
|
382
|
+
};
|
|
383
|
+
if (nwClient.getConfig().debug) {
|
|
384
|
+
log2("[Observyze SDK] Transparently redirected Anthropic client to proxy gateway:", anyClient.baseURL);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
389
|
+
client.messages.create = async function(params, options) {
|
|
390
|
+
const isProxyRedirected = nwClient.getConfig().enableProxyRedirect && anyClient.baseURL?.includes("/api/v1/proxy/anthropic");
|
|
391
|
+
if (isProxyRedirected) {
|
|
392
|
+
return originalCreate(params, options);
|
|
393
|
+
}
|
|
394
|
+
const trace = nwClient.startTrace(`anthropic.messages.create`, {
|
|
395
|
+
provider: "anthropic",
|
|
396
|
+
model: params.model
|
|
397
|
+
});
|
|
398
|
+
const span = trace.startSpan("messages.create", import_types.SpanType.LLM);
|
|
399
|
+
span.setMetadata("model", params.model);
|
|
400
|
+
span.setMetadata("provider", "anthropic");
|
|
401
|
+
if (params.temperature !== void 0) span.setMetadata("temperature", params.temperature);
|
|
402
|
+
if (params.max_tokens !== void 0) span.setMetadata("max_tokens", params.max_tokens);
|
|
403
|
+
if (params.system !== void 0) span.setMetadata("system", params.system);
|
|
404
|
+
span.setInput({
|
|
405
|
+
model: params.model,
|
|
406
|
+
messages: params.messages,
|
|
407
|
+
max_tokens: params.max_tokens,
|
|
408
|
+
temperature: params.temperature,
|
|
409
|
+
system: params.system
|
|
410
|
+
});
|
|
411
|
+
const startTime = Date.now();
|
|
412
|
+
try {
|
|
413
|
+
const response = await originalCreate(params, options);
|
|
414
|
+
if (params.stream) {
|
|
415
|
+
return wrapAnthropicStream(response, span, trace, startTime);
|
|
416
|
+
}
|
|
417
|
+
const messageResponse = response;
|
|
418
|
+
const latency = Date.now() - startTime;
|
|
419
|
+
span.setOutput({
|
|
420
|
+
id: messageResponse.id,
|
|
421
|
+
model: messageResponse.model,
|
|
422
|
+
role: messageResponse.role,
|
|
423
|
+
content: messageResponse.content,
|
|
424
|
+
stop_reason: messageResponse.stop_reason
|
|
425
|
+
});
|
|
426
|
+
if (messageResponse.usage) {
|
|
427
|
+
span.setTokens({
|
|
428
|
+
input: messageResponse.usage.input_tokens,
|
|
429
|
+
output: messageResponse.usage.output_tokens,
|
|
430
|
+
total: messageResponse.usage.input_tokens + messageResponse.usage.output_tokens
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
span.setMetadata("latency_ms", latency);
|
|
434
|
+
span.end();
|
|
435
|
+
trace.end();
|
|
436
|
+
return response;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
const latency = Date.now() - startTime;
|
|
439
|
+
span.setMetadata("latency_ms", latency);
|
|
440
|
+
span.setError(error);
|
|
441
|
+
span.end();
|
|
442
|
+
trace.end();
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
return client;
|
|
447
|
+
}
|
|
448
|
+
function wrapAnthropicStream(stream, span, trace, startTime) {
|
|
449
|
+
const bufferedChunks = [];
|
|
450
|
+
let messageId = "";
|
|
451
|
+
let messageModel = "";
|
|
452
|
+
let stopReason = null;
|
|
453
|
+
let inputTokens = 0;
|
|
454
|
+
let outputTokens = 0;
|
|
455
|
+
return {
|
|
456
|
+
[Symbol.asyncIterator]: async function* () {
|
|
457
|
+
try {
|
|
458
|
+
for await (const event of stream) {
|
|
459
|
+
if (event.type === "message_start" && event.message) {
|
|
460
|
+
messageId = event.message.id;
|
|
461
|
+
messageModel = event.message.model;
|
|
462
|
+
if (event.message.usage) {
|
|
463
|
+
inputTokens = event.message.usage.input_tokens;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (event.type === "content_block_delta" && event.delta?.text) {
|
|
467
|
+
bufferedChunks.push(event.delta.text);
|
|
468
|
+
}
|
|
469
|
+
if (event.type === "message_delta" && event.delta) {
|
|
470
|
+
if (event.delta.stop_reason) {
|
|
471
|
+
stopReason = event.delta.stop_reason;
|
|
472
|
+
}
|
|
473
|
+
if (event.usage?.output_tokens) {
|
|
474
|
+
outputTokens = event.usage.output_tokens;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
yield event;
|
|
478
|
+
}
|
|
479
|
+
const latency = Date.now() - startTime;
|
|
480
|
+
const completeOutput = bufferedChunks.join("");
|
|
481
|
+
span.setOutput({
|
|
482
|
+
id: messageId,
|
|
483
|
+
model: messageModel,
|
|
484
|
+
content: completeOutput,
|
|
485
|
+
stop_reason: stopReason
|
|
486
|
+
});
|
|
487
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
488
|
+
span.setTokens({
|
|
489
|
+
input: inputTokens,
|
|
490
|
+
output: outputTokens,
|
|
491
|
+
total: inputTokens + outputTokens
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
span.setMetadata("latency_ms", latency);
|
|
495
|
+
span.setMetadata("streaming", true);
|
|
496
|
+
span.end();
|
|
497
|
+
trace.end();
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const latency = Date.now() - startTime;
|
|
500
|
+
span.setMetadata("latency_ms", latency);
|
|
501
|
+
span.setError(error);
|
|
502
|
+
span.end();
|
|
503
|
+
trace.end();
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/instrumentation/index.ts
|
|
511
|
+
function wrap(client, nwClient) {
|
|
512
|
+
if ("chat" in client && client.chat && "completions" in client.chat) {
|
|
513
|
+
return wrapOpenAI(client, nwClient);
|
|
514
|
+
}
|
|
515
|
+
if ("messages" in client && client.messages && "create" in client.messages) {
|
|
516
|
+
return wrapAnthropic(client, nwClient);
|
|
517
|
+
}
|
|
518
|
+
throw new Error(
|
|
519
|
+
"Observyze SDK: Unsupported client type. Supported clients: OpenAI, Anthropic"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/client.ts
|
|
524
|
+
var import_fs = __toESM(require("fs"));
|
|
525
|
+
var import_path = __toESM(require("path"));
|
|
526
|
+
var log3 = (0, import_debug3.default)("observyze:sdk");
|
|
527
|
+
var DEFAULT_CONFIG = {
|
|
528
|
+
endpoint: "http://localhost:3001",
|
|
529
|
+
batchSize: 100,
|
|
530
|
+
flushInterval: 5e3,
|
|
531
|
+
enableAutoInstrumentation: true,
|
|
532
|
+
debug: false,
|
|
533
|
+
dryRun: false,
|
|
534
|
+
enablePiiRedaction: true,
|
|
535
|
+
hallucinationThreshold: 0.8,
|
|
536
|
+
safetyThreshold: 0.9,
|
|
537
|
+
confidenceThreshold: 0.4,
|
|
538
|
+
evalEndpoint: process.env.EVAL_ENDPOINT || (process.env.NODE_ENV === "production" ? "https://api.observyze.com" : "http://localhost:3001"),
|
|
539
|
+
enableCircuitBreaker: true,
|
|
540
|
+
failClosed: true,
|
|
541
|
+
enableProxyRedirect: true
|
|
542
|
+
};
|
|
543
|
+
var ObservyzeClient = class _ObservyzeClient {
|
|
544
|
+
config;
|
|
545
|
+
traceBuffer = [];
|
|
546
|
+
flushTimer = null;
|
|
547
|
+
isShuttingDown = false;
|
|
548
|
+
MAX_QUEUE_SIZE = 1e3;
|
|
549
|
+
RETRY_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
550
|
+
/**
|
|
551
|
+
* Parse a JSON API error response and extract trace_id, error code, and message.
|
|
552
|
+
* The api-gateway error handler includes these fields in every error response.
|
|
553
|
+
*/
|
|
554
|
+
static parseApiError(_response, body) {
|
|
555
|
+
try {
|
|
556
|
+
const parsed = JSON.parse(body);
|
|
557
|
+
const error = parsed.error || parsed;
|
|
558
|
+
return {
|
|
559
|
+
traceId: error.trace_id || "unknown",
|
|
560
|
+
code: error.code || "UNKNOWN_ERROR",
|
|
561
|
+
message: error.message || body.slice(0, 200)
|
|
562
|
+
};
|
|
563
|
+
} catch {
|
|
564
|
+
return {
|
|
565
|
+
traceId: "unknown",
|
|
566
|
+
code: "UNKNOWN_ERROR",
|
|
567
|
+
message: body.slice(0, 200)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Format an API error into a user-friendly message with trace_id for correlation.
|
|
573
|
+
* Example output:
|
|
574
|
+
* "Observyze API error (401 [ref: err_a1b2c3d4]): MISSING_PROVIDER_KEY — No API key configured..."
|
|
575
|
+
*/
|
|
576
|
+
static formatApiError(response, body) {
|
|
577
|
+
const { traceId, code, message } = _ObservyzeClient.parseApiError(response, body);
|
|
578
|
+
const prefix = traceId !== "unknown" ? ` [ref: ${traceId}]` : "";
|
|
579
|
+
return `Observyze API error (${response.status}${prefix}): ${code} \u2014 ${message}`;
|
|
580
|
+
}
|
|
581
|
+
constructor(config) {
|
|
582
|
+
if (!config.apiKey) {
|
|
583
|
+
throw new Error("Observyze SDK: apiKey is required");
|
|
584
|
+
}
|
|
585
|
+
this.config = {
|
|
586
|
+
...DEFAULT_CONFIG,
|
|
587
|
+
...config,
|
|
588
|
+
organizationId: config.organizationId || "",
|
|
589
|
+
projectId: config.projectId || ""
|
|
590
|
+
};
|
|
591
|
+
this.startFlushTimer();
|
|
592
|
+
if (this.config.debug) {
|
|
593
|
+
log3("[Observyze SDK] Initialized with config:", {
|
|
594
|
+
endpoint: this.config.endpoint,
|
|
595
|
+
batchSize: this.config.batchSize,
|
|
596
|
+
flushInterval: this.config.flushInterval,
|
|
597
|
+
dryRun: this.config.dryRun
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Start a new trace
|
|
603
|
+
*/
|
|
604
|
+
startTrace(name, metadata) {
|
|
605
|
+
const trace = new Trace(
|
|
606
|
+
name,
|
|
607
|
+
this.config.organizationId,
|
|
608
|
+
this.config.projectId
|
|
609
|
+
);
|
|
610
|
+
if (metadata) {
|
|
611
|
+
trace.setMetadataAll(metadata);
|
|
612
|
+
}
|
|
613
|
+
const originalEnd = trace.end.bind(trace);
|
|
614
|
+
trace.end = (status = import_types.TraceStatus.SUCCESS) => {
|
|
615
|
+
originalEnd(status);
|
|
616
|
+
this.bufferTrace(trace);
|
|
617
|
+
};
|
|
618
|
+
return trace;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Buffer a completed trace for batch sending
|
|
622
|
+
*/
|
|
623
|
+
bufferTrace(trace) {
|
|
624
|
+
if (!trace.isEnded) {
|
|
625
|
+
if (this.config.debug) {
|
|
626
|
+
log3.extend("warn")("[Observyze SDK] Attempted to buffer a trace that has not ended");
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (this.traceBuffer.length >= this.MAX_QUEUE_SIZE) {
|
|
631
|
+
if (this.config.debug) {
|
|
632
|
+
log3.extend("warn")(`[Observyze SDK] Queue at max capacity (${this.MAX_QUEUE_SIZE}), dropping oldest trace`);
|
|
633
|
+
}
|
|
634
|
+
this.traceBuffer.shift();
|
|
635
|
+
}
|
|
636
|
+
this.traceBuffer.push(trace);
|
|
637
|
+
if (this.config.debug) {
|
|
638
|
+
log3(`[Observyze SDK] Buffered trace ${trace.id} (${this.traceBuffer.length}/${this.config.batchSize})`);
|
|
639
|
+
}
|
|
640
|
+
if (this.traceBuffer.length >= this.config.batchSize) {
|
|
641
|
+
this.flush().catch((err) => {
|
|
642
|
+
log3.extend("error")("[Observyze SDK] Error flushing buffer:", err);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Start the auto-flush timer
|
|
648
|
+
*/
|
|
649
|
+
startFlushTimer() {
|
|
650
|
+
if (this.flushTimer) {
|
|
651
|
+
clearInterval(this.flushTimer);
|
|
652
|
+
}
|
|
653
|
+
this.flushTimer = setInterval(() => {
|
|
654
|
+
if (this.traceBuffer.length > 0) {
|
|
655
|
+
this.flush().catch((err) => {
|
|
656
|
+
log3.extend("error")("[Observyze SDK] Error in auto-flush:", err);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}, this.config.flushInterval);
|
|
660
|
+
if (this.flushTimer.unref) {
|
|
661
|
+
this.flushTimer.unref();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Flush all buffered traces to the Ingestion Service
|
|
666
|
+
*/
|
|
667
|
+
async flush() {
|
|
668
|
+
if (this.traceBuffer.length === 0) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const tracesToSend = this.traceBuffer.splice(0, this.config.batchSize);
|
|
672
|
+
if (this.config.debug) {
|
|
673
|
+
log3(`[Observyze SDK] Flushing ${tracesToSend.length} traces`);
|
|
674
|
+
}
|
|
675
|
+
if (this.config.dryRun) {
|
|
676
|
+
if (this.config.debug) {
|
|
677
|
+
log3("[Observyze SDK] Dry-run mode: traces not sent");
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
await this.sendWithRetry(tracesToSend);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
const remainingSpace = this.MAX_QUEUE_SIZE - this.traceBuffer.length;
|
|
685
|
+
if (remainingSpace > 0) {
|
|
686
|
+
this.traceBuffer.unshift(...tracesToSend.slice(0, remainingSpace));
|
|
687
|
+
if (this.config.debug) {
|
|
688
|
+
log3(`[Observyze SDK] Re-queued ${Math.min(tracesToSend.length, remainingSpace)} traces after failure`);
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
if (this.config.debug) {
|
|
692
|
+
log3.extend("warn")(`[Observyze SDK] Queue full, dropped ${tracesToSend.length} traces`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (this.config.debug) {
|
|
696
|
+
log3.extend("error")("[Observyze SDK] Failed to send traces after retries:", error);
|
|
697
|
+
}
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Send traces with exponential backoff retry
|
|
703
|
+
*/
|
|
704
|
+
async sendWithRetry(traces) {
|
|
705
|
+
let lastError = null;
|
|
706
|
+
for (let attempt = 0; attempt < this.RETRY_DELAYS.length + 1; attempt++) {
|
|
707
|
+
try {
|
|
708
|
+
const response = await fetch(`${this.config.endpoint}/api/v1/ingest/batch`, {
|
|
709
|
+
method: "POST",
|
|
710
|
+
headers: {
|
|
711
|
+
"Content-Type": "application/json",
|
|
712
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
713
|
+
},
|
|
714
|
+
body: JSON.stringify({
|
|
715
|
+
traces: traces.map((trace) => {
|
|
716
|
+
const json = trace.toJSON();
|
|
717
|
+
if (this.config.enablePiiRedaction) {
|
|
718
|
+
json.spans = this.sanitizePII(json.spans);
|
|
719
|
+
}
|
|
720
|
+
return json;
|
|
721
|
+
})
|
|
722
|
+
})
|
|
723
|
+
});
|
|
724
|
+
if (!response.ok) {
|
|
725
|
+
const errorBody = await response.text();
|
|
726
|
+
const formatted = _ObservyzeClient.formatApiError(response, errorBody);
|
|
727
|
+
throw new Error(`[Observyze SDK] Trace ingestion failed. ${formatted}`);
|
|
728
|
+
}
|
|
729
|
+
if (this.config.debug) {
|
|
730
|
+
log3(`[Observyze SDK] Successfully sent ${traces.length} traces${attempt > 0 ? ` (after ${attempt} retries)` : ""}`);
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
} catch (error) {
|
|
734
|
+
lastError = error;
|
|
735
|
+
if (attempt >= this.RETRY_DELAYS.length) {
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
const delay = this.RETRY_DELAYS[attempt];
|
|
739
|
+
if (this.config.debug) {
|
|
740
|
+
log3.extend("warn")(`[Observyze SDK] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error);
|
|
741
|
+
}
|
|
742
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
throw lastError || new Error("Failed to send traces after all retries");
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Shutdown the SDK and flush remaining traces
|
|
749
|
+
*/
|
|
750
|
+
async shutdown() {
|
|
751
|
+
if (this.isShuttingDown) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
this.isShuttingDown = true;
|
|
755
|
+
if (this.config.debug) {
|
|
756
|
+
log3("[Observyze SDK] Shutting down...");
|
|
757
|
+
}
|
|
758
|
+
if (this.flushTimer) {
|
|
759
|
+
clearInterval(this.flushTimer);
|
|
760
|
+
this.flushTimer = null;
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
await this.flush();
|
|
764
|
+
} catch (error) {
|
|
765
|
+
log3.extend("error")("[Observyze SDK] Error during shutdown flush:", error);
|
|
766
|
+
}
|
|
767
|
+
if (this.config.debug) {
|
|
768
|
+
log3("[Observyze SDK] Shutdown complete");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Get current buffer size
|
|
773
|
+
*/
|
|
774
|
+
get bufferSize() {
|
|
775
|
+
return this.traceBuffer.length;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Get SDK configuration
|
|
779
|
+
*/
|
|
780
|
+
getConfig() {
|
|
781
|
+
return { ...this.config };
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Wrap an LLM client (OpenAI, Anthropic) to enable auto-instrumentation
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* ```typescript
|
|
788
|
+
* import OpenAI from 'openai'
|
|
789
|
+
* import { ObservyzeClient } from '@observyze/sdk'
|
|
790
|
+
*
|
|
791
|
+
* const nw = new ObservyzeClient({ apiKey: 'your-api-key' })
|
|
792
|
+
* const openai = new OpenAI({ apiKey: 'openai-key' })
|
|
793
|
+
*
|
|
794
|
+
* // Wrap the client to enable auto-instrumentation
|
|
795
|
+
* nw.wrap(openai)
|
|
796
|
+
*
|
|
797
|
+
* // All calls are now automatically traced
|
|
798
|
+
* const response = await openai.chat.completions.create({
|
|
799
|
+
* model: 'gpt-4',
|
|
800
|
+
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
801
|
+
* })
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
wrap(client) {
|
|
805
|
+
return wrap(client, this);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Sync local agent .history file to Observyze cloud
|
|
809
|
+
* Parses JSON/NDJSON agent history and sends to ingestion endpoint.
|
|
810
|
+
*/
|
|
811
|
+
async syncLocalHistory(filePath) {
|
|
812
|
+
try {
|
|
813
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
814
|
+
throw new Error("syncLocalHistory is only available in Node.js environments");
|
|
815
|
+
}
|
|
816
|
+
const fullPath = import_path.default.resolve(process.cwd(), filePath);
|
|
817
|
+
if (!import_fs.default.existsSync(fullPath)) {
|
|
818
|
+
throw new Error(`History file not found: ${fullPath}`);
|
|
819
|
+
}
|
|
820
|
+
const content = import_fs.default.readFileSync(fullPath, "utf-8");
|
|
821
|
+
let items = [];
|
|
822
|
+
try {
|
|
823
|
+
items = JSON.parse(content);
|
|
824
|
+
} catch (e) {
|
|
825
|
+
items = content.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
826
|
+
}
|
|
827
|
+
if (!Array.isArray(items)) {
|
|
828
|
+
items = [items];
|
|
829
|
+
}
|
|
830
|
+
if (this.config.debug) {
|
|
831
|
+
log3(`[Observyze SDK] Syncing ${items.length} traces from ${filePath}`);
|
|
832
|
+
}
|
|
833
|
+
for (let i = 0; i < items.length; i += this.config.batchSize) {
|
|
834
|
+
const batch = items.slice(i, i + this.config.batchSize);
|
|
835
|
+
const response = await fetch(`${this.config.endpoint}/api/v1/ingest/batch`, {
|
|
836
|
+
method: "POST",
|
|
837
|
+
headers: {
|
|
838
|
+
"Content-Type": "application/json",
|
|
839
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
840
|
+
},
|
|
841
|
+
body: JSON.stringify({ traces: batch })
|
|
842
|
+
});
|
|
843
|
+
if (!response.ok) {
|
|
844
|
+
const errorBody = await response.text();
|
|
845
|
+
const formatted = _ObservyzeClient.formatApiError(response, errorBody);
|
|
846
|
+
throw new Error(`[Observyze SDK] Local history sync failed. ${formatted}`);
|
|
847
|
+
}
|
|
848
|
+
if (this.config.debug) {
|
|
849
|
+
log3(`[Observyze SDK] Synced batch of ${batch.length} traces from local history`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} catch (err) {
|
|
853
|
+
log3.extend("error")("[Observyze SDK] Failed to sync local history:", err);
|
|
854
|
+
throw err;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Industry-grade PII Redaction (Compliance & RBAC)
|
|
859
|
+
*
|
|
860
|
+
* Recursively scrubs PII from trace data before transmission to the cloud.
|
|
861
|
+
* Coverage: emails, JWTs, bearer tokens, AWS/API keys, SSNs, credit cards,
|
|
862
|
+
* phone numbers (US + E.164), IPv4/IPv6, passport numbers, ZIP codes, plus
|
|
863
|
+
* key-based redaction for sensitive JSON fields (password, token, api_key, etc.).
|
|
864
|
+
*
|
|
865
|
+
* Design:
|
|
866
|
+
* - Pure function, never mutates the original object
|
|
867
|
+
* - Depth-limited to 16 levels to prevent stack overflow on deep agent outputs
|
|
868
|
+
* - Key-aware: sensitive key names are fully redacted regardless of value format
|
|
869
|
+
*/
|
|
870
|
+
static PII_PATTERNS = [
|
|
871
|
+
{ pattern: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/gi, label: "[EMAIL_REDACTED]" },
|
|
872
|
+
{ pattern: /\beyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\b/g, label: "[JWT_REDACTED]" },
|
|
873
|
+
{ pattern: /\b(Bearer|Token|Basic)\s+[A-Za-z0-9\-.~+\/]+=*\b/gi, label: "[AUTH_TOKEN_REDACTED]" },
|
|
874
|
+
{ pattern: /\b(AKIA|ASIA|AROA|ANPA|ANVA|AIDA)[A-Z0-9]{16}\b/g, label: "[AWS_KEY_REDACTED]" },
|
|
875
|
+
{ pattern: /\b(sk-[A-Za-z0-9\-]{20,}|pk-[A-Za-z0-9\-]{20,}|ob_[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,})\b/g, label: "[API_KEY_REDACTED]" },
|
|
876
|
+
{ pattern: /\b\d{3}-\d{2}-\d{4}\b/g, label: "[SSN_REDACTED]" },
|
|
877
|
+
{ pattern: /\b(?:\d[ \-]?){13,18}\d\b/g, label: "[CC_REDACTED]" },
|
|
878
|
+
{ pattern: /(?:\+1[\s.\-]?)?\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]?\d{4}\b/g, label: "[PHONE_REDACTED]" },
|
|
879
|
+
{ pattern: /\+\d{1,3}[\s.\-]?\(?\d{1,4}\)?[\s.\-]?\d{1,4}[\s.\-]?\d{1,9}/g, label: "[PHONE_REDACTED]" },
|
|
880
|
+
{ pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g, label: "[IP_REDACTED]" },
|
|
881
|
+
{ pattern: /\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b/g, label: "[IP_REDACTED]" }
|
|
882
|
+
];
|
|
883
|
+
static SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
884
|
+
"password",
|
|
885
|
+
"passwd",
|
|
886
|
+
"secret",
|
|
887
|
+
"token",
|
|
888
|
+
"apikey",
|
|
889
|
+
"api_key",
|
|
890
|
+
"accesstoken",
|
|
891
|
+
"access_token",
|
|
892
|
+
"refreshtoken",
|
|
893
|
+
"refresh_token",
|
|
894
|
+
"authorization",
|
|
895
|
+
"auth",
|
|
896
|
+
"credential",
|
|
897
|
+
"credentials",
|
|
898
|
+
"private_key",
|
|
899
|
+
"privatekey",
|
|
900
|
+
"client_secret",
|
|
901
|
+
"clientsecret",
|
|
902
|
+
"ssn",
|
|
903
|
+
"social_security",
|
|
904
|
+
"dob",
|
|
905
|
+
"date_of_birth",
|
|
906
|
+
"dateofbirth",
|
|
907
|
+
"passport",
|
|
908
|
+
"passport_number",
|
|
909
|
+
"credit_card",
|
|
910
|
+
"creditcard",
|
|
911
|
+
"card_number",
|
|
912
|
+
"cardnumber",
|
|
913
|
+
"cvv",
|
|
914
|
+
"cvc",
|
|
915
|
+
"pin",
|
|
916
|
+
"bank_account",
|
|
917
|
+
"routing_number"
|
|
918
|
+
]);
|
|
919
|
+
static isSensitiveKey(key) {
|
|
920
|
+
const normalized = key.toLowerCase().replace(/-/g, "_");
|
|
921
|
+
if (_ObservyzeClient.SENSITIVE_KEYS.has(normalized)) return true;
|
|
922
|
+
const segments = normalized.split("_");
|
|
923
|
+
for (const segment of segments) {
|
|
924
|
+
if (_ObservyzeClient.SENSITIVE_KEYS.has(segment)) return true;
|
|
925
|
+
}
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
sanitizePII(data, depth = 0) {
|
|
929
|
+
if (depth > 16) return "[MAX_DEPTH_EXCEEDED]";
|
|
930
|
+
if (data === null || data === void 0) return data;
|
|
931
|
+
if (typeof data === "string") {
|
|
932
|
+
let result = data;
|
|
933
|
+
for (const { pattern, label } of _ObservyzeClient.PII_PATTERNS) {
|
|
934
|
+
pattern.lastIndex = 0;
|
|
935
|
+
result = result.replace(pattern, label);
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
if (typeof data === "number" || typeof data === "boolean") return data;
|
|
940
|
+
if (Array.isArray(data)) return data.map((item) => this.sanitizePII(item, depth + 1));
|
|
941
|
+
if (typeof data === "object") {
|
|
942
|
+
const sanitized = {};
|
|
943
|
+
for (const [k, v] of Object.entries(data)) {
|
|
944
|
+
sanitized[k] = _ObservyzeClient.isSensitiveKey(k) ? "[REDACTED]" : this.sanitizePII(v, depth + 1);
|
|
945
|
+
}
|
|
946
|
+
return sanitized;
|
|
947
|
+
}
|
|
948
|
+
return data;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Phase 4: Autonomous Circuit Breakers (Requirement 4.1)
|
|
952
|
+
* Evaluate a trace or text for hallucination in real-time.
|
|
953
|
+
* If hallucination score > hallucinationThreshold, the SDK blocks execution.
|
|
954
|
+
*
|
|
955
|
+
* Returns GuardrailResult with score: null when evaluation couldn't be performed.
|
|
956
|
+
* In failClosed mode, null scores result in blocked execution.
|
|
957
|
+
* In failOpen mode, null scores allow execution through.
|
|
958
|
+
*/
|
|
959
|
+
async checkGuardrails(content) {
|
|
960
|
+
if (!this.config.enableCircuitBreaker) {
|
|
961
|
+
return { pass: true, score: 0, safetyScore: 0, evaluationSource: "disabled" };
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
if (this.config.debug) {
|
|
965
|
+
log3(`[Observyze Guardrail] Analyzing payload for hallucination anomalies...`);
|
|
966
|
+
}
|
|
967
|
+
const evalEndpoint = this.config.evalEndpoint;
|
|
968
|
+
const payload = typeof content === "string" ? { text: content, organization_id: this.config.organizationId } : { trace: content, organization_id: this.config.organizationId };
|
|
969
|
+
const controller = new AbortController();
|
|
970
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
971
|
+
let evalResult = null;
|
|
972
|
+
try {
|
|
973
|
+
const response = await fetch(`${evalEndpoint}/api/v1/evaluate/hallucination`, {
|
|
974
|
+
method: "POST",
|
|
975
|
+
headers: {
|
|
976
|
+
"Content-Type": "application/json",
|
|
977
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
978
|
+
},
|
|
979
|
+
body: JSON.stringify(payload),
|
|
980
|
+
signal: controller.signal
|
|
981
|
+
});
|
|
982
|
+
clearTimeout(timeout);
|
|
983
|
+
if (response.ok) {
|
|
984
|
+
evalResult = await response.json();
|
|
985
|
+
} else {
|
|
986
|
+
const errorBody = await response.text();
|
|
987
|
+
const { traceId, code, message } = _ObservyzeClient.parseApiError(response, errorBody);
|
|
988
|
+
if (this.config.debug) {
|
|
989
|
+
log3.extend("warn")(`[Observyze Guardrail] Eval returned ${response.status} [${code}] (ref: ${traceId}): ${message}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} catch (fetchError) {
|
|
993
|
+
clearTimeout(timeout);
|
|
994
|
+
if (fetchError.name === "AbortError") {
|
|
995
|
+
if (this.config.debug) {
|
|
996
|
+
log3.extend("warn")("[Observyze Guardrail] Evaluation timed out after 5s");
|
|
997
|
+
}
|
|
998
|
+
} else if (this.config.debug) {
|
|
999
|
+
log3.extend("warn")("[Observyze Guardrail] Evaluation request failed:", fetchError.message);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
let hallucinationScore = 0;
|
|
1003
|
+
let safetyScore = 0;
|
|
1004
|
+
let evaluationSource = "live";
|
|
1005
|
+
let confidence = null;
|
|
1006
|
+
if (evalResult) {
|
|
1007
|
+
hallucinationScore = evalResult.score ?? evalResult.hallucination_score ?? null;
|
|
1008
|
+
safetyScore = evalResult.safety_score ?? 0;
|
|
1009
|
+
evaluationSource = evalResult.evaluation_source ?? "live";
|
|
1010
|
+
confidence = evalResult.confidence ?? null;
|
|
1011
|
+
if (hallucinationScore === null) {
|
|
1012
|
+
if (this.config.failClosed) {
|
|
1013
|
+
if (this.config.debug) {
|
|
1014
|
+
log3.extend("warn")("[Observyze Guardrail] Eval returned null score \u2014 failing closed (blocking)");
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
pass: false,
|
|
1018
|
+
score: null,
|
|
1019
|
+
confidence: null,
|
|
1020
|
+
safetyScore: null,
|
|
1021
|
+
evaluationSource: "error",
|
|
1022
|
+
fallbackReason: evalResult.message || "Evaluation failed to produce a score",
|
|
1023
|
+
reason: "Evaluation service failed to produce a score. Fail-closed: execution blocked."
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
if (this.config.debug) {
|
|
1027
|
+
log3("[Observyze Guardrail] Eval returned null score \u2014 allowing (fail-open)");
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
pass: true,
|
|
1031
|
+
score: null,
|
|
1032
|
+
confidence: null,
|
|
1033
|
+
safetyScore: null,
|
|
1034
|
+
evaluationSource: "error",
|
|
1035
|
+
fallbackReason: evalResult.message || "Evaluation failed to produce a score"
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
} else if (this.config.failClosed) {
|
|
1039
|
+
if (this.config.debug) {
|
|
1040
|
+
log3.extend("warn")("[Observyze Guardrail] Eval unavailable \u2014 failing closed (blocking)");
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
pass: false,
|
|
1044
|
+
score: null,
|
|
1045
|
+
confidence: null,
|
|
1046
|
+
safetyScore: null,
|
|
1047
|
+
evaluationSource: "error",
|
|
1048
|
+
fallbackReason: "Evaluation service unreachable",
|
|
1049
|
+
reason: "Evaluation service unreachable. Fail-closed: execution blocked."
|
|
1050
|
+
};
|
|
1051
|
+
} else {
|
|
1052
|
+
if (this.config.debug) {
|
|
1053
|
+
log3("[Observyze Guardrail] Eval unavailable \u2014 allowing (fail-open)");
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
pass: true,
|
|
1057
|
+
score: null,
|
|
1058
|
+
confidence: null,
|
|
1059
|
+
safetyScore: null,
|
|
1060
|
+
evaluationSource: "error",
|
|
1061
|
+
fallbackReason: "Evaluation service unreachable"
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const hallThreshold = this.config.hallucinationThreshold;
|
|
1065
|
+
const safeThreshold = this.config.safetyThreshold;
|
|
1066
|
+
const confThreshold = this.config.confidenceThreshold;
|
|
1067
|
+
if (confidence !== null && confidence < confThreshold) {
|
|
1068
|
+
if (hallucinationScore >= hallThreshold) {
|
|
1069
|
+
if (this.config.debug) {
|
|
1070
|
+
log3.extend("warn")(`[Observyze Guardrail] High score (${hallucinationScore.toFixed(2)}) but low confidence (${confidence.toFixed(2)}). Alerting only.`);
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
pass: true,
|
|
1074
|
+
score: hallucinationScore,
|
|
1075
|
+
confidence,
|
|
1076
|
+
safetyScore,
|
|
1077
|
+
evaluationSource,
|
|
1078
|
+
reason: `Score ${hallucinationScore.toFixed(2)} but confidence ${confidence.toFixed(2)} is low. Execution allowed with alert.`
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (hallucinationScore >= hallThreshold) {
|
|
1083
|
+
if (this.config.debug) {
|
|
1084
|
+
log3.extend("warn")(`[Observyze Guardrail] Hallucination circuit breached! Score: ${hallucinationScore.toFixed(2)} >= ${hallThreshold}`);
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
pass: false,
|
|
1088
|
+
score: hallucinationScore,
|
|
1089
|
+
confidence,
|
|
1090
|
+
safetyScore,
|
|
1091
|
+
evaluationSource,
|
|
1092
|
+
reason: `Hallucination score ${hallucinationScore.toFixed(2)} exceeds threshold ${hallThreshold}. Execution blocked for human review.`
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if (safetyScore !== null && safetyScore >= safeThreshold) {
|
|
1096
|
+
if (this.config.debug) {
|
|
1097
|
+
log3.extend("warn")(`[Observyze Guardrail] Safety circuit breached! Score: ${safetyScore.toFixed(2)} >= ${safeThreshold}`);
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
pass: false,
|
|
1101
|
+
score: hallucinationScore,
|
|
1102
|
+
confidence,
|
|
1103
|
+
safetyScore,
|
|
1104
|
+
evaluationSource,
|
|
1105
|
+
reason: `Safety score ${safetyScore.toFixed(2)} exceeds threshold ${safeThreshold}. Execution blocked for safety review.`
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return { pass: true, score: hallucinationScore, confidence, safetyScore, evaluationSource };
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
log3.extend("error")("[Observyze Guardrail] Failed to evaluate:", err);
|
|
1111
|
+
if (this.config.failClosed) {
|
|
1112
|
+
return { pass: false, score: null, confidence: null, safetyScore: null, evaluationSource: "error", fallbackReason: "Guardrail exception", reason: "Guardrail error \u2014 fail-closed: execution blocked." };
|
|
1113
|
+
}
|
|
1114
|
+
return { pass: true, score: null, confidence: null, safetyScore: null, evaluationSource: "error", fallbackReason: "Guardrail exception" };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Phase 4: Autonomous Circuit Breakers
|
|
1119
|
+
* Execute an agent action wrapped with the Circuit Breaker.
|
|
1120
|
+
* Pauses execution if hallucination score >= hallucinationThreshold and requests human review.
|
|
1121
|
+
* @throws Error when execution is blocked by circuit breaker
|
|
1122
|
+
*/
|
|
1123
|
+
async executeWithCircuitBreaker(agentExecution, traceContext) {
|
|
1124
|
+
if (!this.config.enableCircuitBreaker) {
|
|
1125
|
+
return await agentExecution();
|
|
1126
|
+
}
|
|
1127
|
+
const guardResult = await this.checkGuardrails(traceContext || "execution context");
|
|
1128
|
+
if (!guardResult.pass) {
|
|
1129
|
+
const error = new Error(`[Observyze] Execution Blocked by Autonomous Circuit Breaker. Hallucination: ${guardResult.score?.toFixed(2) ?? "N/A"}, Safety: ${(guardResult.safetyScore ?? 0)?.toFixed(2) ?? "N/A"}. Reason: ${guardResult.reason}. Human approval required before agent can continue.`);
|
|
1130
|
+
if (this.config.debug) {
|
|
1131
|
+
log3.extend("error")("[Observyze CircuitBreaker] Execution blocked:", error.message);
|
|
1132
|
+
}
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
if (this.config.debug) {
|
|
1136
|
+
log3(`[Observyze CircuitBreaker] Execution allowed. Hallucination: ${guardResult.score?.toFixed(2) ?? "N/A"}, Safety: ${(guardResult.safetyScore ?? 0)?.toFixed(2) ?? "N/A"}`);
|
|
1137
|
+
}
|
|
1138
|
+
return await agentExecution();
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Phase 4: Bug Bounty Protocol (Automated) (Requirement 4.2)
|
|
1142
|
+
* Automatically shard persistent failure cases to external security researcher endpoints (e.g. HackerOne wrapper)
|
|
1143
|
+
*/
|
|
1144
|
+
async reportBugBounty(traceId, securityEndpoint, failureContext) {
|
|
1145
|
+
try {
|
|
1146
|
+
if (this.config.debug) {
|
|
1147
|
+
log3(`[Observyze SDK] Sharding persistent failure case ${traceId} to Bug Bounty Protocol endpoint...`);
|
|
1148
|
+
}
|
|
1149
|
+
await fetch(securityEndpoint, {
|
|
1150
|
+
method: "POST",
|
|
1151
|
+
headers: { "Content-Type": "application/json" },
|
|
1152
|
+
body: JSON.stringify({
|
|
1153
|
+
alert: "persistent_failure_sharded",
|
|
1154
|
+
trace_id: traceId,
|
|
1155
|
+
context: failureContext,
|
|
1156
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1157
|
+
})
|
|
1158
|
+
});
|
|
1159
|
+
if (this.config.debug) {
|
|
1160
|
+
log3(`[Observyze SDK] Bug Bounty payload successfully transmitted.`);
|
|
1161
|
+
}
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
log3.extend("error")("[Observyze Bug Bounty] Failed to shard failure case:", err);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// src/opentelemetry/exporter.ts
|
|
1169
|
+
var ObservyzeSpanExporter = class {
|
|
1170
|
+
client;
|
|
1171
|
+
config;
|
|
1172
|
+
constructor(client, config) {
|
|
1173
|
+
this.client = client;
|
|
1174
|
+
this.config = {
|
|
1175
|
+
serviceName: config?.serviceName || "unknown-service",
|
|
1176
|
+
projectId: config?.projectId || "",
|
|
1177
|
+
defaultSpanType: config?.defaultSpanType || "llm",
|
|
1178
|
+
headers: config?.headers || {}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Export spans — called by OTel SDK when spans are ready.
|
|
1183
|
+
* Converts OTel spans to Observyze traces and buffers them.
|
|
1184
|
+
*/
|
|
1185
|
+
async export(spans, resultCallback) {
|
|
1186
|
+
if (!spans || spans.length === 0) {
|
|
1187
|
+
resultCallback({ code: 0 });
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
const organizationId = this.client.config?.organizationId || "";
|
|
1192
|
+
const projectId = this.config.projectId || this.client.config?.projectId || "";
|
|
1193
|
+
for (const span of spans) {
|
|
1194
|
+
const spanContext = span.spanContext();
|
|
1195
|
+
const traceId = spanContext?.traceId || span.spanId();
|
|
1196
|
+
const trace = new Trace(
|
|
1197
|
+
span.name || "otel-span",
|
|
1198
|
+
organizationId,
|
|
1199
|
+
projectId
|
|
1200
|
+
);
|
|
1201
|
+
trace.setMetadata("source", "opentelemetry");
|
|
1202
|
+
trace.setMetadata("otel.trace_id", traceId);
|
|
1203
|
+
trace.setMetadata("otel.span_id", spanContext?.spanId || "");
|
|
1204
|
+
trace.setMetadata("service.name", this.config.serviceName);
|
|
1205
|
+
trace.setMetadataAll(span.attributes || {});
|
|
1206
|
+
if (span.resource?.attributes) {
|
|
1207
|
+
trace.setMetadataAll(span.resource.attributes);
|
|
1208
|
+
}
|
|
1209
|
+
const input = span.attributes?.["gen_ai.prompt.0.content"] || span.attributes?.["gen_ai.completion.0.content"] || span.attributes?.["llm.input"] || void 0;
|
|
1210
|
+
const output = span.attributes?.["gen_ai.completion.0.content"] || span.attributes?.["llm.output"] || void 0;
|
|
1211
|
+
const model = span.attributes?.["gen_ai.request.model"] || span.attributes?.["llm.model"] || void 0;
|
|
1212
|
+
const provider = span.attributes?.["gen_ai.request.provider"] || span.attributes?.["llm.provider"] || void 0;
|
|
1213
|
+
const inputTokens = typeof span.attributes?.["gen_ai.usage.input_tokens"] === "number" ? span.attributes["gen_ai.usage.input_tokens"] : void 0;
|
|
1214
|
+
const outputTokens = typeof span.attributes?.["gen_ai.usage.output_tokens"] === "number" ? span.attributes["gen_ai.usage.output_tokens"] : void 0;
|
|
1215
|
+
trace.setMetadata("provider", provider || "unknown");
|
|
1216
|
+
trace.setMetadata("model", model || "unknown");
|
|
1217
|
+
const nsDuration = span.duration;
|
|
1218
|
+
const durationMs = nsDuration ? Math.round(nsDuration / 1e6) : 0;
|
|
1219
|
+
trace.setMetadata("latency_ms", durationMs);
|
|
1220
|
+
trace.setMetadata("otel.duration_ns", nsDuration);
|
|
1221
|
+
const oSpan = trace.startSpan(span.name || "otel-operation", this.config.defaultSpanType);
|
|
1222
|
+
if (input) oSpan.setInput(input);
|
|
1223
|
+
if (output) oSpan.setOutput(output);
|
|
1224
|
+
if (inputTokens || outputTokens) {
|
|
1225
|
+
oSpan.setTokens({
|
|
1226
|
+
input: inputTokens || 0,
|
|
1227
|
+
output: outputTokens || 0,
|
|
1228
|
+
total: (inputTokens || 0) + (outputTokens || 0)
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
if (model) oSpan.setMetadata("model", model);
|
|
1232
|
+
if (provider) oSpan.setMetadata("provider", provider);
|
|
1233
|
+
if (span.attributes) oSpan.setMetadataAll(span.attributes);
|
|
1234
|
+
const status = span.status;
|
|
1235
|
+
const statusCode = status?.code;
|
|
1236
|
+
if (statusCode === 2) {
|
|
1237
|
+
oSpan.setError(new Error(status?.message || "OTel span error"));
|
|
1238
|
+
trace.end(import_types.TraceStatus.ERROR);
|
|
1239
|
+
} else {
|
|
1240
|
+
trace.end(import_types.TraceStatus.SUCCESS);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
resultCallback({ code: 0 });
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
resultCallback({
|
|
1246
|
+
code: 1,
|
|
1247
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Called when the exporter is shut down.
|
|
1253
|
+
* Flushes any remaining buffered traces via the SDK client.
|
|
1254
|
+
*/
|
|
1255
|
+
async shutdown() {
|
|
1256
|
+
try {
|
|
1257
|
+
await this.client.flush();
|
|
1258
|
+
} catch {
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Called by the OTel SDK to force-export buffered spans.
|
|
1263
|
+
*/
|
|
1264
|
+
async forceFlush() {
|
|
1265
|
+
try {
|
|
1266
|
+
await this.client.flush();
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1272
|
+
0 && (module.exports = {
|
|
1273
|
+
ObservyzeClient,
|
|
1274
|
+
ObservyzeSpanExporter,
|
|
1275
|
+
Span,
|
|
1276
|
+
SpanType,
|
|
1277
|
+
Trace,
|
|
1278
|
+
TraceStatus,
|
|
1279
|
+
wrap,
|
|
1280
|
+
wrapAnthropic,
|
|
1281
|
+
wrapOpenAI
|
|
1282
|
+
});
|