observa-sdk 0.0.5 → 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 +547 -19
- package/dist/index.d.cts +134 -2
- package/dist/index.d.ts +134 -2
- package/dist/index.js +547 -19
- package/package.json +5 -2
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,
|
|
@@ -418,13 +870,42 @@ var Observa = class {
|
|
|
418
870
|
finishReason: extracted.finishReason ?? null,
|
|
419
871
|
responseId: extracted.responseId ?? null,
|
|
420
872
|
systemFingerprint: extracted.systemFingerprint ?? null,
|
|
421
|
-
...headers !== void 0 && { headers }
|
|
873
|
+
...headers !== void 0 && { headers },
|
|
874
|
+
// Conversation tracking fields
|
|
875
|
+
...event.conversationId !== void 0 && {
|
|
876
|
+
conversationId: event.conversationId
|
|
877
|
+
},
|
|
878
|
+
...event.sessionId !== void 0 && { sessionId: event.sessionId },
|
|
879
|
+
...event.userId !== void 0 && { userId: event.userId },
|
|
880
|
+
...event.messageIndex !== void 0 && {
|
|
881
|
+
messageIndex: event.messageIndex
|
|
882
|
+
},
|
|
883
|
+
...event.parentMessageId !== void 0 && {
|
|
884
|
+
parentMessageId: event.parentMessageId
|
|
885
|
+
}
|
|
422
886
|
};
|
|
423
887
|
console.log(
|
|
424
|
-
`[Observa] Trace data prepared,
|
|
888
|
+
`[Observa] Trace data prepared, converting to canonical events for ${traceId}, response length: ${fullResponse.length}`
|
|
425
889
|
);
|
|
426
|
-
|
|
427
|
-
|
|
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}`);
|
|
428
909
|
} catch (err) {
|
|
429
910
|
console.error("[Observa] Error capturing stream:", err);
|
|
430
911
|
if (err instanceof Error) {
|
|
@@ -436,15 +917,57 @@ var Observa = class {
|
|
|
436
917
|
}
|
|
437
918
|
}
|
|
438
919
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}
|
|
442
965
|
}
|
|
443
966
|
try {
|
|
444
967
|
const baseUrl = this.apiUrl.replace(/\/+$/, "");
|
|
445
|
-
const url = `${baseUrl}/api/v1/
|
|
968
|
+
const url = `${baseUrl}/api/v1/events/ingest`;
|
|
446
969
|
console.log(
|
|
447
|
-
`[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"}`
|
|
448
971
|
);
|
|
449
972
|
const controller = new AbortController();
|
|
450
973
|
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -455,7 +978,7 @@ var Observa = class {
|
|
|
455
978
|
Authorization: `Bearer ${this.apiKey}`,
|
|
456
979
|
"Content-Type": "application/json"
|
|
457
980
|
},
|
|
458
|
-
body: JSON.stringify(
|
|
981
|
+
body: JSON.stringify(events),
|
|
459
982
|
signal: controller.signal
|
|
460
983
|
});
|
|
461
984
|
clearTimeout(timeoutId);
|
|
@@ -474,9 +997,13 @@ var Observa = class {
|
|
|
474
997
|
`[Observa] Backend API error: ${response.status} ${response.statusText}`,
|
|
475
998
|
errorJson.error || errorText
|
|
476
999
|
);
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Observa API error: ${response.status} ${errorJson.error?.message || errorText}`
|
|
1002
|
+
);
|
|
477
1003
|
} else {
|
|
1004
|
+
const result = await response.json().catch(() => ({}));
|
|
478
1005
|
console.log(
|
|
479
|
-
`\u2705 [Observa]
|
|
1006
|
+
`\u2705 [Observa] Events sent successfully - Trace ID: ${traceId}, Event count: ${result.event_count || events.length}`
|
|
480
1007
|
);
|
|
481
1008
|
}
|
|
482
1009
|
} catch (fetchError) {
|
|
@@ -487,7 +1014,7 @@ var Observa = class {
|
|
|
487
1014
|
throw fetchError;
|
|
488
1015
|
}
|
|
489
1016
|
} catch (error) {
|
|
490
|
-
console.error("[Observa] Failed to send
|
|
1017
|
+
console.error("[Observa] Failed to send events:", error);
|
|
491
1018
|
if (error instanceof Error) {
|
|
492
1019
|
console.error("[Observa] Error message:", error.message);
|
|
493
1020
|
console.error("[Observa] Error name:", error.name);
|
|
@@ -500,6 +1027,7 @@ var Observa = class {
|
|
|
500
1027
|
console.error("[Observa] Error stack:", error.stack);
|
|
501
1028
|
}
|
|
502
1029
|
}
|
|
1030
|
+
throw error;
|
|
503
1031
|
}
|
|
504
1032
|
}
|
|
505
1033
|
};
|