Refresh Logger page params (dl/dt) on SPA navigations - #118
Draft
philipwalton wants to merge 4 commits into
Draft
Conversation
The Logger captured location.href and document.title once at construction, so after an SPA navigation all subsequent beacons carried the original document location and title even though the event-level page_path was updated. Add a refreshPageParams() method and call it from trackPageview() after the new content (and title) has been applied, so page_view and later events report the current URL and title. The route_transition event intentionally keeps the pre-navigation values since it's logged before the content is swapped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rotate the beacon group in refreshPageParams() so events logged before an SPA navigation are sent with the page params that were current when they were logged, rather than being relabeled with the new dl/dt when the pending beacon body is rebuilt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ensures events logged before refreshPageParams() but still awaiting presend dependencies are queued into the pre-refresh beacon group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Makes the refresh/page_view sequencing explicit rather than relying on microtask ordering (codex review suggestion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Loggercapturesdl(location.href) anddt(document.title) once at construction (src/javascript/Logger.ts,_pageParamsinit, ~lines 54-56). After an SPA navigation (src/javascript/content-loader.ts), subsequent beacons carry the updated event-levelep.page_pathbut the original document location and title in the page-leveldl/dtparams. Since the analytics design already tracksoriginal_page_pathseparately (src/javascript/log.ts), the staledl/dtappears to be an oversight rather than intent.Root cause
_pageParams.dland_pageParams.dtwere only ever written in the constructor; nothing updated them when the SPA content loader swapped the page.What changed
src/javascript/Logger.tsrefreshPageParams()method that re-readslocation.hrefand the (suffix-stripped)document.titleintodl/dt. The title-stripping one-liner is factored into agetPageTitle()helper so the/\s+—.*$/regex isn't duplicated._pageParamson every_queue()call, simply mutatingdl/dtwould have relabeled already-queued but unsent events with the new page's params. SorefreshPageParams()also rotates the beacon group: it leaves the pendingfetchLater()beacon scheduled (deliberately not aborted, unlike the superseded-beacon path in_queue()) and starts a new group (_sendCount++, clear queue, drop the result/controller references). Events keep the page params that were current when they were logged; this reuses the same rotation mechanism_queue()already applies after a beacon activates.refreshPageParams()capturesdl/dtsynchronously, then awaits_presendDependenciesbefore rotating. Sinceevent()awaits the same promises before queuing, microtask FIFO ordering guarantees every event logged before the refresh call is queued into the old group and every event logged after lands in the new one — closing an in-flight-event race flagged in review.src/javascript/content-loader.ts—trackPageview()callslog.refreshPageParams()before logging thepage_view. This runs afterloadPage(), i.e. afterexecuteContainerScripts()has applied the partial'sdocument.title. Deliberate choice: the earlierroute_transitionevent (logged infetchPageContent(), before the content is swapped) keeps the pre-navigationdl/dt— that's the document state at the time it's logged — whilepage_viewand everything after report the new URL/title. The beacon rotation is what makes this guarantee hold.src/javascript/Logger.test.ts— two new browser-project unit tests: one simulates an SPA navigation (history.pushState+document.titleassignment) and asserts the next beacon'sdl/dt,_sbump, and that the pre-refresh beacon is left un-aborted with its events; another covers the in-flight race (event logged while a presend dependency is pending, then refresh) and asserts the event stays in the pre-refresh group.test/e2e/log.ts— encodes the new intended semantics:dlmatches the new article URL.clearBeacons()calls and instead waits for each navigation'spage_viewbeacon (matched bynavigation_type: route_change) before navigating again. The old test implicitly depended on the abort-and-resend batching behavior (cleared events reappeared in the final rebuilt beacon); with rotation, each navigation finalizes its beacon and events are not re-sent, and the per-step waits also make the final in-order triple assertion deterministic.Verification
All run inside the worktree on this branch:
npm run lint— 0 errorsnpm run types:check— passnpm run test:unit— 16 files / 69 tests pass (includes the 2 new Logger tests)npm run build— passnpm test(full e2e, multiple runs) — all log.ts tests pass, including both SPA specs with the new assertions. Tolerated failures observed, all pre-existing/known:homepage.ts"working links to all published articles" — deterministic (atom-feed order, PR Sort the Atom feed newest-first #103)content-loading.ts"should not attempt to load non-HTML content" — deterministic (same root cause)content-loading.ts"should show an error if the content cannot be loaded" — intermittent, passes on retrylog.ts"should track engagement time" — environmental on the test machine (needs browser window focus); passed when the window had focusworker.tspriority-hints specs intermittently timed out (inclearStorage()'s/__resetreadiness wait) in some full-suite runs and passed in others, with identical code. Running the spec alone 4x showed the same first-attempt flake (~1 min hang) that always passes on the automatic retry, so it's a pre-existing timing flake.refreshPageParams()is only invoked on SPA navigations, which worker.ts never performs, so this diff's code path is never exercised there.Codex review
Three rounds (read-only):
dl/dtin place would relabel already-queued events (and made the route_transition comment false). Fixed via the beacon-group rotation described above.event()race (event logged pre-refresh but queued post-refresh while awaiting presend dependencies). Fixed by awaiting the same presend dependencies inrefreshPageParams(); regression unit test added.page_viewsequencing explicit was adopted (trackPageview()nowawaits the refresh).Unresolved non-blocking suggestions: (a)${articles[0]?.path}$
new RegExp()in the e2e spec doesn't regex-escape the path — current article slugs contain no regex-significant characters, and this matches the file's existingnew RegExp(...)usage; (b) the in-flight-race unit test asserts grouping (_s, un-aborted first beacon) but doesn't additionally change the URL/title within that test — the dl/dt values themselves are covered by the other new test; (c) explicit: void/return-type annotations on the new method were skipped to match the class's existing style.Please scrutinize
dl/dtwas intentional (e.g. GA-style "dl = the originally loaded document"), close this PR — the source report flagged this issue as "verify against intent". Noteoriginal_page_pathalready preserves the landing path at the event level._s(send count) increments per navigation accordingly, and/logingestion will see more, smaller requests. Verify this is acceptable for the collection endpoint.route_transitionevent intentionally keeps the pre-navigationdl/dt(document state at log time); if you'd rather it carry the destination's params, move therefreshPageParams()call earlier.clearBeacons()) encodes the new no-resend semantics — please confirm the test still checks what you intended it to.Conflicts with sibling PRs
fix/logger-state-init) touchesLogger.ts(different method — constructor/state init).content-loader.ts; Ship partial titles as a data attribute instead of re-executed inline scripts #117 modifiesloadPage()neartrackPageview(). This diff adds only one call + comment insidetrackPageview(), so rebases should be mechanical.🤖 Generated with Claude Code