testdriverai 7.8.0-test.68 → 7.8.0-test.69
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/index.js +8 -0
- package/agent/lib/http.js +21 -3
- package/agent/lib/sandbox.js +23 -2
- package/agent/lib/sdk.js +1 -20
- package/examples/exec-stream-logs.test.mjs +25 -0
- package/lib/sentry.js +39 -22
- package/package.json +1 -1
- package/test-sentry-span.js +35 -0
package/agent/index.js
CHANGED
|
@@ -212,6 +212,14 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// End the Sentry root session span so the trace is finalized
|
|
216
|
+
try {
|
|
217
|
+
const sentry = require("../lib/sentry");
|
|
218
|
+
sentry.clearSessionTraceContext(this.session && this.session.get());
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// Sentry module may not be available, ignore
|
|
221
|
+
}
|
|
222
|
+
|
|
215
223
|
shouldRunPostrun =
|
|
216
224
|
!this.hasRunPostrun &&
|
|
217
225
|
(shouldRunPostrun || this.cliArgs?.command == "run");
|
package/agent/lib/http.js
CHANGED
|
@@ -16,13 +16,31 @@ const { version } = require("../../package.json");
|
|
|
16
16
|
const USER_AGENT = `TestDriverSDK/${version} (Node.js ${process.version})`;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Generate Sentry distributed-tracing headers
|
|
20
|
-
*
|
|
19
|
+
* Generate Sentry distributed-tracing headers.
|
|
20
|
+
*
|
|
21
|
+
* When Sentry is initialized and a span is active, uses Sentry.getTraceData()
|
|
22
|
+
* so the headers reference the real active span (proper parent-child linkage).
|
|
23
|
+
* Falls back to MD5(sessionId)-based headers when Sentry is not available or
|
|
24
|
+
* has no active span (e.g. TD_TELEMETRY=false).
|
|
21
25
|
*
|
|
22
26
|
* @param {string} sessionId
|
|
23
|
-
* @returns {object} Headers object (empty if no sessionId)
|
|
27
|
+
* @returns {object} Headers object (empty if no sessionId and no active span)
|
|
24
28
|
*/
|
|
25
29
|
function getSentryTraceHeaders(sessionId) {
|
|
30
|
+
// Prefer Sentry's own trace propagation when available
|
|
31
|
+
try {
|
|
32
|
+
const Sentry = require("@sentry/node");
|
|
33
|
+
if (typeof Sentry.getTraceData === "function") {
|
|
34
|
+
const traceData = Sentry.getTraceData();
|
|
35
|
+
if (traceData && traceData["sentry-trace"]) {
|
|
36
|
+
return traceData;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Sentry not available — fall through to manual derivation
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback: derive deterministic trace from session ID
|
|
26
44
|
if (!sessionId) return {};
|
|
27
45
|
const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
28
46
|
const spanId = crypto.randomBytes(8).toString("hex");
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -570,13 +570,16 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
570
570
|
if (needsReadyWait) {
|
|
571
571
|
logger.log('Waiting for runner agent to signal readiness...');
|
|
572
572
|
// E2B (Linux) sandboxes need extra time: S3 upload + npm install can add 60-120s on top of sandbox boot
|
|
573
|
-
|
|
573
|
+
// EC2 (Windows) cold starts can be slow due to AV scanning and native module loading
|
|
574
|
+
var readyTimeout = isE2B ? 300000 : 180000; // 5 min for E2B (S3+npm), 3 min for EC2
|
|
574
575
|
await new Promise(function (resolve, reject) {
|
|
575
576
|
var resolved = false;
|
|
577
|
+
var waitStart = Date.now();
|
|
576
578
|
function finish(data) {
|
|
577
579
|
if (resolved) return;
|
|
578
580
|
resolved = true;
|
|
579
581
|
clearTimeout(timer);
|
|
582
|
+
clearInterval(progressTimer);
|
|
580
583
|
self._sessionChannel.unsubscribe('control', onCtrl);
|
|
581
584
|
// Update runner info if provided
|
|
582
585
|
if (data && data.os) reply.runner = reply.runner || {};
|
|
@@ -613,6 +616,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
613
616
|
var timer = setTimeout(function () {
|
|
614
617
|
if (!resolved) {
|
|
615
618
|
resolved = true;
|
|
619
|
+
clearInterval(progressTimer);
|
|
616
620
|
self._sessionChannel.unsubscribe('control', onCtrl);
|
|
617
621
|
var err = new Error('Runner agent did not signal readiness within ' + readyTimeout + 'ms');
|
|
618
622
|
sentry.captureException(err, {
|
|
@@ -624,6 +628,13 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
624
628
|
}, readyTimeout);
|
|
625
629
|
if (timer.unref) timer.unref();
|
|
626
630
|
|
|
631
|
+
// Log progress every 15s so the user knows we're still waiting
|
|
632
|
+
var progressTimer = setInterval(function () {
|
|
633
|
+
if (resolved) return;
|
|
634
|
+
var elapsed = Math.round((Date.now() - waitStart) / 1000);
|
|
635
|
+
logger.log('Still waiting for runner agent... (' + elapsed + 's elapsed, timeout=' + Math.round(readyTimeout / 1000) + 's)');
|
|
636
|
+
}, 15000);
|
|
637
|
+
|
|
627
638
|
// Listen for live runner.ready messages
|
|
628
639
|
var onCtrl;
|
|
629
640
|
onCtrl = function (msg) {
|
|
@@ -722,13 +733,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
722
733
|
var self = this;
|
|
723
734
|
if (reply.agent && this._sessionChannel) {
|
|
724
735
|
logger.log('Waiting for runner agent to signal readiness (direct connection)...');
|
|
725
|
-
var readyTimeout =
|
|
736
|
+
var readyTimeout = 180000; // 180s — Windows cold starts can be slow (AV scanning, native module loading)
|
|
726
737
|
await new Promise(function (resolve, reject) {
|
|
727
738
|
var resolved = false;
|
|
739
|
+
var waitStart = Date.now();
|
|
728
740
|
function finish(data) {
|
|
729
741
|
if (resolved) return;
|
|
730
742
|
resolved = true;
|
|
731
743
|
clearTimeout(timer);
|
|
744
|
+
clearInterval(progressTimer);
|
|
732
745
|
self._sessionChannel.unsubscribe('control', onCtrl);
|
|
733
746
|
logger.log('Runner agent ready (direct, os=' + ((data && data.os) || 'unknown') + ', runner v' + ((data && data.runnerVersion) || 'unknown') + ')');
|
|
734
747
|
if (data && data.update) {
|
|
@@ -749,6 +762,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
749
762
|
var timer = setTimeout(function () {
|
|
750
763
|
if (!resolved) {
|
|
751
764
|
resolved = true;
|
|
765
|
+
clearInterval(progressTimer);
|
|
752
766
|
self._sessionChannel.unsubscribe('control', onCtrl);
|
|
753
767
|
var err = new Error('Runner agent did not signal readiness within ' + readyTimeout + 'ms (direct connection)');
|
|
754
768
|
sentry.captureException(err, {
|
|
@@ -760,6 +774,13 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
760
774
|
}, readyTimeout);
|
|
761
775
|
if (timer.unref) timer.unref();
|
|
762
776
|
|
|
777
|
+
// Log progress every 15s so the user knows we're still waiting
|
|
778
|
+
var progressTimer = setInterval(function () {
|
|
779
|
+
if (resolved) return;
|
|
780
|
+
var elapsed = Math.round((Date.now() - waitStart) / 1000);
|
|
781
|
+
logger.log('Still waiting for runner agent... (' + elapsed + 's elapsed, timeout=' + Math.round(readyTimeout / 1000) + 's)');
|
|
782
|
+
}, 15000);
|
|
783
|
+
|
|
763
784
|
// Listen for live runner.ready messages
|
|
764
785
|
var onCtrl;
|
|
765
786
|
onCtrl = function (msg) {
|
package/agent/lib/sdk.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { events } = require("../events");
|
|
2
|
-
const
|
|
2
|
+
const { getSentryTraceHeaders } = require("./http");
|
|
3
3
|
|
|
4
4
|
// get the version from package.json
|
|
5
5
|
const { version } = require("../../package.json");
|
|
@@ -114,25 +114,6 @@ async function withRetry(fn, options = {}) {
|
|
|
114
114
|
throw lastError;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
* Generate Sentry trace headers for distributed tracing
|
|
119
|
-
* Uses the same trace ID derivation as the API (MD5 hash of session ID)
|
|
120
|
-
* @param {string} sessionId - The session ID
|
|
121
|
-
* @returns {Object} Headers object with sentry-trace and baggage
|
|
122
|
-
*/
|
|
123
|
-
function getSentryTraceHeaders(sessionId) {
|
|
124
|
-
if (!sessionId) return {};
|
|
125
|
-
|
|
126
|
-
// Same logic as API: derive trace ID from session ID
|
|
127
|
-
const traceId = crypto.createHash('md5').update(sessionId).digest('hex');
|
|
128
|
-
const spanId = crypto.randomBytes(8).toString('hex');
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
'sentry-trace': `${traceId}-${spanId}-1`,
|
|
132
|
-
'baggage': `sentry-trace_id=${traceId},sentry-sample_rate=1.0,sentry-sampled=true`
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
117
|
// Factory function that creates SDK with the provided emitter, config, and session
|
|
137
118
|
let token = null;
|
|
138
119
|
const createSDK = (emitter, config, sessionInstance) => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TestDriver } from "../lib/vitest/hooks.mjs";
|
|
3
|
+
import { getDefaults } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
describe("Exec Log Streaming", () => {
|
|
6
|
+
it("should stream exec logs every second for 20 seconds", async (context) => {
|
|
7
|
+
const testdriver = TestDriver(context, { ...getDefaults(context), headless: true });
|
|
8
|
+
await testdriver.provision.chrome({ url: "about:blank" });
|
|
9
|
+
|
|
10
|
+
const code = `for i in $(seq 1 20); do echo "log line $i at $(date +%T)"; sleep 1; done`;
|
|
11
|
+
|
|
12
|
+
const result = await testdriver.exec({
|
|
13
|
+
language: "sh",
|
|
14
|
+
code,
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log("exec result:", result);
|
|
19
|
+
|
|
20
|
+
// Verify we got all 20 log lines
|
|
21
|
+
for (let i = 1; i <= 20; i++) {
|
|
22
|
+
expect(result).toContain(`log line ${i}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
package/lib/sentry.js
CHANGED
|
@@ -180,18 +180,41 @@ function captureMessage(message, level = "info") {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Set the session trace context for distributed tracing
|
|
184
|
-
*
|
|
183
|
+
* Set the session trace context for distributed tracing.
|
|
184
|
+
* Creates a root span (sdk.session) that appears in the Sentry trace waterfall
|
|
185
|
+
* and sets propagation context so all outbound headers (HTTP and Ably) reference
|
|
186
|
+
* the same deterministic traceId = MD5(sessionId).
|
|
187
|
+
*
|
|
185
188
|
* @param {string} sessionId - The session ID
|
|
186
189
|
*/
|
|
187
190
|
function setSessionTraceContext(sessionId) {
|
|
188
191
|
if (!isEnabled() || !sessionId) return;
|
|
189
192
|
|
|
190
|
-
// Derive trace ID from session ID (same algorithm as API)
|
|
193
|
+
// Derive trace ID from session ID (same algorithm as API and Runner)
|
|
191
194
|
const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
|
|
192
195
|
|
|
193
|
-
//
|
|
194
|
-
|
|
196
|
+
// Build a synthetic sentry-trace header to seed the trace with our deterministic ID
|
|
197
|
+
const spanId = crypto.randomBytes(8).toString("hex");
|
|
198
|
+
const sentryTraceHeader = `${traceId}-${spanId}-1`;
|
|
199
|
+
const baggageHeader = `sentry-trace_id=${traceId},sentry-sampled=true`;
|
|
200
|
+
|
|
201
|
+
// continueTrace sets the scope's propagation context so all subsequent
|
|
202
|
+
// getTraceData() calls return our traceId. startInactiveSpan creates a
|
|
203
|
+
// root transaction that will be visible in the Sentry trace waterfall.
|
|
204
|
+
let rootSpan = null;
|
|
205
|
+
Sentry.continueTrace(
|
|
206
|
+
{ sentryTrace: sentryTraceHeader, baggage: baggageHeader },
|
|
207
|
+
() => {
|
|
208
|
+
rootSpan = Sentry.startInactiveSpan({
|
|
209
|
+
name: "sdk.session",
|
|
210
|
+
op: "session",
|
|
211
|
+
forceTransaction: true,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Store per-session trace context (including root span for cleanup)
|
|
217
|
+
_traceContexts.set(sessionId, { traceId, sessionId, rootSpan });
|
|
195
218
|
|
|
196
219
|
// Also update the module-level "latest" for backward compatibility
|
|
197
220
|
currentTraceId = traceId;
|
|
@@ -200,28 +223,17 @@ function setSessionTraceContext(sessionId) {
|
|
|
200
223
|
// Set as global tag so all events include it
|
|
201
224
|
Sentry.setTag("session", sessionId);
|
|
202
225
|
Sentry.setTag("trace_id", currentTraceId);
|
|
203
|
-
|
|
204
|
-
// Try to set propagation context for trace linking (may not be available in all versions)
|
|
205
|
-
try {
|
|
206
|
-
const scope = Sentry.getCurrentScope();
|
|
207
|
-
if (scope && typeof scope.setPropagationContext === "function") {
|
|
208
|
-
scope.setPropagationContext({
|
|
209
|
-
traceId: currentTraceId,
|
|
210
|
-
spanId: currentTraceId.substring(0, 16),
|
|
211
|
-
sampled: true,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
} catch (e) {
|
|
215
|
-
// Ignore errors - propagation context may not be supported
|
|
216
|
-
logger.log("Could not set propagation context:", e.message);
|
|
217
|
-
}
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
/**
|
|
221
|
-
* Clear the session trace context
|
|
229
|
+
* Clear the session trace context and end the root session span.
|
|
222
230
|
*/
|
|
223
231
|
function clearSessionTraceContext(sessionId) {
|
|
224
232
|
if (sessionId) {
|
|
233
|
+
const ctx = _traceContexts.get(sessionId);
|
|
234
|
+
if (ctx && ctx.rootSpan) {
|
|
235
|
+
try { ctx.rootSpan.end(); } catch (e) { /* ignore */ }
|
|
236
|
+
}
|
|
225
237
|
_traceContexts.delete(sessionId);
|
|
226
238
|
// If the cleared session was the "latest", pick another or null
|
|
227
239
|
if (currentSessionId === sessionId) {
|
|
@@ -235,7 +247,12 @@ function clearSessionTraceContext(sessionId) {
|
|
|
235
247
|
}
|
|
236
248
|
}
|
|
237
249
|
} else {
|
|
238
|
-
// Clear all (backward compatibility)
|
|
250
|
+
// Clear all (backward compatibility) — end all root spans
|
|
251
|
+
for (const ctx of _traceContexts.values()) {
|
|
252
|
+
if (ctx.rootSpan) {
|
|
253
|
+
try { ctx.rootSpan.end(); } catch (e) { /* ignore */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
239
256
|
_traceContexts.clear();
|
|
240
257
|
currentTraceId = null;
|
|
241
258
|
currentSessionId = null;
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const Sentry = require('@sentry/node');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
Sentry.init({ dsn: 'https://test@sentry.io/123', tracesSampleRate: 1.0 });
|
|
5
|
+
|
|
6
|
+
const sessionId = 'test-session-123';
|
|
7
|
+
const traceId = crypto.createHash('md5').update(sessionId).digest('hex');
|
|
8
|
+
console.log('expected traceId:', traceId);
|
|
9
|
+
|
|
10
|
+
const sentryTraceHeader = traceId + '-' + crypto.randomBytes(8).toString('hex') + '-1';
|
|
11
|
+
const baggageHeader = 'sentry-trace_id=' + traceId + ',sentry-sampled=true';
|
|
12
|
+
|
|
13
|
+
// Approach: Use continueTrace + startInactiveSpan for the root, then rely on scope
|
|
14
|
+
// for getTraceData propagation
|
|
15
|
+
Sentry.continueTrace(
|
|
16
|
+
{ sentryTrace: sentryTraceHeader, baggage: baggageHeader },
|
|
17
|
+
() => {
|
|
18
|
+
// This sets propagation context on the current scope
|
|
19
|
+
const rootSpan = Sentry.startInactiveSpan({ name: 'sdk.session', op: 'session', forceTransaction: true });
|
|
20
|
+
|
|
21
|
+
// After continueTrace returns, check if getTraceData still uses the right traceId
|
|
22
|
+
const td = Sentry.getTraceData();
|
|
23
|
+
console.log('after continueTrace getTraceData:', JSON.stringify(td));
|
|
24
|
+
console.log('traceId matches:', td['sentry-trace'] && td['sentry-trace'].startsWith(traceId));
|
|
25
|
+
|
|
26
|
+
// Simulate async: do a child span later
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
const td2 = Sentry.getTraceData();
|
|
29
|
+
console.log('async getTraceData:', JSON.stringify(td2));
|
|
30
|
+
console.log('async traceId matches:', td2['sentry-trace'] && td2['sentry-trace'].startsWith(traceId));
|
|
31
|
+
rootSpan.end();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}, 100);
|
|
34
|
+
}
|
|
35
|
+
);
|