Update

Quick win - Run this short script to pick up on location targeting errors

If you've been doing Google Ads for any length of time, you've probably been caught out by incorrectly setting the location targeting -> advanced targeting from "Presence" to "Presence or Interest in".

When I do audits on client accounts, this is one of the most common errors I see and it's easy enough to miss during campaign creation or believing Google's marketing copy that it'll bring in customers from outside your geo targeting who are still qualified.

Instead - what I see is poor quality locations with low to no conversions from less developed countries in Africa and Asia.

So i built a script that you can schedule to run as often as you'd like. Everytime time it runs you get an email and it'll break down how many impressions you got in and outside your targeted locations.

If you see any come through for outside your targeting, it means you need to change your settings.

Anyway - here's the script. All you need to do is add your email address, date range and then authorise and schedule it. You can get more advanced by setting custom date ranges and including/excluding certain labels.

If you need a primer - here's a guide I posted a few months back.

/**
 * Presence-only audit via GAQL.
 * Flags campaigns with impressions where user_location_view.targeting_location = false.
 * Sends an HTML email summary.
 *
 */

const CONFIG = {
  RECIPIENTS: ['Insert Email Address Here'],
  LOOKBACK: 'LAST_30_DAYS', // LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, or 'CUSTOM'
  CUSTOM_START: '2025-09-01', // YYYY-MM-DD when LOOKBACK === 'CUSTOM'
  CUSTOM_END:   '2025-09-30', // YYYY-MM-DD when LOOKBACK === 'CUSTOM'
  INCLUDE_LABELS: [], // [] = include all
  EXCLUDE_LABELS: ['Geo: Exclude From Policy'],
  MIN_OUTSIDE_IMPRESSIONS: 1
};

function main() {
  const started = new Date();
  const {startDate, endDate} = resolveDateRange_(CONFIG.LOOKBACK, CONFIG.CUSTOM_START, CONFIG.CUSTOM_END);

  // Build campaign metadata with labels for filtering
  const campMeta = getCampaignMeta_();

  // GAQL against user_location_view
  // Sum impressions by campaign and targeting_location flag across the date range
  const query = `
    SELECT
      campaign.id,
      campaign.name,
      user_location_view.targeting_location,
      metrics.impressions
    FROM user_location_view
    WHERE segments.date >= '${startDate}'
      AND segments.date <= '${endDate}'
      AND metrics.impressions > 0
  `;

  const rows = AdsApp.search(query); // GAQL in new Scripts
  /** @type {Object.<string, {name:string,total:number,outside:number}>} */
  const agg = {};

  for (const row of rows) {
    const id = String(row.campaign.id);
    const name = row.campaign.name;
    const isTargeting = Boolean(row.userLocationView.targetingLocation);
    const imps = Number(row.metrics.impressions || 0);

    const meta = campMeta[id];
    if (!meta) continue;
    if (shouldSkipByLabels_(meta.labels, CONFIG.INCLUDE_LABELS, CONFIG.EXCLUDE_LABELS)) continue;

    if (!agg[id]) agg[id] = { name, total: 0, outside: 0 };
    agg[id].total += imps;
    if (!isTargeting) agg[id].outside += imps;
  }

  // Prepare flagged table
  const flagged = [];
  let totalImps = 0, totalOutside = 0;
  Object.keys(agg).forEach(id => {
    const {name, total, outside} = agg[id];
    totalImps += total;
    totalOutside += outside;
    if (outside >= CONFIG.MIN_OUTSIDE_IMPRESSIONS) {
      flagged.push({ id, name, total, outside, pct: total ? (outside / total) * 100 : 0 });
    }
  });
  flagged.sort((a, b) => b.pct - a.pct || b.outside - a.outside);

  const html = buildEmailHtml_({
    customerId: AdsApp.currentAccount().getCustomerId(),
    startDate, endDate,
    includeLabels: CONFIG.INCLUDE_LABELS,
    excludeLabels: CONFIG.EXCLUDE_LABELS,
    totals: { totalImps, totalOutside },
    flagged,
    generatedAt: started
  });

  if (CONFIG.RECIPIENTS.length) {
    MailApp.sendEmail({
      to: CONFIG.RECIPIENTS.join(','),
      subject: '[Geo Audit] Campaigns likely NOT using Presence only',
      htmlBody: html
    });
  }
  Logger.log(stripHtml_(html));
}

/* Helpers */

function getCampaignMeta_() {
  const meta = {};
  const it = AdsApp.campaigns()
    .withCondition("Status IN ['ENABLED','PAUSED']")
    .get();
  while (it.hasNext()) {
    const c = it.next();
    const id = String(c.getId());
    const labels = new Set();
    const lits = c.labels().get();
    while (lits.hasNext()) labels.add(lits.next().getName());
    meta[id] = { name: c.getName(), labels };
  }
  return meta;
}

function shouldSkipByLabels_(labelSet, includeLabels, excludeLabels) {
  if (excludeLabels && excludeLabels.length) {
    for (let i = 0; i < excludeLabels.length; i++) {
      if (labelSet.has(excludeLabels[i])) return true;
    }
  }
  if (includeLabels && includeLabels.length) {
    for (let j = 0; j < includeLabels.length; j++) {
      if (labelSet.has(includeLabels[j])) return false;
    }
    return true;
  }
  return false;
}

function resolveDateRange_(mode, customStart, customEnd) {
  if (mode === 'CUSTOM') return { startDate: customStart, endDate: customEnd };
  const today = new Date();
  const end = formatDate_(today);
  const start = new Date(today);
  if (mode === 'LAST_7_DAYS') start.setDate(start.getDate() - 7);
  else if (mode === 'LAST_30_DAYS') start.setDate(start.getDate() - 30);
  else if (mode === 'LAST_90_DAYS') start.setDate(start.getDate() - 90);
  else throw new Error('Unsupported LOOKBACK: ' + mode);
  return { startDate: formatDate_(start), endDate: end };
}

function formatDate_(d) {
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

function buildEmailHtml_(data) {
  const style = `
    <style>
      body{font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111}
      .hdr{font-size:16px;font-weight:600;margin:0 0 6px}
      .meta{color:#555;margin:0 0 12px}
      .pill{display:inline-block;padding:2px 8px;border-radius:12px;background:#eef;border:1px solid #ccd;margin-right:6px;font-size:12px}
      table{border-collapse:collapse;width:100%;margin-top:10px}
      th,td{border:1px solid #ddd;padding:8px;text-align:left}
      th{background:#f7f7f7}
      .right{text-align:right}
      .ok{color:#0b7}
      .bad{color:#b00;font-weight:600}
      .muted{color:#777}
      .small{font-size:12px}
    </style>
  `;

  const hdr = `
    <div class="hdr">Geo Audit: Presence-only compliance</div>
    <div class="meta">
      Account: <strong>${escapeHtml_(data.customerId)}</strong>
      &nbsp;|&nbsp; Range: <strong>${escapeHtml_(data.startDate)} → ${escapeHtml_(data.endDate)}</strong>
      &nbsp;|&nbsp; Generated: ${escapeHtml_(data.generatedAt.toISOString())}
    </div>
    <div class="meta">
      Include labels:
      ${
        data.includeLabels && data.includeLabels.length
          ? data.includeLabels.map(l => `<span class="pill">${escapeHtml_(l)}</span>`).join('')
          : '<span class="muted">ALL</span>'
      }<br/>
      Exclude labels:
      ${
        data.excludeLabels && data.excludeLabels.length
          ? data.excludeLabels.map(l => `<span class="pill">${escapeHtml_(l)}</span>`).join('')
          : '<span class="muted">NONE</span>'
      }
    </div>
  `;

  const totalsRow = `
    <div class="meta">
      Totals in range: Impressions <strong>${fmtInt_(data.totals.totalImps)}</strong>,
      Outside-target impressions <strong>${fmtInt_(data.totals.totalOutside)}</strong>
      ${data.totals.totalImps ? `(${fmtPct_((data.totals.totalOutside / data.totals.totalImps) * 100)})` : ''}
    </div>
  `;

  let table;
  if (!data.flagged.length) {
    table = `<div class="ok">All checked campaigns look compliant. No outside-target impressions at or above threshold.</div>`;
  } else {
    const rows = data.flagged.map(r => `
      <tr>
        <td>${escapeHtml_(r.name)}</td>
        <td>${escapeHtml_(r.id)}</td>
        <td class="right">${fmtInt_(r.total)}</td>
        <td class="right bad">${fmtInt_(r.outside)}</td>
        <td class="right bad">${fmtPct_(r.pct)}</td>
      </tr>`).join('');
    table = `
      <table>
        <thead>
          <tr>
            <th>Campaign</th>
            <th>Campaign ID</th>
            <th class="right">Total Impr.</th>
            <th class="right">Outside-target Impr.</th>
            <th class="right">% Outside</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
      <div class="small muted">
        Outside-target equals impressions where <code>user_location_view.targeting_location = false</code>.
      </div>
    `;
  }

  return `<html><head>${style}</head><body>${hdr}${totalsRow}${table}</body></html>`;
}

function fmtInt_(n){ return (n||0).toLocaleString('en-US'); }
function fmtPct_(p){ return (isFinite(p) ? p : 0).toFixed(2) + '%'; }
function escapeHtml_(s){
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;')
                  .replace(/>/g,'&gt;').replace(/"/g,'&quot;')
                  .replace(/'/g,'&#039;');
}
function stripHtml_(html){ return html.replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim(); }