PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 40s
The location pages were only half-tracked. Umami counts a pageview for each of the ~3,900 URLs automatically, but nothing else: components/ places contained no track() call, and place_viewed was not even a declared event name. The part that mattered was worse than a gap. getNavigationSource mapped a same-origin referrer to a funnel source and had no case for /schools/, so every school view arriving through the location layer fell through to 'direct' — the bucket you read as "typed the URL, no referrer". W2's whole purpose is funnelling search traffic onto school pages, so the one measurement that says whether it worked was reporting the wrong answer, and reporting it confidently. Verified live against staging: expected "place", received "direct". /schools/ is checked before /school/. They differ by one letter and mean different things — the location layer versus a single school — and a prefix test in the wrong order silently merges them. place_viewed carries kind, slug, phase and school_count. kind is the reason it exists: whether to keep investing in these pages turns on which sort earns engagement, and a pageview cannot say, because all four families share the /schools/ prefix and only the registry knows which is which. It is a client component because PlaceView is a server component; one line in PlaceView covers all four families, since they all render through it. Both E2E journeys were verified failing against staging first — one because place_viewed does not exist there, the other on the exact "place" vs "direct" mismatch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Fires `place_viewed` once per location page.
|
|
*
|
|
* A separate client component because PlaceView is a server component and
|
|
* cannot call into the browser. It renders nothing — its whole job is the
|
|
* effect, which keeps the page itself server-rendered.
|
|
*
|
|
* Umami already counts a pageview for every one of these URLs, so this is not
|
|
* about traffic. It is about `kind`: whether to keep investing in the location
|
|
* layer turns on which *sort* of page earns engagement — towns, authorities,
|
|
* London localities or postcode districts — and a pageview cannot say, because
|
|
* all four families share the /schools/ prefix and only the registry knows
|
|
* which is which.
|
|
*/
|
|
|
|
import { useEffect } from 'react';
|
|
import { track, getNavigationSource } from '@/lib/analytics';
|
|
|
|
interface Props {
|
|
kind: string;
|
|
slug: string;
|
|
count: number;
|
|
phase?: 'primary' | 'secondary';
|
|
}
|
|
|
|
export function TrackPlaceView({ kind, slug, count, phase }: Props) {
|
|
useEffect(() => {
|
|
track('place_viewed', {
|
|
kind,
|
|
slug,
|
|
// "all" rather than omitting it, so the unphased page is a value in the
|
|
// same field rather than a gap that has to be interpreted.
|
|
phase: phase ?? 'all',
|
|
school_count: count,
|
|
// Internal navigation only. An arrival from Google reads as 'direct'
|
|
// here; Umami's own pageview referrer is where external attribution
|
|
// lives, and these pages exist to be arrived at externally.
|
|
from: getNavigationSource(),
|
|
});
|
|
// Once per place, not once per render.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [kind, slug, phase]);
|
|
|
|
return null;
|
|
}
|