# Session Tracking Architecture: Client-Side vs Server-Side Approaches
*principal-ade/landing-page@main · investigation · 9 markers*

> **Current state.** Session tracking runs entirely client-side using sessionStorage, with events sent directly to Google Analytics from the browser.
>
> **Scale question.** Should session state live server-side for better reliability, cross-device tracking, and protection against ad blockers — or does the current client-side approach meet production needs?
>
> **Trade-off.** Client-side is simpler and avoids server load; server-side offers data accuracy, GDPR control, and bot filtering at the cost of infrastructure.

**Author:** Julie Allen · **Updated:** 2026-06-15T16:13:39.460Z

## Markers
1. **Session initialization on client** — `src/lib/analytics/journey/sessionManager.ts:60–89`
   User lands on the page. `initSession` checks sessionStorage for an existing session — if found and not expired (30-minute timeout), resume it; otherwise generate a fresh `sessionId` with timestamp + random suffix, store it in sessionStorage, and fire `session_start` to GA4. Session state: `sessionId`, `startTime`, `landingPage`, `referrer`, `pagesVisited[]`, `currentPage`. All stored client-side.
2. **sessionStorage as the source of truth** — `src/lib/analytics/journey/sessionManager.ts:48–57`
   Every session mutation writes to `sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session))`. This is synchronous, tab-local, and survives page refreshes but not tab close. No server involved. If the user switches devices, the session is lost. If an ad blocker is active, GA4 events fire but may be dropped — the session state in storage remains, but analytics gaps appear.
3. **Page navigation updates session** — `src/lib/analytics/journey/sessionManager.ts:92–124`
   On route change, `trackPageNavigation` compares `session.currentPage` with the new path. If different: fires a `page_navigation` event to GA4 with `from_page`, `to_page`, and `navigation_type` (`link`|`back`|`forward`|`direct`), updates `currentPage`, appends to `pagesVisited[]` if unique, and bumps `lastActivityTime`. All mutations written back to sessionStorage.
4. **React hook calls session tracker** — `src/lib/analytics/hooks/usePageTracking.ts:18–35`
   `usePageTracking` runs on every route change (Next.js App Router's `usePathname`/`useSearchParams`). Calls `trackPageView(url)` to send `page_view` to GA4, then calls `trackPageNavigation(pathname, navigationType)` to update the session journey. Session update happens client-side; no server roundtrip.
5. **Events sent directly to GA4** — `src/lib/analytics/core.ts:49–71`
   All analytics events (`session_start`, `page_navigation`, `click`, `scroll_depth`, etc.) go through `event(...)`, which calls `window.gtag('event', action, {...})` — a direct browser→Google Analytics POST. No server middleware. This means: (1) ad blockers can drop events, (2) no server-side enrichment (IP geolocation, user-agent parsing, bot filtering), (3) no fallback if GA4 is unreachable.
6. **Session end on page unload** — `src/lib/analytics/journey/sessionManager.ts:136–160`
   `endSession` fires on `beforeunload` (or after 30-minute inactivity). Reads session from storage, calculates `sessionDurationSeconds`, sends `session_end` event to GA4 with `exit_page`, `total_pages`, and `pages_visited[]` joined as a string, then deletes the sessionStorage entry. If the user closes the tab abruptly and the `beforeunload` event is blocked (common on mobile), the session end event may never fire — server-side tracking avoids this by inferring session end from inactivity.
7. **Server-side session store (alternative)** — _(no file)_ _(subject)_
   **Alternative architecture:** Session state lives server-side in a persistent store (PostgreSQL sessions table or Redis with TTL). When the user lands, the client sends a `POST /api/analytics/session/start` with referrer/landing page; server generates a cryptographically-secure `sessionId` (uuid v4), writes to DB, returns it to client. Client stores only the `sessionId` in a cookie (httpOnly, SameSite=Strict). On every event (page view, click, scroll), client POSTs to `/api/analytics/events` with `sessionId`; server validates, enriches (IP→location, user-agent→device), writes to DB, and optionally forwards to GA4 via Measurement Protocol. Session timeout enforced server-side (last_activity_at). Benefits: survives ad blockers, enables cross-device tracking (same sessionId from cookie), GDPR-compliant data deletion, bot filtering. Trade-offs: adds latency (client→server→GA4 instead of client→GA4), requires DB schema + migration, increases server load (every event is a DB write).
8. **Server API route for event ingestion (hypothetical)** — _(no file)_
   **Hypothetical implementation:** Create `/api/analytics/events/route.ts`. POST handler: (1) extract `sessionId` from cookie, (2) validate against sessions table (404 if not found or expired), (3) parse event payload (type, params), (4) enrich with server context (req.headers['x-forwarded-for'], req.headers['user-agent'], timestamp), (5) insert into `analytics_events` table, (6) update `sessions.last_activity_at`, (7) optionally forward to GA4 Measurement Protocol (server-to-server POST to `https://www.google-analytics.com/mp/collect`). Returns 204. Client hooks (`usePageTracking`, `useClickTracking`) change from calling `window.gtag` to `fetch('/api/analytics/events', { method: 'POST', body: JSON.stringify(event) })`. No source file exists yet — this marker describes what the server-side path would look like.
9. **Root layout wires analytics provider** — `src/app/layout.tsx:55–86`
   `AnalyticsProvider` wraps the app tree and injects the `useAnalytics()` context. `AnalyticsTracker` mounts the automatic tracking hooks (`usePageTracking`, `useClickTracking`, `useScrollTracking`, etc.). Google Analytics script tag loads via `<GoogleAnalytics gaId={...} />` when `NEXT_PUBLIC_GA_ID` is set. All client-side — server components don't participate in session tracking.

---
Interactive view: https://app.principal-ade.com/trail/e16d8a04-c898-46d9-8bac-500fb06f1922
JSON: https://app.principal-ade.com/api/trails/by-id/e16d8a04-c898-46d9-8bac-500fb06f1922
Authored at `37ae714` (main).
To open a trail or tour locally in the interactive viewer, see https://app.principal-ade.com for the Principal CLI quickstart.
