Skip to content

feat(pixel): capture-phase scroll tracking for SPAs and internal scroll containers (SDK-276)#2871

Merged
bkbooth merged 5 commits into
mainfrom
claude/pedantic-brahmagupta-8035a7
May 11, 2026
Merged

feat(pixel): capture-phase scroll tracking for SPAs and internal scroll containers (SDK-276)#2871
bkbooth merged 5 commits into
mainfrom
claude/pedantic-brahmagupta-8035a7

Conversation

@bkbooth
Copy link
Copy Markdown
Contributor

@bkbooth bkbooth commented May 11, 2026

Summary

  • Replaces the window scroll listener with document.addEventListener('scroll', ..., { capture: true }) so scroll_depth milestones fire on SPA pages where document.documentElement never scrolls but an internal overflow: auto container does (the pattern used on godsunchained.com and most Angular/Vue/React apps)
  • Containers smaller than 50% of the viewport height are ignored to filter out noise from dropdowns, tooltips, and autocomplete lists
  • scroll_depth milestones now reset on each pixel.page() call, so SPA route transitions start with a fresh depth counter — without this, milestones would silently do nothing after the first route in a session
  • Fixes a pre-existing bug where autocapture: { scroll: false } was silently ignored (the scroll option was not being passed through to setupAutocapture)
  • setupScrollTracking returns { teardown, reset } and setupAutocapture returns { teardown, resetScroll } to support the reset path without tearing down and re-attaching all listeners
  • Fixes a race where a scroll event arriving just before page() could schedule a rAF that fires milestones against the freshly-cleared set; reset() now cancels any pending rAF
  • Fixes a case where two large containers scrolling within the same animation frame would drop the first one; replaced pendingEl: Element | null with pending: Set<Element> so all containers in a frame are processed
  • README scroll_depth row updated to document SPA support and reset-on-page behaviour

Test plan

  • All 96 tests pass (pnpm test in packages/audience/pixel)
  • Existing document-scroll tests updated to dispatch on document (correct for capture-phase listener)
  • New: internal container fires milestones
  • New: all five milestones fire at 100% in internal container
  • New: containers ≤ 50% viewport height are filtered out
  • New: global milestone dedup across multiple large containers
  • New: detached element (not in DOM) fires nothing
  • New: resetScroll() allows milestones to re-fire after a simulated SPA route change
  • New: stale rAF cancelled on reset so it cannot fire milestones against a fresh page view
  • New: all containers that scroll in the same frame are processed (Set-based queue)
  • New: pixel.page() calls resetScroll (regression guard for milestone reset on route changes)
  • New: autocapture: { scroll: false } is forwarded to setupAutocapture (regression guard for the scroll-option bug)

Closes SDK-276

🤖 Generated with Claude Code


Note

Medium Risk
Changes scroll auto-capture behavior by moving to capture-phase document listeners, adding container filtering, and resetting milestones on page(), which could affect analytics event volume/semantics in production. Includes broad test updates/additions to cover new scroll targets and reset/rAF edge cases.

Overview
Scroll auto-capture is reworked so scroll_depth milestones fire for both document scrolling and SPA-style internal overflow containers by listening on document in capture phase and processing multiple scrolled elements per animation frame.

Milestones are now reset per page view via a new resetScroll hook that Pixel.page() calls (including cancelling pending rAF work), and small scroll containers (≤ 50% viewport height) are ignored to reduce noise; autocapture.scroll is also correctly forwarded and the public API now returns { teardown, resetScroll } from setupAutocapture.

Docs and tests are updated/expanded accordingly, and @imtbl/pixel version is bumped to 0.2.0.

Reviewed by Cursor Bugbot for commit de0b6d3. Bugbot is set up for automated code reviews on this repo. Configure here.

…ll containers (SDK-276)

Replaces the window-level scroll listener with a single capture-phase
listener on document, so scroll_depth milestones fire on SPA pages where
document.documentElement never scrolls but an internal overflow container
does. Containers smaller than 50% of the viewport are ignored to avoid
noise from dropdowns and tooltips. Milestones reset on each pixel.page()
call so SPA route transitions start fresh. Also fixes a pre-existing bug
where autocapture: { scroll: false } was silently ignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bkbooth bkbooth requested a review from a team as a code owner May 11, 2026 06:16
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 11, 2026

View your CI Pipeline Execution ↗ for commit 69cad58

Command Status Duration Result
nx run-many -p @imtbl/sdk,@imtbl/checkout-widge... ✅ Succeeded 1s View ↗
nx affected -t build,lint,test ✅ Succeeded 8s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-11 07:02:20 UTC

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

✅ Pixel Bundle Size — @imtbl/pixel

Metric Size Delta vs main
Gzipped 5884 bytes (5.74 KB) +181 bytes
Raw (minified) 15864 bytes +549 bytes

Budget: 10.00 KB gzipped (warn at 8.00 KB)

Comment thread packages/audience/pixel/src/autocapture.ts
Comment thread packages/audience/pixel/src/autocapture.ts Outdated
bkbooth and others added 2 commits May 11, 2026 16:27
- Trim scroll_depth README cell; iframe limitation belongs in
  docs.immutable.com (separate PR) and isn't scroll-specific anyway
- Drop unnecessary (el as HTMLElement) cast — Element.scrollTop is in
  the DOM spec
- Tighten setupAutocapture docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…per frame

Addresses two Cursor bugbot findings:

1. Stale rAF after reset() (medium): if a scroll event scheduled a rAF
   just before pixel.page() called resetScrollDepth(), the rAF would
   read the (still-stale) scrollTop of a reused container and fire
   milestones against the freshly-cleared set — misattributing scroll
   depth to the new page view. reset() now cancels any pending rAF.

2. Single pendingEl drops concurrent container scrolls (low): if two
   large containers fired scroll events in the same frame, only the
   later target was checked. Replaced pendingEl with a Set so every
   container that scrolled within the frame is processed on rAF.

Adds two regression tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ecaa8f2. Configure here.

Comment thread packages/audience/pixel/src/pixel.test.ts
Comment thread packages/audience/pixel/src/pixel.test.ts
bkbooth and others added 2 commits May 11, 2026 16:52
… tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bkbooth bkbooth added this pull request to the merge queue May 11, 2026
Merged via the queue into main with commit f0f4f43 May 11, 2026
12 checks passed
@bkbooth bkbooth deleted the claude/pedantic-brahmagupta-8035a7 branch May 11, 2026 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants