Phase 03 · v1.0 · Shipped

Capture integrity.

What gets captured must be correct and isolated. GA4 stores every observed event (no pre-drop), captured events are scoped to the current flow, and request listeners detach precisely per scenario so events cannot cross-attribute between flows.

Milestonev1.0 ScopeVAL-01, VAL-06, VAL-07 Depends onPhase 2 Research-gatedQ3
Phase boundary

Capture, not matching. eventStorage shape unchanged.

Not the matching change (Phases 5/6). Not failureKind (Phase 7). Not externalized event mapping (Phase 4). The eventStorage SHAPE is unchanged — additive-only global constraint.

Decisions

Three changes — one of them research-gated.

VAL-01 · GA4 store-all-then-classify

Delete the allow-list gate.

At src/utils/handlerUtils.ts:56, the gate was:

if (en && (en.includes('page') || en.includes('gallery') || en.includes('player') || en.includes('video') || en.includes('articlePart')))

Removed. When en is present, unconditionally call processEventInMemory(mediaName, 'GA4', en, functionName). Classification stays where it is — this phase doesn't touch matching.

(Codex re-discovered the equivalent allow-list on the DataLayer path in May 2026 — that becomes Phase 12 Z1.)

VAL-07 · Per-scenario listener detach

Named handler + scoped detach() return.

The anonymous context.on('request', ...) closure (paired with a blanket removeAllListeners) is replaced by a named handler and a returned detach(). Every robot call site swaps its blanket removal for the scoped detach.

Wired across 6 robots: homepage, pagination, gallery, article-parts, video, dynamic.

Q3 · In-flight wait preservation · research-gated

Don't lose late-arriving GA4 collect requests.

The current removeAllListeners("request", { behavior: "wait" }) WAITS for in-flight handlers to settle. Naive context.off('request', handler) removes immediately and may drop late events.

Decision (post-research): Option C — scoped async detach. attachRequestHandler returns an async detach() that awaits a short settle / network-idle before off. Single highest-risk decision in the phase.

Success criteria
  1. A captured GA4 event whose en doesn't contain any old allow-list substring (e.g. stream_start, cta_click) lands in eventStorage.<media>.<fn>.GA4. Regression-tested.
  2. Each robot pairs its attachRequestHandler with the returned detach(); no blanket removeAllListeners remains.
  3. Late-arriving GA4 collect requests after a flow's last action still get captured (in-flight-drain test).
  4. Cross-flow leakage prevented: events from flow N do not appear in flow N+1's bucket (flow-isolation regression test).
  5. eventStorage shape unchanged byte-for-byte.
Files

What shipped.

modsrc/utils/handlerUtils.tsremove GA4 allow-list, named handler + detach()
modsrc/robots/{homepage,pagination,gallery,article-parts,video,dynamic}.tsscoped detach per attach
newtests/capture-integrity.test.jsstore-all + in-flight-drain + flow-isolation
← PreviousPhase 2 · Missing-row visibility Next →Phase 4 · BI-readable event mapping