Skip to content

Commit a0ef0be

Browse files
shivamku-BSclaude
andcommitted
fix(o11y): prevent oversized cy.task payloads instead of switching dispatch [SDK-7399]
Replaces the earlier cy.now approach, which was wrong: cy.now('task', ...) throws on Cypress 14 in every context (test body, mocha hook and Cypress.on listener), verified on a remote Windows terminal, so it stopped the skipping only by never delivering anything. Browser-side telemetry silently disappeared from the dashboard. Dispatch therefore stays on cy.task, which does deliver. Since cy.task enqueues, its failure surfaces after the enqueue call returns and cannot be caught at the call site -- so the protection is to never build a payload that fails. Measured on a remote Windows terminal, single event per afterEach: 64KB pass 128KB pass 256KB pass 512KB pass 768KB pass 1MB FAIL 8MB FAIL Event count is not a factor: 10, 100 and 1000 small events all pass. The limit is a hard ceiling near 1MB per cy.task payload. sanitizeForTask now caps the serialized payload at 128KB: individual strings longer than 8KB are truncated first (command args are the realistic source of bulk), and the event is skipped only if it is still too large. Skips are logged rather than silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 937f2d9 commit a0ef0be

1 file changed

Lines changed: 49 additions & 13 deletions

File tree

bin/testObservability/cypress/index.js

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,43 @@ const getCircularReplacer = () => {
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.
3434
*/
35+
/*
36+
* [SDK-7399] An oversized cy.task payload fails the command, and because the flush runs
37+
* inside a mocha hook that failure skips every remaining test in the spec. Measured on a
38+
* remote Windows terminal: a single 64KB payload succeeds, 1MB and 8MB fail; event COUNT
39+
* is not the problem (1000 small events succeed). Command args are the realistic source
40+
* of bulk, so cap individual strings first and only drop the event if it is still too
41+
* large. Preventing the oversized dispatch is what keeps the customer's suite intact —
42+
* containment alone cannot, since the failure surfaces after the enqueue call returns.
43+
*/
44+
const MAX_TASK_PAYLOAD_CHARS = 128 * 1024;
45+
const MAX_STRING_CHARS = 8 * 1024;
46+
const TRUNCATION_MARKER = '…[browserstack: truncated]';
47+
48+
const getTruncatingReplacer = () => {
49+
const seen = new WeakSet();
50+
return (key, value) => {
51+
if (typeof value === 'string' && value.length > MAX_STRING_CHARS) {
52+
return value.slice(0, MAX_STRING_CHARS) + TRUNCATION_MARKER;
53+
}
54+
if (typeof value === 'object' && value !== null) {
55+
if (seen.has(value)) return '[Circular]';
56+
seen.add(value);
57+
}
58+
return value;
59+
};
60+
};
61+
62+
/* Returns a JSON-safe plain object small enough to ship, or `null` to skip the event. */
3563
const sanitizeForTask = (data) => {
3664
try {
37-
return JSON.parse(JSON.stringify(data, getCircularReplacer()));
65+
let json = JSON.stringify(data, getCircularReplacer());
66+
if (json === undefined) return null;
67+
if (json.length > MAX_TASK_PAYLOAD_CHARS) {
68+
json = JSON.stringify(data, getTruncatingReplacer());
69+
if (json === undefined || json.length > MAX_TASK_PAYLOAD_CHARS) return null;
70+
}
71+
return JSON.parse(json);
3872
} catch (e) {
3973
return null;
4074
}
@@ -348,14 +382,15 @@ const warnFlushFailure = (stage, err) => {
348382
};
349383

350384
/*
351-
* [SDK-7399] These flush sites run inside mocha beforeEach/afterEach, so a throw here
352-
* fails the hook and mocha then SKIPS every remaining test in the spec. Before v1.33.0
353-
* the same events were dispatched from Cypress.on(...) listeners — outside any hook,
354-
* each wrapped in .catch() — so a failure was harmless. Keep this boundary intact:
355-
* - cy.now, not cy.task: cy.task enqueues, so its failure surfaces later during queue
356-
* drain and fails the hook regardless of any guard here. cy.now runs immediately.
357-
* - try/catch AND .catch: cy.now can throw synchronously out of Cypress'
358-
* runPrivilegedCommand, which a promise .catch() never sees.
385+
* [SDK-7399] These flush sites run inside mocha beforeEach/afterEach, so a failing
386+
* 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+
* throws on Cypress 14 in every context — test body, hook and listener — so switching to
389+
* it silently drops all browser-side telemetry). Because cy.task enqueues, its failure
390+
* surfaces after this function returns and cannot be caught here; the protection is
391+
* therefore to never build a payload that fails — see sanitizeForTask's size cap. The
392+
* try/catch below remains as a backstop for anything raised synchronously while building
393+
* or enqueuing an event.
359394
*/
360395
const flushEventsQueue = () => {
361396
try {
@@ -364,11 +399,12 @@ const flushEventsQueue = () => {
364399
queued.forEach(event => {
365400
try {
366401
const payload = sanitizeForTask(event.data);
367-
if (payload === null) return;
368-
const result = cy.now('task', event.task, payload, event.options);
369-
if (result && typeof result.catch === 'function') {
370-
result.catch(err => warnFlushFailure(`async dispatch of '${event.task}'`, err));
402+
if (payload === null) {
403+
warnFlushFailure(`oversized or unserializable payload for '${event.task}'`,
404+
new Error('event skipped'));
405+
return;
371406
}
407+
cy.task(event.task, payload, event.options);
372408
} catch (e) {
373409
warnFlushFailure(`dispatch of '${event.task}'`, e); /* skip one event, not the rest */
374410
}

0 commit comments

Comments
 (0)