testdriverai 7.11.71-test → 7.11.73-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 +2 -12
- package/agent/lib/provision-commands.js +1 -9
- package/interfaces/vitest-plugin.mjs +1 -24
- package/lib/sentry.js +1 -23
- package/mcp-server/dist/server.mjs +0 -19
- package/mcp-server/src/server.ts +0 -26
- package/package.json +1 -11
- package/lib/betterstack.js +0 -15
- package/scripts/bundle-shared.js +0 -28
- package/vendor/betterstack-otel.js +0 -470
package/agent/lib/http.js
CHANGED
|
@@ -27,22 +27,13 @@ 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
|
-
|
|
39
30
|
// Prefer Sentry's own trace propagation when available
|
|
40
31
|
try {
|
|
41
32
|
const Sentry = require("@sentry/node");
|
|
42
33
|
if (typeof Sentry.getTraceData === "function") {
|
|
43
34
|
const traceData = Sentry.getTraceData();
|
|
44
35
|
if (traceData && traceData["sentry-trace"]) {
|
|
45
|
-
return { ...traceData
|
|
36
|
+
return { ...traceData };
|
|
46
37
|
}
|
|
47
38
|
}
|
|
48
39
|
} catch (e) {
|
|
@@ -50,7 +41,7 @@ function getSentryTraceHeaders(sessionId) {
|
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
// Fallback: derive deterministic trace from session ID
|
|
53
|
-
if (!sessionId) return {
|
|
44
|
+
if (!sessionId) return {};
|
|
54
45
|
const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
55
46
|
const spanId = crypto.randomBytes(8).toString("hex");
|
|
56
47
|
return {
|
|
@@ -59,7 +50,6 @@ function getSentryTraceHeaders(sessionId) {
|
|
|
59
50
|
"sentry-trace_id=" +
|
|
60
51
|
traceId +
|
|
61
52
|
",sentry-sample_rate=1.0,sentry-sampled=true",
|
|
62
|
-
...traceparentHeaders,
|
|
63
53
|
};
|
|
64
54
|
}
|
|
65
55
|
|
|
@@ -41,16 +41,12 @@ 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
|
|
48
44
|
* @param {Object} opts.ablyToken - Ably token object
|
|
49
45
|
* @param {string} opts.channelName - Ably channel name
|
|
50
46
|
* @param {string} [opts.sessionId] - Session ID for distributed tracing (traceId = MD5(sessionId))
|
|
51
47
|
* @returns {Object} Agent config to serialize as JSON
|
|
52
48
|
*/
|
|
53
|
-
function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel,
|
|
49
|
+
function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel, ablyToken, channelName, sessionId }) {
|
|
54
50
|
return {
|
|
55
51
|
sandboxId,
|
|
56
52
|
apiRoot,
|
|
@@ -58,10 +54,6 @@ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnviron
|
|
|
58
54
|
sentryDsn: sentryDsn || undefined,
|
|
59
55
|
sentryEnvironment: sentryEnvironment || 'production',
|
|
60
56
|
sentryChannel: sentryChannel || undefined,
|
|
61
|
-
betterstackSourceToken: betterstackSourceToken || undefined,
|
|
62
|
-
betterstackIngestUrl: betterstackIngestUrl || undefined,
|
|
63
|
-
betterstackErrorDsn: betterstackErrorDsn || undefined,
|
|
64
|
-
betterstackForce: betterstackForce || undefined,
|
|
65
57
|
sessionId: sessionId || undefined,
|
|
66
58
|
ably: {
|
|
67
59
|
token: ablyToken,
|
|
@@ -16,15 +16,6 @@ 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
|
-
|
|
28
19
|
// Track if Sentry has been initialized
|
|
29
20
|
let sentryInitialized = false;
|
|
30
21
|
|
|
@@ -43,12 +34,6 @@ function initializeSentry() {
|
|
|
43
34
|
try {
|
|
44
35
|
const version = resolveTestDriverVersion() || "unknown";
|
|
45
36
|
|
|
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
|
-
|
|
52
37
|
Sentry.init({
|
|
53
38
|
dsn:
|
|
54
39
|
process.env.SENTRY_DSN ||
|
|
@@ -56,9 +41,7 @@ function initializeSentry() {
|
|
|
56
41
|
environment: channelConfig.sentryEnvironment,
|
|
57
42
|
release: version,
|
|
58
43
|
sampleRate: 0.01,
|
|
59
|
-
tracesSampleRate:
|
|
60
|
-
openTelemetrySpanProcessors: [betterstackSpanProcessor].filter(Boolean),
|
|
61
|
-
beforeSendTransaction: (event) => (Math.random() < 0.01 ? event : null),
|
|
44
|
+
tracesSampleRate: 0.01,
|
|
62
45
|
enableLogs: true,
|
|
63
46
|
integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
|
|
64
47
|
initialScope: {
|
|
@@ -115,12 +98,6 @@ function initializeSentry() {
|
|
|
115
98
|
|
|
116
99
|
sentryInitialized = true;
|
|
117
100
|
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
|
-
}
|
|
124
101
|
} catch (err) {
|
|
125
102
|
// Sentry init failed - continue without it
|
|
126
103
|
logger.debug("Failed to initialize Sentry:", err.message);
|
package/lib/sentry.js
CHANGED
|
@@ -16,7 +16,6 @@ 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");
|
|
20
19
|
|
|
21
20
|
// Store trace contexts per session so concurrent tests don't overwrite each other.
|
|
22
21
|
// Keys are sessionIds, values are { traceId, sessionId }.
|
|
@@ -43,9 +42,6 @@ const isEnabled = () => {
|
|
|
43
42
|
|
|
44
43
|
if (isEnabled()) {
|
|
45
44
|
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 });
|
|
49
45
|
Sentry.init({
|
|
50
46
|
dsn:
|
|
51
47
|
process.env.SENTRY_DSN ||
|
|
@@ -53,11 +49,7 @@ if (isEnabled()) {
|
|
|
53
49
|
environment: sentryEnvironment,
|
|
54
50
|
release: version,
|
|
55
51
|
sampleRate: 0.01,
|
|
56
|
-
|
|
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),
|
|
52
|
+
tracesSampleRate: 0.01,
|
|
61
53
|
enableLogs: true,
|
|
62
54
|
integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
|
|
63
55
|
// Set initial context
|
|
@@ -107,11 +99,6 @@ if (isEnabled()) {
|
|
|
107
99
|
return event;
|
|
108
100
|
},
|
|
109
101
|
});
|
|
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 });
|
|
115
102
|
}
|
|
116
103
|
|
|
117
104
|
/**
|
|
@@ -173,14 +160,6 @@ function captureException(error, context = {}) {
|
|
|
173
160
|
}
|
|
174
161
|
Sentry.captureException(error);
|
|
175
162
|
});
|
|
176
|
-
|
|
177
|
-
betterstack.captureException(error, {
|
|
178
|
-
tags: {
|
|
179
|
-
...(context.tags || {}),
|
|
180
|
-
...(currentSessionId ? { session: currentSessionId } : {}),
|
|
181
|
-
},
|
|
182
|
-
extra: context.extra,
|
|
183
|
-
});
|
|
184
163
|
}
|
|
185
164
|
|
|
186
165
|
/**
|
|
@@ -453,7 +432,6 @@ async function withSpan(options, callback) {
|
|
|
453
432
|
*/
|
|
454
433
|
async function flush(timeout = 2000) {
|
|
455
434
|
if (!isEnabled()) return;
|
|
456
|
-
await betterstack.flush(timeout);
|
|
457
435
|
await Sentry.flush(timeout);
|
|
458
436
|
}
|
|
459
437
|
|
|
@@ -20,7 +20,6 @@ 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";
|
|
24
23
|
import * as core from "./core/actions.js";
|
|
25
24
|
import { NoActiveSessionError } from "./core/actions.js";
|
|
26
25
|
import { resolveE2bTemplateId, resolveOs } from "./env-utils.js";
|
|
@@ -112,23 +111,7 @@ if (isSentryEnabled()) {
|
|
|
112
111
|
},
|
|
113
112
|
});
|
|
114
113
|
}
|
|
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
|
-
}
|
|
130
114
|
function captureException(error, context = {}) {
|
|
131
|
-
betterstack?.captureException(error, context);
|
|
132
115
|
if (!isSentryEnabled())
|
|
133
116
|
return;
|
|
134
117
|
Sentry.withScope((scope) => {
|
|
@@ -158,8 +141,6 @@ function setSessionContext(sessionId, sandboxId) {
|
|
|
158
141
|
});
|
|
159
142
|
}
|
|
160
143
|
async function flushSentry(timeout = 2000) {
|
|
161
|
-
if (betterstack)
|
|
162
|
-
await betterstack.flush(timeout);
|
|
163
144
|
if (!isSentryEnabled())
|
|
164
145
|
return;
|
|
165
146
|
await Sentry.flush(timeout);
|
package/mcp-server/src/server.ts
CHANGED
|
@@ -26,8 +26,6 @@ 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
|
-
|
|
31
29
|
import * as core from "./core/actions.js";
|
|
32
30
|
import { NoActiveSessionError, type ActionResult } from "./core/actions.js";
|
|
33
31
|
import { resolveE2bTemplateId, resolveOs } from "./env-utils.js";
|
|
@@ -136,30 +134,7 @@ if (isSentryEnabled()) {
|
|
|
136
134
|
});
|
|
137
135
|
}
|
|
138
136
|
|
|
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
|
-
|
|
160
137
|
function captureException(error: Error, context: { tags?: Record<string, string>; extra?: Record<string, unknown> } = {}) {
|
|
161
|
-
betterstack?.captureException(error, context);
|
|
162
|
-
|
|
163
138
|
if (!isSentryEnabled()) return;
|
|
164
139
|
|
|
165
140
|
Sentry.withScope((scope) => {
|
|
@@ -191,7 +166,6 @@ function setSessionContext(sessionId: string, sandboxId?: string) {
|
|
|
191
166
|
}
|
|
192
167
|
|
|
193
168
|
async function flushSentry(timeout = 2000) {
|
|
194
|
-
if (betterstack) await betterstack.flush(timeout);
|
|
195
169
|
if (!isSentryEnabled()) return;
|
|
196
170
|
await Sentry.flush(timeout);
|
|
197
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.11.
|
|
3
|
+
"version": "7.11.73-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,22 +82,12 @@
|
|
|
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",
|
|
87
85
|
"prepublishOnly": "npm run build:mcp"
|
|
88
86
|
},
|
|
89
87
|
"author": "",
|
|
90
88
|
"license": "ISC",
|
|
91
89
|
"dependencies": {
|
|
92
90
|
"@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",
|
|
101
91
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
102
92
|
"@npmcli/redact": "^3.2.2",
|
|
103
93
|
"@oclif/core": "^4.5.0",
|
package/lib/betterstack.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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/scripts/bundle-shared.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,470 +0,0 @@
|
|
|
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
|
-
};
|