Skip to content

Commit ca6aa50

Browse files
Merge branch 'master' into ops/sdk-6279-gha-ci-build-number
2 parents 525192c + 97b2dd8 commit ca6aa50

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

bin/commands/runs.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,15 @@ module.exports = function run(args, rawArgs) {
364364
// stop the Local instance
365365
if (!turboScaleSession) await utils.stopLocalBinary(bsConfig, bs_local, args, rawArgs, buildReportData);
366366

367+
// SDK-6211: send the Test Observability build-stop now — polling has resolved, so
368+
// the build has finished running on BrowserStack. builds_th.finished_at is stamped
369+
// server-side when the collector receives this stop event, so firing it here (before
370+
// the 5s safety wait, artifact download and HTML report generation below) keeps the
371+
// TRA build "Duration" aligned with the test window instead of the full CLI wall-clock.
372+
// printBuildLink no-ops on non-observability runs and is idempotent (buildStopped
373+
// guard); the later handleSyncExit stop becomes a no-op that still honors the exit code.
374+
await printBuildLink(true);
375+
367376
// waiting for 5 secs for upload to complete (as a safety measure)
368377
await new Promise(resolve => setTimeout(resolve, 5000));
369378

bin/testObservability/cypress/index.js

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,40 @@ const STEP_KEYWORDS = ['given', 'when', 'then', 'and', 'but', '*'];
66
let eventsQueue = [];
77
let testRunStarted = false;
88

9+
/*
10+
* Command args (command.attributes.args) and cy.log items are captured raw and can hold
11+
* circular Cypress runtime objects (e.g. a config-like object whose `renderOptions.host`
12+
* points back to itself). cy.task() JSON-serializes its payload to ship it from the browser
13+
* to the Node plugin process, so a circular arg makes Cypress throw
14+
* "Converting circular structure to JSON" and aborts the run. Decycle the payload before
15+
* handing it to cy.task so o11y instrumentation can never break the customer's tests. [SDK-6016]
16+
*/
17+
const getCircularReplacer = () => {
18+
const seen = new WeakSet();
19+
return (key, value) => {
20+
if (typeof value === 'object' && value !== null) {
21+
if (seen.has(value)) return '[Circular]';
22+
seen.add(value);
23+
}
24+
return value;
25+
};
26+
};
27+
28+
/*
29+
* Returns a decycled, JSON-safe plain object, or `null` if the payload still cannot be
30+
* serialized for a non-circular reason (BigInt, a throwing toJSON, a Proxy trap, etc.).
31+
* `null` is a "skip this event" sentinel — callers must NOT forward it to cy.task, because
32+
* the Node o11y handler expects a structured event payload, not an error stub. Skipping keeps
33+
* graceful degradation total: no crash, and no malformed event reaches the collector.
34+
*/
35+
const sanitizeForTask = (data) => {
36+
try {
37+
return JSON.parse(JSON.stringify(data, getCircularReplacer()));
38+
} catch (e) {
39+
return null;
40+
}
41+
};
42+
943
const browserStackLog = (message) => {
1044

1145
if (!Cypress.env('BROWSERSTACK_LOGS')) return;
@@ -208,7 +242,12 @@ Cypress.on('command:enqueued', (attrs) => {
208242
if (args.includes('test_observability_log') || args.includes('test_observability_command')) return;
209243
const message = args.reduce((result, logItem) => {
210244
if (typeof logItem === 'object') {
211-
return [result, JSON.stringify(logItem)].join(' ');
245+
/* Route through sanitizeForTask so a non-circular serialization failure can never
246+
* throw out of the command:enqueued handler (same graceful-degradation contract as
247+
* the flush sites). sanitizeForTask returns a decycled plain object (safe to stringify)
248+
* or null; on null, contribute nothing for this item rather than crash. */
249+
const safeLog = sanitizeForTask(logItem);
250+
return [result, safeLog === null ? '' : JSON.stringify(safeLog)].join(' ');
212251
}
213252
return [result, logItem ? logItem.toString() : ''].join(' ');
214253
}, '');
@@ -309,7 +348,8 @@ beforeEach(() => {
309348

310349
if (eventsQueue.length > 0) {
311350
eventsQueue.forEach(event => {
312-
cy.task(event.task, event.data, event.options);
351+
const payload = sanitizeForTask(event.data);
352+
if (payload !== null) cy.task(event.task, payload, event.options);
313353
});
314354
}
315355
eventsQueue = [];
@@ -324,7 +364,8 @@ afterEach(function() {
324364

325365
if (eventsQueue.length > 0) {
326366
eventsQueue.forEach(event => {
327-
cy.task(event.task, event.data, event.options);
367+
const payload = sanitizeForTask(event.data);
368+
if (payload !== null) cy.task(event.task, payload, event.options);
328369
});
329370
}
330371

bin/testObservability/helper/helper.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,16 @@ const supportFileCleanup = () => {
9393
exports.buildStopped = false;
9494

9595
exports.printBuildLink = async (shouldStopSession, exitCode = null) => {
96-
if(!this.isTestObservabilitySession() || exports.buildStopped) return;
96+
if(!this.isTestObservabilitySession()) return;
97+
// SDK-6211: the build-stop may be sent early (runs.js fires it at poll-resolution, before the
98+
// post-test 5s wait + artifact download + report generation, so builds_th.finished_at — which
99+
// the collector stamps at stop-event receipt — reflects the test window rather than the full CLI
100+
// wall-clock). A later call here must therefore still honor the exit code instead of returning
101+
// silently, preserving the original failing-build exit behaviour.
102+
if(exports.buildStopped) {
103+
if(exitCode) process.exit(exitCode);
104+
return;
105+
}
97106
exports.buildStopped = true;
98107
try {
99108
if(shouldStopSession) {

0 commit comments

Comments
 (0)