Skip to Content
ChannelsGoogleadsGoogle Ads — Current Implementation

Google Ads — Current Implementation

Authentication & Token Storage

OAuth 2.0 flow. Credentials stored on ConnectedChannel:

FieldContent
tokenInfo (encrypted){ accessToken, refreshToken, expireOn }
subChannelInfo (plain JSON){ id: string } — Google Ads customer ID selected during OAuth

The developer token (GOOGLE_ADS_DEVELOPER_TOKEN) is a platform-level secret required in every API request header.

Database Models

ConnectedChannel

Standard channel record. type = "google_ads". subChannelInfo.id is the Google Ads Customer ID.

CampaignExternalMapping

Links an internal Campaign record to a Google Ads campaign ID.

platform = "google_ads" externalCampaignId = "123456789" // Google Ads campaign resource ID connectedChannelId → ConnectedChannel campaignId → Campaign

CampaignMetrics

Daily/periodic performance snapshots synced by the insights worker.

SearchTermReport

Raw search queries pulled from search_term_view.

tenantId, campaignId, adGroupId? searchTerm, matchType impressions, clicks, cost, conversions, ctr, avgCpc syncDate // date the data was fetched (UTC midnight) @@unique([campaignId, searchTerm, syncDate])

SearchTermClassification

AI + DM classification for each search term.

aiClassification: "add_as_keyword" | "add_as_negative" | "watch" | "irrelevant" aiRationale: string dmDecision: string | null // null until DM reviews status: "pending" | "dm_reviewed" | "pushed" | "skipped" pushedAt: DateTime | null

NegativeKeyword

Campaign-level negative keywords tracked after push.

source: "manual" | "search_term_classification" status: "pending_push" | "active" | "removed" externalCriterionId: string // Google Ads criterion resource ID after push

CampaignKeyword

Active positive keywords synced from Google Ads.

AdGroup

Ad groups with default bids.

GoogleAdsService (packages/providers/google/src/google-ads.ts)

Key methods:

MethodWhat it does
getAllCustomers()Lists accessible top-level accounts (for sub-channel selection)
getCampaignsAsLookup()Returns { id, name } list for all campaigns
getCampaignDailyMetrics(from, to)Daily impressions/clicks/cost/conversions per campaign
getSearchTerms(from, to, campaignId?)Pulls search_term_view rows for the period
getKeywords(campaignId?)Lists active keywords with bid + quality score
getAdGroups(campaignId?)Lists ad groups
getNegativeKeywords(campaignId?)Lists campaign-level negatives
addNegativeKeyword(campaignId, adGroupId, keyword, matchType)Adds a negative keyword
removeNegativeKeyword(campaignId, criterionId)Removes a negative keyword
getKeywordMetricsByKeyword(keyword)Historical search volume for a keyword

All calls use the Authorization: Bearer {accessToken} + developer-token + optional login-customer-id headers. Token refresh is not yet automated in the analytics endpoint — a stale token will return a 401.

Workers

  • Triggered manually via the Insights tab on the channel detail page.
  • Fetches campaign list + daily metrics for the selected period.
  • Passes data to Claude (role: google-ads-insights) to generate a narrative report.
  • Saves result as a ChannelInsight record.
  • Generates RSA (Responsive Search Ad) headline/description copy.
  • Standard content worker pipeline — uses creditType: "google_ads_rsa".

search-term-classifier (agent__search-term-classifier)

  • Receives a batch of SearchTermReport IDs + campaignId.
  • Fetches term metrics from DB.
  • Calls Claude with AgentConfig for role search-term-classifier.
  • Upserts SearchTermClassification records with status: "pending".
  • Concurrency: 2 workers.

Analytics Endpoint

GET /tenant/v1/channels/:id/analytics?from=YYYY-MM-DD&to=YYYY-MM-DD

Supported channel types: google_ads, google_search_console, google_analytics, google_business_profile, facebook, instagram, linkedin.

Response shape for google_ads:

{ "period": { "from": "...", "to": "..." }, "stats": { "impressions": { "current": 0, "previous": 0, "deltaPercent": 0 }, "clicks": { "current": 0, "previous": 0, "deltaPercent": 0 }, "ctr": { "current": 0, "previous": 0, "deltaPercent": 0 }, "avgCpc": { "current": 0, "previous": 0, "deltaPercent": 0 }, "cost": { "current": 0, "previous": 0, "deltaPercent": 0 }, "conversions": { "current": 0, "previous": 0, "deltaPercent": 0 } }, "campaigns": [ { "id": "", "name": "", "status": "ENABLED", "impressions": 0, "clicks": 0, "ctr": 0, "avgCpc": 0, "cost": 0, "conversions": 0 } ], "dailyPerformance": [ { "date": "YYYY-MM-DD", "impressions": 0, "clicks": 0, "cost": 0 } ] }

Dashboard UI

apps/dashboard/src/app/(dashboard)/channels/[id]/GoogleAdsChannelDetail.tsx

Three inner tabs under Analytics:

  • Overview — 6 stat cards with delta vs previous period + daily impressions/clicks line chart
  • Campaigns — sortable/filterable table (name, status, impressions, clicks, CTR, avg CPC, cost, conversions)
  • Daily — impressions/clicks line chart + cost per day bar chart

Plus an Insights tab shared via ChannelInsightsTab.

© 2026 Leadmetrics — Internal use only