elasticdash-sdk 0.2.0
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/LICENSE +21 -0
- package/README.md +775 -0
- package/dist/browser-ui.d.ts +43 -0
- package/dist/browser-ui.d.ts.map +1 -0
- package/dist/browser-ui.js +246 -0
- package/dist/browser-ui.js.map +1 -0
- package/dist/capture/event.d.ts +33 -0
- package/dist/capture/event.d.ts.map +1 -0
- package/dist/capture/event.js +2 -0
- package/dist/capture/event.js.map +1 -0
- package/dist/capture/index.d.ts +4 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +4 -0
- package/dist/capture/index.js.map +1 -0
- package/dist/capture/recorder.d.ts +24 -0
- package/dist/capture/recorder.d.ts.map +1 -0
- package/dist/capture/recorder.js +46 -0
- package/dist/capture/recorder.js.map +1 -0
- package/dist/capture/replay.d.ts +20 -0
- package/dist/capture/replay.d.ts.map +1 -0
- package/dist/capture/replay.js +47 -0
- package/dist/capture/replay.js.map +1 -0
- package/dist/ci/api-client.d.ts +38 -0
- package/dist/ci/api-client.d.ts.map +1 -0
- package/dist/ci/api-client.js +96 -0
- package/dist/ci/api-client.js.map +1 -0
- package/dist/ci/benchmark.d.ts +33 -0
- package/dist/ci/benchmark.d.ts.map +1 -0
- package/dist/ci/benchmark.js +213 -0
- package/dist/ci/benchmark.js.map +1 -0
- package/dist/ci/ed-runner.d.ts +48 -0
- package/dist/ci/ed-runner.d.ts.map +1 -0
- package/dist/ci/ed-runner.js +260 -0
- package/dist/ci/ed-runner.js.map +1 -0
- package/dist/ci/executor.d.ts +13 -0
- package/dist/ci/executor.d.ts.map +1 -0
- package/dist/ci/executor.js +542 -0
- package/dist/ci/executor.js.map +1 -0
- package/dist/ci/git-info.d.ts +17 -0
- package/dist/ci/git-info.d.ts.map +1 -0
- package/dist/ci/git-info.js +102 -0
- package/dist/ci/git-info.js.map +1 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/ci/index.js.map +1 -0
- package/dist/ci/measurement.d.ts +9 -0
- package/dist/ci/measurement.d.ts.map +1 -0
- package/dist/ci/measurement.js +15 -0
- package/dist/ci/measurement.js.map +1 -0
- package/dist/ci/replay.d.ts +31 -0
- package/dist/ci/replay.d.ts.map +1 -0
- package/dist/ci/replay.js +96 -0
- package/dist/ci/replay.js.map +1 -0
- package/dist/ci/reporters/default.d.ts +8 -0
- package/dist/ci/reporters/default.d.ts.map +1 -0
- package/dist/ci/reporters/default.js +46 -0
- package/dist/ci/reporters/default.js.map +1 -0
- package/dist/ci/reporters/index.d.ts +8 -0
- package/dist/ci/reporters/index.d.ts.map +1 -0
- package/dist/ci/reporters/index.js +14 -0
- package/dist/ci/reporters/index.js.map +1 -0
- package/dist/ci/reporters/json.d.ts +8 -0
- package/dist/ci/reporters/json.d.ts.map +1 -0
- package/dist/ci/reporters/json.js +14 -0
- package/dist/ci/reporters/json.js.map +1 -0
- package/dist/ci/reporters/junit.d.ts +8 -0
- package/dist/ci/reporters/junit.d.ts.map +1 -0
- package/dist/ci/reporters/junit.js +48 -0
- package/dist/ci/reporters/junit.js.map +1 -0
- package/dist/ci/runner.d.ts +3 -0
- package/dist/ci/runner.d.ts.map +1 -0
- package/dist/ci/runner.js +187 -0
- package/dist/ci/runner.js.map +1 -0
- package/dist/ci/test-discovery.d.ts +5 -0
- package/dist/ci/test-discovery.d.ts.map +1 -0
- package/dist/ci/test-discovery.js +11 -0
- package/dist/ci/test-discovery.js.map +1 -0
- package/dist/ci/test-loader.d.ts +19 -0
- package/dist/ci/test-loader.d.ts.map +1 -0
- package/dist/ci/test-loader.js +149 -0
- package/dist/ci/test-loader.js.map +1 -0
- package/dist/ci/test-registry.d.ts +42 -0
- package/dist/ci/test-registry.d.ts.map +1 -0
- package/dist/ci/test-registry.js +18 -0
- package/dist/ci/test-registry.js.map +1 -0
- package/dist/ci/trace-schema.d.ts +30 -0
- package/dist/ci/trace-schema.d.ts.map +1 -0
- package/dist/ci/trace-schema.js +66 -0
- package/dist/ci/trace-schema.js.map +1 -0
- package/dist/ci/trace-writer.d.ts +16 -0
- package/dist/ci/trace-writer.d.ts.map +1 -0
- package/dist/ci/trace-writer.js +108 -0
- package/dist/ci/trace-writer.js.map +1 -0
- package/dist/ci/types.d.ts +108 -0
- package/dist/ci/types.d.ts.map +1 -0
- package/dist/ci/types.js +3 -0
- package/dist/ci/types.js.map +1 -0
- package/dist/ci/upload-client.d.ts +74 -0
- package/dist/ci/upload-client.d.ts.map +1 -0
- package/dist/ci/upload-client.js +195 -0
- package/dist/ci/upload-client.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +716 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent-state.d.ts +47 -0
- package/dist/core/agent-state.d.ts.map +1 -0
- package/dist/core/agent-state.js +137 -0
- package/dist/core/agent-state.js.map +1 -0
- package/dist/core/judge-utils.d.ts +22 -0
- package/dist/core/judge-utils.d.ts.map +1 -0
- package/dist/core/judge-utils.js +211 -0
- package/dist/core/judge-utils.js.map +1 -0
- package/dist/core/registry.d.ts +28 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +52 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/dashboard-server.d.ts +65 -0
- package/dist/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard-server.js +3940 -0
- package/dist/dashboard-server.js.map +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 +316 -0
- package/dist/execution/tool-runner.js.map +1 -0
- package/dist/html/dashboard.html +2218 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +13 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +8102 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors/ai-interceptor.d.ts +26 -0
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
- package/dist/interceptors/ai-interceptor.js +756 -0
- package/dist/interceptors/ai-interceptor.js.map +1 -0
- package/dist/interceptors/db-auto.d.ts +8 -0
- package/dist/interceptors/db-auto.d.ts.map +1 -0
- package/dist/interceptors/db-auto.js +217 -0
- package/dist/interceptors/db-auto.js.map +1 -0
- package/dist/interceptors/db.d.ts +23 -0
- package/dist/interceptors/db.d.ts.map +1 -0
- package/dist/interceptors/db.js +137 -0
- package/dist/interceptors/db.js.map +1 -0
- package/dist/interceptors/http.d.ts +28 -0
- package/dist/interceptors/http.d.ts.map +1 -0
- package/dist/interceptors/http.js +356 -0
- package/dist/interceptors/http.js.map +1 -0
- package/dist/interceptors/side-effects.d.ts +7 -0
- package/dist/interceptors/side-effects.d.ts.map +1 -0
- package/dist/interceptors/side-effects.js +72 -0
- package/dist/interceptors/side-effects.js.map +1 -0
- package/dist/interceptors/telemetry-push.d.ts +142 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -0
- package/dist/interceptors/telemetry-push.js +463 -0
- package/dist/interceptors/telemetry-push.js.map +1 -0
- package/dist/interceptors/tool.d.ts +2 -0
- package/dist/interceptors/tool.d.ts.map +1 -0
- package/dist/interceptors/tool.js +274 -0
- package/dist/interceptors/tool.js.map +1 -0
- package/dist/interceptors/workflow-ai.d.ts +5 -0
- package/dist/interceptors/workflow-ai.d.ts.map +1 -0
- package/dist/interceptors/workflow-ai.js +382 -0
- package/dist/interceptors/workflow-ai.js.map +1 -0
- package/dist/internals/conditional-recorder.d.ts +21 -0
- package/dist/internals/conditional-recorder.d.ts.map +1 -0
- package/dist/internals/conditional-recorder.js +54 -0
- package/dist/internals/conditional-recorder.js.map +1 -0
- package/dist/internals/mock-resolver.d.ts +146 -0
- package/dist/internals/mock-resolver.d.ts.map +1 -0
- package/dist/internals/mock-resolver.js +427 -0
- package/dist/internals/mock-resolver.js.map +1 -0
- package/dist/matchers/index.d.ts +96 -0
- package/dist/matchers/index.d.ts.map +1 -0
- package/dist/matchers/index.js +668 -0
- package/dist/matchers/index.js.map +1 -0
- package/dist/observability.d.ts +82 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +471 -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 +324 -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 +279 -0
- package/dist/portal-server.js.map +1 -0
- package/dist/proxy/llm-capture.d.ts +14 -0
- package/dist/proxy/llm-capture.d.ts.map +1 -0
- package/dist/proxy/llm-capture.js +264 -0
- package/dist/proxy/llm-capture.js.map +1 -0
- package/dist/reporter.d.ts +3 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +72 -0
- package/dist/reporter.js.map +1 -0
- package/dist/runWorkflowSubprocess.d.ts +14 -0
- package/dist/runWorkflowSubprocess.d.ts.map +1 -0
- package/dist/runWorkflowSubprocess.js +66 -0
- package/dist/runWorkflowSubprocess.js.map +1 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +138 -0
- package/dist/runner.js.map +1 -0
- package/dist/socket-connector.d.ts +22 -0
- package/dist/socket-connector.d.ts.map +1 -0
- package/dist/socket-connector.js +104 -0
- package/dist/socket-connector.js.map +1 -0
- package/dist/telemetry-batcher.d.ts +56 -0
- package/dist/telemetry-batcher.d.ts.map +1 -0
- package/dist/telemetry-batcher.js +143 -0
- package/dist/telemetry-batcher.js.map +1 -0
- package/dist/test-setup.d.ts +12 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/tool-registry.d.ts +31 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +73 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tool-runner-worker.d.ts +2 -0
- package/dist/tool-runner-worker.d.ts.map +1 -0
- package/dist/tool-runner-worker.js +215 -0
- package/dist/tool-runner-worker.js.map +1 -0
- package/dist/trace-adapter/context.d.ts +72 -0
- package/dist/trace-adapter/context.d.ts.map +1 -0
- package/dist/trace-adapter/context.js +80 -0
- package/dist/trace-adapter/context.js.map +1 -0
- package/dist/tracing.d.ts +2 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +59 -0
- package/dist/tracing.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 +130 -0
- package/dist/trigger-executor.js.map +1 -0
- package/dist/types/portal.d.ts +76 -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/license-error.d.ts +23 -0
- package/dist/utils/license-error.d.ts.map +1 -0
- package/dist/utils/license-error.js +42 -0
- package/dist/utils/license-error.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/dist/workflow-runner-worker.d.ts +2 -0
- package/dist/workflow-runner-worker.d.ts.map +1 -0
- package/dist/workflow-runner-worker.js +329 -0
- package/dist/workflow-runner-worker.js.map +1 -0
- package/dist/workflow-runner.d.ts +14 -0
- package/dist/workflow-runner.d.ts.map +1 -0
- package/dist/workflow-runner.js +34 -0
- package/dist/workflow-runner.js.map +1 -0
- package/docs/agent-coding-instructions.md +138 -0
- package/docs/agent-integration-guide.md +564 -0
- package/docs/agents.md +140 -0
- package/docs/dashboard.md +394 -0
- package/docs/deno.md +69 -0
- package/docs/instrumentation.md +424 -0
- package/docs/langfuse-trace-structure.md +145 -0
- package/docs/matchers.md +173 -0
- package/docs/observability_contract.md +192 -0
- package/docs/observability_mode.md +195 -0
- package/docs/quickstart.md +621 -0
- package/docs/security-compliance.md +566 -0
- package/docs/test-writing-guidelines.md +444 -0
- package/docs/tools.md +165 -0
- package/docs/workflow-modes.md +253 -0
- package/package.json +76 -0
- package/src/browser-ui.ts +281 -0
- package/src/capture/event.ts +30 -0
- package/src/capture/index.ts +3 -0
- package/src/capture/recorder.ts +62 -0
- package/src/capture/replay.ts +55 -0
- package/src/ci/api-client.ts +136 -0
- package/src/ci/benchmark.ts +257 -0
- package/src/ci/ed-runner.ts +351 -0
- package/src/ci/executor.ts +671 -0
- package/src/ci/git-info.ts +127 -0
- package/src/ci/index.ts +5 -0
- package/src/ci/measurement.ts +25 -0
- package/src/ci/replay.ts +127 -0
- package/src/ci/reporters/default.ts +50 -0
- package/src/ci/reporters/index.ts +21 -0
- package/src/ci/reporters/json.ts +18 -0
- package/src/ci/reporters/junit.ts +61 -0
- package/src/ci/runner.ts +208 -0
- package/src/ci/test-discovery.ts +16 -0
- package/src/ci/test-loader.ts +187 -0
- package/src/ci/test-registry.ts +62 -0
- package/src/ci/trace-schema.ts +96 -0
- package/src/ci/trace-writer.ts +107 -0
- package/src/ci/types.ts +115 -0
- package/src/ci/upload-client.ts +300 -0
- package/src/cli.ts +811 -0
- package/src/core/agent-state.ts +162 -0
- package/src/core/judge-utils.ts +232 -0
- package/src/core/registry.ts +92 -0
- package/src/dashboard-server.ts +2047 -0
- package/src/execution/tool-runner.ts +352 -0
- package/src/html/dashboard.html +2218 -0
- package/src/http.ts +13 -0
- package/src/index.ts +138 -0
- package/src/interceptors/ai-interceptor.ts +798 -0
- package/src/interceptors/db-auto.ts +243 -0
- package/src/interceptors/db.ts +156 -0
- package/src/interceptors/http.ts +393 -0
- package/src/interceptors/side-effects.ts +83 -0
- package/src/interceptors/telemetry-push.ts +537 -0
- package/src/interceptors/tool.ts +287 -0
- package/src/interceptors/workflow-ai.ts +419 -0
- package/src/internals/conditional-recorder.ts +63 -0
- package/src/internals/mock-resolver.ts +492 -0
- package/src/matchers/index.ts +824 -0
- package/src/observability.ts +501 -0
- package/src/portal-executor.ts +355 -0
- package/src/portal-server.ts +304 -0
- package/src/proxy/llm-capture.ts +301 -0
- package/src/reporter.ts +81 -0
- package/src/runWorkflowSubprocess.ts +74 -0
- package/src/runner.ts +178 -0
- package/src/socket-connector.ts +117 -0
- package/src/telemetry-batcher.ts +191 -0
- package/src/test-setup.ts +16 -0
- package/src/tool-registry.ts +94 -0
- package/src/tool-runner-worker.ts +244 -0
- package/src/trace-adapter/context.ts +156 -0
- package/src/tracing.ts +62 -0
- package/src/trigger-executor.ts +171 -0
- package/src/types/agent.d.ts +63 -0
- package/src/types/expect.d.ts +81 -0
- package/src/types/modules.d.ts +2 -0
- package/src/types/portal.ts +69 -0
- package/src/utils/debug.ts +8 -0
- package/src/utils/license-error.ts +43 -0
- package/src/utils/redact.ts +25 -0
- package/src/workflow-runner-worker.ts +386 -0
- package/src/workflow-runner.ts +58 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { TelemetryBatcher } from './telemetry-batcher.js'
|
|
3
|
+
import {
|
|
4
|
+
setObservabilityContext,
|
|
5
|
+
getObservabilityContext,
|
|
6
|
+
clearObservabilityContext,
|
|
7
|
+
pushTelemetryEvent,
|
|
8
|
+
} from './interceptors/telemetry-push.js'
|
|
9
|
+
import type { ObservabilityContext } from './interceptors/telemetry-push.js'
|
|
10
|
+
import { installAIInterceptor } from './interceptors/ai-interceptor.js'
|
|
11
|
+
import { interceptFetch, getOriginalFetch } from './interceptors/http.js'
|
|
12
|
+
import { installDBAutoInterceptor } from './interceptors/db-auto.js'
|
|
13
|
+
import { connectToBackend, disconnectFromBackend } from './socket-connector.js'
|
|
14
|
+
import { executeTrigger } from './trigger-executor.js'
|
|
15
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { scanTools, scanWorkflows } from './execution/tool-runner.js'
|
|
18
|
+
import { debugLog } from './utils/debug.js'
|
|
19
|
+
import { isTraceCaptureEnabled, maybeCaptureTrace, maybeCaptureTraceSync } from './ci/trace-writer.js'
|
|
20
|
+
import { detectGitInfo } from './ci/git-info.js'
|
|
21
|
+
import type { WorkflowEvent } from './capture/event.js'
|
|
22
|
+
|
|
23
|
+
export interface ObservabilityOptions {
|
|
24
|
+
serverUrl?: string
|
|
25
|
+
apiKey?: string
|
|
26
|
+
sessionId?: string
|
|
27
|
+
batchIntervalMs?: number
|
|
28
|
+
maxBatchSize?: number
|
|
29
|
+
heartbeatIntervalMs?: number
|
|
30
|
+
sampleRate?: number
|
|
31
|
+
redactKeys?: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ObservabilityHandle {
|
|
35
|
+
sessionId: string
|
|
36
|
+
shutdown: () => Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
40
|
+
let shutdownRegistered = false
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extracts workflow names from the `workflows` object in elasticdash.config.ts
|
|
44
|
+
* by reading the source text. Returns an empty array if the file does not exist
|
|
45
|
+
* or cannot be parsed.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the default workflow name used as traceId prefix.
|
|
49
|
+
*
|
|
50
|
+
* Filters ed_workflows.ts exports to exclude known SDK utility functions
|
|
51
|
+
* (edStartTrace, edEndTrace, setElasticDashModule) that are not actual
|
|
52
|
+
* workflow handlers. If exactly one candidate remains, use it. Otherwise
|
|
53
|
+
* fall back to 'unknown-workflow'.
|
|
54
|
+
*/
|
|
55
|
+
function resolveDefaultWorkflowName(cwd: string, workflows: ReturnType<typeof scanWorkflows>): string {
|
|
56
|
+
// Filter out SDK utility functions that users commonly export from ed_workflows.ts
|
|
57
|
+
const UTILITY_PREFIXES = ['edStartTrace', 'edEndTrace', 'setElasticDashModule', 'setElasticDash']
|
|
58
|
+
const candidates = workflows.filter(w => !UTILITY_PREFIXES.some(p => w.name === p))
|
|
59
|
+
|
|
60
|
+
debugLog(`[elasticdash] scanWorkflows found ${workflows.length} exports: [${workflows.map(w => w.name).join(', ')}]`)
|
|
61
|
+
debugLog(`[elasticdash] After filtering utilities: ${candidates.length} candidates: [${candidates.map(w => w.name).join(', ')}]`)
|
|
62
|
+
|
|
63
|
+
if (candidates.length === 1) {
|
|
64
|
+
debugLog(`[elasticdash] Resolved workflow name: ${candidates[0].name}`)
|
|
65
|
+
return candidates[0].name
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Multiple candidates — try reading elasticdash.config.ts for workflow keys
|
|
69
|
+
if (candidates.length > 1) {
|
|
70
|
+
const configNames = readConfigWorkflowNames(cwd)
|
|
71
|
+
if (configNames.length >= 1) {
|
|
72
|
+
debugLog(`[elasticdash] Config workflow names: [${configNames.join(', ')}] — using first: ${configNames[0]}`)
|
|
73
|
+
return configNames[0]
|
|
74
|
+
}
|
|
75
|
+
// No config — use first candidate
|
|
76
|
+
debugLog(`[elasticdash] Multiple workflow candidates, using first: ${candidates[0].name}`)
|
|
77
|
+
return candidates[0].name
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// No candidates after filtering — use first raw export if available
|
|
81
|
+
if (workflows.length > 0) {
|
|
82
|
+
debugLog(`[elasticdash] No workflow candidates after filtering, using first export: ${workflows[0].name}`)
|
|
83
|
+
return workflows[0].name
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
debugLog(`[elasticdash] No workflows found — using 'unknown-workflow'`)
|
|
87
|
+
return 'unknown-workflow'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads workflow keys from the `workflows` object in elasticdash.config.ts.
|
|
92
|
+
* Uses dynamic import to parse the config properly.
|
|
93
|
+
* Returns an empty array if the file does not exist or has no workflows.
|
|
94
|
+
*/
|
|
95
|
+
function readConfigWorkflowNames(cwd: string): string[] {
|
|
96
|
+
for (const filename of ['elasticdash.config.ts', 'elasticdash.config.js']) {
|
|
97
|
+
const configPath = join(cwd, filename)
|
|
98
|
+
if (!existsSync(configPath)) continue
|
|
99
|
+
try {
|
|
100
|
+
const src = readFileSync(configPath, 'utf8')
|
|
101
|
+
// Match top-level keys inside `workflows: { ... }` using a line-based approach:
|
|
102
|
+
// workflow keys appear as `identifier: {` at the start of a line (with indentation)
|
|
103
|
+
// within the workflows block.
|
|
104
|
+
const wfMatch = src.match(/workflows\s*:\s*\{/)
|
|
105
|
+
if (!wfMatch || wfMatch.index === undefined) continue
|
|
106
|
+
|
|
107
|
+
// Find the workflows block by brace-balancing (skip strings)
|
|
108
|
+
const startIdx = wfMatch.index + wfMatch[0].length
|
|
109
|
+
let depth = 1
|
|
110
|
+
let inString: string | null = null
|
|
111
|
+
let endIdx = startIdx
|
|
112
|
+
for (let i = startIdx; i < src.length && depth > 0; i++) {
|
|
113
|
+
const ch = src[i]
|
|
114
|
+
if (inString) {
|
|
115
|
+
if (ch === '\\') { i++; continue }
|
|
116
|
+
if (ch === inString) inString = null
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (ch === "'" || ch === '"' || ch === '`') { inString = ch; continue }
|
|
120
|
+
if (ch === '{') depth++
|
|
121
|
+
else if (ch === '}') depth--
|
|
122
|
+
if (depth > 0) endIdx = i
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract keys at depth 0 within the block — only `identifier: {` with `mode:` inside
|
|
126
|
+
const block = src.slice(startIdx, endIdx + 1)
|
|
127
|
+
const keys: string[] = []
|
|
128
|
+
// Simple approach: find lines matching `word: {` and check they contain `mode:`
|
|
129
|
+
for (const m of block.matchAll(/^\s+(\w+)\s*:\s*\{/gm)) {
|
|
130
|
+
// Verify this is a workflow config (has a `mode:` property) not a nested object
|
|
131
|
+
const keyStart = m.index! + m[0].length
|
|
132
|
+
// Scan ahead to the matching } to check for `mode:`
|
|
133
|
+
let d = 1
|
|
134
|
+
let inStr: string | null = null
|
|
135
|
+
let slice = ''
|
|
136
|
+
for (let i = keyStart; i < block.length && d > 0; i++) {
|
|
137
|
+
const c = block[i]
|
|
138
|
+
if (inStr) {
|
|
139
|
+
if (c === '\\') { i++; continue }
|
|
140
|
+
if (c === inStr) inStr = null
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
if (c === "'" || c === '"' || c === '`') { inStr = c; continue }
|
|
144
|
+
if (c === '{') d++
|
|
145
|
+
else if (c === '}') d--
|
|
146
|
+
if (d > 0) slice += c
|
|
147
|
+
}
|
|
148
|
+
if (/\bmode\s*:/.test(slice)) {
|
|
149
|
+
keys.push(m[1])
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (keys.length > 0) return keys
|
|
153
|
+
} catch { /* config unreadable — fall through */ }
|
|
154
|
+
}
|
|
155
|
+
return []
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Initialise active observability mode. All `wrapTool` / `wrapAI` calls will
|
|
160
|
+
* automatically record and stream trace events to the configured backend.
|
|
161
|
+
*
|
|
162
|
+
* Reads from environment variables as fallback:
|
|
163
|
+
* - `ELASTICDASH_API_URL` — backend API URL (required)
|
|
164
|
+
* - `ELASTICDASH_API_KEY` — project auth token
|
|
165
|
+
* - `ELASTICDASH_SESSION_ID` — session identifier (auto-generated if omitted)
|
|
166
|
+
*/
|
|
167
|
+
export function initObservability(options?: ObservabilityOptions): ObservabilityHandle {
|
|
168
|
+
const serverUrl = options?.serverUrl ?? process.env.ELASTICDASH_API_URL ?? ''
|
|
169
|
+
if (!serverUrl) {
|
|
170
|
+
throw new Error('[elasticdash] initObservability: serverUrl is required (set ELASTICDASH_API_URL or pass serverUrl option)')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const apiKey = options?.apiKey ?? process.env.ELASTICDASH_API_KEY
|
|
174
|
+
const sessionId = options?.sessionId ?? process.env.ELASTICDASH_SESSION_ID ?? randomUUID()
|
|
175
|
+
const sampleRate = options?.sampleRate ?? 1.0
|
|
176
|
+
const redactKeys = options?.redactKeys ?? []
|
|
177
|
+
const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 30_000
|
|
178
|
+
|
|
179
|
+
// Discover workflow name for traceId prefix.
|
|
180
|
+
// Priority: elasticdash.config.ts workflow keys > single ed_workflows.ts export > 'unknown-workflow'
|
|
181
|
+
const cwd = process.cwd()
|
|
182
|
+
const workflows = scanWorkflows(cwd)
|
|
183
|
+
const defaultWorkflowName = resolveDefaultWorkflowName(cwd, workflows)
|
|
184
|
+
|
|
185
|
+
// Detect git info for session metadata (branch, commit, CI provider)
|
|
186
|
+
// If detection fails, values are omitted — never store 'unknown' placeholders
|
|
187
|
+
let gitInfo: ReturnType<typeof detectGitInfo> = {}
|
|
188
|
+
try { gitInfo = detectGitInfo() } catch { /* not in a git repo — skip */ }
|
|
189
|
+
const sessionMetadata: Record<string, unknown> = {}
|
|
190
|
+
if (gitInfo.branch && gitInfo.branch !== 'unknown') sessionMetadata.branch = gitInfo.branch
|
|
191
|
+
if (gitInfo.commit && gitInfo.commit !== 'unknown') sessionMetadata.commit = gitInfo.commit
|
|
192
|
+
if (gitInfo.ciProvider && gitInfo.ciProvider !== 'local') sessionMetadata.ciProvider = gitInfo.ciProvider
|
|
193
|
+
|
|
194
|
+
const batcher = new TelemetryBatcher({
|
|
195
|
+
serverUrl,
|
|
196
|
+
apiKey,
|
|
197
|
+
sessionId,
|
|
198
|
+
metadata: Object.keys(sessionMetadata).length > 0 ? sessionMetadata : undefined,
|
|
199
|
+
batchIntervalMs: options?.batchIntervalMs,
|
|
200
|
+
maxBatchSize: options?.maxBatchSize,
|
|
201
|
+
redactKeys,
|
|
202
|
+
onTrigger: async (trigger) => {
|
|
203
|
+
await executeTrigger(serverUrl, apiKey, trigger)
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
let counter = 0
|
|
208
|
+
const traceId = `${defaultWorkflowName}::${Date.now()}::${randomUUID().slice(0, 8)}`
|
|
209
|
+
const ctx: ObservabilityContext = {
|
|
210
|
+
sessionId,
|
|
211
|
+
serverUrl,
|
|
212
|
+
apiKey,
|
|
213
|
+
batcher,
|
|
214
|
+
nextId: () => ++counter,
|
|
215
|
+
sampleRate,
|
|
216
|
+
redactKeys,
|
|
217
|
+
traceId,
|
|
218
|
+
defaultWorkflowName,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
setObservabilityContext(ctx)
|
|
222
|
+
installAIInterceptor()
|
|
223
|
+
interceptFetch()
|
|
224
|
+
installDBAutoInterceptor().catch(() => {})
|
|
225
|
+
connectToBackend({ serverUrl, apiKey, sessionId })
|
|
226
|
+
|
|
227
|
+
// Heartbeat
|
|
228
|
+
heartbeatTimer = setInterval(() => {
|
|
229
|
+
batcher.enqueue({
|
|
230
|
+
id: ctx.nextId(),
|
|
231
|
+
type: 'side_effect',
|
|
232
|
+
name: '__heartbeat__',
|
|
233
|
+
input: { sessionId },
|
|
234
|
+
output: { uptime: process.uptime() },
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
durationMs: 0,
|
|
237
|
+
schemaVersion: 1,
|
|
238
|
+
})
|
|
239
|
+
}, heartbeatIntervalMs)
|
|
240
|
+
if (heartbeatTimer && typeof heartbeatTimer === 'object' && 'unref' in heartbeatTimer) {
|
|
241
|
+
heartbeatTimer.unref()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Register process exit hooks (once)
|
|
245
|
+
if (!shutdownRegistered) {
|
|
246
|
+
shutdownRegistered = true
|
|
247
|
+
const onExit = () => { shutdownObservability().catch(() => {}) }
|
|
248
|
+
process.once('beforeExit', onExit)
|
|
249
|
+
process.once('SIGTERM', onExit)
|
|
250
|
+
process.once('SIGINT', onExit)
|
|
251
|
+
|
|
252
|
+
// Sync exit handler: last resort to flush any pending trace capture.
|
|
253
|
+
// process.on('exit') only allows synchronous code — the async handlers above
|
|
254
|
+
// may not complete before the process terminates (e.g. SIGINT on a dev server).
|
|
255
|
+
if (isTraceCaptureEnabled()) {
|
|
256
|
+
process.once('exit', () => {
|
|
257
|
+
const exitCtx = getObservabilityContext()
|
|
258
|
+
if (exitCtx?.eventCollector && exitCtx.eventCollector.length > 0) {
|
|
259
|
+
maybeCaptureTraceSync(exitCtx.eventCollector, exitCtx.traceId)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
debugLog(`[elasticdash] Observability initialised — sessionId=${sessionId} workflow=${defaultWorkflowName} server=${serverUrl}`)
|
|
266
|
+
|
|
267
|
+
// Push workflow/tool catalog to backend (fire-and-forget)
|
|
268
|
+
pushCatalog(serverUrl, apiKey, workflows).catch(() => {})
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
sessionId,
|
|
272
|
+
shutdown: () => shutdownObservability(),
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* POST the workflow/tool catalog to the backend so it registers the real
|
|
278
|
+
* function names from ed_workflows.ts and ed_tools.ts.
|
|
279
|
+
*/
|
|
280
|
+
async function pushCatalog(serverUrl: string, apiKey?: string, workflows: ReturnType<typeof scanWorkflows> = []): Promise<void> {
|
|
281
|
+
const cwd = process.cwd()
|
|
282
|
+
const tools = scanTools(cwd)
|
|
283
|
+
|
|
284
|
+
if (workflows.length === 0 && tools.length === 0) return
|
|
285
|
+
|
|
286
|
+
const body: Record<string, unknown> = {}
|
|
287
|
+
if (workflows.length > 0) body.workflows = workflows.map(w => ({ name: w.name }))
|
|
288
|
+
if (tools.length > 0) body.tools = tools.map(t => ({ name: t.name }))
|
|
289
|
+
|
|
290
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
291
|
+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const res = await getOriginalFetch()(`${serverUrl.replace(/\/$/, '')}/api/observability/catalog`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers,
|
|
297
|
+
body: JSON.stringify(body),
|
|
298
|
+
})
|
|
299
|
+
debugLog(`[elasticdash] Catalog pushed: ${workflows.length} workflows, ${tools.length} tools (status ${res.status})`)
|
|
300
|
+
} catch (err) {
|
|
301
|
+
debugLog(`[elasticdash] Catalog push failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Gracefully shut down observability: flush remaining events, send session_end,
|
|
307
|
+
* stop heartbeat, and clear context.
|
|
308
|
+
*/
|
|
309
|
+
export async function shutdownObservability(): Promise<void> {
|
|
310
|
+
const ctx = getObservabilityContext()
|
|
311
|
+
if (!ctx) return
|
|
312
|
+
|
|
313
|
+
// Flush any pending trace capture before shutdown
|
|
314
|
+
if (ctx.eventCollector && ctx.eventCollector.length > 0) {
|
|
315
|
+
const events = ctx.eventCollector
|
|
316
|
+
ctx.eventCollector = undefined
|
|
317
|
+
await maybeCaptureTrace(events, ctx.traceId)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Stop heartbeat
|
|
321
|
+
if (heartbeatTimer) {
|
|
322
|
+
clearInterval(heartbeatTimer)
|
|
323
|
+
heartbeatTimer = null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Send session_end event
|
|
327
|
+
ctx.batcher.enqueue({
|
|
328
|
+
id: ctx.nextId(),
|
|
329
|
+
type: 'side_effect',
|
|
330
|
+
name: '__session_end__',
|
|
331
|
+
input: { sessionId: ctx.sessionId },
|
|
332
|
+
output: { uptime: process.uptime() },
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
durationMs: 0,
|
|
335
|
+
schemaVersion: 1,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// Disconnect socket
|
|
339
|
+
await disconnectFromBackend()
|
|
340
|
+
|
|
341
|
+
// Flush and shut down the batcher
|
|
342
|
+
await ctx.batcher.shutdown()
|
|
343
|
+
|
|
344
|
+
clearObservabilityContext()
|
|
345
|
+
debugLog(`[elasticdash] Observability shut down — sessionId=${ctx.sessionId}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Start a new trace for the current request/operation.
|
|
350
|
+
*
|
|
351
|
+
* Call this at the start of each request handler to group all tool/AI events
|
|
352
|
+
* under one trace, identified by the workflow name.
|
|
353
|
+
*
|
|
354
|
+
* The generated traceId encodes the workflow name and timestamp for easy
|
|
355
|
+
* parsing by the backend: `{workflowName}::{timestamp}::{shortId}`
|
|
356
|
+
*
|
|
357
|
+
* @param workflowName — the workflow/function being executed (e.g. 'chatStreamHandler')
|
|
358
|
+
* @returns The new traceId
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* // In a route handler:
|
|
363
|
+
* startTrace('chatStreamHandler')
|
|
364
|
+
* // All subsequent wrapTool/wrapAI calls get traceId = "chatStreamHandler::1712851200000::a1b2c3d4"
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
export function startTrace(workflowName?: string): string {
|
|
368
|
+
const ctx = getObservabilityContext()
|
|
369
|
+
if (!ctx) throw new Error('[elasticdash] startTrace: observability not initialised')
|
|
370
|
+
|
|
371
|
+
// Flush any pending trace capture from a previous startTrace() that had no endTrace()
|
|
372
|
+
if (ctx.eventCollector && ctx.eventCollector.length > 0) {
|
|
373
|
+
debugLog(`[elasticdash] startTrace: flushing ${ctx.eventCollector.length} events from previous trace`)
|
|
374
|
+
const events = ctx.eventCollector
|
|
375
|
+
ctx.eventCollector = undefined
|
|
376
|
+
maybeCaptureTrace(events, ctx.traceId).catch((err) => {
|
|
377
|
+
debugLog(`[elasticdash] Failed to capture trace: ${err instanceof Error ? err.message : String(err)}`)
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const resolvedName = workflowName || ctx.defaultWorkflowName
|
|
382
|
+
const timestamp = Date.now()
|
|
383
|
+
const shortId = randomUUID().slice(0, 8)
|
|
384
|
+
ctx.traceId = `${resolvedName}::${timestamp}::${shortId}`
|
|
385
|
+
|
|
386
|
+
// When ELASTICDASH_CAPTURE_TRACE=1, start collecting events for disk trace
|
|
387
|
+
const captureEnabled = isTraceCaptureEnabled()
|
|
388
|
+
debugLog(`[elasticdash] startTrace: ${resolvedName}, capture=${captureEnabled}`)
|
|
389
|
+
if (captureEnabled) {
|
|
390
|
+
ctx.eventCollector = []
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Emit a workflow event so the backend can discover the workflow name
|
|
394
|
+
// (same as wrapWorkflow, but without wrapping — for streaming handlers)
|
|
395
|
+
pushTelemetryEvent({
|
|
396
|
+
id: ctx.nextId(),
|
|
397
|
+
type: 'workflow',
|
|
398
|
+
name: resolvedName,
|
|
399
|
+
input: null,
|
|
400
|
+
output: null,
|
|
401
|
+
timestamp,
|
|
402
|
+
durationMs: 0,
|
|
403
|
+
schemaVersion: 1,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
return ctx.traceId
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* End the current trace. Resets the traceId so subsequent events (heartbeats,
|
|
411
|
+
* new requests) are not attributed to the completed workflow.
|
|
412
|
+
*
|
|
413
|
+
* Call this when a workflow/request handler finishes — especially for streaming
|
|
414
|
+
* handlers where the trace would otherwise persist indefinitely.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* startTrace('chatStreamHandler')
|
|
419
|
+
* // ... workflow logic ...
|
|
420
|
+
* endTrace()
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
export function endTrace(): void {
|
|
424
|
+
const ctx = getObservabilityContext()
|
|
425
|
+
debugLog(`[elasticdash] endTrace called, obsCtx=${!!ctx}, eventCollector=${ctx?.eventCollector?.length ?? 'none'}`)
|
|
426
|
+
if (!ctx) return
|
|
427
|
+
|
|
428
|
+
// Flush collected events to disk if trace capture is enabled
|
|
429
|
+
if (ctx.eventCollector && ctx.eventCollector.length > 0) {
|
|
430
|
+
debugLog(`[elasticdash] endTrace: flushing ${ctx.eventCollector.length} events to disk for traceId=${ctx.traceId}`)
|
|
431
|
+
const events = ctx.eventCollector
|
|
432
|
+
ctx.eventCollector = undefined
|
|
433
|
+
maybeCaptureTrace(events, ctx.traceId).catch((err) => {
|
|
434
|
+
debugLog(`[elasticdash] Failed to capture trace: ${err instanceof Error ? err.message : String(err)}`)
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const timestamp = Date.now()
|
|
439
|
+
const shortId = randomUUID().slice(0, 8)
|
|
440
|
+
ctx.traceId = `${ctx.defaultWorkflowName}::${timestamp}::${shortId}`
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Wrap a workflow function so that every invocation automatically starts a new
|
|
445
|
+
* trace with the workflow name. All tool/AI calls within the function execution
|
|
446
|
+
* are grouped under that trace.
|
|
447
|
+
*
|
|
448
|
+
* Use this in `ed_workflows.ts` to automatically tag each workflow execution:
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* export const chatStreamHandler = wrapWorkflow('chatStreamHandler', async (input) => {
|
|
453
|
+
* const result = await fetchUser(input.userId)
|
|
454
|
+
* const reply = await generateReply(result)
|
|
455
|
+
* return reply
|
|
456
|
+
* })
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
export function wrapWorkflow<Args extends unknown[], R>(
|
|
460
|
+
name: string,
|
|
461
|
+
fn: (...args: Args) => Promise<R>,
|
|
462
|
+
): (...args: Args) => Promise<R> {
|
|
463
|
+
return async (...args: Args): Promise<R> => {
|
|
464
|
+
const ctx = getObservabilityContext()
|
|
465
|
+
if (ctx) {
|
|
466
|
+
startTrace(name)
|
|
467
|
+
const collectedEvents: import('./capture/event.js').WorkflowEvent[] = []
|
|
468
|
+
ctx.eventCollector = collectedEvents
|
|
469
|
+
const start = Date.now()
|
|
470
|
+
let result: R
|
|
471
|
+
let error: unknown = undefined
|
|
472
|
+
try {
|
|
473
|
+
result = await fn(...args)
|
|
474
|
+
} catch (err) {
|
|
475
|
+
error = err
|
|
476
|
+
result = undefined as R
|
|
477
|
+
}
|
|
478
|
+
// Emit a single workflow event regardless of success or failure.
|
|
479
|
+
// Inner tool/AI events already record their own errors individually —
|
|
480
|
+
// the workflow event should reflect the workflow's own output, not
|
|
481
|
+
// escalate inner step failures to the workflow level.
|
|
482
|
+
pushTelemetryEvent({
|
|
483
|
+
id: ctx.nextId(),
|
|
484
|
+
type: 'workflow',
|
|
485
|
+
name,
|
|
486
|
+
input: args.length === 1 ? args[0] : args,
|
|
487
|
+
output: error !== undefined ? null : result,
|
|
488
|
+
timestamp: start,
|
|
489
|
+
durationMs: Date.now() - start,
|
|
490
|
+
schemaVersion: 1,
|
|
491
|
+
})
|
|
492
|
+
ctx.eventCollector = undefined
|
|
493
|
+
// Persist trace to disk if ELASTICDASH_CAPTURE_TRACE=1
|
|
494
|
+
await maybeCaptureTrace(collectedEvents, ctx.traceId)
|
|
495
|
+
endTrace()
|
|
496
|
+
if (error !== undefined) throw error
|
|
497
|
+
return result
|
|
498
|
+
}
|
|
499
|
+
return fn(...args)
|
|
500
|
+
}
|
|
501
|
+
}
|