Skip to content

Commit f64d61d

Browse files
shivamku-BSclaude
andcommitted
fix(o11y): batch the event flush into one cy.task call [SDK-7399]
The real cause of the reported skipping is a TIMEOUT, not an exception. Nothing throws: build-info on a reproducing build reports failed:0, success:6, ignored:27 with all three sessions status=error at ~985s against spec_timeout 900000ms. The specs are killed at spec_timeout and every test that has not run yet is reported as skipped. Each cy.task round-trip costs roughly 0.8s on a remote terminal. Measured there, 6 tests per spec, N events per afterEach: N=1 -> 6 calls -> 113s N=100 -> 600 calls -> 581s N=10 -> 60 calls -> 114s N=1000 -> 6000 calls -> session killed A command-heavy test queues hundreds of events, so the old one-cy.task-per-event flush spent minutes in the hook and blew the spec budget. Locally the same dispatch is effectively free, which is why every local run passed. Batching verified before writing this: the same 600 events sent as ONE cy.task call completed in 109s versus 581s as 600 calls -- i.e. back to baseline. Changes: - plugin: new test_observability_batch task takes an array and fans out to the same IPC events; the four individual tasks stay registered for backward compatibility. - cypress: the flush builds one batch per drain, split at 512KB so each call stays under the ~1MB per-payload ceiling also measured on the remote (768KB passes, 1MB fails). - shouldSkipCommand filters test_observability_batch, otherwise each batch dispatch would be captured as a command event and refill the queue. - sanitizeForTask keeps the 128KB payload cap with 8KB string truncation. This supersedes the earlier cy.now approach, which stopped the skipping only by never delivering anything: cy.now('task') throws on Cypress 14 in every context, so all browser-side telemetry silently disappeared from the dashboard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a0ef0be commit f64d61d

2 files changed

Lines changed: 63 additions & 6 deletions

File tree

‎bin/testObservability/cypress/index.js‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ const getCircularReplacer = () => {
3131
* `null` is a "skip this event" sentinel — callers must NOT forward it to cy.task, because
3232
* the Node o11y handler expects a structured event payload, not an error stub. Skipping keeps
3333
* graceful degradation total: no crash, and no malformed event reaches the collector.
34-
*/
35-
/*
34+
*
3635
* [SDK-7399] An oversized cy.task payload fails the command, and because the flush runs
3736
* inside a mocha hook that failure skips every remaining test in the spec. Measured on a
3837
* remote Windows terminal: a single 64KB payload succeeds, 1MB and 8MB fail; event COUNT
@@ -84,7 +83,9 @@ const shouldSkipCommand = (command) => {
8483
if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) {
8584
return true;
8685
}
87-
return command.attributes.name == 'log' || (command.attributes.name == 'task' && (['test_observability_platform_details', 'test_observability_step', 'test_observability_command', 'browserstack_log', 'test_observability_log'].some(event => command.attributes.args.includes(event))));
86+
/* test_observability_batch must be filtered here too, or each batch dispatch would
87+
* itself be captured as a command event and refill the queue. [SDK-7399] */
88+
return command.attributes.name == 'log' || (command.attributes.name == 'task' && (['test_observability_platform_details', 'test_observability_step', 'test_observability_command', 'test_observability_batch', 'browserstack_log', 'test_observability_log'].some(event => command.attributes.args.includes(event))));
8889
}
8990

9091
Cypress.on('log:changed', (attrs) => {
@@ -384,18 +385,39 @@ const warnFlushFailure = (stage, err) => {
384385
/*
385386
* [SDK-7399] These flush sites run inside mocha beforeEach/afterEach, so a failing
386387
* dispatch here fails the hook and mocha then SKIPS every remaining test in the spec.
387-
* Dispatch stays on cy.task: it is the only form that actually delivers (cy.now('task')
388+
* Dispatch stays on cy.task (cy.now('task')
388389
* throws on Cypress 14 in every context — test body, hook and listener — so switching to
389390
* it silently drops all browser-side telemetry). Because cy.task enqueues, its failure
390391
* surfaces after this function returns and cannot be caught here; the protection is
391392
* therefore to never build a payload that fails — see sanitizeForTask's size cap. The
392393
* try/catch below remains as a backstop for anything raised synchronously while building
393394
* or enqueuing an event.
394395
*/
396+
/* Keep each batch comfortably under the ~1MB per-cy.task ceiling measured on a remote
397+
* terminal (768KB succeeds, 1MB fails), so a large flush is split rather than dropped. */
398+
const MAX_BATCH_CHARS = 512 * 1024;
399+
395400
const flushEventsQueue = () => {
396401
try {
397402
const queued = eventsQueue;
398403
eventsQueue = []; /* cleared before dispatch so a throw cannot replay these events */
404+
if (queued.length === 0) return;
405+
406+
let batch = [];
407+
let batchChars = 0;
408+
409+
const sendBatch = () => {
410+
if (batch.length === 0) return;
411+
const toSend = batch;
412+
batch = [];
413+
batchChars = 0;
414+
try {
415+
cy.task('test_observability_batch', toSend, { log: false });
416+
} catch (e) {
417+
warnFlushFailure(`batch dispatch of ${toSend.length} event(s)`, e);
418+
}
419+
};
420+
399421
queued.forEach(event => {
400422
try {
401423
const payload = sanitizeForTask(event.data);
@@ -404,11 +426,16 @@ const flushEventsQueue = () => {
404426
new Error('event skipped'));
405427
return;
406428
}
407-
cy.task(event.task, payload, event.options);
429+
const size = JSON.stringify(payload).length;
430+
if (batchChars + size > MAX_BATCH_CHARS) sendBatch();
431+
batch.push({ task: event.task, data: payload });
432+
batchChars += size;
408433
} catch (e) {
409-
warnFlushFailure(`dispatch of '${event.task}'`, e); /* skip one event, not the rest */
434+
warnFlushFailure(`preparing '${event.task}'`, e); /* skip one event, not the rest */
410435
}
411436
});
437+
438+
sendBatch();
412439
} catch (e) {
413440
warnFlushFailure('queue flush', e);
414441
eventsQueue = [];

‎bin/testObservability/plugin/index.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const browserstackTestObservabilityPlugin = (on, config, callbacks) => {
1111

1212
connectIPCClient(config);
1313

14+
const IPC_EVENT_FOR_TASK = {
15+
test_observability_log: IPC_EVENTS.LOG,
16+
test_observability_command: IPC_EVENTS.COMMAND,
17+
test_observability_platform_details: IPC_EVENTS.PLATFORM_DETAILS,
18+
test_observability_step: IPC_EVENTS.CUCUMBER,
19+
};
20+
1421
on('task', {
1522
test_observability_log(log) {
1623
ipc.of.browserstackTestObservability.emit(IPC_EVENTS.LOG, log);
@@ -27,6 +34,29 @@ const browserstackTestObservabilityPlugin = (on, config, callbacks) => {
2734
test_observability_step(log) {
2835
ipc.of.browserstackTestObservability.emit(IPC_EVENTS.CUCUMBER, log);
2936
return null;
37+
},
38+
/*
39+
* [SDK-7399] Accepts a whole flush as ONE task so the browser side issues one
40+
* Cypress command per flush instead of one per event. Each cy.task round-trip costs
41+
* roughly 0.8s on a remote terminal, so a command-heavy test used to spend minutes
42+
* in its afterEach and the spec was killed at spec_timeout, reporting every test that
43+
* had not run yet as skipped. Measured: 600 events as 600 calls = 581s; the same 600
44+
* events as 1 call = 109s, i.e. baseline.
45+
* Fans out to exactly the same IPC events as the individual tasks above, which stay
46+
* registered for backward compatibility.
47+
*/
48+
test_observability_batch(events) {
49+
if (!Array.isArray(events)) return null;
50+
events.forEach((event) => {
51+
try {
52+
const ipcEvent = event && IPC_EVENT_FOR_TASK[event.task];
53+
if (!ipcEvent) return;
54+
ipc.of.browserstackTestObservability.emit(ipcEvent, event.data);
55+
} catch (e) {
56+
/* one malformed event must not drop the rest of the batch */
57+
}
58+
});
59+
return null;
3060
}
3161
});
3262

0 commit comments

Comments
 (0)