Feature request: Google Ads criteria tables (ad_group_criterion & campaign_criterion)
enhancementteam/data-warehousefeature/pipeline-sources
<!-- req-ref: 1786124859.443039 -->
## What & why
The Google Ads data warehouse source currently ships a `keyword` table, but it only covers positive keywords (`ad_group_criterion.negative = false`-shaped data via `keyword_view`). There is no table today for negative keywords or for non-keyword targeting criteria — audience lists, geo/location targeting, age range, gender, and similar attachments — at either the ad group or campaign level. There is also no coverage of criterion approval status for these criterion types, or of labels applied to criteria.
This makes it impossible to audit what an advertiser is excluding (negative keywords) or who/where they're targeting (audience, geo, demo criteria) — a common governance and troubleshooting need for advertisers managing Google Ads accounts through PostHog's data warehouse.
## Implementation plan
- **Research findings**: per the [GAQL fields reference (v19)](https://developers.google.com/google-ads/api/fields/v19/overview), the relevant resources are `ad_group_criterion` (criteria attached to an ad group) and `campaign_criterion` (criteria attached directly to a campaign), plus `ad_group_criterion_label` / `campaign_criterion_label` for criterion labels. Criteria are polymorphic on `type` — confirmable values include `KEYWORD` (with `ad_group_criterion.negative = true` for negative keywords), `USER_LIST` (audience attachments), `LOCATION` (geo targeting), `AGE_RANGE`, and `GENDER` (demographic targeting), among others. Both resources expose `approval_status` for reviewing whether a criterion has been approved, is under review, or was disapproved.
- **Pattern to follow**: the existing `keyword` table (`products/warehouse_sources/backend/temporal/data_imports/sources/google_ads/schemas.py`, `resource_name: "keyword_view"`) is the closest template — it already selects `ad_group_criterion.*` fields including `ad_group_criterion.negative`, `ad_group_criterion.approval_status`, and `ad_group_criterion.status`, joined with `ad_group.id`, `campaign.id`, `customer.id`. A new implementation needs a query against `ad_group_criterion` (and separately `campaign_criterion`) with any `type = KEYWORD AND negative = false` -style filtering removed or widened, and additional criterion-type fields added for the non-keyword types above.
- **Concrete steps**:
- Recommend shipping one unified `ad_group_criterion` table plus a separate `campaign_criterion` table (matching GAQL's own resource split), rather than folding negative keywords into the existing `keyword` table with a `negative` boolean column — the existing `keyword` table is scoped to `keyword_view` and is documented/consumed as positive-keyword data; broadening it to include negatives and non-keyword criterion types would change its existing shape and any code depending on `keyword`'s current field set. New tables keep the blast radius limited to net-new code.
- Define primary keys following the `keyword` table's convention: `ad_group_criterion.criterion_id` + `ad_group.id` + `campaign.id` + `customer.id` for `ad_group_criterion`; the analogous `campaign_criterion.criterion_id` + `campaign.id` + `customer.id` for `campaign_criterion`.
- Add criterion label tables (`ad_group_criterion_label`, `campaign_criterion_label`) following the pattern of the existing generic `label` table referenced in `canonical_descriptions.py`.
- Add tests following `products/warehouse_sources/backend/temporal/data_imports/sources/google_ads/tests/test_google_ads_source.py`, and register the new tables/schemas per the `implementing-warehouse-sources` skill workflow (adding datasets to an existing source).
- **Open questions/risks**: criterion `type` polymorphism means different sub-fields are populated depending on the criterion type (e.g. `ad_group_criterion.keyword.text` for `KEYWORD`, `ad_group_criterion.user_list.user_list` for `USER_LIST`, `ad_group_criterion.location.geo_target_constant` for `LOCATION`, `ad_group_criterion.age_range.type` for `AGE_RANGE`, `ad_group_criterion.gender.type` for `GENDER`). Selecting every possible sub-field as its own column produces a very wide, mostly-null table as more criterion types are added over time. Whoever implements this should decide between one column per possible sub-field (simple, matches other tables in this source, but sparse) versus a single JSON column holding type-specific details (denser, but a schema break from the rest of this source) — this is a design decision to make explicitly before implementation, not something to default silently.
Write for a zero-context engineer/agent to execute async.
0 条评论