@testdriverai/runner 7.8.0-canary.21 → 7.8.0-canary.23

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.
@@ -23,6 +23,7 @@ const Sentry = require('@sentry/node');
23
23
  const http = require('http');
24
24
  const https = require('https');
25
25
  const { EventEmitter } = require('events');
26
+ const { maskCommandPayload } = require('./mask-command');
26
27
 
27
28
  /**
28
29
  * Get the local runner version from package.json.
@@ -54,11 +55,13 @@ async function uploadToS3(apiRoot, apiKey, sandboxId, base64, label = 'screensho
54
55
  const fileName = `${label}-${Date.now()}.png`;
55
56
 
56
57
  // Get presigned upload URL from API
58
+ // Propagate Sentry trace context so this request appears under the active command span
59
+ const traceHeaders = Sentry.getTraceData ? Sentry.getTraceData() : {};
57
60
  const uploadUrlResponse = await httpPost(apiRoot, '/api/v7/runner/upload-url', {
58
61
  apiKey,
59
62
  sandboxId,
60
63
  fileName,
61
- });
64
+ }, traceHeaders);
62
65
 
63
66
  if (!uploadUrlResponse || !uploadUrlResponse.uploadUrl) {
64
67
  console.warn('[ably-service] No upload URL returned for screenshot');
@@ -100,8 +103,12 @@ async function uploadToS3(apiRoot, apiKey, sandboxId, base64, label = 'screensho
100
103
 
101
104
  /**
102
105
  * HTTP POST helper
106
+ * @param {string} apiRoot - API base URL
107
+ * @param {string} path - URL path
108
+ * @param {object} body - JSON body
109
+ * @param {object} [extraHeaders] - Additional headers (e.g. Sentry trace)
103
110
  */
104
- async function httpPost(apiRoot, path, body) {
111
+ async function httpPost(apiRoot, path, body, extraHeaders = {}) {
105
112
  const url = new URL(path, apiRoot);
106
113
  const transport = url.protocol === 'https:' ? https : http;
107
114
 
@@ -110,6 +117,7 @@ async function httpPost(apiRoot, path, body) {
110
117
  method: 'POST',
111
118
  headers: {
112
119
  'Content-Type': 'application/json',
120
+ ...extraHeaders,
113
121
  },
114
122
  }, (res) => {
115
123
  let data = '';
@@ -309,8 +317,15 @@ class AblyService extends EventEmitter {
309
317
  success: true,
310
318
  });
311
319
  this.emit('log', `Command completed: ${type} (requestId=${requestId})`);
320
+ const span = Sentry.getActiveSpan();
321
+ if (span) span.setAttribute('command.success', true);
312
322
  } catch (err) {
313
323
  this.emit('log', `Command failed: ${type} — ${err.message}`);
324
+ const span = Sentry.getActiveSpan();
325
+ if (span) {
326
+ span.setAttribute('command.success', false);
327
+ span.setAttribute('command.error', err.message.slice(0, 256));
328
+ }
314
329
  Sentry.captureException(err);
315
330
  await this._sendResponse({
316
331
  requestId,
@@ -323,9 +338,10 @@ class AblyService extends EventEmitter {
323
338
  };
324
339
 
325
340
  if (sentryTrace) {
341
+ const spanAttributes = maskCommandPayload(message);
326
342
  await Sentry.continueTrace({ sentryTrace, baggage }, () => {
327
343
  return Sentry.startSpan(
328
- { name: `runner.command.${type}`, op: 'runner.dispatch' },
344
+ { name: `runner.command.${type}`, op: 'runner.dispatch', attributes: spanAttributes },
329
345
  executeCommand,
330
346
  );
331
347
  });
package/lib/automation.js CHANGED
@@ -18,6 +18,7 @@ const path = require('path');
18
18
  const fs = require('fs');
19
19
  const os = require('os');
20
20
  const { EventEmitter } = require('events');
21
+ const Sentry = require('@sentry/node');
21
22
 
22
23
  const IS_WINDOWS = process.platform === 'win32';
23
24
  const IS_LINUX = process.platform === 'linux';
@@ -621,10 +622,13 @@ class Automation extends EventEmitter {
621
622
  // Use instance-level apiRoot (passed from runner) with fallback to module-level constant
622
623
  const apiRoot = this._apiRoot || API_ROOT;
623
624
 
625
+ // Propagate Sentry trace context so this request appears under the active command span
626
+ const traceHeaders = Sentry.getTraceData ? Sentry.getTraceData() : {};
627
+
624
628
  // Get presigned URL from API (30s timeout)
625
629
  const response = await fetch(`${apiRoot}/api/v7/runner/upload-url`, {
626
630
  method: 'POST',
627
- headers: { 'Content-Type': 'application/json' },
631
+ headers: { 'Content-Type': 'application/json', ...traceHeaders },
628
632
  body: JSON.stringify({
629
633
  apiKey,
630
634
  sandboxId,
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Keys that carry distributed-tracing or internal plumbing data and should
5
+ * never appear as Sentry span attributes.
6
+ */
7
+ const STRIP_KEYS = new Set([
8
+ 'sentryTrace',
9
+ 'baggage',
10
+ ]);
11
+
12
+ /**
13
+ * Keys whose values may contain credentials, tokens or other secrets.
14
+ * Values are replaced with a length indicator + truncated preview.
15
+ */
16
+ const SENSITIVE_KEYS = new Set([
17
+ 'text', // write command — could be a password
18
+ 'command', // exec/run — could embed env vars / tokens
19
+ ]);
20
+
21
+ const MAX_PREVIEW = 80;
22
+ const MAX_VALUE = 256;
23
+
24
+ /**
25
+ * Mask a single value so it's safe for Sentry span attributes.
26
+ * Returns a Sentry-compatible primitive (string | number | boolean).
27
+ */
28
+ function maskValue(key, value) {
29
+ if (value == null) return undefined;
30
+
31
+ // Sensitive fields → length + truncated preview
32
+ if (SENSITIVE_KEYS.has(key) && typeof value === 'string') {
33
+ const preview = value.length > MAX_PREVIEW
34
+ ? value.slice(0, MAX_PREVIEW) + '…'
35
+ : value;
36
+ return `[${value.length} chars] ${preview}`;
37
+ }
38
+
39
+ // Primitives pass through (truncate long strings)
40
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
41
+ if (typeof value === 'string') {
42
+ return value.length > MAX_VALUE ? value.slice(0, MAX_VALUE) + '…' : value;
43
+ }
44
+
45
+ // Arrays / objects → JSON-stringify for readability in Sentry UI
46
+ try {
47
+ const json = JSON.stringify(value);
48
+ return json.length > MAX_VALUE ? json.slice(0, MAX_VALUE) + '…' : json;
49
+ } catch {
50
+ return String(value);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Turn a raw Ably command message into a flat attribute map safe for
56
+ * Sentry span attributes.
57
+ *
58
+ * - Strips distributed-tracing headers (sentryTrace, baggage)
59
+ * - Masks sensitive fields (text, command)
60
+ * - Converts non-primitive values to JSON strings
61
+ * - Prefixes every key with `command.`
62
+ *
63
+ * @param {object} message Raw command message from Ably
64
+ * @returns {Record<string, string|number|boolean>}
65
+ */
66
+ function maskCommandPayload(message) {
67
+ if (!message || typeof message !== 'object') return {};
68
+
69
+ const attrs = {};
70
+ for (const [key, value] of Object.entries(message)) {
71
+ if (STRIP_KEYS.has(key)) continue;
72
+ const masked = maskValue(key, value);
73
+ if (masked !== undefined) {
74
+ attrs[`command.${key}`] = masked;
75
+ }
76
+ }
77
+ return attrs;
78
+ }
79
+
80
+ module.exports = { maskCommandPayload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/runner",
3
- "version": "7.8.0-canary.21",
3
+ "version": "7.8.0-canary.23",
4
4
  "description": "TestDriver Runner - Ably-based remote automation agent with Node.js automation",
5
5
  "main": "index.js",
6
6
  "bin": {