Skip to content

feat(connectors/google-ads): first-class Google Ads primitives + MCP/HTTP-SSE + Temporal#18

Merged
ninan-versioning merged 1 commit into
mainfrom
jared-shoploop/google-ads-connector
Jun 26, 2026
Merged

feat(connectors/google-ads): first-class Google Ads primitives + MCP/HTTP-SSE + Temporal#18
ninan-versioning merged 1 commit into
mainfrom
jared-shoploop/google-ads-connector

Conversation

@ninan-versioning

Copy link
Copy Markdown
Owner

Summary

Adds @shoploop/connector-google-ads — the first Performance Ingest source, closing the right half of the architecture loop:

Distribution (Meta / TikTok / Klaviyo) → Performance Ingest (ROAS, CTR, CAC)
→ Thompson Sampling → brand_rules.json → render pack regenerates

Mirrors connectors/shopify (PR #15) exactly: same primitives/ · mcp/ · temporal/ layout, same HTTP/SSE transport, same bearer auth (SHOPLOOP_MCP_TOKEN), same single-internal-core discipline, same Temporal idempotency convention. MCP-shaped and agent-callable — not a thin REST wrapper. Uses google-ads-api (Google Ads API v17, OAuth2 refresh-token flow, env-driven creds).

Stacked on #15. Branched from jared-shoploop/shopify-connector-mcp; until #15 merges, this PR's diff against main includes the Shopify connector. Review only connectors/google-ads/** + the root README connector matrix here. The join helper reuses the Shopify connector's update_metafield write path (injected — no duplicated Shopify client code).

Four primitives

  1. Performance ingestingestPerformance({ customerId, since, outDir }). GAQL across campaign/ad_group/ad (impressions, clicks, conversions, cost_micros, ctr, average_cpc, derived roas = conversions_value / cost) → one JSONL/day at <outDir>/<customerId>/YYYY-MM-DD.jsonl matching contracts/performance_observation.v1.json. Dedupes by (date, campaign_id, ad_group_id, ad_id). Joins campaign.id ↔ Shopify product.id via the shoploop.gads_campaign_id metafield.
  2. MCP tools (HTTP/SSE on :8788) — list_campaigns, get_campaign, get_performance (daily/weekly rollups), get_search_terms (negative-keyword harvesting), get_change_history; write tools pause_campaign / resume_campaign gated behind SHOPLOOP_GADS_WRITE_ENABLED=true (default off, refused otherwise).
  3. Brand-rules feedback hookrecordPerformanceForBandit(observation) enqueues to BANDIT_QUEUE_PATH. Thompson Sampling update is stubbed with a clear TODO; the typed PerformanceObservation is the producer/consumer contract so a real bandit drops in without changing the producer side.
  4. Scheduled ingest workflowIngestGoogleAdsPerformanceWorkflow(customerId) (hourly, queries last 25h overlap, flushes observations to the bandit queue) + PauseUnderperformingCampaignWorkflow(customerId, campaignId, reason) (typed entry point, no-op behind the write flag). Worker on task queue shoploop-google-ads; idempotency id gads:<customerId>:<workflow>:<hour_bucket>. CLI worker --once --customer <id> runs the hourly workflow on-demand for dev.

CLI binaries: shoploop-google-ads ingest|mcp|worker.

Decision gate (per AGENTS.md)

  1. Varitea dogfood critical path? Yes — Varitea already runs Google Ads; this is the loop's input layer.
  2. Deepens the moat? Yes — feeds the Thompson Sampling bandit that drives brand_rules.json; ingest + signal, no commodity ad-buying.
  3. Mistral/Codestral cheaper? N/A (no autoresearch call).
  4. Ships ≤2 weeks solo? Yes.
  5. Monthly cost: ~$0/mo incremental — API free (Basic Access tier), JSONL storage <$1/mo, co-located on the Shopify t4g.small.

Testing

  • npm run typecheck — clean.
  • npm test23 passing across 3 files:
    • primitives.test.ts — ingest emits 7 JSONL files / 56 observations (8/day), contract shape + derived roas, overlap dedup, Shopify join, rollups (daily + Monday-weekly), bandit enqueue, join metafield write.
    • mcp.test.ts — in-proc over MCP transport: all 7 tools listed, each read tool exercised, write tools refused when the flag is off and succeed when on.
    • workflow.test.ts — Temporal hour-bucket idempotency (duplicate hourly fires collapse to one execution under REJECT_DUPLICATE; next hour starts fresh).
  • npm run build — emits dist/ (CLI bin → dist/cli.js).

Varitea dogfood acceptance

  • shoploop-google-ads ingest --customer 5128873230 --since 2026-06-12 --out ./var/shoploop/perf → JSONL emitted per day under 5128873230/.
  • MCP get_performance returns the 7-day daily rollup (and a Monday-keyed weekly rollup) with derived ROAS.

Test plan

🤖 Generated with Claude Code

…HTTP-SSE + Temporal

Performance Ingest source #1 for the Thompson Sampling loop. Mirrors connectors/shopify
exactly (primitives/, mcp/, temporal/; HTTP/SSE + bearer auth; Temporal idempotency).

Four primitives:
1. ingestPerformance() — GAQL campaign/ad_group/ad metrics → one JSONL/day of typed
   PerformanceObservation (roas = conversions_value/cost). Dedup by
   (date, campaign_id, ad_group_id, ad_id). Shopify join via shoploop.gads_campaign_id.
2. MCP (7 tools on :8788) — list_campaigns, get_campaign, get_performance (daily/weekly),
   get_search_terms, get_change_history; pause/resume gated behind SHOPLOOP_GADS_WRITE_ENABLED.
3. recordPerformanceForBandit() — enqueues observations to BANDIT_QUEUE_PATH; Thompson
   Sampling consumer stubbed with a clear TODO.
4. Temporal — IngestGoogleAdsPerformanceWorkflow (hourly, last 25h overlap) +
   PauseUnderperformingCampaignWorkflow (no-op behind write flag). Idempotency id
   gads:<customerId>:<workflow>:<hour_bucket>. CLI worker --once for dev.

Cost: ~$0/mo incremental (Basic Access free, JSONL <$1, co-located on Shopify t4g.small).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ninan-versioning
ninan-versioning force-pushed the jared-shoploop/google-ads-connector branch from 2946797 to 0dc96dc Compare June 26, 2026 06:00
@ninan-versioning
ninan-versioning merged commit 9f28551 into main Jun 26, 2026
1 check passed
@ninan-versioning
ninan-versioning deleted the jared-shoploop/google-ads-connector branch June 26, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant