Skip to content

feat: send navbar clicks made before analytics loads through the beacon - #909

Open
braianj wants to merge 12 commits into
masterfrom
feat/navbar-early-click-beacon
Open

braianj wants to merge 12 commits into
masterfrom
feat/navbar-early-click-beacon

Conversation

@braianj

@braianj braianj commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Navbar clicks made before analytics is ready are now sent instead of dropped. Analytics loads once the page is idle, and since @dcl/hooks 2.1.0 it reports ready only after Segment has fetched its settings, so until then the click used to be lost.

Before analytics is ready, a click that replaces the page (a same-tab link, Sign In) goes through the same unload-safe beacon the download CTAs use (postSegmentEvent), with the same track_deferred / track_called_at / track_delivered_at fields as every other deferred click. It is skipped for sessions that started on an exempt page (isAnalyticsDisabledForSession) and for bots (isBotClient), the same rules that keep the SDK off. A click that keeps the page open (a new-tab link, ctrl/cmd/shift click, middle click, Jump In) waits in useDeferredTrack's queue instead and is sent by analytics.js with its full context once it is ready. Once analytics is ready, every click goes through track as today.

Planned for the release after the one carrying #903 to #906.

How to test (preview, desktop Chrome, a new incognito window, DevTools Network filtered to api.e.decentraland.org, throttling set to Slow 4G):

  1. Open /events and click Learn within the first second, before the settings request finishes. Expected: a POST to /v1/track (the beacon endpoint) goes out before the settings request finishes, and the blog loads.
  2. Reload with throttling off, wait about 10 seconds, then click Learn. Expected: one Click sent by analytics.js (/v1/t), not a beacon.
  3. Open /privacy in a new incognito window, wait a few seconds, then click Learn. Expected: no Click request (no /v1/track or /v1/t).
  4. needs manual verification: open /events under Slow 4G, hover Create and immediately click Creator documentation (new tab). Expected: no /v1/track beacon. Once settings loads, a Click with track_deferred: true goes out through /v1/t. A component test covers this.

@vercel

vercel Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sites Ready Ready Preview Oct 9, 2026 1:21am UTC

Request Review

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: approve ✅

This is a small change that follows existing patterns: navbar clicks made before analytics is ready now go through the same unload-safe beacon that useDownloadClick uses.

What I checked

  • No double counting. Each click takes exactly one path. When analytics is ready it goes through track and returns; otherwise it goes through the beacon. A beacon click never enters the analytics-next buffer, so it cannot be sent again once Segment finishes loading.
  • The beacon follows the same rules that keep the SDK off.
    • Bots: @dcl/hooks@2.1.0 does export isBotClient, and its AnalyticsProvider uses the same check to skip loading.
    • Exempt sessions: postSegmentEvent deliberately skips the exempt-path check (bypassExemptPathGate), so the explicit isAnalyticsDisabledForSession() call is required, and it is there.
    • No writeKey: postSegmentEvent already returns early.
    • Both checks run before ensureSegmentAnonymousId(), so bots and exempt sessions never get an ajs_anonymous_id cookie written.
  • The navbar links are plain href anchors (full page loads), so the beacon is the right transport for them.
  • Tests. The afterEach with jest.resetAllMocks() plus the new beforeEach keep each case isolated. The new cases cover a person, an exempt session, a bot, and analytics being ready. The ref pattern and the [track] dependencies match useDownloadClick and useDeferredTrack. No unused imports.

Minor findings (P2, not blocking)

  1. P2: sign_in, jump_in and middle-clicks into a new tab do not leave the page, but they also take the beacon path before analytics is ready. This includes the window where the SDK instance exists and would queue them with full context. Details inline.
  2. P2: The download CTA beacon marks its events with track_deferred: true (plus track_called_at and track_delivered_at), but the navbar beacon does not. Details inline.
  3. P2: No test covers a middle-click before analytics loads, or analytics becoming ready after the first render (a rerender before the click). The second is the case the ref exists for.

CI: lint, audit, title-matches-convention, lint-package-json and Vercel pass; test was still pending when I reviewed.
Conventions: the title feat: … and the branch feat/navbar-early-click-beacon follow ADR-6.
Consumer impact: none. This is an internal component change with no public surface.


Reviewed by Jarvis 🤖 · Requested by braianj via GitHub

// links leave the page, so a click before that would be lost with it; the beacon survives the unload. It
// follows the same rules that keep the SDK off: sessions that started on an exempt page, and bots.
if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return
postSegmentEvent(SegmentEvent.CLICK, properties, ensureSegmentAnonymousId())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Some navbar actions do not leave the page: sign_in (button), jump_in (button), and middle-clicks into a new tab (#905). Before analytics is ready they also take the beacon path, so they lose the full analytics-next context (traits, campaign, the SDK-generated messageId) that they would get if queued with useDeferredTrack. That includes the phase where the SDK instance exists but is still fetching settings. The cost is data quality, not lost data. If you want, keep the beacon for anchor clicks that navigate away and route the button and middle-click actions through useDeferredTrack. Fine to leave as is if the uniform payload matters more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied in 6bf7784. Only clicks that replace the page (a same-tab link, Sign In, which redirects) take the beacon now. Clicks that keep the page open (a target="_blank" link, ctrl/cmd/shift click, middle click, Jump In) go to useDeferredTrack, which queues them and sends them through analytics-next with full context once it is ready. The decision comes from isSameTabNavigation (helper with tests). The component spec covers both paths, including the queue draining when readiness flips.

// links leave the page, so a click before that would be lost with it; the beacon survives the unload. It
// follows the same rules that keep the SDK off: sessions that started on an exempt page, and bots.
if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return
postSegmentEvent(SegmentEvent.CLICK, properties, ensureSegmentAnonymousId())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] useDownloadClick tags its beacon clicks with track_deferred: true (plus track_called_at and track_delivered_at), so the warehouse can tell beacon-sent Click events apart from SDK-sent ones. Here they can only be told apart through context.library.name = "dcl-sites-beacon". Consider adding at least track_deferred: true so dashboards can split navbar clicks the same way they split download clicks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added in bf52e07: beacon-sent navbar clicks carry track_deferred: true, like useDownloadClick. Queued ones get it from useDeferredTrack. One caveat for dashboards: stg_landing_click / int_landing_tracks in DCL_NEXT only keep action, section and href from extra_data, so splitting there needs that allowlist widened. Until then, context.library.name = "dcl-sites-beacon" is the split available in the warehouse.

@coveralls

coveralls commented Oct 8, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37869264982

Coverage increased (+0.008%) to 94.189%

Details

  • Coverage increased (+0.008%) from the base build.
  • Patch coverage: 32 of 32 lines across 4 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 15424
Covered Lines: 15035
Line Coverage: 97.48%
Relevant Branches: 7171
Covered Branches: 6247
Branch Coverage: 87.11%
Branches in Coverage %: Yes
Coverage Strength: 25.91 hits per line

💛 - Coveralls

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review

Verdict: Approve. I found no P0 or P1 issues.

The change does what the description says. Before analytics is ready, a click that leaves the page now goes through postSegmentEvent. A click that keeps the page open (target="_blank", ctrl/cmd/shift-click, middle-click, Jump In) is queued in useDeferredTrack and sent once Segment is ready. After that, clicks go through track.

What I checked

  • isBotClient(userAgent) is exported by @dcl/hooks 2.1.0, and it is the same check AnalyticsProvider uses to keep the SDK off for bots. Together with isAnalyticsDisabledForSession(), the beacon is skipped in the same cases where the SDK never loads. This is stricter than useDownloadClick, which skips the exempt-path check on purpose for conversion CTAs. That fits navbar clicks, which are not conversions.
  • Navbar links are plain <a href> elements, so a same-tab click really does load a new page. The beacon is the right transport for these clicks, and each click is sent once only: the beacon branch returns before reaching the queue.
  • ensureSegmentAnonymousId() creates and saves the ajs_anonymous_id before Segment boots, so beacon clicks and later SDK events share the same anonymous id.
  • trackNavbar now reads readiness from isInitializedRef, which matches useDownloadClick. Removing isInitialized from the deps is correct.
  • The tests cover beacon, new tab then drain, exempt session, bot, and the ready path, plus the helper's modifier and _blank cases.

Findings

  • [P2] The beacon payload has track_deferred: true but not track_called_at / track_delivered_at. useDownloadClick's beacon branch and the useDeferredTrack queue both send all three (see the inline comment).

CI: lint, audit, and title checks pass. test was still running when I reviewed.


Reviewed by Jarvis 🤖 · Requested by braianj via GitHub

if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return
// Flagged like useDownloadClick's beacon clicks, so beacon-sent and SDK-sent clicks can be told apart.
// eslint-disable-next-line @typescript-eslint/naming-convention
postSegmentEvent(SegmentEvent.CLICK, { ...properties, track_deferred: true }, ensureSegmentAnonymousId())

@decentraland-bot decentraland-bot Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] The comment says this is "flagged like useDownloadClick's beacon clicks", but that beacon (src/hooks/useDownloadClick.ts) also sends track_called_at and track_delivered_at, and so does every queued click from useDeferredTrack (including the new-tab path just above). As written, a navbar beacon Click is the only deferred click without those fields. Any query that uses them (for example, to measure the delay between the click and when it was sent) will get nulls for these rows. Suggested change to keep the payloads the same:

Suggested change
postSegmentEvent(SegmentEvent.CLICK, { ...properties, track_deferred: true }, ensureSegmentAnonymousId())
const calledAt = Date.now()
postSegmentEvent(
SegmentEvent.CLICK,
// eslint-disable-next-line @typescript-eslint/naming-convention
{ ...properties, track_called_at: calledAt, track_delivered_at: calledAt, track_deferred: true },
ensureSegmentAnonymousId()
)

If you take this, the postSegmentEvent assertion in LandingNavbar.spec.tsx will need expect.any(Number) for the two timestamps.

Note: this suggestion replaces only line 287 and includes its own eslint-disable-next-line, so also delete the existing one on line 286 when you apply it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied in 78291c4: the beacon payload now carries track_called_at and track_delivered_at (both set to the click time, since the beacon is sent on the spot) alongside track_deferred: true. The spec asserts the timestamps with expect.any(Number).

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: send pre-ready navbar clicks through the beacon

Verdict: Approve. I found no P0 or P1 issues. The approach is sound and every handler sends each click down exactly one path: track, the useDeferredTrack queue, or the beacon. Nothing is counted twice.

Verified

  • Click classification matches what each handler actually does.
    • Navbar links are plain <a> elements, so a same-tab click does a full page load and the beacon is right.
    • Sign In ends in redirectToAuth, which calls window.location.replace. The page unloads, so the beacon is right.
    • Jump In calls preventDefault and opens the modal or launcher. The page stays, so the queue is right.
    • DesktopTabWithDropdown uses window.open(url, '_self'), which always navigates the same tab, so the default leavesPage = true is right.
    • Middle click goes through onAuxClick with false, so the queue is right.
  • The beacon follows the same rules that keep the SDK off. In @dcl/hooks 2.1.0, AnalyticsProvider stays off only for an empty write key or a bot.
    • An empty write key makes postSegmentEvent return early.
    • Bots are checked with the same isBotClient, which 2.1.0 exports for this purpose.
    • The isAnalyticsDisabledForSession() check puts back the exempt-path rule that postSegmentEvent skips on purpose.
    • Neither the SDK nor the beacon has a consent or Do Not Track check, so the beacon skips nothing the SDK applies.
  • Reading isInitialized through a ref avoids beaconing a click with a stale false after the render where Segment became ready. This is the same pattern as useDownloadClick.
  • Security: href and action come only from static navbar config, and the body is built with JSON.stringify and sent as text/plain with credentials: 'omit'. No new PII or injection surface.

Findings (all non-blocking)

  • [P2] isSameTabNavigation ignores altKey (inline).
  • [P2] The deferred-beacon payload is copied from useDownloadClick and has already drifted from it (inline).
  • [P2] The positional leavesPage boolean is hard to read at the call sites. trackNavbar('jump_in', undefined, false) reads better as { leavesPage: false }.
  • [P2] Test gaps. The specs cover Learn (beacon), a new-tab link (queue), exempt sessions and bots. They don't cover:
    • the Sign In beacon
    • the DesktopTabWithDropdown beacon
    • the Jump In queue
    • the payload in the "analytics is ready" case (for example, that track_deferred is not set)
  • [P2] Follow-up, out of scope here. The navbar is the only postSegmentEvent caller that filters bots. The download beacons (useDownloadClick, downloadTracking, downloadPageExit, downloadFunnelExit, and others) still send for bots. Moving the bot check into postSegmentEvent would apply one rule to every caller.

CI

lint, audit, lint-package-json, title convention and Vercel pass. test was still pending when this review was posted. I couldn't run the specs locally in this environment, so please confirm test is green before merging.


Reviewed by Jarvis 🤖 · Requested by braianj via GitHub

*/
function isSameTabNavigation(event: { currentTarget: EventTarget; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }): boolean {
const target = event.currentTarget instanceof Element ? event.currentTarget.getAttribute('target') : null
return target !== '_blank' && !event.ctrlKey && !event.metaKey && !event.shiftKey

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Alt-click (Option-click on macOS) on a link downloads it in Chrome and keeps the page open, but this helper counts it as leaving the page. Before analytics is ready, that click goes out by beacon instead of waiting in the queue for the SDK. It is still counted once, just without the SDK's full context. Consider adding altKey to the parameter type and the check (&& !event.altKey), plus an it.each case for it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied in 09c759c: isSameTabNavigation also treats altKey as keeping the page, and the it.each covers it.

postSegmentEvent(
SegmentEvent.CLICK,
// eslint-disable-next-line @typescript-eslint/naming-convention
{ ...properties, track_called_at: calledAt, track_delivered_at: calledAt, track_deferred: true },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] This copies the deferred-beacon payload from useDownloadClick.ts (track_called_at / track_delivered_at / track_deferred + ensureSegmentAnonymousId()), and the two have already drifted: here track_delivered_at reuses calledAt, while useDownloadClick takes a fresh Date.now(). A small shared helper (e.g. postDeferredClick(properties) next to postSegmentEvent) would keep the beacon callers identical, which the comment above says is the intent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied in b7dc1dc. New postDeferredClick(properties) in src/modules/deferredClickBeacon.ts (with a spec) builds the deferral fields once, and both useDownloadClick and the navbar call it. track_delivered_at now takes a fresh Date.now() for both, matching what useDownloadClick did before. useDownloadClick specs pass unchanged.

@braianj

braianj commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Summary-level P2s:

  • Positional leavesPage (609402e): it is now an options object, trackNavbar("jump_in", undefined, { leavesPage: false }).
  • Test gaps (a379f0d): Sign In goes through the beacon. The Discover section tab goes through the beacon before window.open. Jump In is queued, not beaconed, and drains through analytics once ready. The ready-path payload has no deferral fields.
  • Bot check inside postSegmentEvent: left out on purpose. It would stop bot traffic on the download conversion beacons, which changes download funnel numbers, so the data team should see it before it ships. It will be a separate PR.

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

This is a well-scoped change. Every navbar call site is put on the right path:

  • Clicks that replace the page go through the beacon. These are same-tab <a> links (logo, Learn, credits, user menu, internal dropdown items), the section tab buttons (window.open(url, '_self')) and Sign In (redirectToAuth → location.replace).
  • Clicks that keep the page open wait in the useDeferredTrack queue. These are target="_blank" items, modifier clicks, middle clicks and Jump In (useHangOutAction calls preventDefault).

Each branch returns early, so a click never goes through the beacon and also gets queued, and nothing is sent twice. The beacon gate (exempt-session check plus isBotClient) matches the rules that keep the SDK off. isBotClient is exported by the installed @dcl/hooks@2.1.0. Extracting postDeferredClick into its own module does not change what useDownloadClick sends.

Security: I found no issues. The beacon path checks the exempt-page gate and the bot check before sending. The href values come from fixed navbar config and carry no user input or wallet. userId is only present after the SDK has identified the user, the same as SDK-sent events.

Verification (local, head a379f0d): jest on LandingNavbar, deferredClickBeacon and useDownloadClick: 111/111 passing. tsc --noEmit: clean. eslint on the touched files: clean. CI checks were still pending when I posted this review.

Findings

All findings are P2. None blocks the merge.

  • [P2] LandingNavbar.helpers.ts:56: the parameter type is written inline in a .helpers.ts file, which CLAUDE.md forbids ("Types / interfaces: <thing>.types.ts. Never inline in .client.ts, .helpers.ts…").
  • [P2] LandingNavbar.tsx:261: the ref comment claims more than the ref does.
  • [P2] deferredClickBeacon.ts:8: the doc comment says callers apply the exempt and bot gates, but useDownloadClick applies neither.
  • [P2] Test gaps in LandingNavbar.spec.tsx. These cases are not covered at the component level:
    • A middle click (onAuxClick → queue) before analytics is ready.
    • A ctrl/cmd click on a same-tab link through the component. Only the helper is tested for this.
    • A beaconed click followed by analytics finishing, asserting that track is not called afterwards. This would lock in the no-double-send behavior.

Reviewed by Jarvis 🤖 · Requested by braianj via GitHub

* Whether a primary click on a navbar link replaces the current page. A link that opens in a new tab
* (`target="_blank"`, or ctrl/cmd/shift held) or downloads it (alt/option held) leaves this page alive, so its click can wait for analytics.
*/
function isSameTabNavigation(event: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] CLAUDE.md does not allow inline types in .helpers.ts files. This hand-written shape could be replaced with Pick<React.MouseEvent, 'currentTarget' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'altKey'>, or a named type in LandingNavbar.types.ts. Either option also keeps the type in sync with the DOM event.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in e709356: the event is now Pick<MouseEvent, 'currentTarget' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'altKey'> from React, so the instanceof Element guard is gone too.

const { isInitialized, track } = useAnalytics()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)

// Read through a ref (as useDownloadClick does) so a click sees Segment's current readiness even if it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] This comment claims more than the ref does. A ref assigned during render only ever holds the value from the last render, so a click cannot see a readiness change that has not been rendered yet. What the ref does is keep trackNavbar stable, so it no longer depends on isInitialized and doesn't invalidate the memoized handlers. Suggest rewording the comment to say that. useDownloadClick has the same wording, so it could be fixed there too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d881b17: both comments (here and in useDownloadClick) now say the ref keeps the handler's identity when analytics becomes ready.

Comment thread src/modules/deferredClickBeacon.ts Outdated
/**
* Sends a `Click` made before analytics was ready through the unload-safe beacon, for clicks that leave the page.
* Adds the same deferral fields `useDeferredTrack` gives queued clicks, so every deferred click carries them
* whichever transport sent it. Callers apply their own gates (exempt session, bots) before calling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] useDownloadClick is the other caller, and it applies neither gate. Skipping the exempt-session check there is intentional, because /download is exempt but its CTAs must still be counted. Skipping the bot check, though, means a crawler that clicks a download CTA before Segment is ready still sends a beacon. This was already the case before this PR. Two options:

  • Reword the comment so it does not promise gating that only the navbar does.
  • Move the isBotClient(navigator.userAgent) check into postDeferredClick. Both callers would then share it, and the navbar would only keep its exempt-session check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reworded in 916d4d4: the comment now says postDeferredClick applies no gate and names what each caller skips. I left the bot check where it is, because moving it into postDeferredClick would stop counting bot clicks on download CTAs and change the download funnel numbers. That goes in a separate PR, once the data team has seen it.

@braianj

braianj commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The test gaps from the last review are covered in ed2c8c3: a pre-ready middle click and ctrl/cmd click on a same-tab link are queued and drained once analytics is ready, and a beaconed click is not sent again through track after analytics finishes loading.

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review

Verdict: Approve. I found no P0 or P1 issues.

The routing is correct. Before analytics is ready, each click goes to exactly one transport (beacon or queue), never both. Once analytics is ready, every click goes through track as before. I checked each caller's leavesPage value:

  • Leave the page: plain same-tab links, the desktop section tab (window.open(url, '_self'), which stays in the same tab even with a modifier key held), and both Sign In buttons (useSignInRedirect always calls redirectToAuth).
  • Keep the page: _blank links, ctrl/cmd/shift/alt clicks, middle click, and Jump In (it only opens the download modal or the desktop app).

Moving the isInitialized read into a ref is safe: track keeps the same identity while analytics loads, so trackNavbar doesn't capture a stale value. The beacon gate (isAnalyticsDisabledForSession + isBotClient) uses the same conditions that keep the SDK off. It is needed because postSegmentEvent bypasses the exempt-path gate. The postDeferredClick extraction in useDownloadClick sends the same payload as before, and the download CTAs are still ungated on purpose.

Tests cover the beacon path, the queue for new-tab, modifier and middle clicks (including the flush with no double send), the exempt and bot sessions, Sign In, and all four modifier keys in isSameTabNavigation.

Minor (P2)

  • P2 LandingNavbar.tsx:277: in exempt or bot sessions, clicks that keep the page are still pushed into useDeferredTrack's queue. Analytics never loads there, so the queue is never flushed. Nothing is sent, but the closures stay in memory for the life of the component. See the inline comment.
  • P2 (by design, noting it for the data team): a same-tab click made while analytics.js is loaded but not ready goes out through the beacon, so it carries the beacon's context instead of the SDK's (no campaign/UTM enrichment). Analysis that splits on track_deferred = true should keep this in mind.

CI: lint, lint-package-json, title and Vercel are passing. test and audit were still running when I posted this.


Reviewed by Jarvis 🤖 · Requested by braianj via GitHub

}
// Analytics loads once the page is idle and is ready only after Segment fetched its settings. A click
// that stays on the page (new tab, Jump In) waits in the queue with the SDK's full context.
if (!leavesPage) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] In a session that started on an exempt page, or for a bot, analytics never initializes, so this queue is never flushed. Nothing is sent (correct), but each click that keeps the page leaves a closure in queueRef for the life of the navbar. Checking the gate before both branches makes the rule explicit and avoids that:

if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return
if (!leavesPage) {
  deferredTrack(SegmentEvent.CLICK, properties)
  return
}
postDeferredClick(properties)

This doesn't block the merge.

This branch was successfully deployed

1 active deployment
Preview — ed2c8c3b Deployed Oct 9, 2026 by vercel[bot]
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.

3 participants