@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.
- package/lib/ably-service.js +19 -3
- package/lib/automation.js +5 -1
- package/lib/mask-command.js +80 -0
- package/package.json +1 -1
package/lib/ably-service.js
CHANGED
|
@@ -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 };
|