diff --git a/src/runtime/plugin.ts b/src/runtime/plugin.ts index 49c7194..424b3bd 100644 --- a/src/runtime/plugin.ts +++ b/src/runtime/plugin.ts @@ -190,12 +190,18 @@ export default defineNuxtPlugin((nuxtApp) => { refreshHistory() nuxtApp.hook('app:mounted', async () => { + const landing = visitedRoute() if (options.trackOnRouteChange) { - router.afterEach((to, from, failure) => { - if (!failure && to.fullPath !== from.fullPath) void capture(to) + // Without pages, Nuxt updates the route object `from` refers to before afterEach runs, + // so `from` already matches `to` + let lastFullPath = landing.fullPath + router.afterEach((to, _from, failure) => { + if (failure || to.fullPath === lastFullPath) return + lastFullPath = to.fullPath + void capture(to) }) } - await capture() + await capture(landing) }) return { diff --git a/test/fixtures/route-change-pages/app.vue b/test/fixtures/route-change-pages/app.vue new file mode 100644 index 0000000..8f62b8b --- /dev/null +++ b/test/fixtures/route-change-pages/app.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/route-change-pages/nuxt.config.ts b/test/fixtures/route-change-pages/nuxt.config.ts new file mode 100644 index 0000000..5e248e8 --- /dev/null +++ b/test/fixtures/route-change-pages/nuxt.config.ts @@ -0,0 +1,11 @@ +import UtmModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [UtmModule], + routeRules: { + '/static': { prerender: true }, + }, + utm: { + trackOnRouteChange: true, + }, +}) diff --git a/test/fixtures/route-change-pages/package.json b/test/fixtures/route-change-pages/package.json new file mode 100644 index 0000000..8d1fa99 --- /dev/null +++ b/test/fixtures/route-change-pages/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "route-change-pages", + "type": "module" +} diff --git a/test/fixtures/route-change-pages/pages/next.vue b/test/fixtures/route-change-pages/pages/next.vue new file mode 100644 index 0000000..6d1c60e --- /dev/null +++ b/test/fixtures/route-change-pages/pages/next.vue @@ -0,0 +1,7 @@ + + + diff --git a/test/fixtures/route-change-pages/pages/static.vue b/test/fixtures/route-change-pages/pages/static.vue new file mode 100644 index 0000000..4afae61 --- /dev/null +++ b/test/fixtures/route-change-pages/pages/static.vue @@ -0,0 +1,14 @@ + + + diff --git a/test/fixtures/route-change-pages/plugins/tracked-routes.client.ts b/test/fixtures/route-change-pages/plugins/tracked-routes.client.ts new file mode 100644 index 0000000..134a853 --- /dev/null +++ b/test/fixtures/route-change-pages/plugins/tracked-routes.client.ts @@ -0,0 +1,9 @@ +import { defineNuxtPlugin } from '#imports' + +export default defineNuxtPlugin((nuxtApp) => { + const trackedRoutes: string[] = [] + Object.assign(window, { trackedRoutes }) + nuxtApp.hook('utm:before-track', ({ route }) => { + trackedRoutes.push(route.fullPath) + }) +}) diff --git a/test/fixtures/route-change/app.vue b/test/fixtures/route-change/app.vue new file mode 100644 index 0000000..9522935 --- /dev/null +++ b/test/fixtures/route-change/app.vue @@ -0,0 +1,18 @@ + + + diff --git a/test/fixtures/route-change/nuxt.config.ts b/test/fixtures/route-change/nuxt.config.ts new file mode 100644 index 0000000..b4e2a29 --- /dev/null +++ b/test/fixtures/route-change/nuxt.config.ts @@ -0,0 +1,8 @@ +import UtmModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [UtmModule], + utm: { + trackOnRouteChange: true, + }, +}) diff --git a/test/fixtures/route-change/package.json b/test/fixtures/route-change/package.json new file mode 100644 index 0000000..6895968 --- /dev/null +++ b/test/fixtures/route-change/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "route-change", + "type": "module" +} diff --git a/test/integration-route-change-pages.test.ts b/test/integration-route-change-pages.test.ts new file mode 100644 index 0000000..55072ae --- /dev/null +++ b/test/integration-route-change-pages.test.ts @@ -0,0 +1,27 @@ +import { fileURLToPath } from 'node:url' +import { describe, it, expect } from 'vitest' +import { setup, createPage } from '@nuxt/test-utils' + +type TrackingWindow = { trackedRoutes: string[] } + +describe('Route change tracking with pages', async () => { + await setup({ + rootDir: fileURLToPath(new URL('./fixtures/route-change-pages', import.meta.url)), + server: true, + browser: true, + }) + + it('tracks a prerendered landing once, then the route navigated to', async () => { + const page = await createPage('/static?utm_source=static') + await page.getByRole('link', { name: 'Next campaign' }).click() + await page.waitForFunction(() => + (window as unknown as TrackingWindow).trackedRoutes.includes('/next?utm_source=next'), + ) + + expect(await page.evaluate(() => (window as unknown as TrackingWindow).trackedRoutes)).toEqual([ + '/static?utm_source=static', + '/next?utm_source=next', + ]) + await page.close() + }) +}) diff --git a/test/integration-route-change.test.ts b/test/integration-route-change.test.ts new file mode 100644 index 0000000..adeb6d6 --- /dev/null +++ b/test/integration-route-change.test.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { describe, it, expect } from 'vitest' +import { setup, createPage } from '@nuxt/test-utils' + +describe('Route change tracking without pages', async () => { + await setup({ + rootDir: fileURLToPath(new URL('./fixtures/route-change', import.meta.url)), + server: true, + browser: true, + }) + + it('captures a client-side navigation to new campaign parameters', async () => { + const page = await createPage('/?utm_source=landing') + await page.waitForFunction(() => window.localStorage.getItem('nuxt-utm-data')) + await page.getByRole('button', { name: 'Newsletter' }).click() + const lastTouch = page.getByText(/^Last touch:/) + await lastTouch.waitFor() + + expect(new URL(page.url()).search).toBe('?utm_source=newsletter') + expect(await lastTouch.textContent()).toBe('Last touch: newsletter') + const history = await page.evaluate(() => + JSON.parse(window.localStorage.getItem('nuxt-utm-data') ?? '[]').map( + (entry: { + utmParams: { utm_source?: string } + additionalInfo: { landingPageUrl: string } + }) => [entry.utmParams.utm_source, new URL(entry.additionalInfo.landingPageUrl).search], + ), + ) + expect(history).toEqual([ + ['newsletter', '?utm_source=newsletter'], + ['landing', '?utm_source=landing'], + ]) + + await page.close() + }) +})