Skip to content

fix(o11y): batch the event flush so command-heavy specs stop hitting spec_timeout [SDK-7399] - #1177

Open
shivam5643 wants to merge 7 commits into
masterfrom
fix/sdk-7399-o11y-flush-skips-tests
Open

fix(o11y): batch the event flush so command-heavy specs stop hitting spec_timeout [SDK-7399]#1177
shivam5643 wants to merge 7 commits into
masterfrom
fix/sdk-7399-o11y-flush-skips-tests

Conversation

@shivam5643

@shivam5643 shivam5643 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What is this about?

Customers on 1.35.x / 1.36.x see a large share of their tests reported as skipped; the same suite on 1.32.8 is 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_timeout and every test that has not run yet is reported as skipped.

build-info on a reproducing build:

test_status:   { failed: 0, success: 6, queued: 0, ignored: 27, pending: 0 }
spec_timeout:  900000 ms          duration: 993 s
sessions:      status=error, 980 / 984 / 986 s

failed: 0 is the tell. A thrown error always marks a test failing; nothing failed here. The sessions ran to ~985s against a 900s spec_timeout and were killed.

Why they run that long. The queue flush issues one cy.task per queued event, and each cy.task round-trip costs roughly 0.8s on a remote terminal. Measured there, 6 tests per spec, N events per afterEach:

events per afterEach cy.task calls session duration
1 6 113s
10 60 114s
100 600 581s
1000 6000 killed at spec_timeout

A command-heavy test queues hundreds of events (command:start + command:end + platform_details per command, plus log:changed), so its afterEach alone 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.8 is unaffected. It dispatches via cy.now('task', ...), which throws immediately on Cypress 14 with no IPC round-trip. 1.32.8 is fast because its dispatch does nothing — it also delivers no browser-side telemetry there. v1.33.0 moved dispatch into the internal hooks and onto cy.task, which genuinely delivers but costs ~0.8s per event.

Related Jira task/s

  • SDK-7399

Dependent PRs / release order

  • Dependent PRs: None — CLI-only, no paired change in railsApp / realMobile / binary.
  • No cross-repo deploy order applies.

Automation cases to add

  • Cypress case asserting a command-heavy spec completes well inside spec_timeout with expected_skipped_number: 0. Existing cypress_cli_and_dashboard.feature rows already assert skipped counts and cover the success path.

Code changes to check

  • Spread operator is not used.
  • Syntax supported by older Node — no ?. / ??.

The change

plugin/index.js — 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/index.js — the flush builds one batch per drain instead of one cy.task per event, split at 512KB so each call stays under the ~1MB per-payload ceiling (also measured on the remote: 768KB passes, 1MB fails). sanitizeForTask caps payloads at 128KB with 8KB string truncation. shouldSkipCommand filters test_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.task call 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.session per test, per-test retry overrides, fixture chains in before()). Same suite and settings both arms; only the CLI dependency differs. Patched arm confirmed to ship a fresh dependency bundle, not a cached one.

CLI spec verdicts tests run duration build
unpatched 1.36.9 all 3 passed_with_skipped 6 success / 27 ignored 993s automate
this change all 3 passed 33 success / 0 ignored 213s automate · o11y

Telemetry still delivers — checked explicitly, because a passing spec cannot distinguish delivered from dropped. test_observability_batch accepts 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:

  • minor (backwards-compatible feature)
  • patch (bug fix or other small change)

Release notes type:

  • Bug Fix

Release notes (customer-facing):

  • Fixed tests being incorrectly reported as skipped when Test Observability is enabled. Observability data is now sent in batches, so specs no longer run past their spec timeout on command-heavy tests.

Release notes (internal):

  • bin/testObservability/cypress/index.js + bin/testObservability/plugin/index.js: the beforeEach/afterEach event-queue flush now sends one batched cy.task per drain (new test_observability_batch task) instead of one cy.task per 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 at spec_timeout, reporting unrun tests as skipped — the SDK-7399 symptom. Introduced in v1.33.0 when dispatch moved into these hooks and onto cy.task.
  • Batches are split at 512KB and per-event payloads capped at 128KB (8KB string truncation) to stay under the ~1MB per-cy.task ceiling. Skipped events are logged rather than dropped silently.

Checklist

  • Ready to review
  • Has it been approved by a member of your team?
  • Verified end-to-end on BrowserStack infra (build links above)
  • Telemetry delivery verified separately from the pass/fail result
  • Required automation cases noted in description

Reviewer notes

Environment gotchas found while verifying, worth knowing for any future o11y change:

  • A CLI branch must resolve on both the local machine and the remote Windows terminal. A git-branch dependency works (repo is public); a local file: path or a package_config_options.scripts.postinstall pointing at a local path does not — run_settings ships verbatim to the remote (capabilityHelper.js:131), so the remote re-runs it and fails with NPM_INSTALL_FAILED.
  • BrowserStack caches the dependency bundle by the generated package.json md5. Re-testing a changed patch under an unchanged dependency string silently reuses the old bundle ("Skipping the upload of node_modules…"). Add a bare cacheBust key to force a fresh upload.
  • browserstack-cypress build-info <buildId> is the reliable way to see test_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, since cy.now('task') throws on Cypress 14 in every context). Both are superseded by this change; history retained deliberately.

🤖 Generated with Claude Code

…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>
@shivam5643
shivam5643 requested a review from a team as a code owner August 26, 2026 16:29
@shivam5643

shivam5643 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

PR Review: browserstack-cypress-cli PR #1177

Summary

Intent: Add a real try/catch + .catch() error boundary around the test-observability event flush in bin/testObservability/cypress/index.js, and switch the dispatch call from cy.task to cy.now('task', ...), so that an instrumentation failure inside the beforeEach/afterEach mocha hooks can no longer throw and make mocha skip every remaining test in the spec (SDK-7399).
Risk: Medium
1 critical · 0 warnings · 0 suggestions | Files reviewed: 1

═══════════════════════════════════════════════════════════════

Findings

Populated mechanically from unified-findings.json — the union of every unit reviewer's output, never hand-merged.

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)

# Finding File · Symbol Confidence
1 [Graceful Degradation] All three swallow points in flushEventsQueue drop errors with no diagnostic signal at all bin/testObservability/cypress/index.js · flushEventsQueue 🟢

1. [Graceful Degradation] All three swallow points in flushEventsQueue drop errors with no diagnostic signal at allbin/testObservability/cypress/index.js · flushEventsQueue

Problem:
flushEventsQueue() has three places that intentionally swallow an error to protect the customer's run: the async .catch(() => {}) on the cy.now promise, the per-event catch (e), and the outer function-level catch (e) that resets the whole queue. All three are correctly scoped — each isolates one bad event / one bad flush from the rest, which is exactly what this PR needs to satisfy the graceful-degradation contract — but none of them leaves any trace that an event was dropped. Not even a console.warn.

This PR exists specifically because a prior silent-failure mode (a thrown instrumentation error making mocha skip every remaining test) went undiagnosed for a full release cycle before a customer noticed. The fix as written trades that loud failure for a new, completely silent one: an observability event can now fail to reach the dashboard with zero signal anywhere that it happened. The next time this recurs — a different cy.now('task', ...) failure mode, a malformed payload, whatever — nobody will know until someone notices missing data in TestHub/O11y, which is a much harder thing to notice than a customer's suite breaking.

const result = cy.now('task', event.task, payload, event.options);
if (result && typeof result.catch === 'function') result.catch(() => {});
} catch (e) {
  /* one bad event must not stop the remaining events, and must not fail the hook */
}

Suggested Fix:
Add a bare console.warn/console.error inside each of the three catches, e.g. console.warn('[browserstack] failed to flush test-observability event', e). Do not route the failure through another Cypress command (cy.task/cy.now) — that risks reintroducing the exact enqueue-time failure this PR is fixing. A plain console.* call keeps the guarantee that instrumentation can never fail the hook, while leaving a log line an engineer can grep for the next time events go missing.

Confidence: 🟢 — grounded against the default.md graceful-degradation rule (an error boundary must still surface the failure somewhere — debug log, telemetry, or status — its exemption is only for a catch that does log at debug) and verified objectively against the diff: none of the three catch bodies contains any logging call, only code comments.

───────────────────────────────────────────────────────────────

🟠 Warnings (Blocking)

None found — the region-by-region walk of both hook call sites (beforeEach, afterEach) surfaced no other defect.

💡 Suggestions (Non-blocking)

None.

═══════════════════════════════════════════════════════════════

External Services

No external-contract changes detected. (No finding in this PR carries contract_change: true; the change is entirely internal to the CLI's Cypress-hook flush path and does not alter any request/response shape, endpoint, or wire protocol.)

═══════════════════════════════════════════════════════════════

Per-File Confidence (for reviewers)

File Status Reason
bin/testObservability/cypress/index.js 🔴 Author to Fix 1 1 grounded (kb-high) finding — swallowed errors with no diagnostic signal

═══════════════════════════════════════════════════════════════

What's Good

  • Both beforeEach and afterEach flush sites are updated identically to call the same flushEventsQueue() helper — the fix isn't applied asymmetrically to only one of the two hooks.
  • The per-event try/catch inside flushEventsQueue isolates one bad event so the rest of the queued batch still flushes, and the outer try/catch stops a catastrophic failure from ever reaching the mocha hook — the core "instrumentation must never fail the hook" contract is satisfied on every path.
  • The switch from cy.task to cy.now('task', ...) is deliberate and well-documented in the PR body: cy.task enqueues a command that drains later (still inside the hook's failure window), while cy.now executes immediately and returns a promise that can actually be caught here — this is the crux of the fix, not a cosmetic change.

═══════════════════════════════════════════════════════════════

Verdict

🔴 Fix 1 blocking issueflushEventsQueue's three swallow points drop instrumentation errors with no diagnostic signal at all (Critical, grounded).

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>
@shivam5643

shivam5643 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed in 82cf3f9 — valid finding, the guard was silent.

All three suppression points now log via warnFlushFailure(stage, err), naming the stage and failing task. Used console.warn as suggested (not cy.task — that would reintroduce the contained failure); matches the existing convention in accessibility-automation/cypress/index.js.

Guarantee re-verified after the change: 11-test spec with a failing dispatch in afterEach → 11 passing, 0 skipped.

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>
@shivam5643

Copy link
Copy Markdown
Collaborator Author

Good to go

File Status Reason
bin/testObservability/cypress/index.js ✅ All Clear Covered — all 3 regions judged clean; 1 non-blocking Suggestion noted separately, does not affect this status

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
Loading

↻ 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

@shivam5643
shivam5643 requested review from harshit-browserstack and removed request for pri-gadhiya August 27, 2026 08:51
rounak610
rounak610 previously approved these changes Aug 27, 2026
@shivam5643

Copy link
Copy Markdown
Collaborator Author

⚠️ Please hold this PR — do not merge yet. Further verification found a problem with my own change.

What I found. The fix swaps cy.task(...)cy.now('task', ...). On a real remote Windows 11 / Chrome 136 machine, cy.now('task', ...) throws for every task, in every context (test body, mocha hook, and Cypress.on listener), while cy.task(...) delivers correctly:

dispatch remote result
cy.task(...) ✅ delivers
cy.now('task', ...) (this PR) ❌ throws — Cannot read properties of undefined (reading 'get') from runPrivilegedCommand

So this PR stops the skipping by never dispatching anything. Browser-side observability events (test_observability_command / _log / _step / _platform_details) would silently stop reaching the dashboard. My earlier green builds could not tell "contained a genuine error" from "sent nothing" — both produce a passing run.

Side note that reframes the baseline: 1.32.8 dispatches only via cy.now, so on Cypress 14 it delivers no browser-side telemetry either — its throws land in listeners where they are harmless. So this PR is effectively 1.32.8 parity: no skipping, no browser-side telemetry. That is a product trade-off, not just a technical one, since customers on 1.36.x get that telemetry today.

Two other approaches tested and rejected:

  • Error boundary only, keep cy.task — does not fix it (1 failing + 10 skipped). cy.task enqueues; the failure lands during queue drain, outside the try/catch.
  • Keep cy.task + scoped cy.on('fail') filterdangerous. Looked ideal (11 passing, 0 skipped), but a safety check with the customer's own failing assertion in afterEach reported 3 passing / 0 failing — our failing cy.task aborts the hook's command queue, so their real assertion never ran and the failure was hidden. Strictly worse than the original bug.

Still confirmed and unaffected by all this: the root cause (a throw inside the internal beforeEach/afterEach flush makes mocha skip every remaining test in the spec) and the regression window (v1.33.0 moved dispatch into those hooks and dropped the per-call .catch() guards).

Suggested direction. The reporter already runs an HTTP server on 127.0.0.1:REPORTER_API_PORT_NO with CORS enabled (reporter/index.js:267,295-307, used today by accessibility-automation/plugin/index.js). Posting queued events browser→node over that channel bypasses Cypress commands entirely, so a dispatch failure could never fail a mocha hook and delivery is preserved. That is a design change rather than a hotfix and needs a maintainer's call.

Happy to take direction on whether to pursue that here or open a fresh PR.

@shivam5643 shivam5643 changed the title fix(o11y): never let queue-flush instrumentation skip the customer's tests [SDK-7399] [HOLD - do not merge] fix(o11y): never let queue-flush instrumentation skip the customer's tests [SDK-7399] Aug 27, 2026
…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>
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>
@shivam5643 shivam5643 changed the title [HOLD - do not merge] fix(o11y): never let queue-flush instrumentation skip the customer's tests [SDK-7399] fix(o11y): batch the event flush so command-heavy specs stop hitting spec_timeout [SDK-7399] Aug 27, 2026
@shivam5643

Copy link
Copy Markdown
Collaborator Author

Hold lifted. The earlier cy.now approach is superseded — the diagnosis in this PR has been rewritten and the fix reverified.

Correction to my earlier comments: the bug is not a thrown error. build-info on a reproducing build reports failed: 0, success: 6, ignored: 27 with sessions at ~985s against a 900s spec_timeout. Nothing throws — the spec is killed at spec_timeout and every test not yet run is reported as skipped.

Cause: the flush issued one cy.task per queued event, and each round-trip costs ~0.8s on a remote terminal (measured: 600 calls → 581s; 6000 calls → killed). A command-heavy test queues hundreds of events, so its afterEach ran for minutes.

Fix: batch the flush into a single cy.task per drain. Verified before writing the code — the same 600 events as one call took 109s vs 581s as 600 calls, i.e. baseline.

Result on the 3-spec repro: passed_with_skipped / 6 of 33 tests run / 993s → passed / 33 of 33 / 213s.

Telemetry delivery was checked separately this time, since a passing spec cannot distinguish delivered from dropped: test_observability_batch accepts a mixed batch of all four event types, a 600-event batch, and a batch containing an unknown task name, all without failing.

@shivam5643
shivam5643 removed the request for review from kamal-kaur04 August 27, 2026 14:56
shivamku-BS and others added 2 commits August 27, 2026 20:37
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants