testdriverai 7.8.0-test.67 → 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 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 from a session ID.
20
- * Both sandbox.js and sdk.js duplicated this — it now lives here.
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");
@@ -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
- var readyTimeout = isE2B ? 300000 : 120000; // 5 min for E2B (S3+npm), 2 min for EC2
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 = 120000; // 120sallows for SSM provisioning + agent startup
736
+ var readyTimeout = 180000; // 180sWindows 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 crypto = require("crypto");
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
- * This links CLI errors/logs to the same trace as API calls
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
- // Store per-session trace context for concurrent safety
194
- _traceContexts.set(sessionId, { traceId, sessionId });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.8.0-test.67",
3
+ "version": "7.8.0-test.69",
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",
@@ -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
+ );