elasticdash-test 0.1.17 → 0.1.18-alpha
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/capture/event.d.ts +5 -1
- package/dist/capture/event.d.ts.map +1 -1
- package/dist/cli.js +100 -0
- package/dist/cli.js.map +1 -1
- package/dist/evaluators/llm-judge.js +17 -14
- package/dist/evaluators/types.d.ts +1 -0
- package/dist/execution/tool-runner.d.ts +26 -0
- package/dist/execution/tool-runner.d.ts.map +1 -0
- package/dist/execution/tool-runner.js +270 -0
- package/dist/execution/tool-runner.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -0
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +4310 -2672
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
- package/dist/interceptors/ai-interceptor.js +97 -4
- package/dist/interceptors/ai-interceptor.js.map +1 -1
- package/dist/interceptors/db-auto.d.ts.map +1 -1
- package/dist/interceptors/db-auto.js +116 -24
- package/dist/interceptors/db-auto.js.map +1 -1
- package/dist/interceptors/db.d.ts +5 -0
- package/dist/interceptors/db.d.ts.map +1 -1
- package/dist/interceptors/db.js +93 -15
- package/dist/interceptors/db.js.map +1 -1
- package/dist/interceptors/http.d.ts.map +1 -1
- package/dist/interceptors/http.js +125 -93
- package/dist/interceptors/http.js.map +1 -1
- package/dist/interceptors/telemetry-push.d.ts +15 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -1
- package/dist/interceptors/telemetry-push.js +96 -13
- package/dist/interceptors/telemetry-push.js.map +1 -1
- package/dist/interceptors/tool.d.ts.map +1 -1
- package/dist/interceptors/tool.js +42 -5
- package/dist/interceptors/tool.js.map +1 -1
- package/dist/interceptors/workflow-ai.d.ts.map +1 -1
- package/dist/interceptors/workflow-ai.js +46 -2
- package/dist/interceptors/workflow-ai.js.map +1 -1
- package/dist/observability.d.ts +69 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +242 -0
- package/dist/observability.js.map +1 -0
- package/dist/portal-executor.d.ts +30 -0
- package/dist/portal-executor.d.ts.map +1 -0
- package/dist/portal-executor.js +304 -0
- package/dist/portal-executor.js.map +1 -0
- package/dist/portal-server.d.ts +3 -0
- package/dist/portal-server.d.ts.map +1 -0
- package/dist/portal-server.js +265 -0
- package/dist/portal-server.js.map +1 -0
- package/dist/telemetry-batcher.d.ts +43 -0
- package/dist/telemetry-batcher.d.ts.map +1 -0
- package/dist/telemetry-batcher.js +111 -0
- package/dist/telemetry-batcher.js.map +1 -0
- package/dist/trigger-executor.d.ts +12 -0
- package/dist/trigger-executor.d.ts.map +1 -0
- package/dist/trigger-executor.js +83 -0
- package/dist/trigger-executor.js.map +1 -0
- package/dist/types/portal.d.ts +64 -0
- package/dist/types/portal.d.ts.map +1 -0
- package/dist/types/portal.js +2 -0
- package/dist/types/portal.js.map +1 -0
- package/dist/utils/debug.d.ts +3 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +8 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/redact.d.ts +7 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +26 -0
- package/dist/utils/redact.js.map +1 -0
- package/package.json +9 -1
- package/src/capture/event.ts +5 -1
- package/src/cli.ts +109 -0
- package/src/execution/tool-runner.ts +304 -0
- package/src/http.ts +2 -0
- package/src/index.ts +14 -0
- package/src/interceptors/ai-interceptor.ts +110 -4
- package/src/interceptors/db-auto.ts +121 -25
- package/src/interceptors/db.ts +92 -17
- package/src/interceptors/http.ts +145 -107
- package/src/interceptors/telemetry-push.ts +113 -13
- package/src/interceptors/tool.ts +42 -5
- package/src/interceptors/workflow-ai.ts +49 -2
- package/src/observability.ts +281 -0
- package/src/portal-executor.ts +335 -0
- package/src/portal-server.ts +290 -0
- package/src/telemetry-batcher.ts +143 -0
- package/src/trigger-executor.ts +121 -0
- package/src/types/portal.ts +67 -0
- package/src/utils/debug.ts +8 -0
- package/src/utils/redact.ts +25 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getCurrentTrace } from '../trace-adapter/context.js'
|
|
2
2
|
import { getCaptureContext } from '../capture/recorder.js'
|
|
3
3
|
import { rawDateNow } from './side-effects.js'
|
|
4
|
+
import { getObservabilityContext, getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext } from './telemetry-push.js'
|
|
5
|
+
import type { WorkflowEvent } from '../capture/event.js'
|
|
4
6
|
|
|
5
7
|
type UsageInfo = { inputTokens?: number; outputTokens?: number; totalTokens?: number }
|
|
6
8
|
|
|
@@ -385,9 +387,12 @@ export function installAIInterceptor(): void {
|
|
|
385
387
|
|
|
386
388
|
const provider = detectProvider(url)
|
|
387
389
|
const traceAtCall = getCurrentTrace()
|
|
390
|
+
const obsCtx = getObservabilityContext()
|
|
388
391
|
|
|
389
|
-
|
|
390
|
-
|
|
392
|
+
const httpCtx = getHttpRunContext()
|
|
393
|
+
|
|
394
|
+
// No match or no active context: pass through unchanged
|
|
395
|
+
if (!provider || (!traceAtCall && !obsCtx && !httpCtx)) {
|
|
391
396
|
return originalFetch!(input, init)
|
|
392
397
|
}
|
|
393
398
|
|
|
@@ -414,7 +419,106 @@ export function installAIInterceptor(): void {
|
|
|
414
419
|
|
|
415
420
|
const ctx = getCaptureContext()
|
|
416
421
|
|
|
417
|
-
|
|
422
|
+
// Observability-only mode: no trace handle, no capture context — record via pushTelemetryEvent
|
|
423
|
+
if (!traceAtCall && !ctx && obsCtx) {
|
|
424
|
+
const id = obsCtx.nextId()
|
|
425
|
+
const start = rawDateNow()
|
|
426
|
+
const eventInput = { url, provider, model, prompt, messages }
|
|
427
|
+
|
|
428
|
+
const response = await originalFetch!(input, init)
|
|
429
|
+
|
|
430
|
+
if (isStreaming && response.body) {
|
|
431
|
+
const [streamForCaller, streamForRecorder] = response.body.tee()
|
|
432
|
+
bufferSSEStream(provider, streamForRecorder).then((completion) => {
|
|
433
|
+
const durationMs = rawDateNow() - start
|
|
434
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs })
|
|
435
|
+
}).catch(() => {
|
|
436
|
+
const durationMs = rawDateNow() - start
|
|
437
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
|
|
438
|
+
})
|
|
439
|
+
return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const cloned = response.clone()
|
|
444
|
+
const responseBody = await cloned.json() as Record<string, unknown>
|
|
445
|
+
const completion = extractCompletion(provider, responseBody)
|
|
446
|
+
const usage = extractUsage(provider, responseBody)
|
|
447
|
+
const durationMs = rawDateNow() - start
|
|
448
|
+
const event: WorkflowEvent = {
|
|
449
|
+
id, type: 'ai', name: model, input: eventInput, output: { completion },
|
|
450
|
+
timestamp: start, durationMs,
|
|
451
|
+
...(usage ? { usage } : {}),
|
|
452
|
+
}
|
|
453
|
+
pushTelemetryEvent(event)
|
|
454
|
+
} catch {
|
|
455
|
+
const durationMs = rawDateNow() - start
|
|
456
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs })
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return response
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// HTTP mode (no capture context): replay frozen AI events or execute live + push telemetry
|
|
463
|
+
if (!ctx && httpCtx) {
|
|
464
|
+
const id = httpCtx.nextId()
|
|
465
|
+
const eventInput = { url, provider, model, prompt, messages }
|
|
466
|
+
|
|
467
|
+
// Replay frozen step
|
|
468
|
+
const frozen = getHttpFrozenEvent(id)
|
|
469
|
+
if (frozen && frozen.type === 'ai') {
|
|
470
|
+
pushTelemetryEvent(frozen)
|
|
471
|
+
const frozenOutput = frozen.output as Record<string, unknown> | null
|
|
472
|
+
const completion = frozenOutput ? extractCompletion(provider, frozenOutput) : '(replayed)'
|
|
473
|
+
|
|
474
|
+
if (isStreaming) {
|
|
475
|
+
return new Response(synthesizeSSEStream(provider, completion), {
|
|
476
|
+
status: 200,
|
|
477
|
+
headers: { 'Content-Type': provider === 'gemini' ? 'application/json' : 'text/event-stream' },
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const body = frozenOutput?.streamed === true
|
|
482
|
+
? synthesizeCompletionJSON(provider, completion)
|
|
483
|
+
: (frozenOutput ?? synthesizeCompletionJSON(provider, completion))
|
|
484
|
+
return new Response(JSON.stringify(body), {
|
|
485
|
+
status: 200,
|
|
486
|
+
headers: { 'Content-Type': 'application/json' },
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Not frozen → execute live, push telemetry
|
|
491
|
+
const start = rawDateNow()
|
|
492
|
+
const response = await originalFetch!(input, init)
|
|
493
|
+
|
|
494
|
+
if (isStreaming && response.body) {
|
|
495
|
+
const [streamForCaller, streamForRecorder] = response.body.tee()
|
|
496
|
+
bufferSSEStream(provider, streamForRecorder).then((completion) => {
|
|
497
|
+
const durationMs = rawDateNow() - start
|
|
498
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs })
|
|
499
|
+
}).catch(() => {
|
|
500
|
+
const durationMs = rawDateNow() - start
|
|
501
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
|
|
502
|
+
})
|
|
503
|
+
return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers })
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const cloned = response.clone()
|
|
508
|
+
const responseBody = await cloned.json() as Record<string, unknown>
|
|
509
|
+
const completion = extractCompletion(provider, responseBody)
|
|
510
|
+
const usage = extractUsage(provider, responseBody)
|
|
511
|
+
const durationMs = rawDateNow() - start
|
|
512
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { completion }, timestamp: start, durationMs, ...(usage ? { usage } : {}) })
|
|
513
|
+
} catch {
|
|
514
|
+
const durationMs = rawDateNow() - start
|
|
515
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs })
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return response
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (ctx && traceAtCall) {
|
|
418
522
|
const { recorder, replay } = ctx
|
|
419
523
|
const id = recorder.nextId()
|
|
420
524
|
const start = rawDateNow()
|
|
@@ -505,7 +609,9 @@ export function installAIInterceptor(): void {
|
|
|
505
609
|
return response
|
|
506
610
|
}
|
|
507
611
|
|
|
508
|
-
// No capture context — original behaviour (outside of a workflow run)
|
|
612
|
+
// No capture context — original behaviour (trace handle only, outside of a workflow run)
|
|
613
|
+
if (!traceAtCall) return originalFetch!(input, init)
|
|
614
|
+
|
|
509
615
|
const response = await originalFetch!(input, init)
|
|
510
616
|
|
|
511
617
|
if (isStreaming && response.body) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getCaptureContext } from '../capture/recorder.js'
|
|
2
|
+
import { getCurrentTrace } from '../trace-adapter/context.js'
|
|
3
|
+
import { rawDateNow } from './side-effects.js'
|
|
4
|
+
import { getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
|
|
2
5
|
|
|
3
6
|
type AnyFn = (...args: unknown[]) => unknown
|
|
4
7
|
|
|
@@ -10,6 +13,14 @@ interface MethodPatch {
|
|
|
10
13
|
|
|
11
14
|
const appliedPatches: MethodPatch[] = []
|
|
12
15
|
|
|
16
|
+
function toTraceArgs(input: unknown): Record<string, unknown> | undefined {
|
|
17
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
18
|
+
return input as Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
if (input === undefined) return undefined
|
|
21
|
+
return { value: input }
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function wrapProtoMethod(proto: object, method: string, eventName: string): void {
|
|
14
25
|
const p = proto as Record<string, unknown>
|
|
15
26
|
if (typeof p[method] !== 'function') return
|
|
@@ -24,58 +35,143 @@ function wrapProtoMethod(proto: object, method: string, eventName: string): void
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
const ctx = getCaptureContext()
|
|
27
|
-
|
|
38
|
+
const httpCtx = getHttpRunContext()
|
|
39
|
+
const obsCtx = getObservabilityContext()
|
|
40
|
+
const input = args.length === 1 ? args[0] : args
|
|
41
|
+
|
|
42
|
+
if (!ctx && !httpCtx && !obsCtx) return original.apply(this, args)
|
|
43
|
+
|
|
44
|
+
// Observability-only mode: record and push, no mocks/replay
|
|
45
|
+
if (!ctx && !httpCtx && obsCtx) {
|
|
46
|
+
const id = obsCtx.nextId()
|
|
47
|
+
const start = rawDateNow()
|
|
48
|
+
|
|
49
|
+
let result: unknown
|
|
50
|
+
try {
|
|
51
|
+
result = original.apply(this, args)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
54
|
+
throw err
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
|
58
|
+
return (result as Promise<unknown>)
|
|
59
|
+
.then((output: unknown) => {
|
|
60
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output, timestamp: start, durationMs: rawDateNow() - start })
|
|
61
|
+
return output
|
|
62
|
+
})
|
|
63
|
+
.catch((err: unknown) => {
|
|
64
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
65
|
+
throw err
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: result, timestamp: start, durationMs: rawDateNow() - start })
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
28
72
|
|
|
29
|
-
|
|
73
|
+
// HTTP mode (no capture context): replay frozen events or execute live
|
|
74
|
+
if (!ctx && httpCtx) {
|
|
75
|
+
const id = httpCtx.nextId()
|
|
76
|
+
|
|
77
|
+
// Replay frozen step
|
|
78
|
+
const frozen = getHttpFrozenEvent(id)
|
|
79
|
+
if (frozen && frozen.type === 'db') {
|
|
80
|
+
pushTelemetryEvent(frozen)
|
|
81
|
+
return Promise.resolve(frozen.output)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Not frozen → execute live, push telemetry
|
|
85
|
+
const start = rawDateNow()
|
|
86
|
+
|
|
87
|
+
let result: unknown
|
|
88
|
+
try {
|
|
89
|
+
result = original.apply(this, args)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
92
|
+
throw err
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
|
96
|
+
return (result as Promise<unknown>)
|
|
97
|
+
.then((output: unknown) => {
|
|
98
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output, timestamp: start, durationMs: rawDateNow() - start })
|
|
99
|
+
return output
|
|
100
|
+
})
|
|
101
|
+
.catch((err: unknown) => {
|
|
102
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
103
|
+
throw err
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: result, timestamp: start, durationMs: rawDateNow() - start })
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Capture mode (enhanced with telemetry + TraceHandle)
|
|
112
|
+
const trace = getCurrentTrace()
|
|
113
|
+
const { recorder, replay } = ctx!
|
|
30
114
|
const id = recorder.nextId()
|
|
31
115
|
|
|
32
116
|
if (replay.shouldReplay(id)) {
|
|
33
117
|
const historicalEvent = replay.getRecordedEvent(id)
|
|
34
118
|
if (historicalEvent) recorder.record(historicalEvent)
|
|
35
|
-
|
|
119
|
+
if (httpCtx) pushTelemetryEvent(historicalEvent ?? { id, type: 'db', name: eventName, input, output: null, timestamp: rawDateNow(), durationMs: 0 })
|
|
120
|
+
const replayed = replay.getRecordedResult(id)
|
|
121
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
122
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: replayed, workflowEventId: id })
|
|
123
|
+
}
|
|
124
|
+
return Promise.resolve(replayed)
|
|
36
125
|
}
|
|
37
126
|
|
|
38
|
-
const start =
|
|
39
|
-
const input = args.length === 1 ? args[0] : args
|
|
127
|
+
const start = rawDateNow()
|
|
40
128
|
|
|
41
129
|
let result: unknown
|
|
42
130
|
try {
|
|
43
131
|
result = original.apply(this, args)
|
|
44
132
|
} catch (err) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
133
|
+
const durationMs = rawDateNow() - start
|
|
134
|
+
const event = { id, type: 'db' as const, name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs }
|
|
135
|
+
recorder.record(event)
|
|
136
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
137
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
138
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: { error: String(err) }, workflowEventId: id, durationMs })
|
|
139
|
+
}
|
|
50
140
|
throw err
|
|
51
141
|
}
|
|
52
142
|
|
|
53
143
|
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
|
54
144
|
return (result as Promise<unknown>)
|
|
55
145
|
.then((output: unknown) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
146
|
+
const durationMs = rawDateNow() - start
|
|
147
|
+
const event = { id, type: 'db' as const, name: eventName, input, output, timestamp: start, durationMs }
|
|
148
|
+
recorder.record(event)
|
|
149
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
150
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
151
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: output, workflowEventId: id, durationMs })
|
|
152
|
+
}
|
|
61
153
|
return output
|
|
62
154
|
})
|
|
63
155
|
.catch((err: unknown) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
156
|
+
const durationMs = rawDateNow() - start
|
|
157
|
+
const event = { id, type: 'db' as const, name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs }
|
|
158
|
+
recorder.record(event)
|
|
159
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
160
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
161
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: { error: String(err) }, workflowEventId: id, durationMs })
|
|
162
|
+
}
|
|
69
163
|
throw err
|
|
70
164
|
})
|
|
71
165
|
}
|
|
72
166
|
|
|
73
167
|
// Sync return (rare for DB calls)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
168
|
+
const durationMs = rawDateNow() - start
|
|
169
|
+
const event = { id, type: 'db' as const, name: eventName, input, output: result, timestamp: start, durationMs }
|
|
170
|
+
recorder.record(event)
|
|
171
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
172
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
173
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result, workflowEventId: id, durationMs })
|
|
174
|
+
}
|
|
79
175
|
return result
|
|
80
176
|
}
|
|
81
177
|
}
|
package/src/interceptors/db.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { getCaptureContext } from '../capture/recorder.js'
|
|
2
|
+
import { getCurrentTrace } from '../trace-adapter/context.js'
|
|
3
|
+
import { rawDateNow } from './side-effects.js'
|
|
4
|
+
import { getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
|
|
5
|
+
|
|
6
|
+
function toTraceArgs(input: unknown): Record<string, unknown> | undefined {
|
|
7
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
8
|
+
return input as Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
if (input === undefined) return undefined
|
|
11
|
+
return { value: input }
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
/**
|
|
4
15
|
* Wraps named methods on a DB client instance in-place so their calls are
|
|
5
16
|
* recorded as "db" events. Returns the same client object.
|
|
6
17
|
*
|
|
18
|
+
* Supports all three execution modes:
|
|
19
|
+
* - Capture mode: replay from TraceRecorder / ReplayController
|
|
20
|
+
* - HTTP mode: replay frozen events from dashboard, push telemetry
|
|
21
|
+
* - Observability mode: record and push telemetry
|
|
22
|
+
*
|
|
7
23
|
* @param client Any DB client (pg.Client, redis client, mongoose Model, etc.)
|
|
8
24
|
* @param methodNames Method names to wrap
|
|
9
25
|
* @param label Optional label prefix for event names (defaults to constructor name)
|
|
@@ -20,30 +36,89 @@ export function wrapDB<T extends object>(
|
|
|
20
36
|
if (typeof original !== 'function') continue
|
|
21
37
|
|
|
22
38
|
;(client as Record<string, unknown>)[method] = async (...args: unknown[]) => {
|
|
39
|
+
await tryAutoInitHttpContext()
|
|
23
40
|
const ctx = getCaptureContext()
|
|
24
|
-
|
|
41
|
+
const httpCtx = getHttpRunContext()
|
|
42
|
+
const obsCtx = getObservabilityContext()
|
|
43
|
+
const name = `${prefix}.${method}`
|
|
44
|
+
const input = args.length === 1 ? args[0] : args
|
|
45
|
+
|
|
46
|
+
if (!ctx && !httpCtx && !obsCtx) return (original as (...a: unknown[]) => unknown).apply(client, args)
|
|
47
|
+
|
|
48
|
+
// Observability-only mode: record and push, no mocks/replay
|
|
49
|
+
if (!ctx && !httpCtx && obsCtx) {
|
|
50
|
+
const id = obsCtx.nextId()
|
|
51
|
+
const start = rawDateNow()
|
|
52
|
+
try {
|
|
53
|
+
const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
|
|
54
|
+
pushTelemetryEvent({ id, type: 'db', name, input, output, timestamp: start, durationMs: rawDateNow() - start })
|
|
55
|
+
return output
|
|
56
|
+
} catch (e) {
|
|
57
|
+
pushTelemetryEvent({ id, type: 'db', name, input, output: { error: String(e) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
58
|
+
throw e
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// HTTP mode (no capture context): replay frozen events or execute live
|
|
63
|
+
if (!ctx && httpCtx) {
|
|
64
|
+
const id = httpCtx.nextId()
|
|
25
65
|
|
|
26
|
-
|
|
66
|
+
// Replay frozen step
|
|
67
|
+
const frozen = getHttpFrozenEvent(id)
|
|
68
|
+
if (frozen && frozen.type === 'db') {
|
|
69
|
+
pushTelemetryEvent(frozen)
|
|
70
|
+
return frozen.output
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Not frozen → execute live, push telemetry
|
|
74
|
+
const start = rawDateNow()
|
|
75
|
+
try {
|
|
76
|
+
const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
|
|
77
|
+
pushTelemetryEvent({ id, type: 'db', name, input, output, timestamp: start, durationMs: rawDateNow() - start })
|
|
78
|
+
return output
|
|
79
|
+
} catch (e) {
|
|
80
|
+
pushTelemetryEvent({ id, type: 'db', name, input, output: { error: String(e) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
81
|
+
throw e
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Capture mode
|
|
86
|
+
const trace = getCurrentTrace()
|
|
87
|
+
const { recorder, replay } = ctx!
|
|
27
88
|
const id = recorder.nextId()
|
|
28
|
-
const name = `${prefix}.${method}`
|
|
29
89
|
|
|
30
90
|
if (replay.shouldReplay(id)) {
|
|
31
|
-
|
|
91
|
+
const historical = replay.getRecordedEvent(id)
|
|
92
|
+
if (historical) recorder.record(historical)
|
|
93
|
+
if (httpCtx) pushTelemetryEvent(historical ?? { id, type: 'db', name, input, output: null, timestamp: rawDateNow(), durationMs: 0 })
|
|
94
|
+
const replayed = replay.getRecordedResult(id)
|
|
95
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
96
|
+
trace.recordToolCall({ name, args: toTraceArgs(input), result: replayed, workflowEventId: id })
|
|
97
|
+
}
|
|
98
|
+
return replayed
|
|
32
99
|
}
|
|
33
100
|
|
|
34
|
-
const start =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type: 'db',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
101
|
+
const start = rawDateNow()
|
|
102
|
+
try {
|
|
103
|
+
const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
|
|
104
|
+
const durationMs = rawDateNow() - start
|
|
105
|
+
const event = { id, type: 'db' as const, name, input, output, timestamp: start, durationMs }
|
|
106
|
+
recorder.record(event)
|
|
107
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
108
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
109
|
+
trace.recordToolCall({ name, args: toTraceArgs(input), result: output, workflowEventId: id, durationMs })
|
|
110
|
+
}
|
|
111
|
+
return output
|
|
112
|
+
} catch (e) {
|
|
113
|
+
const durationMs = rawDateNow() - start
|
|
114
|
+
const event = { id, type: 'db' as const, name, input, output: { error: String(e) }, timestamp: start, durationMs }
|
|
115
|
+
recorder.record(event)
|
|
116
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
117
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
118
|
+
trace.recordToolCall({ name, args: toTraceArgs(input), result: { error: String(e) }, workflowEventId: id, durationMs })
|
|
119
|
+
}
|
|
120
|
+
throw e
|
|
121
|
+
}
|
|
47
122
|
}
|
|
48
123
|
}
|
|
49
124
|
|