@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/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
+ });