Chapter 9·Part III · Implementation: Correctness and Architecture
Identity, Attribution, and Value
Identity, attribution, and value form a single dependency chain, and a break at any link degrades the two that follow. Identity answers who acted. Attribution answers which marketing touch deserves credit for that action. Value answers how much the action was worth. If GA4 cannot resolve a stable identity for a lead, it cannot join that lead to the session that produced it, and if it cannot join the session it cannot assign the touch a dollar figure that a bidding algorithm could later optimize against. This chapter treats the three as one system because the {COMPANY} property forces the point: a local pest-control business with no online transaction, whose revenue lands offline as booked jobs, has to reconstruct the entire chain from cookie fragments and database columns rather than reading it off a checkout. Chapter 6 (The Server-Side Measurement Protocol) established how the server event is transported; Chapter 7 (Conversion Architecture) established what counts as a conversion; this chapter explains how a person, a source, and a number get stitched to that conversion, and why the stitching caps out at the trackable share of traffic.
The Identity Ladder: client_id and session_id
GA4 identity is a ladder of increasingly durable keys. At the bottom sits the device-and-browser identity, client_id, minted by the gtag JavaScript and written to the first-party _ga cookie with a two-year lifetime. Above it, when a business enables Google Signals and a user is signed in to Google, sits a cross-device User-ID. This property runs with Google Signals deliberately off (Chapter 5 recorded the reason: at roughly 500 sessions per month, Signals mainly triggers data thresholding that hides rows in reports, and it buys almost no cross-device resolution at this volume). So client_id is the ceiling of identity here. Everything the property knows about a returning visitor, it knows through that one cookie.
The _ga cookie is a device-level identity that persists for two years once gtag writes it. Its value looks like GA1.1.1234567890.1700000000, and the client_id GA4 cares about is the last two dot-delimited fields, a random integer and the epoch-seconds timestamp of first write. The server-side lead pipeline reads this cookie directly. When the contact API builds a Measurement Protocol payload for the generate_lead event, it parses _ga, extracts the client_id, and sends it so that the server event joins the same GA4 user the browser already created. Chapter 6 covered the transport defect that made this moot for the property's entire prior lifetime (both routes posted to https://www.google.com/mp/collect, which returns 404, instead of https://www.google-analytics.com/mp/collect, which returns 204). Assume the corrected host from here; the identity question is what the payload carries once it reaches a live endpoint.
The session key and GS1/GS2 parsing
client_id identifies the device across two years. It does not identify the visit. GA4 sessionization lives in a second cookie, _ga_<container>, where the suffix is the measurement-ID stream container (for stream 14548640408 under G-WH410Z73V1). This cookie encodes the current session_id, which GA4 needs to attach a server event to the specific session that the browser opened, rather than treating the event as a sessionless orphan. The original Measurement Protocol payload omitted session_id entirely. The consequence is precise and it is not a rounding error: an event that arrives with a valid client_id but no session_id cannot be matched to the visit's traffic source, so GA4 attributes it to (direct)/(none). A lead that came from google/organic would have been booked to direct traffic, silently, even after the host was fixed. Attribution failure hides inside an identity omission.
Parsing the session cookie is fussier than parsing _ga because Google shipped two formats and left both in the wild. The legacy GS1 format is dot-delimited; the newer GS2 format is dollar-delimited and packs the session fields as key-value pairs. The rewritten block handles both:
// _ga_<container> value, two possible shapes:
// GS1.1.<session_id>.<n>.<...> (dot-delimited, legacy)
// GS2.1.s<session_id>$o<n>$g<...>$... (dollar-delimited, current)
function parseSessionId(cookieVal: string): string | null {
if (cookieVal.startsWith('GS1.')) {
return cookieVal.split('.')[2] ?? null;
}
if (cookieVal.startsWith('GS2.')) {
const seg = cookieVal.split('$').find(p => p.startsWith('s'));
return seg ? seg.slice(1).split('.')[0] : null;
}
return null;
}
Alongside session_id, the payload now sets engagement_time_msec to 1. GA4 uses a nonzero engagement signal to count the event's session as engaged; without it, the server event can fail to register the session as active and again drift toward misattribution. The payload also sets ip_override to the request IP so the conversion geolocates to the lead's location in Central California rather than to the VPS datacenter that hosts the Coolify/Traefik container. For a business whose entire market is Monterey, Santa Cruz, San Benito, and Santa Clara counties, geolocating conversions to the server would poison every geographic report. Three fields, session_id, engagement_time_msec, and ip_override, convert a technically-delivered-but-useless event into an attributable one.
The ad-block ceiling: the measured 69 percent
Server-side collection is often sold as the fix for ad blockers, and the pitch overstates the case. The server can only send what identity it holds, and it inherits that identity from the browser's _ga cookie. If a blocker or a privacy browser prevented gtag from ever running, no _ga cookie exists, so the server has no client_id to send. Server-side collection defeats blockers that suppress the outbound beacon to Google; it does not resurrect an identity that was never minted client-side.
The property makes this measurable because every lead persists its captured ga_client_id to the contact_submissions row. Reading the table directly gave a hard number: of 29 leads, 20 carried a ga_client_id and 9 did not, a 69 percent capture rate. The missing 31 percent is not a timing artifact. The _ga cookie persists for two years once written, so a returning visitor who submits a form days later still carries it; the gap is the share of visitors whose browsers never let gtag write the cookie in the first place, which is ad-block and privacy-browser loss. That 69 percent is the ceiling on server-side attribution for this property. No configuration change lifts it, because the loss happens upstream of any server the business controls.
The correct engineering response to that ceiling is to stop faking identity when it is absent. The original code fell back to server.<timestamp> whenever _ga was missing, minting a fresh phantom client_id for each such lead. Every phantom became a brand-new single-event user in GA4, inflating the user count and attaching a generate_lead to a person who would never be seen again. The rewrite deletes the fallback: if no real ga_client_id is present, the code skips the Measurement Protocol send rather than fabricating one. An untrackable lead stays untracked in GA4 and lives only in the database, which is honest, and it keeps the GA4 user count clean. This behavior was confirmed end to end. A real test submission, row id 32, went through the header quote panel on a clean Windows Chrome user-agent that carried no _ga cookie; the row landed in the database with an empty ga_client_id, and the code correctly withheld the Measurement Protocol send. The database keeps the lead for the offline loop (Chapter 11); GA4 does not receive a hallucinated user. Two environmental constraints made that human submission the only viable test: headless Chrome in the test environment could not reliably load gtag.js from Google's CDN, and Cloudflare Turnstile on the public forms blocked automated submissions.
UTM Governance and First-Touch Storage
GA4 assigns a session's traffic source from campaign parameters and referrer, and it does so under a last-non-direct model that this chapter treats in detail below. A lead-generation business needs a second, independent record of source, because the session that submits the form is frequently not the session that first discovered the brand. The property captures both. Every contact_submissions row stores utm_source, utm_medium, utm_campaign, utm_content, utm_term, plus gclid, fbclid, landing_page, and referrer, read at submit time. Phase 2 extended the click-identifier capture to wbraid and gbraid, the iOS and Safari click IDs that a gclid-only capture misses, because Apple's privacy handling routes many paid clicks through those parameters instead of gclid. It also generates a lead_id UUID per lead. Chapter 11 uses gclid, wbraid, gbraid, and lead_id to close the offline loop; the point here is that first-party capture of these identifiers is an identity decision, not a reporting nicety, and it belongs in the database the moment the form posts.
The subtle governance issue is the divergence between a client-captured first touch and GA4's own attribution. A common pattern, and one appropriate for this property, is a sessionStorage first-touch snapshot: on the first page of a visit, JavaScript reads the current UTMs and referrer and writes them to sessionStorage (or a first-party cookie for cross-visit persistence), so that a form submitted three pages deep still carries the source that opened the visit. That stored value is a genuine first-touch record. GA4's session traffic source is not first-touch; it is last-non-direct, meaning GA4 credits the most recent campaign or referral that was not direct, carrying a prior source forward only across direct sessions. The two models will disagree whenever a user arrives via google/organic, leaves, and returns by typing the URL: the sessionStorage capture on the second visit shows the direct arrival, while GA4 credits the earlier organic touch. Neither is wrong; they answer different questions. A practitioner who stores a first-touch UTM and then compares it to GA4's session source without understanding the model will chase a discrepancy that is definitional. Document which model each store uses, and reconcile deliberately.
Governance also means refusing to write source data into fields that corrupt GA4's own attribution. The original server generate_lead payload carried bare source, medium, and campaign parameters. GA4 reads event-scoped source/medium/campaign as manual traffic-source signals, so sending them on a server event can overwrite the session's real attribution with values the server guessed. The rewrite drops those bare parameters entirely and lets GA4 attribute from client_id plus session_id. The UTMs still live in the database for the offline loop and for the property's own reporting; they simply no longer contaminate the GA4 session-source assignment. The distinction is exact: first-party UTM storage is for the business's records and for Google Ads offline upload; event-scoped source parameters on a Measurement Protocol hit are a foot-gun that rewrites attribution, and this property does not fire them.
Channel Grouping for a Local Service Business
GA4's default channel grouping was built for a generic web economy, and it flattens distinctions that matter to a local operator. The 30-day source/medium mix the audit found makes the problem concrete:
| Source / medium | Sessions |
|---|---|
| (direct) / (none) | 234 |
| google / organic | 192 |
| google / cpc | 46 |
| yahoo / organic | 16 |
| bing / organic | 12 |
| ig / social | 4 |
| facebook.com / referral | 3 |
| blue.bbb.org / referral | 2 |
| chatgpt.com / ai-assistant | 2 |
| yelp.com / referral | 1 |
For a pest-control business, the channels that drive booked jobs are not the ones a default grouping foregrounds. Google Business Profile and Google Maps discovery, the single most important acquisition surface for a local service, land inconsistently: some arrive as google/organic, some as direct when a user taps a Maps listing that opens the site without campaign tags. Local directories carry high intent for this vertical: blue.bbb.org/referral and yelp.com/referral are people actively shopping for a licensed pest-control provider, and folding them into a generic "Referral" bucket with facebook.com erases that intent difference. A well-built property separates Google Business Profile and Maps from generic organic search, isolates local-directory referrals (BBB, Yelp) from social referrals, and keeps the emerging chatgpt.com/ai-assistant channel visible rather than dumping it into an "Unassigned" or "Referral" catch-all. The lever GA4 provides is UTM tagging of the Business Profile website link (so Maps traffic arrives with a stable, self-identified source) plus a custom channel group that maps directory hosts to a Local Directories channel.
The property deferred building that custom channel group, and the deferral is defensible at this volume. GA4's default channels already separate the coarse mix (Organic Search, Paid Search, Direct, Referral, Organic Social), and at roughly 500 sessions per month the analytical return on a bespoke grouping is low relative to the effort and the risk of misconfiguring a rule that silently reclassifies history. The higher-leverage move is UTM discipline on the Business Profile link, which fixes the data at the source and needs no grouping logic. When directory and Maps volume grows enough that the buckets blur decisions, the custom channel group becomes worth its maintenance cost. The USER-scoped session_source custom dimension already on the property (Chapter 8) preserves a durable source signal per user in the meantime, so the raw material for the eventual grouping is being retained.
The Attribution Model and the Volume Threshold Reality
GA4's default reporting attribution model is data-driven attribution (DDA), which distributes conversion credit across touches using the property's own conversion patterns. DDA carries a training-volume requirement, historically on the order of 400 conversions per 28 days, below which Google cannot fit a stable model. A property under that threshold does not receive an error; it silently falls back to last-click behavior for practical purposes, because the model has too little signal to differentiate touches. This property is far under the line. With around 46 paid clicks per month, roughly 500 sessions per month, and lead conversions in the low tens, DDA has nothing to learn from, so every attribution report is effectively last-click no matter which model the interface names. A practitioner who reads GA4's model label as a description of what is happening will mistake a fallback for a choice. The honest statement for a small local property is that it runs on last-click, and its multi-touch reports should be read as last-touch summaries.
The same volume reality governs bidding downstream. Value-based Smart Bidding needs roughly 15 to 30 conversions per month before Google's optimizer stabilizes on value rather than noise, and data-driven attribution's ~400-per-28-days line sits far above anything this property will feed it soon. So even after the offline-revenue loop pipes booked job values to Google Ads, the automated bidding will not fully engage at this scale. That does not make the loop pointless. Its immediate payoff is cost-per-booked-job visibility, the ability to divide google/cpc spend by actually-won jobs rather than by raw form fills, which is a reporting and budgeting win that stands on its own regardless of whether the bidder ever reaches value-based mode. Build the loop for the measurement it provides today; treat automated value bidding as a capability that unlocks later, at volume the business does not yet have.
How Value Reaches Google Ads, and Why the Offline Loop Is the Real Value Channel
Value has to be a number before it can travel. This property assigns provisional, internal per-service estimates in the Measurement Protocol payload, replacing a flat value: 150 that treated a $100 ant call and a $500 fumigation as identical:
const LEAD_VALUE: Record<string, number> = {
termite: 400, fumigation: 500, commercial: 400, 'bed-bug': 250,
rodent: 180, general: 150, pest: 150, ant: 100, 'wasp-bee': 100,
other: 120,
};
These are labeled provisional and internal for a reason that governs the whole value layer: GA lead value is a modeling input, never a figure shown to a customer, so it can be an honest internal estimate without creating any external promise. The generate_lead event, now promoted to a key event and now actually reaching GA4, carries this estimated value with currency: USD.
Here is the mechanism, and its limit. Once the property links to Google Ads (the audit found google/cpc spend with no Ads link, a gap Chapter 7 flags), GA4 key events and their values become importable as Ads conversions, and the estimated per-service value flows into the Ads account as the conversion value the bidder optimizes against. That path carries an estimate, not truth. The generate_lead value is what the business guesses a lead of that service type is worth, assigned at submit time before anyone has quoted or closed the job. For a lead-gen business the gap between estimated lead value and realized job value is the entire commercial question, and no client-side or server-side event can close it, because the job closes weeks later in the office, not in the browser.
The offline-revenue loop closes it. Chapter 11 details the pipeline; the identity-and-value foundation for it was laid here and in Phase 2. Each lead now carries a lead_id UUID and the full click-identifier set (gclid, wbraid, gbraid), and the contact_submissions table gained stage, job_value, and closed_at columns so the office can record what a lead actually became. When a lead is marked won with a real booked job_value, that realized number, keyed back to the original click by gclid/wbraid/gbraid, is what should reach Google Ads as an offline conversion, superseding the submit-time estimate. That is why the offline loop, not the generate_lead event, is the real value channel for this business. The event delivers a fast, estimated signal good enough to register a conversion and geolocate it; the offline upload delivers the true revenue signal that a value optimizer would eventually need. The identity work in this chapter, the honest client_id, the session_id stitch, the click-identifier capture, and the refusal to fake or corrupt source, exists so that when a job closes offline, the property can trace that dollar back to the exact paid click that started it.
Cross-references: the transport and payload construction live in Chapter 6 (The Server-Side Measurement Protocol); conversion selection and the Google Ads link gap in Chapter 7 (Conversion Architecture); the custom dimensions that carry source and slug context in Chapter 8 (Event Taxonomy and Custom Dimensions); the EEA/UK consent defaults that gate identity collection by region in Chapter 10 (Privacy and Consent by Design); the end-to-end offline pipeline and the BigQuery join surface in Chapter 11 (The Offline-Revenue Loop) and Chapter 12 (Warehousing and Reporting).