@buildinams/core 0.3.1 → 0.4.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.
Files changed (79) hide show
  1. package/README.md +24 -1
  2. package/dist/analytics/google/index.d.ts +158 -0
  3. package/dist/analytics/google/index.js +274 -0
  4. package/dist/analytics/google/index.js.map +1 -0
  5. package/dist/analytics/index.d.ts +101 -0
  6. package/dist/analytics/index.js +384 -0
  7. package/dist/analytics/index.js.map +1 -0
  8. package/dist/analytics/shopify/index.d.ts +186 -0
  9. package/dist/analytics/shopify/index.js +175 -0
  10. package/dist/analytics/shopify/index.js.map +1 -0
  11. package/dist/{chunk-5KDGGGVO.js → chunk-FJ7SNQQI.js} +24 -6
  12. package/dist/chunk-FJ7SNQQI.js.map +1 -0
  13. package/dist/chunk-G2BKC4BU.js +8 -0
  14. package/dist/chunk-G2BKC4BU.js.map +1 -0
  15. package/dist/chunk-GQVN2ECX.js +14 -0
  16. package/dist/chunk-GQVN2ECX.js.map +1 -0
  17. package/dist/chunk-LL3VTFNY.js +22 -0
  18. package/dist/chunk-LL3VTFNY.js.map +1 -0
  19. package/dist/chunk-M3HNM5HC.js +48 -0
  20. package/dist/chunk-M3HNM5HC.js.map +1 -0
  21. package/dist/chunk-MHFGMH3G.js +11 -0
  22. package/dist/chunk-MHFGMH3G.js.map +1 -0
  23. package/dist/chunk-OS2T6P2T.js +50 -0
  24. package/dist/chunk-OS2T6P2T.js.map +1 -0
  25. package/dist/chunk-QIJGG2F4.js +51 -0
  26. package/dist/chunk-QIJGG2F4.js.map +1 -0
  27. package/dist/consent/cookiebot/index.d.ts +16 -0
  28. package/dist/consent/cookiebot/index.js +27 -0
  29. package/dist/consent/cookiebot/index.js.map +1 -0
  30. package/dist/consent/google/index.d.ts +23 -0
  31. package/dist/consent/google/index.js +48 -0
  32. package/dist/consent/google/index.js.map +1 -0
  33. package/dist/consent/index.d.ts +1 -0
  34. package/dist/consent/index.js +3 -0
  35. package/dist/consent/index.js.map +1 -0
  36. package/dist/consent/onetrust/index.d.ts +23 -0
  37. package/dist/consent/onetrust/index.js +39 -0
  38. package/dist/consent/onetrust/index.js.map +1 -0
  39. package/dist/consent/shopify/index.d.ts +44 -0
  40. package/dist/consent/shopify/index.js +84 -0
  41. package/dist/consent/shopify/index.js.map +1 -0
  42. package/dist/cookies/index.d.ts +230 -0
  43. package/dist/cookies/index.js +188 -0
  44. package/dist/cookies/index.js.map +1 -0
  45. package/dist/define-events-CxZjoUlG.d.ts +255 -0
  46. package/dist/destination-5KJ5PeR3.d.ts +122 -0
  47. package/dist/index-Zef1nnTP.d.ts +36 -0
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +64 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/listeners-DkU-vBPa.d.ts +15 -0
  52. package/dist/navigation/index.js +1 -1
  53. package/dist/schema/index.d.ts +2 -0
  54. package/dist/schema/index.js +1 -1
  55. package/dist/store/index.js +1 -1
  56. package/dist/tracking/cart/index.d.ts +74 -0
  57. package/dist/tracking/cart/index.js +118 -0
  58. package/dist/tracking/cart/index.js.map +1 -0
  59. package/dist/tracking/dom/index.d.ts +320 -0
  60. package/dist/tracking/dom/index.js +1196 -0
  61. package/dist/tracking/dom/index.js.map +1 -0
  62. package/docs/analytics.md +883 -0
  63. package/docs/cookies.md +354 -0
  64. package/docs/decisions/001-analytics-project-events.md +31 -0
  65. package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
  66. package/docs/decisions/003-tracking-element-data.md +27 -0
  67. package/docs/decisions/004-cookies-explicit-store.md +26 -0
  68. package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
  69. package/docs/decisions/006-cookies-server-only-types.md +19 -0
  70. package/docs/decisions/007-cookies-options-on-definition.md +25 -0
  71. package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
  72. package/docs/decisions/009-analytics-consent-targets.md +29 -0
  73. package/docs/decisions/010-analytics-destination-contract.md +29 -0
  74. package/docs/decisions/011-tracking-decoupled.md +30 -0
  75. package/docs/tracking.md +221 -0
  76. package/package.json +83 -2
  77. package/dist/chunk-5KDGGGVO.js.map +0 -1
  78. package/dist/chunk-SIU7SKJK.js +0 -139
  79. package/dist/chunk-SIU7SKJK.js.map +0 -1
package/README.md CHANGED
@@ -1,7 +1,30 @@
1
1
  # `@buildinams/core`
2
2
 
3
- # Install
3
+ Framework-agnostic utilities used by `@buildinams/react`.
4
+
5
+ | Import | Contents |
6
+ | ------------------------------------ | ---------------------------------------------------------- |
7
+ | `@buildinams/core` | Utility functions, type guards, shared types. |
8
+ | `@buildinams/core/analytics` | Typed, consent-aware analytics events. |
9
+ | `@buildinams/core/analytics/google` | GTM dataLayer destination and GA4 mapping. |
10
+ | `@buildinams/core/analytics/shopify` | Shopify Analytics destination, through Hydrogen. |
11
+ | `@buildinams/core/consent` | Consent categories, `ConsentSource` and `ConsentTarget`. |
12
+ | `@buildinams/core/consent/cookiebot` | Consent from Cookiebot. |
13
+ | `@buildinams/core/consent/onetrust` | Consent from OneTrust. |
14
+ | `@buildinams/core/consent/google` | Google Consent Mode consent target. |
15
+ | `@buildinams/core/consent/shopify` | Consent from, and to, Shopify's Customer Privacy API. |
16
+ | `@buildinams/core/store` | Observable stores and store groups. |
17
+ | `@buildinams/core/tracking/dom` | Tracking from markup: `createDomWatcher()` and plugins. |
18
+ | `@buildinams/core/tracking/cart` | Adds and removes from cart changes: `createCartChanges()`. |
19
+ | `@buildinams/core/schema` | Parse/serialize schemas, incl. `hrefSchema`. |
20
+ | `@buildinams/core/navigation` | Deprecated: use `@buildinams/core/schema`. |
4
21
 
5
22
  ```bash
6
23
  npm install @buildinams/core
7
24
  ```
25
+
26
+ # Docs
27
+
28
+ - [Analytics](./docs/analytics.md): one `track()` typed by the project's own events, mapped to every provider and gated by consent (`@buildinams/core/analytics`, `@buildinams/core/consent`)
29
+ - [Tracking](./docs/tracking.md): page views, products, clicks and cart changes reported from markup and confirmed carts (`@buildinams/core/tracking/dom`, `@buildinams/core/tracking/cart`)
30
+ - [Cookies](./docs/cookies.md): typesafe cookies across Next.js server and client (`@buildinams/core/cookies`)
@@ -0,0 +1,158 @@
1
+ import { E as EventMap, j as EcommerceShapes, h as AnalyticsContext, k as ShapesOf, C as CartItemOf } from '../../define-events-CxZjoUlG.js';
2
+ import { a as AnalyticsMapping, A as AnalyticsDestination, D as DestinationEvent } from '../../destination-5KJ5PeR3.js';
3
+ import '../../index-Zef1nnTP.js';
4
+
5
+ declare const ECOMMERCE_EVENTS: readonly ["view_item_list", "select_item", "view_item", "add_to_wishlist", "add_to_cart", "remove_from_cart", "view_cart", "begin_checkout", "add_shipping_info", "add_payment_info", "purchase", "refund", "view_promotion", "select_promotion"];
6
+ declare const EVENTS: readonly ["search", "select_content", "share", "login", "sign_up", "generate_lead", "click", "video_start", "video_progress", "video_complete", "form_start", "form_submit", "scroll"];
7
+ type Ga4Params = Record<string, unknown>;
8
+ /** @see https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_to_cart_item */
9
+ type Ga4Item = {
10
+ item_id?: string;
11
+ item_name?: string;
12
+ affiliation?: string;
13
+ coupon?: string;
14
+ discount?: number;
15
+ index?: number;
16
+ item_brand?: string;
17
+ item_category?: string;
18
+ item_category2?: string;
19
+ item_category3?: string;
20
+ item_category4?: string;
21
+ item_category5?: string;
22
+ item_list_id?: string;
23
+ item_list_name?: string;
24
+ item_variant?: string;
25
+ location_id?: string;
26
+ /** Unit price, after discount. */
27
+ price?: number;
28
+ quantity?: number;
29
+ promotion_id?: string;
30
+ promotion_name?: string;
31
+ creative_name?: string;
32
+ creative_slot?: string;
33
+ } & Ga4Params;
34
+ type Ga4List = {
35
+ item_list_id?: string;
36
+ item_list_name?: string;
37
+ };
38
+ type Ga4Promotion = {
39
+ promotion_id?: string;
40
+ promotion_name?: string;
41
+ creative_name?: string;
42
+ creative_slot?: string;
43
+ };
44
+ /** An ecommerce event's params, pushed under `ecommerce`. */
45
+ type Ga4Ecommerce = {
46
+ /** ISO 4217, e.g. `"EUR"`. */
47
+ currency?: string;
48
+ value?: number;
49
+ items?: Ga4Item[];
50
+ } & Ga4Params;
51
+ /**
52
+ * What GTM accepts: GA4's events by name, in GA4's params, and `custom` for any other event name. Each is pushed with
53
+ * `event_id` and the context variables, e.g. `{ event: "login", event_id, user_id, method }`, and ecommerce events'
54
+ * params under `ecommerce`.
55
+ */
56
+ type GtmEvents = {
57
+ [N in (typeof ECOMMERCE_EVENTS)[number]]: Ga4Ecommerce;
58
+ } & {
59
+ [N in (typeof EVENTS)[number]]: Ga4Params;
60
+ } & {
61
+ page_view: {
62
+ page_location: string;
63
+ page_path: string;
64
+ page_title: string;
65
+ page_referrer: string;
66
+ page_type?: string;
67
+ } & Ga4Params;
68
+ /** Pushed as `{ event, ...params }`. */
69
+ custom: {
70
+ event: string;
71
+ params: Ga4Params;
72
+ };
73
+ };
74
+ type Ga4MappingOptions<Shapes extends EcommerceShapes, CartItem, Context extends object> = {
75
+ /** For what `ecommerce()` reports: the project's item as a GA4 item, e.g. `({ product }) => ({ item_id: product.id })`. */
76
+ item?: (item: Shapes["item"]) => Ga4Item;
77
+ /** `itemViewed`'s currency, with its price as `value`, e.g. from the item or the context's market. */
78
+ currency?: (item: Shapes["item"], context: AnalyticsContext<Context>) => string | undefined;
79
+ list?: (list: Shapes["list"]) => Ga4List;
80
+ promotion?: (promotion: Shapes["promotion"]) => Ga4Promotion;
81
+ } & ([CartItem] extends [never | Shapes["item"]] ? {
82
+ /**
83
+ * For what `createCartChanges()` reports: the project's cart line item as a GA4 item. Price and quantity come
84
+ * from the line. Defaults to `item`.
85
+ */
86
+ cartItem?: (item: CartItem) => Ga4Item;
87
+ } : {
88
+ item?: undefined;
89
+ cartItem?: (item: CartItem) => Ga4Item;
90
+ } | {
91
+ /**
92
+ * For what `createCartChanges()` reports: the project's cart line item as a GA4 item. Price and quantity
93
+ * come from the line. Required with `item`, as the cart item isn't one.
94
+ */
95
+ cartItem: (item: CartItem) => Ga4Item;
96
+ });
97
+ /**
98
+ * Maps what tracking reports to GA4's recommended events, e.g. `clicked` to `click`, `itemsViewed` to
99
+ * `view_item_list` and `addedToCart` to `add_to_cart`: `DomEvents` with their camelCase fields as snake_case params,
100
+ * `EcommerceEvents` with `item`, `list` and `promotion`, and `CartEvents` with `cartItem`. Events without what they
101
+ * need, e.g. ecommerce events without `item`, aren't mapped. Spread it to add or override events.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * map(gtmDestination(), {
106
+ * ...mapEvery(events, toGa4CustomEvent),
107
+ * ...ga4Mapping<Events>({
108
+ * item: ({ product, variant }) => ({
109
+ * item_id: product.id,
110
+ * item_name: product.title,
111
+ * item_variant: variant?.title,
112
+ * price: Number((variant ?? product).price.amount),
113
+ * }),
114
+ * currency: ({ product }) => product.price.currencyCode,
115
+ * list: ({ id, name }) => ({ item_list_id: id, item_list_name: name }),
116
+ * }),
117
+ * newsletterSignedUp: ({ source }) => ({ name: "generate_lead", payload: { lead_source: source } }),
118
+ * });
119
+ * ```
120
+ */
121
+ declare const ga4Mapping: <Events extends EventMap, Context extends object = Record<never, never>>({ item: toItem, currency, list: toList, promotion: toPromotion, cartItem, }?: Ga4MappingOptions<ShapesOf<Events>, CartItemOf<Events>, Context>) => AnalyticsMapping<Events, GtmEvents, Context>;
122
+ /**
123
+ * For `mapEvery()`, spread before the other entries: an event without one, e.g. a custom one, under its name and with
124
+ * its fields in snake_case, e.g. `newsletterSignedUp` as `newsletter_signed_up`. Unmapped ecommerce events aren't sent, as GA4's
125
+ * tags would read them with the `ecommerce` of an earlier event.
126
+ */
127
+ declare const toGa4CustomEvent: (event: {
128
+ name: string;
129
+ payload: object;
130
+ }) => DestinationEvent<GtmEvents> | null;
131
+ type GtmDestinationOptions = {
132
+ /** Default `"dataLayer"`. */
133
+ dataLayerName?: string;
134
+ };
135
+ /**
136
+ * Pushes events to the GTM dataLayer, with the context as dataLayer variables (`user_data` only with marketing
137
+ * consent), and ecommerce events' params under `ecommerce`. Map it with `ga4Mapping<Events>()`, and give it
138
+ * `withConsent(…, "none")` for Google Consent Mode's advanced setup, where Consent Mode gates tags inside the container, or
139
+ * `["analytics"]` for its basic one. Consent Mode signals come from the consent platform's integration, e.g.
140
+ * Cookiebot's, or from `googleConsentMode()`. See `docs/analytics.md`.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * export const analytics = createAnalytics<Events>({
145
+ * events,
146
+ * consent: cookiebotConsent(),
147
+ * destinations: [
148
+ * withConsent(
149
+ * map(gtmDestination(), { ...mapEvery(events, toGa4CustomEvent), ...ga4Mapping<Events>({ item: toGa4Item }) }),
150
+ * "none",
151
+ * ),
152
+ * ],
153
+ * });
154
+ * ```
155
+ */
156
+ declare const gtmDestination: ({ dataLayerName, }?: GtmDestinationOptions) => AnalyticsDestination<GtmEvents>;
157
+
158
+ export { type Ga4Ecommerce, type Ga4Item, type Ga4List, type Ga4Promotion, type GtmDestinationOptions, type GtmEvents, ga4Mapping, gtmDestination, toGa4CustomEvent };
@@ -0,0 +1,274 @@
1
+ import { IS_DEVELOPMENT } from '../../chunk-GQVN2ECX.js';
2
+
3
+ // src/analytics/google/gtm-destination.ts
4
+ var ECOMMERCE_EVENTS = [
5
+ "view_item_list",
6
+ "select_item",
7
+ "view_item",
8
+ "add_to_wishlist",
9
+ "add_to_cart",
10
+ "remove_from_cart",
11
+ "view_cart",
12
+ "begin_checkout",
13
+ "add_shipping_info",
14
+ "add_payment_info",
15
+ "purchase",
16
+ "refund",
17
+ "view_promotion",
18
+ "select_promotion"
19
+ ];
20
+ var EVENTS = [
21
+ "search",
22
+ "select_content",
23
+ "share",
24
+ "login",
25
+ "sign_up",
26
+ "generate_lead",
27
+ "click",
28
+ "video_start",
29
+ "video_progress",
30
+ "video_complete",
31
+ "form_start",
32
+ "form_submit",
33
+ "scroll"
34
+ ];
35
+ var ECOMMERCE = new Set(ECOMMERCE_EVENTS);
36
+ var toSnakeCase = (key) => key.replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase();
37
+ var toParams = (input, names = {}) => {
38
+ const params = {};
39
+ for (const [key, value] of Object.entries(input)) {
40
+ if (value === void 0) continue;
41
+ params[names[key] ?? toSnakeCase(key)] = value;
42
+ }
43
+ return params;
44
+ };
45
+ var USER_PARAM_NAMES = {
46
+ id: "user_id",
47
+ type: "user_type"
48
+ };
49
+ var CONTEXT_PARAM_NAMES = { locale: "language" };
50
+ var PAGE_CONTEXT_KEYS = /* @__PURE__ */ new Set(["url", "title", "referrer"]);
51
+ var toContextParams = (context, includeUserData) => {
52
+ const { user, ...fields } = context;
53
+ const rest = Object.entries(fields).filter(
54
+ ([key]) => !PAGE_CONTEXT_KEYS.has(key)
55
+ );
56
+ const { email, phone, ...userFields } = user ?? {};
57
+ const userParams = {};
58
+ for (const [key, value] of Object.entries(userFields)) {
59
+ userParams[USER_PARAM_NAMES[key] ?? `user_${toSnakeCase(key)}`] = value;
60
+ }
61
+ return {
62
+ user_id: void 0,
63
+ user_type: void 0,
64
+ ...userParams,
65
+ language: void 0,
66
+ market: void 0,
67
+ ...Object.fromEntries(
68
+ rest.map(([key, value]) => [
69
+ CONTEXT_PARAM_NAMES[key] ?? toSnakeCase(key),
70
+ value
71
+ ])
72
+ ),
73
+ user_data: includeUserData && (email || phone) ? toParams({ email, phone_number: phone }, {}) : void 0
74
+ };
75
+ };
76
+ var toPageView = (payload, context) => {
77
+ const { url = context.url, title = context.title, pageType } = payload;
78
+ return {
79
+ name: "page_view",
80
+ payload: {
81
+ page_location: url,
82
+ page_path: new URL(url, context.url).pathname,
83
+ page_title: title,
84
+ page_referrer: context.referrer,
85
+ // Only on page views, so GTM's data model keeps it for the page's other events.
86
+ page_type: pageType
87
+ }
88
+ };
89
+ };
90
+ var DOM_EVENTS = {
91
+ clicked: "click",
92
+ videoStarted: "video_start",
93
+ videoProgressed: "video_progress",
94
+ videoCompleted: "video_complete",
95
+ formStarted: "form_start",
96
+ formSubmitted: "form_submit",
97
+ scrolled: "scroll"
98
+ };
99
+ var CART_EVENTS = {
100
+ addedToCart: "add_to_cart",
101
+ removedFromCart: "remove_from_cart"
102
+ };
103
+ var TRACKER_ECOMMERCE = new Set(
104
+ Object.keys({
105
+ itemViewed: true,
106
+ itemSelected: true,
107
+ itemsViewed: true,
108
+ promotionViewed: true,
109
+ promotionSelected: true,
110
+ ...CART_EVENTS
111
+ })
112
+ );
113
+ var ga4Mapping = ({
114
+ item: toItem,
115
+ currency,
116
+ list: toList = () => ({}),
117
+ promotion: toPromotion = () => ({}),
118
+ cartItem
119
+ } = {}) => {
120
+ const toCartItem = cartItem ?? toItem;
121
+ const entries = {
122
+ pageViewed: (payload, { context }) => toPageView(payload, context)
123
+ };
124
+ for (const [event, name] of Object.entries(DOM_EVENTS)) {
125
+ entries[event] = (payload) => ({
126
+ name,
127
+ payload: toParams(payload)
128
+ });
129
+ }
130
+ if (toItem) {
131
+ const toPlacedItem = ({
132
+ item,
133
+ list,
134
+ index,
135
+ promotion
136
+ }) => ({
137
+ ...toItem(item),
138
+ ...list ? toList(list) : {},
139
+ ...promotion ? toPromotion(promotion) : {},
140
+ ...index !== void 0 && { index }
141
+ });
142
+ Object.assign(entries, {
143
+ itemViewed: (placed, { context }) => {
144
+ const item = toPlacedItem(placed);
145
+ const itemCurrency = currency?.(placed.item, context);
146
+ return {
147
+ name: "view_item",
148
+ payload: toParams({
149
+ currency: itemCurrency,
150
+ value: itemCurrency && item.price !== void 0 ? item.price * (item.quantity ?? 1) : void 0,
151
+ items: [item]
152
+ })
153
+ };
154
+ },
155
+ itemSelected: ({ list, ...placed }) => ({
156
+ name: "select_item",
157
+ payload: {
158
+ ...list ? toList(list) : {},
159
+ items: [toPlacedItem(placed)]
160
+ }
161
+ }),
162
+ itemsViewed: ({ list, items }) => ({
163
+ name: "view_item_list",
164
+ payload: {
165
+ ...list ? toList(list) : {},
166
+ items: items.map(toPlacedItem)
167
+ }
168
+ }),
169
+ promotionViewed: ({
170
+ promotion,
171
+ items
172
+ }) => ({
173
+ name: "view_promotion",
174
+ payload: {
175
+ ...toPromotion(promotion),
176
+ ...items.length > 0 && { items: items.map((item) => toItem(item)) }
177
+ }
178
+ }),
179
+ promotionSelected: ({
180
+ promotion,
181
+ item
182
+ }) => ({
183
+ name: "select_promotion",
184
+ payload: {
185
+ ...toPromotion(promotion),
186
+ ...item ? { items: [toItem(item)] } : {}
187
+ }
188
+ })
189
+ });
190
+ }
191
+ if (toCartItem) {
192
+ for (const [event, name] of Object.entries(CART_EVENTS)) {
193
+ entries[event] = ({
194
+ currency: cartCurrency,
195
+ value,
196
+ items
197
+ }) => ({
198
+ name,
199
+ payload: {
200
+ currency: cartCurrency,
201
+ value,
202
+ items: items.map(({ item, quantity, price }) => ({
203
+ ...toCartItem(item),
204
+ price,
205
+ quantity
206
+ }))
207
+ }
208
+ });
209
+ }
210
+ }
211
+ return entries;
212
+ };
213
+ var warnedUnmapped = /* @__PURE__ */ new Set();
214
+ var toGa4CustomEvent = (event) => {
215
+ const name = toSnakeCase(event.name);
216
+ if (ECOMMERCE.has(name) || TRACKER_ECOMMERCE.has(event.name)) {
217
+ if (IS_DEVELOPMENT && !warnedUnmapped.has(event.name)) {
218
+ warnedUnmapped.add(event.name);
219
+ console.warn(
220
+ `[analytics] gtm: "${event.name}" isn't mapped. Pass ga4Mapping()'s adapters or map it yourself.`
221
+ );
222
+ }
223
+ return null;
224
+ }
225
+ return {
226
+ name: "custom",
227
+ payload: { event: name, params: toParams(event.payload) }
228
+ };
229
+ };
230
+ var gtmDestination = ({
231
+ dataLayerName = "dataLayer"
232
+ } = {}) => {
233
+ const dataLayer = () => {
234
+ const target = window;
235
+ return target[dataLayerName] ??= [];
236
+ };
237
+ const contextKeys = /* @__PURE__ */ new Set();
238
+ return {
239
+ name: "gtm",
240
+ setup: ({ subscribe }) => {
241
+ const push = (event, params, { id, context, consent }) => {
242
+ const contextParams = toContextParams(context, !!consent?.marketing);
243
+ for (const key of contextKeys) {
244
+ if (!(key in contextParams)) contextParams[key] = void 0;
245
+ }
246
+ Object.keys(contextParams).forEach((key) => contextKeys.add(key));
247
+ if ("ecommerce" in params) dataLayer().push({ ecommerce: null });
248
+ dataLayer().push({ event, event_id: id, ...contextParams, ...params });
249
+ };
250
+ for (const name of [
251
+ ...ECOMMERCE_EVENTS,
252
+ ...EVENTS,
253
+ "page_view"
254
+ ]) {
255
+ subscribe(
256
+ name,
257
+ (params, context) => push(
258
+ name,
259
+ ECOMMERCE.has(name) ? { ecommerce: params } : params,
260
+ context
261
+ )
262
+ );
263
+ }
264
+ subscribe(
265
+ "custom",
266
+ ({ event, params }, context) => push(event, params, context)
267
+ );
268
+ }
269
+ };
270
+ };
271
+
272
+ export { ga4Mapping, gtmDestination, toGa4CustomEvent };
273
+ //# sourceMappingURL=index.js.map
274
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/analytics/google/gtm-destination.ts"],"names":[],"mappings":";;;AAuBA,IAAM,gBAAA,GAAmB;AAAA,EACxB,gBAAA;AAAA,EACA,aAAA;AAAA,EACA,WAAA;AAAA,EACA,iBAAA;AAAA,EACA,aAAA;AAAA,EACA,kBAAA;AAAA,EACA,WAAA;AAAA,EACA,gBAAA;AAAA,EACA,mBAAA;AAAA,EACA,kBAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,gBAAA;AAAA,EACA;AACD,CAAA;AAEA,IAAM,MAAA,GAAS;AAAA,EACd,QAAA;AAAA,EACA,gBAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA,eAAA;AAAA,EACA,OAAA;AAAA,EACA,aAAA;AAAA,EACA,gBAAA;AAAA,EACA,gBAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA;AACD,CAAA;AAEA,IAAM,SAAA,GAAY,IAAI,GAAA,CAAY,gBAAgB,CAAA;AAsElD,IAAM,WAAA,GAAc,CAAC,GAAA,KACpB,GAAA,CACE,OAAA,CAAQ,mBAAA,EAAqB,OAAO,CAAA,CACpC,OAAA,CAAQ,uBAAA,EAAyB,OAAO,CAAA,CACxC,WAAA,EAAY;AAGf,IAAM,QAAA,GAAW,CAChB,KAAA,EACA,KAAA,GAAgC,EAAC,KACJ;AAC7B,EAAA,MAAM,SAAkC,EAAC;AACzC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AACjD,IAAA,IAAI,UAAU,MAAA,EAAW;AACzB,IAAA,MAAA,CAAO,MAAM,GAAG,CAAA,IAAK,WAAA,CAAY,GAAG,CAAC,CAAA,GAAI,KAAA;AAAA,EAC1C;AACA,EAAA,OAAO,MAAA;AACR,CAAA;AAEA,IAAM,gBAAA,GAA2C;AAAA,EAChD,EAAA,EAAI,SAAA;AAAA,EACJ,IAAA,EAAM;AACP,CAAA;AAEA,IAAM,mBAAA,GAA8C,EAAE,MAAA,EAAQ,UAAA,EAAW;AAGzE,IAAM,oCAAoB,IAAI,GAAA,CAAI,CAAC,KAAA,EAAO,OAAA,EAAS,UAAU,CAAC,CAAA;AAMvD,IAAM,eAAA,GAAkB,CAC9B,OAAA,EACA,eAAA,KAC6B;AAC7B,EAAA,MAAM,EAAE,IAAA,EAAM,GAAG,MAAA,EAAO,GAAI,OAAA;AAC5B,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,CAAE,MAAA;AAAA,IACnC,CAAC,CAAC,GAAG,MAAM,CAAC,iBAAA,CAAkB,IAAI,GAAG;AAAA,GACtC;AACA,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAO,GAAG,UAAA,EAAW,GAAI,QAAQ,EAAC;AACjD,EAAA,MAAM,aAAsC,EAAC;AAC7C,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,UAAU,CAAA,EAAG;AACtD,IAAA,UAAA,CAAW,gBAAA,CAAiB,GAAG,CAAA,IAAK,CAAA,KAAA,EAAQ,YAAY,GAAG,CAAC,EAAE,CAAA,GAAI,KAAA;AAAA,EACnE;AAEA,EAAA,OAAO;AAAA,IACN,OAAA,EAAS,MAAA;AAAA,IACT,SAAA,EAAW,MAAA;AAAA,IACX,GAAG,UAAA;AAAA,IACH,QAAA,EAAU,MAAA;AAAA,IACV,MAAA,EAAQ,MAAA;AAAA,IACR,GAAG,MAAA,CAAO,WAAA;AAAA,MACT,KAAK,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,KAAM;AAAA,QAC1B,mBAAA,CAAoB,GAAG,CAAA,IAAK,WAAA,CAAY,GAAG,CAAA;AAAA,QAC3C;AAAA,OACA;AAAA,KACF;AAAA,IACA,SAAA,EACC,eAAA,KAAoB,KAAA,IAAS,KAAA,CAAA,GAC1B,QAAA,CAAS,EAAE,KAAA,EAAO,YAAA,EAAc,KAAA,EAAM,EAAG,EAAE,CAAA,GAC3C;AAAA,GACL;AACD,CAAA;AAoCA,IAAM,UAAA,GAAa,CAClB,OAAA,EACA,OAAA,KACiC;AACjC,EAAA,MAAM,EAAE,MAAM,OAAA,CAAQ,GAAA,EAAK,QAAQ,OAAA,CAAQ,KAAA,EAAO,UAAS,GAAI,OAAA;AAC/D,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,WAAA;AAAA,IACN,OAAA,EAAS;AAAA,MACR,aAAA,EAAe,GAAA;AAAA,MACf,WAAW,IAAI,GAAA,CAAI,GAAA,EAAK,OAAA,CAAQ,GAAG,CAAA,CAAE,QAAA;AAAA,MACrC,UAAA,EAAY,KAAA;AAAA,MACZ,eAAe,OAAA,CAAQ,QAAA;AAAA;AAAA,MAEvB,SAAA,EAAW;AAAA;AACZ,GACD;AACD,CAAA;AAEA,IAAM,UAAA,GAAa;AAAA,EAClB,OAAA,EAAS,OAAA;AAAA,EACT,YAAA,EAAc,aAAA;AAAA,EACd,eAAA,EAAiB,gBAAA;AAAA,EACjB,cAAA,EAAgB,gBAAA;AAAA,EAChB,WAAA,EAAa,YAAA;AAAA,EACb,aAAA,EAAe,aAAA;AAAA,EACf,QAAA,EAAU;AACX,CAAA;AAKA,IAAM,WAAA,GAAc;AAAA,EACnB,WAAA,EAAa,aAAA;AAAA,EACb,eAAA,EAAiB;AAClB,CAAA;AAGA,IAAM,oBAAoB,IAAI,GAAA;AAAA,EAC7B,OAAO,IAAA,CAAK;AAAA,IACX,UAAA,EAAY,IAAA;AAAA,IACZ,YAAA,EAAc,IAAA;AAAA,IACd,WAAA,EAAa,IAAA;AAAA,IACb,eAAA,EAAiB,IAAA;AAAA,IACjB,iBAAA,EAAmB,IAAA;AAAA,IACnB,GAAG;AAAA,GAIH;AACF,CAAA;AA+BO,IAAM,aAAa,CAGxB;AAAA,EACD,IAAA,EAAM,MAAA;AAAA,EACN,QAAA;AAAA,EACA,IAAA,EAAM,MAAA,GAAS,OAAO,EAAC,CAAA;AAAA,EACvB,SAAA,EAAW,WAAA,GAAc,OAAO,EAAC,CAAA;AAAA,EACjC;AACD,CAAA,GAII,EAAC,KAAoD;AAGxD,EAAA,MAAM,aAAc,QAAA,IAAY,MAAA;AAIhC,EAAA,MAAM,OAAA,GAAiC;AAAA,IACtC,UAAA,EAAY,CAAC,OAAA,EAAkC,EAAE,SAAQ,KACxD,UAAA,CAAW,SAAS,OAAO;AAAA,GAC7B;AACA,EAAA,KAAA,MAAW,CAAC,KAAA,EAAO,IAAI,KAAK,MAAA,CAAO,OAAA,CAAQ,UAAU,CAAA,EAAG;AACvD,IAAA,OAAA,CAAQ,KAAK,CAAA,GAAI,CAAC,OAAA,MAAqB;AAAA,MACtC,IAAA;AAAA,MACA,OAAA,EAAS,SAAS,OAAO;AAAA,KAC1B,CAAA;AAAA,EACD;AAEA,EAAA,IAAI,MAAA,EAAQ;AACX,IAAA,MAAM,eAAe,CAAC;AAAA,MACrB,IAAA;AAAA,MACA,IAAA;AAAA,MACA,KAAA;AAAA,MACA;AAAA,KACD,MAAqE;AAAA,MACpE,GAAG,OAAO,IAAI,CAAA;AAAA,MACd,GAAI,IAAA,GAAO,MAAA,CAAO,IAAI,IAAI,EAAC;AAAA,MAC3B,GAAI,SAAA,GAAY,WAAA,CAAY,SAAS,IAAI,EAAC;AAAA,MAC1C,GAAI,KAAA,KAAU,MAAA,IAAa,EAAE,KAAA;AAAM,KACpC,CAAA;AAEA,IAAA,MAAA,CAAO,OAAO,OAAA,EAAS;AAAA,MACtB,UAAA,EAAY,CACX,MAAA,EACA,EAAE,SAAQ,KACN;AACJ,QAAA,MAAM,IAAA,GAAO,aAAa,MAAM,CAAA;AAChC,QAAA,MAAM,YAAA,GAAe,QAAA,GAAW,MAAA,CAAO,IAAA,EAAM,OAAO,CAAA;AACpD,QAAA,OAAO;AAAA,UACN,IAAA,EAAM,WAAA;AAAA,UACN,SAAS,QAAA,CAAS;AAAA,YACjB,QAAA,EAAU,YAAA;AAAA,YACV,KAAA,EACC,gBAAgB,IAAA,CAAK,KAAA,KAAU,SAC5B,IAAA,CAAK,KAAA,IAAS,IAAA,CAAK,QAAA,IAAY,CAAA,CAAA,GAC/B,MAAA;AAAA,YACJ,KAAA,EAAO,CAAC,IAAI;AAAA,WACZ;AAAA,SACF;AAAA,MACD,CAAA;AAAA,MACA,cAAc,CAAC,EAAE,IAAA,EAAM,GAAG,QAAO,MAAkC;AAAA,QAClE,IAAA,EAAM,aAAA;AAAA,QACN,OAAA,EAAS;AAAA,UACR,GAAI,IAAA,GAAO,MAAA,CAAO,IAAI,IAAI,EAAC;AAAA,UAC3B,KAAA,EAAO,CAAC,YAAA,CAAa,MAAM,CAAC;AAAA;AAC7B,OACD,CAAA;AAAA,MACA,WAAA,EAAa,CAAC,EAAE,IAAA,EAAM,OAAM,MAAiC;AAAA,QAC5D,IAAA,EAAM,gBAAA;AAAA,QACN,OAAA,EAAS;AAAA,UACR,GAAI,IAAA,GAAO,MAAA,CAAO,IAAI,IAAI,EAAC;AAAA,UAC3B,KAAA,EAAO,KAAA,CAAM,GAAA,CAAI,YAAY;AAAA;AAC9B,OACD,CAAA;AAAA,MACA,iBAAiB,CAAC;AAAA,QACjB,SAAA;AAAA,QACA;AAAA,OACD,MAAqC;AAAA,QACpC,IAAA,EAAM,gBAAA;AAAA,QACN,OAAA,EAAS;AAAA,UACR,GAAG,YAAY,SAAS,CAAA;AAAA,UACxB,GAAI,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,EAAE,KAAA,EAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,KAAS,MAAA,CAAO,IAAI,CAAC,CAAA;AAAE;AACpE,OACD,CAAA;AAAA,MACA,mBAAmB,CAAC;AAAA,QACnB,SAAA;AAAA,QACA;AAAA,OACD,MAAuC;AAAA,QACtC,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EAAS;AAAA,UACR,GAAG,YAAY,SAAS,CAAA;AAAA,UACxB,GAAI,IAAA,GAAO,EAAE,KAAA,EAAO,CAAC,OAAO,IAAI,CAAC,CAAA,EAAE,GAAI;AAAC;AACzC,OACD;AAAA,KACyC,CAAA;AAAA,EAC3C;AAEA,EAAA,IAAI,UAAA,EAAY;AACf,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,IAAI,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxD,MAAA,OAAA,CAAQ,KAAK,IAAI,CAAC;AAAA,QACjB,QAAA,EAAU,YAAA;AAAA,QACV,KAAA;AAAA,QACA;AAAA,OACD,MAIO;AAAA,QACN,IAAA;AAAA,QACA,OAAA,EAAS;AAAA,UACR,QAAA,EAAU,YAAA;AAAA,UACV,KAAA;AAAA,UACA,KAAA,EAAO,MAAM,GAAA,CAAI,CAAC,EAAE,IAAA,EAAM,QAAA,EAAU,OAAM,MAAO;AAAA,YAChD,GAAG,WAAW,IAAI,CAAA;AAAA,YAClB,KAAA;AAAA,YACA;AAAA,WACD,CAAE;AAAA;AACH,OACD,CAAA;AAAA,IACD;AAAA,EACD;AAEA,EAAA,OAAO,OAAA;AACR;AAEA,IAAM,cAAA,uBAAqB,GAAA,EAAY;AAOhC,IAAM,gBAAA,GAAmB,CAAC,KAAA,KAGS;AACzC,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA;AACnC,EAAA,IAAI,SAAA,CAAU,IAAI,IAAI,CAAA,IAAK,kBAAkB,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,EAAG;AAC7D,IAAA,IAAI,kBAAkB,CAAC,cAAA,CAAe,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,EAAG;AACtD,MAAA,cAAA,CAAe,GAAA,CAAI,MAAM,IAAI,CAAA;AAC7B,MAAA,OAAA,CAAQ,IAAA;AAAA,QACP,CAAA,kBAAA,EAAqB,MAAM,IAAI,CAAA,gEAAA;AAAA,OAChC;AAAA,IACD;AACA,IAAA,OAAO,IAAA;AAAA,EACR;AACA,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,QAAA;AAAA,IACN,OAAA,EAAS,EAAE,KAAA,EAAO,IAAA,EAAM,QAAQ,QAAA,CAAS,KAAA,CAAM,OAAO,CAAA;AAAE,GACzD;AACD;AA4BO,IAAM,iBAAiB,CAAC;AAAA,EAC9B,aAAA,GAAgB;AACjB,CAAA,GAA2B,EAAC,KAAuC;AAClE,EAAA,MAAM,YAAY,MAAM;AACvB,IAAA,MAAM,MAAA,GAAS,MAAA;AACf,IAAA,OAAQ,MAAA,CAAO,aAAa,CAAA,KAAM,EAAC;AAAA,EACpC,CAAA;AAIA,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AAEpC,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,KAAA;AAAA,IACN,KAAA,EAAO,CAAC,EAAE,SAAA,EAAU,KAAM;AACzB,MAAA,MAAM,IAAA,GAAO,CACZ,KAAA,EACA,MAAA,EACA,EAAE,EAAA,EAAI,OAAA,EAAS,SAAQ,KACnB;AACJ,QAAA,MAAM,gBAAgB,eAAA,CAAgB,OAAA,EAAS,CAAC,CAAC,SAAS,SAAS,CAAA;AACnE,QAAA,KAAA,MAAW,OAAO,WAAA,EAAa;AAC9B,UAAA,IAAI,EAAE,GAAA,IAAO,aAAA,CAAA,EAAgB,aAAA,CAAc,GAAG,CAAA,GAAI,MAAA;AAAA,QACnD;AACA,QAAA,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA,CAAE,OAAA,CAAQ,CAAC,GAAA,KAAQ,WAAA,CAAY,GAAA,CAAI,GAAG,CAAC,CAAA;AAEhE,QAAA,IAAI,WAAA,IAAe,QAAQ,SAAA,EAAU,CAAE,KAAK,EAAE,SAAA,EAAW,MAAM,CAAA;AAC/D,QAAA,SAAA,EAAU,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,QAAA,EAAU,IAAI,GAAG,aAAA,EAAe,GAAG,MAAA,EAAQ,CAAA;AAAA,MACtE,CAAA;AAEA,MAAA,KAAA,MAAW,IAAA,IAAQ;AAAA,QAClB,GAAG,gBAAA;AAAA,QACH,GAAG,MAAA;AAAA,QACH;AAAA,OACD,EAAY;AACX,QAAA,SAAA;AAAA,UAAU,IAAA;AAAA,UAAM,CAAC,QAAQ,OAAA,KACxB,IAAA;AAAA,YACC,IAAA;AAAA,YACA,UAAU,GAAA,CAAI,IAAI,IAAI,EAAE,SAAA,EAAW,QAAO,GAAI,MAAA;AAAA,YAC9C;AAAA;AACD,SACD;AAAA,MACD;AACA,MAAA,SAAA;AAAA,QAAU,QAAA;AAAA,QAAU,CAAC,EAAE,KAAA,EAAO,MAAA,IAAU,OAAA,KACvC,IAAA,CAAK,KAAA,EAAO,MAAA,EAAQ,OAAO;AAAA,OAC5B;AAAA,IACD;AAAA,GACD;AACD","file":"index.js","sourcesContent":["import { IS_DEVELOPMENT } from \"../../config\";\nimport type {\n\tCartOccurrences,\n\tEcommerceOccurrences,\n\tEcommerceShapes,\n\tPlacedItem,\n} from \"../../tracking/occurrences\";\nimport type {\n\tAnalyticsDestination,\n\tAnalyticsMapping,\n\tDestinationEvent,\n\tDestinationEventContext,\n} from \"../destination\";\nimport type {\n\tAnalyticsContext,\n\tCartItemOf,\n\tDomEvents,\n\tEcommerceEvents,\n\tEventMap,\n\tShapesOf,\n} from \"../events\";\n\n// Pushed under `ecommerce`, as GA4's ecommerce tags read them there.\nconst ECOMMERCE_EVENTS = [\n\t\"view_item_list\",\n\t\"select_item\",\n\t\"view_item\",\n\t\"add_to_wishlist\",\n\t\"add_to_cart\",\n\t\"remove_from_cart\",\n\t\"view_cart\",\n\t\"begin_checkout\",\n\t\"add_shipping_info\",\n\t\"add_payment_info\",\n\t\"purchase\",\n\t\"refund\",\n\t\"view_promotion\",\n\t\"select_promotion\",\n] as const;\n\nconst EVENTS = [\n\t\"search\",\n\t\"select_content\",\n\t\"share\",\n\t\"login\",\n\t\"sign_up\",\n\t\"generate_lead\",\n\t\"click\",\n\t\"video_start\",\n\t\"video_progress\",\n\t\"video_complete\",\n\t\"form_start\",\n\t\"form_submit\",\n\t\"scroll\",\n] as const;\n\nconst ECOMMERCE = new Set<string>(ECOMMERCE_EVENTS);\n\ntype Ga4Params = Record<string, unknown>;\n\n/** @see https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_to_cart_item */\nexport type Ga4Item = {\n\titem_id?: string;\n\titem_name?: string;\n\taffiliation?: string;\n\tcoupon?: string;\n\tdiscount?: number;\n\tindex?: number;\n\titem_brand?: string;\n\titem_category?: string;\n\titem_category2?: string;\n\titem_category3?: string;\n\titem_category4?: string;\n\titem_category5?: string;\n\titem_list_id?: string;\n\titem_list_name?: string;\n\titem_variant?: string;\n\tlocation_id?: string;\n\t/** Unit price, after discount. */\n\tprice?: number;\n\tquantity?: number;\n\tpromotion_id?: string;\n\tpromotion_name?: string;\n\tcreative_name?: string;\n\tcreative_slot?: string;\n} & Ga4Params;\n\nexport type Ga4List = { item_list_id?: string; item_list_name?: string };\n\nexport type Ga4Promotion = {\n\tpromotion_id?: string;\n\tpromotion_name?: string;\n\tcreative_name?: string;\n\tcreative_slot?: string;\n};\n\n/** An ecommerce event's params, pushed under `ecommerce`. */\nexport type Ga4Ecommerce = {\n\t/** ISO 4217, e.g. `\"EUR\"`. */\n\tcurrency?: string;\n\tvalue?: number;\n\titems?: Ga4Item[];\n} & Ga4Params;\n\n/**\n * What GTM accepts: GA4's events by name, in GA4's params, and `custom` for any other event name. Each is pushed with\n * `event_id` and the context variables, e.g. `{ event: \"login\", event_id, user_id, method }`, and ecommerce events'\n * params under `ecommerce`.\n */\nexport type GtmEvents = {\n\t[N in (typeof ECOMMERCE_EVENTS)[number]]: Ga4Ecommerce;\n} & {\n\t[N in (typeof EVENTS)[number]]: Ga4Params;\n} & {\n\tpage_view: {\n\t\tpage_location: string;\n\t\tpage_path: string;\n\t\tpage_title: string;\n\t\tpage_referrer: string;\n\t\tpage_type?: string;\n\t} & Ga4Params;\n\t/** Pushed as `{ event, ...params }`. */\n\tcustom: { event: string; params: Ga4Params };\n};\n\n// Acronyms stay one word: `viewedFAQ` as `viewed_faq`.\nconst toSnakeCase = (key: string) =>\n\tkey\n\t\t.replace(/([a-z\\d])([A-Z])/g, \"$1_$2\")\n\t\t.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1_$2\")\n\t\t.toLowerCase();\n\n// Without `undefined` fields, so GTM's data model keeps no stale keys.\nconst toParams = (\n\tinput: object,\n\tnames: Record<string, string> = {},\n): Record<string, unknown> => {\n\tconst params: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(input)) {\n\t\tif (value === undefined) continue;\n\t\tparams[names[key] ?? toSnakeCase(key)] = value;\n\t}\n\treturn params;\n};\n\nconst USER_PARAM_NAMES: Record<string, string> = {\n\tid: \"user_id\",\n\ttype: \"user_type\",\n};\n\nconst CONTEXT_PARAM_NAMES: Record<string, string> = { locale: \"language\" };\n\n// Mapped to `page_*` params on page views instead.\nconst PAGE_CONTEXT_KEYS = new Set([\"url\", \"title\", \"referrer\"]);\n\n/**\n * Context as dataLayer variables: built-in and the project's fields, in snake_case, with user fields prefixed `user_`.\n * Email and phone only go into `user_data`, which Google's enhanced conversions read and hash before sending.\n */\nexport const toContextParams = (\n\tcontext: AnalyticsContext,\n\tincludeUserData: boolean,\n): Record<string, unknown> => {\n\tconst { user, ...fields } = context;\n\tconst rest = Object.entries(fields).filter(\n\t\t([key]) => !PAGE_CONTEXT_KEYS.has(key),\n\t);\n\tconst { email, phone, ...userFields } = user ?? {};\n\tconst userParams: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(userFields)) {\n\t\tuserParams[USER_PARAM_NAMES[key] ?? `user_${toSnakeCase(key)}`] = value;\n\t}\n\n\treturn {\n\t\tuser_id: undefined,\n\t\tuser_type: undefined,\n\t\t...userParams,\n\t\tlanguage: undefined,\n\t\tmarket: undefined,\n\t\t...Object.fromEntries(\n\t\t\trest.map(([key, value]) => [\n\t\t\t\tCONTEXT_PARAM_NAMES[key] ?? toSnakeCase(key),\n\t\t\t\tvalue,\n\t\t\t]),\n\t\t),\n\t\tuser_data:\n\t\t\tincludeUserData && (email || phone)\n\t\t\t\t? toParams({ email, phone_number: phone }, {})\n\t\t\t\t: undefined,\n\t};\n};\n\ntype Ga4MappingOptions<\n\tShapes extends EcommerceShapes,\n\tCartItem,\n\tContext extends object,\n> = {\n\t/** For what `ecommerce()` reports: the project's item as a GA4 item, e.g. `({ product }) => ({ item_id: product.id })`. */\n\titem?: (item: Shapes[\"item\"]) => Ga4Item;\n\t/** `itemViewed`'s currency, with its price as `value`, e.g. from the item or the context's market. */\n\tcurrency?: (\n\t\titem: Shapes[\"item\"],\n\t\tcontext: AnalyticsContext<Context>,\n\t) => string | undefined;\n\tlist?: (list: Shapes[\"list\"]) => Ga4List;\n\tpromotion?: (promotion: Shapes[\"promotion\"]) => Ga4Promotion;\n} & ([CartItem] extends [never | Shapes[\"item\"]]\n\t? {\n\t\t\t/**\n\t\t\t * For what `createCartChanges()` reports: the project's cart line item as a GA4 item. Price and quantity come\n\t\t\t * from the line. Defaults to `item`.\n\t\t\t */\n\t\t\tcartItem?: (item: CartItem) => Ga4Item;\n\t\t}\n\t:\n\t\t\t| { item?: undefined; cartItem?: (item: CartItem) => Ga4Item }\n\t\t\t| {\n\t\t\t\t\t/**\n\t\t\t\t\t * For what `createCartChanges()` reports: the project's cart line item as a GA4 item. Price and quantity\n\t\t\t\t\t * come from the line. Required with `item`, as the cart item isn't one.\n\t\t\t\t\t */\n\t\t\t\t\tcartItem: (item: CartItem) => Ga4Item;\n\t\t\t });\n\ntype DomEventName = keyof DomEvents & string;\n\nconst toPageView = (\n\tpayload: DomEvents[\"pageViewed\"],\n\tcontext: AnalyticsContext,\n): DestinationEvent<GtmEvents> => {\n\tconst { url = context.url, title = context.title, pageType } = payload;\n\treturn {\n\t\tname: \"page_view\",\n\t\tpayload: {\n\t\t\tpage_location: url,\n\t\t\tpage_path: new URL(url, context.url).pathname,\n\t\t\tpage_title: title,\n\t\t\tpage_referrer: context.referrer,\n\t\t\t// Only on page views, so GTM's data model keeps it for the page's other events.\n\t\t\tpage_type: pageType,\n\t\t},\n\t};\n};\n\nconst DOM_EVENTS = {\n\tclicked: \"click\",\n\tvideoStarted: \"video_start\",\n\tvideoProgressed: \"video_progress\",\n\tvideoCompleted: \"video_complete\",\n\tformStarted: \"form_start\",\n\tformSubmitted: \"form_submit\",\n\tscrolled: \"scroll\",\n} as const satisfies Record<\n\tExclude<DomEventName, \"pageViewed\">,\n\tkeyof GtmEvents\n>;\n\nconst CART_EVENTS = {\n\taddedToCart: \"add_to_cart\",\n\tremovedFromCart: \"remove_from_cart\",\n} as const satisfies Record<keyof CartOccurrences<object>, keyof GtmEvents>;\n\n// Mapped only with `ga4Mapping()`'s adapters.\nconst TRACKER_ECOMMERCE = new Set<string>(\n\tObject.keys({\n\t\titemViewed: true,\n\t\titemSelected: true,\n\t\titemsViewed: true,\n\t\tpromotionViewed: true,\n\t\tpromotionSelected: true,\n\t\t...CART_EVENTS,\n\t} satisfies Record<\n\t\tkeyof EcommerceOccurrences<EcommerceShapes> | keyof typeof CART_EVENTS,\n\t\tunknown\n\t>),\n);\n\ntype Entry = (\n\tpayload: never,\n\tevent: { context: AnalyticsContext<never> },\n) => DestinationEvent<GtmEvents> | null;\n\n/**\n * Maps what tracking reports to GA4's recommended events, e.g. `clicked` to `click`, `itemsViewed` to\n * `view_item_list` and `addedToCart` to `add_to_cart`: `DomEvents` with their camelCase fields as snake_case params,\n * `EcommerceEvents` with `item`, `list` and `promotion`, and `CartEvents` with `cartItem`. Events without what they\n * need, e.g. ecommerce events without `item`, aren't mapped. Spread it to add or override events.\n *\n * @example\n * ```ts\n * map(gtmDestination(), {\n * ...mapEvery(events, toGa4CustomEvent),\n * ...ga4Mapping<Events>({\n * item: ({ product, variant }) => ({\n * item_id: product.id,\n * item_name: product.title,\n * item_variant: variant?.title,\n * price: Number((variant ?? product).price.amount),\n * }),\n * currency: ({ product }) => product.price.currencyCode,\n * list: ({ id, name }) => ({ item_list_id: id, item_list_name: name }),\n * }),\n * newsletterSignedUp: ({ source }) => ({ name: \"generate_lead\", payload: { lead_source: source } }),\n * });\n * ```\n */\nexport const ga4Mapping = <\n\tEvents extends EventMap,\n\tContext extends object = Record<never, never>,\n>({\n\titem: toItem,\n\tcurrency,\n\tlist: toList = () => ({}),\n\tpromotion: toPromotion = () => ({}),\n\tcartItem,\n}: Ga4MappingOptions<\n\tShapesOf<Events>,\n\tCartItemOf<Events>,\n\tContext\n> = {}): AnalyticsMapping<Events, GtmEvents, Context> => {\n\ttype Shapes = ShapesOf<Events>;\n\ttype Ecommerce = EcommerceEvents<Shapes>;\n\tconst toCartItem = (cartItem ?? toItem) as\n\t\t| ((item: CartItemOf<Events>) => Ga4Item)\n\t\t| undefined;\n\n\tconst entries: Record<string, Entry> = {\n\t\tpageViewed: (payload: DomEvents[\"pageViewed\"], { context }) =>\n\t\t\ttoPageView(payload, context),\n\t};\n\tfor (const [event, name] of Object.entries(DOM_EVENTS)) {\n\t\tentries[event] = (payload: object) => ({\n\t\t\tname,\n\t\t\tpayload: toParams(payload),\n\t\t});\n\t}\n\n\tif (toItem) {\n\t\tconst toPlacedItem = ({\n\t\t\titem,\n\t\t\tlist,\n\t\t\tindex,\n\t\t\tpromotion,\n\t\t}: Omit<PlacedItem<Shapes>, \"list\"> & { list?: Shapes[\"list\"] }) => ({\n\t\t\t...toItem(item),\n\t\t\t...(list ? toList(list) : {}),\n\t\t\t...(promotion ? toPromotion(promotion) : {}),\n\t\t\t...(index !== undefined && { index }),\n\t\t});\n\n\t\tObject.assign(entries, {\n\t\t\titemViewed: (\n\t\t\t\tplaced: Ecommerce[\"itemViewed\"],\n\t\t\t\t{ context }: { context: AnalyticsContext<Context> },\n\t\t\t) => {\n\t\t\t\tconst item = toPlacedItem(placed);\n\t\t\t\tconst itemCurrency = currency?.(placed.item, context);\n\t\t\t\treturn {\n\t\t\t\t\tname: \"view_item\",\n\t\t\t\t\tpayload: toParams({\n\t\t\t\t\t\tcurrency: itemCurrency,\n\t\t\t\t\t\tvalue:\n\t\t\t\t\t\t\titemCurrency && item.price !== undefined\n\t\t\t\t\t\t\t\t? item.price * (item.quantity ?? 1)\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\titems: [item],\n\t\t\t\t\t}),\n\t\t\t\t};\n\t\t\t},\n\t\t\titemSelected: ({ list, ...placed }: Ecommerce[\"itemSelected\"]) => ({\n\t\t\t\tname: \"select_item\",\n\t\t\t\tpayload: {\n\t\t\t\t\t...(list ? toList(list) : {}),\n\t\t\t\t\titems: [toPlacedItem(placed)],\n\t\t\t\t},\n\t\t\t}),\n\t\t\titemsViewed: ({ list, items }: Ecommerce[\"itemsViewed\"]) => ({\n\t\t\t\tname: \"view_item_list\",\n\t\t\t\tpayload: {\n\t\t\t\t\t...(list ? toList(list) : {}),\n\t\t\t\t\titems: items.map(toPlacedItem),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tpromotionViewed: ({\n\t\t\t\tpromotion,\n\t\t\t\titems,\n\t\t\t}: Ecommerce[\"promotionViewed\"]) => ({\n\t\t\t\tname: \"view_promotion\",\n\t\t\t\tpayload: {\n\t\t\t\t\t...toPromotion(promotion),\n\t\t\t\t\t...(items.length > 0 && { items: items.map((item) => toItem(item)) }),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tpromotionSelected: ({\n\t\t\t\tpromotion,\n\t\t\t\titem,\n\t\t\t}: Ecommerce[\"promotionSelected\"]) => ({\n\t\t\t\tname: \"select_promotion\",\n\t\t\t\tpayload: {\n\t\t\t\t\t...toPromotion(promotion),\n\t\t\t\t\t...(item ? { items: [toItem(item)] } : {}),\n\t\t\t\t},\n\t\t\t}),\n\t\t} satisfies Record<keyof Ecommerce, Entry>);\n\t}\n\n\tif (toCartItem) {\n\t\tfor (const [event, name] of Object.entries(CART_EVENTS)) {\n\t\t\tentries[event] = ({\n\t\t\t\tcurrency: cartCurrency,\n\t\t\t\tvalue,\n\t\t\t\titems,\n\t\t\t}: {\n\t\t\t\tcurrency: string;\n\t\t\t\tvalue: number;\n\t\t\t\titems: { item: CartItemOf<Events>; quantity: number; price: number }[];\n\t\t\t}) => ({\n\t\t\t\tname,\n\t\t\t\tpayload: {\n\t\t\t\t\tcurrency: cartCurrency,\n\t\t\t\t\tvalue,\n\t\t\t\t\titems: items.map(({ item, quantity, price }) => ({\n\t\t\t\t\t\t...toCartItem(item),\n\t\t\t\t\t\tprice,\n\t\t\t\t\t\tquantity,\n\t\t\t\t\t})),\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\treturn entries as unknown as AnalyticsMapping<Events, GtmEvents, Context>;\n};\n\nconst warnedUnmapped = new Set<string>();\n\n/**\n * For `mapEvery()`, spread before the other entries: an event without one, e.g. a custom one, under its name and with\n * its fields in snake_case, e.g. `newsletterSignedUp` as `newsletter_signed_up`. Unmapped ecommerce events aren't sent, as GA4's\n * tags would read them with the `ecommerce` of an earlier event.\n */\nexport const toGa4CustomEvent = (event: {\n\tname: string;\n\tpayload: object;\n}): DestinationEvent<GtmEvents> | null => {\n\tconst name = toSnakeCase(event.name);\n\tif (ECOMMERCE.has(name) || TRACKER_ECOMMERCE.has(event.name)) {\n\t\tif (IS_DEVELOPMENT && !warnedUnmapped.has(event.name)) {\n\t\t\twarnedUnmapped.add(event.name);\n\t\t\tconsole.warn(\n\t\t\t\t`[analytics] gtm: \"${event.name}\" isn't mapped. Pass ga4Mapping()'s adapters or map it yourself.`,\n\t\t\t);\n\t\t}\n\t\treturn null;\n\t}\n\treturn {\n\t\tname: \"custom\",\n\t\tpayload: { event: name, params: toParams(event.payload) },\n\t};\n};\n\nexport type GtmDestinationOptions = {\n\t/** Default `\"dataLayer\"`. */\n\tdataLayerName?: string;\n};\n\n/**\n * Pushes events to the GTM dataLayer, with the context as dataLayer variables (`user_data` only with marketing\n * consent), and ecommerce events' params under `ecommerce`. Map it with `ga4Mapping<Events>()`, and give it\n * `withConsent(…, \"none\")` for Google Consent Mode's advanced setup, where Consent Mode gates tags inside the container, or\n * `[\"analytics\"]` for its basic one. Consent Mode signals come from the consent platform's integration, e.g.\n * Cookiebot's, or from `googleConsentMode()`. See `docs/analytics.md`.\n *\n * @example\n * ```ts\n * export const analytics = createAnalytics<Events>({\n * events,\n * consent: cookiebotConsent(),\n * destinations: [\n * withConsent(\n * map(gtmDestination(), { ...mapEvery(events, toGa4CustomEvent), ...ga4Mapping<Events>({ item: toGa4Item }) }),\n * \"none\",\n * ),\n * ],\n * });\n * ```\n */\nexport const gtmDestination = ({\n\tdataLayerName = \"dataLayer\",\n}: GtmDestinationOptions = {}): AnalyticsDestination<GtmEvents> => {\n\tconst dataLayer = () => {\n\t\tconst target = window as unknown as Record<string, unknown[] | undefined>;\n\t\treturn (target[dataLayerName] ??= []);\n\t};\n\n\t// GTM's data model keeps a value until it's overwritten, so a context key that disappears, like `user_id` after\n\t// logout, is pushed as `undefined`.\n\tconst contextKeys = new Set<string>();\n\n\treturn {\n\t\tname: \"gtm\",\n\t\tsetup: ({ subscribe }) => {\n\t\t\tconst push = (\n\t\t\t\tevent: string,\n\t\t\t\tparams: Ga4Params,\n\t\t\t\t{ id, context, consent }: DestinationEventContext,\n\t\t\t) => {\n\t\t\t\tconst contextParams = toContextParams(context, !!consent?.marketing);\n\t\t\t\tfor (const key of contextKeys) {\n\t\t\t\t\tif (!(key in contextParams)) contextParams[key] = undefined;\n\t\t\t\t}\n\t\t\t\tObject.keys(contextParams).forEach((key) => contextKeys.add(key));\n\t\t\t\t// Clears the previous ecommerce object, which GTM would otherwise merge into this one.\n\t\t\t\tif (\"ecommerce\" in params) dataLayer().push({ ecommerce: null });\n\t\t\t\tdataLayer().push({ event, event_id: id, ...contextParams, ...params });\n\t\t\t};\n\n\t\t\tfor (const name of [\n\t\t\t\t...ECOMMERCE_EVENTS,\n\t\t\t\t...EVENTS,\n\t\t\t\t\"page_view\",\n\t\t\t] as const) {\n\t\t\t\tsubscribe(name, (params, context) =>\n\t\t\t\t\tpush(\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tECOMMERCE.has(name) ? { ecommerce: params } : params,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tsubscribe(\"custom\", ({ event, params }, context) =>\n\t\t\t\tpush(event, params, context),\n\t\t\t);\n\t\t},\n\t};\n};\n"]}
@@ -0,0 +1,101 @@
1
+ import { C as ConsentCategory, a as ConsentSource, b as ConsentTarget } from '../index-Zef1nnTP.js';
2
+ import { E as EventMap, A as AnalyticsEventName, a as AnalyticsPayloadArgs, b as AnalyticsContextFields, c as AnalyticsEnvelope, d as EventCatalog } from '../define-events-CxZjoUlG.js';
3
+ export { h as AnalyticsContext, i as AnalyticsUser, f as EventDefinition, g as EventsOf, S as Shape, e as defineEvents, s as shape } from '../define-events-CxZjoUlG.js';
4
+ import { C as ConsentedDestination } from '../destination-5KJ5PeR3.js';
5
+ export { A as AnalyticsDestination, a as AnalyticsMapping, b as ConsentRequirement, D as DestinationEvent, c as DestinationEventContext, d as DestinationSetupContext, M as MappingResult, m as map, e as mapEvery, w as withConsent } from '../destination-5KJ5PeR3.js';
6
+
7
+ type AnalyticsOptions<Events extends EventMap, Context extends object> = {
8
+ /** Every event the project tracks, from `defineEvents()`. */
9
+ events: EventCatalog<Events>;
10
+ /** E.g. `cookiebotConsent()` from `@buildinams/core/consent`. Until answered, events are queued. */
11
+ consent: ConsentSource;
12
+ /** Told every answer, before destinations receive events under it, e.g. `googleConsentMode()`. */
13
+ consentTargets?: ConsentTarget[];
14
+ /** Destinations with the consent they require, from `withConsent()`. */
15
+ destinations?: ConsentedDestination<Events, Context>[];
16
+ /**
17
+ * What reports events to track under their own names, e.g. `createDomWatcher()` and `createCartChanges()`, with their
18
+ * `events` in the catalog. A source feeds one instance: a later `createAnalytics()` with it, e.g. after HMR, takes it
19
+ * over.
20
+ */
21
+ sources?: NoInfer<AnalyticsSource<Events>>[];
22
+ /** Initial `context` for every event, after `url`, `title` and `referrer`. Update it with `setContext()`. */
23
+ context?: AnalyticsContextFields<Context>;
24
+ /**
25
+ * `"deferred"` delivers events to destinations after the next paint, so tracking in an event handler doesn't delay
26
+ * its response (INP), or straight away while the page is hidden or unloading. `"immediate"` delivers them as they're
27
+ * tracked, e.g. in tests. Subscribers always receive them as they're tracked. Either way, a prerendered page delivers
28
+ * nothing until the visitor navigates to it.
29
+ */
30
+ delivery?: "deferred" | "immediate";
31
+ };
32
+ declare const types: unique symbol;
33
+ /** Reports events of the project, e.g. `createDomWatcher()` or `createCartChanges()`. */
34
+ type AnalyticsSource<Events extends EventMap = EventMap> = {
35
+ subscribe: (listener: (event: {
36
+ [N in AnalyticsEventName<Events>]: {
37
+ name: N;
38
+ payload: Events[N];
39
+ };
40
+ }[AnalyticsEventName<Events>]) => void) => () => void;
41
+ };
42
+ /** How a destination receives an event. */
43
+ type AnalyticsRoute = {
44
+ /** The consent categories it waits for, or `"none"`. */
45
+ requires: readonly ConsentCategory[] | "none";
46
+ };
47
+ /** Per event of the catalog, its description and the destinations that receive it, by name. */
48
+ type AnalyticsRoutes<Events extends EventMap> = {
49
+ [N in AnalyticsEventName<Events>]: {
50
+ description: string;
51
+ destinations: Record<string, AnalyticsRoute>;
52
+ };
53
+ };
54
+ type Analytics<Events extends EventMap, Context extends object = Record<never, never>> = {
55
+ /** Type only, so helpers taking the instance infer its events. */
56
+ readonly [types]?: {
57
+ events: Events;
58
+ context: Context;
59
+ };
60
+ track: <N extends AnalyticsEventName<Events>>(name: N, ...args: AnalyticsPayloadArgs<Events, N>) => void;
61
+ /**
62
+ * Merges into the context of events tracked from now on, e.g. `{ user }` after login. Pass `undefined` to clear
63
+ * a field.
64
+ */
65
+ setContext: (fields: AnalyticsContextFields<Context>) => void;
66
+ /** Every tracked event as it's tracked, before consent gating, e.g. to log it. Returns a function that unsubscribes. */
67
+ subscribe: (listener: (event: AnalyticsEnvelope<Events, Context>) => void) => () => void;
68
+ /**
69
+ * Which destinations receive each event, and how, e.g. to check in a test, or for an agent or analyst to read what
70
+ * the project tracks where. Also on the server.
71
+ */
72
+ routes: () => AnalyticsRoutes<Events>;
73
+ };
74
+ /**
75
+ * Creates the analytics bus: one `track()`, typed by the project's `Events` from `defineEvents()`, that forwards to
76
+ * every destination the visitor consented to, and tracks what its `sources` report. `Context` adds fields to the
77
+ * built-in context. Never throws: invalid input and failing destinations
78
+ * are logged instead. Calls on the server do nothing. See `docs/analytics.md`.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const events = defineEvents({
83
+ * ...domWatcher.events,
84
+ * newsletterSignedUp: { description: "The newsletter form was submitted.", payload: shape<{ source: "footer" }>() },
85
+ * });
86
+ *
87
+ * type Events = EventsOf<typeof events>;
88
+ *
89
+ * export const analytics = createAnalytics<Events>({
90
+ * events,
91
+ * consent,
92
+ * sources: [domWatcher],
93
+ * destinations: [withConsent(map(gtmDestination(), ga4Mapping<Events>()), "none")],
94
+ * });
95
+ *
96
+ * analytics.track("newsletterSignedUp", { source: "footer" });
97
+ * ```
98
+ */
99
+ declare const createAnalytics: <Events extends EventMap, Context extends object = Record<never, never>>({ events, consent: consentSource, consentTargets, destinations: allDestinations, sources, context: initialContext, delivery, }: AnalyticsOptions<Events, Context>) => Analytics<Events, Context>;
100
+
101
+ export { type Analytics, AnalyticsContextFields, AnalyticsEnvelope, AnalyticsEventName, type AnalyticsRoute, type AnalyticsRoutes, type AnalyticsSource, ConsentedDestination, EventCatalog, EventMap, createAnalytics };