fix(o11y): batch the event flush so command-heavy specs stop hitting spec_timeout [SDK-7399] - #1177
fix(o11y): batch the event flush so command-heavy specs stop hitting spec_timeout [SDK-7399]#1177shivam5643 wants to merge 9 commits into
Conversation
…tests [SDK-7399]
Observability events queued during a test are flushed from the internal beforeEach /
afterEach hooks in bin/testObservability/cypress/index.js. Those flush sites have no
error handling, so an instrumentation failure throws inside a mocha hook. A failing hook
makes mocha skip every remaining test in that suite, and those tests are then reported
as skipped even though the customer never skipped them.
Regression window: up to v1.32.8 the same events were dispatched from Cypress.on(...)
listeners -- outside any mocha hook -- and each dispatch was wrapped in .catch(), so an
instrumentation failure could not affect the run. v1.33.0 moved the dispatch into these
hooks and dropped the error handling. Customers on 1.35.x / 1.36.x see large numbers of
tests reported as skipped; the same suite on 1.32.8 is clean.
Measured on an 11-test spec whose afterEach issues one failing task, mirroring the flush
site:
before 1 failing + 10 SKIPPED
"Because this error occurred during a `after each` hook we are skipping the
remaining tests in the current suite"
after 11 passing + 0 skipped
Verified end to end on BrowserStack with an 11-test spec that reproduces the customer's
shape (cy.session per test, per-test retry overrides, fixture chains in before()):
unpatched 1.36.9 -> spec status passed_with_skipped, 985s
this change -> spec status passed, 0 skipped, 163s
Both observed failure modes are handled:
- cy.task / cy.now can throw SYNCHRONOUSLY out of Cypress' runPrivilegedCommand
(TypeError: Cannot read properties of null (reading 'get')). A promise .catch() never
runs for that, so a real try/catch is required -- confirmed by experiment.
- an async rejection, covered by the promise .catch().
cy.now('task', ...) replaces cy.task(...) because cy.task enqueues a Cypress command
whose failure surfaces later while the queue drains, failing the hook regardless of any
guard around the enqueue call. cy.now executes immediately and returns a promise, which
is what v1.32.8 did and is therefore containable.
The queue is also cleared before dispatch so a throw cannot replay the same events on
the next flush.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Review: browserstack-cypress-cli PR #1177SummaryIntent: Add a real try/catch + ═══════════════════════════════════════════════════════════════ FindingsPopulated mechanically from Two channels. Blocking = Critical + Warning — the must-fix set the Verdict gates on. Non-blocking = Suggestions — polish; never gates. An empty Warning/Suggestion section here means examined and clean, not an incomplete review. 🔴 Critical (Blocking)
1. [Graceful Degradation] All three swallow points in
─────────────────────────────────────────────────────────────── 🟠 Warnings (Blocking)None found — the region-by-region walk of both hook call sites ( 💡 Suggestions (Non-blocking)None. ═══════════════════════════════════════════════════════════════ External ServicesNo external-contract changes detected. (No finding in this PR carries ═══════════════════════════════════════════════════════════════ Per-File Confidence (for reviewers)
═══════════════════════════════════════════════════════════════ What's Good
═══════════════════════════════════════════════════════════════ Verdict🔴 Fix 1 blocking issue — Coverage: 3 of 3 regions judged, 0 unjudged. No coverage gap. ═══════════════════════════════════════════════════════════════ — SDK PR Review Agent |
…silently
Review follow-up. The three suppression points in flushEventsQueue (async promise
rejection, per-event throw, whole-flush throw) protected the customer's run but left no
trace at all, so a dropped observability event would only surface later as data quietly
missing from the dashboard -- trading a loud failure for a silent one.
Each now routes through warnFlushFailure(), which names the stage and the failing task.
console.warn is used deliberately rather than browserStackLog/cy.task: routing a
diagnostic through another Cypress command would reintroduce the enqueue-time failure
this change exists to contain. Same convention already used for the equivalent case in
bin/accessibility-automation/cypress/index.js ("suppressed afterEach error").
warnFlushFailure is itself wrapped so logging can never throw.
Re-verified the guarantee is unchanged: 11-test spec with a failing dispatch in afterEach
-> 11 passing, 0 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in 82cf3f9 — valid finding, the guard was silent. All three suppression points now log via Guarantee re-verified after the change: 11-test spec with a failing dispatch in Also trimmed the code comments in 937f2d9 (33 → 12 lines), no logic change. |
Comments outnumbered code roughly 2:1. Kept only what stops the boundary being undone by a later refactor -- why a throw here skips the rest of the spec, why cy.now rather than cy.task, why try/catch as well as .catch, and why console.warn rather than cy.task. No logic change (33 -> 12 comment lines). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
✅ Good to go
Change map (generated deterministically from the diff)graph LR
subgraph ncypress_cli["cypress-cli"]
nbin_testObservability_cypress_index_js["index.js<br/>~57 lines"]
end
↻ This verdict comment is the review anchor — it's updated in place on each run (the gate posts its status separately). — SDK PR Review Agent |
|
What I found. The fix swaps
So this PR stops the skipping by never dispatching anything. Browser-side observability events ( Side note that reframes the baseline: Two other approaches tested and rejected:
Still confirmed and unaffected by all this: the root cause (a throw inside the internal Suggested direction. The reporter already runs an HTTP server on Happy to take direction on whether to pursue that here or open a fresh PR. |
…spatch [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>
a0ef0be
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>
|
Hold lifted. The earlier Correction to my earlier comments: the bug is not a thrown error. Cause: the flush issued one Fix: batch the flush into a single Result on the 3-spec repro: Telemetry delivery was checked separately this time, since a passing spec cannot distinguish delivered from dropped: |
…eds [SDK-7399] The 128KB per-event cap and 8KB string truncation were written for an earlier, wrong theory (that oversized payloads were the cause). Batching is what fixes the timeout, so the truncation fixed nothing and would have changed behaviour for customers who work fine today: any command arg or log string over 8KB would have started arriving truncated. Removed. Nothing under 512KB is altered any more. Kept: the 512KB batch split, which is required -- a batch of many events can otherwise cross the ~1MB per-cy.task ceiling measured on a remote terminal (768KB passes, 1MB fails). Added: a single event larger than 512KB is dropped with a log line instead of being sent. That is not a fidelity regression -- before batching such an event was dispatched on its own and would have failed the command anyway, taking the spec with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7399] The comment above flushEventsQueue still described the earlier payload-cap approach and pointed at "sanitizeForTask's size cap", which no longer exists after abb89b6 removed it. It also never stated batching as the mechanism, which is the actual fix. Rewritten to state the measured cause (~0.8s per cy.task round-trip, N=100 -> 581s, N=1000 -> session killed at spec_timeout, failed:0 so nothing throws) and the measured remedy (600 events as one call: 109s). Also notes why dispatch stays on cy.task, and that the try/catch is only a backstop since a cy.task failure surfaces after the enqueue call. Corrected one log message: sanitizeForTask returning null now means unserializable only, not oversized. Removed an orphan comment line. No functional change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shivam5643
left a comment
There was a problem hiding this comment.
🔴 SDK PR Review — 1 blocking finding
Verdict: 🔴 Author to fix. The batching fix itself is sound and well-evidenced; one line regresses per-event delivery.
What this PR does
Each cy.task round-trip costs ~0.8s on a remote terminal, so the old one-task-per-event flush ran for minutes inside afterEach on command-heavy specs, blew spec_timeout, and reported every not-yet-run test as skipped — with nothing thrown (failed: 0), which is why it never reproduced locally. This batches a hook's whole drain into a single cy.task('test_observability_batch', ...) (600 events: 581s → 109s), fans out plugin-side to the same four IPC events, keeps the per-event tasks registered for back-compat, and excludes the batch dispatch in shouldSkipCommand so it can't recapture itself into the queue.
Blocking
1. MAX_BATCH_CHARS reused as the single-event drop ceiling — bin/testObservability/cypress/index.js:408. Full detail in the inline comment. Short version: the code drops any single event over 512KB, but the constant's own comment records that 768KB succeeds in one cy.task. Events in the 512KB–768KB band were delivered before this PR and are now silently discarded behind a console.warn.
Non-blocking
2. Dead field — every queued event still carries options: { log: false } (index.js:85, :105, :125, :141, :170, and the rest), but post-batching nothing reads it: the outer cy.task('test_observability_batch', toSend, { log: false }) hardcodes its own, and event.options is no longer passed anywhere. Worth removing, or leaving a note on why it's kept.
3. Test coverage — no automated guard on the new flush path (existing unit tests cover helper.js, not this file). The manual before/after infra evidence is the right kind for this bug class — a passing spec alone can't distinguish "delivered" from "silently dropped" — but the BStackAutomation case asserting expected_skipped_number: 0 that the PR description flags as pending is what turns this into a standing regression guard.
What's good
- Both
beforeEachandafterEachroute through the sameflushEventsQueue()— no asymmetric fix. - Clean error boundaries: queue cleared before dispatch so a throw can't replay events, per-event try/catch isolates one bad payload from the rest, outer try/catch backstops the flush, and
warnFlushFailuredeliberately usesconsole.warnrather than another Cypress command. IPC_EVENT_FOR_TASKmaps all four queued task types correctly, and the batch handler guards bothArray.isArrayand unknown task names.- The
cy.now('task', ...)alternative is explicitly ruled out in a comment with the reason (throws on Cypress 14, would "fix" the skipping by delivering nothing) — exactly the kind of rejected-alternative note that saves the next reader a day.
Coverage ledger
All 6 diff regions across both changed files judged — no gap. default + node rule packs applied; the shared observability pack (OB-01/OB-05) applied as supporting judgment only, since this repo isn't one of the six SDKs it mechanically covers. The gRPC/Binary-pairing gate did not fire and correctly so — no .proto or generated-stub touch, and this repo has no Binary contract. Existing PR comments are all author-written and excluded per the author-exclusion rule.
SDK PR Review Agent · head 33a9232 · posted as a COMMENT (GitHub rejects APPROVE/REQUEST_CHANGES on a self-authored PR)
…limit [SDK-7399]
Review finding, and correct: MAX_BATCH_CHARS (512KB) was doing double duty as the
single-event drop ceiling, while the measurement it came from records 768KB succeeding in
one cy.task. Events between 512KB and 768KB were delivered before this PR and were being
discarded behind a console.warn -- a regression introduced by this branch.
Split into two constants: MAX_BATCH_CHARS (512KB) decides when to close a batch,
MAX_EVENT_CHARS (768KB, the largest payload proven to send) decides when an event cannot
be delivered at all. An oversized-but-sendable event now travels alone in its own batch
instead of being dropped.
Also from the review:
- added test/unit/bin/testObservability/batchFlush.js -- 6 cases pinning the fan-out:
batch task registered alongside the per-event tasks, all four types routed to distinct
IPC events in order, 600-entry batch emits 600 times, unknown task name skipped without
losing the rest, malformed input never throws, one failing emit does not drop the
remainder. This is the guard a passing spec cannot provide, since it cannot distinguish
delivered from silently dropped.
- noted at the dispatch site that per-event options are deliberately not forwarded (all
push sites use { log: false }, which the batch call passes once).
Comments across both files trimmed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three addressed in 3ec1263. 1 (blocking) — 2 — dead 3 — test coverage. Added
9 passing with the existing suite. This is the guard a passing spec can't give, since a spec cannot distinguish delivered from silently dropped — which is exactly how an earlier attempt on this branch looked green while delivering nothing. The BStackAutomation case asserting Comments across both files trimmed. |
shivam5643
left a comment
There was a problem hiding this comment.
✅ SDK PR Review — re-review at 3ec1263, blocker resolved
Incremental pass over 33a9232..3ec1263. Good to go.
Finding 1 (Critical) — RESOLVED, verified by trace
index.js:363-364 splits the two thresholds, and index.js:404-407 is the actual fix:
if (batch.length > 0 && batchChars + size > MAX_BATCH_CHARS) sendBatch();
batch.push({ task: event.task, data: payload });
batchChars += size;
if (batchChars >= MAX_BATCH_CHARS) sendBatch();Traced the 600KB case both ways rather than assuming the shape is right. Batch empty: the new batch.length > 0 guard correctly skips the pre-flush, the event is pushed, then line 407 fires (600K ≥ 512K) and dispatches it alone. Batch non-empty: line 404 flushes the accumulated content first, the event lands in a fresh batch, line 407 dispatches it alone. Either way the 512–768KB band now travels instead of being dropped — which is what the finding asked for.
Two invariants the two-constant refactor could plausibly have broken, both checked:
- No batch can exceed the measured-safe ceiling. Line 404 permits an append only when
batchChars + size ≤ 512KB, or when the batch is empty. So a multi-event batch is ≤512KB and a single-event batch is ≤768KB — nothing can be assembled past the point wherecy.taskwas measured to succeed. - No stranded or double-sent event.
batchCharsincrements exactly once per event;sendBatch()clearsbatch/batchCharsbefore dispatch and no-ops on empty — so the unconditional tailsendBatch()at line 413 can neither re-send a flushed batch nor leave the last partial one behind.
Finding 2 (dead options field) — RESOLVED
index.js:381 documents it rather than removing it, and the claim holds — all queued push sites do pass { log: false }. Fine as a documented simplification.
Finding 3 (test coverage) — PARTIALLY RESOLVED, non-blocking
test/unit/bin/testObservability/batchFlush.js adds 6 well-chosen cases — fan-out order, 600-entry batch, unknown task name, malformed input, one-failing-emit isolation. But all six target plugin/index.js's fan-out. The threshold arithmetic in cypress/index.js — the exact logic finding 1 was about — still has no automated guard. A case asserting that a 600KB event produces its own single-entry cy.task call would have caught the original bug and would catch a regression of it; right now that behavior is guarded only by the manual trace above. Worth a follow-up.
Non-blocking before merge
1. Release notes (internal) are stale. The section still reads "Batches are split at 512KB … A single event above that is skipped", and ## The change repeats "before batching it was dispatched alone and would have failed the command anyway". That's the pre-fix threshold plus the justification this commit abandoned — and it contradicts the 768KB-succeeds measurement cited in the same paragraph. It matters more than a description nit because that section is changelog-facing. One-line edit: 512KB → 768KB for the drop ceiling, and drop the "would have failed anyway" clause.
2. Comment asserts more than the data shows — index.js:398: /* unsendable at any size; the per-event flush could not deliver it either */. The measurement establishes only that 768KB passes and 1MB fails; the band between them is untested, so an 800KB event might have gone through pre-PR. Dropping there is the right conservative call — the comment just overstates the evidence for it.
Still clean at the new head
Both hooks symmetric on flushEventsQueue(), all three error boundaries intact (queue cleared before dispatch, per-event isolation, outer backstop), IPC_EVENT_FOR_TASK complete across all four task types, shouldSkipCommand still filtering test_observability_batch.
Coverage ledger
All diff regions in 3ec1263 judged: the two constants + flushEventsQueue/sendBatch (traced in full), the shouldSkipCommand and warnFlushFailure comment trims (no logic change), the plugin/index.js comment trims (no logic change), and all 6 cases in the new test file. No gaps.
SDK PR Review Agent · incremental pass, head 3ec1263 · posted as a COMMENT (GitHub rejects APPROVE/REQUEST_CHANGES on a self-authored PR)
…d comment [SDK-7399] Re-review follow-ups. Added test/unit/bin/testObservability/batchThresholds.js. The previous test file covered the plugin-side fan-out only, leaving the threshold arithmetic in cypress/index.js -- the exact logic the blocking finding was about -- guarded only by a manual trace. Six cases now pin it: small events share one batch, a 600KB event is dispatched alone rather than dropped (the regression), a 600KB event is kept out of the batch holding small ones, an event past the largest measured-safe size is skipped, a multi-event batch never exceeds the split figure, and an empty queue sends nothing. Writing it surfaced a detail worth recording: command:start queues two events (the command plus small platform details), so a single oversized command legitimately produces two batches -- the big event alone, the small one after. The first draft of the test asserted one batch and failed; the code was right. Also softened the drop-path comment. It claimed the event was "unsendable at any size", but the measurement only establishes that 768KB succeeds and 1MB fails -- the band between is untested, so skipping there is a conservative choice rather than a proven necessity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both non-blocking items addressed, plus the coverage gap, in 456295d. 1. Release notes stale. Correct — and changelog-facing, so worth the fix. Both the 2. Comment overstated the evidence. Also correct. 3. Threshold arithmetic now has an automated guard. Added
15 passing across the o11y suite. Worth recording, since it corrected me rather than the code: |
shivam5643
left a comment
There was a problem hiding this comment.
✅ GTG — clear for regression
Final pass at 456295d. All findings from the earlier passes are resolved; nothing outstanding is a defect.
Resolution status
| Finding | Status |
|---|---|
[Critical] MAX_BATCH_CHARS used as both the batch split point and the single-event drop ceiling, silently dropping 512–768KB events that were delivered pre-PR |
✅ Resolved in 3ec1263 — verified by tracing both the empty-batch and non-empty-batch paths, not by the commit message. The band now dispatches alone. |
[Minor] per-event options: { log: false } unread after batching |
✅ Documented at index.js:381; claim verified against all push sites |
| [Minor] comment overstated the measurement | ✅ Softened at index.js:399 to match what the data actually supports |
| [Minor] no automated guard on the flush threshold arithmetic | ✅ batchThresholds.js added; the 600KB event alone case asserts positively and genuinely re-guards the Critical |
| [Docs] stale description / release notes | ✅ Both sections now state the two-threshold split correctly |
Also verified clean at this head: batching preserves end-to-end event order; clearing the queue before dispatch is strictly safer than pre-PR (which had no try/catch around the per-event calls and could throw into the customer's hook); and no re-entrancy path exists — shouldSkipCommand filters the batch task, and { log: false } suppresses the $Log so the dispatch fires no log:changed.
Two follow-ups — deliberately not merge gates
1. Batch accounting undercounts the wire payload. index.js:396 measures JSON.stringify(payload).length, but what is dispatched is an array of { task, data } wrappers — an unaccounted 42–55 bytes per event. On tiny events (retry storms) that is a ~42% undercount: batchChars reads 512KB while ~730KB goes on the wire.
This is not a live failure. 730KB is below the 768KB measured to succeed in a single cy.task, so no input drops or fails on it today — the code's internal margin assumption is simply wider than reality. One-line fix when convenient:
const size = JSON.stringify({ task: event.task, data: payload }).length;2. Two of the six new threshold cases can pass vacuously. batchThresholds.js:94-103 and :105-115 assert only inside forEach over delivered batches, so they run zero assertions if everything is dropped — they cannot fail on over-dropping, which is the Critical's own bug class. A positive lower-bound assertion (totalSent equals the expected count) closes it. The other four cases, including the regression, do assert positively.
Regression
Clear to proceed. Regression is also the right gate for the one thing static review cannot settle — whether real remote-terminal cy.task dispatch behaves as measured. Worth including the command-heavy Cypress case asserting expected_skipped_number: 0 that the description still lists as pending, since that is what proves SDK-7399 fixed end to end.
SDK PR Review Agent · final pass, head 456295d · COMMENT event (GitHub rejects a decision on a self-authored PR)
What is this about?
Customers on
1.35.x/1.36.xsee a large share of their tests reported as skipped; the same suite on1.32.8is clean (SDK-7399: ~113 of ~525 skipped on 1.36.18 vs 4 on 1.32.8).The tests are not skipped by mocha and nothing throws — the spec is killed at
spec_timeoutand every test that has not run yet is reported as skipped.build-infoon a reproducing build:failed: 0is the tell. A thrown error always marks a test failing; nothing failed here. The sessions ran to ~985s against a 900sspec_timeoutand were killed.Why they run that long. The queue flush issues one
cy.taskper queued event, and eachcy.taskround-trip costs roughly 0.8s on a remote terminal. Measured there, 6 tests per spec, N events perafterEach:afterEachcy.taskcallsspec_timeoutA command-heavy test queues hundreds of events (
command:start+command:end+platform_detailsper command, pluslog:changed), so itsafterEachalone runs for minutes. Load-dependent by construction: heavy tests exhaust the spec budget, light tests do not — which is exactly the reported 45-passed / 113-skipped shape rather than all-or-nothing.Why
1.32.8is unaffected. It dispatches viacy.now('task', ...), which throws immediately on Cypress 14 with no IPC round-trip.1.32.8is fast because its dispatch does nothing — it also delivers no browser-side telemetry there.v1.33.0moved dispatch into the internal hooks and ontocy.task, which genuinely delivers but costs ~0.8s per event.Related Jira task/s
Dependent PRs / release order
Automation cases to add
spec_timeoutwithexpected_skipped_number: 0. Existingcypress_cli_and_dashboard.featurerows already assert skipped counts and cover the success path.Code changes to check
?./??.The change
plugin/index.js— newtest_observability_batchtask takes an array and fans out to the same IPC events. The four individual tasks stay registered for backward compatibility.cypress/index.js— the flush builds one batch per drain instead of onecy.taskper event, split at 512KB so each call stays under the ~1MB per-payload ceiling (also measured on the remote: 768KB passes, 1MB fails). Two thresholds, both from the same measurement: batches are closed at 512KB, and a single event above 768KB — the largest payload measured to send — is skipped with a log line. An event between the two still sends, alone in its own batch. Nothing is altered or truncated.shouldSkipCommandfilterstest_observability_batch, without which each batch dispatch would itself be captured as a command event and refill the queue.Batching was verified before the code was written: the same 600 events sent as one
cy.taskcall completed in 109s versus 581s as 600 calls — i.e. back to the ~110s baseline for that spec shape.Verification
Fix, 3-spec repro (suite modelled on the customer's shape:
cy.sessionper test, per-test retry overrides, fixture chains inbefore()). Same suite and settings both arms; only the CLI dependency differs. Patched arm confirmed to ship a fresh dependency bundle, not a cached one, and the git dependency was verified to serve the branch head before running.Current head
456295dpassed_with_skippederror— killed atspec_timeoutpasseddoneVideo was enabled on both arms (
video: true, confirmed in each build'svideo_config). The unpatched session is killed mid-run, so Cypress never finalises the recording; the patched session completes and the video is written — the "video will not load" complaint resolves with the same fix.Earlier run at
f64d61d, before the threshold split — same outcome, which is what shows3ec1263changed nothing for normal-sized eventspassed_with_skippedf64d61dpassedTelemetry still delivers — checked explicitly, because a passing spec cannot distinguish delivered from dropped.
test_observability_batchaccepts a mixed batch of all four event types, a 600-event batch, and a batch containing an unknown task name, all without failing (3/3).Release
Version bump:
Release notes type:
Release notes (customer-facing):
Release notes (internal):
bin/testObservability/cypress/index.js+bin/testObservability/plugin/index.js: thebeforeEach/afterEachevent-queue flush now sends one batchedcy.taskper drain (newtest_observability_batchtask) instead of onecy.taskper event. Each round-trip costs ~0.8s on a remote terminal, so command-heavy tests were spending minutes in the hook and the spec was killed atspec_timeout, reporting unrun tests as skipped — the SDK-7399 symptom. Introduced in v1.33.0 when dispatch moved into these hooks and ontocy.task.test/unit/bin/testObservability/batchFlush.jscovering the batch fan-out and the single-event dispatch threshold.cy.task, 1MB fails): batches are closed at 512KB, and only a single event above 768KB is skipped with a log line. An event between the two is dispatched alone in its own batch rather than dropped. Skipped events are logged rather than dropped silently.Checklist
Reviewer notes
Environment gotchas found while verifying, worth knowing for any future o11y change:
file:path or apackage_config_options.scripts.postinstallpointing at a local path does not —run_settingsships verbatim to the remote (capabilityHelper.js:131), so the remote re-runs it and fails withNPM_INSTALL_FAILED.package.jsonmd5. Re-testing a changed patch under an unchanged dependency string silently reuses the old bundle ("Skipping the upload of node_modules…"). Add a barecacheBustkey to force a fresh upload.browserstack-cypress build-info <buildId>is the reliable way to seetest_status/spec_timeout/ session durations. The o11y ext API returned nulls for these builds and the session-logs URL returns a bot-challenge page.Earlier commits on this branch explored two approaches that were verified and rejected: a reporter-side duplicate-event guard (no effect on a remote A/B), and switching dispatch to
cy.now(stopped the skipping only by never delivering anything, sincecy.now('task')throws on Cypress 14 in every context). Both are superseded by this change; history retained deliberately.🤖 Generated with Claude Code