Summary
The ads insights path surfaces impressions, clicks, and spend per campaign and ad group, but not conversions. Downstream consumers want to rank ad groups (and the themes they map to) by conversions, so a keep/stop/scale decision can be made on real outcome data, not just spend or name matching. Conversions are currently absent end to end: not requested, not stored, not served.
Today
packages/canonry/src/ads-sync.ts requests only {impressions,clicks,spend} per level (the CAMPAIGN_INSIGHT_FIELDS / AD_GROUP_INSIGHT_FIELDS arrays).
OpenAiAdsInsightRow (packages/integration-openai-ads/src/types.ts) has no conversion field; adsInsightsDaily (packages/db/src/schema.ts) has impressions, clicks, spendMicros only; the DTOs (packages/contracts/src/ads.ts: adsInsightRowDtoSchema, adsTotalsDtoSchema) and the serializers (packages/api-routes/src/ads.ts) emit no conversions.
bidding_type IS already surfaced on the campaign DTO. conversion_event_setting_ids is fetched on the campaign but dropped.
Proposed (additive, no new endpoint or table)
- Step 0, blocking: curl a live ad account with
fields[]=campaign.conversions (and likely cost_per_conversion) to confirm the real field name, then capture the response into packages/integration-openai-ads/test/fixtures.ts. Per the integration's rule, do not add a field that has not been observed. If conversions are not a fields[] metric on the existing /insights surface, re-scope to the correct endpoint.
- Add
conversions?: number (and optionally cost_per_conversion) to OpenAiAdsInsightRow.
- Request the field in
ads-sync.ts for both campaign and ad-group levels; read it into the insight upsert.
- Add a
conversions column to adsInsightsDaily with a new idempotent MIGRATION_VERSIONS entry in packages/db/src/migrate.ts.
- Add
conversions to adsInsightRowDtoSchema + adsTotalsDtoSchema; serialize on GET /ads/insights and GET /ads/summary; pnpm gen to regenerate the SDK.
- Render conversions in the
ads insights / ads summary CLI.
- Tests asserting the conversion totals/roll-up math (mirror the existing
adsCtr / adsCpcMicros tests) plus the schema+DTO drift checks.
The app already reads GET /ads/insights?level=ad_group, so ad-group ranking is unblocked once the field lands on the row DTO.
Size
Medium. Crosses upstream type, sync, schema + migration, DTOs, routes, CLI, and tests, but all additive, no new endpoint or table. The only unknown is the upstream field name (Step 0).
Out of scope (possible follow-up)
A caller-supplied seedQueries param on POST /projects/:name/discover/run so callers can seed the fan-out with their own phrasings (e.g. existing ad context hints). Small, but optional: a consumer can merge such seeds app-side. File separately only if engine-side dedup/probe/classification of seeded hints is wanted.
Summary
The ads insights path surfaces
impressions,clicks, andspendper campaign and ad group, but not conversions. Downstream consumers want to rank ad groups (and the themes they map to) by conversions, so a keep/stop/scale decision can be made on real outcome data, not just spend or name matching. Conversions are currently absent end to end: not requested, not stored, not served.Today
packages/canonry/src/ads-sync.tsrequests only{impressions,clicks,spend}per level (theCAMPAIGN_INSIGHT_FIELDS/AD_GROUP_INSIGHT_FIELDSarrays).OpenAiAdsInsightRow(packages/integration-openai-ads/src/types.ts) has no conversion field;adsInsightsDaily(packages/db/src/schema.ts) hasimpressions, clicks, spendMicrosonly; the DTOs (packages/contracts/src/ads.ts:adsInsightRowDtoSchema,adsTotalsDtoSchema) and the serializers (packages/api-routes/src/ads.ts) emit no conversions.bidding_typeIS already surfaced on the campaign DTO.conversion_event_setting_idsis fetched on the campaign but dropped.Proposed (additive, no new endpoint or table)
fields[]=campaign.conversions(and likelycost_per_conversion) to confirm the real field name, then capture the response intopackages/integration-openai-ads/test/fixtures.ts. Per the integration's rule, do not add a field that has not been observed. If conversions are not afields[]metric on the existing/insightssurface, re-scope to the correct endpoint.conversions?: number(and optionallycost_per_conversion) toOpenAiAdsInsightRow.ads-sync.tsfor both campaign and ad-group levels; read it into the insight upsert.conversionscolumn toadsInsightsDailywith a new idempotentMIGRATION_VERSIONSentry inpackages/db/src/migrate.ts.conversionstoadsInsightRowDtoSchema+adsTotalsDtoSchema; serialize onGET /ads/insightsandGET /ads/summary;pnpm gento regenerate the SDK.ads insights/ads summaryCLI.adsCtr/adsCpcMicrostests) plus the schema+DTO drift checks.The app already reads
GET /ads/insights?level=ad_group, so ad-group ranking is unblocked once the field lands on the row DTO.Size
Medium. Crosses upstream type, sync, schema + migration, DTOs, routes, CLI, and tests, but all additive, no new endpoint or table. The only unknown is the upstream field name (Step 0).
Out of scope (possible follow-up)
A caller-supplied
seedQueriesparam onPOST /projects/:name/discover/runso callers can seed the fan-out with their own phrasings (e.g. existing ad context hints). Small, but optional: a consumer can merge such seeds app-side. File separately only if engine-side dedup/probe/classification of seeded hints is wanted.