Chapter 4·Part II · Diagnosis
Auditing Methodology: Programmatic and Adversarial
An audit is a claim about the state of a system, and a claim is only as good as the method that produced it. The default GA4 audit is a person clicking through the Admin interface, reading settings screens, and writing down impressions. That method fails in three predictable ways. It samples rather than enumerates, so a defect that lives in the one screen the auditor skipped survives. It records the reading without the evidence, so a later reader cannot tell whether the claim came from an API response or a memory. And it collapses two distinct kinds of error, the factual error (this setting is not what I wrote down) and the reasoning error (this setting is fine, but I concluded the wrong thing from it), into a single undifferentiated pile.
This chapter documents an audit built to defeat those three failures. It has two layers. The first is a programmatic inventory: a read-only snapshot of the property taken through the GA4 Admin API and Data API, under a named service-account identity, hitting a fixed list of endpoints, producing a machine-readable record that any later reader can diff. That layer establishes the facts. The second is an adversarial multi-agent audit: nine measurement pillars, each researched by one agent and then attacked by a second agent whose only job is to break the first agent's claims, run as a workflow of eighteen agents consuming roughly 943,000 tokens over about eighteen minutes. That layer establishes the judgments and, more importantly, catches the judgments that were wrong. Chapter 5 (Audit Findings) reports what these two layers found on GA4 property 534525683. This chapter is about how to run them so that you, the practitioner, can reproduce the method on a property you have never seen.
The division of labor is deliberate. The first layer answers "what is true?" and is fully deterministic: run it twice on an unchanged property and you get the same bytes. The second layer answers "what does it mean, and am I sure?" and is where the expensive, unavoidable judgment lives. Keeping them separate is what lets you trust the second layer at all, because an adversarial critic can only fact-check a claim if the underlying facts were captured with enough precision to check against.
4.1 Why the Inventory Comes First
A measurement audit that begins with judgment before it has enumerated the property is guessing. Before anyone can say the event data retention is too short, someone has to read the actual dataRetentionSettings value and record it verbatim. Before anyone argues that Enhanced Measurement is over-firing, someone has to read enhancedMeasurementSettings and confirm which of the seven toggles are on. The inventory is the substrate. Every later claim in Chapter 5 traces to a specific field in a specific API response captured in this phase, and that traceability is the whole point.
The inventory also has to be reproducible by someone other than its author. A screenshot proves what one person saw once. An API call, documented with its endpoint, its identity, and its scopes, can be rerun by a reviewer six months later to confirm the finding still holds or to detect that someone changed the setting in the interim. Reproducibility is not a nicety here. It is the mechanism by which a governance process (Chapter 13, Data Quality and Governance) can re-verify the property state on a schedule without a human re-clicking every screen.
4.2 Identity: Service-Account Impersonation
The inventory runs under a Google Cloud service account rather than a human Google login, for two reasons. First, a service account is a stable, auditable identity: its access can be granted, scoped, and revoked as a unit, and its actions appear in Cloud audit logs under one principal instead of scattering across whichever staff member happened to be signed in. Second, a service account can be driven from a script and a CI job, which a human interactive login cannot without fragile browser automation.
The account for this engagement was id-01-{company}@gen-lang-client-0444184725.iam.gserviceaccount.com, living in the same Google Cloud project as the property's other infrastructure, gen-lang-client-0444184725. The practitioner never downloaded the service account's private key. Downloaded JSON keys are the most common way service-account credentials leak, because they end up in a dotfile, a shell history, or a git commit. Instead the audit used short-lived impersonation: a human operator with the Service Account Token Creator role minted a temporary access token on demand.
# Mint a short-lived token for the audit identity without ever
# holding its private key. The token expires within the hour.
gcloud auth print-access-token \
--impersonate-service-account=id-01-{company}@gen-lang-client-0444184725.iam.gserviceaccount.com \
--scopes=https://www.googleapis.com/auth/analytics.readonly
The token this command returns carries the impersonated identity and the requested scopes, and nothing else. It expires. If it leaks, the blast radius is one hour of read access, not a permanent credential. That property matters enough that it belongs in every GA4 audit, not just this one.
4.3 Scopes: Least Privilege, Read by Default
OAuth scopes decide what a token can do, and the audit used three, chosen to be as narrow as each task allowed.
analytics.readonly covers the Data API. Reading traffic-source breakdowns, session counts, and event tallies requires nothing more than read access to reporting data, so that is all the reporting queries got.
analytics.edit covers the Admin API reads and, later, the Admin API writes of the remediation phases. A subtle point trips up practitioners here: reading configuration objects such as customDimensions and keyEvents through the Admin API is authorized under analytics.edit, because the edit scope subsumes configuration reads. The audit deliberately performed only reads during the inventory even while holding the edit scope, and separated the write operations into their own later phases (Chapter 6, The Server-Side Measurement Protocol, and the remediation phases described there). Holding a capability and exercising it are different things, and an audit should exercise the minimum.
analytics.manage.users.readonly covers exactly one endpoint: the read of accessBindings, the property's user-and-role list (analyzed in Chapter 15, Access and Permissions Model). This scope is genuinely separate from analytics.edit, and the separation has teeth. A token minted with only analytics.edit returns ACCESS_TOKEN_SCOPE_INSUFFICIENT when it hits the accessBindings endpoint. Google fenced user-and-permission data behind a distinct scope on purpose, so that a tool granted broad configuration-edit rights cannot silently read the roster of who has access. The lesson for the practitioner: when your inventory script errors on the access-bindings read while every other read succeeds, you have a scope problem, not a permissions problem, and no amount of adding GA4 roles will fix it. You have to request the user-management scope in the token itself.
Requesting the three scopes together looks like this:
gcloud auth print-access-token \
--impersonate-service-account=id-01-{company}@gen-lang-client-0444184725.iam.gserviceaccount.com \
--scopes=\
https://www.googleapis.com/auth/analytics.edit,\
https://www.googleapis.com/auth/analytics.readonly,\
https://www.googleapis.com/auth/analytics.manage.users.readonly
The service account also has to hold a GA4 property role, which is a separate axis from the Cloud scope. Cloud IAM and OAuth scopes decide whether the token may call the API at all; the GA4 property role decides what that identity may see and change inside the property. This account held predefinedRoles/admin at the property, confirmed by reading it back rather than assuming it, which is what let the later remediation create custom dimensions and key events. Chapter 15 develops the full role model; for the inventory it is enough to know that Editor would have covered every read in this phase except the access-bindings read, which additionally needs the user-management scope.
4.4 The Endpoints: A Fixed Snapshot List
The inventory reads a fixed list of endpoints, in a fixed order, so that the output is comparable across runs and across properties. Each endpoint answers a specific question about the property's configured state.
accountSummariesenumerates the accounts and properties the identity can see, and confirms the audit is pointed at the right property (534525683) before any other read runs.- The
propertyresource returns the top-level configuration: timezone America/Los_Angeles, currency USD, industry HOME_AND_GARDEN, creation date 2026-04-24, and the parent Cloud project. These are the fields that later analysis leans on, for example the reminder that a HOME_AND_GARDEN pest-control business books offline and has no ecommerce. dataStreamslists the web and app streams. This property has one web stream, 14548640408, carrying measurement ID G-WH410Z73V1. The stream is also the parent resource for the Enhanced Measurement read below, so it has to be enumerated first.customDimensionsreturns the registered dimension set with scopes. Reading it verbatim is what let the audit record all eight (cta_location, session_source at USER scope, page_type, job_slug, location_slug, service_slug, form_source, pest_slug) and reason about which event parameters were registered and which were being sent but dropped.keyEventsreturns the conversion set. Reading it named the six key events and, critically, exposed thatpurchasewas marked a key event on a site with no ecommerce while the server-sidegenerate_leadwas not a key event at all.dataRetentionSettingsreturns the event-data retention window. It read TWO_MONTHS, the floor, which is the single highest-leverage zero-risk finding in the whole audit.enhancedMeasurementSettings, read against the stream, returns the seven auto-collection toggles. All seven read on, including page-changes and form-interactions, which is what later exposed the collision with the site's custom events.googleSignalsSettingsreturns the Signals state, which read already disabled.
The reads are GET requests against analyticsadmin.googleapis.com (Admin API) and analyticsdata.googleapis.com (Data API), authorized with the impersonated token. A single configuration read looks like this:
TOKEN="$(gcloud auth print-access-token \
--impersonate-service-account=id-01-{company}@gen-lang-client-0444184725.iam.gserviceaccount.com \
--scopes=https://www.googleapis.com/auth/analytics.edit)"
curl -s \
-H "Authorization: Bearer ${TOKEN}" \
"https://analyticsadmin.googleapis.com/v1beta/properties/534525683/dataRetentionSettings"
The Data API reads are POST requests carrying a report definition, which is how the audit pulled the 30-day traffic-source table (234 (direct)/(none) sessions, 192 google/organic, 46 google/cpc, and the long tail down to a single yelp.com/referral) and the volume envelope (2,328 events, 401 active users, 527 sessions, 24 to 113 events per day). Those numbers are the baseline against which Chapter 5 argues, and the google/cpc line is the one that exposes a Google Ads account spending money into a property that has no Ads link.
The discipline that matters: persist every raw response to disk before interpreting any of it. The inventory's output is a directory of JSON files, one per endpoint, timestamped. That directory is the evidence base. The adversarial layer that follows consumes it, and Appendix A (the config record) is generated from it. A finding without a corresponding response file on disk is not a finding; it is a memory, and memories do not survive review.
4.5 Layer Two: The Adversarial Multi-Agent Audit
The inventory establishes what is configured. It does not establish what that configuration means for a pest-control business in Central California, nor whether the auditor's interpretation is correct. Interpretation is where audits go wrong, because a plausible-sounding measurement claim can be confidently false, and a single auditor has no adversary to catch the confident falsehood. The second layer supplies the adversary.
The design is a workflow of eighteen agents organized as nine pillars, each pillar staffed by a researcher and a fact-checker. The researcher investigates one measurement domain and writes claims. The fact-checker's only mandate is to break those claims: to demand a source, to find the counterexample, to flag the over-reach. The nine pillars were measurement strategy and value, event taxonomy, conversion design, server-side collection, consent and privacy, offline conversions, attribution, warehousing, and data quality. The run consumed roughly 943,000 tokens over about eighteen minutes.
Why pay for two agents per pillar when one can write a report? Because the failure mode of a single competent analyst, human or model, is not sloppiness. It is fluent over-claiming. An analyst who knows the domain writes claims that sound authoritative, and the very fluency that makes them persuasive is what makes their errors hard to catch. The fact-checker is a structural fix for that failure mode. It changes the incentive of the exchange: the researcher is rewarded for a claim that survives attack, not for a claim that reads well. Separating the two roles into two agents, rather than asking one agent to self-critique, matters because self-critique inherits the original reasoning's blind spots. A fresh agent pointed at the claim with instructions to falsify it starts from a different place.
4.5.1 Over-Claims the Verify Pass Corrected
The value of the adversarial layer is legible in the specific claims it downgraded or reversed. Four are worth naming, because each is a claim a competent single auditor would plausibly have shipped.
The researcher on consent and privacy asserted that Consent Mode v2 was legally required for this business. The fact-checker corrected it: Consent Mode v2 is a Google requirement tied to serving Google Ads to users in the EEA and the UK, not a statute, and this is a US-only operator serving California counties with no EEA ad delivery. So Consent Mode v2 is not a legal mandate for this property. The distinction changed the remediation. Consent Mode was still worth implementing as a defensive posture (Chapter 10, Privacy and Consent by Design), but as an engineering choice, not a compliance emergency, and that reframing is what let the team skip a full consent-management platform and cookie banner as unnecessary for a US-only local operator.
The researcher on server-side collection warned that the Measurement Protocol payload risked leaking personally identifiable information into GA4. The fact-checker read the actual payload captured in the inventory and found it carried no PII: client_id, currency, a numeric value, form and service identifiers, and traffic parameters, none of which is a name, an email, or a phone number. The warning was theoretically sound and factually wrong for this payload. Shipping it would have sent the team hunting for a leak that did not exist. (The real defects in that same payload, a broken endpoint host and a synthetic client_id, were separately confirmed and are the subject of Chapter 6.)
The researcher on attribution and reporting claimed that leaving Google Signals enabled would corrupt the data. The fact-checker refined the mechanism and the magnitude. Signals was already disabled here, so the point was partly moot, but the correction to the reasoning mattered for the general method: at this property's low volume, Signals mainly harms reporting by triggering data thresholding, which suppresses entire rows to protect user anonymity and thereby hides the by-service and by-county detail this business most needs. The harm is withheld rows, not corrupted numbers. Naming the correct mechanism is what lets a reader weigh the tradeoff instead of reciting a slogan.
The researcher on offline conversions proposed uploading booked-job values through the legacy Google Ads API offline-conversion path. The fact-checker caught a deprecation the researcher had missed: that upload path was blocked on 2026-06-15 for developer tokens not previously allowlisted, so new work has to target the Data Manager API instead. A single auditor working from training data a few months stale would have designed the offline-revenue loop (Chapter 11) against an endpoint that no longer accepts new integrations, and the error would have surfaced only at implementation time, weeks later.
4.5.2 A Defect the Verify Pass Surfaced
Adversarial review does more than downgrade over-claims; it finds omissions. The most consequential addition in this run was a collision that the pillar researchers had each half-seen but none had stated. Enhanced Measurement's form-interactions setting auto-fires GA4's own form_start and form_submit events on any form it detects. The site already fires custom form_start and contact_form_submit/quote_form_submit events by hand. So the property was double-counting form activity: the auto-collected pair shadowing the hand-built pair. The event-taxonomy researcher had catalogued the custom events; the conversion researcher had catalogued the key events; neither had connected the Enhanced Measurement toggle read in the inventory to the custom events read in the same inventory. The fact-checking cross-pass, reading across pillar boundaries, is what joined them. That collision drove a concrete remediation: the team switched off page-changes and form-interactions in Enhanced Measurement (Chapter 8, Event Taxonomy and Custom Dimensions), because the custom events already covered the ground and the auto-collection only duplicated it.
4.5.3 Why the Cost Is Justified
Roughly 943,000 tokens and eighteen minutes of compute is not free, and a practitioner has to be able to defend the spend. The defense is the cost of the errors avoided. Each of the four corrected over-claims, if shipped, buys a downstream failure: a wasted compliance scramble, a phantom PII hunt, a reporting slogan that misdirects the fix, and an offline-conversion integration built against a dead endpoint. Any one of those costs more than the audit run, measured in engineering hours and in the credibility of the audit itself. The token budget buys insurance against confidently-wrong recommendations, and confidently-wrong is the expensive kind, because it survives casual review precisely because it is confident. Adversarial verification is the mechanism that makes confidence earn its keep.
4.6 Running Both Layers Yourself
The method reduces to a sequence you can rerun on any property.
First, establish identity without a downloaded key. Grant a service account a GA4 property role and, in Cloud IAM, grant a human operator Token Creator on that account. Mint short-lived tokens with gcloud auth print-access-token --impersonate-service-account, and request only the scopes the phase needs: analytics.readonly for the Data API, analytics.edit for Admin reads and writes, and analytics.manage.users.readonly for the access-bindings read. Expect ACCESS_TOKEN_SCOPE_INSUFFICIENT on the user-management endpoint if you forget the third scope, and recognize it as a scope error rather than a roles error.
Second, run the inventory as a fixed, ordered list of reads: accountSummaries, then the property, then dataStreams, then customDimensions, keyEvents, dataRetentionSettings, enhancedMeasurementSettings, and googleSignalsSettings, plus the Data API pulls for the traffic-source mix and the volume envelope. Persist every raw JSON response to a timestamped directory before you interpret a single field. That directory is your evidence base and the input to everything downstream.
Third, run the adversarial layer against that evidence base. Split the analysis into measurement pillars, and for each pillar pair a researcher with a fact-checker whose mandate is to falsify, not to agree. Require the fact-checker to read across pillar boundaries, because the highest-value findings, like the form-interactions collision, live in the seams between domains. Treat every surviving claim as provisional until an adversary has tried and failed to break it.
The output of the two layers together is a set of findings where the facts are reproducible bytes on disk and the judgments have each survived a hostile read. Chapter 5 presents those findings for property 534525683. The remediation chapters that follow act on them, and Chapter 13 shows how to fold the inventory read into a recurring governance check so the property never drifts far from a known-good snapshot again.