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.
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.
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.)
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.
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.
en doesn't contain any old allow-list substring (e.g. stream_start, cta_click) lands in eventStorage.<media>.<fn>.GA4. Regression-tested.attachRequestHandler with the returned detach(); no blanket removeAllListeners remains.collect requests after a flow's last action still get captured (in-flight-drain test).eventStorage shape unchanged byte-for-byte.