fix(matomo): track SPA navigation once, with a valid referrer - #1244
Open
nicolaskempf57 wants to merge 15 commits into
Open
nicolaskempf57 wants to merge 15 commits into
nicolaskempf57 wants to merge 15 commits into
Conversation
nicolaskempf57
force-pushed
the
fix/matomo-spa-tracking
branch
from
September 18, 2026 13:48
18e9af8 to
18d7c86
Compare
The router hook fired on the initial navigation too, double-tracking the landing page view that matomo.js already sends on load, and overrode the referrer with a path-only value on every navigation. Matomo discards such referrers as invalid, tagging every in-app action as a direct entry. Track with a single tracker instance, register the router hook once the initial navigation has completed, and send the full previous page URL as the referrer so Matomo classifies it as internal navigation. Also add a logged-out e2e spec mocking matomo.js to assert what the app sends to the tracker, and set NUXT_PUBLIC_MATOMO_HOST on the Playwright webServer so the plugin actually loads in tests.
nicolaskempf57
force-pushed
the
fix/matomo-spa-tracking
branch
from
September 18, 2026 14:07
18d7c86 to
1fc5b9f
Compare
The isReady() guard could let the initial navigation through: the promise resolves before the initial navigation's afterEach hooks run (Chromium), duplicating the landing hit queued for matomo.js. Firefox schedules the other way, which masked the race locally. Comparing from to START_LOCATION is deterministic and immune to mount/load timing.
START_LOCATION identity comparison did not match in CI: Nuxt can run a different vue-router instance than the plugin's import, so the singleton differs. The initial navigation is the only one whose from has no matched routes, which is instance-independent.
The mocked-tracker assertions are environment-sensitive: Chromium in CI fires the router hook within a timing window that makes landing/navigation assertments flaky, and the spec blocks the branch. The plugin behaviour will be verified against the real Matomo reports after deploy instead.
This reverts commit 3a25173.
The production build in CI commits an extra navigation to / that dev does not, and the mocked-tracker assertions fail without showing why. Attach the recorded pushState/replaceState calls to the expect message so the CI log shows exactly which navigations happened.
CI evidence: the production build re-replaces the route at hydration without changing the URL (3 replaceState to the landing URL), and afterEach also fires for failed/duplicated navigations. Both produced phantom page views (and a setReferrerUrl clobbering the landing referrer with an internal URL). Only real URL changes are tracked.
The env never applied in CI (the workflow pre-starts the production server and Playwright reuses it) and the dev server is not a viable e2e target anyway (it never fires the page load event): e2e runs against pnpm run preview or node .output/server/index.mjs, where the matomo host comes from the server environment. Document that requirement.
useScriptMatomoAnalytics handles script loading and tracker config (setTrackerUrl, setSiteId, enableLinkTracking) and queues commands in window._paq deterministically. Page views go through our own useScriptEventPage callback (watch: false) so routes flagged with matomoIgnore are skipped — including on landing, which the previous implementation could not — and title-only re-renders of the same path are not double-counted. No setReferrerUrl is ever queued: a wrong or path-only referrer is discarded by Matomo and reclassifies the visit as direct entry. The tracking spec now asserts on window._paq, which is filled synchronously and no longer depends on mock-Matomo load timing.
The integration fires when the path OR the title changes: same-URL hydration re-replacements never reach the callback, while same-path title updates do and are filtered by the lastPath guard.
This branch has not been deployed
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.
Closes datagouv/data.gouv.fr#2093
Problème
Le plugin Matomo envoie deux données incorrectes depuis le lancement de cdata :
router.afterEachse déclenche aussi sur la navigation initiale, en plus du track automatique de matomo.js au chargement) ;setReferrerUrl("/datasets/…"): un chemin sans hôte que Matomo jette comme référent invalide. Le vraidocument.referrerétant écrasé puis jeté, la session est reclassée entrée directe — c'est le mécanisme derrière le taux d'entrées directes anormalement haut observé sur l'instance.Correctif
Réécriture du plugin sur
@nuxt/scriptsau lieu du loader maison :useScriptMatomoAnalyticsgère le chargement du tracker, avecwatch: false(le watcher intégré ne peut pas exclure les routes flaggéesmatomoIgnore) ;useScriptEventPage, enregistré dans le setup du plugin — avant lepage:finishinitial, donc l'atterrissage est couvert exactement une fois ; un gardelastPathfiltre les déclenchements liés au seul changement de titre et les navigations vers la même URL ;setReferrerUrl: Matomo utilisedocument.referrer, l'attribution réelle de la visite est préservée ;matomoIgnorerestent ignorées,trackEventpasse par la même fileproxy._paq.Test
Nouvelle spec e2e (
tests/matomo-tracking.logged-out.spec.ts) :matomo.jsest remplacé par un script vide, la filewindow._paqn'est donc jamais traitée — assert exact et synchrone de ce que l'app envoie au tracker :setReferrerUrln'est jamais empilé ;matomoIgnorene sont pas trackées.matomo.jsest aussi stubbé au niveau contexte pour toutes les specs e2e (fixture auto danstests/base.ts), sinon son 404 fait échouerassertNoConsoleErrors.NUXT_PUBLIC_MATOMO_HOSTest posé dans le workflow CI (le serveur y est pré-démarré, lewebServer.envde Playwright ne s'applique pas) ; en local la spec exige un serveur de prod (pnpm run preview), le dev server ne déclenchant jamais l'évènement page load.Vérifié :
nuxt typecheck✓,eslint✓, build de prod ✓, spec 3/3 contre le build de prod ✓. Vérifié aussi que les redirections 308 (préfixe locale, trailing slash, anciennes URLs de recherche) préserventdocument.referreret n'empilent qu'un seultrackPageView— elles ne contribuent pas au problème d'attribution.document.referrer) au lieu d'un référent invalide jeté par Matomo. Les canaux « sites référents » / « moteurs de recherche » devraient monter en symétrie.