observa-sdk 0.0.6 → 0.0.7
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.cjs +543 -21
- package/dist/index.d.cts +129 -2
- package/dist/index.d.ts +129 -2
- package/dist/index.js +543 -21
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,14 @@ __export(index_exports, {
|
|
|
24
24
|
init: () => init
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var contextModule = null;
|
|
28
|
+
try {
|
|
29
|
+
const requireFn = globalThis.require;
|
|
30
|
+
if (typeof requireFn !== "undefined") {
|
|
31
|
+
contextModule = requireFn("./context.js");
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
27
35
|
function getNodeEnv() {
|
|
28
36
|
try {
|
|
29
37
|
const proc = globalThis.process;
|
|
@@ -246,6 +254,20 @@ var Observa = class {
|
|
|
246
254
|
isProduction;
|
|
247
255
|
sampleRate;
|
|
248
256
|
maxResponseChars;
|
|
257
|
+
// Buffering and retry (now stores canonical events)
|
|
258
|
+
eventBuffer = [];
|
|
259
|
+
flushPromise = null;
|
|
260
|
+
flushInProgress = false;
|
|
261
|
+
maxBufferSize = 100;
|
|
262
|
+
flushIntervalMs = 5e3;
|
|
263
|
+
// Flush every 5 seconds
|
|
264
|
+
flushIntervalId = null;
|
|
265
|
+
// Span hierarchy tracking (for manual trace management)
|
|
266
|
+
currentTraceId = null;
|
|
267
|
+
rootSpanId = null;
|
|
268
|
+
spanStack = [];
|
|
269
|
+
// Stack for tracking parent-child relationships
|
|
270
|
+
traceStartTime = null;
|
|
249
271
|
constructor(config) {
|
|
250
272
|
this.apiKey = config.apiKey;
|
|
251
273
|
let apiUrlEnv;
|
|
@@ -290,35 +312,465 @@ var Observa = class {
|
|
|
290
312
|
`\u{1F517} [Observa] Auth: ${jwtContext ? "JWT (auto-extracted)" : "Legacy (config)"}`
|
|
291
313
|
);
|
|
292
314
|
}
|
|
315
|
+
try {
|
|
316
|
+
if (typeof setInterval !== "undefined") {
|
|
317
|
+
this.flushIntervalId = setInterval(() => {
|
|
318
|
+
this.flush().catch((err) => {
|
|
319
|
+
console.error("[Observa] Periodic flush failed:", err);
|
|
320
|
+
});
|
|
321
|
+
}, this.flushIntervalMs);
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Flush buffered events to the API
|
|
328
|
+
* Returns a promise that resolves when all events are sent
|
|
329
|
+
*/
|
|
330
|
+
async flush() {
|
|
331
|
+
if (this.flushInProgress || this.eventBuffer.length === 0) {
|
|
332
|
+
return this.flushPromise || Promise.resolve();
|
|
333
|
+
}
|
|
334
|
+
this.flushInProgress = true;
|
|
335
|
+
this.flushPromise = this._doFlush();
|
|
336
|
+
try {
|
|
337
|
+
await this.flushPromise;
|
|
338
|
+
} finally {
|
|
339
|
+
this.flushInProgress = false;
|
|
340
|
+
this.flushPromise = null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Helper: Create base event properties
|
|
345
|
+
*/
|
|
346
|
+
createBaseEventProperties() {
|
|
347
|
+
const traceId = this.currentTraceId || crypto.randomUUID();
|
|
348
|
+
return {
|
|
349
|
+
tenant_id: this.tenantId,
|
|
350
|
+
project_id: this.projectId,
|
|
351
|
+
environment: this.environment,
|
|
352
|
+
trace_id: traceId
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Helper: Add event to buffer with proper span hierarchy
|
|
357
|
+
*/
|
|
358
|
+
addEvent(eventData) {
|
|
359
|
+
const baseProps = this.createBaseEventProperties();
|
|
360
|
+
const parentSpanId = this.spanStack.length > 0 ? this.spanStack[this.spanStack.length - 1] : null;
|
|
361
|
+
const event = {
|
|
362
|
+
...baseProps,
|
|
363
|
+
span_id: eventData.span_id || crypto.randomUUID(),
|
|
364
|
+
parent_span_id: (eventData.parent_span_id !== void 0 ? eventData.parent_span_id : parentSpanId) ?? null,
|
|
365
|
+
timestamp: eventData.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
366
|
+
event_type: eventData.event_type,
|
|
367
|
+
conversation_id: eventData.conversation_id ?? null,
|
|
368
|
+
session_id: eventData.session_id ?? null,
|
|
369
|
+
user_id: eventData.user_id ?? null,
|
|
370
|
+
agent_name: eventData.agent_name ?? null,
|
|
371
|
+
version: eventData.version ?? null,
|
|
372
|
+
route: eventData.route ?? null,
|
|
373
|
+
attributes: eventData.attributes
|
|
374
|
+
};
|
|
375
|
+
this.eventBuffer.push(event);
|
|
376
|
+
if (this.eventBuffer.length >= this.maxBufferSize) {
|
|
377
|
+
this.flush().catch((err) => {
|
|
378
|
+
console.error("[Observa] Auto-flush failed:", err);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Start a new trace (manual trace management)
|
|
384
|
+
*/
|
|
385
|
+
startTrace(options = {}) {
|
|
386
|
+
if (this.currentTraceId) {
|
|
387
|
+
console.warn("[Observa] Ending previous trace before starting new one");
|
|
388
|
+
this.endTrace().catch(console.error);
|
|
389
|
+
}
|
|
390
|
+
this.currentTraceId = crypto.randomUUID();
|
|
391
|
+
this.rootSpanId = crypto.randomUUID();
|
|
392
|
+
this.spanStack = [this.rootSpanId];
|
|
393
|
+
this.traceStartTime = Date.now();
|
|
394
|
+
this.addEvent({
|
|
395
|
+
event_type: "trace_start",
|
|
396
|
+
span_id: this.rootSpanId,
|
|
397
|
+
parent_span_id: null,
|
|
398
|
+
conversation_id: options.conversationId || null,
|
|
399
|
+
session_id: options.sessionId || null,
|
|
400
|
+
user_id: options.userId || null,
|
|
401
|
+
attributes: {
|
|
402
|
+
trace_start: {
|
|
403
|
+
name: options.name || null,
|
|
404
|
+
metadata: options.metadata || null
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
return this.currentTraceId;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Track a tool call
|
|
412
|
+
*/
|
|
413
|
+
trackToolCall(options) {
|
|
414
|
+
const spanId = crypto.randomUUID();
|
|
415
|
+
this.addEvent({
|
|
416
|
+
event_type: "tool_call",
|
|
417
|
+
span_id: spanId,
|
|
418
|
+
attributes: {
|
|
419
|
+
tool_call: {
|
|
420
|
+
tool_name: options.toolName,
|
|
421
|
+
args: options.args || null,
|
|
422
|
+
result: options.result || null,
|
|
423
|
+
result_status: options.resultStatus,
|
|
424
|
+
latency_ms: options.latencyMs,
|
|
425
|
+
error_message: options.errorMessage || null
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return spanId;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Track a retrieval operation
|
|
433
|
+
*/
|
|
434
|
+
trackRetrieval(options) {
|
|
435
|
+
const spanId = crypto.randomUUID();
|
|
436
|
+
this.addEvent({
|
|
437
|
+
event_type: "retrieval",
|
|
438
|
+
span_id: spanId,
|
|
439
|
+
attributes: {
|
|
440
|
+
retrieval: {
|
|
441
|
+
retrieval_context_ids: options.contextIds || null,
|
|
442
|
+
retrieval_context_hashes: options.contextHashes || null,
|
|
443
|
+
k: options.k || null,
|
|
444
|
+
top_k: options.k || null,
|
|
445
|
+
similarity_scores: options.similarityScores || null,
|
|
446
|
+
latency_ms: options.latencyMs
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
return spanId;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Track an error with stack trace support
|
|
454
|
+
*/
|
|
455
|
+
trackError(options) {
|
|
456
|
+
const spanId = crypto.randomUUID();
|
|
457
|
+
let stackTrace = options.stackTrace;
|
|
458
|
+
if (!stackTrace && options.error instanceof Error && options.error.stack) {
|
|
459
|
+
stackTrace = options.error.stack;
|
|
460
|
+
}
|
|
461
|
+
this.addEvent({
|
|
462
|
+
event_type: "error",
|
|
463
|
+
span_id: spanId,
|
|
464
|
+
attributes: {
|
|
465
|
+
error: {
|
|
466
|
+
error_type: options.errorType,
|
|
467
|
+
error_message: options.errorMessage,
|
|
468
|
+
stack_trace: stackTrace || null,
|
|
469
|
+
context: options.context || null
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
return spanId;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Track user feedback
|
|
477
|
+
*/
|
|
478
|
+
trackFeedback(options) {
|
|
479
|
+
const spanId = options.spanId || crypto.randomUUID();
|
|
480
|
+
const parentSpanId = options.parentSpanId ?? (this.spanStack.length > 0 ? this.spanStack[this.spanStack.length - 1] : null);
|
|
481
|
+
let rating = options.rating;
|
|
482
|
+
if (rating !== void 0 && rating !== null) {
|
|
483
|
+
rating = Math.max(1, Math.min(5, rating));
|
|
484
|
+
}
|
|
485
|
+
this.addEvent({
|
|
486
|
+
event_type: "feedback",
|
|
487
|
+
span_id: spanId,
|
|
488
|
+
parent_span_id: parentSpanId ?? null,
|
|
489
|
+
conversation_id: options.conversationId ?? null,
|
|
490
|
+
session_id: options.sessionId ?? null,
|
|
491
|
+
user_id: options.userId ?? null,
|
|
492
|
+
agent_name: options.agentName ?? null,
|
|
493
|
+
version: options.version ?? null,
|
|
494
|
+
route: options.route ?? null,
|
|
495
|
+
attributes: {
|
|
496
|
+
feedback: {
|
|
497
|
+
type: options.type,
|
|
498
|
+
rating: rating ?? null,
|
|
499
|
+
comment: options.comment || null,
|
|
500
|
+
outcome: options.outcome || null
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
return spanId;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Track final output
|
|
508
|
+
*/
|
|
509
|
+
trackOutput(options) {
|
|
510
|
+
const spanId = crypto.randomUUID();
|
|
511
|
+
this.addEvent({
|
|
512
|
+
event_type: "output",
|
|
513
|
+
span_id: spanId,
|
|
514
|
+
attributes: {
|
|
515
|
+
output: {
|
|
516
|
+
final_output: options.finalOutput || null,
|
|
517
|
+
output_length: options.outputLength || null
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
return spanId;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Execute a function within a span context (for nested operations)
|
|
525
|
+
* This allows tool calls to be nested under LLM calls, etc.
|
|
526
|
+
*/
|
|
527
|
+
withSpan(spanId, fn) {
|
|
528
|
+
this.spanStack.push(spanId);
|
|
529
|
+
try {
|
|
530
|
+
return fn();
|
|
531
|
+
} finally {
|
|
532
|
+
this.spanStack.pop();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Execute an async function within a span context (for nested operations)
|
|
537
|
+
*/
|
|
538
|
+
async withSpanAsync(spanId, fn) {
|
|
539
|
+
this.spanStack.push(spanId);
|
|
540
|
+
try {
|
|
541
|
+
return await fn();
|
|
542
|
+
} finally {
|
|
543
|
+
this.spanStack.pop();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* End trace and send events (manual trace management)
|
|
548
|
+
*/
|
|
549
|
+
async endTrace(options = {}) {
|
|
550
|
+
if (!this.currentTraceId || !this.rootSpanId) {
|
|
551
|
+
throw new Error("[Observa] No active trace. Call startTrace() first.");
|
|
552
|
+
}
|
|
553
|
+
const traceEvents = this.eventBuffer.filter(
|
|
554
|
+
(e) => e.trace_id === this.currentTraceId
|
|
555
|
+
);
|
|
556
|
+
const llmEvents = traceEvents.filter((e) => e.event_type === "llm_call");
|
|
557
|
+
const totalTokens = llmEvents.reduce(
|
|
558
|
+
(sum, e) => sum + (e.attributes.llm_call?.total_tokens || 0),
|
|
559
|
+
0
|
|
560
|
+
);
|
|
561
|
+
const totalCost = llmEvents.reduce(
|
|
562
|
+
(sum, e) => sum + (e.attributes.llm_call?.cost || 0),
|
|
563
|
+
0
|
|
564
|
+
);
|
|
565
|
+
const totalLatency = this.traceStartTime !== null ? Date.now() - this.traceStartTime : null;
|
|
566
|
+
this.addEvent({
|
|
567
|
+
event_type: "trace_end",
|
|
568
|
+
span_id: this.rootSpanId,
|
|
569
|
+
parent_span_id: null,
|
|
570
|
+
attributes: {
|
|
571
|
+
trace_end: {
|
|
572
|
+
total_latency_ms: totalLatency,
|
|
573
|
+
total_tokens: totalTokens || null,
|
|
574
|
+
total_cost: totalCost || null,
|
|
575
|
+
outcome: options.outcome || "success"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
const traceEventsToSend = this.eventBuffer.filter(
|
|
580
|
+
(e) => e.trace_id === this.currentTraceId
|
|
581
|
+
);
|
|
582
|
+
if (traceEventsToSend.length > 0) {
|
|
583
|
+
await this._sendEventsWithRetry(traceEventsToSend);
|
|
584
|
+
this.eventBuffer = this.eventBuffer.filter(
|
|
585
|
+
(e) => e.trace_id !== this.currentTraceId
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const traceId = this.currentTraceId;
|
|
589
|
+
this.currentTraceId = null;
|
|
590
|
+
this.rootSpanId = null;
|
|
591
|
+
this.spanStack = [];
|
|
592
|
+
this.traceStartTime = null;
|
|
593
|
+
return traceId;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Convert legacy TraceData to canonical events
|
|
597
|
+
*/
|
|
598
|
+
traceDataToCanonicalEvents(trace) {
|
|
599
|
+
const events = [];
|
|
600
|
+
const baseEvent = {
|
|
601
|
+
tenant_id: trace.tenantId,
|
|
602
|
+
project_id: trace.projectId,
|
|
603
|
+
environment: trace.environment,
|
|
604
|
+
trace_id: trace.traceId,
|
|
605
|
+
conversation_id: trace.conversationId || null,
|
|
606
|
+
session_id: trace.sessionId || null,
|
|
607
|
+
user_id: trace.userId || null
|
|
608
|
+
};
|
|
609
|
+
events.push({
|
|
610
|
+
...baseEvent,
|
|
611
|
+
span_id: trace.spanId,
|
|
612
|
+
parent_span_id: trace.parentSpanId || null,
|
|
613
|
+
timestamp: trace.timestamp,
|
|
614
|
+
event_type: "trace_start",
|
|
615
|
+
attributes: {
|
|
616
|
+
trace_start: {
|
|
617
|
+
metadata: trace.metadata || null
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
if (trace.model) {
|
|
622
|
+
const llmSpanId = crypto.randomUUID();
|
|
623
|
+
events.push({
|
|
624
|
+
...baseEvent,
|
|
625
|
+
span_id: llmSpanId,
|
|
626
|
+
parent_span_id: trace.spanId,
|
|
627
|
+
timestamp: trace.timestamp,
|
|
628
|
+
event_type: "llm_call",
|
|
629
|
+
attributes: {
|
|
630
|
+
llm_call: {
|
|
631
|
+
model: trace.model,
|
|
632
|
+
input: trace.query || null,
|
|
633
|
+
output: trace.response || null,
|
|
634
|
+
input_tokens: trace.tokensPrompt || null,
|
|
635
|
+
output_tokens: trace.tokensCompletion || null,
|
|
636
|
+
total_tokens: trace.tokensTotal || null,
|
|
637
|
+
latency_ms: trace.latencyMs,
|
|
638
|
+
time_to_first_token_ms: trace.timeToFirstTokenMs || null,
|
|
639
|
+
streaming_duration_ms: trace.streamingDurationMs || null,
|
|
640
|
+
finish_reason: trace.finishReason || null,
|
|
641
|
+
response_id: trace.responseId || null,
|
|
642
|
+
system_fingerprint: trace.systemFingerprint || null,
|
|
643
|
+
cost: null
|
|
644
|
+
// Cost calculation handled by backend
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
events.push({
|
|
650
|
+
...baseEvent,
|
|
651
|
+
span_id: crypto.randomUUID(),
|
|
652
|
+
parent_span_id: trace.spanId,
|
|
653
|
+
timestamp: trace.timestamp,
|
|
654
|
+
event_type: "output",
|
|
655
|
+
attributes: {
|
|
656
|
+
output: {
|
|
657
|
+
final_output: trace.response || null,
|
|
658
|
+
output_length: trace.responseLength || null
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
events.push({
|
|
663
|
+
...baseEvent,
|
|
664
|
+
span_id: trace.spanId,
|
|
665
|
+
parent_span_id: trace.parentSpanId || null,
|
|
666
|
+
timestamp: trace.timestamp,
|
|
667
|
+
event_type: "trace_end",
|
|
668
|
+
attributes: {
|
|
669
|
+
trace_end: {
|
|
670
|
+
total_latency_ms: trace.latencyMs,
|
|
671
|
+
total_tokens: trace.tokensTotal || null,
|
|
672
|
+
total_cost: null,
|
|
673
|
+
// Cost calculation handled by backend
|
|
674
|
+
outcome: trace.status && trace.status >= 200 && trace.status < 300 ? "success" : "error"
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
return events;
|
|
293
679
|
}
|
|
294
|
-
|
|
680
|
+
/**
|
|
681
|
+
* Internal flush implementation
|
|
682
|
+
*/
|
|
683
|
+
async _doFlush() {
|
|
684
|
+
const eventsToFlush = [...this.eventBuffer];
|
|
685
|
+
this.eventBuffer = [];
|
|
686
|
+
if (eventsToFlush.length === 0) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const eventsByTrace = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const event of eventsToFlush) {
|
|
691
|
+
if (!eventsByTrace.has(event.trace_id)) {
|
|
692
|
+
eventsByTrace.set(event.trace_id, []);
|
|
693
|
+
}
|
|
694
|
+
eventsByTrace.get(event.trace_id).push(event);
|
|
695
|
+
}
|
|
696
|
+
for (const [traceId, events] of eventsByTrace.entries()) {
|
|
697
|
+
await this._sendEventsWithRetry(events);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Send canonical events with exponential backoff retry
|
|
702
|
+
*/
|
|
703
|
+
async _sendEventsWithRetry(events, maxRetries = 3) {
|
|
704
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
705
|
+
try {
|
|
706
|
+
await this.sendEvents(events);
|
|
707
|
+
return;
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (attempt === maxRetries) {
|
|
710
|
+
console.error(
|
|
711
|
+
`[Observa] Failed to send events after ${maxRetries + 1} attempts, re-buffering:`,
|
|
712
|
+
error
|
|
713
|
+
);
|
|
714
|
+
this.eventBuffer.push(...events);
|
|
715
|
+
if (this.eventBuffer.length > this.maxBufferSize * 2) {
|
|
716
|
+
const toDrop = this.eventBuffer.length - this.maxBufferSize;
|
|
717
|
+
this.eventBuffer.splice(0, toDrop);
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const delayMs = 100 * Math.pow(2, attempt);
|
|
722
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Cleanup (call when shutting down)
|
|
728
|
+
*/
|
|
729
|
+
async end() {
|
|
730
|
+
if (this.flushIntervalId) {
|
|
731
|
+
clearInterval(this.flushIntervalId);
|
|
732
|
+
this.flushIntervalId = null;
|
|
733
|
+
}
|
|
734
|
+
await this.flush();
|
|
735
|
+
}
|
|
736
|
+
async track(event, action, options) {
|
|
295
737
|
if (this.sampleRate < 1 && Math.random() > this.sampleRate) {
|
|
296
738
|
return action();
|
|
297
739
|
}
|
|
298
740
|
const startTime = Date.now();
|
|
299
741
|
const traceId = crypto.randomUUID();
|
|
300
742
|
const spanId = traceId;
|
|
301
|
-
const
|
|
743
|
+
const runWithContext = contextModule?.runInTraceContextAsync || ((ctx, fn) => fn());
|
|
744
|
+
const context = contextModule?.createSpanContext?.(
|
|
745
|
+
traceId,
|
|
746
|
+
spanId,
|
|
747
|
+
null
|
|
748
|
+
) || { traceId, spanId, parentSpanId: null };
|
|
749
|
+
const originalResponse = await runWithContext(context, action);
|
|
302
750
|
if (!originalResponse.body) return originalResponse;
|
|
303
751
|
const responseHeaders = {};
|
|
304
752
|
originalResponse.headers.forEach((value, key) => {
|
|
305
753
|
responseHeaders[key] = value;
|
|
306
754
|
});
|
|
307
755
|
const [stream1, stream2] = originalResponse.body.tee();
|
|
308
|
-
|
|
309
|
-
this.captureStream({
|
|
756
|
+
const capturePromise = this.captureStream({
|
|
310
757
|
stream: stream2,
|
|
311
758
|
event,
|
|
312
759
|
traceId,
|
|
313
760
|
spanId,
|
|
314
|
-
parentSpanId:
|
|
761
|
+
parentSpanId: context.parentSpanId,
|
|
315
762
|
startTime,
|
|
316
763
|
status: originalResponse.status,
|
|
317
764
|
statusText: originalResponse.statusText,
|
|
318
765
|
headers: responseHeaders
|
|
319
|
-
}).catch((err) => {
|
|
320
|
-
console.error("[Observa] captureStream promise rejected:", err);
|
|
321
766
|
});
|
|
767
|
+
if (options?.trackBlocking) {
|
|
768
|
+
await capturePromise;
|
|
769
|
+
} else {
|
|
770
|
+
capturePromise.catch((err) => {
|
|
771
|
+
console.error("[Observa] captureStream promise rejected:", err);
|
|
772
|
+
});
|
|
773
|
+
}
|
|
322
774
|
return new Response(stream1, {
|
|
323
775
|
headers: originalResponse.headers,
|
|
324
776
|
status: originalResponse.status,
|
|
@@ -420,17 +872,40 @@ var Observa = class {
|
|
|
420
872
|
systemFingerprint: extracted.systemFingerprint ?? null,
|
|
421
873
|
...headers !== void 0 && { headers },
|
|
422
874
|
// Conversation tracking fields
|
|
423
|
-
...event.conversationId !== void 0 && {
|
|
875
|
+
...event.conversationId !== void 0 && {
|
|
876
|
+
conversationId: event.conversationId
|
|
877
|
+
},
|
|
424
878
|
...event.sessionId !== void 0 && { sessionId: event.sessionId },
|
|
425
879
|
...event.userId !== void 0 && { userId: event.userId },
|
|
426
|
-
...event.messageIndex !== void 0 && {
|
|
427
|
-
|
|
880
|
+
...event.messageIndex !== void 0 && {
|
|
881
|
+
messageIndex: event.messageIndex
|
|
882
|
+
},
|
|
883
|
+
...event.parentMessageId !== void 0 && {
|
|
884
|
+
parentMessageId: event.parentMessageId
|
|
885
|
+
}
|
|
428
886
|
};
|
|
429
887
|
console.log(
|
|
430
|
-
`[Observa] Trace data prepared,
|
|
888
|
+
`[Observa] Trace data prepared, converting to canonical events for ${traceId}, response length: ${fullResponse.length}`
|
|
431
889
|
);
|
|
432
|
-
|
|
433
|
-
|
|
890
|
+
const canonicalEvents = this.traceDataToCanonicalEvents(traceData);
|
|
891
|
+
this.eventBuffer.push(...canonicalEvents);
|
|
892
|
+
if (this.eventBuffer.length >= this.maxBufferSize) {
|
|
893
|
+
this.flush().catch((err) => {
|
|
894
|
+
console.error("[Observa] Auto-flush failed:", err);
|
|
895
|
+
});
|
|
896
|
+
} else {
|
|
897
|
+
this._sendEventsWithRetry(canonicalEvents).catch((err) => {
|
|
898
|
+
console.error("[Observa] Failed to send events:", err);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
if (!this.isProduction) {
|
|
902
|
+
const traceUrl = `${this.apiUrl.replace(
|
|
903
|
+
/\/api.*$/,
|
|
904
|
+
""
|
|
905
|
+
)}/traces/${traceId}`;
|
|
906
|
+
console.log(`[Observa] \u{1F54A}\uFE0F Trace captured: ${traceUrl}`);
|
|
907
|
+
}
|
|
908
|
+
console.log(`[Observa] Trace queued for sending: ${traceId}`);
|
|
434
909
|
} catch (err) {
|
|
435
910
|
console.error("[Observa] Error capturing stream:", err);
|
|
436
911
|
if (err instanceof Error) {
|
|
@@ -442,15 +917,57 @@ var Observa = class {
|
|
|
442
917
|
}
|
|
443
918
|
}
|
|
444
919
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
920
|
+
/**
|
|
921
|
+
* Send canonical events to Observa backend
|
|
922
|
+
* (internal method, use _sendEventsWithRetry for retry logic)
|
|
923
|
+
*/
|
|
924
|
+
async sendEvents(events) {
|
|
925
|
+
if (events.length === 0) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const traceId = events[0]?.trace_id;
|
|
929
|
+
if (!this.isProduction && traceId) {
|
|
930
|
+
const llmEvent = events.find((e) => e.event_type === "llm_call");
|
|
931
|
+
const outputEvent = events.find((e) => e.event_type === "output");
|
|
932
|
+
const traceEndEvent = events.find((e) => e.event_type === "trace_end");
|
|
933
|
+
if (llmEvent && outputEvent) {
|
|
934
|
+
const llmAttrs = llmEvent.attributes.llm_call;
|
|
935
|
+
const outputAttrs = outputEvent.attributes.output;
|
|
936
|
+
const traceData = {
|
|
937
|
+
traceId: llmEvent.trace_id,
|
|
938
|
+
spanId: llmEvent.parent_span_id || llmEvent.span_id,
|
|
939
|
+
parentSpanId: llmEvent.parent_span_id || null,
|
|
940
|
+
timestamp: llmEvent.timestamp,
|
|
941
|
+
tenantId: llmEvent.tenant_id,
|
|
942
|
+
projectId: llmEvent.project_id,
|
|
943
|
+
environment: llmEvent.environment,
|
|
944
|
+
query: llmAttrs?.input || "",
|
|
945
|
+
response: outputAttrs?.final_output || "",
|
|
946
|
+
responseLength: outputAttrs?.output_length || 0,
|
|
947
|
+
...llmAttrs?.model && { model: llmAttrs.model },
|
|
948
|
+
tokensPrompt: llmAttrs?.input_tokens ?? null,
|
|
949
|
+
tokensCompletion: llmAttrs?.output_tokens ?? null,
|
|
950
|
+
tokensTotal: llmAttrs?.total_tokens ?? null,
|
|
951
|
+
latencyMs: llmAttrs?.latency_ms || 0,
|
|
952
|
+
timeToFirstTokenMs: llmAttrs?.time_to_first_token_ms ?? null,
|
|
953
|
+
streamingDurationMs: llmAttrs?.streaming_duration_ms ?? null,
|
|
954
|
+
finishReason: llmAttrs?.finish_reason ?? null,
|
|
955
|
+
responseId: llmAttrs?.response_id ?? null,
|
|
956
|
+
systemFingerprint: llmAttrs?.system_fingerprint ?? null,
|
|
957
|
+
...llmEvent.conversation_id && {
|
|
958
|
+
conversationId: llmEvent.conversation_id
|
|
959
|
+
},
|
|
960
|
+
...llmEvent.session_id && { sessionId: llmEvent.session_id },
|
|
961
|
+
...llmEvent.user_id && { userId: llmEvent.user_id }
|
|
962
|
+
};
|
|
963
|
+
formatBeautifulLog(traceData);
|
|
964
|
+
}
|
|
448
965
|
}
|
|
449
966
|
try {
|
|
450
967
|
const baseUrl = this.apiUrl.replace(/\/+$/, "");
|
|
451
|
-
const url = `${baseUrl}/api/v1/
|
|
968
|
+
const url = `${baseUrl}/api/v1/events/ingest`;
|
|
452
969
|
console.log(
|
|
453
|
-
`[Observa] Sending
|
|
970
|
+
`[Observa] Sending ${events.length} canonical events - URL: ${url}, TraceID: ${traceId}, Tenant: ${events[0]?.tenant_id}, Project: ${events[0]?.project_id}, APIKey: ${this.apiKey ? `Yes(${this.apiKey.length} chars)` : "No"}`
|
|
454
971
|
);
|
|
455
972
|
const controller = new AbortController();
|
|
456
973
|
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -461,7 +978,7 @@ var Observa = class {
|
|
|
461
978
|
Authorization: `Bearer ${this.apiKey}`,
|
|
462
979
|
"Content-Type": "application/json"
|
|
463
980
|
},
|
|
464
|
-
body: JSON.stringify(
|
|
981
|
+
body: JSON.stringify(events),
|
|
465
982
|
signal: controller.signal
|
|
466
983
|
});
|
|
467
984
|
clearTimeout(timeoutId);
|
|
@@ -480,9 +997,13 @@ var Observa = class {
|
|
|
480
997
|
`[Observa] Backend API error: ${response.status} ${response.statusText}`,
|
|
481
998
|
errorJson.error || errorText
|
|
482
999
|
);
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Observa API error: ${response.status} ${errorJson.error?.message || errorText}`
|
|
1002
|
+
);
|
|
483
1003
|
} else {
|
|
1004
|
+
const result = await response.json().catch(() => ({}));
|
|
484
1005
|
console.log(
|
|
485
|
-
`\u2705 [Observa]
|
|
1006
|
+
`\u2705 [Observa] Events sent successfully - Trace ID: ${traceId}, Event count: ${result.event_count || events.length}`
|
|
486
1007
|
);
|
|
487
1008
|
}
|
|
488
1009
|
} catch (fetchError) {
|
|
@@ -493,7 +1014,7 @@ var Observa = class {
|
|
|
493
1014
|
throw fetchError;
|
|
494
1015
|
}
|
|
495
1016
|
} catch (error) {
|
|
496
|
-
console.error("[Observa] Failed to send
|
|
1017
|
+
console.error("[Observa] Failed to send events:", error);
|
|
497
1018
|
if (error instanceof Error) {
|
|
498
1019
|
console.error("[Observa] Error message:", error.message);
|
|
499
1020
|
console.error("[Observa] Error name:", error.name);
|
|
@@ -506,6 +1027,7 @@ var Observa = class {
|
|
|
506
1027
|
console.error("[Observa] Error stack:", error.stack);
|
|
507
1028
|
}
|
|
508
1029
|
}
|
|
1030
|
+
throw error;
|
|
509
1031
|
}
|
|
510
1032
|
}
|
|
511
1033
|
};
|