@shware/analytics 8.15.1 → 9.0.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/README.md +28 -4
- package/dist/attribution/classify.cjs +17 -5
- package/dist/attribution/classify.cjs.map +1 -1
- package/dist/attribution/classify.d.cts +2 -1
- package/dist/attribution/classify.d.cts.map +1 -1
- package/dist/attribution/classify.d.mts +2 -1
- package/dist/attribution/classify.d.mts.map +1 -1
- package/dist/attribution/classify.mjs +17 -5
- package/dist/attribution/classify.mjs.map +1 -1
- package/dist/attribution/vocabulary.cjs +99 -19
- package/dist/attribution/vocabulary.cjs.map +1 -1
- package/dist/attribution/vocabulary.d.cts +15 -4
- package/dist/attribution/vocabulary.d.cts.map +1 -1
- package/dist/attribution/vocabulary.d.mts +15 -4
- package/dist/attribution/vocabulary.d.mts.map +1 -1
- package/dist/attribution/vocabulary.mjs +99 -19
- package/dist/attribution/vocabulary.mjs.map +1 -1
- package/dist/click-id/index.cjs +24 -6
- package/dist/click-id/index.cjs.map +1 -1
- package/dist/click-id/index.d.cts +14 -3
- package/dist/click-id/index.d.cts.map +1 -1
- package/dist/click-id/index.d.mts +14 -3
- package/dist/click-id/index.d.mts.map +1 -1
- package/dist/click-id/index.mjs +23 -7
- package/dist/click-id/index.mjs.map +1 -1
- package/dist/schema/index.cjs +27 -9
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +126 -54
- package/dist/schema/index.d.cts.map +1 -1
- package/dist/schema/index.d.mts +126 -54
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +27 -9
- package/dist/schema/index.mjs.map +1 -1
- package/dist/server/click-ids.cjs +93 -0
- package/dist/server/click-ids.cjs.map +1 -0
- package/dist/server/click-ids.d.cts +40 -0
- package/dist/server/click-ids.d.cts.map +1 -0
- package/dist/server/click-ids.d.mts +40 -0
- package/dist/server/click-ids.d.mts.map +1 -0
- package/dist/server/click-ids.mjs +85 -0
- package/dist/server/click-ids.mjs.map +1 -0
- package/dist/server/google-data-manager.cjs +2 -1
- package/dist/server/google-data-manager.cjs.map +1 -1
- package/dist/server/google-data-manager.d.cts.map +1 -1
- package/dist/server/google-data-manager.d.mts.map +1 -1
- package/dist/server/google-data-manager.mjs +2 -1
- package/dist/server/google-data-manager.mjs.map +1 -1
- package/dist/server/linkedin-conversions-api.cjs +9 -4
- package/dist/server/linkedin-conversions-api.cjs.map +1 -1
- package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
- package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
- package/dist/server/linkedin-conversions-api.mjs +9 -4
- package/dist/server/linkedin-conversions-api.mjs.map +1 -1
- package/dist/server/meta-capi.cjs +5 -4
- package/dist/server/meta-capi.cjs.map +1 -1
- package/dist/server/meta-capi.mjs +5 -4
- package/dist/server/meta-capi.mjs.map +1 -1
- package/dist/server/meta-conversions-api.cjs +5 -4
- package/dist/server/meta-conversions-api.cjs.map +1 -1
- package/dist/server/meta-conversions-api.d.cts.map +1 -1
- package/dist/server/meta-conversions-api.d.mts.map +1 -1
- package/dist/server/meta-conversions-api.mjs +5 -4
- package/dist/server/meta-conversions-api.mjs.map +1 -1
- package/dist/server/microsoft-conversions-api.cjs +3 -1
- package/dist/server/microsoft-conversions-api.cjs.map +1 -1
- package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
- package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
- package/dist/server/microsoft-conversions-api.mjs +3 -1
- package/dist/server/microsoft-conversions-api.mjs.map +1 -1
- package/dist/server/openai-conversions-api.cjs +33 -10
- package/dist/server/openai-conversions-api.cjs.map +1 -1
- package/dist/server/openai-conversions-api.d.cts +19 -8
- package/dist/server/openai-conversions-api.d.cts.map +1 -1
- package/dist/server/openai-conversions-api.d.mts +19 -8
- package/dist/server/openai-conversions-api.d.mts.map +1 -1
- package/dist/server/openai-conversions-api.mjs +33 -10
- package/dist/server/openai-conversions-api.mjs.map +1 -1
- package/dist/server/reddit-conversions-api.cjs +3 -2
- package/dist/server/reddit-conversions-api.cjs.map +1 -1
- package/dist/server/reddit-conversions-api.d.cts.map +1 -1
- package/dist/server/reddit-conversions-api.d.mts.map +1 -1
- package/dist/server/reddit-conversions-api.mjs +3 -2
- package/dist/server/reddit-conversions-api.mjs.map +1 -1
- package/dist/track/types.d.cts +50 -16
- package/dist/track/types.d.cts.map +1 -1
- package/dist/track/types.d.mts +50 -16
- package/dist/track/types.d.mts.map +1 -1
- package/dist/web/index.cjs +19 -11
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts.map +1 -1
- package/dist/web/index.d.mts.map +1 -1
- package/dist/web/index.mjs +19 -11
- package/dist/web/index.mjs.map +1 -1
- package/dist/web/page-key.cjs +4 -2
- package/dist/web/page-key.cjs.map +1 -1
- package/dist/web/page-key.d.cts.map +1 -1
- package/dist/web/page-key.d.mts.map +1 -1
- package/dist/web/page-key.mjs +4 -2
- package/dist/web/page-key.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ export declare const SOURCE_ALIASES: {
|
|
|
36
36
|
readonly ig: 'meta';
|
|
37
37
|
readonly an: 'meta';
|
|
38
38
|
readonly msg: 'meta';
|
|
39
|
+
readonly th: 'meta';
|
|
39
40
|
readonly facebook: 'meta';
|
|
40
41
|
readonly instagram: 'meta';
|
|
41
42
|
readonly bing: 'microsoft';
|
|
@@ -54,11 +55,21 @@ export declare const SOURCE_ALIASES: {
|
|
|
54
55
|
readonly 'kimi.com': 'kimi';
|
|
55
56
|
};
|
|
56
57
|
/**
|
|
57
|
-
* Which click id belongs to which channel, in priority order
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* Which click id belongs to which channel, in priority order, and whether the platform puts it on
|
|
59
|
+
* its ad clicks only (`'ads'`) or on other links as well (`'any'`). Every key is a field this SDK
|
|
60
|
+
* collects (`AdvertisingInfo`).
|
|
61
|
+
*
|
|
62
|
+
* The third column decides whether the id can say a click was paid when a `utm_source` names the
|
|
63
|
+
* channel but no `utm_medium` says how (a Reddit ad tagged `utm_source=reddit` and nothing more).
|
|
64
|
+
* `fbclid` is `'any'`: Meta decorates every outbound link with it, organic posts, profile links
|
|
65
|
+
* and shares included. `twclid` and `ko_click_id` are `'any'` until it is clear they never
|
|
66
|
+
* decorate an organic link.
|
|
67
|
+
*
|
|
68
|
+
* Only URL parameters, never the cookies the tags keep beside them (`_fbc`, `_gcl_aw`, … — every
|
|
69
|
+
* underscore key of `AdvertisingInfo`): a click cookie lives up to 90 days after the click, so
|
|
70
|
+
* counting it named every later visit — typed in, from search, from an email — a new ad click.
|
|
60
71
|
*/
|
|
61
|
-
export declare const CLICK_ID_CHANNELS: readonly [readonly ["fbclid", "meta"
|
|
72
|
+
export declare const CLICK_ID_CHANNELS: readonly [readonly ["fbclid", "meta", "any"], readonly ["gclid", "google", "ads"], readonly ["gbraid", "google", "ads"], readonly ["wbraid", "google", "ads"], readonly ["gad_source", "google", "ads"], readonly ["gad_campaignid", "google", "ads"], readonly ["dclid", "dv360", "ads"], readonly ["msclkid", "microsoft", "ads"], readonly ["ttclid", "tiktok", "ads"], readonly ["rdt_cid", "reddit", "ads"], readonly ["li_fat_id", "linkedin", "ads"], readonly ["ScCid", "snapchat", "ads"], readonly ["twclid", "x", "any"], readonly ["ko_click_id", "kakao", "any"], readonly ["yclid", "yandex", "ads"], readonly ["oppref", "chatgpt", "ads"], readonly ["epik", "pinterest", "ads"]];
|
|
62
73
|
/**
|
|
63
74
|
* A landing page reserved for one channel's ads, for ads that lost their parameters:
|
|
64
75
|
* `/lp/<channel>`, with or without host, query and hash; the capture is the channel. A product
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vocabulary.d.mts","names":[],"sources":["../../src/attribution/vocabulary.ts"],"mappings":";;;;;;;;qBAUa;;qBAEA;;qBAEA;;qBAGA;;;;;;;;;;;;YA4CD,kBAAkB;;qBAGjB;;;YAiBD,uBAAuB;;qBAGtB;WACP;WACA;WACA;WACC;
|
|
1
|
+
{"version":3,"file":"vocabulary.d.mts","names":[],"sources":["../../src/attribution/vocabulary.ts"],"mappings":";;;;;;;;qBAUa;;qBAEA;;qBAEA;;qBAGA;;;;;;;;;;;;YA4CD,kBAAkB;;qBAGjB;;;YAiBD,uBAAuB;;qBAGtB;WACP;WACA;WACA;WACC;WACD;WACM;WACC;WACL;WACG;WACK;WACH;WACF;WACM;WACI;WACX;WACS;WACJ;WACE;WACC;WACF;WACF;;;;;;;;;;;;;;;;;qBAkBD;;;;;;qBAyBA,iBAAe;YAEhB;;;;;;;;;qBAUC,qDAAc,uCAAA,mCAAA,mCAAA,oCAAA,iCAAA,qCAAA,mCAAA,iCAAA,iCAAA,oCAAA,kCAAA,kCAAA,wCAAA,2CAAA,uCAAA,4CAAA,uCAAA,wCAAA,wCAAA,uCAAA,uCAAA,qCAAA,kCAAA,yCAAA,uCAAA,uCAAA,0CAAA,yCAAA,uCAAA;;;;;;;;;;;;;qBA6Cd,iCAAqB,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA;;qBAgCrB,aAAW;;;;;;;;;;;qBAWX;;;;qBA2CA;;qBAEA;;qBAEA,cAAY;;qBAGZ;YACD,sBAAsB;;;;;;;;qBASrB;YACD,4BAA4B;;;;;;;;;;;;qBAa3B;;WAED;;WAEA;;WAEA;;WAED;;;qBAIE"}
|
|
@@ -69,6 +69,7 @@ const SOURCE_ALIASES = {
|
|
|
69
69
|
ig: "meta",
|
|
70
70
|
an: "meta",
|
|
71
71
|
msg: "meta",
|
|
72
|
+
th: "meta",
|
|
72
73
|
facebook: "meta",
|
|
73
74
|
instagram: "meta",
|
|
74
75
|
bing: "microsoft",
|
|
@@ -87,27 +88,106 @@ const SOURCE_ALIASES = {
|
|
|
87
88
|
"kimi.com": "kimi"
|
|
88
89
|
};
|
|
89
90
|
/**
|
|
90
|
-
* Which click id belongs to which channel, in priority order
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* Which click id belongs to which channel, in priority order, and whether the platform puts it on
|
|
92
|
+
* its ad clicks only (`'ads'`) or on other links as well (`'any'`). Every key is a field this SDK
|
|
93
|
+
* collects (`AdvertisingInfo`).
|
|
94
|
+
*
|
|
95
|
+
* The third column decides whether the id can say a click was paid when a `utm_source` names the
|
|
96
|
+
* channel but no `utm_medium` says how (a Reddit ad tagged `utm_source=reddit` and nothing more).
|
|
97
|
+
* `fbclid` is `'any'`: Meta decorates every outbound link with it, organic posts, profile links
|
|
98
|
+
* and shares included. `twclid` and `ko_click_id` are `'any'` until it is clear they never
|
|
99
|
+
* decorate an organic link.
|
|
100
|
+
*
|
|
101
|
+
* Only URL parameters, never the cookies the tags keep beside them (`_fbc`, `_gcl_aw`, … — every
|
|
102
|
+
* underscore key of `AdvertisingInfo`): a click cookie lives up to 90 days after the click, so
|
|
103
|
+
* counting it named every later visit — typed in, from search, from an email — a new ad click.
|
|
93
104
|
*/
|
|
94
105
|
const CLICK_ID_CHANNELS = [
|
|
95
|
-
[
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
[
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
[
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
[
|
|
106
|
+
[
|
|
107
|
+
"fbclid",
|
|
108
|
+
"meta",
|
|
109
|
+
"any"
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"gclid",
|
|
113
|
+
"google",
|
|
114
|
+
"ads"
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"gbraid",
|
|
118
|
+
"google",
|
|
119
|
+
"ads"
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"wbraid",
|
|
123
|
+
"google",
|
|
124
|
+
"ads"
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
"gad_source",
|
|
128
|
+
"google",
|
|
129
|
+
"ads"
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
"gad_campaignid",
|
|
133
|
+
"google",
|
|
134
|
+
"ads"
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"dclid",
|
|
138
|
+
"dv360",
|
|
139
|
+
"ads"
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"msclkid",
|
|
143
|
+
"microsoft",
|
|
144
|
+
"ads"
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"ttclid",
|
|
148
|
+
"tiktok",
|
|
149
|
+
"ads"
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
"rdt_cid",
|
|
153
|
+
"reddit",
|
|
154
|
+
"ads"
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
"li_fat_id",
|
|
158
|
+
"linkedin",
|
|
159
|
+
"ads"
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
"ScCid",
|
|
163
|
+
"snapchat",
|
|
164
|
+
"ads"
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"twclid",
|
|
168
|
+
"x",
|
|
169
|
+
"any"
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"ko_click_id",
|
|
173
|
+
"kakao",
|
|
174
|
+
"any"
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"yclid",
|
|
178
|
+
"yandex",
|
|
179
|
+
"ads"
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
"oppref",
|
|
183
|
+
"chatgpt",
|
|
184
|
+
"ads"
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
"epik",
|
|
188
|
+
"pinterest",
|
|
189
|
+
"ads"
|
|
190
|
+
]
|
|
111
191
|
];
|
|
112
192
|
/**
|
|
113
193
|
* A landing page reserved for one channel's ads, for ads that lost their parameters:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vocabulary.mjs","names":[],"sources":["../../src/attribution/vocabulary.ts"],"sourcesContent":["import type { AdvertisingInfo } from '../track/types';\n\n/**\n * The vocabulary attribution is built from, shared by every product's `session` table and by the\n * dashboards that read it. Data for `classifyTouch` (./classify), which turns it into a channel;\n * a product's dashboards read the same names back. What stays with the product is its own hostnames\n * (referrer exclusions) and its landing-page conventions.\n */\n\n/** A session that arrived through nothing we can name. GA's spelling, so dashboards read as GA. */\nexport const DIRECT_CHANNEL = '(direct)';\n/** The medium of a direct session. */\nexport const NO_MEDIUM = '(none)';\n/** The medium of a tagged link that named a source but no medium. */\nexport const MEDIUM_NOT_SET = '(not set)';\n\n/** The channels the rules below can name. A `utm_source` outside this list is its own channel. */\nexport const CHANNELS = [\n 'meta',\n 'google',\n 'dv360',\n 'microsoft',\n 'tiktok',\n 'reddit',\n 'linkedin',\n 'snapchat',\n 'x',\n 'kakao',\n 'yandex',\n 'yahoo',\n 'duckduckgo',\n 'baidu',\n 'ecosia',\n 'naver',\n 'brave',\n 'pinterest',\n 'youtube',\n /** AI assistants that send people to sites: ChatGPT tags its links `utm_source=chatgpt.com`, the\n * others show as a referrer. Their own channel group, `organic_ai`, since GA4's default groups\n * would file them under Referral / Unassigned and the question \"how much does AI bring\" comes up. */\n 'chatgpt',\n 'perplexity',\n 'gemini',\n 'claude',\n 'copilot',\n 'grok',\n /** and the Chinese ones — DeepSeek, 豆包, Kimi, 通义, 腾讯元宝, 文心一言, 智谱清言 */\n 'deepseek',\n 'doubao',\n 'kimi',\n 'qwen',\n 'yuanbao',\n 'ernie',\n 'zhipu',\n /** the product's own referral programme: the session came through a member's link or code.\n * How a product recognises one (a `/refer/<code>` path, a code at sign-up) is its own rule. Not\n * `referral`: that word is the channel group of any outside site, and a channel of the same name\n * would be impossible to tell apart in queries. */\n 'referral_program',\n] as const;\n\nexport type Channel = (typeof CHANNELS)[number];\n\n/** GA4's default channel groups, as identifiers; a dashboard maps them to display names. */\nexport const CHANNEL_GROUPS = [\n 'direct',\n 'paid_search',\n 'paid_social',\n 'paid_other',\n 'display',\n 'organic_search',\n 'organic_social',\n 'organic_video',\n /** not in GA4's default grouping: sessions an AI assistant sent, by referrer or by ChatGPT's utm */\n 'organic_ai',\n 'referral',\n 'email',\n 'affiliate',\n 'unassigned',\n] as const;\n\nexport type ChannelGroup = (typeof CHANNEL_GROUPS)[number];\n\n/** `utm_source` spellings that mean the same channel: Meta's `{{site_source_name}}` values and the usual synonyms. */\nexport const SOURCE_ALIASES = {\n fb: 'meta',\n ig: 'meta',\n an: 'meta',\n msg: 'meta',\n facebook: 'meta',\n instagram: 'meta',\n bing: 'microsoft',\n twitter: 'x',\n 'google ads': 'google',\n googleads: 'google',\n adwords: 'google',\n 'chatgpt.com': 'chatgpt',\n 'chat.openai.com': 'chatgpt',\n openai: 'chatgpt',\n 'perplexity.ai': 'perplexity',\n 'claude.ai': 'claude',\n 'copilot.com': 'copilot',\n 'deepseek.com': 'deepseek',\n 'doubao.com': 'doubao',\n 'kimi.com': 'kimi',\n} as const satisfies Record<string, Channel>;\n\n/**\n * Which click id belongs to which channel, in priority order. Every key is a field this SDK\n * collects (`AdvertisingInfo`); `fbp` and `rdt_uuid` are deliberately absent — they are pixel\n * browser ids, present with or without a click.\n */\nexport const CLICK_ID_CHANNELS = [\n ['fbclid', 'meta'],\n ['fbc', 'meta'],\n ['gclid', 'google'],\n ['gbraid', 'google'],\n ['wbraid', 'google'],\n ['gad_source', 'google'],\n ['gad_campaignid', 'google'],\n ['dclid', 'dv360'],\n ['msclkid', 'microsoft'],\n ['ttclid', 'tiktok'],\n ['rdt_cid', 'reddit'],\n ['li_fat_id', 'linkedin'],\n ['sccid', 'snapchat'],\n ['twclid', 'x'],\n ['ko_click_id', 'kakao'],\n ['yclid', 'yandex'],\n] as const satisfies readonly (readonly [keyof AdvertisingInfo, Channel])[];\n\n/**\n * A landing page reserved for one channel's ads, for ads that lost their parameters:\n * `/lp/<channel>`, with or without host, query and hash; the capture is the channel. A product\n * that names its landing pages differently replaces this.\n */\nexport const AD_LANDING_PAGE = /^(?:https?:\\/\\/[^/]+)?\\/lp\\/([a-z]+)(?:[/?#]|$)/;\n\nexport type ReferrerMedium = 'organic' | 'social' | 'video' | 'ai';\n\n/**\n * Referrer hosts that name a channel, as regular expressions over the lower-cased host,\n * with the medium GA4 gives that kind of site. Anything else that refers is `referral`, kept as\n * its host. A country TLD is one or two labels (`google.com`, `google.co.uk`, `google.com.hk`),\n * never an open tail, so `google.com.evil.io` is not Google. In match order: the AI assistants\n * first, because `gemini.google.com`, `copilot.microsoft.com` and `yiyan.baidu.com` would\n * otherwise be Google, Microsoft and Baidu search.\n */\nexport const REFERRER_SITES = [\n ['chatgpt', 'ai', /(^|\\.)(chatgpt\\.com|chat\\.openai\\.com)$/],\n ['perplexity', 'ai', /(^|\\.)perplexity\\.ai$/],\n ['gemini', 'ai', /^gemini\\.google\\.com$/],\n ['claude', 'ai', /(^|\\.)claude\\.ai$/],\n ['copilot', 'ai', /^copilot\\.microsoft\\.com$|(^|\\.)copilot\\.com$/],\n ['grok', 'ai', /(^|\\.)(grok\\.com|x\\.ai)$/],\n ['deepseek', 'ai', /(^|\\.)deepseek\\.com$/],\n ['doubao', 'ai', /(^|\\.)doubao\\.com$/],\n ['kimi', 'ai', /(^|\\.)(kimi\\.com|kimi\\.moonshot\\.cn|moonshot\\.cn)$/],\n ['qwen', 'ai', /^tongyi\\.aliyun\\.com$|(^|\\.)(tongyi\\.com|qianwen\\.com|qwen\\.ai)$/],\n ['yuanbao', 'ai', /^yuanbao\\.tencent\\.com$/],\n ['ernie', 'ai', /^yiyan\\.baidu\\.com$|(^|\\.)ernie\\.baidu\\.com$/],\n ['zhipu', 'ai', /(^|\\.)(chatglm\\.cn|zhipuai\\.cn|bigmodel\\.cn)$/],\n ['google', 'organic', /(^|\\.)google\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['microsoft', 'organic', /(^|\\.)bing\\.com$/],\n ['yahoo', 'organic', /(^|\\.)yahoo\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['duckduckgo', 'organic', /(^|\\.)duckduckgo\\.com$/],\n ['baidu', 'organic', /(^|\\.)baidu\\.com$/],\n ['yandex', 'organic', /(^|\\.)yandex\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['ecosia', 'organic', /(^|\\.)ecosia\\.org$/],\n ['naver', 'organic', /(^|\\.)naver\\.com$/],\n ['brave', 'organic', /^search\\.brave\\.com$/],\n ['meta', 'social', /(^|\\.)(facebook\\.com|instagram\\.com|fb\\.com|threads\\.net)$/],\n ['x', 'social', /(^|\\.)(twitter\\.com|x\\.com|t\\.co)$/],\n ['linkedin', 'social', /(^|\\.)(linkedin\\.com|lnkd\\.in)$/],\n ['reddit', 'social', /(^|\\.)reddit\\.com$/],\n ['tiktok', 'social', /(^|\\.)tiktok\\.com$/],\n ['pinterest', 'social', /(^|\\.)pinterest\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['snapchat', 'social', /(^|\\.)snapchat\\.com$/],\n ['youtube', 'video', /(^|\\.)(youtube\\.com|youtu\\.be)$/],\n] as const satisfies readonly (readonly [Channel, ReferrerMedium, RegExp])[];\n\n/**\n * Referrer hosts that are navigation, not acquisition, when a session happens to start on the way\n * back from them: the local dev host; the payment providers a visitor is bounced through; the\n * sign-in providers whose login lives on a host of its own (Google, Apple, Microsoft, WeChat,\n * Kakao, LINE, Naver, Yahoo, Twitch, X's OAuth 1.0a endpoint); and the hosted auth services a\n * product may sit behind (Auth0, Okta, Supabase, Firebase, Clerk). A product appends its own\n * hostnames — a session that starts from an internal link after the session timeout refers to\n * them. Matched before `REFERRER_SITES`, so `nid.naver.com` and `login.yahoo.com` are not read as\n * a search. Providers whose login shares the host with their content — Facebook, X's OAuth 2.0,\n * LinkedIn, GitHub, Discord — cannot be told apart by host (referrer policies strip the path) and\n * are left as the referrers they usually are. Regular expressions over the lower-cased host.\n */\nexport const REFERRERS_NOT_A_TOUCH = [\n /^localhost(:|$)/,\n // payment\n /(^|\\.)stripe\\.com$/,\n /^pay\\.google\\.com$/,\n /(^|\\.)paypal\\.com$/,\n /(^|\\.)alipay\\.com$/,\n /(^|\\.)paddle\\.com$/,\n /(^|\\.)lemonsqueezy\\.com$/,\n // app stores\n /^apps\\.apple\\.com$/,\n /^play\\.google\\.com$/,\n // sign-in providers with a login host of their own\n /^accounts\\.google\\.com$/,\n /^appleid\\.apple\\.com$/,\n /^login\\.(microsoftonline|live)\\.com$/,\n /^open\\.weixin\\.qq\\.com$/,\n /^(kauth|accounts)\\.kakao\\.com$/,\n /^access\\.line\\.me$/,\n /^nid\\.naver\\.com$/,\n /^login\\.yahoo\\.com$/,\n /^id\\.twitch\\.tv$/,\n /^api\\.twitter\\.com$/,\n // hosted auth services\n /(^|\\.)auth0\\.com$/,\n /(^|\\.)okta\\.com$/,\n /(^|\\.)supabase\\.co$/,\n /(^|\\.)firebaseapp\\.com$/,\n /(^|\\.)clerk\\.accounts\\.dev$/,\n] as const;\n\n/** GA4's paid mediums — cpc, cpm, ppc, retargeting, paid_social, … — plus Performance Max. */\nexport const PAID_MEDIUM = /^(.*cp.*|ppc|retargeting|paid.*|pmax|performance_max)$/;\n/**\n * Meta's `{{placement}}` values, lower-cased, which an ad URL template of `utm_medium={{placement}}`\n * puts in the medium. Only an ad carries one — an organic post arrives as a referrer, with no utm\n * at all — so a medium in this list is a paid Meta click whatever the source says (a template with\n * `utm_source=meta-websitekeyinfo` was seen). Listed one by one rather than matched by prefix, so\n * that a hand-tagged `instagram_stories` post cannot be mistaken for an ad only because of how it\n * is spelled … which means a placement Meta adds later shows up as unassigned until it is added\n * here. Meta's documented values plus the ones seen in the wild (`facebook_mobile_reels`,\n * `facebook_notification`, `an`), and the literal macro for a template that did not expand it.\n */\nexport const META_PLACEMENTS = [\n 'facebook_desktop_feed',\n 'facebook_mobile_feed',\n 'facebook_mobile_reels',\n 'facebook_reels',\n 'facebook_reels_overlay',\n 'facebook_right_column',\n 'facebook_marketplace',\n 'facebook_video_feeds',\n 'facebook_stories',\n 'facebook_search',\n 'facebook_instream_video',\n 'facebook_instant_article',\n 'facebook_groups_feed',\n 'facebook_profile_feed',\n 'facebook_notification',\n 'facebook_notifications',\n 'facebook_business_explore',\n 'instagram_feed',\n 'instagram_stories',\n 'instagram_reels',\n 'instagram_reels_overlay',\n 'instagram_explore',\n 'instagram_explore_home',\n 'instagram_search',\n 'instagram_shop',\n 'instagram_profile_feed',\n 'instagram_profile_reels',\n 'instagram_igtv',\n 'messenger_inbox',\n 'messenger_stories',\n 'messenger_sponsored_messages',\n 'threads_feed',\n 'whatsapp_status',\n 'audience_network_classic',\n 'audience_network_rewarded_video',\n 'audience_network_native_banner_and_interstitial',\n 'an',\n 'others',\n /** the macro itself, when Meta did not expand it (written in a field it does not substitute, or a\n * preview click): still only ever produced by a Meta ad URL, so still a paid Meta click */\n '{{placement}}',\n] as const;\nexport const DISPLAY_MEDIUMS = ['display', 'banner', 'expandable', 'interstitial'] as const;\n/** GA4's email spellings, as a source or a medium. */\nexport const EMAIL_MEDIUMS = ['email', 'e-mail', 'e_mail', 'e mail'] as const;\n/** A medium that says email or newsletter anywhere in it: `outbound email`, `cold_email`, `newsletter`, and GA4's four. */\nexport const EMAIL_MEDIUM = /(^|[^a-z])(e[-_ ]?mail|newsletter)([^a-z]|$)/;\n\n/** Where a touch came from: a tracked session, or a report by the user or staff. */\nexport const TOUCH_SOURCES = ['tracked', 'reported'] as const;\nexport type TouchSource = (typeof TOUCH_SOURCES)[number];\n\n/**\n * How a reported touch was learned:\n * - survey: the user answered \"how did you hear about us\" (onboarding, checkout)\n * - promo_code: the user redeemed a code that belongs to a channel\n * - phone: a call came in on a channel's dedicated number, marked by staff or the phone system\n * - manual: staff recorded what a customer told them\n */\nexport const REPORTED_TOUCH_KINDS = ['survey', 'promo_code', 'phone', 'manual'] as const;\nexport type ReportedTouchKind = (typeof REPORTED_TOUCH_KINDS)[number];\n\n/**\n * How much a touch is worth against another when attribution has to choose between sessions;\n * lower wins, and the latest wins among equals. Stored on the session when it is classified, so\n * renumbering means reclassifying.\n *\n * This ranks touches against each other. Within one session the rules that name its channel have\n * their own order — `utm_source`, then a click id, then the ad landing page, then the referrer —\n * and the first that says something wins; the session's priority is then the tier of that rule.\n * So two sessions tagged with utm and gclid respectively are equal here (both `campaign`) and the\n * later one is credited, while one session carrying both is named by its utm.\n */\nexport const TOUCH_PRIORITY = {\n /** utm, click id, ad landing page: someone tagged that link (in that order within a session) */\n campaign: 1,\n /** a reported touch staff stand behind: a call on a channel's number, a recorded conversation */\n verified: 1,\n /** the browser said where the visitor came from; nobody chose it */\n referrer: 2,\n /** a reported touch that is the user's claim: a survey answer, a code that may have been shared */\n claimed: 3,\n} as const satisfies Record<string, number>;\n\n/** Which reported kinds are staff facts and which are the user's claims. */\nexport const REPORTED_TOUCH_PRIORITY = {\n phone: TOUCH_PRIORITY.verified,\n manual: TOUCH_PRIORITY.verified,\n survey: TOUCH_PRIORITY.claimed,\n promo_code: TOUCH_PRIORITY.claimed,\n} as const satisfies Record<ReportedTouchKind, number>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vocabulary.mjs","names":[],"sources":["../../src/attribution/vocabulary.ts"],"sourcesContent":["import type { AdvertisingInfo } from '../track/types';\n\n/**\n * The vocabulary attribution is built from, shared by every product's `session` table and by the\n * dashboards that read it. Data for `classifyTouch` (./classify), which turns it into a channel;\n * a product's dashboards read the same names back. What stays with the product is its own hostnames\n * (referrer exclusions) and its landing-page conventions.\n */\n\n/** A session that arrived through nothing we can name. GA's spelling, so dashboards read as GA. */\nexport const DIRECT_CHANNEL = '(direct)';\n/** The medium of a direct session. */\nexport const NO_MEDIUM = '(none)';\n/** The medium of a tagged link that named a source but no medium. */\nexport const MEDIUM_NOT_SET = '(not set)';\n\n/** The channels the rules below can name. A `utm_source` outside this list is its own channel. */\nexport const CHANNELS = [\n 'meta',\n 'google',\n 'dv360',\n 'microsoft',\n 'tiktok',\n 'reddit',\n 'linkedin',\n 'snapchat',\n 'x',\n 'kakao',\n 'yandex',\n 'yahoo',\n 'duckduckgo',\n 'baidu',\n 'ecosia',\n 'naver',\n 'brave',\n 'pinterest',\n 'youtube',\n /** AI assistants that send people to sites: ChatGPT tags its links `utm_source=chatgpt.com`, the\n * others show as a referrer. Their own channel group, `organic_ai`, since GA4's default groups\n * would file them under Referral / Unassigned and the question \"how much does AI bring\" comes up. */\n 'chatgpt',\n 'perplexity',\n 'gemini',\n 'claude',\n 'copilot',\n 'grok',\n /** and the Chinese ones — DeepSeek, 豆包, Kimi, 通义, 腾讯元宝, 文心一言, 智谱清言 */\n 'deepseek',\n 'doubao',\n 'kimi',\n 'qwen',\n 'yuanbao',\n 'ernie',\n 'zhipu',\n /** the product's own referral programme: the session came through a member's link or code.\n * How a product recognises one (a `/refer/<code>` path, a code at sign-up) is its own rule. Not\n * `referral`: that word is the channel group of any outside site, and a channel of the same name\n * would be impossible to tell apart in queries. */\n 'referral_program',\n] as const;\n\nexport type Channel = (typeof CHANNELS)[number];\n\n/** GA4's default channel groups, as identifiers; a dashboard maps them to display names. */\nexport const CHANNEL_GROUPS = [\n 'direct',\n 'paid_search',\n 'paid_social',\n 'paid_other',\n 'display',\n 'organic_search',\n 'organic_social',\n 'organic_video',\n /** not in GA4's default grouping: sessions an AI assistant sent, by referrer or by ChatGPT's utm */\n 'organic_ai',\n 'referral',\n 'email',\n 'affiliate',\n 'unassigned',\n] as const;\n\nexport type ChannelGroup = (typeof CHANNEL_GROUPS)[number];\n\n/** `utm_source` spellings that mean the same channel: Meta's `{{site_source_name}}` values and the usual synonyms. */\nexport const SOURCE_ALIASES = {\n fb: 'meta',\n ig: 'meta',\n an: 'meta',\n msg: 'meta',\n th: 'meta',\n facebook: 'meta',\n instagram: 'meta',\n bing: 'microsoft',\n twitter: 'x',\n 'google ads': 'google',\n googleads: 'google',\n adwords: 'google',\n 'chatgpt.com': 'chatgpt',\n 'chat.openai.com': 'chatgpt',\n openai: 'chatgpt',\n 'perplexity.ai': 'perplexity',\n 'claude.ai': 'claude',\n 'copilot.com': 'copilot',\n 'deepseek.com': 'deepseek',\n 'doubao.com': 'doubao',\n 'kimi.com': 'kimi',\n} as const satisfies Record<string, Channel>;\n\n/**\n * Which click id belongs to which channel, in priority order, and whether the platform puts it on\n * its ad clicks only (`'ads'`) or on other links as well (`'any'`). Every key is a field this SDK\n * collects (`AdvertisingInfo`).\n *\n * The third column decides whether the id can say a click was paid when a `utm_source` names the\n * channel but no `utm_medium` says how (a Reddit ad tagged `utm_source=reddit` and nothing more).\n * `fbclid` is `'any'`: Meta decorates every outbound link with it, organic posts, profile links\n * and shares included. `twclid` and `ko_click_id` are `'any'` until it is clear they never\n * decorate an organic link.\n *\n * Only URL parameters, never the cookies the tags keep beside them (`_fbc`, `_gcl_aw`, … — every\n * underscore key of `AdvertisingInfo`): a click cookie lives up to 90 days after the click, so\n * counting it named every later visit — typed in, from search, from an email — a new ad click.\n */\nexport const CLICK_ID_CHANNELS = [\n ['fbclid', 'meta', 'any'],\n ['gclid', 'google', 'ads'],\n ['gbraid', 'google', 'ads'],\n ['wbraid', 'google', 'ads'],\n ['gad_source', 'google', 'ads'],\n ['gad_campaignid', 'google', 'ads'],\n ['dclid', 'dv360', 'ads'],\n ['msclkid', 'microsoft', 'ads'],\n ['ttclid', 'tiktok', 'ads'],\n ['rdt_cid', 'reddit', 'ads'],\n ['li_fat_id', 'linkedin', 'ads'],\n ['ScCid', 'snapchat', 'ads'],\n ['twclid', 'x', 'any'],\n ['ko_click_id', 'kakao', 'any'],\n ['yclid', 'yandex', 'ads'],\n ['oppref', 'chatgpt', 'ads'],\n ['epik', 'pinterest', 'ads'],\n] as const satisfies readonly (readonly [keyof AdvertisingInfo, Channel, 'ads' | 'any'])[];\n\n/**\n * A landing page reserved for one channel's ads, for ads that lost their parameters:\n * `/lp/<channel>`, with or without host, query and hash; the capture is the channel. A product\n * that names its landing pages differently replaces this.\n */\nexport const AD_LANDING_PAGE = /^(?:https?:\\/\\/[^/]+)?\\/lp\\/([a-z]+)(?:[/?#]|$)/;\n\nexport type ReferrerMedium = 'organic' | 'social' | 'video' | 'ai';\n\n/**\n * Referrer hosts that name a channel, as regular expressions over the lower-cased host,\n * with the medium GA4 gives that kind of site. Anything else that refers is `referral`, kept as\n * its host. A country TLD is one or two labels (`google.com`, `google.co.uk`, `google.com.hk`),\n * never an open tail, so `google.com.evil.io` is not Google. In match order: the AI assistants\n * first, because `gemini.google.com`, `copilot.microsoft.com` and `yiyan.baidu.com` would\n * otherwise be Google, Microsoft and Baidu search.\n */\nexport const REFERRER_SITES = [\n ['chatgpt', 'ai', /(^|\\.)(chatgpt\\.com|chat\\.openai\\.com)$/],\n ['perplexity', 'ai', /(^|\\.)perplexity\\.ai$/],\n ['gemini', 'ai', /^gemini\\.google\\.com$/],\n ['claude', 'ai', /(^|\\.)claude\\.ai$/],\n ['copilot', 'ai', /^copilot\\.microsoft\\.com$|(^|\\.)copilot\\.com$/],\n ['grok', 'ai', /(^|\\.)(grok\\.com|x\\.ai)$/],\n ['deepseek', 'ai', /(^|\\.)deepseek\\.com$/],\n ['doubao', 'ai', /(^|\\.)doubao\\.com$/],\n ['kimi', 'ai', /(^|\\.)(kimi\\.com|kimi\\.moonshot\\.cn|moonshot\\.cn)$/],\n ['qwen', 'ai', /^tongyi\\.aliyun\\.com$|(^|\\.)(tongyi\\.com|qianwen\\.com|qwen\\.ai)$/],\n ['yuanbao', 'ai', /^yuanbao\\.tencent\\.com$/],\n ['ernie', 'ai', /^yiyan\\.baidu\\.com$|(^|\\.)ernie\\.baidu\\.com$/],\n ['zhipu', 'ai', /(^|\\.)(chatglm\\.cn|zhipuai\\.cn|bigmodel\\.cn)$/],\n ['google', 'organic', /(^|\\.)google\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['microsoft', 'organic', /(^|\\.)bing\\.com$/],\n ['yahoo', 'organic', /(^|\\.)yahoo\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['duckduckgo', 'organic', /(^|\\.)duckduckgo\\.com$/],\n ['baidu', 'organic', /(^|\\.)baidu\\.com$/],\n ['yandex', 'organic', /(^|\\.)yandex\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['ecosia', 'organic', /(^|\\.)ecosia\\.org$/],\n ['naver', 'organic', /(^|\\.)naver\\.com$/],\n ['brave', 'organic', /^search\\.brave\\.com$/],\n ['meta', 'social', /(^|\\.)(facebook\\.com|instagram\\.com|fb\\.com|threads\\.net)$/],\n ['x', 'social', /(^|\\.)(twitter\\.com|x\\.com|t\\.co)$/],\n ['linkedin', 'social', /(^|\\.)(linkedin\\.com|lnkd\\.in)$/],\n ['reddit', 'social', /(^|\\.)reddit\\.com$/],\n ['tiktok', 'social', /(^|\\.)tiktok\\.com$/],\n ['pinterest', 'social', /(^|\\.)pinterest\\.[a-z]{2,}(\\.[a-z]{2,3})?$/],\n ['snapchat', 'social', /(^|\\.)snapchat\\.com$/],\n ['youtube', 'video', /(^|\\.)(youtube\\.com|youtu\\.be)$/],\n] as const satisfies readonly (readonly [Channel, ReferrerMedium, RegExp])[];\n\n/**\n * Referrer hosts that are navigation, not acquisition, when a session happens to start on the way\n * back from them: the local dev host; the payment providers a visitor is bounced through; the\n * sign-in providers whose login lives on a host of its own (Google, Apple, Microsoft, WeChat,\n * Kakao, LINE, Naver, Yahoo, Twitch, X's OAuth 1.0a endpoint); and the hosted auth services a\n * product may sit behind (Auth0, Okta, Supabase, Firebase, Clerk). A product appends its own\n * hostnames — a session that starts from an internal link after the session timeout refers to\n * them. Matched before `REFERRER_SITES`, so `nid.naver.com` and `login.yahoo.com` are not read as\n * a search. Providers whose login shares the host with their content — Facebook, X's OAuth 2.0,\n * LinkedIn, GitHub, Discord — cannot be told apart by host (referrer policies strip the path) and\n * are left as the referrers they usually are. Regular expressions over the lower-cased host.\n */\nexport const REFERRERS_NOT_A_TOUCH = [\n /^localhost(:|$)/,\n // payment\n /(^|\\.)stripe\\.com$/,\n /^pay\\.google\\.com$/,\n /(^|\\.)paypal\\.com$/,\n /(^|\\.)alipay\\.com$/,\n /(^|\\.)paddle\\.com$/,\n /(^|\\.)lemonsqueezy\\.com$/,\n // app stores\n /^apps\\.apple\\.com$/,\n /^play\\.google\\.com$/,\n // sign-in providers with a login host of their own\n /^accounts\\.google\\.com$/,\n /^appleid\\.apple\\.com$/,\n /^login\\.(microsoftonline|live)\\.com$/,\n /^open\\.weixin\\.qq\\.com$/,\n /^(kauth|accounts)\\.kakao\\.com$/,\n /^access\\.line\\.me$/,\n /^nid\\.naver\\.com$/,\n /^login\\.yahoo\\.com$/,\n /^id\\.twitch\\.tv$/,\n /^api\\.twitter\\.com$/,\n // hosted auth services\n /(^|\\.)auth0\\.com$/,\n /(^|\\.)okta\\.com$/,\n /(^|\\.)supabase\\.co$/,\n /(^|\\.)firebaseapp\\.com$/,\n /(^|\\.)clerk\\.accounts\\.dev$/,\n] as const;\n\n/** GA4's paid mediums — cpc, cpm, ppc, retargeting, paid_social, … — plus Performance Max. */\nexport const PAID_MEDIUM = /^(.*cp.*|ppc|retargeting|paid.*|pmax|performance_max)$/;\n/**\n * Meta's `{{placement}}` values, lower-cased, which an ad URL template of `utm_medium={{placement}}`\n * puts in the medium. Only an ad carries one — an organic post arrives as a referrer, with no utm\n * at all — so a medium in this list is a paid Meta click whatever the source says (a template with\n * `utm_source=meta-websitekeyinfo` was seen). Listed one by one rather than matched by prefix, so\n * that a hand-tagged `instagram_stories` post cannot be mistaken for an ad only because of how it\n * is spelled … which means a placement Meta adds later shows up as unassigned until it is added\n * here. Meta's documented values plus the ones seen in the wild (`facebook_mobile_reels`,\n * `facebook_notification`, `an`), and the literal macro for a template that did not expand it.\n */\nexport const META_PLACEMENTS = [\n 'facebook_desktop_feed',\n 'facebook_mobile_feed',\n 'facebook_mobile_reels',\n 'facebook_reels',\n 'facebook_reels_overlay',\n 'facebook_right_column',\n 'facebook_marketplace',\n 'facebook_video_feeds',\n 'facebook_stories',\n 'facebook_search',\n 'facebook_instream_video',\n 'facebook_instant_article',\n 'facebook_groups_feed',\n 'facebook_profile_feed',\n 'facebook_notification',\n 'facebook_notifications',\n 'facebook_business_explore',\n 'instagram_feed',\n 'instagram_stories',\n 'instagram_reels',\n 'instagram_reels_overlay',\n 'instagram_explore',\n 'instagram_explore_home',\n 'instagram_search',\n 'instagram_shop',\n 'instagram_profile_feed',\n 'instagram_profile_reels',\n 'instagram_igtv',\n 'messenger_inbox',\n 'messenger_stories',\n 'messenger_sponsored_messages',\n 'threads_feed',\n 'whatsapp_status',\n 'audience_network_classic',\n 'audience_network_rewarded_video',\n 'audience_network_native_banner_and_interstitial',\n 'an',\n 'others',\n /** the macro itself, when Meta did not expand it (written in a field it does not substitute, or a\n * preview click): still only ever produced by a Meta ad URL, so still a paid Meta click */\n '{{placement}}',\n] as const;\nexport const DISPLAY_MEDIUMS = ['display', 'banner', 'expandable', 'interstitial'] as const;\n/** GA4's email spellings, as a source or a medium. */\nexport const EMAIL_MEDIUMS = ['email', 'e-mail', 'e_mail', 'e mail'] as const;\n/** A medium that says email or newsletter anywhere in it: `outbound email`, `cold_email`, `newsletter`, and GA4's four. */\nexport const EMAIL_MEDIUM = /(^|[^a-z])(e[-_ ]?mail|newsletter)([^a-z]|$)/;\n\n/** Where a touch came from: a tracked session, or a report by the user or staff. */\nexport const TOUCH_SOURCES = ['tracked', 'reported'] as const;\nexport type TouchSource = (typeof TOUCH_SOURCES)[number];\n\n/**\n * How a reported touch was learned:\n * - survey: the user answered \"how did you hear about us\" (onboarding, checkout)\n * - promo_code: the user redeemed a code that belongs to a channel\n * - phone: a call came in on a channel's dedicated number, marked by staff or the phone system\n * - manual: staff recorded what a customer told them\n */\nexport const REPORTED_TOUCH_KINDS = ['survey', 'promo_code', 'phone', 'manual'] as const;\nexport type ReportedTouchKind = (typeof REPORTED_TOUCH_KINDS)[number];\n\n/**\n * How much a touch is worth against another when attribution has to choose between sessions;\n * lower wins, and the latest wins among equals. Stored on the session when it is classified, so\n * renumbering means reclassifying.\n *\n * This ranks touches against each other. Within one session the rules that name its channel have\n * their own order — `utm_source`, then a click id, then the ad landing page, then the referrer —\n * and the first that says something wins; the session's priority is then the tier of that rule.\n * So two sessions tagged with utm and gclid respectively are equal here (both `campaign`) and the\n * later one is credited, while one session carrying both is named by its utm.\n */\nexport const TOUCH_PRIORITY = {\n /** utm, click id, ad landing page: someone tagged that link (in that order within a session) */\n campaign: 1,\n /** a reported touch staff stand behind: a call on a channel's number, a recorded conversation */\n verified: 1,\n /** the browser said where the visitor came from; nobody chose it */\n referrer: 2,\n /** a reported touch that is the user's claim: a survey answer, a code that may have been shared */\n claimed: 3,\n} as const satisfies Record<string, number>;\n\n/** Which reported kinds are staff facts and which are the user's claims. */\nexport const REPORTED_TOUCH_PRIORITY = {\n phone: TOUCH_PRIORITY.verified,\n manual: TOUCH_PRIORITY.verified,\n survey: TOUCH_PRIORITY.claimed,\n promo_code: TOUCH_PRIORITY.claimed,\n} as const satisfies Record<ReportedTouchKind, number>;\n"],"mappings":"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"}
|
package/dist/click-id/index.cjs
CHANGED
|
@@ -40,16 +40,19 @@ const GCL_AW_COOKIE = "_gcl_aw";
|
|
|
40
40
|
const GCL_GB_COOKIE = "_gcl_gb";
|
|
41
41
|
const UET_MSCLKID_COOKIE = "_uetmsclkid";
|
|
42
42
|
/**
|
|
43
|
-
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's
|
|
44
|
+
* Parameter Builder adds to the values it writes ("an appendix field at the end of each
|
|
45
|
+
* parameter"); it is not part of the click id, so `fbclid` is the fourth segment alone and the
|
|
46
|
+
* value is passed on as `raw`, unchanged. Returns undefined for anything malformed.
|
|
47
|
+
* ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library
|
|
46
48
|
*/
|
|
47
49
|
function parseFbc(raw, now = Date.now()) {
|
|
48
50
|
if (!raw) return void 0;
|
|
49
51
|
const parts = raw.split(".");
|
|
50
|
-
if (parts.length < 4 || parts[0] !== "fb") return void 0;
|
|
52
|
+
if (parts.length < 4 || parts.length > 5 || parts[0] !== "fb") return void 0;
|
|
53
|
+
if (parts.length === 5 && !parts[4]) return void 0;
|
|
51
54
|
const creationTime = Number(parts[2]);
|
|
52
|
-
const fbclid = parts
|
|
55
|
+
const fbclid = parts[3];
|
|
53
56
|
if (!fbclid) return void 0;
|
|
54
57
|
if (!Number.isFinite(creationTime)) return void 0;
|
|
55
58
|
if (creationTime < 0xe8d4a51000 || creationTime > now + CLOCK_SKEW_MS) return void 0;
|
|
@@ -66,6 +69,15 @@ function parseFbc(raw, now = Date.now()) {
|
|
|
66
69
|
function formatFbc(fbclid, now, subdomainIndex = 1) {
|
|
67
70
|
return `fb.${subdomainIndex}.${now}.${fbclid}`;
|
|
68
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or
|
|
74
|
+
* `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on
|
|
75
|
+
* Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.
|
|
76
|
+
* ref: https://support.google.com/sa360/answer/7342044
|
|
77
|
+
*/
|
|
78
|
+
function isGoogleAdsGclid(gclsrc) {
|
|
79
|
+
return !gclsrc || gclsrc === "aw.ds";
|
|
80
|
+
}
|
|
69
81
|
const GCL_CLICK_ID = /^[\w-]+$/;
|
|
70
82
|
/**
|
|
71
83
|
* Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp
|
|
@@ -96,6 +108,10 @@ function formatGcl(clickId, now) {
|
|
|
96
108
|
}
|
|
97
109
|
const UET_MSCLKID_PREFIX = "_uet";
|
|
98
110
|
const MSCLKID = /^[0-9a-f]{32}$/i;
|
|
111
|
+
/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */
|
|
112
|
+
function isMsclkid(value) {
|
|
113
|
+
return !!value && MSCLKID.test(value);
|
|
114
|
+
}
|
|
99
115
|
/**
|
|
100
116
|
* Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined
|
|
101
117
|
* for anything bat.js itself would not read back.
|
|
@@ -178,7 +194,7 @@ function resolveClickIdCookies(input) {
|
|
|
178
194
|
const gclsrc = params.get("gclsrc") || void 0;
|
|
179
195
|
const google = [{
|
|
180
196
|
cookie: GCL_AW_COOKIE,
|
|
181
|
-
urlClickId:
|
|
197
|
+
urlClickId: isGoogleAdsGclid(gclsrc) ? params.get("gclid") || void 0 : void 0,
|
|
182
198
|
assign: (value) => result.gclid = value
|
|
183
199
|
}, {
|
|
184
200
|
cookie: GCL_GB_COOKIE,
|
|
@@ -229,6 +245,8 @@ exports.formatFbc = formatFbc;
|
|
|
229
245
|
exports.formatGcl = formatGcl;
|
|
230
246
|
exports.formatMsclkid = formatMsclkid;
|
|
231
247
|
exports.formatUetMsclkid = formatUetMsclkid;
|
|
248
|
+
exports.isGoogleAdsGclid = isGoogleAdsGclid;
|
|
249
|
+
exports.isMsclkid = isMsclkid;
|
|
232
250
|
exports.parseFbc = parseFbc;
|
|
233
251
|
exports.parseGcl = parseGcl;
|
|
234
252
|
exports.parseUetMsclkid = parseUetMsclkid;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["parseCookie","cookie","stringifySetCookie"],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`, `_rdt_cid`,\n * `_uetmsclkid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * The Google pair follows the same architecture Google itself ships for server containers —\n * sGTM's Conversion Linker sets FPGCLAW via Set-Cookie for 90 days — and the sGTM ecosystem\n * productized for gtag's own cookies (stape Cookie Keeper re-issues `_gcl_*` over HTTP; vendor\n * case studies report 5–20% of Google Ads conversions recovered, up to +44% on the Safari\n * slice). One deployment caveat from WebKit's rules: the HTTP-cookie exemption holds only for\n * responses from the site's own infrastructure — a CNAME or third-party-IP host (a tagging\n * subdomain on someone else's cloud) gets its Set-Cookie silently capped to 7 days. This module\n * runs on the host's own document response, which is exactly the exempt case.\n *\n * references:\n * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n * - https://webkit.org/tracking-prevention/ (7d JS cookies, 24h on ad-decorated landings,\n * CNAME/third-party-IP HTTP cookies capped to 7d, same-origin HTTP cookies exempt)\n * - https://www.simoahava.com/analytics/google-ads-server-side-tagging-google-tag-manager/\n * (Conversion Linker → FPGCLAW via Set-Cookie, Google's own server-side equivalent)\n * - https://stape.io/helpdesk/documentation/cookie-keeper-power-up (the productized re-issue)\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// bat.js's own expiry for _uetmsclkid (`msClkIdExpirationTime`), the Microsoft Ads click window.\nconst MSCLKID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// gtag's own expiry for _gcl_* — 90 days, the length of a Google Ads click's upload window.\nconst GCL_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\nexport const GCL_AW_COOKIE = '_gcl_aw';\nexport const GCL_GB_COOKIE = '_gcl_gb';\nexport const UET_MSCLKID_COOKIE = '_uetmsclkid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>`.\n * Returns undefined for anything malformed — a fbclid never contains a dot, but joining the tail\n * back together keeps us forward-compatible if that ever changes.\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts[0] !== 'fb') return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts.slice(3).join('.');\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ParsedGcl = { raw: string; creationTime: number; clickId: string };\n\n// gtag's own value validator, extracted from the live gtag.js bundle (2026-09): its parser\n// accepts version \"GCL\" or \"1\", a /^\\d+$/ seconds timestamp, and a click id matching this.\n// Re-verify against gtag.js before changing anything here — the format is gtag's, not ours.\nconst GCL_CLICK_ID = /^[\\w-]+$/;\n\n/**\n * Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp\n * is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and\n * validates the segments exactly this way (`/^\\d+$/` timestamp, `/^[\\w-]+$/` click id); any\n * labels tail is preserved through `raw` so a re-issue stays byte-identical.\n */\nexport function parseGcl(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedGcl | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 3) return undefined;\n if (parts[0] !== 'GCL' && parts[0] !== '1') return undefined;\n const seconds = parts[1];\n const clickId = parts[2];\n if (seconds === undefined || !/^\\d+$/.test(seconds)) return undefined;\n if (clickId === undefined || !GCL_CLICK_ID.test(clickId)) return undefined;\n const creationTime = Number(seconds) * 1000;\n // Reject millisecond-precision or future-dated values as malformed.\n if (creationTime >= 1e15 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n return { raw, creationTime, clickId };\n}\n\n/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */\nexport function formatGcl(clickId: string, now: number): string {\n return `GCL.${Math.floor(now / 1000)}.${clickId}`;\n}\n\n// bat.js writes the click id as `_uet<msclkid>` and reads it back only when what follows the\n// prefix is at most 32 characters (`msClkIdCookieValuePrefix`, `lengthMsClkId`); a msclkid is a\n// 32-hex-digit UUID without dashes. Re-verify against bat.js before changing anything here —\n// the format is UET's, not ours.\nconst UET_MSCLKID_PREFIX = '_uet';\nconst MSCLKID = /^[0-9a-f]{32}$/i;\n\n/**\n * Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined\n * for anything bat.js itself would not read back.\n */\nexport function parseUetMsclkid(raw: string | undefined | null): string | undefined {\n if (!raw || !raw.startsWith(UET_MSCLKID_PREFIX)) return undefined;\n const clickId = raw.slice(UET_MSCLKID_PREFIX.length);\n return MSCLKID.test(clickId) ? clickId.toLowerCase() : undefined;\n}\n\n/** Build a `_uetmsclkid` value in bat.js's own format. */\nexport function formatUetMsclkid(clickId: string): string {\n return `${UET_MSCLKID_PREFIX}${clickId.toLowerCase()}`;\n}\n\n/**\n * The click id as the Conversions API wants it: a dashed, lowercase UUID. The URL and the cookie\n * carry the 32-hex form; a value in any other shape is returned untouched for the API to judge.\n */\nexport function formatMsclkid(clickId: string): string {\n const id = clickId.trim().toLowerCase();\n if (!MSCLKID.test(id)) return id;\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n /** The resolved Google Ads click id (from the URL or a still-valid `_gcl_aw`). */\n gclid?: string;\n /** The resolved iOS web-to-app click id (from the URL or a still-valid `_gcl_gb`). */\n wbraid?: string;\n /** The resolved Microsoft Ads click id (from the URL or `_uetmsclkid`), 32-hex form. */\n msclkid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Google Ads _gcl_aw / _gcl_gb ---\n // The same first-party HTTP persistence Google's own server-side Conversion Linker performs\n // (it sets FPGCLAW via Set-Cookie for 90 days), applied to gtag's cookies the way the sGTM\n // ecosystem does (stape Cookie Keeper re-issues _gcl_* over HTTP). Ownership is shared with\n // gtag, so the rules are stricter than for _fbc: a value this module cannot parse is left\n // untouched — never rewritten, never deleted — and a re-issue is byte-identical (labels tail\n // included) at the window's REMAINING lifetime, so the 90-day click window never slides.\n const gclsrc = params.get('gclsrc') || undefined;\n const google = [\n {\n cookie: GCL_AW_COOKIE,\n // gtag's own gating, verbatim: a gclid enters _gcl_aw only when gclsrc is absent or\n // 'aw.ds' — 'ds'/'3p.ds' clicks are Search Ads 360's and belong to _gcl_dc.\n urlClickId: !gclsrc || gclsrc === 'aw.ds' ? params.get('gclid') || undefined : undefined,\n assign: (value: string) => (result.gclid = value),\n },\n {\n cookie: GCL_GB_COOKIE,\n urlClickId: params.get('wbraid') || undefined,\n assign: (value: string) => (result.wbraid = value),\n },\n ];\n for (const { cookie, urlClickId, assign } of google) {\n const existing = parseGcl(jar[cookie], now);\n if (urlClickId && GCL_CLICK_ID.test(urlClickId) && urlClickId !== existing?.clickId) {\n // A new click always wins and opens a fresh 90-day window, in gtag's exact format.\n set(cookie, formatGcl(urlClickId, now), GCL_TTL_MS);\n assign(urlClickId);\n } else if (existing) {\n const remainingMs = existing.creationTime + GCL_TTL_MS - now;\n if (remainingMs > 0) {\n assign(existing.clickId);\n if (refresh) set(cookie, existing.raw, remainingMs);\n }\n // Expired: not returned and not re-issued, but gtag owns the deletion.\n }\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n // --- Microsoft Ads _uetmsclkid ---\n // The UET tag's own cookie, in its own format, so the tag and this module read each other's\n // writes. Microsoft's ITP answer is this first-party cookie (plus a localStorage backup), but\n // bat.js writes it through `document.cookie`, which Safari caps at 7 days — 24 hours on a\n // landing page decorated by a classified domain, and a bing.com ad click is exactly that.\n // Persisting it here over HTTP on the document response restores the 90-day window.\n //\n // The re-issue (default on, see `refresh`) is the same self-heal as for `_fbc`, and it matters\n // more here: bat.js rewrites this cookie on EVERY page load, so without the re-issue the HTTP\n // copy is downgraded to a JS cookie on the very next page. There is no embedded timestamp to\n // compute a remaining lifetime from, so it is re-issued at a fresh 90 days — which is exactly\n // what bat.js itself does on each page (`msClkIdExpirationTime` from now), so the window\n // slides no more than the tag already slides it. A value this module cannot parse belongs to\n // the tag: never rewritten, never deleted.\n const urlMsclkid = params.get('msclkid')?.trim();\n const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);\n if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {\n set(UET_MSCLKID_COOKIE, formatUetMsclkid(urlMsclkid), MSCLKID_TTL_MS);\n result.msclkid = urlMsclkid.toLowerCase();\n } else if (existingMsclkid) {\n result.msclkid = existingMsclkid;\n if (refresh) set(UET_MSCLKID_COOKIE, formatUetMsclkid(existingMsclkid), MSCLKID_TTL_MS);\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["parseCookie","cookie","stringifySetCookie"],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`, `_rdt_cid`,\n * `_uetmsclkid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * The Google pair follows the same architecture Google itself ships for server containers —\n * sGTM's Conversion Linker sets FPGCLAW via Set-Cookie for 90 days — and the sGTM ecosystem\n * productized for gtag's own cookies (stape Cookie Keeper re-issues `_gcl_*` over HTTP; vendor\n * case studies report 5–20% of Google Ads conversions recovered, up to +44% on the Safari\n * slice). One deployment caveat from WebKit's rules: the HTTP-cookie exemption holds only for\n * responses from the site's own infrastructure — a CNAME or third-party-IP host (a tagging\n * subdomain on someone else's cloud) gets its Set-Cookie silently capped to 7 days. This module\n * runs on the host's own document response, which is exactly the exempt case.\n *\n * references:\n * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n * - https://webkit.org/tracking-prevention/ (7d JS cookies, 24h on ad-decorated landings,\n * CNAME/third-party-IP HTTP cookies capped to 7d, same-origin HTTP cookies exempt)\n * - https://www.simoahava.com/analytics/google-ads-server-side-tagging-google-tag-manager/\n * (Conversion Linker → FPGCLAW via Set-Cookie, Google's own server-side equivalent)\n * - https://stape.io/helpdesk/documentation/cookie-keeper-power-up (the productized re-issue)\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// bat.js's own expiry for _uetmsclkid (`msClkIdExpirationTime`), the Microsoft Ads click window.\nconst MSCLKID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// gtag's own expiry for _gcl_* — 90 days, the length of a Google Ads click's upload window.\nconst GCL_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\nexport const GCL_AW_COOKIE = '_gcl_aw';\nexport const GCL_GB_COOKIE = '_gcl_gb';\nexport const UET_MSCLKID_COOKIE = '_uetmsclkid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's\n * Parameter Builder adds to the values it writes (\"an appendix field at the end of each\n * parameter\"); it is not part of the click id, so `fbclid` is the fourth segment alone and the\n * value is passed on as `raw`, unchanged. Returns undefined for anything malformed.\n * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts.length > 5 || parts[0] !== 'fb') return undefined;\n if (parts.length === 5 && !parts[4]) return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts[3];\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ParsedGcl = { raw: string; creationTime: number; clickId: string };\n\n/**\n * Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or\n * `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on\n * Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.\n * ref: https://support.google.com/sa360/answer/7342044\n */\nexport function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean {\n return !gclsrc || gclsrc === 'aw.ds';\n}\n\n// gtag's own value validator, extracted from the live gtag.js bundle (2026-09): its parser\n// accepts version \"GCL\" or \"1\", a /^\\d+$/ seconds timestamp, and a click id matching this.\n// Re-verify against gtag.js before changing anything here — the format is gtag's, not ours.\nconst GCL_CLICK_ID = /^[\\w-]+$/;\n\n/**\n * Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp\n * is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and\n * validates the segments exactly this way (`/^\\d+$/` timestamp, `/^[\\w-]+$/` click id); any\n * labels tail is preserved through `raw` so a re-issue stays byte-identical.\n */\nexport function parseGcl(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedGcl | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 3) return undefined;\n if (parts[0] !== 'GCL' && parts[0] !== '1') return undefined;\n const seconds = parts[1];\n const clickId = parts[2];\n if (seconds === undefined || !/^\\d+$/.test(seconds)) return undefined;\n if (clickId === undefined || !GCL_CLICK_ID.test(clickId)) return undefined;\n const creationTime = Number(seconds) * 1000;\n // Reject millisecond-precision or future-dated values as malformed.\n if (creationTime >= 1e15 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n return { raw, creationTime, clickId };\n}\n\n/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */\nexport function formatGcl(clickId: string, now: number): string {\n return `GCL.${Math.floor(now / 1000)}.${clickId}`;\n}\n\n// bat.js writes the click id as `_uet<msclkid>` and reads it back only when what follows the\n// prefix is at most 32 characters (`msClkIdCookieValuePrefix`, `lengthMsClkId`); a msclkid is a\n// 32-hex-digit UUID without dashes. Re-verify against bat.js before changing anything here —\n// the format is UET's, not ours.\nconst UET_MSCLKID_PREFIX = '_uet';\nconst MSCLKID = /^[0-9a-f]{32}$/i;\n\n/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */\nexport function isMsclkid(value: string | null | undefined): value is string {\n return !!value && MSCLKID.test(value);\n}\n\n/**\n * Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined\n * for anything bat.js itself would not read back.\n */\nexport function parseUetMsclkid(raw: string | undefined | null): string | undefined {\n if (!raw || !raw.startsWith(UET_MSCLKID_PREFIX)) return undefined;\n const clickId = raw.slice(UET_MSCLKID_PREFIX.length);\n return MSCLKID.test(clickId) ? clickId.toLowerCase() : undefined;\n}\n\n/** Build a `_uetmsclkid` value in bat.js's own format. */\nexport function formatUetMsclkid(clickId: string): string {\n return `${UET_MSCLKID_PREFIX}${clickId.toLowerCase()}`;\n}\n\n/**\n * The click id as the Conversions API wants it: a dashed, lowercase UUID. The URL and the cookie\n * carry the 32-hex form; a value in any other shape is returned untouched for the API to judge.\n */\nexport function formatMsclkid(clickId: string): string {\n const id = clickId.trim().toLowerCase();\n if (!MSCLKID.test(id)) return id;\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n /** The resolved Google Ads click id (from the URL or a still-valid `_gcl_aw`). */\n gclid?: string;\n /** The resolved iOS web-to-app click id (from the URL or a still-valid `_gcl_gb`). */\n wbraid?: string;\n /** The resolved Microsoft Ads click id (from the URL or `_uetmsclkid`), 32-hex form. */\n msclkid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Google Ads _gcl_aw / _gcl_gb ---\n // The same first-party HTTP persistence Google's own server-side Conversion Linker performs\n // (it sets FPGCLAW via Set-Cookie for 90 days), applied to gtag's cookies the way the sGTM\n // ecosystem does (stape Cookie Keeper re-issues _gcl_* over HTTP). Ownership is shared with\n // gtag, so the rules are stricter than for _fbc: a value this module cannot parse is left\n // untouched — never rewritten, never deleted — and a re-issue is byte-identical (labels tail\n // included) at the window's REMAINING lifetime, so the 90-day click window never slides.\n const gclsrc = params.get('gclsrc') || undefined;\n const google = [\n {\n cookie: GCL_AW_COOKIE,\n // gtag's own gating (`isGoogleAdsGclid`): a gclid enters _gcl_aw only when gclsrc is\n // absent or 'aw.ds' — 'ds'/'3p.ds' clicks are Search Ads 360's and belong to _gcl_dc.\n urlClickId: isGoogleAdsGclid(gclsrc) ? params.get('gclid') || undefined : undefined,\n assign: (value: string) => (result.gclid = value),\n },\n {\n cookie: GCL_GB_COOKIE,\n urlClickId: params.get('wbraid') || undefined,\n assign: (value: string) => (result.wbraid = value),\n },\n ];\n for (const { cookie, urlClickId, assign } of google) {\n const existing = parseGcl(jar[cookie], now);\n if (urlClickId && GCL_CLICK_ID.test(urlClickId) && urlClickId !== existing?.clickId) {\n // A new click always wins and opens a fresh 90-day window, in gtag's exact format.\n set(cookie, formatGcl(urlClickId, now), GCL_TTL_MS);\n assign(urlClickId);\n } else if (existing) {\n const remainingMs = existing.creationTime + GCL_TTL_MS - now;\n if (remainingMs > 0) {\n assign(existing.clickId);\n if (refresh) set(cookie, existing.raw, remainingMs);\n }\n // Expired: not returned and not re-issued, but gtag owns the deletion.\n }\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n // --- Microsoft Ads _uetmsclkid ---\n // The UET tag's own cookie, in its own format, so the tag and this module read each other's\n // writes. Microsoft's ITP answer is this first-party cookie (plus a localStorage backup), but\n // bat.js writes it through `document.cookie`, which Safari caps at 7 days — 24 hours on a\n // landing page decorated by a classified domain, and a bing.com ad click is exactly that.\n // Persisting it here over HTTP on the document response restores the 90-day window.\n //\n // The re-issue (default on, see `refresh`) is the same self-heal as for `_fbc`, and it matters\n // more here: bat.js rewrites this cookie on EVERY page load, so without the re-issue the HTTP\n // copy is downgraded to a JS cookie on the very next page. There is no embedded timestamp to\n // compute a remaining lifetime from, so it is re-issued at a fresh 90 days — which is exactly\n // what bat.js itself does on each page (`msClkIdExpirationTime` from now), so the window\n // slides no more than the tag already slides it. A value this module cannot parse belongs to\n // the tag: never rewritten, never deleted.\n const urlMsclkid = params.get('msclkid')?.trim();\n const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);\n if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {\n set(UET_MSCLKID_COOKIE, formatUetMsclkid(urlMsclkid), MSCLKID_TTL_MS);\n result.msclkid = urlMsclkid.toLowerCase();\n } else if (existingMsclkid) {\n result.msclkid = existingMsclkid;\n if (refresh) set(UET_MSCLKID_COOKIE, formatUetMsclkid(existingMsclkid), MSCLKID_TTL_MS);\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":"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"}
|
|
@@ -11,9 +11,11 @@ export type ParsedFbc = {
|
|
|
11
11
|
fbclid: string;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's
|
|
15
|
+
* Parameter Builder adds to the values it writes ("an appendix field at the end of each
|
|
16
|
+
* parameter"); it is not part of the click id, so `fbclid` is the fourth segment alone and the
|
|
17
|
+
* value is passed on as `raw`, unchanged. Returns undefined for anything malformed.
|
|
18
|
+
* ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library
|
|
17
19
|
*/
|
|
18
20
|
export declare function parseFbc(raw: string | undefined | null, now?: number): ParsedFbc | undefined;
|
|
19
21
|
/**
|
|
@@ -26,6 +28,13 @@ export type ParsedGcl = {
|
|
|
26
28
|
creationTime: number;
|
|
27
29
|
clickId: string;
|
|
28
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or
|
|
33
|
+
* `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on
|
|
34
|
+
* Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.
|
|
35
|
+
* ref: https://support.google.com/sa360/answer/7342044
|
|
36
|
+
*/
|
|
37
|
+
export declare function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean;
|
|
29
38
|
/**
|
|
30
39
|
* Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp
|
|
31
40
|
* is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and
|
|
@@ -35,6 +44,8 @@ export type ParsedGcl = {
|
|
|
35
44
|
export declare function parseGcl(raw: string | undefined | null, now?: number): ParsedGcl | undefined;
|
|
36
45
|
/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */
|
|
37
46
|
export declare function formatGcl(clickId: string, now: number): string;
|
|
47
|
+
/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */
|
|
48
|
+
export declare function isMsclkid(value: string | null | undefined): value is string;
|
|
38
49
|
/**
|
|
39
50
|
* Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined
|
|
40
51
|
* for anything bat.js itself would not read back.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;qBA2Ca;qBACA;qBACA;qBACA;qBACA;YAED;EAAc;EAAa;EAAsB
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;qBA2Ca;qBACA;qBACA;qBACA;qBACA;YAED;EAAc;EAAa;EAAsB;;;;;;;;;wBAS7C,SACd,gCACA,eACC;;;;;wBAoBa,UAAU,gBAAgB,aAAa;YAI3C;EAAc;EAAa;EAAsB;;;;;;;;wBAQ7C,iBAAiB;;;;;;;wBAejB,SACd,gCACA,eACC;;wBAgBa,UAAU,iBAAiB;;wBAY3B,UAAU,mCAAmC;;;;;wBAQ7C,gBAAgB;;wBAOhB,iBAAiB;;;;;wBAQjB,cAAc;YAMlB;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;;EAeA;;YAGU;;;;;EAKV,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;wBAuBc,sBACd,OAAO,6BACN;;wBAoHa,mBAAmB,SAAS"}
|
|
@@ -11,9 +11,11 @@ export type ParsedFbc = {
|
|
|
11
11
|
fbclid: string;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's
|
|
15
|
+
* Parameter Builder adds to the values it writes ("an appendix field at the end of each
|
|
16
|
+
* parameter"); it is not part of the click id, so `fbclid` is the fourth segment alone and the
|
|
17
|
+
* value is passed on as `raw`, unchanged. Returns undefined for anything malformed.
|
|
18
|
+
* ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library
|
|
17
19
|
*/
|
|
18
20
|
export declare function parseFbc(raw: string | undefined | null, now?: number): ParsedFbc | undefined;
|
|
19
21
|
/**
|
|
@@ -26,6 +28,13 @@ export type ParsedGcl = {
|
|
|
26
28
|
creationTime: number;
|
|
27
29
|
clickId: string;
|
|
28
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or
|
|
33
|
+
* `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on
|
|
34
|
+
* Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.
|
|
35
|
+
* ref: https://support.google.com/sa360/answer/7342044
|
|
36
|
+
*/
|
|
37
|
+
export declare function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean;
|
|
29
38
|
/**
|
|
30
39
|
* Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp
|
|
31
40
|
* is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and
|
|
@@ -35,6 +44,8 @@ export type ParsedGcl = {
|
|
|
35
44
|
export declare function parseGcl(raw: string | undefined | null, now?: number): ParsedGcl | undefined;
|
|
36
45
|
/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */
|
|
37
46
|
export declare function formatGcl(clickId: string, now: number): string;
|
|
47
|
+
/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */
|
|
48
|
+
export declare function isMsclkid(value: string | null | undefined): value is string;
|
|
38
49
|
/**
|
|
39
50
|
* Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined
|
|
40
51
|
* for anything bat.js itself would not read back.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;qBA2Ca;qBACA;qBACA;qBACA;qBACA;YAED;EAAc;EAAa;EAAsB
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;qBA2Ca;qBACA;qBACA;qBACA;qBACA;YAED;EAAc;EAAa;EAAsB;;;;;;;;;wBAS7C,SACd,gCACA,eACC;;;;;wBAoBa,UAAU,gBAAgB,aAAa;YAI3C;EAAc;EAAa;EAAsB;;;;;;;;wBAQ7C,iBAAiB;;;;;;;wBAejB,SACd,gCACA,eACC;;wBAgBa,UAAU,iBAAiB;;wBAY3B,UAAU,mCAAmC;;;;;wBAQ7C,gBAAgB;;wBAOhB,iBAAiB;;;;;wBAQjB,cAAc;YAMlB;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;;EAeA;;YAGU;;;;;EAKV,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;wBAuBc,sBACd,OAAO,6BACN;;wBAoHa,mBAAmB,SAAS"}
|
package/dist/click-id/index.mjs
CHANGED
|
@@ -39,16 +39,19 @@ const GCL_AW_COOKIE = "_gcl_aw";
|
|
|
39
39
|
const GCL_GB_COOKIE = "_gcl_gb";
|
|
40
40
|
const UET_MSCLKID_COOKIE = "_uetmsclkid";
|
|
41
41
|
/**
|
|
42
|
-
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's
|
|
43
|
+
* Parameter Builder adds to the values it writes ("an appendix field at the end of each
|
|
44
|
+
* parameter"); it is not part of the click id, so `fbclid` is the fourth segment alone and the
|
|
45
|
+
* value is passed on as `raw`, unchanged. Returns undefined for anything malformed.
|
|
46
|
+
* ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library
|
|
45
47
|
*/
|
|
46
48
|
function parseFbc(raw, now = Date.now()) {
|
|
47
49
|
if (!raw) return void 0;
|
|
48
50
|
const parts = raw.split(".");
|
|
49
|
-
if (parts.length < 4 || parts[0] !== "fb") return void 0;
|
|
51
|
+
if (parts.length < 4 || parts.length > 5 || parts[0] !== "fb") return void 0;
|
|
52
|
+
if (parts.length === 5 && !parts[4]) return void 0;
|
|
50
53
|
const creationTime = Number(parts[2]);
|
|
51
|
-
const fbclid = parts
|
|
54
|
+
const fbclid = parts[3];
|
|
52
55
|
if (!fbclid) return void 0;
|
|
53
56
|
if (!Number.isFinite(creationTime)) return void 0;
|
|
54
57
|
if (creationTime < 0xe8d4a51000 || creationTime > now + CLOCK_SKEW_MS) return void 0;
|
|
@@ -65,6 +68,15 @@ function parseFbc(raw, now = Date.now()) {
|
|
|
65
68
|
function formatFbc(fbclid, now, subdomainIndex = 1) {
|
|
66
69
|
return `fb.${subdomainIndex}.${now}.${fbclid}`;
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or
|
|
73
|
+
* `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on
|
|
74
|
+
* Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.
|
|
75
|
+
* ref: https://support.google.com/sa360/answer/7342044
|
|
76
|
+
*/
|
|
77
|
+
function isGoogleAdsGclid(gclsrc) {
|
|
78
|
+
return !gclsrc || gclsrc === "aw.ds";
|
|
79
|
+
}
|
|
68
80
|
const GCL_CLICK_ID = /^[\w-]+$/;
|
|
69
81
|
/**
|
|
70
82
|
* Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp
|
|
@@ -95,6 +107,10 @@ function formatGcl(clickId, now) {
|
|
|
95
107
|
}
|
|
96
108
|
const UET_MSCLKID_PREFIX = "_uet";
|
|
97
109
|
const MSCLKID = /^[0-9a-f]{32}$/i;
|
|
110
|
+
/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */
|
|
111
|
+
function isMsclkid(value) {
|
|
112
|
+
return !!value && MSCLKID.test(value);
|
|
113
|
+
}
|
|
98
114
|
/**
|
|
99
115
|
* Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined
|
|
100
116
|
* for anything bat.js itself would not read back.
|
|
@@ -177,7 +193,7 @@ function resolveClickIdCookies(input) {
|
|
|
177
193
|
const gclsrc = params.get("gclsrc") || void 0;
|
|
178
194
|
const google = [{
|
|
179
195
|
cookie: GCL_AW_COOKIE,
|
|
180
|
-
urlClickId:
|
|
196
|
+
urlClickId: isGoogleAdsGclid(gclsrc) ? params.get("gclid") || void 0 : void 0,
|
|
181
197
|
assign: (value) => result.gclid = value
|
|
182
198
|
}, {
|
|
183
199
|
cookie: GCL_GB_COOKIE,
|
|
@@ -219,6 +235,6 @@ function toSetCookieHeaders(cookies) {
|
|
|
219
235
|
return cookies.map((cookie) => stringifySetCookie(cookie));
|
|
220
236
|
}
|
|
221
237
|
//#endregion
|
|
222
|
-
export { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, RDT_CID_COOKIE, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, toSetCookieHeaders };
|
|
238
|
+
export { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, RDT_CID_COOKIE, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, isGoogleAdsGclid, isMsclkid, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, toSetCookieHeaders };
|
|
223
239
|
|
|
224
240
|
//# sourceMappingURL=index.mjs.map
|