DEV Community

Cover image for Tracking ChatGPT, Perplexity and Claude Referrals Properly: GA4 Regex, Server Logs and the 'Dark AI' Problem
Furqan Khalid
Furqan Khalid

Posted on

Tracking ChatGPT, Perplexity and Claude Referrals Properly: GA4 Regex, Server Logs and the 'Dark AI' Problem

"How much traffic do we get from ChatGPT?" sounds like a one-line question. In practice, the honest answer is "at least X, and probably more."

AI assistants do send visitors: people click the citations in ChatGPT search, Perplexity, Claude, Gemini and Copilot. But that traffic reaches you in at least four different shapes, and default analytics setups only catch some of them.

This post gives you:

  1. A classifier that recognises AI referrals from the referrer and UTM tags
  2. A GA4 custom channel group regex
  3. Express/Next.js middleware to log AI visits server-side
  4. SQL to report on them, plus a way to estimate the part you can't see

The four shapes of AI traffic

Shape What you see Example
Referrer present document.referrer / Referer header from an AI domain https://www.perplexity.ai/
UTM tagged AI product adds a UTM to outbound links ?utm_source=chatgpt.com
No referrer Desktop apps, mobile apps, copy-pasted links, strict referrer policies Lands as Direct
Inside Google Clicks from AI Overviews / AI Mode Lands as Organic Search (google)

Shapes 1 and 2 are trackable. Shape 3 is the "dark AI" traffic. Shape 4 is AI-driven, but Google reports it as part of normal Search (Search Console's generative AI performance report, rolled out to all sites in 2026, is the place to look for that).

![Four shapes of AI traffic: referrer present, UTM tagged, no referrer and inside Google, with how trackable each one is]

Image: diagram by the author.

1. A classifier you can reuse everywhere

Keep the rules in one module so the browser, the server and your SQL all agree.

// ai-referrers.js
export const AI_SOURCES = [
  { name: "ChatGPT",    hosts: ["chatgpt.com", "chat.openai.com"],          utm: ["chatgpt.com", "chatgpt"] },
  { name: "Perplexity", hosts: ["perplexity.ai", "www.perplexity.ai"],     utm: ["perplexity", "perplexity.ai"] },
  { name: "Claude",     hosts: ["claude.ai"],                              utm: ["claude.ai", "claude"] },
  { name: "Gemini",     hosts: ["gemini.google.com", "bard.google.com"],   utm: ["gemini", "gemini.google.com"] },
  { name: "Copilot",    hosts: ["copilot.microsoft.com", "copilot.cloud.microsoft"], utm: ["copilot"] },
  { name: "DeepSeek",   hosts: ["chat.deepseek.com"],                      utm: ["deepseek"] },
  { name: "Meta AI",    hosts: ["meta.ai", "www.meta.ai"],                 utm: ["meta.ai"] },
  { name: "Grok",       hosts: ["grok.com"],                               utm: ["grok"] },
];

export function classifyAI({ referrer = "", url = "" }) {
  let refHost = "";
  try { refHost = new URL(referrer).hostname.toLowerCase(); } catch {}

  let utmSource = "";
  try { utmSource = (new URL(url).searchParams.get("utm_source") || "").toLowerCase(); } catch {}

  for (const src of AI_SOURCES) {
    if (src.hosts.some(h => refHost === h || refHost.endsWith("." + h))) {
      return { source: src.name, method: "referrer" };
    }
    if (src.utm.includes(utmSource)) {
      return { source: src.name, method: "utm" };
    }
  }
  return null;
}
Enter fullscreen mode Exit fullscreen mode
classifyAI({ referrer: "https://www.perplexity.ai/", url: "https://example.com/docs" });
// → { source: "Perplexity", method: "referrer" }

classifyAI({ referrer: "", url: "https://example.com/pricing?utm_source=chatgpt.com" });
// → { source: "ChatGPT", method: "utm" }
Enter fullscreen mode Exit fullscreen mode

Domains change. Gemini used to be Bard, Copilot has moved hosts more than once, and new assistants keep launching. Review this list every quarter and treat it as config, not code.

2. GA4: a custom channel group

Some 2026 reports say GA4 added a built-in AI assistant channel. I couldn't find a Google document listing exactly which sources it covers, so I still recommend your own channel group, where you control the rules.

Admin → Data display → Channel groups → Create new channel group → Add new channel named AI Assistants, with the condition:

  • Session source · matches regex ·
^(.*\.)?(chatgpt\.com|chat\.openai\.com|perplexity\.ai|claude\.ai|gemini\.google\.com|bard\.google\.com|copilot\.microsoft\.com|copilot\.cloud\.microsoft|chat\.deepseek\.com|meta\.ai|grok\.com)$|^(chatgpt|perplexity|claude|gemini|copilot|deepseek|grok)$
Enter fullscreen mode Exit fullscreen mode

Two settings people get wrong:

  1. Don't add a medium = referral condition. UTM-tagged ChatGPT visits often arrive as chatgpt.com / (none) or (not set). Restricting the medium drops them.
  2. Move the channel above "Referral" and "Organic Search". GA4 evaluates channels top to bottom and the first match wins.

Channel groups apply to historical data in reports, which is handy for checking how this year compares with last.

3. Log AI visits server-side

GA4 misses visitors who block analytics, and you can't easily join it with your own data (signups, orders). A tiny middleware fixes both.

Express

// ai-log.js
import { classifyAI } from "./ai-referrers.js";

export function aiReferralLogger(db) {
  return (req, res, next) => {
    if (req.method !== "GET" || !req.accepts("html")) return next();

    const hit = classifyAI({
      referrer: req.get("referer") || "",
      url: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
    });

    if (hit) {
      db.query(
        `INSERT INTO ai_visits (ts, source, method, path, user_agent)
         VALUES (now(), $1, $2, $3, $4)`,
        [hit.source, hit.method, req.path, req.get("user-agent") || ""]
      ).catch(() => {}); // never block the request on analytics
    }
    next();
  };
}
Enter fullscreen mode Exit fullscreen mode

Next.js (App Router)

Middleware can't easily write to a database at the edge, so forward the hit to a route handler or your log pipeline:

// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { classifyAI } from "./lib/ai-referrers";

export function middleware(req: NextRequest) {
  const hit = classifyAI({ referrer: req.headers.get("referer") ?? "", url: req.url });
  if (hit) {
    // Structured log line; ship it with your log drain.
    console.log(JSON.stringify({ type: "ai_visit", ...hit, path: req.nextUrl.pathname, ts: Date.now() }));
  }
  return NextResponse.next();
}

export const config = { matcher: ["/((?!_next|api|.*\\..*).*)"] };
Enter fullscreen mode Exit fullscreen mode

Note the referrer only shows the origin (https://chatgpt.com/), not the conversation, and that's how it should be. You'll never know which prompt sent the visit, so don't try to fingerprint it.

4. Report on it

-- Weekly AI visits by source
SELECT date_trunc('week', ts) AS week, source, count(*) AS visits
FROM ai_visits
GROUP BY 1, 2
ORDER BY 1 DESC, 3 DESC;

-- Which pages do AI assistants send people to?
SELECT path, source, count(*) AS visits
FROM ai_visits
WHERE ts > now() - interval '30 days'
GROUP BY 1, 2
ORDER BY 3 DESC
LIMIT 25;
Enter fullscreen mode Exit fullscreen mode

The second query is the useful one. AI assistants tend to send people to a different set of pages from Google: deep docs pages, comparison pages and pricing are common. That tells you which content is getting cited.

5. Estimating the dark part

You can't classify visits with no referrer, but you can spot when Direct traffic behaves like AI traffic:

  • Deep-page Direct landings. Real "direct" visits (typed URL, bookmark) mostly land on the homepage. A spike in Direct visits to /docs/configuring-webhooks usually means someone shared or cited the link, and AI apps are a common source.
  • Correlate with known AI visits. If a page's referrer-tagged ChatGPT visits double and its deep-page Direct visits rise in the same week, some of that Direct is probably AI too.
  • Ask. A "How did you hear about us?" field on signup with an "AI assistant (ChatGPT, Claude…)" option is low-tech and surprisingly informative.
-- Deep-page "direct" sessions: a rough proxy for unattributed shares/citations
SELECT landing_path, count(*) AS sessions
FROM sessions
WHERE channel = 'Direct'
  AND landing_path NOT IN ('/', '/login', '/app')
  AND ts > now() - interval '30 days'
GROUP BY 1
ORDER BY 2 DESC
LIMIT 25;
Enter fullscreen mode Exit fullscreen mode

Report AI traffic as "tracked AI visits (floor)" and leave it at that. Inventing a multiplier for the dark part won't survive the first question in a meeting.

Traffic is the lagging indicator

Referral tracking tells you what happened after an assistant cited you. It doesn't tell you how often you were mentioned without a click, or how often a competitor was recommended instead. Those are the leading indicators. We track them in Vista AI's brand visibility dashboard, which samples real prompts across the major assistants. Whatever you use, put both views next to each other: visibility going up while referrals stay flat usually means you're being mentioned but not linked.


Did I miss an AI source that sends you traffic? Post the referrer host in the comments and I'll add it to the list.

Next in the series: ChatGPT can't cite what Bing hasn't indexed: IndexNow in Next.js.

Top comments (2)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

the "tracked ai visits (floor)" advice is the best part. every team i've watched would rather report a confident wrong number than an honest floor, then quietly optimise against it. reporting the floor and refusing to multiply is the whole discipline.

Collapse
 
hanin_mp profile image
Hanin Mp •

The floor framing is the right call. One addition for the server-side part: your classifier tags human visits that arrive from an AI app, but the same logs also contain the assistants' own fetches (OAI-SearchBot, PerplexityBot and similar). Those deserve a separate table, because a user agent string proves nothing: anyone can send it. Most major vendors publish IP ranges for their documented bots, so matching IPs against those lists lets you split verified fetches from spoofed ones before you count them. Otherwise "AI crawler activity" can quietly include scrapers.