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.
Files changed (349) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +775 -0
  3. package/dist/browser-ui.d.ts +43 -0
  4. package/dist/browser-ui.d.ts.map +1 -0
  5. package/dist/browser-ui.js +246 -0
  6. package/dist/browser-ui.js.map +1 -0
  7. package/dist/capture/event.d.ts +33 -0
  8. package/dist/capture/event.d.ts.map +1 -0
  9. package/dist/capture/event.js +2 -0
  10. package/dist/capture/event.js.map +1 -0
  11. package/dist/capture/index.d.ts +4 -0
  12. package/dist/capture/index.d.ts.map +1 -0
  13. package/dist/capture/index.js +4 -0
  14. package/dist/capture/index.js.map +1 -0
  15. package/dist/capture/recorder.d.ts +24 -0
  16. package/dist/capture/recorder.d.ts.map +1 -0
  17. package/dist/capture/recorder.js +46 -0
  18. package/dist/capture/recorder.js.map +1 -0
  19. package/dist/capture/replay.d.ts +20 -0
  20. package/dist/capture/replay.d.ts.map +1 -0
  21. package/dist/capture/replay.js +47 -0
  22. package/dist/capture/replay.js.map +1 -0
  23. package/dist/ci/api-client.d.ts +38 -0
  24. package/dist/ci/api-client.d.ts.map +1 -0
  25. package/dist/ci/api-client.js +96 -0
  26. package/dist/ci/api-client.js.map +1 -0
  27. package/dist/ci/benchmark.d.ts +33 -0
  28. package/dist/ci/benchmark.d.ts.map +1 -0
  29. package/dist/ci/benchmark.js +213 -0
  30. package/dist/ci/benchmark.js.map +1 -0
  31. package/dist/ci/ed-runner.d.ts +48 -0
  32. package/dist/ci/ed-runner.d.ts.map +1 -0
  33. package/dist/ci/ed-runner.js +260 -0
  34. package/dist/ci/ed-runner.js.map +1 -0
  35. package/dist/ci/executor.d.ts +13 -0
  36. package/dist/ci/executor.d.ts.map +1 -0
  37. package/dist/ci/executor.js +542 -0
  38. package/dist/ci/executor.js.map +1 -0
  39. package/dist/ci/git-info.d.ts +17 -0
  40. package/dist/ci/git-info.d.ts.map +1 -0
  41. package/dist/ci/git-info.js +102 -0
  42. package/dist/ci/git-info.js.map +1 -0
  43. package/dist/ci/index.d.ts +6 -0
  44. package/dist/ci/index.d.ts.map +1 -0
  45. package/dist/ci/index.js +4 -0
  46. package/dist/ci/index.js.map +1 -0
  47. package/dist/ci/measurement.d.ts +9 -0
  48. package/dist/ci/measurement.d.ts.map +1 -0
  49. package/dist/ci/measurement.js +15 -0
  50. package/dist/ci/measurement.js.map +1 -0
  51. package/dist/ci/replay.d.ts +31 -0
  52. package/dist/ci/replay.d.ts.map +1 -0
  53. package/dist/ci/replay.js +96 -0
  54. package/dist/ci/replay.js.map +1 -0
  55. package/dist/ci/reporters/default.d.ts +8 -0
  56. package/dist/ci/reporters/default.d.ts.map +1 -0
  57. package/dist/ci/reporters/default.js +46 -0
  58. package/dist/ci/reporters/default.js.map +1 -0
  59. package/dist/ci/reporters/index.d.ts +8 -0
  60. package/dist/ci/reporters/index.d.ts.map +1 -0
  61. package/dist/ci/reporters/index.js +14 -0
  62. package/dist/ci/reporters/index.js.map +1 -0
  63. package/dist/ci/reporters/json.d.ts +8 -0
  64. package/dist/ci/reporters/json.d.ts.map +1 -0
  65. package/dist/ci/reporters/json.js +14 -0
  66. package/dist/ci/reporters/json.js.map +1 -0
  67. package/dist/ci/reporters/junit.d.ts +8 -0
  68. package/dist/ci/reporters/junit.d.ts.map +1 -0
  69. package/dist/ci/reporters/junit.js +48 -0
  70. package/dist/ci/reporters/junit.js.map +1 -0
  71. package/dist/ci/runner.d.ts +3 -0
  72. package/dist/ci/runner.d.ts.map +1 -0
  73. package/dist/ci/runner.js +187 -0
  74. package/dist/ci/runner.js.map +1 -0
  75. package/dist/ci/test-discovery.d.ts +5 -0
  76. package/dist/ci/test-discovery.d.ts.map +1 -0
  77. package/dist/ci/test-discovery.js +11 -0
  78. package/dist/ci/test-discovery.js.map +1 -0
  79. package/dist/ci/test-loader.d.ts +19 -0
  80. package/dist/ci/test-loader.d.ts.map +1 -0
  81. package/dist/ci/test-loader.js +149 -0
  82. package/dist/ci/test-loader.js.map +1 -0
  83. package/dist/ci/test-registry.d.ts +42 -0
  84. package/dist/ci/test-registry.d.ts.map +1 -0
  85. package/dist/ci/test-registry.js +18 -0
  86. package/dist/ci/test-registry.js.map +1 -0
  87. package/dist/ci/trace-schema.d.ts +30 -0
  88. package/dist/ci/trace-schema.d.ts.map +1 -0
  89. package/dist/ci/trace-schema.js +66 -0
  90. package/dist/ci/trace-schema.js.map +1 -0
  91. package/dist/ci/trace-writer.d.ts +16 -0
  92. package/dist/ci/trace-writer.d.ts.map +1 -0
  93. package/dist/ci/trace-writer.js +108 -0
  94. package/dist/ci/trace-writer.js.map +1 -0
  95. package/dist/ci/types.d.ts +108 -0
  96. package/dist/ci/types.d.ts.map +1 -0
  97. package/dist/ci/types.js +3 -0
  98. package/dist/ci/types.js.map +1 -0
  99. package/dist/ci/upload-client.d.ts +74 -0
  100. package/dist/ci/upload-client.d.ts.map +1 -0
  101. package/dist/ci/upload-client.js +195 -0
  102. package/dist/ci/upload-client.js.map +1 -0
  103. package/dist/cli.d.ts +3 -0
  104. package/dist/cli.d.ts.map +1 -0
  105. package/dist/cli.js +716 -0
  106. package/dist/cli.js.map +1 -0
  107. package/dist/core/agent-state.d.ts +47 -0
  108. package/dist/core/agent-state.d.ts.map +1 -0
  109. package/dist/core/agent-state.js +137 -0
  110. package/dist/core/agent-state.js.map +1 -0
  111. package/dist/core/judge-utils.d.ts +22 -0
  112. package/dist/core/judge-utils.d.ts.map +1 -0
  113. package/dist/core/judge-utils.js +211 -0
  114. package/dist/core/judge-utils.js.map +1 -0
  115. package/dist/core/registry.d.ts +28 -0
  116. package/dist/core/registry.d.ts.map +1 -0
  117. package/dist/core/registry.js +52 -0
  118. package/dist/core/registry.js.map +1 -0
  119. package/dist/dashboard-server.d.ts +65 -0
  120. package/dist/dashboard-server.d.ts.map +1 -0
  121. package/dist/dashboard-server.js +3940 -0
  122. package/dist/dashboard-server.js.map +1 -0
  123. package/dist/execution/tool-runner.d.ts +26 -0
  124. package/dist/execution/tool-runner.d.ts.map +1 -0
  125. package/dist/execution/tool-runner.js +316 -0
  126. package/dist/execution/tool-runner.js.map +1 -0
  127. package/dist/html/dashboard.html +2218 -0
  128. package/dist/http.d.ts +14 -0
  129. package/dist/http.d.ts.map +1 -0
  130. package/dist/http.js +13 -0
  131. package/dist/http.js.map +1 -0
  132. package/dist/index.cjs +8102 -0
  133. package/dist/index.d.ts +61 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +67 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/interceptors/ai-interceptor.d.ts +26 -0
  138. package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
  139. package/dist/interceptors/ai-interceptor.js +756 -0
  140. package/dist/interceptors/ai-interceptor.js.map +1 -0
  141. package/dist/interceptors/db-auto.d.ts +8 -0
  142. package/dist/interceptors/db-auto.d.ts.map +1 -0
  143. package/dist/interceptors/db-auto.js +217 -0
  144. package/dist/interceptors/db-auto.js.map +1 -0
  145. package/dist/interceptors/db.d.ts +23 -0
  146. package/dist/interceptors/db.d.ts.map +1 -0
  147. package/dist/interceptors/db.js +137 -0
  148. package/dist/interceptors/db.js.map +1 -0
  149. package/dist/interceptors/http.d.ts +28 -0
  150. package/dist/interceptors/http.d.ts.map +1 -0
  151. package/dist/interceptors/http.js +356 -0
  152. package/dist/interceptors/http.js.map +1 -0
  153. package/dist/interceptors/side-effects.d.ts +7 -0
  154. package/dist/interceptors/side-effects.d.ts.map +1 -0
  155. package/dist/interceptors/side-effects.js +72 -0
  156. package/dist/interceptors/side-effects.js.map +1 -0
  157. package/dist/interceptors/telemetry-push.d.ts +142 -0
  158. package/dist/interceptors/telemetry-push.d.ts.map +1 -0
  159. package/dist/interceptors/telemetry-push.js +463 -0
  160. package/dist/interceptors/telemetry-push.js.map +1 -0
  161. package/dist/interceptors/tool.d.ts +2 -0
  162. package/dist/interceptors/tool.d.ts.map +1 -0
  163. package/dist/interceptors/tool.js +274 -0
  164. package/dist/interceptors/tool.js.map +1 -0
  165. package/dist/interceptors/workflow-ai.d.ts +5 -0
  166. package/dist/interceptors/workflow-ai.d.ts.map +1 -0
  167. package/dist/interceptors/workflow-ai.js +382 -0
  168. package/dist/interceptors/workflow-ai.js.map +1 -0
  169. package/dist/internals/conditional-recorder.d.ts +21 -0
  170. package/dist/internals/conditional-recorder.d.ts.map +1 -0
  171. package/dist/internals/conditional-recorder.js +54 -0
  172. package/dist/internals/conditional-recorder.js.map +1 -0
  173. package/dist/internals/mock-resolver.d.ts +146 -0
  174. package/dist/internals/mock-resolver.d.ts.map +1 -0
  175. package/dist/internals/mock-resolver.js +427 -0
  176. package/dist/internals/mock-resolver.js.map +1 -0
  177. package/dist/matchers/index.d.ts +96 -0
  178. package/dist/matchers/index.d.ts.map +1 -0
  179. package/dist/matchers/index.js +668 -0
  180. package/dist/matchers/index.js.map +1 -0
  181. package/dist/observability.d.ts +82 -0
  182. package/dist/observability.d.ts.map +1 -0
  183. package/dist/observability.js +471 -0
  184. package/dist/observability.js.map +1 -0
  185. package/dist/portal-executor.d.ts +30 -0
  186. package/dist/portal-executor.d.ts.map +1 -0
  187. package/dist/portal-executor.js +324 -0
  188. package/dist/portal-executor.js.map +1 -0
  189. package/dist/portal-server.d.ts +3 -0
  190. package/dist/portal-server.d.ts.map +1 -0
  191. package/dist/portal-server.js +279 -0
  192. package/dist/portal-server.js.map +1 -0
  193. package/dist/proxy/llm-capture.d.ts +14 -0
  194. package/dist/proxy/llm-capture.d.ts.map +1 -0
  195. package/dist/proxy/llm-capture.js +264 -0
  196. package/dist/proxy/llm-capture.js.map +1 -0
  197. package/dist/reporter.d.ts +3 -0
  198. package/dist/reporter.d.ts.map +1 -0
  199. package/dist/reporter.js +72 -0
  200. package/dist/reporter.js.map +1 -0
  201. package/dist/runWorkflowSubprocess.d.ts +14 -0
  202. package/dist/runWorkflowSubprocess.d.ts.map +1 -0
  203. package/dist/runWorkflowSubprocess.js +66 -0
  204. package/dist/runWorkflowSubprocess.js.map +1 -0
  205. package/dist/runner.d.ts +16 -0
  206. package/dist/runner.d.ts.map +1 -0
  207. package/dist/runner.js +138 -0
  208. package/dist/runner.js.map +1 -0
  209. package/dist/socket-connector.d.ts +22 -0
  210. package/dist/socket-connector.d.ts.map +1 -0
  211. package/dist/socket-connector.js +104 -0
  212. package/dist/socket-connector.js.map +1 -0
  213. package/dist/telemetry-batcher.d.ts +56 -0
  214. package/dist/telemetry-batcher.d.ts.map +1 -0
  215. package/dist/telemetry-batcher.js +143 -0
  216. package/dist/telemetry-batcher.js.map +1 -0
  217. package/dist/test-setup.d.ts +12 -0
  218. package/dist/test-setup.d.ts.map +1 -0
  219. package/dist/test-setup.js +13 -0
  220. package/dist/test-setup.js.map +1 -0
  221. package/dist/tool-registry.d.ts +31 -0
  222. package/dist/tool-registry.d.ts.map +1 -0
  223. package/dist/tool-registry.js +73 -0
  224. package/dist/tool-registry.js.map +1 -0
  225. package/dist/tool-runner-worker.d.ts +2 -0
  226. package/dist/tool-runner-worker.d.ts.map +1 -0
  227. package/dist/tool-runner-worker.js +215 -0
  228. package/dist/tool-runner-worker.js.map +1 -0
  229. package/dist/trace-adapter/context.d.ts +72 -0
  230. package/dist/trace-adapter/context.d.ts.map +1 -0
  231. package/dist/trace-adapter/context.js +80 -0
  232. package/dist/trace-adapter/context.js.map +1 -0
  233. package/dist/tracing.d.ts +2 -0
  234. package/dist/tracing.d.ts.map +1 -0
  235. package/dist/tracing.js +59 -0
  236. package/dist/tracing.js.map +1 -0
  237. package/dist/trigger-executor.d.ts +12 -0
  238. package/dist/trigger-executor.d.ts.map +1 -0
  239. package/dist/trigger-executor.js +130 -0
  240. package/dist/trigger-executor.js.map +1 -0
  241. package/dist/types/portal.d.ts +76 -0
  242. package/dist/types/portal.d.ts.map +1 -0
  243. package/dist/types/portal.js +2 -0
  244. package/dist/types/portal.js.map +1 -0
  245. package/dist/utils/debug.d.ts +3 -0
  246. package/dist/utils/debug.d.ts.map +1 -0
  247. package/dist/utils/debug.js +8 -0
  248. package/dist/utils/debug.js.map +1 -0
  249. package/dist/utils/license-error.d.ts +23 -0
  250. package/dist/utils/license-error.d.ts.map +1 -0
  251. package/dist/utils/license-error.js +42 -0
  252. package/dist/utils/license-error.js.map +1 -0
  253. package/dist/utils/redact.d.ts +7 -0
  254. package/dist/utils/redact.d.ts.map +1 -0
  255. package/dist/utils/redact.js +26 -0
  256. package/dist/utils/redact.js.map +1 -0
  257. package/dist/workflow-runner-worker.d.ts +2 -0
  258. package/dist/workflow-runner-worker.d.ts.map +1 -0
  259. package/dist/workflow-runner-worker.js +329 -0
  260. package/dist/workflow-runner-worker.js.map +1 -0
  261. package/dist/workflow-runner.d.ts +14 -0
  262. package/dist/workflow-runner.d.ts.map +1 -0
  263. package/dist/workflow-runner.js +34 -0
  264. package/dist/workflow-runner.js.map +1 -0
  265. package/docs/agent-coding-instructions.md +138 -0
  266. package/docs/agent-integration-guide.md +564 -0
  267. package/docs/agents.md +140 -0
  268. package/docs/dashboard.md +394 -0
  269. package/docs/deno.md +69 -0
  270. package/docs/instrumentation.md +424 -0
  271. package/docs/langfuse-trace-structure.md +145 -0
  272. package/docs/matchers.md +173 -0
  273. package/docs/observability_contract.md +192 -0
  274. package/docs/observability_mode.md +195 -0
  275. package/docs/quickstart.md +621 -0
  276. package/docs/security-compliance.md +566 -0
  277. package/docs/test-writing-guidelines.md +444 -0
  278. package/docs/tools.md +165 -0
  279. package/docs/workflow-modes.md +253 -0
  280. package/package.json +76 -0
  281. package/src/browser-ui.ts +281 -0
  282. package/src/capture/event.ts +30 -0
  283. package/src/capture/index.ts +3 -0
  284. package/src/capture/recorder.ts +62 -0
  285. package/src/capture/replay.ts +55 -0
  286. package/src/ci/api-client.ts +136 -0
  287. package/src/ci/benchmark.ts +257 -0
  288. package/src/ci/ed-runner.ts +351 -0
  289. package/src/ci/executor.ts +671 -0
  290. package/src/ci/git-info.ts +127 -0
  291. package/src/ci/index.ts +5 -0
  292. package/src/ci/measurement.ts +25 -0
  293. package/src/ci/replay.ts +127 -0
  294. package/src/ci/reporters/default.ts +50 -0
  295. package/src/ci/reporters/index.ts +21 -0
  296. package/src/ci/reporters/json.ts +18 -0
  297. package/src/ci/reporters/junit.ts +61 -0
  298. package/src/ci/runner.ts +208 -0
  299. package/src/ci/test-discovery.ts +16 -0
  300. package/src/ci/test-loader.ts +187 -0
  301. package/src/ci/test-registry.ts +62 -0
  302. package/src/ci/trace-schema.ts +96 -0
  303. package/src/ci/trace-writer.ts +107 -0
  304. package/src/ci/types.ts +115 -0
  305. package/src/ci/upload-client.ts +300 -0
  306. package/src/cli.ts +811 -0
  307. package/src/core/agent-state.ts +162 -0
  308. package/src/core/judge-utils.ts +232 -0
  309. package/src/core/registry.ts +92 -0
  310. package/src/dashboard-server.ts +2047 -0
  311. package/src/execution/tool-runner.ts +352 -0
  312. package/src/html/dashboard.html +2218 -0
  313. package/src/http.ts +13 -0
  314. package/src/index.ts +138 -0
  315. package/src/interceptors/ai-interceptor.ts +798 -0
  316. package/src/interceptors/db-auto.ts +243 -0
  317. package/src/interceptors/db.ts +156 -0
  318. package/src/interceptors/http.ts +393 -0
  319. package/src/interceptors/side-effects.ts +83 -0
  320. package/src/interceptors/telemetry-push.ts +537 -0
  321. package/src/interceptors/tool.ts +287 -0
  322. package/src/interceptors/workflow-ai.ts +419 -0
  323. package/src/internals/conditional-recorder.ts +63 -0
  324. package/src/internals/mock-resolver.ts +492 -0
  325. package/src/matchers/index.ts +824 -0
  326. package/src/observability.ts +501 -0
  327. package/src/portal-executor.ts +355 -0
  328. package/src/portal-server.ts +304 -0
  329. package/src/proxy/llm-capture.ts +301 -0
  330. package/src/reporter.ts +81 -0
  331. package/src/runWorkflowSubprocess.ts +74 -0
  332. package/src/runner.ts +178 -0
  333. package/src/socket-connector.ts +117 -0
  334. package/src/telemetry-batcher.ts +191 -0
  335. package/src/test-setup.ts +16 -0
  336. package/src/tool-registry.ts +94 -0
  337. package/src/tool-runner-worker.ts +244 -0
  338. package/src/trace-adapter/context.ts +156 -0
  339. package/src/tracing.ts +62 -0
  340. package/src/trigger-executor.ts +171 -0
  341. package/src/types/agent.d.ts +63 -0
  342. package/src/types/expect.d.ts +81 -0
  343. package/src/types/modules.d.ts +2 -0
  344. package/src/types/portal.ts +69 -0
  345. package/src/utils/debug.ts +8 -0
  346. package/src/utils/license-error.ts +43 -0
  347. package/src/utils/redact.ts +25 -0
  348. package/src/workflow-runner-worker.ts +386 -0
  349. 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
+ }