# Platform reads: exact read-only queries per platform

Level L2 checks come from these reads. Every read here changes nothing. Field and tool names were checked on 2026-11-01 and 2026-11-02, and the GA4, Google Ads and BigQuery MCP tools on 2026-11-06, against the sources in `sources.yaml`; confirm them in the live reference or the tool's schema before quoting them, and save each output with its read time as an evidence item.

Completed days only: skip the last 2 days for GA4 reports (processing can take 24 to 48 hours), end BigQuery export reads 4 days before the run (Google can change a daily table for up to 3 days after its date), and skip the account's conversion lag for Google Ads.

## GA4 (Google Analytics MCP server)

Run the server with `analytics.readonly` credentials and without `--allow-write` (write tools such as `create_property_annotation` exist only behind that flag). Read tools: `get_account_summaries`, `get_property_details`, `list_key_events`, `list_google_ads_links`, `list_property_annotations`, `get_custom_dimensions_and_metrics`, `run_report`, `run_funnel_report`, `run_conversions_report`, `run_realtime_report`.

In order:

1. `get_account_summaries`: find the property. Stop if it is not the one in scope.
2. `get_property_details`: reporting time zone and currency against metrics.md (CFG-01).
3. `list_google_ads_links`: expected links present (CFG-03).
4. `list_key_events`: the property's key events and their counting method (CNV-05).
5. `get_custom_dimensions_and_metrics`: parameters the plan reports on are registered; no order, user or session IDs registered as dimensions (EVT-07).
6. `run_report` requests (Data API names; read the tool's schema for argument names):
   - dimension `eventName`, metrics `eventCount` and `keyEvents`, last 28 completed days: unplanned and legacy names, case variants (EVT-06); events with key events above zero that are not outcomes (CNV-05).
   - dimension `transactionId`, metrics `ecommercePurchases` and `purchaseRevenue`, 7 to 28 completed days: the GA4 side of the order sample (CNV-10). The result holds order IDs: keep it in the project's exports folder with the order export and quote counts only.
   - dimension `sessionSource` filtered to the payment, booking and sign-in hosts, metrics `sessions` and `keyEvents`: processors appearing as sources (XDS-03).
   - dimension `hostName`, metric `sessions`: staging, preview or unknown hosts in production data.
   - dimension `sessionDefaultChannelGroup`, metric `keyEvents`, 8 completed weeks by week: Unassigned and (not set) share against its band (CFG-05).
7. `list_property_annotations`: line release dates up with breaks before calling something a trend.

The GA4 MCP server does not read data retention, enhanced measurement or data redaction. The Admin API does with `analytics.readonly`, through any read-only client the user already runs:

- `properties.getDataRetentionSettings` (v1beta): data retention (CFG-02).
- `properties.dataStreams.getEnhancedMeasurementSettings` (v1alpha): enhanced measurement and its page changes setting per web stream (CFG-04). The settings take effect only when the stream has enhanced measurement on.
- `properties.dataStreams.getDataRedactionSettings` (v1alpha): email and query parameter redaction per web stream (PII-01 evidence).
- Read field names in the live reference before quoting them; v1alpha methods can change.

Data filter states (PII-04: an active filter's effect is permanent), unwanted referrals (XDS-03) and configured domains (XDS-07) come from an Admin UI read by the owner or from screenshots.

## BigQuery export (BigQuery MCP server, `execute_sql_readonly`)

Two BigQuery MCP tools are not read-only: `execute_sql` and `cancel_job`, which aborts a running job. The tool reference marks both Read Only Hint false and Destructive Hint true (checked 2026-11-06); the BigQuery MCP guide still names only `execute_sql`, so read the tool reference. Deny both in the client's tool permissions, ask an administrator to apply the deny policy that restricts read-write MCP tool use, as the guide describes (it denies any tool not annotated read-only), use an identity with read-only roles on the export dataset, and call only `execute_sql_readonly`, plus `get_query_results` and `get_job` for a long-running read. Queries stop after three minutes and return at most 3,000 rows by default. Dry-run first (`dryRun` on `execute_sql_readonly`; read the argument names from the client's tool schema, because Google's pages use two spellings), set `jobTimeoutMs` and a label, and agree a byte budget with the user: the tool has no maximum bytes billed, and a project quota is a backstop, not a per-query cap. Always filter `_TABLE_SUFFIX`. Replace the project and dataset placeholders. The dates below suit a run on 2026-11-01.

```sql
-- CNV-03: purchases with an empty or missing transaction_id, by day
SELECT event_date,
  COUNTIF(ecommerce.transaction_id IS NULL OR ecommerce.transaction_id IN ('', '(not set)')) AS no_id,
  COUNT(*) AS purchases
FROM `project.analytics_PROPERTY_ID.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261028' AND event_name = 'purchase'
GROUP BY event_date ORDER BY event_date;

-- CNV-02 and CNV-10: transaction IDs with more than one purchase event
SELECT ecommerce.transaction_id, COUNT(*) AS purchases, COUNT(DISTINCT stream_id) AS streams
FROM `project.analytics_PROPERTY_ID.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261028' AND event_name = 'purchase'
  AND ecommerce.transaction_id NOT IN ('', '(not set)')
GROUP BY 1 HAVING COUNT(*) > 1 ORDER BY purchases DESC LIMIT 50;

-- SST-06: events without a session ID, by stream and day (server events missing visitor context)
SELECT event_date, stream_id, COUNT(*) AS events,
  COUNTIF((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') IS NULL) AS no_session
FROM `project.analytics_PROPERTY_ID.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261028'
GROUP BY 1, 2 ORDER BY 1, 2;

-- PII-05: production events carrying debug_mode, and which hosts sent them
SELECT event_date, device.web_info.hostname AS host, COUNT(*) AS debug_events
FROM `project.analytics_PROPERTY_ID.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261028'
  AND EXISTS (SELECT 1 FROM UNNEST(event_params) WHERE key = 'debug_mode')
GROUP BY 1, 2 ORDER BY 1;

-- Hostnames per day: staging or preview hosts leaking into production
SELECT event_date, device.web_info.hostname AS host, COUNT(*) AS events
FROM `project.analytics_PROPERTY_ID.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261028'
GROUP BY 1, 2 ORDER BY 1, 3 DESC;
```

## Google Ads (Google Ads MCP server)

The server is not read-only as shipped. Its README lists mutate tools (`create_asset`, `upload_video_asset`, `manage_labels`, `manage_label_associations`) that are on by default, while Google's developer page still says read-only (checked 2026-11-06). Set `labels: false` and `assets: false` in its `tools_config.yaml`, connect as a user with Read-only access, and call only `search`, `get_resource_metadata` for a resource's selectable fields, and `list_accessible_customers` (listed as `search_search`, `metadata_get_resource_metadata` and `customers_list_accessible_customers` with the bundled tools configuration). `search` builds the GAQL itself: pass each query below as its parts (`resource`, `fields`, `conditions`, `orderings`, `limit`) with `customer_id` in digits, and read the argument names from the tool's schema. Call `get_resource_metadata` on `customer`, `conversion_action`, `campaign`, `landing_page_view` and `offline_conversion_upload_conversion_action_summary` before the first query, and check every field below against it, because field compatibility differs by resource and API version (fields here are from API v26).

```sql
-- CID-02 and CNV-09: account settings
SELECT customer.id, customer.auto_tagging_enabled,
  customer.conversion_tracking_setting.conversion_tracking_status,
  customer.conversion_tracking_setting.accepted_customer_data_terms
FROM customer

-- CID-01 and INV-05: the landing URLs that carry paid clicks, to choose the routes to test
SELECT landing_page_view.unexpanded_final_url, metrics.clicks
FROM landing_page_view
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.clicks DESC

-- CID-01: tracking templates and final URL suffixes the test URLs are built from
SELECT customer.tracking_url_template, customer.final_url_suffix FROM customer

SELECT campaign.name, campaign.tracking_url_template, campaign.final_url_suffix
FROM campaign
WHERE campaign.status = 'ENABLED'

-- CNV-05 to CNV-09, CNV-12: conversion actions
SELECT conversion_action.id, conversion_action.name, conversion_action.status,
  conversion_action.type, conversion_action.category, conversion_action.origin,
  conversion_action.primary_for_goal, conversion_action.counting_type,
  conversion_action.include_in_conversions_metric,
  conversion_action.value_settings.default_value,
  conversion_action.value_settings.always_use_default_value,
  conversion_action.value_settings.default_currency_code,
  conversion_action.click_through_lookback_window_days
FROM conversion_action
WHERE conversion_action.status != 'REMOVED'

-- CNV-09: conversions by action on conversion date, 30 completed days (90 for low-volume actions)
SELECT segments.conversion_action_name, segments.date,
  metrics.all_conversions_by_conversion_date, metrics.all_conversions_value_by_conversion_date
FROM campaign
WHERE segments.date BETWEEN '2026-09-27' AND '2026-10-26'

-- CID-08: offline upload diagnostics
SELECT offline_conversion_upload_conversion_action_summary.conversion_action_name,
  offline_conversion_upload_conversion_action_summary.status,
  offline_conversion_upload_conversion_action_summary.alerts,
  offline_conversion_upload_conversion_action_summary.last_upload_date_time
FROM offline_conversion_upload_conversion_action_summary
```

How to read them:

- Landing routes: test the URLs carrying most paid clicks and every distinct host among them. Templates and suffixes can also sit on ad groups and ads, and the most specific level wins (ad over ad group over campaign over account), so read the lower levels for the routes you test.
- Type values to recognise: `WEBPAGE` (website tag), `WEBPAGE_CODELESS` (rules built in the UI on clicks or URLs), `GOOGLE_ANALYTICS_4_PURCHASE` and `GOOGLE_ANALYTICS_4_CUSTOM` (GA4 imports), `UPLOAD_CLICKS` (GCLID uploads), `SALESFORCE`.
- CNV-06: group actions by `category`; more than one `primary_for_goal = true` action counting the same outcome is a FAIL.
- CNV-07: lead categories (`SUBMIT_LEAD_FORM`, `QUALIFIED_LEAD`) expect `ONE_PER_CLICK`; purchases expect `MANY_PER_CLICK`.
- CNV-08: a revenue action with `always_use_default_value = true` reports a constant.
- CNV-09: status labels in the UI are changing; read the status tooltip and judge by meaning (never seen, stopped, or seen with no recent conversions). For an action that normally records under one conversion a week, compare its last conversion with its usual interval and with CRM outcomes before rating it. Upload summaries are generated from the most recent calendar day with upload stats; add the count fields your API version lists.
- CID-08: record the upload route. Since 15 June 2026 offline conversion import and enhanced conversions for leads uploads go through the Data Manager API and are blocked in the Google Ads API, and developer tokens with no request between January and June 2026 are not allowlisted for legacy access (help 2998031). Offline imports are accepted up to 90 days after the last click (63 days for enhanced conversions for leads, help 15081888), and Data Manager imports only the last 14 days on the first successful HubSpot or Salesforce run (help 7012522).

## Meta (Meta Ads MCP server, read tools only)

In order: `ads_get_datasets`; `ads_get_dataset_details` (Conversions API setup); `ads_get_dataset_stats` (up to 28 days, broken down by event source or device: a Purchase count near twice the orders with both sources present points at SST-02); `ads_get_dataset_quality` (Event Match Quality, per-key coverage and freshness by channel for SST-07; report as a trend, not a grade); `ads_pixel_event_read` and `ads_pixel_parameter_read` (event setup tool rules for CNV-12); `ads_get_customconversions` (URL rules that duplicate coded events).

- Never call `ads_pixel_event_create`, `ads_pixel_event_update`, `ads_pixel_event_delete`, `ads_pixel_parameter_create`, `ads_pixel_parameter_update` or `ads_pixel_parameter_delete`.
- Events Manager, read by a person with dataset access: Test Events shows whether a browser and server pair was deduplicated; the Overview can show events received before deduplication, so a raw received count is not attributed purchases.

## TikTok, LinkedIn, Microsoft Advertising and OpenAI Ads

No official read-only MCP routes are listed here; use UI reads by the account owner and verify menu paths live.

- TikTok Events Manager: diagnostics and the deduplication view for pixel and Events API pairs (SST-03).
- LinkedIn Campaign Manager: one conversion rule per data source; when the Insight Tag and Conversions API send the same event id, only the Insight Tag event counts (SST-03); URL-rule conversions (CNV-12).
- Microsoft Advertising: UET tag status and its consent signal, auto-tagging of msclkid (CID-02) and the UET Tag Helper for request checks, including the `asc` state.
- OpenAI Ads: the conversion sources and settings in Ads Manager. Browser and server copies deduplicate on pixel ID, event name and the pixel `event_id` equal to the Conversions API `id`; the first event received is kept (SST-03).

## CRM exports

- HubSpot contacts: Record ID, Create date, Original Traffic Source, Latest Traffic Source and their drill-downs, plus the click ID and UTM properties. HubSpot sets its default Google ad click ID and Facebook click ID itself, not from form submissions (HubSpot default contact properties), while hidden fields write custom properties such as gclid. Export both, run crm-attribution-coverage once per column, and record which one the offline import reads (CID-05, CID-06, CID-08). Labels vary by account and language; read internal names in property settings and pass the column names to the script.
- Salesforce: a report on leads, contacts and opportunities with the custom click ID and UTM fields, Lead Source and created date. Check field lengths against click ID lengths, and remember that lead conversion fills empty mapped fields and does not overwrite existing contact values (CID-07).
- Consent state: where the decision forbids click ID capture without consent, add the column that records consent at submission and pass it with `--consent` and `--consented`.
- Drop before analysis: names, emails, phones, addresses and free text. The script ignores such columns, names them so they can be removed, and refuses one passed as `--id`, `--split` or `--consent`.
- Dates: ISO dates (2026-08-03 09:14) are read as they are. Salesforce and some HubSpot exports use slash dates, which read differently by locale: pass `--date-order mdy` or `dmy`, or the script counts them unreadable instead of guessing.

## Order export

- Order ID, placed and paid time with time zone, status, net and gross value, currency, channel and a test flag. No customer fields.
- Join on order ID to GA4 `transactionId` for 7 to 28 completed days. Under 200 eligible orders, extend the window or mark the result provisional.

## Never call

- Tag Manager: create, update, remove, revert, createVersion, sync, resolveConflict, quickPreview, publish, setLatest, undelete, importFromGallery, on any route (API, Stape's GTM MCP server or a community server).
- The GA4 MCP server started with `--allow-write`, or any GA4 Admin write.
- The Meta pixel event and parameter create, update or delete tools.
- BigQuery `execute_sql` or `cancel_job`.
- The Google Ads MCP tools `create_asset`, `upload_video_asset`, `manage_labels` and `manage_label_associations`.
- Any conversion upload, Measurement Protocol send, Conversions API send or Events API send.
