Machina

Appendix C·Appendix

The Measurement Plan

This appendix reproduces and expands MEASUREMENT_PLAN.md, the file committed to the {COMPANY} repository as the living contract for measurement. It is not documentation written after the fact. A developer reads it before adding a tracking call, an analyst reads it before trusting a report, and the office manager reads the conversions table before deciding which numbers describe booked work. When any of the four pillars below (conversions, taxonomy, dimensions, value) changes in code, the author of that change edits this file in the same commit. A pull request that adds an event and leaves this file untouched is incomplete by policy.

The plan governs GA4 property 534525683 (measurement ID G-WH410Z73V1, web stream 14548640408) in Google Cloud project gen-lang-client-0444184725. The full audit that produced it is recorded in Chapter 5, Audit Findings; the server transport it depends on is built in Chapter 6, The Server-Side Measurement Protocol; the conversion semantics are argued in Chapter 7, Conversion Architecture; the taxonomy design is Chapter 8, Event Taxonomy and Custom Dimensions.

C.1 Conversions

{COMPANY} sells booked jobs offline. A lead is the online event that precedes revenue, and the property counts leads through one canonical server-side event plus a small set of supporting client events. The table below states which events count as conversions, which are funnel steps that must never be promoted, and whether Google Ads may ever import them.

EventRoleSourceKey eventImport to Google Ads
generate_leadCanonical lead conversion. One booked-value estimate per qualified form submission.Server (Measurement Protocol from the contact API)YesYes, when the Ads link exists
call_button_clickPhone-tap intent. Counts taps on the click-to-call control, the proxy for the phone channel until real call tracking exists.Client (gtag)YesYes, as a secondary conversion
career_application_submitJob applicants. Operationally useful, commercially irrelevant.Client (gtag)YesNever. Applicants are not customers and must not train bidding.
contact_form_submitFunnel event. The client-side signal that a contact form reached the success state.Client (gtag)Yes (interim)No
quote_form_submitFunnel event. The client-side signal that the header quote panel submitted.Client (gtag)Yes (interim)No

generate_lead is the conversion that carries value and geolocation and survives ad-blockers, because the server posts it after the row is written to PostgreSQL. contact_form_submit and quote_form_submit are semantically funnel events: they mark that a browser reached a success screen, nothing more. They remain flagged as key events on an interim basis because the property has no Google Ads link yet, so the overlap between them and generate_lead is harmless GA-only reporting. Keeping all three as key events maximizes capture while the property waits for the link. Chapter 7 sets the demotion trigger: at Ads-link time one canonical conversion gets chosen (generate_lead), and the two client form events drop to non-key so a single click cannot count two conversions.

The no-double-fire rule

GA4 deduplicates exactly one event by identifier: purchase, keyed on transaction_id. Every other event, including generate_lead, double-counts if it fires twice for one action. There is no server-side dedupe key that GA4 honors for custom events. The plan therefore enforces single emission at the source rather than hoping the platform will reconcile:

  • One page_view per route. The gtag config sets send_page_view: false, and a single RouteAnalytics component fires one page_view per navigation. Enhanced Measurement pageChangesEnabled is off so client navigation does not fire a second one.
  • One form-submit signal per form. Enhanced Measurement formInteractionsEnabled is off, because GA4's automatic form_start and form_submit collide with the site's own form_start, contact_form_submit, and quote_form_submit.
  • One server lead per submission. The contact API sends generate_lead once, after the insert. The careers route sends no server lead at all; its client career_application_submit is sufficient for that low-volume form.

C.2 Event taxonomy

Every event the property emits, with its parameters. Client events fire through gtag; the one server event posts through the Measurement Protocol. Parameter names in this table are the exact strings the code sends and, where registered, the exact parameterName of the custom dimension.

EventTransportParameters
page_viewClientpage_type, and one of service_slug, location_slug, pest_slug, job_slug per route
contact_form_submitClientform_source
quote_form_submitClientform_source
career_application_submitClientjob_slug
call_button_clickClientcta_location
cta_clickClientcta_label, cta_location
form_startClientform_source
form_errorClientform_source, error_message
view_itemClientDeprecated. Sent flat item_category/item_id/item_name instead of the required items[] array, so item reports never populated. Slated for removal; harmless dead weight until then.
generate_leadServerclient_id, session_id, currency (USD), value (per-service map), form_source, service_type, engagement_time_msec, ip_override

The generate_lead payload is the load-bearing artifact of the whole plan, so its shape is fixed here and mirrored in Appendix B. The transport posts to the documented host, sends only when a real ga_client_id is present, and includes the session identifier so GA4 attributes the conversion to the acquiring source rather than to (direct)/(none):

{
  "client_id": "1234567890.1699999999",
  "events": [{
    "name": "generate_lead",
    "params": {
      "session_id": "1699999999",
      "engagement_time_msec": 1,
      "currency": "USD",
      "value": 400,
      "form_source": "contact_page",
      "service_type": "termite-control"
    }
  }],
  "ip_override": "203.0.113.10"
}

C.3 Custom dimensions

The event-scoped custom dimensions registered on the property. A parameter that is not registered here is collected but is not reportable in the standard UI, which is why registration precedes reliance. session_source is the one user-scoped dimension and sits outside this table by design.

Dimension (display)parameterNameEmitting events
Page typepage_typepage_view
Service slugservice_slugpage_view
Location sluglocation_slugpage_view
Pest slugpest_slugpage_view
Job slugjob_slugpage_view, career_application_submit
Form sourceform_sourcecontact_form_submit, quote_form_submit, form_start, form_error, generate_lead
CTA locationcta_locationcall_button_click, cta_click
CTA labelcta_labelcta_click
Service typeservice_typegenerate_lead
Error messageerror_messageform_error

service_type, cta_label, and error_message were registered during the Phase 1 rebuild. error_message is safe to register only because the code emits bounded codes such as server_error and network_error, never raw text. That constraint is a rule, not a convention; see C.6.

C.4 Lead value model

generate_lead carries a per-service value so that cost-per-booked-job analysis and, later, value-based bidding have something to weigh. These figures are provisional internal estimates of booked job value by category. They are never shown to a customer, never printed on a quote, and exist only inside GA4 and BigQuery. They are the office's best guess at relative worth, and they will be replaced by real job_value from the offline loop (Chapter 11) once enough jobs close.

Service categoryProvisional value (USD)
Fumigation500
Termite400
Commercial400
Bed bug250
Rodent180
General150
Pest150
Other120
Ant100
Wasp / bee100

This map replaced the earlier flat value: 150, which treated a fumigation lead and an ant lead as equal and would have taught any future bidding strategy the wrong lesson. The values are relative, not accounting-grade; their job is to rank categories correctly, which they do.

C.5 Property configuration summary

SettingValue
Property ID534525683
Measurement IDG-WH410Z73V1
Web stream ID14548640408
Google Cloud projectgen-lang-client-0444184725
TimezoneAmerica/Los_Angeles
CurrencyUSD
IndustryHOME_AND_GARDEN
Created2026-04-24
Event data retentionFOURTEEN_MONTHS
Google SignalsOff
BigQuery exportDaily on, streaming off (US location)
Enhanced MeasurementOn, except pageChangesEnabled and formInteractionsEnabled (off)
Consent Mode v2Global default granted; region default denied for 32 EEA/UK/CH codes, wait_for_update 500

The 14-month retention matters for one reason: GA4 standard aggregate reports retain effectively forever, so the cap bites only Explorations and funnels that slice by event- or user-scoped custom dimensions. Those are precisely the by-service and by-county seasonal analyses this business needs across a full termite-swarm-to-rodent year, which is why the floor of two months was raised. The change is not retroactive.

C.6 The rules

These are enforceable constraints. A pull request that violates one is rejected, not merged with a note.

  1. Never send PII as an event parameter. No email, phone, name, address, or message text reaches GA4 or the Measurement Protocol. Those fields live in contact_submissions in PostgreSQL, joined to analytics by ga_client_id when needed, never transmitted to Google. The audited payload already carried no PII, and that state is now a rule rather than an accident.
  2. Error parameters are bounded codes, not raw text. error_message accepts values from a closed set (server_error, network_error, and their kin). A raw exception string or user input in that parameter is a PII leak and a cardinality explosion at once.
  3. A custom dimension parameterName must match the code parameter exactly, and must be registered before anything relies on it. Case and spelling are load-bearing. cta_label in code and cta_label in the dimension are the same string or they are two silent failures.
  4. The server Measurement Protocol sends only when a real ga_client_id is present, and posts only to https://www.google-analytics.com/mp/collect. The synthetic server.<timestamp> fallback is removed: an untrackable submission is skipped, not faked, because a phantom client_id mints a phantom user. The host https://www.google.com/mp/collect returns 404 and drops the event; the documented host returns 204. This single line was the property's primary defect for its entire lifetime, so the plan names both hosts explicitly to keep a future refactor from reintroducing the typo.

C.7 Roadmap: not yet done

The plan closes with the work that remains, each item held for a stated reason rather than forgotten.

  • Google Ads link and offline conversion upload. GA4 is not yet linked to Google Ads, even though the account spends (46 paid clicks per month appear as google/cpc). The link requires Administrator, now held. The offline upload of booked job_value must target the Data Manager API, because the legacy Google Ads API offline-conversion path was closed on 2026-06-15 to developer tokens not previously allowlisted. See Chapter 11.
  • Demote the client form conversions. At Ads-link time, contact_form_submit and quote_form_submit drop from key events, leaving generate_lead and call_button_click as the imported conversions. This prevents a single lead from counting three times in bidding.
  • Phone-call tracking. A phone-heavy business needs real call attribution (a service such as CallRail, or Google Ads call conversions). It is flagged, not built, because it is a paid-tool decision for the owner.
  • Search Console link. Needs Administrator (held) plus verified Search Console property ownership, which is the owner's to grant.
  • Deliberate skips. A custom channel group is skipped as low return at roughly 500 sessions per month; GA4 default channels already separate the mix. view_item retirement is deferred as harmless dead weight. A full consent-management platform is judged unnecessary for a US-only California operator, since the Consent Mode defaults cover the posture. Each skip is a decision on record, revisited when volume changes.