Appendix B·Appendix
Annotated Code Artifacts
This appendix reproduces the code that carried the reconstruction, annotated. The excerpts are lightly trimmed for length and anonymized where a brand string would appear, but the logic is verbatim from the production change set. File paths are given so a reader can locate the analog in their own codebase. The chapters that argue for each change are cross-referenced.
The stack is Next.js 15 (App Router), TypeScript, Drizzle over PostgreSQL, and gtag.js plus the
Measurement Protocol. None of these artifacts contains a brand identifier; the GA credentials
live in environment variables (NEXT_PUBLIC_GA_ID, GA_API_SECRET, ADMIN_SECRET).
B.1 The server-side generate_lead, before and after
This is the artifact at the center of Chapter 6, The Server-Side Measurement Protocol. The first block is what the audit found. Read the host and the fallback before reading the fix.
Before (the defect):
// src/app/api/contact/route.ts (as found)
if (process.env.NEXT_PUBLIC_GA_ID && process.env.GA_API_SECRET) {
const clientId = attribution?.ga_client_id || `server.${Date.now()}`
fetch(
`https://www.google.com/mp/collect?measurement_id=${process.env.NEXT_PUBLIC_GA_ID}&api_secret=${process.env.GA_API_SECRET}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: clientId,
events: [{
name: 'generate_lead',
params: {
currency: 'USD',
value: 150,
form_name: source || 'contact-form',
service_type: service || 'general',
source: attribution?.utm_source || 'direct',
medium: attribution?.utm_medium || '',
campaign: attribution?.utm_campaign || '',
},
}],
}),
}
).catch(() => { /* non-blocking */ })
}
Four defects sit in that block. The host is www.google.com, which returns 404 and records
nothing. The client_id falls back to server.${Date.now()}, minting a fresh pseudonymous user
on every cookie-less lead. The payload omits session_id and engagement_time_msec, so a hit
that did land would attribute to (direct)/(none). The .catch(() => {}) swallows every
failure, which is why a 404 ran silent for the property lifetime.
After (the fix):
// src/app/api/contact/route.ts
// Internal GA4/Ads lead-value weights (rough expected revenue per lead = job value ×
// assumed close rate). NOT customer pricing and never shown to users. PROVISIONAL.
const SERVICE_LEAD_VALUE: Record<string, number> = {
termite: 400, fumigation: 500, commercial: 400, 'bed-bug': 250,
rodent: 180, pest: 150, general: 150, ant: 100, 'wasp-bee': 100,
other: 120, default: 150,
}
// ... inside POST(), after the lead row is written to PostgreSQL:
if (process.env.NEXT_PUBLIC_GA_ID && process.env.GA_API_SECRET && attribution?.ga_client_id) {
const leadValue = SERVICE_LEAD_VALUE[service as string] ?? SERVICE_LEAD_VALUE.default
fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.NEXT_PUBLIC_GA_ID}&api_secret=${process.env.GA_API_SECRET}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: attribution.ga_client_id,
ip_override: ip,
events: [{
name: 'generate_lead',
params: {
currency: 'USD',
value: leadValue,
form_source: source || 'contact-form',
service_type: service || 'general',
engagement_time_msec: 1,
...(attribution.ga_session_id ? { session_id: attribution.ga_session_id } : {}),
},
}],
}),
}
)
.then(r => { if (!r.ok) console.error('[contact route] GA4 MP non-200:', r.status) })
.catch(err => console.error('[contact route] GA4 MP failed:', err))
}
The guard changed from two conditions to three. The send now requires attribution?.ga_client_id
to be present, which is the deliberate refusal to invent a user for an untrackable lead argued in
Chapter 6. The host is corrected. ip_override: ip geolocates the conversion to the visitor.
engagement_time_msec: 1 makes the event count in the session model. session_id is spread in
only when it was captured. form_name became form_source to match the client parameter and the
registered dimension (Chapter 8). The bare source, medium, and campaign parameters are gone,
because GA4 reads event-scoped traffic parameters as manual overrides that corrupt attribution
(Chapter 9). The response is inspected and logged (Chapter 13).
The careers route carried the same broken block under a different event name, career_application,
which never matched the client career_application_submit. Rather than fix an orphan for a
low-volume form, the reconstruction deleted the server send there and kept the client key event.
B.2 Reading client_id and session_id from the first-party cookies
The server holds no cookie of its own. The browser forwards the attribution object with the form POST, and the client reads the two identifiers out of GA4's own cookies. Chapter 2 explains the cookie formats; this is the parser.
// src/lib/utm.ts (inside getAttribution(), called at submit time)
const gaMatch = document.cookie.match(/_ga=GA\d+\.\d+\.(\d+\.\d+)/)
if (gaMatch?.[1]) stored.ga_client_id = gaMatch[1]
// Session id from the _ga_<container> cookie so the server Measurement Protocol event
// stitches to the real session. Handles both GS1 (dot-delimited) and GS2 ($-delimited).
const sess = document.cookie.match(/_ga_[A-Z0-9]+=([^;]+)/)?.[1]
if (sess) {
const sid = sess.includes('$') ? sess.match(/s(\d+)/)?.[1] : sess.split('.')[2]
if (sid) stored.ga_session_id = sid
}
The _ga cookie holds GA1.1.<client_id>, where client_id is the two-segment value after the
version prefix. The _ga_<container> cookie holds the session. Google shipped two formats: the
legacy GS1.1.<sessionId>.<...>, where the session id is the third dot-delimited segment, and the
newer GS2.1.s<sessionId>$o1$g0$..., where the session id follows the s. The parser branches on
the presence of $. A generic _ga_[A-Z0-9]+ match avoids hardcoding the container id, so the
same code works if the measurement id changes.
B.3 One page_view per route: RouteAnalytics
Chapter 8 treats the double-firing page_view as a taxonomy problem. The fix moves page-view
emission out of the config and the per-page tracker into a single component mounted in the layout.
// src/components/ui/route-analytics.tsx
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
import { trackPageView } from '@/lib/analytics'
function derive(path: string) {
if (path === '/') return { page_type: 'home' }
const [a, b] = path.split('/').filter(Boolean)
if (a === 'services' && b) return { page_type: 'service', service_slug: b }
if (a === 'pests' && b) return { page_type: 'pest', pest_slug: b }
if (a === 'locations' && b) return { page_type: 'location', location_slug: b }
if (a === 'careers' && b) return { page_type: 'careers', job_slug: b }
if (a === 'contact') return { page_type: 'contact' }
if (a === 'about' || a === 'team') return { page_type: 'about' }
if (a === 'faq') return { page_type: 'faq' }
if (a === 'resources' || a === 'articles') return { page_type: 'resources' }
return { page_type: 'other' }
}
export function RouteAnalytics() {
const pathname = usePathname()
useEffect(() => { trackPageView(derive(pathname)) }, [pathname])
return null
}
The component derives page_type and the relevant slug from the URL path and fires one
page_view on mount and on every usePathname change, which covers both the initial load and
client-side navigations across the roughly 1,200 static pages. It works only in concert with two
other changes: send_page_view:false on the config (below), and Enhanced Measurement page-change
tracking switched off (Appendix A), so no automatic page_view competes with this one.
B.4 The gtag bootstrap: consent, afterInteractive, send_page_view:false
Chapter 10 covers Consent Mode; Chapter 8 covers the load-strategy and page-view flags. All three live in the inline bootstrap.
// src/app/(frontend)/layout.tsx
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
strategy="afterInteractive"
/>
<Script id="ga-init" strategy="afterInteractive">
{`window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}
gtag('consent','default',{ad_storage:'granted',ad_user_data:'granted',ad_personalization:'granted',analytics_storage:'granted'});
gtag('consent','default',{ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied',analytics_storage:'denied',region:['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV','LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE','IS','LI','NO','GB','CH'],wait_for_update:500});
gtag('js',new Date());
gtag('config','${GA_ID}',{send_page_view:false})`}
</Script>
The first consent default grants every signal, which is the state for the US audience that makes
up this property's traffic, so nothing changes for the real users. The second consent default,
scoped to the 32 EEA, UK, and Switzerland region codes, denies storage until an update arrives.
The order matters: the region-scoped default overrides the global default for a matching visitor.
strategy moved from lazyOnload to afterInteractive so the tag loads right after hydration and
sets the _ga cookie before a fast visitor submits a form, which lifts the ceiling discussed in
Chapter 6. send_page_view:false hands page-view emission to RouteAnalytics.
B.5 Server Actions that guard themselves
The lead-outcome editor (Chapter 11) writes to the database through a Next.js Server Action. Route middleware protects page loads, but a Server Action is an independently addressable endpoint, so it verifies the session itself.
// src/lib/admin-auth.ts
import { cookies } from 'next/headers'
export async function verifyAdminToken(token: string | undefined): Promise<boolean> {
if (!token) return false
const secret = process.env.ADMIN_SECRET
if (!secret) return false
try {
const dot = token.lastIndexOf('.')
if (dot === -1) return false
const expiry = token.slice(0, dot)
const sig = token.slice(dot + 1)
if (Date.now() > parseInt(expiry, 10)) return false
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])
const sigBytes = Uint8Array.from(atob(sig.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
return await crypto.subtle.verify('HMAC', key, sigBytes, new TextEncoder().encode(expiry))
} catch { return false }
}
export async function requireAdmin(): Promise<void> {
const token = (await cookies()).get('admin_session')?.value
if (!(await verifyAdminToken(token))) throw new Error('Unauthorized')
}
// src/app/(admin)/admin/actions.ts
'use server'
import { db } from '@/lib/db'
import { contactSubmissions } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { revalidatePath } from 'next/cache'
import { requireAdmin } from '@/lib/admin-auth'
const STAGES = ['new', 'contacted', 'quoted', 'won', 'lost']
export async function updateLeadStatus(id: number, stage: string, jobValue: number | null) {
await requireAdmin()
if (!STAGES.includes(stage)) throw new Error('Invalid stage')
const closed = stage === 'won' || stage === 'lost'
await db.update(contactSubmissions)
.set({
stage,
jobValue: stage === 'won' ? (jobValue ?? null) : null,
closedAt: closed ? new Date() : null,
})
.where(eq(contactSubmissions.id, id))
revalidatePath('/admin')
}
requireAdmin() reads the signed admin_session cookie and verifies its HMAC and expiry with the
same scheme the middleware uses. The action calls it first, validates the stage against an
allow-list, sets job_value only for won deals, stamps closed_at on a terminal stage, and
revalidates the page. A reader porting this pattern should note the lesson: a Server Action is not
covered by the route matcher that protects the page it lives on.
B.6 The outcome data model
The offline-revenue loop (Chapter 11) needs somewhere to record what became of a lead. The columns were added to the production database with an idempotent migration, then mirrored in the Drizzle schema.
-- production migration, applied with psql
ALTER TABLE contact_submissions
ADD COLUMN IF NOT EXISTS attribution_wbraid varchar,
ADD COLUMN IF NOT EXISTS attribution_gbraid varchar,
ADD COLUMN IF NOT EXISTS lead_id varchar,
ADD COLUMN IF NOT EXISTS stage varchar DEFAULT 'new',
ADD COLUMN IF NOT EXISTS job_value integer,
ADD COLUMN IF NOT EXISTS closed_at timestamptz;
// src/lib/db/schema.ts (Drizzle additions to contactSubmissions)
attributionWbraid: varchar('attribution_wbraid'),
attributionGbraid: varchar('attribution_gbraid'),
leadId: varchar('lead_id'),
stage: varchar('stage').default('new'),
jobValue: integer('job_value'),
closedAt: timestamp('closed_at', { withTimezone: true }),
Every column is nullable or defaulted, which makes the migration backward compatible: the running
application ignored the new columns until the deploy that referenced them shipped, so there was no
window where old code met a new schema and failed. attribution_wbraid and attribution_gbraid
capture the iOS and Safari click identifiers that gclid alone misses, and the capture happens
now so the history exists when Google Ads links later. lead_id is an opaque UUID for internal
reconciliation, never an email or phone number.
See Appendix A for the property configuration these artifacts produced, and Appendix D for the Admin API calls that set the server-side configuration.