@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.
@@ -1,8 +1,8 @@
1
1
  //#region src/attribution/vocabulary.d.ts
2
2
  /**
3
3
  * The vocabulary attribution is built from, shared by every product's `session` table and by the
4
- * dashboards that read it. Data only: `classifyTouch` (./classify) turns it into a channel, and a
5
- * product's dashboards read the same names back. What stays with the product is its own hostnames
4
+ * dashboards that read it. Data for `classifyTouch` (./classify), which turns it into a channel;
5
+ * a product's dashboards read the same names back. What stays with the product is its own hostnames
6
6
  * (referrer exclusions) and its landing-page conventions.
7
7
  */
8
8
  /** A session that arrived through nothing we can name. GA's spelling, so dashboards read as GA. */
@@ -13,6 +13,12 @@ declare const NO_MEDIUM = "(none)";
13
13
  declare const MEDIUM_NOT_SET = "(not set)";
14
14
  /** The channels the rules below can name. A `utm_source` outside this list is its own channel. */
15
15
  declare const CHANNELS: readonly ['meta', 'google', 'dv360', 'microsoft', 'tiktok', 'reddit', 'linkedin', 'snapchat', 'x', 'kakao', 'yandex', 'yahoo', 'duckduckgo', 'baidu', 'ecosia', 'naver', 'brave', 'pinterest', 'youtube',
16
+ /** AI assistants that send people to sites: ChatGPT tags its links `utm_source=chatgpt.com`, the
17
+ * others show as a referrer. Their own channel group, `organic_ai`, since GA4's default groups
18
+ * would file them under Referral / Unassigned and the question "how much does AI bring" comes up. */
19
+ 'chatgpt', 'perplexity', 'gemini', 'claude', 'copilot', 'grok',
20
+ /** and the Chinese ones — DeepSeek, 豆包, Kimi, 通义, 腾讯元宝, 文心一言, 智谱清言 */
21
+ 'deepseek', 'doubao', 'kimi', 'qwen', 'yuanbao', 'ernie', 'zhipu',
16
22
  /** the product's own referral programme: the session came through a member's link or code.
17
23
  * How a product recognises one (a `/refer/<code>` path, a code at sign-up) is its own rule. Not
18
24
  * `referral`: that word is the channel group of any outside site, and a channel of the same name
@@ -20,7 +26,9 @@ declare const CHANNELS: readonly ['meta', 'google', 'dv360', 'microsoft', 'tikto
20
26
  'referral_program'];
21
27
  type Channel = (typeof CHANNELS)[number];
22
28
  /** GA4's default channel groups, as identifiers; a dashboard maps them to display names. */
23
- declare const CHANNEL_GROUPS: readonly ['direct', 'paid_search', 'paid_social', 'paid_other', 'display', 'organic_search', 'organic_social', 'organic_video', 'referral', 'email', 'affiliate', 'unassigned'];
29
+ declare const CHANNEL_GROUPS: readonly ['direct', 'paid_search', 'paid_social', 'paid_other', 'display', 'organic_search', 'organic_social', 'organic_video',
30
+ /** not in GA4's default grouping: sessions an AI assistant sent, by referrer or by ChatGPT's utm */
31
+ 'organic_ai', 'referral', 'email', 'affiliate', 'unassigned'];
24
32
  type ChannelGroup = (typeof CHANNEL_GROUPS)[number];
25
33
  /** `utm_source` spellings that mean the same channel: Meta's `{{site_source_name}}` values and the usual synonyms. */
26
34
  declare const SOURCE_ALIASES: {
@@ -32,6 +40,18 @@ declare const SOURCE_ALIASES: {
32
40
  readonly instagram: 'meta';
33
41
  readonly bing: 'microsoft';
34
42
  readonly twitter: 'x';
43
+ readonly 'google ads': 'google';
44
+ readonly googleads: 'google';
45
+ readonly adwords: 'google';
46
+ readonly 'chatgpt.com': 'chatgpt';
47
+ readonly 'chat.openai.com': 'chatgpt';
48
+ readonly openai: 'chatgpt';
49
+ readonly 'perplexity.ai': 'perplexity';
50
+ readonly 'claude.ai': 'claude';
51
+ readonly 'copilot.com': 'copilot';
52
+ readonly 'deepseek.com': 'deepseek';
53
+ readonly 'doubao.com': 'doubao';
54
+ readonly 'kimi.com': 'kimi';
35
55
  };
36
56
  /**
37
57
  * Which click id belongs to which channel, in priority order. Every key is a field this SDK
@@ -44,26 +64,51 @@ declare const CLICK_ID_CHANNELS: readonly [readonly ["fbclid", "meta"], readonly
44
64
  * `/lp/<channel>`, with or without host, query and hash; the capture is the channel. A product
45
65
  * that names its landing pages differently replaces this.
46
66
  */
47
- declare const AD_LANDING_PAGE = "^(?:https?://[^/]+)?/lp/([a-z]+)(?:[/?#]|$)";
48
- type ReferrerMedium = 'organic' | 'social' | 'video';
67
+ declare const AD_LANDING_PAGE: RegExp;
68
+ type ReferrerMedium = 'organic' | 'social' | 'video' | 'ai';
49
69
  /**
50
- * Referrer hosts that name a channel, as POSIX regular expressions over the lower-cased host,
70
+ * Referrer hosts that name a channel, as regular expressions over the lower-cased host,
51
71
  * with the medium GA4 gives that kind of site. Anything else that refers is `referral`, kept as
52
72
  * its host. A country TLD is one or two labels (`google.com`, `google.co.uk`, `google.com.hk`),
53
- * never an open tail, so `google.com.evil.io` is not Google.
73
+ * never an open tail, so `google.com.evil.io` is not Google. In match order: the AI assistants
74
+ * first, because `gemini.google.com`, `copilot.microsoft.com` and `yiyan.baidu.com` would
75
+ * otherwise be Google, Microsoft and Baidu search.
54
76
  */
55
- declare const REFERRER_SITES: readonly [readonly ["google", "organic", string], readonly ["microsoft", "organic", string], readonly ["yahoo", "organic", string], readonly ["duckduckgo", "organic", string], readonly ["baidu", "organic", string], readonly ["yandex", "organic", string], readonly ["ecosia", "organic", string], readonly ["naver", "organic", string], readonly ["brave", "organic", string], readonly ["meta", "social", string], readonly ["x", "social", string], readonly ["linkedin", "social", string], readonly ["reddit", "social", string], readonly ["tiktok", "social", string], readonly ["pinterest", "social", string], readonly ["snapchat", "social", string], readonly ["youtube", "video", string]];
77
+ declare const REFERRER_SITES: readonly [readonly ["chatgpt", "ai", RegExp], readonly ["perplexity", "ai", RegExp], readonly ["gemini", "ai", RegExp], readonly ["claude", "ai", RegExp], readonly ["copilot", "ai", RegExp], readonly ["grok", "ai", RegExp], readonly ["deepseek", "ai", RegExp], readonly ["doubao", "ai", RegExp], readonly ["kimi", "ai", RegExp], readonly ["qwen", "ai", RegExp], readonly ["yuanbao", "ai", RegExp], readonly ["ernie", "ai", RegExp], readonly ["zhipu", "ai", RegExp], readonly ["google", "organic", RegExp], readonly ["microsoft", "organic", RegExp], readonly ["yahoo", "organic", RegExp], readonly ["duckduckgo", "organic", RegExp], readonly ["baidu", "organic", RegExp], readonly ["yandex", "organic", RegExp], readonly ["ecosia", "organic", RegExp], readonly ["naver", "organic", RegExp], readonly ["brave", "organic", RegExp], readonly ["meta", "social", RegExp], readonly ["x", "social", RegExp], readonly ["linkedin", "social", RegExp], readonly ["reddit", "social", RegExp], readonly ["tiktok", "social", RegExp], readonly ["pinterest", "social", RegExp], readonly ["snapchat", "social", RegExp], readonly ["youtube", "video", RegExp]];
56
78
  /**
57
- * Referrer hosts that are navigation, not acquisition: the payment and sign-in providers a
58
- * visitor is bounced through and back from, and the local dev host. A product appends its own
79
+ * Referrer hosts that are navigation, not acquisition, when a session happens to start on the way
80
+ * back from them: the local dev host; the payment providers a visitor is bounced through; the
81
+ * sign-in providers whose login lives on a host of its own (Google, Apple, Microsoft, WeChat,
82
+ * Kakao, LINE, Naver, Yahoo, Twitch, X's OAuth 1.0a endpoint); and the hosted auth services a
83
+ * product may sit behind (Auth0, Okta, Supabase, Firebase, Clerk). A product appends its own
59
84
  * hostnames — a session that starts from an internal link after the session timeout refers to
60
- * them. POSIX regular expressions over the lower-cased host.
85
+ * them. Matched before `REFERRER_SITES`, so `nid.naver.com` and `login.yahoo.com` are not read as
86
+ * a search. Providers whose login shares the host with their content — Facebook, X's OAuth 2.0,
87
+ * LinkedIn, GitHub, Discord — cannot be told apart by host (referrer policies strip the path) and
88
+ * are left as the referrers they usually are. Regular expressions over the lower-cased host.
61
89
  */
62
- declare const REFERRERS_NOT_A_TOUCH: readonly [string, string, string, string, string, string];
63
- /** GA4's paid mediums: cpc, cpm, ppc, retargeting, paid_social, … */
64
- declare const PAID_MEDIUM = "^(.*cp.*|ppc|retargeting|paid.*)$";
90
+ declare const REFERRERS_NOT_A_TOUCH: readonly [RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp];
91
+ /** GA4's paid mediums — cpc, cpm, ppc, retargeting, paid_social, … — plus Performance Max. */
92
+ declare const PAID_MEDIUM: RegExp;
93
+ /**
94
+ * Meta's `{{placement}}` values, lower-cased, which an ad URL template of `utm_medium={{placement}}`
95
+ * puts in the medium. Only an ad carries one — an organic post arrives as a referrer, with no utm
96
+ * at all — so a medium in this list is a paid Meta click whatever the source says (a template with
97
+ * `utm_source=meta-websitekeyinfo` was seen). Listed one by one rather than matched by prefix, so
98
+ * that a hand-tagged `instagram_stories` post cannot be mistaken for an ad only because of how it
99
+ * is spelled … which means a placement Meta adds later shows up as unassigned until it is added
100
+ * here. Meta's documented values plus the ones seen in the wild (`facebook_mobile_reels`,
101
+ * `facebook_notification`, `an`), and the literal macro for a template that did not expand it.
102
+ */
103
+ declare const META_PLACEMENTS: readonly ['facebook_desktop_feed', 'facebook_mobile_feed', 'facebook_mobile_reels', 'facebook_reels', 'facebook_reels_overlay', 'facebook_right_column', 'facebook_marketplace', 'facebook_video_feeds', 'facebook_stories', 'facebook_search', 'facebook_instream_video', 'facebook_instant_article', 'facebook_groups_feed', 'facebook_profile_feed', 'facebook_notification', 'facebook_notifications', 'facebook_business_explore', 'instagram_feed', 'instagram_stories', 'instagram_reels', 'instagram_reels_overlay', 'instagram_explore', 'instagram_explore_home', 'instagram_search', 'instagram_shop', 'instagram_profile_feed', 'instagram_profile_reels', 'instagram_igtv', 'messenger_inbox', 'messenger_stories', 'messenger_sponsored_messages', 'threads_feed', 'whatsapp_status', 'audience_network_classic', 'audience_network_rewarded_video', 'audience_network_native_banner_and_interstitial', 'an', 'others',
104
+ /** the macro itself, when Meta did not expand it (written in a field it does not substitute, or a
105
+ * preview click): still only ever produced by a Meta ad URL, so still a paid Meta click */
106
+ '{{placement}}'];
65
107
  declare const DISPLAY_MEDIUMS: readonly ['display', 'banner', 'expandable', 'interstitial'];
108
+ /** GA4's email spellings, as a source or a medium. */
66
109
  declare const EMAIL_MEDIUMS: readonly ['email', 'e-mail', 'e_mail', 'e mail'];
110
+ /** A medium that says email or newsletter anywhere in it: `outbound email`, `cold_email`, `newsletter`, and GA4's four. */
111
+ declare const EMAIL_MEDIUM: RegExp;
67
112
  /** Where a touch came from: a tracked session, or a report by the user or staff. */
68
113
  declare const TOUCH_SOURCES: readonly ['tracked', 'reported'];
69
114
  type TouchSource = (typeof TOUCH_SOURCES)[number];
@@ -105,5 +150,5 @@ declare const REPORTED_TOUCH_PRIORITY: {
105
150
  readonly promo_code: 3;
106
151
  };
107
152
  //#endregion
108
- export { 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 };
153
+ export { 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 };
109
154
  //# sourceMappingURL=vocabulary.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vocabulary.d.mts","names":[],"sources":["../../src/attribution/vocabulary.ts"],"mappings":";;;;;;;;cAUa;;cAEA;;cAEA;;cAGA;;;;;;KA2BD,kBAAkB;;cAGjB;KAeD,uBAAuB;;cAGtB;WACP;WACA;WACA;WACC;WACK;WACC;WACL;WACG;;;;;;;cAQE;;;;;;cAwBA;KAED;;;;;;;cAQC;;;;;;;cA0BA;;cAUA;cACA;cACA;;cAGA;KACD,sBAAsB;;;;;;;;cASrB;KACD,4BAA4B;;;;;;;;;;;;cAa3B;;WAED;;WAEA;;WAEA;;WAED;;;cAIE"}
1
+ {"version":3,"file":"vocabulary.d.mts","names":[],"sources":["../../src/attribution/vocabulary.ts"],"mappings":";;;;;;;;cAUa;;cAEA;;cAEA;;cAGA;;;;;;;;;;;;KA4CD,kBAAkB;;cAGjB;;;KAiBD,uBAAuB;;cAGtB;WACP;WACA;WACA;WACC;WACK;WACC;WACL;WACG;WACK;WACH;WACF;WACM;WACI;WACX;WACS;WACJ;WACE;WACC;WACF;WACF;;;;;;;cAQD;;;;;;cAwBA,iBAAe;KAEhB;;;;;;;;;cAUC,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;;;;;;;;;;;;;cA6Cd,iCAAqB,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA,QAAA;;cAgCrB,aAAW;;;;;;;;;;;cAWX;;;;cA2CA;;cAEA;;cAEA,cAAY;;cAGZ;KACD,sBAAsB;;;;;;;;cASrB;KACD,4BAA4B;;;;;;;;;;;;cAa3B;;WAED;;WAEA;;WAEA;;WAED;;;cAIE"}
@@ -1,8 +1,8 @@
1
1
  //#region src/attribution/vocabulary.ts
2
2
  /**
3
3
  * The vocabulary attribution is built from, shared by every product's `session` table and by the
4
- * dashboards that read it. Data only: `classifyTouch` (./classify) turns it into a channel, and a
5
- * product's dashboards read the same names back. What stays with the product is its own hostnames
4
+ * dashboards that read it. Data for `classifyTouch` (./classify), which turns it into a channel;
5
+ * a product's dashboards read the same names back. What stays with the product is its own hostnames
6
6
  * (referrer exclusions) and its landing-page conventions.
7
7
  */
8
8
  /** A session that arrived through nothing we can name. GA's spelling, so dashboards read as GA. */
@@ -32,6 +32,19 @@ const CHANNELS = [
32
32
  "brave",
33
33
  "pinterest",
34
34
  "youtube",
35
+ "chatgpt",
36
+ "perplexity",
37
+ "gemini",
38
+ "claude",
39
+ "copilot",
40
+ "grok",
41
+ "deepseek",
42
+ "doubao",
43
+ "kimi",
44
+ "qwen",
45
+ "yuanbao",
46
+ "ernie",
47
+ "zhipu",
35
48
  "referral_program"
36
49
  ];
37
50
  /** GA4's default channel groups, as identifiers; a dashboard maps them to display names. */
@@ -44,6 +57,7 @@ const CHANNEL_GROUPS = [
44
57
  "organic_search",
45
58
  "organic_social",
46
59
  "organic_video",
60
+ "organic_ai",
47
61
  "referral",
48
62
  "email",
49
63
  "affiliate",
@@ -58,7 +72,19 @@ const SOURCE_ALIASES = {
58
72
  facebook: "meta",
59
73
  instagram: "meta",
60
74
  bing: "microsoft",
61
- twitter: "x"
75
+ twitter: "x",
76
+ "google ads": "google",
77
+ googleads: "google",
78
+ adwords: "google",
79
+ "chatgpt.com": "chatgpt",
80
+ "chat.openai.com": "chatgpt",
81
+ openai: "chatgpt",
82
+ "perplexity.ai": "perplexity",
83
+ "claude.ai": "claude",
84
+ "copilot.com": "copilot",
85
+ "deepseek.com": "deepseek",
86
+ "doubao.com": "doubao",
87
+ "kimi.com": "kimi"
62
88
  };
63
89
  /**
64
90
  * Which click id belongs to which channel, in priority order. Every key is a field this SDK
@@ -88,128 +114,273 @@ const CLICK_ID_CHANNELS = [
88
114
  * `/lp/<channel>`, with or without host, query and hash; the capture is the channel. A product
89
115
  * that names its landing pages differently replaces this.
90
116
  */
91
- const AD_LANDING_PAGE = "^(?:https?://[^/]+)?/lp/([a-z]+)(?:[/?#]|$)";
117
+ const AD_LANDING_PAGE = /^(?:https?:\/\/[^/]+)?\/lp\/([a-z]+)(?:[/?#]|$)/;
92
118
  /**
93
- * Referrer hosts that name a channel, as POSIX regular expressions over the lower-cased host,
119
+ * Referrer hosts that name a channel, as regular expressions over the lower-cased host,
94
120
  * with the medium GA4 gives that kind of site. Anything else that refers is `referral`, kept as
95
121
  * its host. A country TLD is one or two labels (`google.com`, `google.co.uk`, `google.com.hk`),
96
- * never an open tail, so `google.com.evil.io` is not Google.
122
+ * never an open tail, so `google.com.evil.io` is not Google. In match order: the AI assistants
123
+ * first, because `gemini.google.com`, `copilot.microsoft.com` and `yiyan.baidu.com` would
124
+ * otherwise be Google, Microsoft and Baidu search.
97
125
  */
98
126
  const REFERRER_SITES = [
127
+ [
128
+ "chatgpt",
129
+ "ai",
130
+ /(^|\.)(chatgpt\.com|chat\.openai\.com)$/
131
+ ],
132
+ [
133
+ "perplexity",
134
+ "ai",
135
+ /(^|\.)perplexity\.ai$/
136
+ ],
137
+ [
138
+ "gemini",
139
+ "ai",
140
+ /^gemini\.google\.com$/
141
+ ],
142
+ [
143
+ "claude",
144
+ "ai",
145
+ /(^|\.)claude\.ai$/
146
+ ],
147
+ [
148
+ "copilot",
149
+ "ai",
150
+ /^copilot\.microsoft\.com$|(^|\.)copilot\.com$/
151
+ ],
152
+ [
153
+ "grok",
154
+ "ai",
155
+ /(^|\.)(grok\.com|x\.ai)$/
156
+ ],
157
+ [
158
+ "deepseek",
159
+ "ai",
160
+ /(^|\.)deepseek\.com$/
161
+ ],
162
+ [
163
+ "doubao",
164
+ "ai",
165
+ /(^|\.)doubao\.com$/
166
+ ],
167
+ [
168
+ "kimi",
169
+ "ai",
170
+ /(^|\.)(kimi\.com|kimi\.moonshot\.cn|moonshot\.cn)$/
171
+ ],
172
+ [
173
+ "qwen",
174
+ "ai",
175
+ /^tongyi\.aliyun\.com$|(^|\.)(tongyi\.com|qianwen\.com|qwen\.ai)$/
176
+ ],
177
+ [
178
+ "yuanbao",
179
+ "ai",
180
+ /^yuanbao\.tencent\.com$/
181
+ ],
182
+ [
183
+ "ernie",
184
+ "ai",
185
+ /^yiyan\.baidu\.com$|(^|\.)ernie\.baidu\.com$/
186
+ ],
187
+ [
188
+ "zhipu",
189
+ "ai",
190
+ /(^|\.)(chatglm\.cn|zhipuai\.cn|bigmodel\.cn)$/
191
+ ],
99
192
  [
100
193
  "google",
101
194
  "organic",
102
- String.raw`(^|\.)google\.[a-z]{2,}(\.[a-z]{2,3})?$`
195
+ /(^|\.)google\.[a-z]{2,}(\.[a-z]{2,3})?$/
103
196
  ],
104
197
  [
105
198
  "microsoft",
106
199
  "organic",
107
- String.raw`(^|\.)bing\.com$`
200
+ /(^|\.)bing\.com$/
108
201
  ],
109
202
  [
110
203
  "yahoo",
111
204
  "organic",
112
- String.raw`(^|\.)yahoo\.[a-z]{2,}(\.[a-z]{2,3})?$`
205
+ /(^|\.)yahoo\.[a-z]{2,}(\.[a-z]{2,3})?$/
113
206
  ],
114
207
  [
115
208
  "duckduckgo",
116
209
  "organic",
117
- String.raw`(^|\.)duckduckgo\.com$`
210
+ /(^|\.)duckduckgo\.com$/
118
211
  ],
119
212
  [
120
213
  "baidu",
121
214
  "organic",
122
- String.raw`(^|\.)baidu\.com$`
215
+ /(^|\.)baidu\.com$/
123
216
  ],
124
217
  [
125
218
  "yandex",
126
219
  "organic",
127
- String.raw`(^|\.)yandex\.[a-z]{2,}(\.[a-z]{2,3})?$`
220
+ /(^|\.)yandex\.[a-z]{2,}(\.[a-z]{2,3})?$/
128
221
  ],
129
222
  [
130
223
  "ecosia",
131
224
  "organic",
132
- String.raw`(^|\.)ecosia\.org$`
225
+ /(^|\.)ecosia\.org$/
133
226
  ],
134
227
  [
135
228
  "naver",
136
229
  "organic",
137
- String.raw`(^|\.)naver\.com$`
230
+ /(^|\.)naver\.com$/
138
231
  ],
139
232
  [
140
233
  "brave",
141
234
  "organic",
142
- String.raw`^search\.brave\.com$`
235
+ /^search\.brave\.com$/
143
236
  ],
144
237
  [
145
238
  "meta",
146
239
  "social",
147
- String.raw`(^|\.)(facebook\.com|instagram\.com|fb\.com|threads\.net)$`
240
+ /(^|\.)(facebook\.com|instagram\.com|fb\.com|threads\.net)$/
148
241
  ],
149
242
  [
150
243
  "x",
151
244
  "social",
152
- String.raw`(^|\.)(twitter\.com|x\.com|t\.co)$`
245
+ /(^|\.)(twitter\.com|x\.com|t\.co)$/
153
246
  ],
154
247
  [
155
248
  "linkedin",
156
249
  "social",
157
- String.raw`(^|\.)(linkedin\.com|lnkd\.in)$`
250
+ /(^|\.)(linkedin\.com|lnkd\.in)$/
158
251
  ],
159
252
  [
160
253
  "reddit",
161
254
  "social",
162
- String.raw`(^|\.)reddit\.com$`
255
+ /(^|\.)reddit\.com$/
163
256
  ],
164
257
  [
165
258
  "tiktok",
166
259
  "social",
167
- String.raw`(^|\.)tiktok\.com$`
260
+ /(^|\.)tiktok\.com$/
168
261
  ],
169
262
  [
170
263
  "pinterest",
171
264
  "social",
172
- String.raw`(^|\.)pinterest\.[a-z]{2,}(\.[a-z]{2,3})?$`
265
+ /(^|\.)pinterest\.[a-z]{2,}(\.[a-z]{2,3})?$/
173
266
  ],
174
267
  [
175
268
  "snapchat",
176
269
  "social",
177
- String.raw`(^|\.)snapchat\.com$`
270
+ /(^|\.)snapchat\.com$/
178
271
  ],
179
272
  [
180
273
  "youtube",
181
274
  "video",
182
- String.raw`(^|\.)(youtube\.com|youtu\.be)$`
275
+ /(^|\.)(youtube\.com|youtu\.be)$/
183
276
  ]
184
277
  ];
185
278
  /**
186
- * Referrer hosts that are navigation, not acquisition: the payment and sign-in providers a
187
- * visitor is bounced through and back from, and the local dev host. A product appends its own
279
+ * Referrer hosts that are navigation, not acquisition, when a session happens to start on the way
280
+ * back from them: the local dev host; the payment providers a visitor is bounced through; the
281
+ * sign-in providers whose login lives on a host of its own (Google, Apple, Microsoft, WeChat,
282
+ * Kakao, LINE, Naver, Yahoo, Twitch, X's OAuth 1.0a endpoint); and the hosted auth services a
283
+ * product may sit behind (Auth0, Okta, Supabase, Firebase, Clerk). A product appends its own
188
284
  * hostnames — a session that starts from an internal link after the session timeout refers to
189
- * them. POSIX regular expressions over the lower-cased host.
285
+ * them. Matched before `REFERRER_SITES`, so `nid.naver.com` and `login.yahoo.com` are not read as
286
+ * a search. Providers whose login shares the host with their content — Facebook, X's OAuth 2.0,
287
+ * LinkedIn, GitHub, Discord — cannot be told apart by host (referrer policies strip the path) and
288
+ * are left as the referrers they usually are. Regular expressions over the lower-cased host.
190
289
  */
191
290
  const REFERRERS_NOT_A_TOUCH = [
192
- String.raw`^localhost(:|$)`,
193
- String.raw`(^|\.)stripe\.com$`,
194
- String.raw`^(pay|accounts)\.google\.com$`,
195
- String.raw`^(appleid|apps)\.apple\.com$`,
196
- String.raw`^play\.google\.com$`,
197
- String.raw`^login\.microsoftonline\.com$`
291
+ /^localhost(:|$)/,
292
+ /(^|\.)stripe\.com$/,
293
+ /^pay\.google\.com$/,
294
+ /(^|\.)paypal\.com$/,
295
+ /(^|\.)alipay\.com$/,
296
+ /(^|\.)paddle\.com$/,
297
+ /(^|\.)lemonsqueezy\.com$/,
298
+ /^apps\.apple\.com$/,
299
+ /^play\.google\.com$/,
300
+ /^accounts\.google\.com$/,
301
+ /^appleid\.apple\.com$/,
302
+ /^login\.(microsoftonline|live)\.com$/,
303
+ /^open\.weixin\.qq\.com$/,
304
+ /^(kauth|accounts)\.kakao\.com$/,
305
+ /^access\.line\.me$/,
306
+ /^nid\.naver\.com$/,
307
+ /^login\.yahoo\.com$/,
308
+ /^id\.twitch\.tv$/,
309
+ /^api\.twitter\.com$/,
310
+ /(^|\.)auth0\.com$/,
311
+ /(^|\.)okta\.com$/,
312
+ /(^|\.)supabase\.co$/,
313
+ /(^|\.)firebaseapp\.com$/,
314
+ /(^|\.)clerk\.accounts\.dev$/
315
+ ];
316
+ /** GA4's paid mediums — cpc, cpm, ppc, retargeting, paid_social, … — plus Performance Max. */
317
+ const PAID_MEDIUM = /^(.*cp.*|ppc|retargeting|paid.*|pmax|performance_max)$/;
318
+ /**
319
+ * Meta's `{{placement}}` values, lower-cased, which an ad URL template of `utm_medium={{placement}}`
320
+ * puts in the medium. Only an ad carries one — an organic post arrives as a referrer, with no utm
321
+ * at all — so a medium in this list is a paid Meta click whatever the source says (a template with
322
+ * `utm_source=meta-websitekeyinfo` was seen). Listed one by one rather than matched by prefix, so
323
+ * that a hand-tagged `instagram_stories` post cannot be mistaken for an ad only because of how it
324
+ * is spelled … which means a placement Meta adds later shows up as unassigned until it is added
325
+ * here. Meta's documented values plus the ones seen in the wild (`facebook_mobile_reels`,
326
+ * `facebook_notification`, `an`), and the literal macro for a template that did not expand it.
327
+ */
328
+ const META_PLACEMENTS = [
329
+ "facebook_desktop_feed",
330
+ "facebook_mobile_feed",
331
+ "facebook_mobile_reels",
332
+ "facebook_reels",
333
+ "facebook_reels_overlay",
334
+ "facebook_right_column",
335
+ "facebook_marketplace",
336
+ "facebook_video_feeds",
337
+ "facebook_stories",
338
+ "facebook_search",
339
+ "facebook_instream_video",
340
+ "facebook_instant_article",
341
+ "facebook_groups_feed",
342
+ "facebook_profile_feed",
343
+ "facebook_notification",
344
+ "facebook_notifications",
345
+ "facebook_business_explore",
346
+ "instagram_feed",
347
+ "instagram_stories",
348
+ "instagram_reels",
349
+ "instagram_reels_overlay",
350
+ "instagram_explore",
351
+ "instagram_explore_home",
352
+ "instagram_search",
353
+ "instagram_shop",
354
+ "instagram_profile_feed",
355
+ "instagram_profile_reels",
356
+ "instagram_igtv",
357
+ "messenger_inbox",
358
+ "messenger_stories",
359
+ "messenger_sponsored_messages",
360
+ "threads_feed",
361
+ "whatsapp_status",
362
+ "audience_network_classic",
363
+ "audience_network_rewarded_video",
364
+ "audience_network_native_banner_and_interstitial",
365
+ "an",
366
+ "others",
367
+ "{{placement}}"
198
368
  ];
199
- /** GA4's paid mediums: cpc, cpm, ppc, retargeting, paid_social, … */
200
- const PAID_MEDIUM = "^(.*cp.*|ppc|retargeting|paid.*)$";
201
369
  const DISPLAY_MEDIUMS = [
202
370
  "display",
203
371
  "banner",
204
372
  "expandable",
205
373
  "interstitial"
206
374
  ];
375
+ /** GA4's email spellings, as a source or a medium. */
207
376
  const EMAIL_MEDIUMS = [
208
377
  "email",
209
378
  "e-mail",
210
379
  "e_mail",
211
380
  "e mail"
212
381
  ];
382
+ /** A medium that says email or newsletter anywhere in it: `outbound email`, `cold_email`, `newsletter`, and GA4's four. */
383
+ const EMAIL_MEDIUM = /(^|[^a-z])(e[-_ ]?mail|newsletter)([^a-z]|$)/;
213
384
  /** Where a touch came from: a tracked session, or a report by the user or staff. */
214
385
  const TOUCH_SOURCES = ["tracked", "reported"];
215
386
  /**
@@ -254,6 +425,6 @@ const REPORTED_TOUCH_PRIORITY = {
254
425
  promo_code: TOUCH_PRIORITY.claimed
255
426
  };
256
427
  //#endregion
257
- 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 };
428
+ 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 };
258
429
 
259
430
  //# sourceMappingURL=vocabulary.mjs.map
@@ -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 only: `classifyTouch` (./classify) turns it into a channel, and a\n * 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 /** 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 '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} 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';\n\n/**\n * Referrer hosts that name a channel, as POSIX 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.\n */\nexport const REFERRER_SITES = [\n ['google', 'organic', String.raw`(^|\\.)google\\.[a-z]{2,}(\\.[a-z]{2,3})?$`],\n ['microsoft', 'organic', String.raw`(^|\\.)bing\\.com$`],\n ['yahoo', 'organic', String.raw`(^|\\.)yahoo\\.[a-z]{2,}(\\.[a-z]{2,3})?$`],\n ['duckduckgo', 'organic', String.raw`(^|\\.)duckduckgo\\.com$`],\n ['baidu', 'organic', String.raw`(^|\\.)baidu\\.com$`],\n ['yandex', 'organic', String.raw`(^|\\.)yandex\\.[a-z]{2,}(\\.[a-z]{2,3})?$`],\n ['ecosia', 'organic', String.raw`(^|\\.)ecosia\\.org$`],\n ['naver', 'organic', String.raw`(^|\\.)naver\\.com$`],\n ['brave', 'organic', String.raw`^search\\.brave\\.com$`],\n ['meta', 'social', String.raw`(^|\\.)(facebook\\.com|instagram\\.com|fb\\.com|threads\\.net)$`],\n ['x', 'social', String.raw`(^|\\.)(twitter\\.com|x\\.com|t\\.co)$`],\n ['linkedin', 'social', String.raw`(^|\\.)(linkedin\\.com|lnkd\\.in)$`],\n ['reddit', 'social', String.raw`(^|\\.)reddit\\.com$`],\n ['tiktok', 'social', String.raw`(^|\\.)tiktok\\.com$`],\n ['pinterest', 'social', String.raw`(^|\\.)pinterest\\.[a-z]{2,}(\\.[a-z]{2,3})?$`],\n ['snapchat', 'social', String.raw`(^|\\.)snapchat\\.com$`],\n ['youtube', 'video', String.raw`(^|\\.)(youtube\\.com|youtu\\.be)$`],\n] as const satisfies readonly (readonly [Channel, ReferrerMedium, string])[];\n\n/**\n * Referrer hosts that are navigation, not acquisition: the payment and sign-in providers a\n * visitor is bounced through and back from, and the local dev host. A product appends its own\n * hostnames — a session that starts from an internal link after the session timeout refers to\n * them. POSIX regular expressions over the lower-cased host.\n */\nexport const REFERRERS_NOT_A_TOUCH = [\n String.raw`^localhost(:|$)`,\n String.raw`(^|\\.)stripe\\.com$`,\n String.raw`^(pay|accounts)\\.google\\.com$`,\n String.raw`^(appleid|apps)\\.apple\\.com$`,\n String.raw`^play\\.google\\.com$`,\n String.raw`^login\\.microsoftonline\\.com$`,\n] as const;\n\n/** GA4's paid mediums: cpc, cpm, ppc, retargeting, paid_social, … */\nexport const PAID_MEDIUM = '^(.*cp.*|ppc|retargeting|paid.*)$';\nexport const DISPLAY_MEDIUMS = ['display', 'banner', 'expandable', 'interstitial'] as const;\nexport const EMAIL_MEDIUMS = ['email', 'e-mail', 'e_mail', 'e mail'] as const;\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":";;;;;;;;AAUA,MAAa,iBAAiB;;AAE9B,MAAa,YAAY;;AAEzB,MAAa,iBAAiB;;AAG9B,MAAa,WAAW;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAKA;AACF;;AAKA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAKA,MAAa,iBAAiB;CAC5B,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,KAAK;CACL,UAAU;CACV,WAAW;CACX,MAAM;CACN,SAAS;AACX;;;;;;AAOA,MAAa,oBAAoB;CAC/B,CAAC,UAAU,MAAM;CACjB,CAAC,OAAO,MAAM;CACd,CAAC,SAAS,QAAQ;CAClB,CAAC,UAAU,QAAQ;CACnB,CAAC,UAAU,QAAQ;CACnB,CAAC,cAAc,QAAQ;CACvB,CAAC,kBAAkB,QAAQ;CAC3B,CAAC,SAAS,OAAO;CACjB,CAAC,WAAW,WAAW;CACvB,CAAC,UAAU,QAAQ;CACnB,CAAC,WAAW,QAAQ;CACpB,CAAC,aAAa,UAAU;CACxB,CAAC,SAAS,UAAU;CACpB,CAAC,UAAU,GAAG;CACd,CAAC,eAAe,OAAO;CACvB,CAAC,SAAS,QAAQ;AACpB;;;;;;AAOA,MAAa,kBAAkB;;;;;;;AAU/B,MAAa,iBAAiB;CAC5B;EAAC;EAAU;EAAW,OAAO,GAAG;CAAyC;CACzE;EAAC;EAAa;EAAW,OAAO,GAAG;CAAkB;CACrD;EAAC;EAAS;EAAW,OAAO,GAAG;CAAwC;CACvE;EAAC;EAAc;EAAW,OAAO,GAAG;CAAwB;CAC5D;EAAC;EAAS;EAAW,OAAO,GAAG;CAAmB;CAClD;EAAC;EAAU;EAAW,OAAO,GAAG;CAAyC;CACzE;EAAC;EAAU;EAAW,OAAO,GAAG;CAAoB;CACpD;EAAC;EAAS;EAAW,OAAO,GAAG;CAAmB;CAClD;EAAC;EAAS;EAAW,OAAO,GAAG;CAAsB;CACrD;EAAC;EAAQ;EAAU,OAAO,GAAG;CAA4D;CACzF;EAAC;EAAK;EAAU,OAAO,GAAG;CAAoC;CAC9D;EAAC;EAAY;EAAU,OAAO,GAAG;CAAiC;CAClE;EAAC;EAAU;EAAU,OAAO,GAAG;CAAoB;CACnD;EAAC;EAAU;EAAU,OAAO,GAAG;CAAoB;CACnD;EAAC;EAAa;EAAU,OAAO,GAAG;CAA4C;CAC9E;EAAC;EAAY;EAAU,OAAO,GAAG;CAAsB;CACvD;EAAC;EAAW;EAAS,OAAO,GAAG;CAAiC;AAClE;;;;;;;AAQA,MAAa,wBAAwB;CACnC,OAAO,GAAG;CACV,OAAO,GAAG;CACV,OAAO,GAAG;CACV,OAAO,GAAG;CACV,OAAO,GAAG;CACV,OAAO,GAAG;AACZ;;AAGA,MAAa,cAAc;AAC3B,MAAa,kBAAkB;CAAC;CAAW;CAAU;CAAc;AAAc;AACjF,MAAa,gBAAgB;CAAC;CAAS;CAAU;CAAU;AAAQ;;AAGnE,MAAa,gBAAgB,CAAC,WAAW,UAAU;;;;;;;;AAUnD,MAAa,uBAAuB;CAAC;CAAU;CAAc;CAAS;AAAQ;;;;;;;;;;;;AAc9E,MAAa,iBAAiB;;CAE5B,UAAU;;CAEV,UAAU;;CAEV,UAAU;;CAEV,SAAS;AACX;;AAGA,MAAa,0BAA0B;CACrC,OAAO,eAAe;CACtB,QAAQ,eAAe;CACvB,QAAQ,eAAe;CACvB,YAAY,eAAe;AAC7B"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/analytics",
3
- "version": "8.12.0",
3
+ "version": "8.13.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",