testdriverai 7.11.70-test → 7.11.71-test

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/agent/lib/http.js CHANGED
@@ -27,13 +27,22 @@ const USER_AGENT = `TestDriverSDK/${version} (Node.js ${process.version})`;
27
27
  * @returns {object} Headers object (empty if no sessionId and no active span)
28
28
  */
29
29
  function getSentryTraceHeaders(sessionId) {
30
+ // Add a W3C traceparent for BetterStack/OTel continuity alongside Sentry's
31
+ // headers so the same distributed trace reaches both backends.
32
+ let traceparentHeaders = {};
33
+ try {
34
+ traceparentHeaders = require("../../lib/betterstack").getTraceparentHeaders(sessionId);
35
+ } catch (e) {
36
+ // BetterStack module not present — Sentry headers still propagate.
37
+ }
38
+
30
39
  // Prefer Sentry's own trace propagation when available
31
40
  try {
32
41
  const Sentry = require("@sentry/node");
33
42
  if (typeof Sentry.getTraceData === "function") {
34
43
  const traceData = Sentry.getTraceData();
35
44
  if (traceData && traceData["sentry-trace"]) {
36
- return traceData;
45
+ return { ...traceData, ...traceparentHeaders };
37
46
  }
38
47
  }
39
48
  } catch (e) {
@@ -41,7 +50,7 @@ function getSentryTraceHeaders(sessionId) {
41
50
  }
42
51
 
43
52
  // Fallback: derive deterministic trace from session ID
44
- if (!sessionId) return {};
53
+ if (!sessionId) return { ...traceparentHeaders };
45
54
  const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
46
55
  const spanId = crypto.randomBytes(8).toString("hex");
47
56
  return {
@@ -50,6 +59,7 @@ function getSentryTraceHeaders(sessionId) {
50
59
  "sentry-trace_id=" +
51
60
  traceId +
52
61
  ",sentry-sample_rate=1.0,sentry-sampled=true",
62
+ ...traceparentHeaders,
53
63
  };
54
64
  }
55
65
 
@@ -41,12 +41,16 @@ function majorMinor(version) {
41
41
  * @param {string} [opts.sentryDsn]
42
42
  * @param {string} [opts.sentryEnvironment]
43
43
  * @param {string} [opts.sentryChannel]
44
+ * @param {string} [opts.betterstackSourceToken] - BetterStack OTLP source token
45
+ * @param {string} [opts.betterstackIngestUrl] - BetterStack OTLP ingest host/URL
46
+ * @param {string} [opts.betterstackErrorDsn] - BetterStack error-app Sentry DSN
47
+ * @param {boolean} [opts.betterstackForce] - Force BetterStack on dev/test channels
44
48
  * @param {Object} opts.ablyToken - Ably token object
45
49
  * @param {string} opts.channelName - Ably channel name
46
50
  * @param {string} [opts.sessionId] - Session ID for distributed tracing (traceId = MD5(sessionId))
47
51
  * @returns {Object} Agent config to serialize as JSON
48
52
  */
49
- function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel, ablyToken, channelName, sessionId }) {
53
+ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel, betterstackSourceToken, betterstackIngestUrl, betterstackErrorDsn, betterstackForce, ablyToken, channelName, sessionId }) {
50
54
  return {
51
55
  sandboxId,
52
56
  apiRoot,
@@ -54,6 +58,10 @@ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnviron
54
58
  sentryDsn: sentryDsn || undefined,
55
59
  sentryEnvironment: sentryEnvironment || 'production',
56
60
  sentryChannel: sentryChannel || undefined,
61
+ betterstackSourceToken: betterstackSourceToken || undefined,
62
+ betterstackIngestUrl: betterstackIngestUrl || undefined,
63
+ betterstackErrorDsn: betterstackErrorDsn || undefined,
64
+ betterstackForce: betterstackForce || undefined,
57
65
  sessionId: sessionId || undefined,
58
66
  ably: {
59
67
  token: ablyToken,
@@ -2,16 +2,16 @@
2
2
  "$schema": "./examples-manifest.schema.json",
3
3
  "examples": {
4
4
  "assert.test.mjs": {
5
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f56846eeed49c011dd38",
6
- "lastUpdated": "2026-08-19T00:14:32.187Z"
5
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798a278a75a86fc4b64f4",
6
+ "lastUpdated": "2026-08-21T00:15:30.566Z"
7
7
  },
8
8
  "drag-and-drop.test.mjs": {
9
9
  "url": "https://console.testdriver.ai/runs/69a62b3aaa712ecd3dea730a/69a62b42fc0ac3cc632a918b",
10
10
  "lastUpdated": "2026-03-03T00:32:25.275Z"
11
11
  },
12
12
  "exec-pwsh.test.mjs": {
13
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5581504d895610bbca4",
14
- "lastUpdated": "2026-08-19T00:14:51.304Z"
13
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a87989378a75a86fc4b64f1",
14
+ "lastUpdated": "2026-08-21T00:15:48.924Z"
15
15
  },
16
16
  "match-image.test.mjs": {
17
17
  "url": "https://console-test.testdriver.ai/runs/69c8738614b73310c7839412/69c8738c14b73310c783941d",
@@ -22,78 +22,78 @@
22
22
  "lastUpdated": "2026-03-03T00:32:25.282Z"
23
23
  },
24
24
  "hover-text-with-description.test.mjs": {
25
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f55c46eeed49c011dd35",
26
- "lastUpdated": "2026-08-19T00:14:54.264Z"
25
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a879896b7cddebeab9bfbee",
26
+ "lastUpdated": "2026-08-21T00:15:51.746Z"
27
27
  },
28
28
  "windows-installer.test.mjs": {
29
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f55d1504d895610bbca5",
30
- "lastUpdated": "2026-08-19T00:20:18.039Z"
29
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a879898b7cddebeab9bfbef",
30
+ "lastUpdated": "2026-08-21T00:20:29.119Z"
31
31
  },
32
32
  "exec-output.test.mjs": {
33
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f55e5834570b701a4866",
34
- "lastUpdated": "2026-08-19T00:15:30.355Z"
33
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798993062df0fb9b85595",
34
+ "lastUpdated": "2026-08-21T00:16:27.019Z"
35
35
  },
36
36
  "chrome-extension.test.mjs": {
37
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5561504d895610bbca3",
38
- "lastUpdated": "2026-08-19T00:14:49.438Z"
37
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798913062df0fb9b85592",
38
+ "lastUpdated": "2026-08-21T00:15:47.131Z"
39
39
  },
40
40
  "launch-vscode-linux.test.mjs": {
41
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f55a5834570b701a4864",
42
- "lastUpdated": "2026-08-19T00:15:25.390Z"
41
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a87989441d49e9ea55bd199",
42
+ "lastUpdated": "2026-08-21T00:16:21.995Z"
43
43
  },
44
44
  "hover-image.test.mjs": {
45
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5605834570b701a4867",
46
- "lastUpdated": "2026-08-19T00:14:24.482Z"
45
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a87989bb7cddebeab9bfbf0",
46
+ "lastUpdated": "2026-08-21T00:15:23.027Z"
47
47
  },
48
48
  "installer.test.mjs": {
49
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f56246eeed49c011dd36",
50
- "lastUpdated": "2026-08-19T00:14:26.478Z"
49
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a87989d78a75a86fc4b64f3",
50
+ "lastUpdated": "2026-08-21T00:15:24.990Z"
51
51
  },
52
52
  "type.test.mjs": {
53
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5641504d895610bbca8",
54
- "lastUpdated": "2026-08-19T00:15:35.394Z"
53
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a87989fb7cddebeab9bfbf2",
54
+ "lastUpdated": "2026-08-21T00:16:31.968Z"
55
55
  },
56
56
  "press-keys.test.mjs": {
57
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5661504d895610bbca9",
58
- "replayUrl": "https://console-test.testdriver.ai/replay/6a84f82c1504d895610bbd4f?share=gvyKbIHOKiMqcVLAXfPhpg",
59
- "embedUrl": "https://console-test.testdriver.ai/replay/6a84f82c1504d895610bbd4f?share=gvyKbIHOKiMqcVLAXfPhpg&embed=true",
60
- "lastUpdated": "2026-08-19T00:26:25.444Z"
57
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798a13062df0fb9b85597",
58
+ "replayUrl": "https://console-test.testdriver.ai/replay/6a879adf3062df0fb9b855fc?share=FFAdwAvK0aNJAa3xPOtWA",
59
+ "embedUrl": "https://console-test.testdriver.ai/replay/6a879adf3062df0fb9b855fc?share=FFAdwAvK0aNJAa3xPOtWA&embed=true",
60
+ "lastUpdated": "2026-08-21T00:25:08.572Z"
61
61
  },
62
62
  "scroll-keyboard.test.mjs": {
63
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f56c5834570b701a4869",
64
- "lastUpdated": "2026-08-19T00:15:42.176Z"
63
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798a6b7cddebeab9bfbf4",
64
+ "lastUpdated": "2026-08-21T00:16:38.650Z"
65
65
  },
66
66
  "scroll.test.mjs": {
67
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5721504d895610bbcac",
68
- "lastUpdated": "2026-08-19T00:15:15.384Z"
67
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798ac3062df0fb9b8559c",
68
+ "lastUpdated": "2026-08-21T00:16:12.199Z"
69
69
  },
70
70
  "scroll-until-image.test.mjs": {
71
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f56d1504d895610bbcaa",
72
- "lastUpdated": "2026-08-19T00:14:37.902Z"
71
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798a73062df0fb9b8559a",
72
+ "lastUpdated": "2026-08-21T00:15:35.988Z"
73
73
  },
74
74
  "prompt.test.mjs": {
75
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f56f46eeed49c011dd3a",
76
- "lastUpdated": "2026-08-19T00:15:45.483Z"
75
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798a93062df0fb9b8559b",
76
+ "lastUpdated": "2026-08-21T00:16:41.842Z"
77
77
  },
78
78
  "focus-window.test.mjs": {
79
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5701504d895610bbcab",
80
- "lastUpdated": "2026-08-19T00:15:46.880Z"
79
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798aa78a75a86fc4b64f6",
80
+ "lastUpdated": "2026-08-21T00:16:43.205Z"
81
81
  },
82
82
  "captcha-api.test.mjs": {
83
83
  "url": "https://console.testdriver.ai/runs/698f7df69e27ce1528d7d087/698f7fb0d3b320ad547d9d44",
84
84
  "lastUpdated": "2026-02-13T19:55:05.951Z"
85
85
  },
86
86
  "element-not-found.test.mjs": {
87
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5745834570b701a486a",
88
- "lastUpdated": "2026-08-19T00:15:17.069Z"
87
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798ae3062df0fb9b8559d",
88
+ "lastUpdated": "2026-08-21T00:16:13.841Z"
89
89
  },
90
90
  "formatted-logging.test.mjs": {
91
91
  "url": "https://console-test.testdriver.ai/runs/69c8738614b73310c7839412/69c873a714b73310c7839450",
92
92
  "lastUpdated": "2026-03-29T00:36:10.628Z"
93
93
  },
94
94
  "hover-text.test.mjs": {
95
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f5765834570b701a486b",
96
- "lastUpdated": "2026-08-19T00:15:18.750Z"
95
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798afb7cddebeab9bfbf5",
96
+ "lastUpdated": "2026-08-21T00:16:15.554Z"
97
97
  },
98
98
  "no-provision.test.mjs": {
99
99
  "url": "https://console.testdriver.ai/runs/69a62b3aaa712ecd3dea730a/69a62b7706a177a05bccd1cf",
@@ -138,12 +138,12 @@
138
138
  "lastUpdated": "2026-02-13T19:55:05.953Z"
139
139
  },
140
140
  "findall-coffee-icons.test.mjs": {
141
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f56a46eeed49c011dd39",
142
- "lastUpdated": "2026-08-19T00:14:33.969Z"
141
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798a43062df0fb9b85599",
142
+ "lastUpdated": "2026-08-21T00:15:32.294Z"
143
143
  },
144
144
  "parse.test.mjs": {
145
- "url": "https://console-test.testdriver.ai/runs/6a84f5551504d895610bbca1/6a84f57746eeed49c011dd3b",
146
- "lastUpdated": "2026-08-19T00:15:20.419Z"
145
+ "url": "https://console-test.testdriver.ai/runs/6a87988fb7cddebeab9bfbea/6a8798b178a75a86fc4b64f7",
146
+ "lastUpdated": "2026-08-21T00:16:17.187Z"
147
147
  },
148
148
  "flake-diffthreshold-001.test.mjs": {
149
149
  "url": "https://console.testdriver.ai/runs/69a62b3aaa712ecd3dea730a/69a62bcafc0ac3cc632a91aa",
package/docs/docs.json CHANGED
@@ -41,6 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "/v7/what-is-testdriver",
44
+ "/v7/agent",
44
45
  {
45
46
  "group": "Deployment",
46
47
  "icon": "server",
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: "The TestDriver Agent"
3
+ sidebarTitle: "Agent"
4
+ description: "How the TestDriver agent behaves on GitHub issues, pull requests, and @mentions"
5
+ icon: "robot"
6
+ ---
7
+
8
+ The TestDriver agent is an AI teammate that lives in your GitHub repository. It's
9
+ **code review that runs your app** — instead of only reading a diff and guessing
10
+ what might break, it starts a real sandbox, drives the real app, and reports what
11
+ actually happens.
12
+
13
+ ## Scope: it only builds and maintains tests
14
+
15
+ The agent is focused. The one thing it does is **write, debug, fix, and maintain
16
+ automated tests** with TestDriver, and review pull requests by writing and running
17
+ a test against the change. It will politely decline off-scope work — it won't edit
18
+ your application code, fix product bugs, answer unrelated programming questions, or
19
+ act as a general coding assistant.
20
+
21
+ ## How it behaves depends on the surface
22
+
23
+ The agent reacts to three different GitHub surfaces, and its behavior is
24
+ noticeably different on each.
25
+
26
+ | Surface | Trigger | What it does |
27
+ |---------|---------|--------------|
28
+ | **Pull request opened** | Automatic (no mention) | Silent, evidence-based **code review** |
29
+ | **Issue opened** | Automatic (no mention) | **Onboarding / test-building** |
30
+ | **@mention** (issue or PR) | You mention the bot | Interactive, chatty conversation |
31
+
32
+ ## Pull requests — automatic code review
33
+
34
+ When a pull request is **opened**, the agent reviews it automatically — nobody has
35
+ to ask.
36
+
37
+ - **The review is the deliverable, not chatter.** An automatic review is
38
+ unsolicited, so the agent stays quiet while it works and posts a **single review
39
+ comment** at the end rather than scattering inline threads across the diff or
40
+ narrating each step.
41
+ - **It runs the change when it can.** Its default first move is to start a session,
42
+ work out what the PR changes from a user's point of view, and write and run a
43
+ TestDriver test that exercises exactly that. A bug it *watched happen* is far
44
+ more valuable than one it inferred.
45
+ - **Findings are labeled by how they were found.** Each finding is marked
46
+ `observed` (proven by actually running the change) or `suspected` (reasoned out
47
+ from reading the diff). If the installation isn't signed in to TestDriver, the
48
+ agent can't run anything, so it reviews by reading and marks every finding
49
+ `suspected`.
50
+ - **It focuses on real bugs.** Logic errors, regressions, broken edge cases,
51
+ mishandled errors — not style, naming, or "consider extracting this."
52
+ - **Zero findings is a good review.** If the change looks correct, it says so in a
53
+ sentence. If there's nothing worth reviewing (only lockfiles, generated files, or
54
+ formatting), it posts nothing at all.
55
+ - **It comments, it never blocks.** The review is always a `COMMENT` — it never
56
+ approves and never requests changes, so it can't gate a merge.
57
+
58
+ It also fires a native GitHub **Check Run** for the PR's TestDriver suite, and
59
+ re-runs that check on every push (not a full re-review — push more commits and
60
+ @mention the agent if you want it to review again).
61
+
62
+ <Note>
63
+ The agent never reviews its own pull requests. PRs opened by the bot are skipped,
64
+ so it won't wake itself up to review the tests it just committed.
65
+ </Note>
66
+
67
+ ## Issues — onboarding and test building
68
+
69
+ When an issue is **opened**, the agent treats it as a cue to help you build a test.
70
+
71
+ - **If the issue describes a task**, it acts on the request — writing or debugging
72
+ the test you asked for.
73
+ - **If the issue is empty**, it introduces itself, explains that it writes and
74
+ debugs computer-use tests, and walks you through signing in to TestDriver and
75
+ creating your first test.
76
+ - **It finishes by opening a pull request** with the test it wrote.
77
+
78
+ ## @mentions — interactive conversation
79
+
80
+ Mentioning the bot (on either an issue or a PR) starts a **solicited**, interactive
81
+ turn. Because you asked directly, the agent is chatty: it posts progress updates,
82
+ shows you screenshots of what's happening in the sandbox, and answers follow-ups in
83
+ the thread.
84
+
85
+ On a PR, an @mention overrides the silent auto-review behavior — you'll see the full
86
+ play-by-play instead of just the final review comment.
87
+
88
+ You can also manage the agent from a comment:
89
+
90
+ - **`@bot logout`** (or "sign out") revokes the installation's shared TestDriver
91
+ sign-in and tears down the live sandbox, so the next mention will prompt whoever
92
+ responds to sign in again.
93
+
94
+ ## Signing in
95
+
96
+ Reading a diff needs nothing, but **running** the app needs a TestDriver session,
97
+ which requires the installation to be signed in. Sign-in state doesn't decide
98
+ *whether* the agent reviews — it decides *how good* the review gets. A signed-out
99
+ install still gets its bugs caught from reading the code; signing in upgrades
100
+ findings from `suspected` to `observed`.
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* assert.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f56846eeed49c011dd38/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798a278a75a86fc4b64f4/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* chrome-extension.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f5561504d895610bbca3/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798913062df0fb9b85592/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* element-not-found.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f5745834570b701a486a/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798ae3062df0fb9b8559d/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -18,7 +18,7 @@ Watch this test execute in a real sandbox environment:
18
18
 
19
19
  {/* findall-coffee-icons.test.mjs output */}
20
20
  <iframe
21
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f56a46eeed49c011dd39/replay"
21
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798a43062df0fb9b85599/replay"
22
22
  width="100%"
23
23
  height="600"
24
24
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* hover-image.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f5605834570b701a4867/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a87989bb7cddebeab9bfbf0/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -18,7 +18,7 @@ Watch this test execute in a real sandbox environment:
18
18
 
19
19
  {/* hover-text-with-description.test.mjs output */}
20
20
  <iframe
21
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f55c46eeed49c011dd35/replay"
21
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a879896b7cddebeab9bfbee/replay"
22
22
  width="100%"
23
23
  height="600"
24
24
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* hover-text.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f5765834570b701a486b/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798afb7cddebeab9bfbf5/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* installer.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f56246eeed49c011dd36/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a87989d78a75a86fc4b64f3/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* launch-vscode-linux.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f55a5834570b701a4864/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a87989441d49e9ea55bd199/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -18,7 +18,7 @@ Watch this test execute in a real sandbox environment:
18
18
 
19
19
  {/* parse.test.mjs output */}
20
20
  <iframe
21
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f57746eeed49c011dd3b/replay"
21
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798b178a75a86fc4b64f7/replay"
22
22
  width="100%"
23
23
  height="600"
24
24
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* press-keys.test.mjs output */}
14
14
  <iframe
15
- src="https://console-test.testdriver.ai/replay/6a84f82c1504d895610bbd4f?share=gvyKbIHOKiMqcVLAXfPhpg&embed=true"
15
+ src="https://console-test.testdriver.ai/replay/6a879adf3062df0fb9b855fc?share=FFAdwAvK0aNJAa3xPOtWA&embed=true"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* scroll-keyboard.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f56c5834570b701a4869/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798a6b7cddebeab9bfbf4/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* scroll.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f5721504d895610bbcac/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a8798ac3062df0fb9b8559c/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -12,7 +12,7 @@ Watch this test execute in a real sandbox environment:
12
12
 
13
13
  {/* type.test.mjs output */}
14
14
  <iframe
15
- src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a84f5641504d895610bbca8/replay"
15
+ src="https://api-test.testdriver.ai/api/v1/testdriver/testcase/6a87989fb7cddebeab9bfbf2/replay"
16
16
  width="100%"
17
17
  height="390"
18
18
  style={{ border: "1px solid #333", borderRadius: "8px" }}
@@ -16,6 +16,15 @@ const environments = require("../lib/environments.json");
16
16
  const Sentry = require("@sentry/node");
17
17
  const chalk = require("chalk");
18
18
 
19
+ // BetterStack telemetry alongside Sentry. Loads from shared/ in the monorepo and
20
+ // from the bundled copy when installed from npm (see lib/betterstack.js).
21
+ let betterstack = null;
22
+ try {
23
+ betterstack = require("../lib/betterstack.js");
24
+ } catch {
25
+ betterstack = null;
26
+ }
27
+
19
28
  // Track if Sentry has been initialized
20
29
  let sentryInitialized = false;
21
30
 
@@ -34,6 +43,12 @@ function initializeSentry() {
34
43
  try {
35
44
  const version = resolveTestDriverVersion() || "unknown";
36
45
 
46
+ // Build BetterStack's span processor first so it attaches to Sentry's OTel
47
+ // provider and mirrors 100% of spans (see betterstack-otel.js).
48
+ const betterstackSpanProcessor = betterstack
49
+ ? betterstack.createSpanProcessor({ service: "vitest", version })
50
+ : null;
51
+
37
52
  Sentry.init({
38
53
  dsn:
39
54
  process.env.SENTRY_DSN ||
@@ -41,7 +56,9 @@ function initializeSentry() {
41
56
  environment: channelConfig.sentryEnvironment,
42
57
  release: version,
43
58
  sampleRate: 0.01,
44
- tracesSampleRate: 0.01,
59
+ tracesSampleRate: 1,
60
+ openTelemetrySpanProcessors: [betterstackSpanProcessor].filter(Boolean),
61
+ beforeSendTransaction: (event) => (Math.random() < 0.01 ? event : null),
45
62
  enableLogs: true,
46
63
  integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
47
64
  initialScope: {
@@ -98,6 +115,12 @@ function initializeSentry() {
98
115
 
99
116
  sentryInitialized = true;
100
117
  logger.debug("Sentry initialized for vitest");
118
+
119
+ if (betterstack) {
120
+ betterstack.init({ service: "vitest", version });
121
+ // Forward errors to BetterStack's error app so releases populate.
122
+ betterstack.initErrorReporting({ service: "vitest", version });
123
+ }
101
124
  } catch (err) {
102
125
  // Sentry init failed - continue without it
103
126
  logger.debug("Failed to initialize Sentry:", err.message);
@@ -0,0 +1,15 @@
1
+ // Loads the shared BetterStack/OTel telemetry module so it's available both in
2
+ // the monorepo (shared/ sibling) and when installed from npm (bundled copy at
3
+ // vendor/betterstack-otel.js, created by the prepack script). BetterStack should
4
+ // always work like Sentry — never silently disabled just because of packaging.
5
+
6
+ let betterstack;
7
+ try {
8
+ // Monorepo location (dev / running from source).
9
+ betterstack = require("../../shared/betterstack-otel");
10
+ } catch (_) {
11
+ // Published package: shared/ is copied into the package by prepack.
12
+ betterstack = require("../vendor/betterstack-otel");
13
+ }
14
+
15
+ module.exports = betterstack;
package/lib/sentry.js CHANGED
@@ -16,6 +16,7 @@ const os = require("os");
16
16
  const { version } = require("../package.json");
17
17
  const { active, sentryEnvironment } = require("./resolve-channel");
18
18
  const logger = require("../agent/lib/logger");
19
+ const betterstack = require("./betterstack");
19
20
 
20
21
  // Store trace contexts per session so concurrent tests don't overwrite each other.
21
22
  // Keys are sessionIds, values are { traceId, sessionId }.
@@ -42,6 +43,9 @@ const isEnabled = () => {
42
43
 
43
44
  if (isEnabled()) {
44
45
  logger.log("Analytics enabled. Set TD_TELEMETRY=false to disable.");
46
+ // Build BetterStack's span processor first so it can attach to Sentry's own
47
+ // OpenTelemetry provider and mirror 100% of spans (see betterstack-otel.js).
48
+ const betterstackSpanProcessor = betterstack.createSpanProcessor({ service: "sdk", version });
45
49
  Sentry.init({
46
50
  dsn:
47
51
  process.env.SENTRY_DSN ||
@@ -49,7 +53,11 @@ if (isEnabled()) {
49
53
  environment: sentryEnvironment,
50
54
  release: version,
51
55
  sampleRate: 0.01,
52
- tracesSampleRate: 0.01,
56
+ // Record 100% of spans so BetterStack gets them all; Sentry ingestion is
57
+ // throttled back to ~1% in beforeSendTransaction below.
58
+ tracesSampleRate: 1,
59
+ openTelemetrySpanProcessors: [betterstackSpanProcessor].filter(Boolean),
60
+ beforeSendTransaction: (event) => (Math.random() < 0.01 ? event : null),
53
61
  enableLogs: true,
54
62
  integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
55
63
  // Set initial context
@@ -99,6 +107,11 @@ if (isEnabled()) {
99
107
  return event;
100
108
  },
101
109
  });
110
+
111
+ // Attach BetterStack after Sentry so it shares the OTel provider and trace ids.
112
+ betterstack.init({ service: "sdk", version });
113
+ // Forward errors to BetterStack's error app so releases populate (release+environment).
114
+ betterstack.initErrorReporting({ service: "sdk", version });
102
115
  }
103
116
 
104
117
  /**
@@ -160,6 +173,14 @@ function captureException(error, context = {}) {
160
173
  }
161
174
  Sentry.captureException(error);
162
175
  });
176
+
177
+ betterstack.captureException(error, {
178
+ tags: {
179
+ ...(context.tags || {}),
180
+ ...(currentSessionId ? { session: currentSessionId } : {}),
181
+ },
182
+ extra: context.extra,
183
+ });
163
184
  }
164
185
 
165
186
  /**
@@ -432,6 +453,7 @@ async function withSpan(options, callback) {
432
453
  */
433
454
  async function flush(timeout = 2000) {
434
455
  if (!isEnabled()) return;
456
+ await betterstack.flush(timeout);
435
457
  await Sentry.flush(timeout);
436
458
  }
437
459
 
@@ -20,6 +20,7 @@ import * as os from "os";
20
20
  import * as path from "path";
21
21
  import { fileURLToPath, pathToFileURL } from "url";
22
22
  import { z } from "zod";
23
+ import { createRequire } from "module";
23
24
  import * as core from "./core/actions.js";
24
25
  import { NoActiveSessionError } from "./core/actions.js";
25
26
  import { resolveE2bTemplateId, resolveOs } from "./env-utils.js";
@@ -111,7 +112,23 @@ if (isSentryEnabled()) {
111
112
  },
112
113
  });
113
114
  }
115
+ let betterstack = null;
116
+ try {
117
+ const req = createRequire(import.meta.url);
118
+ // Monorepo location, falling back to the bundled copy in the published package.
119
+ try {
120
+ betterstack = req(path.join(sdkRoot, "..", "shared", "betterstack-otel.js"));
121
+ }
122
+ catch {
123
+ betterstack = req(path.join(sdkRoot, "vendor", "betterstack-otel.js"));
124
+ }
125
+ betterstack.init({ service: "mcp-server", version });
126
+ }
127
+ catch {
128
+ betterstack = null;
129
+ }
114
130
  function captureException(error, context = {}) {
131
+ betterstack?.captureException(error, context);
115
132
  if (!isSentryEnabled())
116
133
  return;
117
134
  Sentry.withScope((scope) => {
@@ -141,6 +158,8 @@ function setSessionContext(sessionId, sandboxId) {
141
158
  });
142
159
  }
143
160
  async function flushSentry(timeout = 2000) {
161
+ if (betterstack)
162
+ await betterstack.flush(timeout);
144
163
  if (!isSentryEnabled())
145
164
  return;
146
165
  await Sentry.flush(timeout);
@@ -26,6 +26,8 @@ import * as path from "path";
26
26
  import { fileURLToPath, pathToFileURL } from "url";
27
27
  import { z } from "zod";
28
28
 
29
+ import { createRequire } from "module";
30
+
29
31
  import * as core from "./core/actions.js";
30
32
  import { NoActiveSessionError, type ActionResult } from "./core/actions.js";
31
33
  import { resolveE2bTemplateId, resolveOs } from "./env-utils.js";
@@ -134,7 +136,30 @@ if (isSentryEnabled()) {
134
136
  });
135
137
  }
136
138
 
139
+ // BetterStack telemetry alongside Sentry. Loaded from the shared module when
140
+ // available (it lives outside the published SDK package, so guard the require).
141
+ type BetterStack = {
142
+ init: (opts: { service: string; version?: string }) => boolean;
143
+ captureException: (error: unknown, ctx?: { tags?: Record<string, string>; extra?: Record<string, unknown> }) => void;
144
+ flush: (timeout?: number) => Promise<void>;
145
+ };
146
+ let betterstack: BetterStack | null = null;
147
+ try {
148
+ const req = createRequire(import.meta.url);
149
+ // Monorepo location, falling back to the bundled copy in the published package.
150
+ try {
151
+ betterstack = req(path.join(sdkRoot, "..", "shared", "betterstack-otel.js")) as BetterStack;
152
+ } catch {
153
+ betterstack = req(path.join(sdkRoot, "vendor", "betterstack-otel.js")) as BetterStack;
154
+ }
155
+ betterstack.init({ service: "mcp-server", version });
156
+ } catch {
157
+ betterstack = null;
158
+ }
159
+
137
160
  function captureException(error: Error, context: { tags?: Record<string, string>; extra?: Record<string, unknown> } = {}) {
161
+ betterstack?.captureException(error, context);
162
+
138
163
  if (!isSentryEnabled()) return;
139
164
 
140
165
  Sentry.withScope((scope) => {
@@ -166,6 +191,7 @@ function setSessionContext(sessionId: string, sandboxId?: string) {
166
191
  }
167
192
 
168
193
  async function flushSentry(timeout = 2000) {
194
+ if (betterstack) await betterstack.flush(timeout);
169
195
  if (!isSentryEnabled()) return;
170
196
  await Sentry.flush(timeout);
171
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.11.70-test",
3
+ "version": "7.11.71-test",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",
@@ -82,12 +82,22 @@
82
82
  "report": "npm run generate-report && npm run serve-report",
83
83
  "generate:skills": "node scripts/generate-skills.js",
84
84
  "build:mcp": "cd mcp-server && npm ci && npm run build",
85
+ "bundle:shared": "node scripts/bundle-shared.js",
86
+ "prepack": "npm run bundle:shared",
85
87
  "prepublishOnly": "npm run build:mcp"
86
88
  },
87
89
  "author": "",
88
90
  "license": "ISC",
89
91
  "dependencies": {
90
92
  "@modelcontextprotocol/ext-apps": "^1.0.0",
93
+ "@opentelemetry/api": "^1.9.0",
94
+ "@opentelemetry/api-logs": "^0.208.0",
95
+ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
96
+ "@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
97
+ "@opentelemetry/resources": "^2.2.0",
98
+ "@opentelemetry/sdk-logs": "^0.208.0",
99
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
100
+ "@opentelemetry/sdk-trace-node": "^2.2.0",
91
101
  "@modelcontextprotocol/sdk": "^1.24.0",
92
102
  "@npmcli/redact": "^3.2.2",
93
103
  "@oclif/core": "^4.5.0",
@@ -0,0 +1,28 @@
1
+ // Copies shared/ modules that the SDK depends on into the package (vendor/) so
2
+ // they ship in the npm tarball. Without this, requires like
3
+ // ../../shared/betterstack-otel escape the package once installed and throw
4
+ // MODULE_NOT_FOUND. Run from prepack so it happens before `npm pack`/publish.
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const sdkRoot = path.join(__dirname, "..");
10
+ const sharedRoot = path.join(sdkRoot, "..", "shared");
11
+ const vendorRoot = path.join(sdkRoot, "vendor");
12
+
13
+ // Files copied from shared/ into vendor/. betterstack-otel.js tolerates a
14
+ // missing resolve-channel (it falls back to env vars), so only it is required.
15
+ const files = ["betterstack-otel.js"];
16
+
17
+ fs.mkdirSync(vendorRoot, { recursive: true });
18
+
19
+ for (const file of files) {
20
+ const src = path.join(sharedRoot, file);
21
+ const dest = path.join(vendorRoot, file);
22
+ if (!fs.existsSync(src)) {
23
+ console.error(`[bundle-shared] missing source: ${src}`);
24
+ process.exit(1);
25
+ }
26
+ fs.copyFileSync(src, dest);
27
+ console.log(`[bundle-shared] ${file} → vendor/${file}`);
28
+ }
@@ -0,0 +1,470 @@
1
+ // shared/betterstack-otel.js
2
+ //
3
+ // BetterStack (Telemetry) observability that runs ALONGSIDE Sentry.
4
+ //
5
+ // Sentry v10 already registers a global OpenTelemetry TracerProvider + span
6
+ // processor. Rather than stand up a competing provider (which triggers the
7
+ // "duplicate provider" errors noted in api/config/log.js), this module attaches
8
+ // a BetterStack OTLP span processor to whatever TracerProvider is already
9
+ // active — so every span Sentry sees is ALSO exported to BetterStack under the
10
+ // same trace id. When Sentry is disabled we create our own NodeTracerProvider so
11
+ // tracing still works standalone.
12
+ //
13
+ // Logs are exported via the OTel Logs SDK; console.* is bridged to log records
14
+ // to match Sentry's consoleLoggingIntegration behavior (capture all logs).
15
+ //
16
+ // Gating mirrors Sentry exactly: disabled on dev/test channels, PR previews,
17
+ // and when TD_TELEMETRY=false or no BetterStack token is configured.
18
+
19
+ const crypto = require('crypto');
20
+ const os = require('os');
21
+ const path = require('path');
22
+ const { createRequire } = require('module');
23
+
24
+ // Resolve OTel deps from the caller's node_modules (api/, runner/, image-worker/,
25
+ // …) rather than shared/, which has no node_modules of its own — same trick as
26
+ // shared/load-env.js.
27
+ let _callerRequire = require;
28
+ try {
29
+ _callerRequire = createRequire(path.resolve(process.cwd(), 'package.json'));
30
+ } catch (_) {
31
+ // fall back to this module's require
32
+ }
33
+
34
+ let _initialized = false;
35
+ let _enabled = false;
36
+ let _loggerProvider = null;
37
+ let _otelLogger = null;
38
+ let _tracerProvider = null;
39
+ let _resourceAttrs = {};
40
+ // BetterStack span processor, built once and shared between createSpanProcessor()
41
+ // (passed into Sentry.init) and init() (fallback provider).
42
+ let _spanProcessor = null;
43
+ // Secondary Sentry client + scope pointed at BetterStack's error-tracking app.
44
+ // Errors captured through it carry `release`/`environment`, which is what
45
+ // populates the BetterStack Releases tab. Built once by initErrorReporting().
46
+ let _errorClient = null;
47
+ let _errorScope = null;
48
+
49
+ /**
50
+ * Resolve the release channel using the shared helper, tolerating callers that
51
+ * have their own copy of resolve-channel.
52
+ */
53
+ function resolveChannelSafe() {
54
+ try {
55
+ const { resolveChannel } = require('./resolve-channel');
56
+ return resolveChannel();
57
+ } catch (_) {
58
+ return process.env.TD_CHANNEL || process.env.TD_ENV || 'stable';
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Should BetterStack be active? Same rules as Sentry so both backends stay in
64
+ * lockstep: opt-out via TD_TELEMETRY=false, off in dev/test and PR previews,
65
+ * and requires a source token.
66
+ *
67
+ * Escape hatch: TD_BETTERSTACK_FORCE=true enables it on dev/test too (for local
68
+ * verification), still honoring TD_TELEMETRY=false and the token requirement.
69
+ */
70
+ function isEnabled(channel) {
71
+ if (process.env.TD_TELEMETRY === 'false') return false;
72
+ if (!process.env.BETTERSTACK_SOURCE_TOKEN) return false;
73
+ const forced = process.env.TD_BETTERSTACK_FORCE === 'true';
74
+ if (forced) return true;
75
+ if (process.env.IS_PULL_REQUEST === 'true') return false;
76
+ if (channel === 'dev' || channel === 'test') return false;
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * BetterStack ingest endpoint. Users get this from the Telemetry source page
82
+ * (e.g. https://s1234567.eu-nbg-2.betterstackdata.com). We append the standard
83
+ * OTLP/HTTP paths.
84
+ */
85
+ function ingestBase() {
86
+ const raw = process.env.BETTERSTACK_INGEST_URL;
87
+ if (!raw) return null;
88
+ // Tolerate a bare host (no scheme) — BetterStack shows the ingesting host
89
+ // without https://, and the OTLP exporter requires an absolute URL.
90
+ const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
91
+ return withScheme.replace(/\/+$/, '');
92
+ }
93
+
94
+ function authHeaders() {
95
+ return { Authorization: `Bearer ${process.env.BETTERSTACK_SOURCE_TOKEN}` };
96
+ }
97
+
98
+ /**
99
+ * DSN for BetterStack's Sentry-compatible error-tracking app. Errors sent here
100
+ * (unlike OTLP traces/logs) populate the Errors + Releases tabs. Format matches
101
+ * a standard Sentry DSN: https://<token>@<ingest-host>/<application-id>.
102
+ */
103
+ function errorDsn() {
104
+ return process.env.BETTERSTACK_ERROR_DSN || null;
105
+ }
106
+
107
+ /**
108
+ * Build (once) a BetterStack OTLP BatchSpanProcessor for traces.
109
+ *
110
+ * Pass the result into `Sentry.init({ openTelemetrySpanProcessors: [...] })` so
111
+ * it attaches to Sentry's own instrumented TracerProvider and receives every
112
+ * span Sentry's sampler records — the only reliable way to mirror spans on
113
+ * Sentry v10, whose provider has no public addSpanProcessor().
114
+ *
115
+ * Returns null when BetterStack is disabled or OTel trace packages are missing,
116
+ * so callers can spread it safely: `[bs.createSpanProcessor(...)].filter(Boolean)`.
117
+ *
118
+ * @param {object} opts - same shape as init(): { service, version, attributes }
119
+ * @returns {object|null} a BatchSpanProcessor, or null
120
+ */
121
+ function createSpanProcessor(opts = {}) {
122
+ if (_spanProcessor) return _spanProcessor;
123
+
124
+ const channel = resolveChannelSafe();
125
+ if (!isEnabled(channel)) return null;
126
+
127
+ const base = ingestBase();
128
+ if (!base) return null;
129
+
130
+ let tracesModule;
131
+ let otlpTraceModule;
132
+ try {
133
+ tracesModule = _callerRequire('@opentelemetry/sdk-trace-base');
134
+ otlpTraceModule = _callerRequire('@opentelemetry/exporter-trace-otlp-http');
135
+ } catch (_) {
136
+ return null; // OTel trace packages not installed for this service
137
+ }
138
+
139
+ try {
140
+ const { BatchSpanProcessor } = tracesModule;
141
+ const { OTLPTraceExporter } = otlpTraceModule;
142
+ const traceExporter = new OTLPTraceExporter({
143
+ url: `${base}/v1/traces`,
144
+ headers: authHeaders(),
145
+ });
146
+ _spanProcessor = new BatchSpanProcessor(traceExporter);
147
+ return _spanProcessor;
148
+ } catch (_) {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Initialize BetterStack telemetry.
155
+ *
156
+ * @param {object} opts
157
+ * @param {string} opts.service - logical service name (api, runner, sdk, …)
158
+ * @param {string} [opts.version] - service version / release
159
+ * @param {object} [opts.attributes] - extra resource attributes
160
+ * @returns {boolean} whether BetterStack was enabled
161
+ */
162
+ function init(opts = {}) {
163
+ if (_initialized) return _enabled;
164
+ _initialized = true;
165
+
166
+ const channel = resolveChannelSafe();
167
+ if (!isEnabled(channel)) {
168
+ _enabled = false;
169
+ return false;
170
+ }
171
+
172
+ const base = ingestBase();
173
+ if (!base) {
174
+ _enabled = false;
175
+ return false;
176
+ }
177
+
178
+ let resourceModule;
179
+ let tracesModule;
180
+ let logsSdkModule;
181
+ let logsApiModule;
182
+ let otlpTraceModule;
183
+ let otlpLogModule;
184
+ try {
185
+ resourceModule = _callerRequire('@opentelemetry/resources');
186
+ tracesModule = _callerRequire('@opentelemetry/sdk-trace-base');
187
+ logsSdkModule = _callerRequire('@opentelemetry/sdk-logs');
188
+ logsApiModule = _callerRequire('@opentelemetry/api-logs');
189
+ otlpTraceModule = _callerRequire('@opentelemetry/exporter-trace-otlp-http');
190
+ otlpLogModule = _callerRequire('@opentelemetry/exporter-logs-otlp-http');
191
+ } catch (err) {
192
+ // OTel packages not installed for this service — skip silently so the
193
+ // service still boots. Install the @opentelemetry/* deps to enable.
194
+ _enabled = false;
195
+ return false;
196
+ }
197
+
198
+ const { resourceFromAttributes } = resourceModule;
199
+ const { BatchSpanProcessor } = tracesModule;
200
+ const { LoggerProvider, BatchLogRecordProcessor } = logsSdkModule;
201
+ const { OTLPTraceExporter } = otlpTraceModule;
202
+ const { OTLPLogExporter } = otlpLogModule;
203
+
204
+ _resourceAttrs = {
205
+ 'service.name': opts.service || 'unknown',
206
+ 'service.version': opts.version || 'unknown',
207
+ 'deployment.environment': channel,
208
+ 'host.name': os.hostname(),
209
+ ...(opts.attributes || {}),
210
+ };
211
+
212
+ const resource = resourceFromAttributes(_resourceAttrs);
213
+
214
+ // ── Traces ────────────────────────────────────────────────────────────────
215
+ // Preferred path: the service already passed createSpanProcessor() into
216
+ // Sentry.init({ openTelemetrySpanProcessors }), so BetterStack rides Sentry's
217
+ // instrumented provider and gets every recorded span. Nothing to do here.
218
+ //
219
+ // Fallback: no processor handed to Sentry (e.g. Sentry disabled). Stand up our
220
+ // own provider so tracing still works standalone. NOTE: this provider has no
221
+ // auto-instrumentation, so it only exports spans created via the OTel API
222
+ // directly — not automatic HTTP/Express/Mongo spans.
223
+ if (!_spanProcessor) {
224
+ try {
225
+ const traceExporter = new OTLPTraceExporter({
226
+ url: `${base}/v1/traces`,
227
+ headers: authHeaders(),
228
+ });
229
+ _spanProcessor = new BatchSpanProcessor(traceExporter);
230
+
231
+ const { NodeTracerProvider } = _callerRequire('@opentelemetry/sdk-trace-node');
232
+ const provider = new NodeTracerProvider({
233
+ resource,
234
+ spanProcessors: [_spanProcessor],
235
+ });
236
+ provider.register();
237
+ _tracerProvider = provider;
238
+ } catch (_) {
239
+ // Tracing wiring failed — continue with logs only.
240
+ }
241
+ }
242
+
243
+ // ── Logs ────────────────────────────────────────────────────────────────
244
+ try {
245
+ const logExporter = new OTLPLogExporter({
246
+ url: `${base}/v1/logs`,
247
+ headers: authHeaders(),
248
+ });
249
+ _loggerProvider = new LoggerProvider({
250
+ resource,
251
+ processors: [new BatchLogRecordProcessor(logExporter)],
252
+ });
253
+ logsApiModule.logs.setGlobalLoggerProvider(_loggerProvider);
254
+ _otelLogger = _loggerProvider.getLogger(opts.service || 'unknown');
255
+ _bridgeConsole(logsApiModule.SeverityNumber);
256
+ } catch (_) {
257
+ // Logs wiring failed — continue with traces only.
258
+ }
259
+
260
+ _enabled = true;
261
+ return true;
262
+ }
263
+
264
+ /**
265
+ * Mirror console.log/info/warn/error to OTel log records (parity with Sentry's
266
+ * consoleLoggingIntegration). The original console methods still run.
267
+ */
268
+ function _bridgeConsole(SeverityNumber) {
269
+ const levels = [
270
+ ['log', SeverityNumber.INFO, 'INFO'],
271
+ ['info', SeverityNumber.INFO, 'INFO'],
272
+ ['warn', SeverityNumber.WARN, 'WARN'],
273
+ ['error', SeverityNumber.ERROR, 'ERROR'],
274
+ ];
275
+ for (const [method, severityNumber, severityText] of levels) {
276
+ const original = console[method] ? console[method].bind(console) : null;
277
+ if (!original) continue;
278
+ console[method] = (...args) => {
279
+ original(...args);
280
+ try {
281
+ _otelLogger.emit({
282
+ severityNumber,
283
+ severityText,
284
+ body: args
285
+ .map((a) => (typeof a === 'string' ? a : safeStringify(a)))
286
+ .join(' '),
287
+ });
288
+ } catch (_) {
289
+ // Never let logging crash the caller.
290
+ }
291
+ };
292
+ }
293
+ }
294
+
295
+ function safeStringify(value) {
296
+ try {
297
+ if (value instanceof Error) return `${value.name}: ${value.message}`;
298
+ return JSON.stringify(value);
299
+ } catch (_) {
300
+ return String(value);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Stand up a secondary Sentry client aimed at BetterStack's error-tracking app
306
+ * so handled/unhandled errors land there tagged with `release` + `environment`.
307
+ * This is what fills BetterStack's Releases tab — OTLP traces/logs never do.
308
+ *
309
+ * Runs alongside the service's primary Sentry client (Sentry SaaS) without
310
+ * touching it: we build an isolated Client + Scope and only ever capture
311
+ * through that scope. Gating mirrors init(); no-op when disabled, when no
312
+ * BETTERSTACK_ERROR_DSN is set, or when @sentry/node isn't installed.
313
+ *
314
+ * @param {object} opts - { service, version, attributes } (same shape as init())
315
+ * @returns {boolean} whether error reporting was wired up
316
+ */
317
+ function initErrorReporting(opts = {}) {
318
+ if (_errorClient) return true;
319
+
320
+ const channel = resolveChannelSafe();
321
+ if (!isEnabled(channel)) return false;
322
+
323
+ const dsn = errorDsn();
324
+ if (!dsn) return false;
325
+
326
+ let Sentry;
327
+ try {
328
+ Sentry = _callerRequire('@sentry/node');
329
+ } catch (_) {
330
+ // @sentry/node not installed for this service — skip silently.
331
+ return false;
332
+ }
333
+
334
+ try {
335
+ const client = new Sentry.NodeClient({
336
+ dsn,
337
+ environment: channel,
338
+ release: opts.version || 'unknown',
339
+ // This client only forwards errors; traces/logs already go via OTLP.
340
+ tracesSampleRate: 0,
341
+ defaultIntegrations: false,
342
+ integrations: [],
343
+ transport: Sentry.makeNodeTransport,
344
+ stackParser: Sentry.defaultStackParser,
345
+ });
346
+
347
+ const scope = new Sentry.Scope();
348
+ scope.setClient(client);
349
+ scope.setTag('service', opts.service || 'unknown');
350
+ if (opts.attributes) {
351
+ for (const [k, v] of Object.entries(opts.attributes)) scope.setTag(k, String(v));
352
+ }
353
+ client.init();
354
+
355
+ _errorClient = client;
356
+ _errorScope = scope;
357
+ return true;
358
+ } catch (_) {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Record an exception to BetterStack as an ERROR log record (parity with
365
+ * Sentry.captureException). Traces are captured automatically via the shared
366
+ * span processor; this ensures crashes/handled errors are always searchable.
367
+ *
368
+ * Also forwards to BetterStack's error-tracking app (when initErrorReporting()
369
+ * wired up a client) so the error carries `release` + `environment`.
370
+ *
371
+ * @param {Error} error
372
+ * @param {object} [context] - { tags, extra } merged into log attributes
373
+ */
374
+ function captureException(error, context = {}) {
375
+ if (_errorScope) {
376
+ try {
377
+ _errorScope.captureException(error, {
378
+ captureContext: { tags: context.tags, extra: context.extra },
379
+ });
380
+ } catch (_) {
381
+ // never let error reporting crash the caller
382
+ }
383
+ }
384
+ if (!_enabled || !_otelLogger) return;
385
+ try {
386
+ const { SeverityNumber } = _callerRequire('@opentelemetry/api-logs');
387
+ const attributes = { 'exception.type': error?.name, 'exception.message': error?.message };
388
+ if (error?.stack) attributes['exception.stacktrace'] = error.stack;
389
+ if (context.tags) {
390
+ for (const [k, v] of Object.entries(context.tags)) attributes[`tag.${k}`] = String(v);
391
+ }
392
+ if (context.extra) {
393
+ for (const [k, v] of Object.entries(context.extra)) attributes[`extra.${k}`] = safeStringify(v);
394
+ }
395
+ _otelLogger.emit({
396
+ severityNumber: SeverityNumber.ERROR,
397
+ severityText: 'ERROR',
398
+ body: error?.message || String(error),
399
+ attributes,
400
+ });
401
+ } catch (_) {
402
+ // swallow
403
+ }
404
+ }
405
+
406
+ /**
407
+ * W3C traceparent headers derived from a session id, mirroring the SDK's
408
+ * MD5(sessionId) scheme so BetterStack sees the same distributed trace that
409
+ * Sentry does across SDK → API → Runner → Worker.
410
+ *
411
+ * Prefer the active OTel span's real context; fall back to the deterministic
412
+ * session-derived id when no span is active.
413
+ *
414
+ * @param {string} sessionId
415
+ * @returns {object} { traceparent } (empty if nothing to propagate)
416
+ */
417
+ function getTraceparentHeaders(sessionId) {
418
+ try {
419
+ const { trace, context } = _callerRequire('@opentelemetry/api');
420
+ const span = trace.getSpan(context.active());
421
+ const sc = span && span.spanContext();
422
+ if (sc && sc.traceId && sc.spanId) {
423
+ const flags = (sc.traceFlags & 1) ? '01' : '00';
424
+ return { traceparent: `00-${sc.traceId}-${sc.spanId}-${flags}` };
425
+ }
426
+ } catch (_) {
427
+ // fall through to deterministic derivation
428
+ }
429
+
430
+ if (!sessionId) return {};
431
+ const traceId = crypto.createHash('md5').update(sessionId).digest('hex');
432
+ const spanId = crypto.randomBytes(8).toString('hex');
433
+ return { traceparent: `00-${traceId}-${spanId}-01` };
434
+ }
435
+
436
+ /**
437
+ * Flush pending telemetry (call before process exit).
438
+ * @param {number} [timeout]
439
+ */
440
+ async function flush(timeout = 2000) {
441
+ const jobs = [];
442
+ if (_errorClient?.flush) jobs.push(_errorClient.flush(timeout));
443
+ if (!_enabled && jobs.length === 0) return;
444
+ if (_loggerProvider?.forceFlush) jobs.push(_loggerProvider.forceFlush());
445
+ if (_tracerProvider?.forceFlush) jobs.push(_tracerProvider.forceFlush());
446
+ try {
447
+ await Promise.race([
448
+ Promise.all(jobs),
449
+ new Promise((r) => setTimeout(r, timeout)),
450
+ ]);
451
+ } catch (_) {
452
+ // ignore flush errors on shutdown
453
+ }
454
+ }
455
+
456
+ function enabled() {
457
+ return _enabled;
458
+ }
459
+
460
+ module.exports = {
461
+ init,
462
+ initErrorReporting,
463
+ createSpanProcessor,
464
+ captureException,
465
+ getTraceparentHeaders,
466
+ flush,
467
+ enabled,
468
+ isEnabled,
469
+ resolveChannelSafe,
470
+ };
File without changes