Machina

Chapter 2·Part I · Foundations

The GA4 Data Model as a Working Mental Model

Every decision in the chapters that follow rests on a small set of primitives: an event, a parameter, a scope, an identity, a session, and a transport. A practitioner who holds these six ideas in the right relation can read a property's configuration and predict how it will behave under load, under ad-blocking, and under migration pressure. A practitioner who does not will misdiagnose. The audit of GA4 property 534525683 (measurement ID G-WH410Z73V1, web stream 14548640408, in Google Cloud project gen-lang-client-0444184725) surfaced defects that looked unrelated on the surface, a phantom-user problem, an item report that never populated, a server conversion that produced zero rows, and a two-month data ceiling. Each traced back to a misreading of one primitive. This chapter builds the shared mental model so that Chapter 5, "Audit Findings," can catalog those defects without re-teaching the vocabulary, and so that the implementation chapters can name a fix and assume you know why it works.

The case property belongs to {COMPANY}, a pest-control operator serving Central California (Monterey, Santa Cruz, San Benito, and Santa Clara counties). It carries no online transaction: revenue arrives as phone calls and web form leads that convert to booked jobs offline, days or weeks later. That fact shapes the model. A measurement design tuned for an e-commerce cart, where the money event fires in the browser, will fit this business badly, and much of what trips up the migrator (discussed in section 2.7) comes from carrying cart-shaped assumptions into a lead-shaped world.

2.1 The Event-and-Parameter Core

GA4 records exactly one kind of thing: an event. There is no separate concept of a pageview, a screen view, a social interaction, a timing hit, or a transaction as a first-class type. Each of those is an event with a name and a bag of parameters. A pageview is an event named page_view. A form submission on the {COMPANY} site is an event named contact_form_submit. A call-to-action tap is cta_click. The model collapses to a single sentence: something happened (the name), and here is what characterized it (the parameters).

An event name is a short string. A parameter is a key and a value attached to that specific event occurrence. When the {COMPANY} homepage sends page_view, it attaches page_type: home. When a visitor loads /services/termite-control, the client sends page_view with page_type: service and service_slug: termite-control. When a visitor loads /locations/salinas, the client sends page_view with page_type: location and location_slug: salinas. The event name is stable across all three; the parameters carry the detail that makes one pageview analytically different from another.

This is the first mental shift a practitioner must internalize: in GA4, meaning lives in parameters, not in event names. The temptation, especially for someone coming from a system with rigid hit types, is to encode detail into the name, to mint page_view_service and page_view_location and page_view_pest. That produces a sprawling, low-cardinality-per-name event list that resists aggregation. You cannot ask "how many pageviews did the site serve" without summing across a dozen names, and you cannot pivot pageviews by type because the type is fused into the identifier. The {COMPANY} design does the opposite and does it well: one page_view name, with page_type as the pivot and a single slug parameter (service_slug, location_slug, pest_slug, or job_slug) carrying the specific page. Chapter 8, "Event Taxonomy and Custom Dimensions," develops the naming discipline in full. For the mental model, hold this: name the verb, parameterize the nouns.

GA4 ships some parameters on every event automatically. The measurement tag stamps a timestamp, a page location and title, a language, a screen resolution, and, load-bearing for everything in this chapter, an identity and a session stamp. It also fires a set of automatic and Enhanced Measurement events without any code from the site author: session_start, first_visit, scroll, click on outbound links, and, when Enhanced Measurement's relevant toggles are on, page_view on history changes and form_start / form_submit on form interactions. That automation is a convenience and a hazard. In the case property it was a hazard: Enhanced Measurement had all seven options enabled, including pageChangesEnabled and formInteractionsEnabled, which meant GA4's own machinery double-fired page_view on client-side navigation and fired its own form_start and form_submit alongside the site's hand-authored form_start, contact_form_submit, and quote_form_submit. Two systems measuring the same act produce inflated counts and ambiguous funnels. Section 2.6 returns to why the site author and the platform can collide, and Chapter 14, "Anti-Pattern Catalog," records the collision as a named anti-pattern.

2.2 Parameters, Registration, and the Custom Dimension Layer

Sending a parameter and being able to report on it are two different things. GA4 accepts arbitrary parameters on custom events and stores them, but it will not expose a custom parameter in standard reports or in the Explore surface until you register it as a custom dimension (for a text-like value) or custom metric (for a number you want to aggregate). Registration is the act that promotes a raw parameter into a queryable field. Until you perform it, the value rides along in the raw event stream (and in the BigQuery export, if one exists) but stays invisible to the report builder.

The case property had eight custom dimensions registered at audit time:

DimensionScope
cta_locationEvent
page_typeEvent
job_slugEvent
location_slugEvent
service_slugEvent
form_sourceEvent
pest_slugEvent
session_sourceUser

Seven are event-scoped. One, session_source, is user-scoped. That single difference in the last column is the most consequential design decision in the whole dimension table, and it was set wrong. To see why, you need the scope model.

Scope is a design decision, not a formality

GA4 offers three scopes for a custom dimension: event, user, and item. Scope answers the question, "to what does this value belong, and how long does it persist." An event-scoped dimension attaches to the single event that carried it and describes only that event. page_type: service is true of one page_view and says nothing about any other event that user later fires. That is the correct scope for nearly everything descriptive: the page a hit occurred on, the source of a form, the location of a clicked CTA. Each of page_type, service_slug, location_slug, pest_slug, job_slug, form_source, and cta_location describes the circumstances of one event and rightly carries event scope.

A user-scoped dimension attaches to the user identity and describes the person across their whole history in the property. When you write a user-scoped value, GA4 stores it against the identity and, on every subsequent event from that user, reports the most recently written value. This is the mechanism, and it is a memory that keeps only the last write. User scope is correct for genuinely durable traits: a loyalty tier, an account type, a first-touch channel you deliberately freeze. It is wrong for anything that legitimately changes across visits, because GA4 will overwrite silently and retroactively recolor the user's entire history with whatever value landed last.

An item scope exists too. It attaches to individual products inside the items[] array of an e-commerce event, so that an item_category or item_variant describes one line item rather than the whole event or the whole user. Item scope only functions when events are built in the prescribed e-commerce shape, which the case property does not use for revenue and, as section 2.7 and Chapter 7 discuss, mis-sent on its lone view_item event. For a lead-generation business with no cart, item scope is mostly academic, but a practitioner must know it exists so as not to reach for it, or misuse it, by reflex.

session_source: the cautionary user-scoped dimension

session_source is registered at user scope. Read the name against the mechanism and the defect is immediate. A source is a per-visit attribute: a person arrives from google/organic on Monday and returns via (direct)/(none) on Thursday. Under user scope, GA4 keeps only the latest write, so Thursday's direct visit overwrites Monday's organic value, and every historical event for that user, including Monday's, now reports session_source as direct. The dimension name promises a session-level attribute; the scope delivers a last-write-wins user attribute. The two disagree, and the scope wins.

The damage is quiet, which is what makes it dangerous. No error appears. Reports populate. A row exists for every value. An analyst segmenting by session_source sees plausible-looking numbers that misattribute earlier behavior to a later source, understating the channels that open relationships and overstating the channels that close them. In a business whose entire seasonal story is which channel drives spring termite demand versus cooler-month rodent demand, a source dimension that overwrites itself corrupts the exact analysis it was built to serve. The correct scope for a source you want tied to the visit is event scope on the events of that visit, or reliance on GA4's built-in session-scoped traffic dimensions. Chapter 8 specifies the remediation; the lesson for the mental model is the general one: choosing a scope is choosing a persistence and overwrite behavior, and you must pick it from the value's true lifetime, not from the word that reads well in the dimension name.

2.3 Key Events, and the Retirement of "Goals" and "Conversions"

GA4 marks certain events as more important than others by flagging them as key events. A key event is a plain event whose name you have toggled as significant, so that GA4 counts it in the key-events metric, exposes it in conversion-oriented reports, and, once a Google Ads link exists, makes it eligible to import as a bidding conversion. There is no separate goal object to configure, no funnel or destination or duration goal type as an older Google analytics practitioner would expect. You fire an event; you flag the name; it becomes a key event. The terminology itself moved: what Google long called a "conversion" in GA4's own interface, and what a Universal Analytics user called a "goal," is now a "key event." Chapter 7, "Conversion Architecture," treats the design of conversions as a discipline; here the point is vocabulary and mechanism.

The case property had six key events:

Key eventStatus at audit
purchaseGA4 default, never fired (no e-commerce)
contact_form_submitClient-side lead event, working
quote_form_submitClient-side lead event, working
career_application_submitClient-side, low volume
call_button_clickClient-side
cta_clickClient-side, over-broad

Two entries teach the model by their pathology. purchase is a GA4 default key event that the platform pre-flags on property creation. This site has no transaction, so purchase never fired and never will, yet it sits in the key-event list as inert weight. It cannot even be removed: the Admin API rejects deletion with "The event cannot be deleted" (INVALID_ARGUMENT), because GA4 protects certain reserved names. A practitioner must recognize that a key event's presence in the list is not evidence that it carries data. cta_click sits at the opposite pole: it fired constantly, on every call-to-action tap across roughly 1,200 pages, which made it far too broad to represent a business outcome. Counting every CTA tap as a conversion inflates the conversion count with intent that has not yet become a lead. The remediation, recorded in Chapter 5, deleted cta_click as a key event while keeping it as an ordinary event for engagement analysis, and left purchase inert because the platform forbids its deletion.

The deeper mechanism worth internalizing: flagging an event as a key event is a reporting-and-activation decision layered on top of collection, not a change to collection itself. The event fires and is stored whether or not it is a key event. The flag governs which reports foreground it and whether Ads can bid on it. This separation explains why the property's most important lead signal produced no value for the entire property lifetime. The server-side generate_lead event, described next and dissected in Chapter 6, never reached GA4 at all, so no one had any reason to flag it as a key event, and the only lead conversion actually flagged, contact_form_submit, is a client-side event that ad-blockers and privacy browsers suppress. The flag was correct in intent and useless in effect, because collection failed upstream of it. Collection first, then flagging: keep the order straight.

2.4 Identity and the Session Model

Every event must attach to someone and to some visit, or it cannot be counted as a user or a session and cannot be attributed to a source. GA4 assembles identity from a small set of identifiers, and the practitioner who confuses them will misread both the user counts and the attribution. Chapter 9, "Identity, Attribution, and Value," develops the attribution consequences; this section fixes the identifiers themselves.

client_id: the browser's fingerprint

The client_id identifies a browser, not a person. GA4's JavaScript tag mints it on first visit and stores it in the _ga cookie, from which it is read on every subsequent event. Two people sharing a laptop share a client_id; one person on a phone and a desktop has two. It is the default unit behind "active users" when no stronger identity is present. In the case property, the 30-day baseline of 401 active users and 527 sessions rests on client_id counting, because Google Signals was disabled and no user_id was set.

The client_id also matters off the browser. The server-side lead pipeline reads the _ga cookie value, extracts the client_id, and stores it on each contact_submissions row as ga_client_id, so that a lead the server reports later can be stitched to the same browser the client tag saw. The audit measured that stitch rate directly from the database: of 29 leads, 20 carried a ga_client_id and 9 did not, a 69 percent capture rate. The missing 31 percent is ad-block and privacy-browser loss, not timing, because once the tag sets the _ga cookie it persists for two years, so a returning real visitor keeps a stable client_id across visits. A test submission (database row id 32, through the header quote panel on a clean Windows Chrome user-agent) landed with an empty ga_client_id, which confirmed the tester's browser sent no _ga cookie at all. Hold the distinction: a client_id identifies a browser that ran the tag, and no tag means no client_id, which the server pipeline must handle rather than fake.

session_id and the session model

A session in GA4 is not a fixed clock window inherited from an older model; it is anchored by a session_id that the tag writes into a session cookie and refreshes on activity. The tag stamps session_id (and an engagement signal) onto events so GA4 can group them into one visit and attribute that visit to a source. This is where the case property's server events failed even in principle. The original Measurement Protocol payload omitted session_id and engagement_time_msec entirely, so any event that did arrive could not join a session and would default its traffic to (direct)/(none), stripping the very source attribution the offline-revenue loop needs. The mechanism to recover session_id on the server is a cookie parse, described next.

The cookies: _ga and _ga_<container>, GS1 versus GS2

GA4 uses two cookie families, and a practitioner who wants to reconstruct identity server-side must read both. The first, _ga, is the client-level cookie. It holds the client_id and persists two years. Its value takes a form like GA1.1.1234567890.1710000000, where the trailing two segments compose the client_id.

The second is the session cookie, named _ga_ followed by the container id derived from the measurement ID. For measurement ID G-WH410Z73V1 that cookie is _ga_WH410Z73V1. It holds session state, including the session_id and a session count, and it is scoped per stream, which is why its name carries the stream's container suffix rather than being a single shared name. GA4 has written this cookie in two formats over time, and code that parses it must handle both:

# Legacy GS1, dot-delimited:
_ga_WH410Z73V1 = GS1.1.<session_id>.<session_number>.<...>

# Newer GS2, dollar-delimited:
_ga_WH410Z73V1 = GS2.1.s<session_id>$o<session_number>$g<...>$t<...>

The GS1 form separates fields with dots; the GS2 form separates them with dollar signs and prefixes each field with a letter code, so the session_id follows an s. The corrected server pipeline in this project parses _ga_WH410Z73V1, detects the prefix, splits on the right delimiter, and extracts session_id for both formats, which is the difference between a server event that joins its session and one that lands in (direct)/(none). Chapter 6 shows the parser; for the mental model, know that there are two cookies, that the session cookie's name is stream-specific, and that its internal format has two dialects you cannot assume away.

user_id and Google Signals: two ways to reach past the browser

Two mechanisms let GA4 identify beyond a single browser. A user_id is a stable identifier you assign when you know who someone is, typically after authentication, so GA4 can unify their sessions across devices under one person. The case property sets no user_id, which is defensible: its visitors are anonymous prospects, not logged-in account holders, so there is no authenticated key to assign.

Google Signals is the other mechanism. When enabled, it lets GA4 use Google's own cross-device graph, drawn from signed-in Google users who opted into ads personalization, to deduplicate users and unlock demographic and cross-device reporting. It was already disabled on this property, and the adversarial audit (Chapter 4) confirmed that leaving it off was correct here rather than a gap. At roughly 500 sessions per month, Google Signals mainly harms reporting by triggering data thresholding: to prevent re-identification of individuals in small cohorts, GA4 withholds rows whose counts fall below a threshold, so on a low-volume property Signals hides exactly the granular by-service and by-county rows this business most needs to read. The mental model point is that identity strength trades against reporting completeness at low volume, and more identity is not always better. Chapter 9 and Chapter 10, "Privacy and Consent by Design," carry the trade-off further.

2.5 Two Ways In: gtag Versus the Measurement Protocol

Events reach property 534525683 by two transports, and each can do things the other cannot. Understanding the split is the last piece of the mental model, because most of the implementation work in Chapter 6 lives on the boundary between them.

Client-side, the gtag.js tag runs in the visitor's browser. It sees everything a browser sees, the _ga and _ga_<container> cookies, the page URL, the referrer, the screen, and it stamps client_id, session_id, and engagement automatically onto every event. The case property's client events, page_view with page_type and a slug, contact_form_submit and quote_form_submit with form_source, career_application_submit with job_slug, call_button_click with cta_location, cta_click with label and cta_location, form_start, and form_error, all ride this transport. What the browser tag cannot do is fire when it is blocked. Ad-blockers, privacy browsers, and consent refusals suppress gtag.js, which is precisely the 31 percent of leads that carried no ga_client_id, and there is no client-side fallback for an event the browser never runs.

Server-side, the Measurement Protocol is an HTTP transport: your server posts a JSON payload of events to a Google endpoint, with the client_id (and ideally session_id) supplied in the body rather than read from a cookie the server does not automatically have. It runs where an ad-blocker cannot reach it, which is why the case property routes a generate_lead event from its contact API through it. What the Measurement Protocol cannot do is invent identity or session context: it sends only what you put in the body, it does not validate that the data is sane, and it will not attach a client_id or session_id for you. That is the root of the property's primary defect. The server posted generate_lead to https://www.google.com/mp/collect, which returns HTTP 404, rather than the documented https://www.google-analytics.com/mp/collect, which returns HTTP 204, so across the entire property lifetime the one event engineered to survive ad-blockers reached GA4 zero times. The transport was capable; the address was wrong. Chapter 6 is devoted to the fix and its verification; for the mental model, fix the pairing in your head: the browser tag supplies identity automatically but can be blocked, and the Measurement Protocol cannot be blocked but supplies nothing you do not send.

2.6 What Universal Analytics Muscle Memory Gets Wrong

A migrator carries habits that the GA4 model quietly breaks, and the case property shows three of them. First, hit types are gone. There is no pageview type, event type, or transaction type to select; there is only the event, named and parameterized (section 2.1). The habit of choosing a hit type becomes the discipline of naming an event and choosing its parameters.

Second, e-commerce is a strict shape, not a set of loose fields. A Universal Analytics practitioner grew used to flat product fields, and the case property's lone view_item reproduced that habit: it sent flat item_category, item_id, and item_name parameters instead of the required items[] array. GA4's item reports read only the array, so the flat version stored data that never populated a single item report. The lesson generalizes past e-commerce: GA4's prebuilt reports read prescribed parameter shapes, and a value in the wrong shape is a value that does not exist for reporting. Chapter 7 records the disposition (the event was left as harmless dead weight rather than repaired, since the business sells no items).

Third, sessions and sources compute differently. Universal Analytics reset a session on a source change and used a last-non-direct attribution habit that many analysts internalized as truth. GA4 anchors sessions by session_id and treats event-scoped source, medium, and campaign as manual traffic signals that can override its own attribution. The original server payload sent bare source, medium, and campaign fields, which GA4 read as manual signals capable of corrupting attribution rather than as harmless metadata; the remediation dropped them. The migrator's instinct to pass source strings around freely is exactly wrong in GA4, where an unqualified source parameter is an instruction, not a note.

2.7 The Vocabulary the Rest of the Dissertation Assumes

This chapter fixed six primitives and their traps. An event names a verb and carries parameters that hold the nouns; the case property's single page_view with a page_type pivot is the pattern to imitate. A custom dimension registers a parameter for reporting, and its scope (event, user, or item) sets its persistence and overwrite behavior, which is why user-scoped session_source silently overwrites a visit attribute to its latest value and misattributes history. A key event is a flagged name that governs reporting and Ads activation but not collection, which is why an inert purchase and an unreachable generate_lead teach opposite halves of the same lesson. Identity assembles from client_id (a browser, stored in _ga), session_id (a visit, stored in _ga_WH410Z73V1 in either GS1 dot-delimited or GS2 dollar-delimited form), and the optional user_id and Google Signals, with more identity trading against reporting completeness at 500 sessions a month. And events arrive by two transports, the browser gtag.js that supplies identity but can be blocked, and the Measurement Protocol that cannot be blocked but supplies only what you send. Chapter 3, "A Measurement Maturity Model," now places these primitives on a maturity ladder, and Chapter 5, "Audit Findings," uses them to name every defect the property carried without re-teaching a single term.