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 +12 -2
- package/agent/lib/provision-commands.js +9 -1
- package/docs/_data/examples-manifest.json +42 -42
- package/docs/docs.json +1 -0
- package/docs/v7/agent.mdx +100 -0
- package/docs/v7/examples/assert.mdx +1 -1
- package/docs/v7/examples/chrome-extension.mdx +1 -1
- package/docs/v7/examples/element-not-found.mdx +1 -1
- package/docs/v7/examples/findall-coffee-icons.mdx +1 -1
- package/docs/v7/examples/hover-image.mdx +1 -1
- package/docs/v7/examples/hover-text-with-description.mdx +1 -1
- package/docs/v7/examples/hover-text.mdx +1 -1
- package/docs/v7/examples/installer.mdx +1 -1
- package/docs/v7/examples/launch-vscode-linux.mdx +1 -1
- package/docs/v7/examples/parse.mdx +1 -1
- package/docs/v7/examples/press-keys.mdx +1 -1
- package/docs/v7/examples/scroll-keyboard.mdx +1 -1
- package/docs/v7/examples/scroll.mdx +1 -1
- package/docs/v7/examples/type.mdx +1 -1
- package/interfaces/vitest-plugin.mjs +24 -1
- package/lib/betterstack.js +15 -0
- package/lib/sentry.js +23 -1
- package/mcp-server/dist/server.mjs +19 -0
- package/mcp-server/src/server.ts +26 -0
- package/package.json +11 -1
- package/scripts/bundle-shared.js +28 -0
- package/vendor/betterstack-otel.js +470 -0
- /package/{examples → manual}/chrome-extension.test.mjs +0 -0
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/
|
|
6
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
14
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
26
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
30
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
34
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
38
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
42
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
46
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
50
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
54
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
58
|
-
"replayUrl": "https://console-test.testdriver.ai/replay/
|
|
59
|
-
"embedUrl": "https://console-test.testdriver.ai/replay/
|
|
60
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
64
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
68
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
72
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
76
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
80
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
88
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
96
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
142
|
-
"lastUpdated": "2026-08-
|
|
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/
|
|
146
|
-
"lastUpdated": "2026-08-
|
|
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
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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
|
-
|
|
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);
|
package/mcp-server/src/server.ts
CHANGED
|
@@ -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.
|
|
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
|