Machina

Chapter 14·Part V · Reflection

An Anti-Pattern Catalog

Every defect in this chapter comes from one property: GA4 534525683, measurement ID G-WH410Z73V1, web stream 14548640408, running under Google Cloud project gen-lang-client-0444184725 for a pest-control operator in Central California. The audit and reconstruction described in Chapters 4 through 13 turned that single property into a field of failure modes. This chapter reorganizes those failures into a reference. Chapter 5, "Audit Findings," narrated them in the order the audit uncovered them; here they appear as a catalog you can read against your own property line by line.

Each entry follows the same shape: a Name you can cite in a code review, a Symptom an analyst observes without reading code, a Mechanism that explains why the symptom occurs, a Fix stated as the concrete change this project shipped, and a Severity. The entries group under five categories: Collection, Conversion, Taxonomy, Configuration, and Governance. Each entry cross-references the chapter that treats it in depth, so this catalog stays a catalog and does not restate the mechanism a specialist chapter already builds.

Read the severity scale as impact on the specific property, with the conditional cases named where a change in setup would move the rating:

  • Critical: the defect produces zero usable data or destroys data you cannot recover.
  • High: the defect systematically corrupts a core metric (users, sessions, conversions) that decisions depend on.
  • Medium: the defect degrades analysis or carries harm that activates under a named condition, such as linking Google Ads.
  • Low: dead weight that costs nothing to leave and little to remove.

Use the final section as a pre-launch and quarterly checklist. If any line is true of your property, the matching entry tells you what breaks and what to change.

Collection

Collection anti-patterns corrupt data before GA4 receives it. They are the most expensive class because no downstream report, model, or warehouse query can repair an event that never arrived or arrived mislabeled.

C1. Measurement Protocol posted to the wrong host

Symptom. A server-side event you can see in your application logs, generate_lead in this case, never appears in GA4, not in Realtime, not in DebugView, not in standard reports. Because it never appears, nobody promotes it to a key event, and its absence looks like a design choice rather than a failure.

Mechanism. The contact route (api/contact/route.ts line 384) and the careers route (api/careers/route.ts line 288) both posted to https://www.google.com/mp/collect. That host is not the Measurement Protocol collector. Verified live with curl using the identical payload, the two hosts return different status codes:

POST https://www.google.com/mp/collect            -> 404 Not Found
POST https://www.google-analytics.com/mp/collect  -> 204 No Content

The Measurement Protocol answers with 204 on accept and never with a body, so a caller that ignores the status code cannot tell a working endpoint from a 404. Across the property lifetime the server generate_lead reached GA4 zero times. The event engineered to survive ad-blockers produced no data, which meant the only working lead signal was the client-side contact_form_submit that blockers suppress.

Fix. The host was corrected to www.google-analytics.com/mp/collect in both routes. The payload then validated against https://www.google-analytics.com/debug/mp/collect with validationMessages: [], and a production generate_lead returned 204 and registered in GA4 Realtime within about 20 seconds.

Severity: Critical. One wrong hostname erased the entire server-side conversion signal.

See Chapter 6, "The Server-Side Measurement Protocol."

C2. Synthetic client_id that mints a phantom user per lead

Symptom. Active-user counts drift above plausible reality, and server-attributed conversions almost never join a real user's session history. Each lead looks like a brand-new person who existed for one event.

Mechanism. When the _ga cookie was absent, the contact route fell back to a client_id of the form server.<timestamp>. Every such value is unique, so GA4 treats every fallback lead as a distinct user with no prior events. The fabricated identifier does not match any browser's real client_id, so the conversion cannot stitch to the session that produced it. The fallback did not rescue an untracked lead; it manufactured a phantom.

Fix. The fallback was removed. The Measurement Protocol event now sends only when a real ga_client_id is present, so an untrackable submission is skipped rather than faked. Database measurement confirmed the tradeoff is small and honest: of 29 leads, 20 carried a ga_client_id and 9 did not, a 69 percent capture rate, with the missing 31 percent attributable to ad-block and privacy browsers rather than timing.

Severity: High. Fabricated identifiers corrupt user counts and sever attribution, the two things server-side collection exists to protect.

See Chapter 6, "The Server-Side Measurement Protocol," and Chapter 9, "Identity, Attribution, and Value."

C3. Measurement Protocol failures swallowed by an empty catch

Symptom. The dashboard shows no errors and the pipeline looks healthy, yet conversions are missing. A defect as total as C1 sat undetected because nothing ever logged it.

Mechanism. The server send wrapped its network call in a catch block that discarded the result. Because the Measurement Protocol returns 204 on success and a bare status on failure, and because the code inspected neither, a 404 against the wrong host looked identical to a 204 against the right one. The empty catch converted a Critical outage into silence, which is why C1 survived for the property's whole life.

Fix. Non-200 responses from the Measurement Protocol now log rather than being swallowed. The send still fails soft so a lead is never lost to an analytics error, but the failure becomes visible to the operator and to the data-quality checks described in Chapter 13.

Severity: High. Silent failure is a force multiplier: it hides every other collection defect and postpones detection indefinitely.

See Chapter 6, "The Server-Side Measurement Protocol," and Chapter 13, "Data Quality and Governance."

Symptom. A visitor who lands, reads, and submits quickly produces a lead with no client_id, while slower visitors on the same page are tracked. Capture rate correlates with dwell time rather than with the browser's privacy posture.

Mechanism. The gtag script loaded with lazyOnload, which defers the tag until the browser is idle after full page load. gtag sets the first-party _ga cookie only after it executes. A visitor who submits before the idle callback fires has no _ga cookie yet, so the server has no real client_id to send, and C2's honest skip then drops the conversion. The loss is timing-induced, distinct from the privacy-induced loss in C2.

Fix. The load strategy changed from lazyOnload to afterInteractive, so the tag loads sooner and sets _ga before a fast visitor reaches the submit button. This narrows the window in which a real user submits before an identifier exists.

Severity: Medium. It costs a recoverable slice of conversions rather than corrupting data, and the loss shrinks but does not vanish after the fix.

See Chapter 6, "The Server-Side Measurement Protocol."

Conversion

Conversion anti-patterns misdefine what counts as success. They do not lose data; they teach GA4, and any bidding system linked to it, to optimize toward the wrong thing.

V1. A ubiquitous micro-interaction marked as a key event

Symptom. Conversion counts run high and flat, dominated by an event that fires on nearly every page. The signal that should mark commercial intent instead measures scrolling past a button.

Mechanism. cta_click was configured as a key event. That event fires from calls-to-action on roughly 1,200 statically generated pages. A key event should approximate revenue intent; cta_click approximates page presence. Had this property been linked to Google Ads, the platform would have optimized bids toward button visibility.

Fix. The cta_click key event was deleted through the Admin API. The click is still collected as a plain event for engagement analysis; it is no longer a conversion.

Severity: High. A key event this broad drowns real conversions and, once an Ads link exists, actively misdirects spend.

See Chapter 7, "Conversion Architecture."

V2. One lead counted twice by client and server events

Symptom. Lead totals in GA4 exceed the count of rows in the leads database. A single form submission produces both a client contact_form_submit and a server generate_lead.

Mechanism. The browser fires contact_form_submit and the contact API fires generate_lead for the same submission. Both are, or become, key events. In a property linked to Google Ads this double-counts conversions and inflates any value-based bidding signal. Absent an Ads link, the duplication is GA-only reporting noise.

Fix. This project deliberately deferred deduplication. With no Google Ads link present, keeping both events as key events maximizes capture while ad-blockers suppress the client path and the server path covers the gap. The canonical single conversion gets chosen at Ads-link time, when the double-count would begin to cost money. The deferral is documented in the committed MEASUREMENT_PLAN.md so it is a decision, not an oversight.

Severity: Medium. Harmless in the current GA-only state, High the moment Google Ads is linked. The rating is conditional and the condition is written down.

See Chapter 7, "Conversion Architecture."

V3. A flat lead value that hides which services are worth more

Symptom. Every conversion carries the same value, 150, regardless of whether the lead is a termite job or a single ant call. Reports cannot rank services by economic weight, and value-based bidding has nothing to bid on.

Mechanism. The server sent a constant value: 150. A pest-control operator's jobs vary widely in worth, so a flat value discards the one dimension that would let GA4 or Smart Bidding favor high-value work.

Fix. A per-service value map replaced the constant, with provisional internal estimates: termite 400, fumigation 500, commercial 400, bed-bug 250, rodent 180, general and pest 150, ant and wasp-bee 100, other 120. The values are labeled provisional and internal in the code because a GA lead value is a bidding and reporting input, never shown to a customer.

Severity: Medium. It caps analytical and bidding value rather than corrupting data, and at roughly 15 to 30 conversions per month the value-based bidding it unlocks will not fully stabilize, so the near-term payoff is reporting clarity.

See Chapter 9, "Identity, Attribution, and Value."

Taxonomy

Taxonomy anti-patterns break the contract between the parameters an event carries and the dimensions a report can read. The data often arrives intact and still cannot be analyzed.

T1. view_item sent as flat parameters instead of the items array

Symptom. The ecommerce item reports stay empty even though a view_item event fires. Item name, category, and ID are nowhere in the interface.

Mechanism. The event sent flat item_category, item_id, and item_name parameters. GA4 ecommerce reports read only the structured items[] array; flat top-level parameters of those names populate nothing. The data is transmitted and then ignored.

Fix. This project skipped the repair. On a lead-generation site with no ecommerce, view_item is harmless dead weight, and rebuilding it into a correct items[] payload buys nothing. The entry stays in the catalog so the pattern is recognized where it does matter.

Severity: Low. No revenue depends on it and it corrupts nothing; the cost is a misleading event name in the stream.

See Chapter 8, "Event Taxonomy and Custom Dimensions."

T2. A user-scoped dimension that overwrites to its latest value

Symptom. Analyzing users by their acquisition source returns each user's most recent source, not the source that first brought them. First-touch analysis is impossible from this dimension.

Mechanism. session_source was registered as a USER-scoped custom dimension. User scope stores one value per user and overwrites it on every new event that carries the parameter, so the dimension always reflects the latest source. Session-level source belongs in session or event scope; at user scope it becomes a last-write-wins field that erases history.

Fix. The scoping defect is documented as a taxonomy correction in the measurement plan, with session and event scope reserved for session-varying signals and user scope reserved for genuinely stable user properties. Chapter 8 sets the scope-selection rule the fix applies.

Severity: Medium. It silently answers source questions with the wrong value, and the error is invisible because the dimension still returns data.

See Chapter 8, "Event Taxonomy and Custom Dimensions," and Chapter 9, "Identity, Attribution, and Value."

T3. Event parameters sent but never registered as custom dimensions

Symptom. A parameter rides along on every event yet cannot be selected in any report or Exploration. The data exists in collection and is absent from analysis.

Mechanism. GA4 collects any parameter you send, but a parameter is only queryable once you register it as a custom dimension, and registration is not retroactive: reporting begins from the registration date forward. Sending service_type, cta_label, or error_message without registering them means the values accumulate invisibly and the pre-registration history is unrecoverable.

Fix. service_type, cta_label, and error_message were registered as custom dimensions so their values become reportable going forward. The lesson generalizes: register a dimension before or at the moment you start sending its parameter, never after you want to analyze it.

Severity: Medium. No data corrupts, but every day of delay permanently forfeits that day's analyzable history.

See Chapter 8, "Event Taxonomy and Custom Dimensions."

T4. A raw error string used as a dimension

Symptom. An error dimension explodes into thousands of near-unique values, blows past the custom-dimension cardinality limits, and risks carrying user text into analytics.

Mechanism. Registering a dimension fed by an unbounded string, such as a raw error message, produces high cardinality that GA4 handles by bucketing overflow values into (other), which hides the long tail. Worse, a raw message can embed whatever a user typed, so the dimension becomes a PII channel.

Fix. error_message was registered only because the code sends bounded codes, never raw text: the client emits fixed tokens such as server_error and network_error. A dimension is safe to register when its input is a small closed set. If the code had sent the raw string, the correct move is to map it to a code before it ever reaches gtag.

Severity: High. The cardinality damage degrades reporting and the PII exposure creates a privacy liability, a combination worse than either alone.

See Chapter 8, "Event Taxonomy and Custom Dimensions," and Chapter 10, "Privacy and Consent by Design."

Configuration

Configuration anti-patterns arise from settings that each look reasonable in isolation and collide when combined. They typically double-count high-frequency events.

F1. Default send_page_view plus a manual page_view

Symptom. page_view counts run at roughly twice the real page-load count. Sessions and engagement metrics that derive from page_view inherit the inflation.

Mechanism. The gtag config left send_page_view at its default of true, which fires a page_view on load, while the application also fired its own page_view carrying page_type and slug. Two page_views per navigation followed. The custom event existed to attach the dimensions the automatic one lacks, but nobody disabled the automatic one.

Fix. The config now sets send_page_view: false, and a single RouteAnalytics client component fires one page_view per route, deriving page_type and the relevant slug from usePathname. The duplicate trackPageView call in the per-page tracker was also removed. Headless-Chrome verification read the dataLayer and confirmed one push per route: {page_type: home} on the homepage, {page_type: service, service_slug: termite-control} on a service page, {page_type: location, location_slug: salinas} on a location page.

Severity: High. page_view is the load-bearing event for sessions and engagement, so doubling it corrupts a wide band of downstream metrics.

See Chapter 8, "Event Taxonomy and Custom Dimensions," and Chapter 6 for the load and firing sequence.

F2. Enhanced Measurement colliding with custom events

Symptom. Both a GA4 automatic event and a site custom event fire for the same page change and the same form interaction. page_view, form_start, and form_submit each arrive twice on a single-page navigation.

Mechanism. Enhanced Measurement had all seven options on. pageChangesEnabled fires a GA4 page_view on History API navigation, duplicating the custom RouteAnalytics page_view. formInteractionsEnabled auto-fires GA4's own form_start and form_submit, duplicating the site's custom form_start, form_error, contact_form_submit, and quote_form_submit. The adversarial verify pass surfaced the form collision, which the initial pass had missed. Site search was also on despite no search box existing on the site.

Fix. pageChangesEnabled and formInteractionsEnabled were switched off through the Admin API so the custom events are the single source of truth for navigation and form behavior. The rule generalizes: turn off any Enhanced Measurement option whose behavior you also implement by hand, and turn off options for features the site does not have.

Severity: Medium, trending High. The page-change duplication compounds F1, and the form duplication would double-count the lead-adjacent events that matter most.

See Chapter 8, "Event Taxonomy and Custom Dimensions."

Governance

Governance anti-patterns are settings nobody revisits after creation. They cost nothing on day one and quietly cap what the property can ever tell you.

G1. Event data retention left at the two-month floor

Symptom. Explorations and funnels that use event- or user-scoped custom dimensions return nothing beyond about 60 days. Any attempt at year-over-year seasonal analysis dead-ends.

Mechanism. Event data retention sat at TWO_MONTHS, the GA4 minimum. Standard aggregate reports retain effectively indefinitely, so the floor is invisible until an analyst opens an Exploration that pivots on a custom dimension, at which point the 14-month-versus-2-month gap becomes the difference between having and not having the data. For a business whose demand swings with termite swarm season in spring and rodents in cooler months, and whose analysis pivots by service and by county, the two-month floor removes the exact windows that matter.

Fix. A single dataRetentionSettings PATCH raised retention from TWO_MONTHS to FOURTEEN_MONTHS. The change is not retroactive and takes about 24 hours to apply, so the fix only preserves data from the change forward, which is itself an argument for setting retention correctly at property creation rather than discovering it in an audit.

Severity: Medium. No live data corrupts, but the default silently forecloses the seasonal, dimension-driven analysis this property was built to support, and every day at the floor is a day of long-range history lost.

See Chapter 13, "Data Quality and Governance," and Chapter 12, "Warehousing and Reporting," for how a BigQuery export makes retention a non-issue for warehoused history.

Using the catalog as a checklist

Run these fourteen lines against your own property at launch and each quarter. A "yes" on any line points to the entry above and the chapter that fixes it.

IDAnti-patternCategorySeverityDepth chapter
C1Measurement Protocol posted to www.google.com, not www.google-analytics.comCollectionCritical6
C2Synthetic client_id (server.timestamp) minting a phantom user per leadCollectionHigh6, 9
C3Measurement Protocol failures swallowed by an empty catchCollectionHigh6, 13
C4GA loaded lazyOnload, _ga cookie set after fast visitors submitCollectionMedium6
V1cta_click marked a key event across 1,200 pagesConversionHigh7
V2Client contact_form_submit and server generate_lead double-count one leadConversionMedium (High once Ads-linked)7
V3Flat lead value hiding which services are worth moreConversionMedium9
T1view_item sent as flat params instead of the items[] arrayTaxonomyLow8
T2User-scoped session_source overwriting to the latest valueTaxonomyMedium8, 9
T3Event parameters sent but never registered as custom dimensionsTaxonomyMedium8
T4Raw error string used as a dimension (cardinality and PII)TaxonomyHigh8, 10
F1Default send_page_view plus a manual page_view firing two page_viewsConfigurationHigh8
F2Enhanced Measurement pageChanges and formInteractions colliding with custom eventsConfigurationMedium (trending High)8
G1Event data retention left at the two-month floorGovernanceMedium13

Two structural lessons cut across the table. First, the empty catch (C3) is the entry that let the Critical defect (C1) survive, so silence in the failure path deserves scrutiny out of proportion to its own direct severity: a defect you cannot see is a defect you cannot fix. Second, several Medium ratings are conditional, and the condition is almost always linking Google Ads. V2 and V3 sit quietly in a GA-only property and turn expensive the day spend flows through them. The audit found a Google Ads account already buying google/cpc clicks with no GA4 link in place, so those conditions are not hypothetical for this business; they are pending. Chapter 15, "Access and Permissions Model," explains why creating that link, and closing the conditional-severity entries it activates, requires the Administrator role this property's automation service account was finally granted.