___TERMS_OF_SERVICE___ By creating or modifying this file you agree to Google Tag Manager's Community Template Gallery Developer Terms of Service available at https://developers.google.com/tag-manager/gallery-tos (or such other URL as Google may provide), as modified from time to time. ___INFO___ { "type": "MACRO", "id": "cvt_temp_public_id", "version": 1, "securityGroups": [], "displayName": "AI Assistant Traffic", "categories": [ "ANALYTICS", "ATTRIBUTION", "UTILITY" ], "brand": { "id": "brand_dummy", "displayName": "Metric Owl", "thumbnail": "data:image/png;base64,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" }, "description": "Tells you when a visit came from an AI assistant such as ChatGPT, Perplexity, Gemini, Copilot or Claude, from the referrer or the utm_source the assistant added. Returns the assistant's name, a channel label or true/false, for a GA4 custom dimension or event parameter.", "containerContexts": [ "WEB" ] } ___TEMPLATE_PARAMETERS___ [ { "type": "SELECT", "name": "output", "displayName": "Return", "macrosInSelect": false, "simpleValueType": true, "selectItems": [ { "value": "name", "displayValue": "The assistant's name, such as ChatGPT" }, { "value": "group", "displayValue": "The label \"AI assistant\"" }, { "value": "boolean", "displayValue": "true or false" } ], "defaultValue": "name", "help": "What the variable returns for a visit from an AI assistant." }, { "type": "TEXT", "name": "otherValue", "displayName": "Value for other visits", "simpleValueType": true, "help": "Returned when the visit isn't from an AI assistant. Leave blank to return undefined, so GA4 doesn't send the parameter.", "enablingConditions": [ { "paramName": "output", "paramValue": "boolean", "type": "NOT_EQUALS" } ] }, { "type": "CHECKBOX", "name": "checkUtmSource", "checkboxText": "Also check utm_source on the page address", "simpleValueType": true, "defaultValue": true, "help": "ChatGPT adds utm_source=chatgpt.com to many links it cites, which still works when the app sends no referrer." }, { "type": "TEXT", "name": "extraAssistants", "displayName": "Extra assistants", "simpleValueType": true, "help": "One per line, as hostname=Name, for example chat.example.ai=Example. These are checked before the built-in list.", "lineCount": 4 }, { "type": "GROUP", "name": "advanced", "displayName": "Advanced", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "TEXT", "name": "referrerOverride", "displayName": "Referrer to check", "simpleValueType": true, "help": "Leave blank to use the page's referrer. Set it to another variable to classify a stored or first-touch referrer." }, { "type": "TEXT", "name": "urlOverride", "displayName": "Page address to check", "simpleValueType": true, "help": "Leave blank to use the current page address." } ] } ] ___SANDBOXED_JS_FOR_WEB_TEMPLATE___ const getReferrerUrl = require('getReferrerUrl'); const getUrl = require('getUrl'); const parseUrl = require('parseUrl'); const makeString = require('makeString'); const getType = require('getType'); // The sandbox doesn't guarantee String.trim, so this removes spaces, tabs and carriage returns. const trim = function (value) { const str = makeString(value); let start = 0; let end = str.length; while (start < end && ' \t\r'.indexOf(str.charAt(start)) > -1) start++; while (end > start && ' \t\r'.indexOf(str.charAt(end - 1)) > -1) end--; return str.slice(start, end); }; // Hostname (or the end of one) and the assistant's name. Kept up to date by Metric Owl. const ASSISTANTS = [ ['chatgpt.com', 'ChatGPT'], ['chat.openai.com', 'ChatGPT'], ['perplexity.ai', 'Perplexity'], ['gemini.google.com', 'Gemini'], ['bard.google.com', 'Gemini'], ['copilot.microsoft.com', 'Copilot'], ['copilot.cloud.microsoft', 'Copilot'], ['claude.ai', 'Claude'], ['chat.deepseek.com', 'DeepSeek'], ['chat.mistral.ai', 'Mistral'], ['grok.com', 'Grok'], ['meta.ai', 'Meta AI'], ['you.com', 'You.com'], ['poe.com', 'Poe'], ['phind.com', 'Phind'] ]; // Extra assistants from the settings, one per line as hostname=Name. They're checked first. const list = []; const extra = makeString(data.extraAssistants || '').split('\n'); for (let i = 0; i < extra.length; i++) { const parts = extra[i].split('='); if (parts.length === 2 && trim(parts[0]) && trim(parts[1])) list.push([trim(parts[0]).toLowerCase(), trim(parts[1])]); } for (let i = 0; i < ASSISTANTS.length; i++) list.push(ASSISTANTS[i]); // A full URL or a bare hostname (utm_source=chatgpt.com) becomes a lowercase hostname. const hostOf = function (value) { let v = trim(value || '').toLowerCase(); if (!v) return ''; if (v.indexOf('://') > -1) { const parsed = parseUrl(v); v = parsed ? parsed.hostname : ''; } v = v.split('/')[0].split(':')[0]; if (v.indexOf('www.') === 0) v = v.slice(4); return v; }; const match = function (host) { if (!host) return undefined; for (let i = 0; i < list.length; i++) { const domain = list[i][0]; if (host === domain || host.slice(-(domain.length + 1)) === '.' + domain) return list[i][1]; } return undefined; }; let name = match(hostOf(data.referrerOverride || getReferrerUrl())); // ChatGPT and others add utm_source to the links they cite, which survives when the referrer doesn't. if (!name && data.checkUtmSource) { const page = parseUrl(data.urlOverride || getUrl()); let source = page && page.searchParams ? page.searchParams.utm_source : undefined; if (getType(source) === 'array') source = source[0]; name = match(hostOf(source)); } if (data.output === 'boolean') return !!name; if (name) return data.output === 'group' ? 'AI assistant' : name; return data.otherValue ? data.otherValue : undefined; ___WEB_PERMISSIONS___ [ { "instance": { "key": { "publicId": "get_referrer", "versionId": "1" }, "param": [ { "key": "urlParts", "value": { "type": 1, "string": "any" } }, { "key": "queriesAllowed", "value": { "type": 1, "string": "any" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "get_url", "versionId": "1" }, "param": [ { "key": "urlParts", "value": { "type": 1, "string": "any" } }, { "key": "queriesAllowed", "value": { "type": 1, "string": "any" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true } ] ___TESTS___ scenarios: - name: ChatGPT referrer returns ChatGPT code: |- mock('getReferrerUrl', 'https://chatgpt.com/'); mock('getUrl', 'https://example.co.uk/'); assertThat(runCode({output: 'name', checkUtmSource: true})).isEqualTo('ChatGPT'); - name: Older ChatGPT and www hosts are recognised code: |- mock('getUrl', 'https://example.co.uk/'); mock('getReferrerUrl', 'https://chat.openai.com/c/123'); assertThat(runCode({output: 'name'})).isEqualTo('ChatGPT'); mock('getReferrerUrl', 'https://www.perplexity.ai/search?q=x'); assertThat(runCode({output: 'name'})).isEqualTo('Perplexity'); mock('getReferrerUrl', 'https://gemini.google.com/app'); assertThat(runCode({output: 'name'})).isEqualTo('Gemini'); - name: utm_source is used when there is no referrer code: |- mock('getReferrerUrl', ''); mock('getUrl', 'https://example.co.uk/page?utm_source=chatgpt.com'); assertThat(runCode({output: 'name', checkUtmSource: true})).isEqualTo('ChatGPT'); - name: utm_source is ignored when the setting is off code: |- mock('getReferrerUrl', ''); mock('getUrl', 'https://example.co.uk/page?utm_source=chatgpt.com'); assertThat(runCode({output: 'name', checkUtmSource: false})).isUndefined(); - name: Other referrers return undefined, or the chosen value code: |- mock('getReferrerUrl', 'https://www.google.com/'); mock('getUrl', 'https://example.co.uk/'); assertThat(runCode({output: 'name', checkUtmSource: true})).isUndefined(); assertThat(runCode({output: 'name', otherValue: '(not AI)'})).isEqualTo('(not AI)'); - name: Look-alike domains don't match code: |- mock('getReferrerUrl', 'https://notchatgpt.com/'); mock('getUrl', 'https://example.co.uk/'); assertThat(runCode({output: 'name'})).isUndefined(); mock('getReferrerUrl', 'https://chatgpt.com.example.net/'); assertThat(runCode({output: 'name'})).isUndefined(); mock('getReferrerUrl', 'https://help.openai.com/en/articles/1'); assertThat(runCode({output: 'name'})).isUndefined(); mock('getReferrerUrl', 'https://www.deepseek.com/'); assertThat(runCode({output: 'name'})).isUndefined(); mock('getReferrerUrl', 'https://chat.deepseek.com/a/chat'); assertThat(runCode({output: 'name'})).isEqualTo('DeepSeek'); - name: Group label and true/false output code: |- mock('getReferrerUrl', 'https://claude.ai/chat/1'); mock('getUrl', 'https://example.co.uk/'); assertThat(runCode({output: 'group'})).isEqualTo('AI assistant'); assertThat(runCode({output: 'boolean'})).isTrue(); mock('getReferrerUrl', 'https://bing.com/'); assertThat(runCode({output: 'boolean'})).isFalse(); - name: Extra assistants are checked first code: |- mock('getReferrerUrl', 'https://chat.example.ai/'); mock('getUrl', 'https://example.co.uk/'); assertThat(runCode({output: 'name', extraAssistants: ' chat.example.ai = Example \r\nbroken line'})).isEqualTo('Example'); - name: Referrer override is used instead of the page referrer code: |- mock('getReferrerUrl', 'https://www.google.com/'); mock('getUrl', 'https://example.co.uk/'); assertThat(runCode({output: 'name', referrerOverride: 'https://copilot.microsoft.com/'})).isEqualTo('Copilot'); ___NOTES___ Built and maintained by Marc Alexander, Metric Owl Ltd (https://www.metricowl.co.uk). Licensed under the Apache License 2.0.