Skip to content

feat: record the landing campaign when tracking is enabled after navigation - #237

Merged
sadjow merged 6 commits into
mainfrom
fix/consent-after-navigation
Oct 15, 2026
Merged

sadjow merged 6 commits into
mainfrom
fix/consent-after-navigation

Conversation

@sadjow

@sadjow sadjow commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

When an app starts with tracking disabled, enableTracking() captured only the page the visitor was on when it ran. A visitor who landed on /?utm_source=google, followed a client-side link, and then accepted the consent banner lost the campaign that brought them.

The plugin now holds the landing route in memory when tracking is disabled as the app mounts. enableTracking() records that route first, with the time the app mounted, and then the current page if its URL differs. The held route never reaches storage and lives only as long as the page, so a reload or a new visit starts without it. It is dropped once recorded and when clearData() runs. Hooks run when the held route is processed, so a utm:before-track consent check still decides whether it is saved. Matomo's consent mode and Segment's consent wrapper take a similar approach: they keep a page's calls in memory and send them once consent is given, and neither carries them across a reload. disableTracking() on its own keeps the held route, so a visitor who declines and then accepts during the same visit still has the landing page recorded, and an app that switches tracking off at startup still recovers it; the README's refusal path also calls clearData(), which drops it. Apps that keep tracking enabled, the default, don't hold anything and behave as before.

A new captureLandingOnEnable option controls this and defaults to true. With false, nothing is held and enableTracking() records only the current page. Only the landing route is held, not the pages in between. When tracking is already enabled at startup, for example through a saved preference that outlived expired consent, the landing page is recorded right away, so nothing stays held unless disableTracking() cancels that capture.

The held route is a copy of the landing route, because Nuxt's router for apps without pages/ updates one route object in place on navigation; holding the live object would make the landing look like the current page. The hold is released only once its capture completes. A capture that disableTracking() cancels, whether in the same tick or while a utm:before-track hook is still pending, hands the route back, so the next enableTracking() still records it; a hook that skips it, or a deduplicated or retained-out entry, drops it as intended.

The README's Privacy and consent section now lists the three ways an app can combine the module with consent: track regardless of consent, the default; keep tracking and check consent where the data leaves the browser; or track only after consent. For the second, it shows guarding getAttribution() with the consent tool's check and recording consent on each entry from a utm:before-persist hook. The section also describes the held landing route and its limits, replacing the note that campaigns from pages visited before consent are lost. It says to read the page from context.route in hooks, since the landing page's hooks run when tracking is enabled, and to call clearData() only on an actual refusal, since some consent tools report a refusal before the visitor chooses. The Configuration section no longer says that all new options are opt-in, since captureLandingOnEnable is on by default; it affects only apps that start with tracking disabled.

Release note: when tracking starts disabled, enableTracking() now records the landing page of the current page load, timestamped when the visitor arrived, before the current page. Set captureLandingOnEnable: false to record only the current page.

Validation: three new unit tests in test/runtime-plugin.test.ts failed on main, either because the landing entry was missing or because it carried the time of enableTracking() instead of the arrival time; they pass now, as do a test showing that clearing data drops the held route and one showing that always-on tracking is unchanged. A unit test that turns captureLandingOnEnable off failed before the option existed, because the landing entry was still recorded, and passes now. Four more unit tests failed before the hold was copied and kept across cancellation, each missing the landing entry: one models the router of apps without pages/, which updates the route in place, and three cancel the landing capture with disableTracking(), in the same tick, during a pending hook, and during the startup capture of an app that starts enabled. A browser test in test/integration-route-change.test.ts, whose fixture has no pages/, lands with tracking disabled, navigates with navigateTo, enables tracking, and finds both entries stored. A new browser test in test/integration-rendering-modes.test.ts lands on a campaign URL with tracking disabled, follows a client-side link, and enables tracking. On main it stored only the page after the link for the server-rendered, prerendered async, prerendered synchronous, and client-rendered pages; it now stores both entries for all four. test:compatibility gained the same late-consent scenario for the packed module: with main's runtime it failed on Nuxt 3.21.11 for the server-rendered, client-rendered, and static pages, and with this change it passes on Nuxt 3.21.11, 4.6.1, and the 5.0.0 nightly. pnpm test (163 passed, 1 skipped), pnpm lint --max-warnings 0, pnpm format:check, pnpm test:types, and pnpm test:package, whose consumer now sets the new option, pass.

@sadjow sadjow changed the title fix: record the landing campaign when tracking is enabled after navigation feat: record the landing campaign when tracking is enabled after navigation Oct 15, 2026
@sadjow
sadjow merged commit 99e75ec into main Oct 15, 2026
9 checks passed
@sadjow
sadjow deleted the fix/consent-after-navigation branch October 15, 2026 20:55
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.

1 participant