Merge remote-tracking branch 'origin/main' into feat/ks4-destinations
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 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m12s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 4m4s
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 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m12s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 4m4s
# Conflicts: # nextjs-app/__tests__/components/darkThemeSafety.test.ts
This commit is contained in:
commit
68a192e430
14 files changed
+421
-21
No files matched your search
+66
-11
@@ -60,22 +60,77 @@ export function track(name: EventName, data?: Payload): void {
|
||||
export type NavigationSource =
|
||||
'search' | 'rankings' | 'compare' | 'detail' | 'place' | 'direct';
|
||||
|
||||
/*
|
||||
* The in-app trail.
|
||||
*
|
||||
* document.referrer is written by the browser only when a *document* loads.
|
||||
* Every internal navigation here is an App Router soft navigation —
|
||||
* history.pushState, no new document — so document.referrer goes on naming
|
||||
* whatever opened the tab (usually nothing, or a search engine) for the whole
|
||||
* session. Reading it to answer "which page did they come from" therefore
|
||||
* returned 'direct' for essentially every in-app journey, including the one
|
||||
* the location layer exists to produce.
|
||||
*
|
||||
* Verified on staging: /schools/brentwood, click a school, the URL becomes
|
||||
* /school/… and document.referrer is still "".
|
||||
*
|
||||
* A module-level trail is the counterpart with exactly the right lifetime. It
|
||||
* survives soft navigation, and it dies on a real document load — which is
|
||||
* precisely when document.referrer becomes meaningful again, so the two cover
|
||||
* each other with no overlap.
|
||||
*/
|
||||
const TRAIL_LIMIT = 4;
|
||||
const trail: string[] = [];
|
||||
|
||||
/** Record a path the user is now on. Called by RouteTrail on every route. */
|
||||
export function recordVisitedPath(path: string): void {
|
||||
if (trail[trail.length - 1] === path) return;
|
||||
trail.push(path);
|
||||
if (trail.length > TRAIL_LIMIT) trail.shift();
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent path that is not the one being viewed.
|
||||
*
|
||||
* Skipping the current path rather than taking trail[length - 2] is what
|
||||
* makes the answer independent of ordering: the trail is written by a
|
||||
* layout-level effect and read by a page-level one, and React orders those by
|
||||
* tree position — not a contract worth resting a measurement on. It also
|
||||
* gives the right answer when the user goes back to a page they came from.
|
||||
*/
|
||||
function previousInAppPath(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const current = window.location.pathname;
|
||||
for (let i = trail.length - 1; i >= 0; i -= 1) {
|
||||
if (trail[i] !== current) return trail[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function classifyPath(p: string): NavigationSource {
|
||||
if (p === '/' || p === '') return 'search';
|
||||
if (p.startsWith('/rankings')) return 'rankings';
|
||||
if (p.startsWith('/compare')) return 'compare';
|
||||
// `/schools/` before `/school/`: they differ by one letter and mean
|
||||
// different things — the location layer versus a single school. Checked
|
||||
// first so the narrower-looking prefix cannot shadow it if either string
|
||||
// is ever edited.
|
||||
if (p.startsWith('/schools/')) return 'place';
|
||||
if (p.startsWith('/school/')) return 'detail';
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
export function getNavigationSource(): NavigationSource {
|
||||
const internal = previousInAppPath();
|
||||
if (internal) return classifyPath(internal);
|
||||
|
||||
// No trail means this is the first page of the document, so the referrer is
|
||||
// the only witness — and an honest one.
|
||||
if (typeof window === 'undefined' || !document.referrer) return 'direct';
|
||||
try {
|
||||
const ref = new URL(document.referrer);
|
||||
if (ref.origin !== window.location.origin) return 'direct';
|
||||
const p = ref.pathname;
|
||||
if (p === '/' || p === '') return 'search';
|
||||
if (p.startsWith('/rankings')) return 'rankings';
|
||||
if (p.startsWith('/compare')) return 'compare';
|
||||
// `/schools/` before `/school/`: they differ by one letter and mean
|
||||
// different things — the location layer versus a single school. Checked
|
||||
// first so the narrower-looking prefix cannot shadow it if either string
|
||||
// is ever edited.
|
||||
if (p.startsWith('/schools/')) return 'place';
|
||||
if (p.startsWith('/school/')) return 'detail';
|
||||
return 'direct';
|
||||
return classifyPath(ref.pathname);
|
||||
} catch {
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user