@shware/analytics 8.12.0 → 8.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attribution/classify.cjs +35 -26
- package/dist/attribution/classify.cjs.map +1 -1
- package/dist/attribution/classify.d.cts +9 -2
- package/dist/attribution/classify.d.cts.map +1 -1
- package/dist/attribution/classify.d.mts +9 -2
- package/dist/attribution/classify.d.mts.map +1 -1
- package/dist/attribution/classify.mjs +36 -27
- package/dist/attribution/classify.mjs.map +1 -1
- package/dist/attribution/index.cjs +2 -0
- package/dist/attribution/index.d.cts +2 -2
- package/dist/attribution/index.d.mts +2 -2
- package/dist/attribution/index.mjs +2 -2
- package/dist/attribution/vocabulary.cjs +207 -34
- package/dist/attribution/vocabulary.cjs.map +1 -1
- package/dist/attribution/vocabulary.d.cts +60 -15
- package/dist/attribution/vocabulary.d.cts.map +1 -1
- package/dist/attribution/vocabulary.d.mts +60 -15
- package/dist/attribution/vocabulary.d.mts.map +1 -1
- package/dist/attribution/vocabulary.mjs +206 -35
- package/dist/attribution/vocabulary.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_attribution_vocabulary = require("./vocabulary.cjs");
|
|
3
3
|
//#region src/attribution/classify.ts
|
|
4
|
-
const
|
|
5
|
-
const paidMedium = new RegExp(require_attribution_vocabulary.PAID_MEDIUM);
|
|
4
|
+
const metaPlacements = new Set(require_attribution_vocabulary.META_PLACEMENTS);
|
|
6
5
|
const referrerHostOf = /^https?:\/\/([^/:?#]+)/i;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
const channelsBy = (medium) => new Set(require_attribution_vocabulary.REFERRER_SITES.filter(([, m]) => m === medium).map(([channel]) => channel));
|
|
7
|
+
const searchChannels = channelsBy("organic");
|
|
8
|
+
const socialChannels = channelsBy("social");
|
|
9
|
+
const videoChannels = channelsBy("video");
|
|
10
|
+
const aiChannels = channelsBy("ai");
|
|
11
|
+
/**
|
|
12
|
+
* A tag as text; absent when missing, empty, or the strings a broken template writes
|
|
13
|
+
* (`undefined`, `null`, an unexpanded `{{macro}}` other than Meta's placement, read elsewhere).
|
|
14
|
+
*/
|
|
16
15
|
function text(tags, key) {
|
|
17
16
|
const value = tags[key];
|
|
18
|
-
if (value === void 0 || value === null
|
|
19
|
-
|
|
17
|
+
if (value === void 0 || value === null) return null;
|
|
18
|
+
const s = String(value);
|
|
19
|
+
return s === "" || s === "undefined" || s === "null" ? null : s;
|
|
20
20
|
}
|
|
21
21
|
/** 1. What the campaign said: `utm_source`, with the aliases folded. */
|
|
22
22
|
function utmChannel(tags) {
|
|
23
|
-
const source = text(tags, "utm_source")?.toLowerCase();
|
|
23
|
+
const source = text(tags, "utm_source")?.toLowerCase().split(/[?&#]/)[0]?.replace(/\/+$/, "");
|
|
24
24
|
if (!source) return null;
|
|
25
25
|
return require_attribution_vocabulary.SOURCE_ALIASES[source] ?? source;
|
|
26
26
|
}
|
|
@@ -34,7 +34,7 @@ function clickChannel(tags) {
|
|
|
34
34
|
/** 3. A landing page reserved for one channel's ads. */
|
|
35
35
|
function landingChannel(tags) {
|
|
36
36
|
const location = text(tags, "page_location");
|
|
37
|
-
return location ?
|
|
37
|
+
return location ? require_attribution_vocabulary.AD_LANDING_PAGE.exec(location)?.[1] ?? null : null;
|
|
38
38
|
}
|
|
39
39
|
/** 4. The product's rules, in order. */
|
|
40
40
|
function productTouch(tags, rules) {
|
|
@@ -52,9 +52,9 @@ function referrer(tags, ownHosts) {
|
|
|
52
52
|
const url = text(tags, "page_referrer");
|
|
53
53
|
const host = url ? referrerHostOf.exec(url)?.[1]?.toLowerCase() : void 0;
|
|
54
54
|
if (!host) return null;
|
|
55
|
-
if (
|
|
56
|
-
if (ownHosts.some((pattern) =>
|
|
57
|
-
const site =
|
|
55
|
+
if (require_attribution_vocabulary.REFERRERS_NOT_A_TOUCH.some((pattern) => pattern.test(host))) return null;
|
|
56
|
+
if (ownHosts.some((pattern) => pattern.test(host))) return null;
|
|
57
|
+
const site = require_attribution_vocabulary.REFERRER_SITES.find(([, , pattern]) => pattern.test(host));
|
|
58
58
|
return site ? {
|
|
59
59
|
channel: site[0],
|
|
60
60
|
medium: site[1]
|
|
@@ -63,21 +63,30 @@ function referrer(tags, ownHosts) {
|
|
|
63
63
|
medium: "referral"
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* GA4's default channel group of a (channel, medium) pair, with GA4's rule that the source
|
|
68
|
+
* decides too: `linkedin / (not set)` is Organic Social and `email / promo` is Email, because
|
|
69
|
+
* GA4 matches its site lists on the source, not only the medium. Two additions to GA4: Meta's
|
|
70
|
+
* placement names as a medium are a paid Meta click (see `META_PLACEMENTS`), and the AI
|
|
71
|
+
* assistants have `organic_ai`. Checked in GA4's order — paid before organic, the organic groups before
|
|
72
|
+
* email, referral last — so a pair that fits two rules lands where GA4 would put it.
|
|
73
|
+
*/
|
|
67
74
|
function channelGroupOf(channel, medium) {
|
|
68
75
|
if (channel === "(direct)") return "direct";
|
|
69
76
|
if (require_attribution_vocabulary.DISPLAY_MEDIUMS.includes(medium)) return "display";
|
|
70
|
-
if (
|
|
71
|
-
|
|
77
|
+
if (metaPlacements.has(medium)) return "paid_social";
|
|
78
|
+
if (require_attribution_vocabulary.PAID_MEDIUM.test(medium)) {
|
|
79
|
+
if (socialChannels.has(channel) || videoChannels.has(channel)) return "paid_social";
|
|
72
80
|
if (searchChannels.has(channel)) return "paid_search";
|
|
73
81
|
return "paid_other";
|
|
74
82
|
}
|
|
75
|
-
if (medium === "
|
|
76
|
-
if (medium === "
|
|
77
|
-
if (medium === "
|
|
78
|
-
if (medium === "
|
|
79
|
-
if (require_attribution_vocabulary.EMAIL_MEDIUMS.includes(
|
|
83
|
+
if (medium === "social" || socialChannels.has(channel)) return "organic_social";
|
|
84
|
+
if (medium === "video" || videoChannels.has(channel)) return "organic_video";
|
|
85
|
+
if (medium === "ai" || aiChannels.has(channel)) return "organic_ai";
|
|
86
|
+
if (medium === "organic" || searchChannels.has(channel)) return "organic_search";
|
|
87
|
+
if (require_attribution_vocabulary.EMAIL_MEDIUM.test(medium) || require_attribution_vocabulary.EMAIL_MEDIUMS.includes(channel)) return "email";
|
|
80
88
|
if (medium === "affiliate") return "affiliate";
|
|
89
|
+
if (medium === "referral") return "referral";
|
|
81
90
|
return "unassigned";
|
|
82
91
|
}
|
|
83
92
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.cjs","names":["AD_LANDING_PAGE","PAID_MEDIUM","REFERRERS_NOT_A_TOUCH","REFERRER_SITES","SOURCE_ALIASES","CLICK_ID_CHANNELS","DISPLAY_MEDIUMS","EMAIL_MEDIUMS","TOUCH_PRIORITY"],"sources":["../../src/attribution/classify.ts"],"sourcesContent":["import type { TrackTags } from '../track/types';\nimport {\n AD_LANDING_PAGE,\n CLICK_ID_CHANNELS,\n type ChannelGroup,\n DIRECT_CHANNEL,\n DISPLAY_MEDIUMS,\n EMAIL_MEDIUMS,\n MEDIUM_NOT_SET,\n NO_MEDIUM,\n PAID_MEDIUM,\n REFERRERS_NOT_A_TOUCH,\n REFERRER_SITES,\n SOURCE_ALIASES,\n TOUCH_PRIORITY,\n} from './vocabulary';\n\n/** What a session's tags say about how it arrived: the touch, as the session row stores it. */\nexport interface Touch {\n /** The source, as GA calls it: a `Channel`, a `utm_source` outside the list, a referring host, or `(direct)`. */\n channel: string;\n /** How, as GA calls it: `utm_medium` as declared, else what the rule that named the channel implies; `(none)` for direct. */\n medium: string;\n /** GA4's default channel group of the two. */\n channel_group: ChannelGroup;\n /** `utm_campaign`, else what a product rule captured (a referral code); null when neither. */\n campaign: string | null;\n /** `TOUCH_PRIORITY` of the rule that named the channel; null for direct, which is no touch. */\n priority: number | null;\n}\n\n/** What a product rule names, when it recognises the session; the channel group is derived. */\nexport type ProductTouch = Omit<Touch, 'channel_group'>;\n\n/**\n * A product's own rule: a landing page convention, a code in the URL. Tried after the campaign\n * rules (utm, click id, ad landing page) and before the referrer, so a link shared on Facebook\n * or WhatsApp is credited to what the product recognises, not to the site it was shared on.\n * Returns null when the session is not one of its.\n */\nexport type TouchRule = (tags: TrackTags) => ProductTouch | null;\n\nexport interface ClassifyOptions {\n /**\n * The product's own hosts, as patterns over the lower-cased referrer host: a session that\n * starts from an internal link after the session timeout refers to them, which is navigation,\n * not acquisition. The SDK adds the payment and sign-in providers (`REFERRERS_NOT_A_TOUCH`).\n */\n ownHosts?: readonly (RegExp | string)[];\n /** The product's rules, in order; the first that names the session wins. */\n rules?: readonly TouchRule[];\n}\n\nconst adLandingPage = new RegExp(AD_LANDING_PAGE);\nconst paidMedium = new RegExp(PAID_MEDIUM);\nconst referrerHostOf = /^https?:\\/\\/([^/:?#]+)/i;\nconst notATouch = REFERRERS_NOT_A_TOUCH.map((pattern) => new RegExp(pattern));\nconst referrerSites = REFERRER_SITES.map(\n ([channel, medium, pattern]) => [channel, medium, new RegExp(pattern)] as const\n);\nconst searchChannels = new Set<string>(\n REFERRER_SITES.filter(([, medium]) => medium === 'organic').map(([channel]) => channel)\n);\nconst socialChannels = new Set<string>(\n REFERRER_SITES.filter(([, medium]) => medium !== 'organic').map(([channel]) => channel)\n);\n\n/** A tag as text, lower-cased; absent when missing or empty. */\nfunction text(tags: TrackTags, key: string): string | null {\n const value = tags[key];\n if (value === undefined || value === null || value === '') return null;\n return String(value);\n}\n\n/** 1. What the campaign said: `utm_source`, with the aliases folded. */\nfunction utmChannel(tags: TrackTags): string | null {\n const source = text(tags, 'utm_source')?.toLowerCase();\n if (!source) return null;\n return (SOURCE_ALIASES as Record<string, string>)[source] ?? source;\n}\n\n/**\n * 2. A click id, possibly carried by a first-party cookie from an earlier click. Presence of the\n * key is the signal, as it is for the pixel that reads it.\n */\nfunction clickChannel(tags: TrackTags): string | null {\n return CLICK_ID_CHANNELS.find(([key]) => key in tags)?.[1] ?? null;\n}\n\n/** 3. A landing page reserved for one channel's ads. */\nfunction landingChannel(tags: TrackTags): string | null {\n const location = text(tags, 'page_location');\n return location ? (adLandingPage.exec(location)?.[1] ?? null) : null;\n}\n\n/** 4. The product's rules, in order. */\nfunction productTouch(tags: TrackTags, rules: readonly TouchRule[]): ProductTouch | null {\n for (const rule of rules) {\n const touch = rule(tags);\n if (touch) return touch;\n }\n return null;\n}\n\n/**\n * 5. The referrer's host: a known search engine or social network folded to its channel, any\n * other site kept as its host, and null for no referrer or one that is no touch.\n */\nfunction referrer(\n tags: TrackTags,\n ownHosts: readonly (RegExp | string)[]\n): { channel: string; medium: string } | null {\n const url = text(tags, 'page_referrer');\n const host = url ? referrerHostOf.exec(url)?.[1]?.toLowerCase() : undefined;\n if (!host) return null;\n if (notATouch.some((pattern) => pattern.test(host))) return null;\n if (ownHosts.some((pattern) => new RegExp(pattern).test(host))) return null;\n const site = referrerSites.find(([, , pattern]) => pattern.test(host));\n return site ? { channel: site[0], medium: site[1] } : { channel: host, medium: 'referral' };\n}\n\n/** GA4's default channel group of a (channel, medium) pair. */\nexport function channelGroupOf(channel: string, medium: string): ChannelGroup {\n if (channel === DIRECT_CHANNEL) return 'direct';\n if ((DISPLAY_MEDIUMS as readonly string[]).includes(medium)) return 'display';\n if (paidMedium.test(medium)) {\n if (socialChannels.has(channel)) return 'paid_social';\n if (searchChannels.has(channel)) return 'paid_search';\n return 'paid_other';\n }\n if (medium === 'organic') return 'organic_search';\n if (medium === 'social') return 'organic_social';\n if (medium === 'video') return 'organic_video';\n if (medium === 'referral') return 'referral';\n if ((EMAIL_MEDIUMS as readonly string[]).includes(medium)) return 'email';\n if (medium === 'affiliate') return 'affiliate';\n return 'unassigned';\n}\n\n/**\n * Reads the tags a session arrived with (its `session_start` tags) as one touch. Run once, when\n * the session is written; the result is stored, and the views only read it.\n *\n * The rules, in order, the first that says something naming the channel: an explicit\n * `utm_source`, a click id, an ad landing page, the product's own rules, the referrer's host;\n * `(direct)` when none does. `medium` is `utm_medium` as declared (lower-cased), else what that\n * rule implies: `(not set)` for a utm_source alone, `cpc` for a bare click id or ad landing page,\n * the product rule's own, `organic` / `social` / `video` / `referral` for a referrer, `(none)`\n * for direct. `campaign` is `utm_campaign`, else what a product rule captured — even when the\n * utm named the channel, so a tagged referral link keeps its code. `priority` is the tier of\n * the rule that named the channel (`TOUCH_PRIORITY`): campaign touches, then the product's own\n * as they declare, then the referrer.\n */\nexport function classifyTouch(tags: TrackTags, options: ClassifyOptions = {}): Touch {\n const utm = utmChannel(tags);\n const click = clickChannel(tags);\n const landing = landingChannel(tags);\n const product = productTouch(tags, options.rules ?? []);\n const referred = referrer(tags, options.ownHosts ?? []);\n\n const channel =\n utm ?? click ?? landing ?? product?.channel ?? referred?.channel ?? DIRECT_CHANNEL;\n const declaredMedium = text(tags, 'utm_medium')?.toLowerCase() ?? null;\n const medium =\n declaredMedium ??\n (utm\n ? MEDIUM_NOT_SET\n : click || landing\n ? 'cpc'\n : (product?.medium ?? referred?.medium ?? NO_MEDIUM));\n const campaign = text(tags, 'utm_campaign') ?? product?.campaign ?? null;\n const priority =\n utm || click || landing\n ? TOUCH_PRIORITY.campaign\n : product\n ? product.priority\n : referred\n ? TOUCH_PRIORITY.referrer\n : null;\n\n return { channel, medium, channel_group: channelGroupOf(channel, medium), campaign, priority };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"classify.cjs","names":["META_PLACEMENTS","REFERRER_SITES","SOURCE_ALIASES","CLICK_ID_CHANNELS","AD_LANDING_PAGE","REFERRERS_NOT_A_TOUCH","DISPLAY_MEDIUMS","PAID_MEDIUM","EMAIL_MEDIUM","EMAIL_MEDIUMS","TOUCH_PRIORITY"],"sources":["../../src/attribution/classify.ts"],"sourcesContent":["import type { TrackTags } from '../track/types';\nimport {\n AD_LANDING_PAGE,\n CLICK_ID_CHANNELS,\n type ChannelGroup,\n DIRECT_CHANNEL,\n DISPLAY_MEDIUMS,\n EMAIL_MEDIUM,\n EMAIL_MEDIUMS,\n MEDIUM_NOT_SET,\n META_PLACEMENTS,\n NO_MEDIUM,\n PAID_MEDIUM,\n REFERRERS_NOT_A_TOUCH,\n REFERRER_SITES,\n SOURCE_ALIASES,\n TOUCH_PRIORITY,\n} from './vocabulary';\n\n/** What a session's tags say about how it arrived: the touch, as the session row stores it. */\nexport interface Touch {\n /** The source, as GA calls it: a `Channel`, a `utm_source` outside the list, a referring host, or `(direct)`. */\n channel: string;\n /** How, as GA calls it: `utm_medium` as declared, else what the rule that named the channel implies; `(none)` for direct. */\n medium: string;\n /** GA4's default channel group of the two. */\n channel_group: ChannelGroup;\n /** `utm_campaign`, else what a product rule captured (a referral code); null when neither. */\n campaign: string | null;\n /** `TOUCH_PRIORITY` of the rule that named the channel; null for direct, which is no touch. */\n priority: number | null;\n}\n\n/** What a product rule names, when it recognises the session; the channel group is derived. */\nexport type ProductTouch = Omit<Touch, 'channel_group'>;\n\n/**\n * A product's own rule: a landing page convention, a code in the URL. Tried after the campaign\n * rules (utm, click id, ad landing page) and before the referrer, so a link shared on Facebook\n * or WhatsApp is credited to what the product recognises, not to the site it was shared on.\n * Returns null when the session is not one of its.\n */\nexport type TouchRule = (tags: TrackTags) => ProductTouch | null;\n\nexport interface ClassifyOptions {\n /**\n * The product's own hosts, as patterns over the lower-cased referrer host: a session that\n * starts from an internal link after the session timeout refers to them, which is navigation,\n * not acquisition. The SDK adds the payment and sign-in providers (`REFERRERS_NOT_A_TOUCH`).\n */\n ownHosts?: readonly RegExp[];\n /** The product's rules, in order; the first that names the session wins. */\n rules?: readonly TouchRule[];\n}\n\nconst metaPlacements = new Set<string>(META_PLACEMENTS);\nconst referrerHostOf = /^https?:\\/\\/([^/:?#]+)/i;\nconst channelsBy = (medium: string) =>\n new Set<string>(REFERRER_SITES.filter(([, m]) => m === medium).map(([channel]) => channel));\nconst searchChannels = channelsBy('organic');\nconst socialChannels = channelsBy('social');\nconst videoChannels = channelsBy('video');\nconst aiChannels = channelsBy('ai');\n\n/**\n * A tag as text; absent when missing, empty, or the strings a broken template writes\n * (`undefined`, `null`, an unexpanded `{{macro}}` other than Meta's placement, read elsewhere).\n */\nfunction text(tags: TrackTags, key: string): string | null {\n const value = tags[key];\n if (value === undefined || value === null) return null;\n const s = String(value);\n return s === '' || s === 'undefined' || s === 'null' ? null : s;\n}\n\n/** 1. What the campaign said: `utm_source`, with the aliases folded. */\nfunction utmChannel(tags: TrackTags): string | null {\n // A source with the rest of the query glued on (`email&utm_medium=promo`, `toolify/`,\n // `x?utm_source=x`): a link built by hand or double-encoded. Keep the source.\n const source = text(tags, 'utm_source')?.toLowerCase().split(/[?&#]/)[0]?.replace(/\\/+$/, '');\n if (!source) return null;\n return (SOURCE_ALIASES as Record<string, string>)[source] ?? source;\n}\n\n/**\n * 2. A click id, possibly carried by a first-party cookie from an earlier click. Presence of the\n * key is the signal, as it is for the pixel that reads it.\n */\nfunction clickChannel(tags: TrackTags): string | null {\n return CLICK_ID_CHANNELS.find(([key]) => key in tags)?.[1] ?? null;\n}\n\n/** 3. A landing page reserved for one channel's ads. */\nfunction landingChannel(tags: TrackTags): string | null {\n const location = text(tags, 'page_location');\n return location ? (AD_LANDING_PAGE.exec(location)?.[1] ?? null) : null;\n}\n\n/** 4. The product's rules, in order. */\nfunction productTouch(tags: TrackTags, rules: readonly TouchRule[]): ProductTouch | null {\n for (const rule of rules) {\n const touch = rule(tags);\n if (touch) return touch;\n }\n return null;\n}\n\n/**\n * 5. The referrer's host: a known search engine or social network folded to its channel, any\n * other site kept as its host, and null for no referrer or one that is no touch.\n */\nfunction referrer(\n tags: TrackTags,\n ownHosts: readonly RegExp[]\n): { channel: string; medium: string } | null {\n const url = text(tags, 'page_referrer');\n const host = url ? referrerHostOf.exec(url)?.[1]?.toLowerCase() : undefined;\n if (!host) return null;\n if (REFERRERS_NOT_A_TOUCH.some((pattern) => pattern.test(host))) return null;\n if (ownHosts.some((pattern) => pattern.test(host))) return null;\n const site = REFERRER_SITES.find(([, , pattern]) => pattern.test(host));\n return site ? { channel: site[0], medium: site[1] } : { channel: host, medium: 'referral' };\n}\n\n/**\n * GA4's default channel group of a (channel, medium) pair, with GA4's rule that the source\n * decides too: `linkedin / (not set)` is Organic Social and `email / promo` is Email, because\n * GA4 matches its site lists on the source, not only the medium. Two additions to GA4: Meta's\n * placement names as a medium are a paid Meta click (see `META_PLACEMENTS`), and the AI\n * assistants have `organic_ai`. Checked in GA4's order — paid before organic, the organic groups before\n * email, referral last — so a pair that fits two rules lands where GA4 would put it.\n */\nexport function channelGroupOf(channel: string, medium: string): ChannelGroup {\n if (channel === DIRECT_CHANNEL) return 'direct';\n if ((DISPLAY_MEDIUMS as readonly string[]).includes(medium)) return 'display';\n // A Meta placement as the medium is a Meta ad whatever the source was tagged as.\n if (metaPlacements.has(medium)) return 'paid_social';\n if (PAID_MEDIUM.test(medium)) {\n if (socialChannels.has(channel) || videoChannels.has(channel)) return 'paid_social';\n if (searchChannels.has(channel)) return 'paid_search';\n return 'paid_other';\n }\n if (medium === 'social' || socialChannels.has(channel)) return 'organic_social';\n if (medium === 'video' || videoChannels.has(channel)) return 'organic_video';\n if (medium === 'ai' || aiChannels.has(channel)) return 'organic_ai';\n if (medium === 'organic' || searchChannels.has(channel)) return 'organic_search';\n if (EMAIL_MEDIUM.test(medium) || (EMAIL_MEDIUMS as readonly string[]).includes(channel))\n return 'email';\n if (medium === 'affiliate') return 'affiliate';\n if (medium === 'referral') return 'referral';\n return 'unassigned';\n}\n\n/**\n * Reads the tags a session arrived with (its `session_start` tags) as one touch. Run once, when\n * the session is written; the result is stored, and the views only read it.\n *\n * The rules, in order, the first that says something naming the channel: an explicit\n * `utm_source`, a click id, an ad landing page, the product's own rules, the referrer's host;\n * `(direct)` when none does. `medium` is `utm_medium` as declared (lower-cased), else what that\n * rule implies: `(not set)` for a utm_source alone, `cpc` for a bare click id or ad landing page,\n * the product rule's own, `organic` / `social` / `video` / `referral` for a referrer, `(none)`\n * for direct. `campaign` is `utm_campaign`, else what a product rule captured — even when the\n * utm named the channel, so a tagged referral link keeps its code. `priority` is the tier of\n * the rule that named the channel (`TOUCH_PRIORITY`): campaign touches, then the product's own\n * as they declare, then the referrer.\n */\nexport function classifyTouch(tags: TrackTags, options: ClassifyOptions = {}): Touch {\n const utm = utmChannel(tags);\n const click = clickChannel(tags);\n const landing = landingChannel(tags);\n const product = productTouch(tags, options.rules ?? []);\n const referred = referrer(tags, options.ownHosts ?? []);\n\n const channel =\n utm ?? click ?? landing ?? product?.channel ?? referred?.channel ?? DIRECT_CHANNEL;\n const declaredMedium = text(tags, 'utm_medium')?.toLowerCase() ?? null;\n const medium =\n declaredMedium ??\n (utm\n ? MEDIUM_NOT_SET\n : click || landing\n ? 'cpc'\n : (product?.medium ?? referred?.medium ?? NO_MEDIUM));\n const campaign = text(tags, 'utm_campaign') ?? product?.campaign ?? null;\n const priority =\n utm || click || landing\n ? TOUCH_PRIORITY.campaign\n : product\n ? product.priority\n : referred\n ? TOUCH_PRIORITY.referrer\n : null;\n\n return { channel, medium, channel_group: channelGroupOf(channel, medium), campaign, priority };\n}\n"],"mappings":"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"}
|
|
@@ -29,11 +29,18 @@ interface ClassifyOptions {
|
|
|
29
29
|
* starts from an internal link after the session timeout refers to them, which is navigation,
|
|
30
30
|
* not acquisition. The SDK adds the payment and sign-in providers (`REFERRERS_NOT_A_TOUCH`).
|
|
31
31
|
*/
|
|
32
|
-
ownHosts?: readonly
|
|
32
|
+
ownHosts?: readonly RegExp[];
|
|
33
33
|
/** The product's rules, in order; the first that names the session wins. */
|
|
34
34
|
rules?: readonly TouchRule[];
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* GA4's default channel group of a (channel, medium) pair, with GA4's rule that the source
|
|
38
|
+
* decides too: `linkedin / (not set)` is Organic Social and `email / promo` is Email, because
|
|
39
|
+
* GA4 matches its site lists on the source, not only the medium. Two additions to GA4: Meta's
|
|
40
|
+
* placement names as a medium are a paid Meta click (see `META_PLACEMENTS`), and the AI
|
|
41
|
+
* assistants have `organic_ai`. Checked in GA4's order — paid before organic, the organic groups before
|
|
42
|
+
* email, referral last — so a pair that fits two rules lands where GA4 would put it.
|
|
43
|
+
*/
|
|
37
44
|
declare function channelGroupOf(channel: string, medium: string): ChannelGroup;
|
|
38
45
|
/**
|
|
39
46
|
* Reads the tags a session arrived with (its `session_start` tags) as one touch. Run once, when
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.cts","names":[],"sources":["../../src/attribution/classify.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"classify.d.cts","names":[],"sources":["../../src/attribution/classify.ts"],"mappings":";;;;UAoBiB;;EAEf;;EAEA;;EAEA,eAAe;;EAEf;;EAEA;;;KAIU,eAAe,KAAK;;;;;;;KAQpB,aAAa,MAAM,cAAc;UAE5B;;;;;;EAMf,oBAAoB;;EAEpB,iBAAiB;;;;;;;;;;iBAgFH,eAAe,iBAAiB,iBAAiB;;;;;;;;;;;;;;;iBAmCjD,cAAc,MAAM,WAAW,UAAS,kBAAuB"}
|
|
@@ -29,11 +29,18 @@ interface ClassifyOptions {
|
|
|
29
29
|
* starts from an internal link after the session timeout refers to them, which is navigation,
|
|
30
30
|
* not acquisition. The SDK adds the payment and sign-in providers (`REFERRERS_NOT_A_TOUCH`).
|
|
31
31
|
*/
|
|
32
|
-
ownHosts?: readonly
|
|
32
|
+
ownHosts?: readonly RegExp[];
|
|
33
33
|
/** The product's rules, in order; the first that names the session wins. */
|
|
34
34
|
rules?: readonly TouchRule[];
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* GA4's default channel group of a (channel, medium) pair, with GA4's rule that the source
|
|
38
|
+
* decides too: `linkedin / (not set)` is Organic Social and `email / promo` is Email, because
|
|
39
|
+
* GA4 matches its site lists on the source, not only the medium. Two additions to GA4: Meta's
|
|
40
|
+
* placement names as a medium are a paid Meta click (see `META_PLACEMENTS`), and the AI
|
|
41
|
+
* assistants have `organic_ai`. Checked in GA4's order — paid before organic, the organic groups before
|
|
42
|
+
* email, referral last — so a pair that fits two rules lands where GA4 would put it.
|
|
43
|
+
*/
|
|
37
44
|
declare function channelGroupOf(channel: string, medium: string): ChannelGroup;
|
|
38
45
|
/**
|
|
39
46
|
* Reads the tags a session arrived with (its `session_start` tags) as one touch. Run once, when
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.mts","names":[],"sources":["../../src/attribution/classify.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"classify.d.mts","names":[],"sources":["../../src/attribution/classify.ts"],"mappings":";;;;UAoBiB;;EAEf;;EAEA;;EAEA,eAAe;;EAEf;;EAEA;;;KAIU,eAAe,KAAK;;;;;;;KAQpB,aAAa,MAAM,cAAc;UAE5B;;;;;;EAMf,oBAAoB;;EAEpB,iBAAiB;;;;;;;;;;iBAgFH,eAAe,iBAAiB,iBAAiB;;;;;;;;;;;;;;;iBAmCjD,cAAc,MAAM,WAAW,UAAS,kBAAuB"}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { AD_LANDING_PAGE, CLICK_ID_CHANNELS, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, SOURCE_ALIASES, TOUCH_PRIORITY } from "./vocabulary.mjs";
|
|
1
|
+
import { AD_LANDING_PAGE, CLICK_ID_CHANNELS, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, META_PLACEMENTS, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, SOURCE_ALIASES, TOUCH_PRIORITY } from "./vocabulary.mjs";
|
|
2
2
|
//#region src/attribution/classify.ts
|
|
3
|
-
const
|
|
4
|
-
const paidMedium = new RegExp(PAID_MEDIUM);
|
|
3
|
+
const metaPlacements = new Set(META_PLACEMENTS);
|
|
5
4
|
const referrerHostOf = /^https?:\/\/([^/:?#]+)/i;
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
const channelsBy = (medium) => new Set(REFERRER_SITES.filter(([, m]) => m === medium).map(([channel]) => channel));
|
|
6
|
+
const searchChannels = channelsBy("organic");
|
|
7
|
+
const socialChannels = channelsBy("social");
|
|
8
|
+
const videoChannels = channelsBy("video");
|
|
9
|
+
const aiChannels = channelsBy("ai");
|
|
10
|
+
/**
|
|
11
|
+
* A tag as text; absent when missing, empty, or the strings a broken template writes
|
|
12
|
+
* (`undefined`, `null`, an unexpanded `{{macro}}` other than Meta's placement, read elsewhere).
|
|
13
|
+
*/
|
|
15
14
|
function text(tags, key) {
|
|
16
15
|
const value = tags[key];
|
|
17
|
-
if (value === void 0 || value === null
|
|
18
|
-
|
|
16
|
+
if (value === void 0 || value === null) return null;
|
|
17
|
+
const s = String(value);
|
|
18
|
+
return s === "" || s === "undefined" || s === "null" ? null : s;
|
|
19
19
|
}
|
|
20
20
|
/** 1. What the campaign said: `utm_source`, with the aliases folded. */
|
|
21
21
|
function utmChannel(tags) {
|
|
22
|
-
const source = text(tags, "utm_source")?.toLowerCase();
|
|
22
|
+
const source = text(tags, "utm_source")?.toLowerCase().split(/[?&#]/)[0]?.replace(/\/+$/, "");
|
|
23
23
|
if (!source) return null;
|
|
24
24
|
return SOURCE_ALIASES[source] ?? source;
|
|
25
25
|
}
|
|
@@ -33,7 +33,7 @@ function clickChannel(tags) {
|
|
|
33
33
|
/** 3. A landing page reserved for one channel's ads. */
|
|
34
34
|
function landingChannel(tags) {
|
|
35
35
|
const location = text(tags, "page_location");
|
|
36
|
-
return location ?
|
|
36
|
+
return location ? AD_LANDING_PAGE.exec(location)?.[1] ?? null : null;
|
|
37
37
|
}
|
|
38
38
|
/** 4. The product's rules, in order. */
|
|
39
39
|
function productTouch(tags, rules) {
|
|
@@ -51,9 +51,9 @@ function referrer(tags, ownHosts) {
|
|
|
51
51
|
const url = text(tags, "page_referrer");
|
|
52
52
|
const host = url ? referrerHostOf.exec(url)?.[1]?.toLowerCase() : void 0;
|
|
53
53
|
if (!host) return null;
|
|
54
|
-
if (
|
|
55
|
-
if (ownHosts.some((pattern) =>
|
|
56
|
-
const site =
|
|
54
|
+
if (REFERRERS_NOT_A_TOUCH.some((pattern) => pattern.test(host))) return null;
|
|
55
|
+
if (ownHosts.some((pattern) => pattern.test(host))) return null;
|
|
56
|
+
const site = REFERRER_SITES.find(([, , pattern]) => pattern.test(host));
|
|
57
57
|
return site ? {
|
|
58
58
|
channel: site[0],
|
|
59
59
|
medium: site[1]
|
|
@@ -62,21 +62,30 @@ function referrer(tags, ownHosts) {
|
|
|
62
62
|
medium: "referral"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* GA4's default channel group of a (channel, medium) pair, with GA4's rule that the source
|
|
67
|
+
* decides too: `linkedin / (not set)` is Organic Social and `email / promo` is Email, because
|
|
68
|
+
* GA4 matches its site lists on the source, not only the medium. Two additions to GA4: Meta's
|
|
69
|
+
* placement names as a medium are a paid Meta click (see `META_PLACEMENTS`), and the AI
|
|
70
|
+
* assistants have `organic_ai`. Checked in GA4's order — paid before organic, the organic groups before
|
|
71
|
+
* email, referral last — so a pair that fits two rules lands where GA4 would put it.
|
|
72
|
+
*/
|
|
66
73
|
function channelGroupOf(channel, medium) {
|
|
67
74
|
if (channel === "(direct)") return "direct";
|
|
68
75
|
if (DISPLAY_MEDIUMS.includes(medium)) return "display";
|
|
69
|
-
if (
|
|
70
|
-
|
|
76
|
+
if (metaPlacements.has(medium)) return "paid_social";
|
|
77
|
+
if (PAID_MEDIUM.test(medium)) {
|
|
78
|
+
if (socialChannels.has(channel) || videoChannels.has(channel)) return "paid_social";
|
|
71
79
|
if (searchChannels.has(channel)) return "paid_search";
|
|
72
80
|
return "paid_other";
|
|
73
81
|
}
|
|
74
|
-
if (medium === "
|
|
75
|
-
if (medium === "
|
|
76
|
-
if (medium === "
|
|
77
|
-
if (medium === "
|
|
78
|
-
if (EMAIL_MEDIUMS.includes(
|
|
82
|
+
if (medium === "social" || socialChannels.has(channel)) return "organic_social";
|
|
83
|
+
if (medium === "video" || videoChannels.has(channel)) return "organic_video";
|
|
84
|
+
if (medium === "ai" || aiChannels.has(channel)) return "organic_ai";
|
|
85
|
+
if (medium === "organic" || searchChannels.has(channel)) return "organic_search";
|
|
86
|
+
if (EMAIL_MEDIUM.test(medium) || EMAIL_MEDIUMS.includes(channel)) return "email";
|
|
79
87
|
if (medium === "affiliate") return "affiliate";
|
|
88
|
+
if (medium === "referral") return "referral";
|
|
80
89
|
return "unassigned";
|
|
81
90
|
}
|
|
82
91
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.mjs","names":[],"sources":["../../src/attribution/classify.ts"],"sourcesContent":["import type { TrackTags } from '../track/types';\nimport {\n AD_LANDING_PAGE,\n CLICK_ID_CHANNELS,\n type ChannelGroup,\n DIRECT_CHANNEL,\n DISPLAY_MEDIUMS,\n EMAIL_MEDIUMS,\n MEDIUM_NOT_SET,\n NO_MEDIUM,\n PAID_MEDIUM,\n REFERRERS_NOT_A_TOUCH,\n REFERRER_SITES,\n SOURCE_ALIASES,\n TOUCH_PRIORITY,\n} from './vocabulary';\n\n/** What a session's tags say about how it arrived: the touch, as the session row stores it. */\nexport interface Touch {\n /** The source, as GA calls it: a `Channel`, a `utm_source` outside the list, a referring host, or `(direct)`. */\n channel: string;\n /** How, as GA calls it: `utm_medium` as declared, else what the rule that named the channel implies; `(none)` for direct. */\n medium: string;\n /** GA4's default channel group of the two. */\n channel_group: ChannelGroup;\n /** `utm_campaign`, else what a product rule captured (a referral code); null when neither. */\n campaign: string | null;\n /** `TOUCH_PRIORITY` of the rule that named the channel; null for direct, which is no touch. */\n priority: number | null;\n}\n\n/** What a product rule names, when it recognises the session; the channel group is derived. */\nexport type ProductTouch = Omit<Touch, 'channel_group'>;\n\n/**\n * A product's own rule: a landing page convention, a code in the URL. Tried after the campaign\n * rules (utm, click id, ad landing page) and before the referrer, so a link shared on Facebook\n * or WhatsApp is credited to what the product recognises, not to the site it was shared on.\n * Returns null when the session is not one of its.\n */\nexport type TouchRule = (tags: TrackTags) => ProductTouch | null;\n\nexport interface ClassifyOptions {\n /**\n * The product's own hosts, as patterns over the lower-cased referrer host: a session that\n * starts from an internal link after the session timeout refers to them, which is navigation,\n * not acquisition. The SDK adds the payment and sign-in providers (`REFERRERS_NOT_A_TOUCH`).\n */\n ownHosts?: readonly (RegExp | string)[];\n /** The product's rules, in order; the first that names the session wins. */\n rules?: readonly TouchRule[];\n}\n\nconst adLandingPage = new RegExp(AD_LANDING_PAGE);\nconst paidMedium = new RegExp(PAID_MEDIUM);\nconst referrerHostOf = /^https?:\\/\\/([^/:?#]+)/i;\nconst notATouch = REFERRERS_NOT_A_TOUCH.map((pattern) => new RegExp(pattern));\nconst referrerSites = REFERRER_SITES.map(\n ([channel, medium, pattern]) => [channel, medium, new RegExp(pattern)] as const\n);\nconst searchChannels = new Set<string>(\n REFERRER_SITES.filter(([, medium]) => medium === 'organic').map(([channel]) => channel)\n);\nconst socialChannels = new Set<string>(\n REFERRER_SITES.filter(([, medium]) => medium !== 'organic').map(([channel]) => channel)\n);\n\n/** A tag as text, lower-cased; absent when missing or empty. */\nfunction text(tags: TrackTags, key: string): string | null {\n const value = tags[key];\n if (value === undefined || value === null || value === '') return null;\n return String(value);\n}\n\n/** 1. What the campaign said: `utm_source`, with the aliases folded. */\nfunction utmChannel(tags: TrackTags): string | null {\n const source = text(tags, 'utm_source')?.toLowerCase();\n if (!source) return null;\n return (SOURCE_ALIASES as Record<string, string>)[source] ?? source;\n}\n\n/**\n * 2. A click id, possibly carried by a first-party cookie from an earlier click. Presence of the\n * key is the signal, as it is for the pixel that reads it.\n */\nfunction clickChannel(tags: TrackTags): string | null {\n return CLICK_ID_CHANNELS.find(([key]) => key in tags)?.[1] ?? null;\n}\n\n/** 3. A landing page reserved for one channel's ads. */\nfunction landingChannel(tags: TrackTags): string | null {\n const location = text(tags, 'page_location');\n return location ? (adLandingPage.exec(location)?.[1] ?? null) : null;\n}\n\n/** 4. The product's rules, in order. */\nfunction productTouch(tags: TrackTags, rules: readonly TouchRule[]): ProductTouch | null {\n for (const rule of rules) {\n const touch = rule(tags);\n if (touch) return touch;\n }\n return null;\n}\n\n/**\n * 5. The referrer's host: a known search engine or social network folded to its channel, any\n * other site kept as its host, and null for no referrer or one that is no touch.\n */\nfunction referrer(\n tags: TrackTags,\n ownHosts: readonly (RegExp | string)[]\n): { channel: string; medium: string } | null {\n const url = text(tags, 'page_referrer');\n const host = url ? referrerHostOf.exec(url)?.[1]?.toLowerCase() : undefined;\n if (!host) return null;\n if (notATouch.some((pattern) => pattern.test(host))) return null;\n if (ownHosts.some((pattern) => new RegExp(pattern).test(host))) return null;\n const site = referrerSites.find(([, , pattern]) => pattern.test(host));\n return site ? { channel: site[0], medium: site[1] } : { channel: host, medium: 'referral' };\n}\n\n/** GA4's default channel group of a (channel, medium) pair. */\nexport function channelGroupOf(channel: string, medium: string): ChannelGroup {\n if (channel === DIRECT_CHANNEL) return 'direct';\n if ((DISPLAY_MEDIUMS as readonly string[]).includes(medium)) return 'display';\n if (paidMedium.test(medium)) {\n if (socialChannels.has(channel)) return 'paid_social';\n if (searchChannels.has(channel)) return 'paid_search';\n return 'paid_other';\n }\n if (medium === 'organic') return 'organic_search';\n if (medium === 'social') return 'organic_social';\n if (medium === 'video') return 'organic_video';\n if (medium === 'referral') return 'referral';\n if ((EMAIL_MEDIUMS as readonly string[]).includes(medium)) return 'email';\n if (medium === 'affiliate') return 'affiliate';\n return 'unassigned';\n}\n\n/**\n * Reads the tags a session arrived with (its `session_start` tags) as one touch. Run once, when\n * the session is written; the result is stored, and the views only read it.\n *\n * The rules, in order, the first that says something naming the channel: an explicit\n * `utm_source`, a click id, an ad landing page, the product's own rules, the referrer's host;\n * `(direct)` when none does. `medium` is `utm_medium` as declared (lower-cased), else what that\n * rule implies: `(not set)` for a utm_source alone, `cpc` for a bare click id or ad landing page,\n * the product rule's own, `organic` / `social` / `video` / `referral` for a referrer, `(none)`\n * for direct. `campaign` is `utm_campaign`, else what a product rule captured — even when the\n * utm named the channel, so a tagged referral link keeps its code. `priority` is the tier of\n * the rule that named the channel (`TOUCH_PRIORITY`): campaign touches, then the product's own\n * as they declare, then the referrer.\n */\nexport function classifyTouch(tags: TrackTags, options: ClassifyOptions = {}): Touch {\n const utm = utmChannel(tags);\n const click = clickChannel(tags);\n const landing = landingChannel(tags);\n const product = productTouch(tags, options.rules ?? []);\n const referred = referrer(tags, options.ownHosts ?? []);\n\n const channel =\n utm ?? click ?? landing ?? product?.channel ?? referred?.channel ?? DIRECT_CHANNEL;\n const declaredMedium = text(tags, 'utm_medium')?.toLowerCase() ?? null;\n const medium =\n declaredMedium ??\n (utm\n ? MEDIUM_NOT_SET\n : click || landing\n ? 'cpc'\n : (product?.medium ?? referred?.medium ?? NO_MEDIUM));\n const campaign = text(tags, 'utm_campaign') ?? product?.campaign ?? null;\n const priority =\n utm || click || landing\n ? TOUCH_PRIORITY.campaign\n : product\n ? product.priority\n : referred\n ? TOUCH_PRIORITY.referrer\n : null;\n\n return { channel, medium, channel_group: channelGroupOf(channel, medium), campaign, priority };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"classify.mjs","names":[],"sources":["../../src/attribution/classify.ts"],"sourcesContent":["import type { TrackTags } from '../track/types';\nimport {\n AD_LANDING_PAGE,\n CLICK_ID_CHANNELS,\n type ChannelGroup,\n DIRECT_CHANNEL,\n DISPLAY_MEDIUMS,\n EMAIL_MEDIUM,\n EMAIL_MEDIUMS,\n MEDIUM_NOT_SET,\n META_PLACEMENTS,\n NO_MEDIUM,\n PAID_MEDIUM,\n REFERRERS_NOT_A_TOUCH,\n REFERRER_SITES,\n SOURCE_ALIASES,\n TOUCH_PRIORITY,\n} from './vocabulary';\n\n/** What a session's tags say about how it arrived: the touch, as the session row stores it. */\nexport interface Touch {\n /** The source, as GA calls it: a `Channel`, a `utm_source` outside the list, a referring host, or `(direct)`. */\n channel: string;\n /** How, as GA calls it: `utm_medium` as declared, else what the rule that named the channel implies; `(none)` for direct. */\n medium: string;\n /** GA4's default channel group of the two. */\n channel_group: ChannelGroup;\n /** `utm_campaign`, else what a product rule captured (a referral code); null when neither. */\n campaign: string | null;\n /** `TOUCH_PRIORITY` of the rule that named the channel; null for direct, which is no touch. */\n priority: number | null;\n}\n\n/** What a product rule names, when it recognises the session; the channel group is derived. */\nexport type ProductTouch = Omit<Touch, 'channel_group'>;\n\n/**\n * A product's own rule: a landing page convention, a code in the URL. Tried after the campaign\n * rules (utm, click id, ad landing page) and before the referrer, so a link shared on Facebook\n * or WhatsApp is credited to what the product recognises, not to the site it was shared on.\n * Returns null when the session is not one of its.\n */\nexport type TouchRule = (tags: TrackTags) => ProductTouch | null;\n\nexport interface ClassifyOptions {\n /**\n * The product's own hosts, as patterns over the lower-cased referrer host: a session that\n * starts from an internal link after the session timeout refers to them, which is navigation,\n * not acquisition. The SDK adds the payment and sign-in providers (`REFERRERS_NOT_A_TOUCH`).\n */\n ownHosts?: readonly RegExp[];\n /** The product's rules, in order; the first that names the session wins. */\n rules?: readonly TouchRule[];\n}\n\nconst metaPlacements = new Set<string>(META_PLACEMENTS);\nconst referrerHostOf = /^https?:\\/\\/([^/:?#]+)/i;\nconst channelsBy = (medium: string) =>\n new Set<string>(REFERRER_SITES.filter(([, m]) => m === medium).map(([channel]) => channel));\nconst searchChannels = channelsBy('organic');\nconst socialChannels = channelsBy('social');\nconst videoChannels = channelsBy('video');\nconst aiChannels = channelsBy('ai');\n\n/**\n * A tag as text; absent when missing, empty, or the strings a broken template writes\n * (`undefined`, `null`, an unexpanded `{{macro}}` other than Meta's placement, read elsewhere).\n */\nfunction text(tags: TrackTags, key: string): string | null {\n const value = tags[key];\n if (value === undefined || value === null) return null;\n const s = String(value);\n return s === '' || s === 'undefined' || s === 'null' ? null : s;\n}\n\n/** 1. What the campaign said: `utm_source`, with the aliases folded. */\nfunction utmChannel(tags: TrackTags): string | null {\n // A source with the rest of the query glued on (`email&utm_medium=promo`, `toolify/`,\n // `x?utm_source=x`): a link built by hand or double-encoded. Keep the source.\n const source = text(tags, 'utm_source')?.toLowerCase().split(/[?&#]/)[0]?.replace(/\\/+$/, '');\n if (!source) return null;\n return (SOURCE_ALIASES as Record<string, string>)[source] ?? source;\n}\n\n/**\n * 2. A click id, possibly carried by a first-party cookie from an earlier click. Presence of the\n * key is the signal, as it is for the pixel that reads it.\n */\nfunction clickChannel(tags: TrackTags): string | null {\n return CLICK_ID_CHANNELS.find(([key]) => key in tags)?.[1] ?? null;\n}\n\n/** 3. A landing page reserved for one channel's ads. */\nfunction landingChannel(tags: TrackTags): string | null {\n const location = text(tags, 'page_location');\n return location ? (AD_LANDING_PAGE.exec(location)?.[1] ?? null) : null;\n}\n\n/** 4. The product's rules, in order. */\nfunction productTouch(tags: TrackTags, rules: readonly TouchRule[]): ProductTouch | null {\n for (const rule of rules) {\n const touch = rule(tags);\n if (touch) return touch;\n }\n return null;\n}\n\n/**\n * 5. The referrer's host: a known search engine or social network folded to its channel, any\n * other site kept as its host, and null for no referrer or one that is no touch.\n */\nfunction referrer(\n tags: TrackTags,\n ownHosts: readonly RegExp[]\n): { channel: string; medium: string } | null {\n const url = text(tags, 'page_referrer');\n const host = url ? referrerHostOf.exec(url)?.[1]?.toLowerCase() : undefined;\n if (!host) return null;\n if (REFERRERS_NOT_A_TOUCH.some((pattern) => pattern.test(host))) return null;\n if (ownHosts.some((pattern) => pattern.test(host))) return null;\n const site = REFERRER_SITES.find(([, , pattern]) => pattern.test(host));\n return site ? { channel: site[0], medium: site[1] } : { channel: host, medium: 'referral' };\n}\n\n/**\n * GA4's default channel group of a (channel, medium) pair, with GA4's rule that the source\n * decides too: `linkedin / (not set)` is Organic Social and `email / promo` is Email, because\n * GA4 matches its site lists on the source, not only the medium. Two additions to GA4: Meta's\n * placement names as a medium are a paid Meta click (see `META_PLACEMENTS`), and the AI\n * assistants have `organic_ai`. Checked in GA4's order — paid before organic, the organic groups before\n * email, referral last — so a pair that fits two rules lands where GA4 would put it.\n */\nexport function channelGroupOf(channel: string, medium: string): ChannelGroup {\n if (channel === DIRECT_CHANNEL) return 'direct';\n if ((DISPLAY_MEDIUMS as readonly string[]).includes(medium)) return 'display';\n // A Meta placement as the medium is a Meta ad whatever the source was tagged as.\n if (metaPlacements.has(medium)) return 'paid_social';\n if (PAID_MEDIUM.test(medium)) {\n if (socialChannels.has(channel) || videoChannels.has(channel)) return 'paid_social';\n if (searchChannels.has(channel)) return 'paid_search';\n return 'paid_other';\n }\n if (medium === 'social' || socialChannels.has(channel)) return 'organic_social';\n if (medium === 'video' || videoChannels.has(channel)) return 'organic_video';\n if (medium === 'ai' || aiChannels.has(channel)) return 'organic_ai';\n if (medium === 'organic' || searchChannels.has(channel)) return 'organic_search';\n if (EMAIL_MEDIUM.test(medium) || (EMAIL_MEDIUMS as readonly string[]).includes(channel))\n return 'email';\n if (medium === 'affiliate') return 'affiliate';\n if (medium === 'referral') return 'referral';\n return 'unassigned';\n}\n\n/**\n * Reads the tags a session arrived with (its `session_start` tags) as one touch. Run once, when\n * the session is written; the result is stored, and the views only read it.\n *\n * The rules, in order, the first that says something naming the channel: an explicit\n * `utm_source`, a click id, an ad landing page, the product's own rules, the referrer's host;\n * `(direct)` when none does. `medium` is `utm_medium` as declared (lower-cased), else what that\n * rule implies: `(not set)` for a utm_source alone, `cpc` for a bare click id or ad landing page,\n * the product rule's own, `organic` / `social` / `video` / `referral` for a referrer, `(none)`\n * for direct. `campaign` is `utm_campaign`, else what a product rule captured — even when the\n * utm named the channel, so a tagged referral link keeps its code. `priority` is the tier of\n * the rule that named the channel (`TOUCH_PRIORITY`): campaign touches, then the product's own\n * as they declare, then the referrer.\n */\nexport function classifyTouch(tags: TrackTags, options: ClassifyOptions = {}): Touch {\n const utm = utmChannel(tags);\n const click = clickChannel(tags);\n const landing = landingChannel(tags);\n const product = productTouch(tags, options.rules ?? []);\n const referred = referrer(tags, options.ownHosts ?? []);\n\n const channel =\n utm ?? click ?? landing ?? product?.channel ?? referred?.channel ?? DIRECT_CHANNEL;\n const declaredMedium = text(tags, 'utm_medium')?.toLowerCase() ?? null;\n const medium =\n declaredMedium ??\n (utm\n ? MEDIUM_NOT_SET\n : click || landing\n ? 'cpc'\n : (product?.medium ?? referred?.medium ?? NO_MEDIUM));\n const campaign = text(tags, 'utm_campaign') ?? product?.campaign ?? null;\n const priority =\n utm || click || landing\n ? TOUCH_PRIORITY.campaign\n : product\n ? product.priority\n : referred\n ? TOUCH_PRIORITY.referrer\n : null;\n\n return { channel, medium, channel_group: channelGroupOf(channel, medium), campaign, priority };\n}\n"],"mappings":"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"}
|
|
@@ -7,8 +7,10 @@ exports.CHANNEL_GROUPS = require_attribution_vocabulary.CHANNEL_GROUPS;
|
|
|
7
7
|
exports.CLICK_ID_CHANNELS = require_attribution_vocabulary.CLICK_ID_CHANNELS;
|
|
8
8
|
exports.DIRECT_CHANNEL = require_attribution_vocabulary.DIRECT_CHANNEL;
|
|
9
9
|
exports.DISPLAY_MEDIUMS = require_attribution_vocabulary.DISPLAY_MEDIUMS;
|
|
10
|
+
exports.EMAIL_MEDIUM = require_attribution_vocabulary.EMAIL_MEDIUM;
|
|
10
11
|
exports.EMAIL_MEDIUMS = require_attribution_vocabulary.EMAIL_MEDIUMS;
|
|
11
12
|
exports.MEDIUM_NOT_SET = require_attribution_vocabulary.MEDIUM_NOT_SET;
|
|
13
|
+
exports.META_PLACEMENTS = require_attribution_vocabulary.META_PLACEMENTS;
|
|
12
14
|
exports.NO_MEDIUM = require_attribution_vocabulary.NO_MEDIUM;
|
|
13
15
|
exports.PAID_MEDIUM = require_attribution_vocabulary.PAID_MEDIUM;
|
|
14
16
|
exports.REFERRERS_NOT_A_TOUCH = require_attribution_vocabulary.REFERRERS_NOT_A_TOUCH;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, MEDIUM_NOT_SET, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, TouchSource } from "./vocabulary.cjs";
|
|
1
|
+
import { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, MEDIUM_NOT_SET, META_PLACEMENTS, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, TouchSource } from "./vocabulary.cjs";
|
|
2
2
|
import { ClassifyOptions, ProductTouch, Touch, TouchRule, channelGroupOf, classifyTouch } from "./classify.cjs";
|
|
3
|
-
export { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, ClassifyOptions, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, MEDIUM_NOT_SET, NO_MEDIUM, PAID_MEDIUM, ProductTouch, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, Touch, TouchRule, TouchSource, channelGroupOf, classifyTouch };
|
|
3
|
+
export { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, ClassifyOptions, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, MEDIUM_NOT_SET, META_PLACEMENTS, NO_MEDIUM, PAID_MEDIUM, ProductTouch, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, Touch, TouchRule, TouchSource, channelGroupOf, classifyTouch };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, MEDIUM_NOT_SET, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, TouchSource } from "./vocabulary.mjs";
|
|
1
|
+
import { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, MEDIUM_NOT_SET, META_PLACEMENTS, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, TouchSource } from "./vocabulary.mjs";
|
|
2
2
|
import { ClassifyOptions, ProductTouch, Touch, TouchRule, channelGroupOf, classifyTouch } from "./classify.mjs";
|
|
3
|
-
export { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, ClassifyOptions, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, MEDIUM_NOT_SET, NO_MEDIUM, PAID_MEDIUM, ProductTouch, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, Touch, TouchRule, TouchSource, channelGroupOf, classifyTouch };
|
|
3
|
+
export { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, Channel, ChannelGroup, ClassifyOptions, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, MEDIUM_NOT_SET, META_PLACEMENTS, NO_MEDIUM, PAID_MEDIUM, ProductTouch, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, ReferrerMedium, ReportedTouchKind, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, Touch, TouchRule, TouchSource, channelGroupOf, classifyTouch };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, MEDIUM_NOT_SET, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES } from "./vocabulary.mjs";
|
|
1
|
+
import { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, MEDIUM_NOT_SET, META_PLACEMENTS, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES } from "./vocabulary.mjs";
|
|
2
2
|
import { channelGroupOf, classifyTouch } from "./classify.mjs";
|
|
3
|
-
export { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUMS, MEDIUM_NOT_SET, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, channelGroupOf, classifyTouch };
|
|
3
|
+
export { AD_LANDING_PAGE, CHANNELS, CHANNEL_GROUPS, CLICK_ID_CHANNELS, DIRECT_CHANNEL, DISPLAY_MEDIUMS, EMAIL_MEDIUM, EMAIL_MEDIUMS, MEDIUM_NOT_SET, META_PLACEMENTS, NO_MEDIUM, PAID_MEDIUM, REFERRERS_NOT_A_TOUCH, REFERRER_SITES, REPORTED_TOUCH_KINDS, REPORTED_TOUCH_PRIORITY, SOURCE_ALIASES, TOUCH_PRIORITY, TOUCH_SOURCES, channelGroupOf, classifyTouch };
|