# Processing and reporting layers: platform, export, warehouse, report

Layers 6 to 9 of the break map. Facts were checked against the vendor pages in `sources.yaml` on 2026-10-17, and the review fixes on 2026-10-20; re-check anything marked "verify live". GA4 is the worked analytics platform. Google Ads and Meta checks are listed where they differ; for other platforms, ask the same questions of their own documentation.

Replace `your-project` and `analytics_PROPERTY_ID` in the SQL. Run queries read-only (the BigQuery MCP server's `execute_sql_readonly`, or a role without write access), check the bytes estimate first and always filter `_TABLE_SUFFIX`.

## Layer 6: platform processing

### What breaks or misleads

- Processing time. Google says processing can take 24 to 48 hours and reports may change in that window. Normal-size standard properties get intraday data in about 2 to 6 hours and daily data typically in about 12 hours; larger property tiers take longer (the freshness table lists 18-hour and 24-plus-hour daily tiers). Reports and Explore may not be synchronised.
- Privacy thresholds. They are system defined and cannot be adjusted. The data quality indicator says thresholding was applied. A withheld row is not zero.
- Sampling and the `(other)` row, both shown in the data quality indicator. Standard properties sample event-level queries above 10 million events per query (verify live). Different mechanisms from thresholds; save the indicator with any comparison.
- Approximate counts. GA4 estimates Active users and Sessions with HyperLogLog++ even when nothing is sampled. BigQuery can count exact distinct values, so small differences in users and sessions are expected; a step change on one date is not.
- Data filters (internal traffic, developer traffic):
  - States are Testing, Active and Inactive. In Testing, matching data is only labelled through the "Test data filter name" dimension.
  - An active filter's effect is permanent and not retroactive. Excluded data is never processed and is not available in GA4 or BigQuery.
  - Internal traffic is defined by IP rules in the web stream's tag settings and labels events with `traffic_type`, default value `internal`.
  - The developer traffic filter removes activity sent in debug mode. If `debug_mode` reaches production, an active developer filter removes production traffic. Google notes that setting `debug_mode` to `false` does not turn it off; the parameter has to be omitted.
  - With a server container, check which IP GA4 receives before trusting IP rules (layer 5).
- Key events. A renamed event is not a key event until someone marks the new name; anything importing that key event (Google Ads, audiences) goes quiet with it. The reverse also happens: with Ads Personalization on for a linked Google Ads account, Google marks seven events, including `session_start`, `view_item` and `begin_checkout`, as key events, and a "key events" total jumps with no collection change.
- Event collection limits. GA4 does not log events, parameters or user properties over its limits: 40 characters for an event or parameter name, 100 characters for most parameter values, 25 parameters per event (verify live). A refactor that lengthens names or adds parameters can cross them silently.
- Custom dimensions. A parameter is reportable only as a registered custom dimension, and Google says reporting starts 24 to 48 hours after registration. Reports can show `(not set)` for dates before registration while BigQuery holds the parameter for every date (verify the current behaviour live). Rule: parameter present in BigQuery but `(not set)` in reports means check the custom dimension's registration date before suspecting collection.
- Reporting identity. Blended (User-ID, device ID, then modelling), Observed (User-ID, then device ID) and Device based (device ID only) de-duplicate users differently. Google says the choice does not affect collection or processing, but user counts depend on it, and Blended can include modelled data when the property is eligible. Do not compare a modelled user count with a backend count as if they were the same thing.
- Measurement Protocol:
  - The production endpoint returns no HTTP error codes, even for malformed events. Validate on the validation server; events sent there do not show in reports.
  - Events and user properties can be backdated up to 72 hours. Under the default `RELAXED` validation behaviour, an older event is accepted with its timestamp re-stamped to 72 hours ago, so a backfill succeeds silently and lands on the wrong dates; under `ENFORCE_RECOMMENDATIONS` it is rejected. Events meant to join browser-collected events should arrive within 48 hours of the original client event.
  - Session attribution. Google's Measurement Protocol use-cases page says an event gets the online session's attributes only when it carries that session's `session_id`, the request is sent no later than 24 hours after the session started, and any overridden `timestamp_micros` falls between the session's start and end. Session identifiers are not added automatically. Server-sent conversions that arrive as Unassigned usually miss one of these. Never move a timestamp to make it fit.
- Retention. Standard properties offer 2 or 14 months for user and event data. Large standard properties and XL 360 properties are limited to 2 months, and age, gender and interest data are always kept for 2 months. The setting affects explorations and funnel reports, not standard aggregated reports. An exploration that cannot reach last year is not a collection break.
- Transaction ID deduplication works for web streams only, and all purchases with `transaction_id=""` are deduplicated together.
- Google Ads conversion actions, when Google Ads is the complaint or imports the event (verify labels live):
  - Tracking status. Goals, Summary, with All conversions selected, shows a Tracking status per action (Google's troubleshooting page names Unverified, Tag inactive, Needs attention, no recent conversions and Recording). Troubleshoot on the status opens Tag Assistant.
  - Primary or secondary. An action with `primary_for_goal` false is not biddable for campaigns using account or campaign goals; custom conversion goals ignore the flag. An action moved to secondary or out of the bid goal stops feeding bidding with no tracking change.
  - Imported GA4 key events. The event must be a key event in GA4 before Google Ads can import it, so a rename or an unmarked key event stops the import. Google's data freshness page lists slower availability for conversions imported from Google Analytics than for Google Ads tag conversions (12 against 3 hours under last click, 24 against 15 under other models, checked 2026-10-20).
  - Settings changed in the window: counting, conversion windows, attribution model, inclusion in the Conversions column. Any of these moves the numbers with no tracking change.
  - Conversion lag: recent days fill in late; see the Google Ads guard in [sizing-and-proof.md](sizing-and-proof.md).
- Meta, where a Meta route is in the path: Events Manager's Overview counts events received before deduplication, discarded for consent or policy, and processed, and shows the connection method per event. A drop in processed events with steady received events points at deduplication or policy, not collection.

### Tests

1. Use completed days outside the processing window for every comparison.
2. Send one test event through the failing journey, under the test-traffic rules, and find it in DebugView (or Realtime if no developer filter is active). This proves collection reached GA4 now; it does not prove processed reports.
3. Admin review, read-only: data filters with state and last change date, internal traffic rules, key event list, custom dimension list, reporting identity, attribution settings, and Property change history. Line up any change date with the first-change date from step 2 of the procedure.
4. Pull the same metric by date with the GA4 MCP server's `run_report`, with the dimension that isolates the failing segment (page path, `method`, browser), so the evidence is a reproducible query rather than a screenshot.
5. Measurement Protocol events: take a sample of server-sent events from the backend log and put event time, request time and the browser session's start side by side. Pass: `session_id` is present, the request went out within 24 hours of session start, and the event timestamp sits inside the session.
6. Google Ads, read-only through the Google Ads MCP server's `search` tool and the UI. For the affected action read `conversion_action.status`, `conversion_action.primary_for_goal`, `conversion_action.include_in_conversions_metric`, `conversion_action.type` (the `GOOGLE_ANALYTICS_4_` types are GA4 imports) and `conversion_action.click_through_lookback_window_days`, then the UI Tracking status and Google Ads change history for the action since the baseline. Pass: the action is enabled and recording, still primary and in the bid goal, and none of its settings changed on the first-change date.

### Evidence to save

- The `run_report` request and response, with dates, dimensions and filters.
- For Google Ads, the GAQL query and its result for the action, the Tracking status, and the change history entries for the window.
- The data quality indicator text for the same view.
- Data filters with state and last-change date; custom dimension registration dates where relevant.
- A DebugView capture of the test event, with the test identity and time.

### Who fixes it

The GA4 property administrator, after the analytics owner approves. Filter activation needs particular care because its effect cannot be undone.

## Layer 7: export (GA4 to BigQuery)

### What breaks or misleads

- Daily export writes `events_YYYYMMDD` only if the daily option is enabled, after GA4 has collected the day. Google updates daily tables for up to 2 calendar days plus today, so the last three dates are provisional.
- Streaming export writes `events_intraday_YYYYMMDD` only if streaming is enabled. It is best effort, can have gaps, and the table is deleted once `events_YYYYMMDD` is complete. A streaming-only link therefore keeps its intraday tables. Streaming does not include campaign, source and medium for new users; take user attribution from the daily tables.
- A 360 property can also have Fresh Daily export, set independently of Daily and Streaming, with its own update timing and a completeness signal in Cloud Logging that names which export it belongs to. Treat it as a third mode with its own tables and timing (verify the table naming live).
- Older daily tables missing. In a BigQuery sandbox project, tables and partitions expire after 60 days and streaming data is not supported; any dataset can also carry a default table expiration. Days older than the expiry then look as if they were never exported. Read the dataset's default table expiration and whether billing is enabled on the project.
- Standard properties have a 1 million events per day limit on daily export. Google emails editors and administrators each time the property exceeds it, and pauses daily export when the property consistently exceeds it, or immediately when it significantly exceeds it. Paused days are not reprocessed. Streaming has no event limit.
- Export failures: no valid payment method in Cloud (data for that time cannot be re-exported), no service account with an active role on the project, or an organisation policy blocking export.
- Link scope: the streams selected, the export types (daily, streaming) and the events excluded in Admin, Product Links, BigQuery Links.
- Table dates follow the property timezone. Changing the property timezone can cause discrepancies or a skipped daily export.
- Google signals data is not exported, and data removed by active filters never arrives.

### Tests

1. Read the link's export types first. Then list daily and intraday tables separately, so a pile of intraday tables cannot push the daily ones out of the result:

   ```sql
   SELECT table_name, creation_time
   FROM `your-project.analytics_PROPERTY_ID.INFORMATION_SCHEMA.TABLES`
   WHERE table_name LIKE 'events_2%'
   ORDER BY table_name DESC
   LIMIT 30;

   SELECT table_name, creation_time
   FROM `your-project.analytics_PROPERTY_ID.INFORMATION_SCHEMA.TABLES`
   WHERE table_name LIKE 'events_intraday_%'
   ORDER BY table_name DESC
   LIMIT 30;
   ```

   Pass, daily export enabled: one `events_YYYYMMDD` per expected date, and an intraday table older than three days means that day's daily table never completed. Streaming only: intraday tables are expected and the rule does not apply. With Fresh Daily on a 360 property, list its tables separately as a third set. A run of daily tables that stops at a fixed age points at table expiration, not export (check the dataset's default table expiration).
2. Daily count for the event, daily tables only:

   ```sql
   SELECT _TABLE_SUFFIX AS table_date, COUNT(*) AS events
   FROM `your-project.analytics_PROPERTY_ID.events_*`
   WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260914'
     AND event_name = 'sign_up'
   GROUP BY table_date
   ORDER BY table_date;
   ```

   The wildcard matches every table starting with `events_`, intraday included. The numeric `BETWEEN` bounds exclude `intraday_` suffixes, because letters sort after digits. A query using `events_*` with no suffix filter double counts any day that still has both tables.
3. Compare with the GA4 interface for the same event, dates and timezone. Expect small definitional differences (deduplication, modelling, thresholds, HyperLogLog++ estimates of users and sessions), not a step change on one date.

### Evidence to save

- The link's export types and selected streams.
- Both table lists from test 1 and the per-day count from test 2, with the bytes processed.
- The GA4 figure for the same days and the stated definitional differences.

### Who fixes it

The Google Cloud project owner for billing, service accounts and policies; the GA4 administrator for link settings.

## Layer 8: warehouse model

### What breaks

- Deduplication on the wrong key. Grouping by user, event name and date drops legitimate repeats; no dedupe at all keeps double-sender duplicates. Do not assume the raw export matches GA4's deduplicated purchase count; check repeated IDs.
- Late data. An incremental model that only reloads yesterday misses GA4's updates to the last 2 calendar days plus today, and late backend records. Recent days look low and rise later, or never rise.
- Time zones. `event_date` is in the property timezone; `event_timestamp` is microseconds UTC. A backend in UTC compared with GA4 dates in a local timezone shifts counts across midnight, and daylight saving moves the boundary twice a year.
- Joins. Inner joins drop events whose key is null or unmatched, for example rows without `user_pseudo_id` or a session ID. Check identifier coverage by `privacy_info.analytics_storage` before trusting a joined count.
- Model filters: test accounts, internal domains or a hard-coded event list that a rename left behind.
- Location. BigQuery returns an error, or runs a global query, when a job touches datasets in mismatched locations; check the export dataset's location against the datasets it is joined with.
- Freshness. A successful run says the job finished, not that the source was complete. Check the source-through time and expected slices.

### Tests

1. Count the model against the raw export for the same days and definition:

   ```sql
   WITH raw AS (
     SELECT PARSE_DATE('%Y%m%d', event_date) AS d, COUNT(*) AS n
     FROM `your-project.analytics_PROPERTY_ID.events_*`
     WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260914'
       AND event_name = 'sign_up'
     GROUP BY d
   ), model AS (
     SELECT signup_date AS d, COUNT(*) AS n
     FROM `your-project.marketing.fct_signups`
     WHERE signup_date BETWEEN '2026-09-01' AND '2026-09-14'
     GROUP BY d
   )
   SELECT d, raw.n AS raw_events, model.n AS model_rows,
     IFNULL(model.n, 0) - IFNULL(raw.n, 0) AS diff
   FROM raw FULL OUTER JOIN model USING (d)
   ORDER BY d;
   ```

   Replace the model table and column with the real ones. Every non-zero `diff` needs a documented reason.
2. Duplicate check on the model's declared grain (one row per signup ID, transaction ID or event ID).
3. Timezone check: count by `DATE(TIMESTAMP_MICROS(event_timestamp))` in UTC and by `event_date` side by side; a constant offset of rows near midnight is a timezone mismatch, not lost data.
4. Read the model's incremental lookback and compare it with the three provisional dates.

### Evidence to save

- The raw-versus-model diff output for the incident days, with bytes processed.
- The duplicate check result on the declared grain.
- The model's incremental lookback setting and the commit that set it.

### Who fixes it

The owner of the model's incremental config, named in the model's metadata or the project's ownership file, as a draft change for review.

## Layer 9: report

### What breaks or misleads

- Date range and timezone: a partial current day, a different property or dashboard timezone, a period that crosses a timezone change. Google says a timezone change only affects data going forward and can show a flat spot or spike.
- Scope. User acquisition is scoped to new users and Traffic acquisition to sessions; key event credit is a third question. Two correct reports can disagree.
- Attribution settings. Changing the reporting attribution model applies to historical and future data; changing a lookback window applies going forward. Defaults are 30 days for acquisition key events and 90 days for the others. Check the change history before comparing periods.
- Key event selection and counting: "key events" can sum several outcomes.
- Hidden filters and scopes:
  - Looker Studio (named Data Studio in Google's current docs) filters can sit on a chart or control, a group, a page or the report. Higher-level filters pass down only when the child's data source has the filter's fields, and an editor can turn inheritance off per component, so two tiles on one page can apply different filters. Read the Filter section of each tile's Setup panel and the report's filter manager.
  - A GA4 Explore segment is scoped to users, sessions or events, and the scope changes which events are included. Two explorations with the "same" segment at different scopes disagree.
  - A customised GA4 standard report can carry a filter or comparison saved into the report itself, which a viewer may not notice. Rebuild the number from the property, not from the saved view.
- Looker Studio on GA4 (verify live):
  - The data source has a data freshness setting; Google's page gives 12 hours as the default for the Google Analytics connector. Cached results can be served within that interval, so a chart that loads is not proof that the source advanced. The report shows its last refresh time, and editors can use Refresh data.
  - The GA4 Data API behind the connector has per-property token quotas and a separate quota of 120 potentially thresholded requests per hour for demographic and audience dimensions. Quota errors show as broken or empty charts; they are not privacy thresholds and not missing data.
  - The tile's date range and timezone come from the report and the data source, not from the GA4 property view the complainant used.
  - Blended data sources join on keys with a configured join type; unmatched rows drop out or show as null depending on it. Check the keys and join type before comparing a blended tile.
- Google Ads reports conversions on the date of the ad, while other tools use the conversion date. Use the Conversions (by conv. time) columns when lining Google Ads up with GA4 or the backend.
- Thresholds, sampling and `(other)` (layer 6), which change what a report can show without any collection change.

### Tests

1. Rebuild the number from the layer below with the same definition: the GA4 MCP server's `run_report` for a GA4 card or a Looker Studio tile on GA4 (same dimensions, metric, dates and filters), BigQuery for an export-based card, the warehouse model for a dashboard tile.
2. Write both definitions side by side (metric, scope, dates, timezone, filters, attribution, freshness) and change one difference at a time until they agree or a named cause remains.

### Evidence to save

- Both definitions side by side.
- The rebuilt number and the query that produced it.
- The single change that closed the gap, or the named cause that remains.

### Who fixes it

The report owner. A definitional difference is documented next to the report, not "fixed" by changing collection.
