Lens

Use Case: GA4 Sessions and Google Ads Cost per Campaign

Put GA4 sessions and Google Ads spend on one row per campaign per day, add cost per session and ROAS, and share the result as a report.

The goal

Marketing wants one view of what each paid campaign costs and what it brings: sessions from GA4, spend and conversion value from Google Ads, and cost per session and return on ad spend worked out per campaign per day.

You will buildLens feature
A Google Ads table: cost and conversion value per campaign per dayGoogle Ads source
A GA4 table: sessions per campaign per dayBigQuery source on the GA4 export
One table with both, matched by date and campaignJoin
Cost per session and ROASRatio metrics
A board and a client reportBoards, Reports

Why the campaign column needs the same name

A join matches rows on the date plus key dimensions that have the same name in both tables. On the Campaign Performance stream, Google Ads stores the campaign name in a dimension called name. A GA4 Data API table stores it as sessionCampaignName, and GA4 dimension names cannot be renamed, so those two tables cannot be joined on campaign.

The GA4 BigQuery export solves this: a Custom SQL query can name its columns anything. This walkthrough names the GA4 campaign column name so it matches Google Ads.

What you need

A Google connection with access to the Ads account, and a Google service account that can read the BigQuery dataset holding your GA4 export.

Step 1: Google Ads table

  1. Configure > Data Tables > + New Table > Google Ads.
  2. Pick the connection and customer account.
  3. Choose the Campaign Performance stream. Its fields include the dimension name and the metrics cost and conversions_value.
  4. Name it "Google Ads Spend", set a 90-day backfill and a daily schedule, and save.

Step 2: GA4 sessions per campaign

  1. + New Table > Google BigQuery, mode Custom SQL.
  2. Use a query like this one, with your own project and dataset:
SELECT
  PARSE_DATE('%Y%m%d', event_date) AS date,
  collected_traffic_source.manual_campaign_name AS name,
  COUNT(DISTINCT CONCAT(user_pseudo_id, CAST(
    (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING))) AS sessions
FROM `your-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', @start_date) AND FORMAT_DATE('%Y%m%d', @end_date)
  AND collected_traffic_source.manual_campaign_name IS NOT NULL
GROUP BY date, name
  1. Set Date Column to date, mark name as a dimension and sessions as a metric.
  2. Name it "GA4 Campaign Sessions", set the same 90-day backfill, check Estimated scan per sync, and save.

Each table now has one row per campaign per day, which is what a join on campaign needs. Both are stored in your property's time zone, so their days line up.

Step 3: Join them

  1. + New Table, under Composite Tables choose Joined. Name it "Campaign Sessions and Cost".
  2. Base Table: "GA4 Campaign Sessions".
  3. + Add Join Table: "Google Ads Spend".
  4. Join Keys: name. Date is always matched; do not add it.
  5. Save. The join builds right away, and rebuilds every time either source syncs.

Every GA4 row is kept. Campaigns with sessions but no Google Ads spend, such as email or social campaigns, show empty cost.

Step 4: Add the metrics

On the joined table, add:

MetricTypeNumeratorDenominatorFormat
cost_per_sessionRatiocostsessionsCurrency
roasRatioconversions_valuecostNumber, 2 decimals

Ratios are worked out after grouping, so a campaign's ROAS over 30 days is its total value divided by its total cost, not an average of daily ROAS.

Step 5: Build the board

Boards > + New Board, "Paid Campaign Performance", then add:

TileTableMetricGroup ByOptions
KPICampaign Sessions and CostcostCompare To: previous period
KPICampaign Sessions and CostroasCompare To: previous period
LineCampaign Sessions and Costcost_per_sessionCompare overlay: previous period
TableCampaign Sessions and Costcost, then sessions, cost_per_session, roasnameSort by cost, descending; show total row

Click Create Board. Rename the name column to "Campaign" in the joined table's Column Labels panel so tiles read well.

Step 6: Share it

Reports > + New Report, tick the board, add the client's logo URL and a password, and click Save Report. Send the link from the Reports list.

Check the numbers

  • Total cost on the board for last month should match Google Ads for the same dates in the account's time zone.
  • Total sessions will be lower than GA4's own campaign report if many sessions have no campaign; this query only counts sessions with a manual campaign name.
  • If cost shows empty for a campaign you know spent money, compare the campaign name in both tables: the match is exact and case-sensitive.