react-marketing-tools 1.0.0-alpha.2 → 1.0.0-alpha.4

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,154 +1,371 @@
1
- //#region lib/destinations/gtm.ts
2
- var e = /^GTM-[A-Z0-9]+$/, t = () => window.dataLayer ??= [], n = (e) => Array.from(document.scripts).some((t) => t.src === e), r = ({ src: e, nonce: t }) => {
1
+ //#region lib/core/consent.ts
2
+ var e = [
3
+ "analytics",
4
+ "ads",
5
+ "adUserData",
6
+ "adPersonalization"
7
+ ], t = (e) => e === "granted" || e === "denied", n = ({ consent: e, gpc: t }) => {
8
+ let n = t ? "denied" : e;
9
+ return {
10
+ analytics: e,
11
+ ads: n,
12
+ adUserData: n,
13
+ adPersonalization: n
14
+ };
15
+ }, r = (e, t) => ({
16
+ analytics: t.analytics ?? e.analytics,
17
+ ads: t.ads ?? e.ads,
18
+ adUserData: t.adUserData ?? t.ads ?? e.adUserData,
19
+ adPersonalization: t.adPersonalization ?? t.ads ?? e.adPersonalization
20
+ }), i = (e) => ({
21
+ analytics_storage: e.analytics,
22
+ ad_storage: e.ads,
23
+ ad_user_data: e.adUserData,
24
+ ad_personalization: e.adPersonalization
25
+ }), a = (e) => Object.values(e).includes("denied"), o = () => typeof navigator < "u" && navigator.globalPrivacyControl === !0, s = () => window.dataLayer ??= [], c = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/gtag/js")), l = () => window.gtag ??= function() {
26
+ s().push(arguments);
27
+ }, u = ({ consent: e, waitForUpdate: t }) => {
28
+ l()("consent", "default", {
29
+ ...i(e),
30
+ ...a(e) ? { wait_for_update: t } : {}
31
+ });
32
+ }, d = (e) => {
33
+ l()("consent", "update", i(e));
34
+ }, f = (e, t) => {
35
+ let n = URL.canParse(e) ? new URL(e) : void 0;
36
+ if (n?.protocol !== "https:") throw Error(`[react-marketing-tools] ${t} must be an https:// URL (received ${JSON.stringify(e)}).`);
37
+ return n;
38
+ }, p = (e) => Array.from(document.scripts).some((t) => t.src === e), m = ({ src: e, nonce: t }) => {
3
39
  let n = document.createElement("script");
4
40
  n.async = !0, n.src = e, t && n.setAttribute("nonce", t), document.head.append(n);
5
- }, i = ({ containerId: i, loadScript: a = !0, nonce: o }) => {
6
- if (!e.test(i)) throw Error(`[react-marketing-tools] gtm.containerId must look like "GTM-XXXXXXX" (received ${JSON.stringify(i)}).`);
7
- let s = `https://www.googletagmanager.com/gtm.js?id=${i}`;
41
+ }, h = /^G-[A-Z0-9]+$/, g = ({ measurementId: e, pageViews: t = "auto", loadScript: n = !0, serverContainerUrl: r, waitForUpdate: i = 500, nonce: a }) => {
42
+ if (!h.test(e)) throw Error(`[react-marketing-tools] ga4.measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)}).`);
43
+ r !== void 0 && f(r, "ga4.serverContainerUrl");
44
+ let o = `https://www.googletagmanager.com/gtag/js?id=${e}`, s = (e, t) => r ? {
45
+ ...e,
46
+ event_id: t
47
+ } : e;
48
+ return {
49
+ name: "ga4",
50
+ start({ consent: s }) {
51
+ u({
52
+ consent: s,
53
+ waitForUpdate: i
54
+ }), c() || (l()("js", /* @__PURE__ */ new Date()), n && m({
55
+ src: o,
56
+ nonce: a
57
+ })), l()("config", e, {
58
+ ...t === "manual" ? { send_page_view: !1 } : {},
59
+ ...r ? { server_container_url: r } : {}
60
+ });
61
+ },
62
+ track({ name: e, params: t, eventId: n }) {
63
+ l()("event", e, s(t, n));
64
+ },
65
+ consent(e) {
66
+ d(e);
67
+ },
68
+ page({ params: e, eventId: n }) {
69
+ t === "manual" && l()("event", "page_view", s(e, n));
70
+ },
71
+ identify({ userId: e }) {
72
+ l()("set", { user_id: e });
73
+ },
74
+ reset() {
75
+ l()("set", { user_id: null });
76
+ }
77
+ };
78
+ }, _ = /^GTM-[A-Z0-9]+$/, v = (e, t) => {
79
+ let n = f(t ?? "https://www.googletagmanager.com/gtm.js", "gtm.scriptUrl");
80
+ return n.searchParams.set("id", e), n.href;
81
+ }, y = ({ containerId: e, loadScript: t = !0, scriptUrl: n, waitForUpdate: r = 500, nonce: i }) => {
82
+ if (!_.test(e)) throw Error(`[react-marketing-tools] gtm.containerId must look like "GTM-XXXXXXX" (received ${JSON.stringify(e)}).`);
83
+ let a = v(e, n);
8
84
  return {
9
85
  name: "gtm",
10
- start() {
11
- let e = t();
12
- a && !n(s) && (e.push({
86
+ start({ consent: e }) {
87
+ u({
88
+ consent: e,
89
+ waitForUpdate: r
90
+ }), t && !p(a) && (s().push({
13
91
  "gtm.start": Date.now(),
14
92
  event: "gtm.js"
15
- }), r({
16
- src: s,
17
- nonce: o
93
+ }), m({
94
+ src: a,
95
+ nonce: i
18
96
  }));
19
97
  },
20
- track({ name: e, params: n, eventId: r }) {
21
- t().push({
22
- ...n,
98
+ track({ name: e, params: t, eventId: n }) {
99
+ s().push({
100
+ ...t,
23
101
  event: e,
24
- event_id: r
102
+ event_id: n
25
103
  });
26
104
  },
105
+ consent(e) {
106
+ d(e);
107
+ },
27
108
  identify({ userId: e }) {
28
- t().push({
109
+ s().push({
29
110
  event: "identify",
30
111
  user_id: e
31
112
  });
32
113
  },
33
114
  reset() {
34
- t().push({
115
+ s().push({
35
116
  event: "reset",
36
117
  user_id: void 0
37
118
  });
38
119
  }
39
120
  };
40
- }, a = class extends Error {
121
+ }, b = /* @__PURE__ */ new Set([
122
+ "AddPaymentInfo",
123
+ "AddToCart",
124
+ "AddToWishlist",
125
+ "CompleteRegistration",
126
+ "Contact",
127
+ "CustomizeProduct",
128
+ "Donate",
129
+ "FindLocation",
130
+ "InitiateCheckout",
131
+ "Lead",
132
+ "PageView",
133
+ "Purchase",
134
+ "Schedule",
135
+ "Search",
136
+ "StartTrial",
137
+ "SubmitApplication",
138
+ "Subscribe",
139
+ "ViewContent"
140
+ ]), x = {
141
+ purchase: "Purchase",
142
+ sign_up: "CompleteRegistration",
143
+ generate_lead: "Lead",
144
+ add_to_cart: "AddToCart",
145
+ begin_checkout: "InitiateCheckout",
146
+ view_item: "ViewContent",
147
+ search: "Search",
148
+ add_payment_info: "AddPaymentInfo",
149
+ add_to_wishlist: "AddToWishlist"
150
+ }, S = (e) => typeof e == "object" && !!e, C = (e) => {
151
+ let t = e.filter(S).filter((e) => e.item_id !== void 0).map((e) => ({
152
+ id: String(e.item_id),
153
+ quantity: typeof e.quantity == "number" ? e.quantity : 1
154
+ }));
155
+ return {
156
+ content_ids: t.map((e) => e.id),
157
+ contents: t,
158
+ num_items: t.reduce((e, t) => e + t.quantity, 0),
159
+ content_type: "product"
160
+ };
161
+ }, w = ({ items: e, search_term: t, ...n }) => ({
162
+ ...n,
163
+ ...typeof t == "string" ? { search_string: t } : {},
164
+ ...Array.isArray(e) ? C(e) : {}
165
+ }), T = ({ name: e, params: t, options: n }) => {
166
+ let r = n?.meta, i = r?.event ?? x[e] ?? e;
167
+ return {
168
+ command: b.has(i) ? "track" : "trackCustom",
169
+ name: i,
170
+ params: {
171
+ ...w(t),
172
+ ...r?.params
173
+ }
174
+ };
175
+ }, E = /^\d+$/, D = "https://connect.facebook.net/en_US/fbevents.js", O = () => {
176
+ let e = window;
177
+ if (!e.fbq) {
178
+ let t = function() {
179
+ let e = arguments;
180
+ t.callMethod ? t.callMethod.call(t, ...e) : t.queue.push(e);
181
+ };
182
+ t.push = t, t.loaded = !0, t.version = "2.0", t.queue = [], e.fbq = t, e._fbq ??= t;
183
+ }
184
+ return e.fbq;
185
+ }, k = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/fbevents.js")), A = ({ userId: e, traits: t }) => Object.fromEntries(Object.entries({
186
+ external_id: e,
187
+ em: t.email,
188
+ ph: t.phone,
189
+ fn: t.firstName,
190
+ ln: t.lastName
191
+ }).filter(([, e]) => e !== void 0)), j = (e) => e.adUserData === "granted" ? "grant" : "revoke", M = ({ pixelId: e, pageViews: t = "auto", loadScript: n = !0, nonce: r }) => {
192
+ if (!E.test(e)) throw Error(`[react-marketing-tools] metaPixel.pixelId must be the numeric pixel ID (received ${JSON.stringify(e)}).`);
193
+ return {
194
+ name: "metaPixel",
195
+ start({ consent: i, identity: a }) {
196
+ let o = O();
197
+ t === "manual" && (o.disablePushState = !0), j(i) === "revoke" && o("consent", "revoke"), a ? o("init", e, A(a)) : o("init", e), t === "auto" && o("track", "PageView"), n && !k() && m({
198
+ src: D,
199
+ nonce: r
200
+ });
201
+ },
202
+ track(e) {
203
+ let { command: t, name: n, params: r } = T(e);
204
+ O()(t, n, r, { eventID: e.eventId });
205
+ },
206
+ page({ eventId: e }) {
207
+ t === "manual" && O()("track", "PageView", {}, { eventID: e });
208
+ },
209
+ consent(e) {
210
+ O()("consent", j(e));
211
+ }
212
+ };
213
+ }, N = class extends Error {
41
214
  code;
42
215
  constructor(e, t, n) {
43
216
  super(`[react-marketing-tools] ${t}`, n), this.name = "AnalyticsError", this.code = e;
44
217
  }
45
- }, o = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, s = [
218
+ }, P = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, F = [
46
219
  "google_",
47
220
  "ga_",
48
221
  "firebase_"
49
- ], c = 25, l = 100, u = {
222
+ ], I = 25, L = 100, R = {
50
223
  page_location: 1e3,
51
224
  page_referrer: 420,
52
225
  page_title: 300
53
- }, d = [
226
+ }, z = [
54
227
  "email",
55
228
  "phone",
56
229
  "first_name",
57
230
  "last_name",
58
231
  "address",
59
232
  "password"
60
- ], f = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, p = "[redacted]", m = (e) => {
61
- if (!o.test(e)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
62
- let t = s.find((t) => e.toLowerCase().startsWith(t));
233
+ ], B = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, V = "[redacted]", H = (e) => {
234
+ if (!P.test(e)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
235
+ let t = F.find((t) => e.toLowerCase().startsWith(t));
63
236
  return t && `must not start with the reserved prefix "${t}"`;
64
- }, h = (e) => {
65
- let t = m(e);
237
+ }, U = (e) => {
238
+ let t = H(e);
66
239
  return t && `event name "${e}" ${t}`;
67
- }, g = (e) => {
240
+ }, W = (e) => {
68
241
  let t = Object.keys(e), n = t.flatMap((t) => {
69
- let n = m(t);
242
+ let n = H(t);
70
243
  if (n) return [`param "${t}" ${n}`];
71
- let r = e[t], i = u[t] ?? l;
244
+ let r = e[t], i = R[t] ?? L;
72
245
  return typeof r == "string" && r.length > i ? [`param "${t}" is longer than ${i} characters`] : [];
73
246
  });
74
- return t.length > c ? [`has ${t.length} params; the limit is ${c}`, ...n] : n;
75
- }, _ = (e) => new RegExp(f, "i").test(e), v = (e, t) => (typeof t == "string" || typeof t == "number") && d.some((t) => e.toLowerCase().includes(t)) ? p : typeof t == "string" ? t.replace(new RegExp(f, "gi"), p) : t, y = (e) => {
247
+ return t.length > I ? [`has ${t.length} params; the limit is ${I}`, ...n] : n;
248
+ }, G = (e) => new RegExp(B, "i").test(e), K = (e, t) => (typeof t == "string" || typeof t == "number") && z.some((t) => e.toLowerCase().includes(t)) ? V : typeof t == "string" ? t.replace(new RegExp(B, "gi"), V) : t, q = (e) => {
76
249
  let t = Object.entries(e).map(([e, t]) => [
77
250
  e,
78
251
  t,
79
- v(e, t)
252
+ K(e, t)
80
253
  ]);
81
254
  return {
82
255
  params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
83
256
  redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
84
257
  };
85
- }, b = () => typeof window < "u", x = () => typeof crypto.randomUUID == "function" ? crypto.randomUUID() : "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (e) => (Number(e) ^ crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(1))[0] & 15 >> Number(e) / 4).toString(16)), S = (e) => {
258
+ }, J = () => typeof window < "u", Y = () => typeof crypto.randomUUID == "function" ? crypto.randomUUID() : "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (e) => (Number(e) ^ crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(1))[0] & 15 >> Number(e) / 4).toString(16)), X = (e) => {
86
259
  if (e.consent !== "granted" && e.consent !== "denied") throw Error(`[react-marketing-tools] createAnalytics: "consent" must be 'granted' or 'denied' (received ${JSON.stringify(e.consent)}).`);
87
- }, C = (e) => {
88
- S(e);
89
- let { debug: t = !1, onError: n = console.error } = e, r = [...e.gtm ? [i({
90
- ...e.gtm,
91
- nonce: e.nonce
92
- })] : [], ...e.destinations ?? []], o = [], s = !1, c = (e) => {
93
- if (t) throw e;
94
- n(e);
95
- }, l = (e) => {
96
- for (let t of r) try {
260
+ }, Z = (i) => {
261
+ X(i);
262
+ let { debug: a = !1, onError: s = console.error, respectGpc: c = !0 } = i, l = n({
263
+ consent: i.consent,
264
+ gpc: c && o()
265
+ }), u = l, d, f = [
266
+ ...i.gtm ? [y({
267
+ ...i.gtm,
268
+ nonce: i.nonce
269
+ })] : [],
270
+ ...i.ga4 ? [g({
271
+ ...i.ga4,
272
+ nonce: i.nonce
273
+ })] : [],
274
+ ...i.metaPixel ? [M({
275
+ ...i.metaPixel,
276
+ nonce: i.nonce
277
+ })] : [],
278
+ ...i.destinations ?? []
279
+ ], p = [], m = !1, h = (e) => {
280
+ if (a) throw e;
281
+ s(e);
282
+ }, _ = (e) => {
283
+ for (let t of f) try {
97
284
  e(t);
98
285
  } catch (e) {
99
- n(new a("destination_failed", `destination "${t.name}" failed`, { cause: e }));
286
+ s(new N("destination_failed", `destination "${t.name}" failed`, { cause: e }));
100
287
  }
101
- }, u = (e) => {
102
- s ? l(e) : o.push(e);
103
- }, d = (e, t) => {
104
- let n = h(e);
105
- if (n) {
106
- c(new a("invalid_event", n));
288
+ }, v = (e) => {
289
+ m ? _(e) : p.push(e);
290
+ }, b = ({ name: e, params: t, options: n }) => {
291
+ let r = U(e);
292
+ if (r) {
293
+ h(new N("invalid_event", r));
107
294
  return;
108
295
  }
109
- for (let n of g(t)) c(new a("invalid_param", `event "${e}" ${n}`));
110
- let { params: r, redactedKeys: i } = y(t);
111
- return i.length > 0 && c(new a("pii_redacted", `event "${e}": personal data redacted from ${i.join(", ")}`)), {
296
+ for (let n of W(t)) h(new N("invalid_param", `event "${e}" ${n}`));
297
+ let i = q(t), a = n?.meta?.params && q(n.meta.params), o = [...i.redactedKeys, ...a?.redactedKeys.map((e) => `meta.${e}`) ?? []];
298
+ return o.length > 0 && h(new N("pii_redacted", `event "${e}": personal data redacted from ${o.join(", ")}`)), {
112
299
  name: e,
113
- params: r,
114
- eventId: x(),
300
+ params: i.params,
301
+ ...n && { options: a ? {
302
+ ...n,
303
+ meta: {
304
+ ...n.meta,
305
+ params: a.params
306
+ }
307
+ } : n },
308
+ eventId: Y(),
115
309
  timestamp: Date.now()
116
310
  };
117
311
  };
118
312
  return {
119
313
  start() {
120
- !s && b() && (s = !0, l((e) => e.start()), o.splice(0).forEach(l));
314
+ !m && J() && (m = !0, _((e) => e.start({
315
+ consent: l,
316
+ identity: d
317
+ })), p.splice(0).forEach(_));
121
318
  },
122
- track(e, t = {}) {
123
- if (!b()) return;
124
- let n = d(e, t);
125
- n && u((e) => e.track(n));
319
+ track(e, t = {}, n) {
320
+ if (!J()) return;
321
+ let r = b({
322
+ name: e,
323
+ params: t,
324
+ options: n
325
+ });
326
+ r && v((e) => e.track(r));
126
327
  },
127
328
  page(e = {}) {
128
- if (!b()) return;
129
- let t = d("page_view", {
130
- page_location: location.href,
131
- page_title: document.title,
132
- ...e
329
+ if (!J()) return;
330
+ let t = b({
331
+ name: "page_view",
332
+ params: {
333
+ page_location: location.href,
334
+ page_title: document.title,
335
+ ...e
336
+ }
133
337
  });
134
- t && u((e) => e.page ? e.page(t) : e.track(t));
338
+ t && v((e) => e.page ? e.page(t) : e.track(t));
135
339
  },
136
340
  identify(e, t = {}) {
137
- if (b()) {
138
- if (!e || _(e)) {
139
- c(new a("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
140
- return;
141
- }
142
- u((n) => n.identify?.({
143
- userId: e,
144
- traits: t
145
- }));
341
+ if (!J()) return;
342
+ if (!e || G(e)) {
343
+ h(new N("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
344
+ return;
146
345
  }
346
+ let n = {
347
+ userId: e,
348
+ traits: t
349
+ };
350
+ d = n, v((e) => e.identify?.(n));
147
351
  },
148
352
  reset() {
149
- b() && u((e) => e.reset?.());
353
+ J() && (d = void 0, v((e) => e.reset?.()));
354
+ },
355
+ consent: {
356
+ update(n) {
357
+ if (!J()) return;
358
+ let i = Object.entries(n).filter(([n, r]) => r !== void 0 && !(e.includes(n) && t(r)));
359
+ if (i.length > 0) {
360
+ h(new N("invalid_consent", `consent.update() ignored: ${i.map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(", ")}. Use analytics, ads, adUserData or adPersonalization with 'granted' or 'denied'.`));
361
+ return;
362
+ }
363
+ let a = r(u, n);
364
+ u = a, v((e) => e.consent?.(a));
365
+ },
366
+ get: () => u
150
367
  }
151
368
  };
152
369
  };
153
370
  //#endregion
154
- export { a as n, C as t };
371
+ export { N as n, Z as t };
@@ -0,0 +1,20 @@
1
+ import type { ConsentState, ConsentStatus, ConsentUpdate } from './types.js';
2
+ export declare const CONSENT_KEYS: readonly (keyof ConsentState)[];
3
+ export declare const isConsentStatus: (value: unknown) => value is ConsentStatus;
4
+ /** Every signal follows `consent`, except that Global Privacy Control (an ad opt-out) denies the advertising signals. */
5
+ export declare const initialConsentState: ({ consent, gpc, }: {
6
+ consent: ConsentStatus;
7
+ gpc: boolean;
8
+ }) => ConsentState;
9
+ /** `ads` also sets `adUserData` and `adPersonalization`, unless the update gives them explicitly (e.g. TCF-style CMPs). */
10
+ export declare const applyConsentUpdate: (state: ConsentState, update: ConsentUpdate) => ConsentState;
11
+ /** Google Consent Mode v2 signals. */
12
+ export declare const toGoogleConsent: (state: ConsentState) => {
13
+ analytics_storage: ConsentStatus;
14
+ ad_storage: ConsentStatus;
15
+ ad_user_data: ConsentStatus;
16
+ ad_personalization: ConsentStatus;
17
+ };
18
+ export declare const hasDeniedSignal: (state: ConsentState) => boolean;
19
+ /** Global Privacy Control: a browser-level "do not sell or share" signal. Absent in browsers that don't support it. */
20
+ export declare const readGlobalPrivacyControl: () => boolean;
@@ -1,4 +1,4 @@
1
- export type AnalyticsErrorCode = 'invalid_event' | 'invalid_param' | 'invalid_user_id' | 'pii_redacted' | 'destination_failed';
1
+ export type AnalyticsErrorCode = 'invalid_event' | 'invalid_param' | 'invalid_user_id' | 'invalid_consent' | 'pii_redacted' | 'destination_failed';
2
2
  /** Every problem the library reports to `onError`, or throws when `debug` is on. */
3
3
  export declare class AnalyticsError extends Error {
4
4
  readonly code: AnalyticsErrorCode;
@@ -1,10 +1,30 @@
1
1
  import type { AnalyticsError } from './errors.js';
2
2
  export type ConsentStatus = 'granted' | 'denied';
3
+ /**
4
+ * The visitor's consent, per purpose. `ads` covers advertising storage; `adUserData` (sending user data to ad platforms)
5
+ * and `adPersonalization` follow `ads` unless set explicitly.
6
+ */
7
+ export type ConsentState = {
8
+ analytics: ConsentStatus;
9
+ ads: ConsentStatus;
10
+ adUserData: ConsentStatus;
11
+ adPersonalization: ConsentStatus;
12
+ };
13
+ export type ConsentUpdate = Partial<ConsentState>;
3
14
  /** Event parameters, passed to every destination as-is. */
4
15
  export type EventParams = Record<string, unknown>;
16
+ /** Per-call adjustments for one destination, when the automatic mapping isn't what you want. */
17
+ export type TrackOptions = {
18
+ /** Meta Pixel: send as this event name (standard or custom), with these params merged over the mapped ones. */
19
+ meta?: {
20
+ event?: string;
21
+ params?: EventParams;
22
+ };
23
+ };
5
24
  export type AnalyticsEvent = {
6
25
  name: string;
7
26
  params: EventParams;
27
+ options?: TrackOptions;
8
28
  /** Unique per `track()` call and shared by every destination, so vendors can deduplicate the same event. */
9
29
  eventId: string;
10
30
  /** Milliseconds since the Unix epoch at the moment `track()` was called. */
@@ -27,9 +47,17 @@ export type Identity = {
27
47
  /** Somewhere events are sent. Built-in destinations are configured by key; custom ones go in `destinations`. */
28
48
  export type Destination = {
29
49
  name: string;
30
- /** Called once, in the browser, by `analytics.start()`. */
31
- start(): void;
50
+ /**
51
+ * Called once, in the browser, by `analytics.start()`, with the consent state the page started with and the user
52
+ * identified before start, if any. Some vendors (the Meta Pixel) only accept user data when they initialise.
53
+ */
54
+ start(context: {
55
+ consent: ConsentState;
56
+ identity?: Identity;
57
+ }): void;
32
58
  track(event: AnalyticsEvent): void;
59
+ /** Receives every consent change, in order with events. */
60
+ consent?(state: ConsentState): void;
33
61
  /** Receives `page_view` events. Destinations without it get them through `track`. */
34
62
  page?(event: AnalyticsEvent): void;
35
63
  identify?(identity: Identity): void;
@@ -41,11 +69,48 @@ export type GtmConfig = {
41
69
  containerId: string;
42
70
  /** Set to `false` when the page already includes the GTM snippet. Defaults to `true`. */
43
71
  loadScript?: boolean;
72
+ /** Load `gtm.js` from this https URL instead of googletagmanager.com, e.g. your server-side GTM domain. */
73
+ scriptUrl?: string;
74
+ /** Milliseconds tags wait for a consent update when a Consent Mode signal starts denied. Defaults to 500. */
75
+ waitForUpdate?: number;
76
+ };
77
+ export type Ga4Config = {
78
+ /** Google Analytics 4 measurement ID, e.g. `G-XXXXXXX`. */
79
+ measurementId: string;
80
+ /**
81
+ * `'auto'` (default): GA4 records page views itself, including history changes when Enhanced Measurement is on.
82
+ * `'manual'`: only `analytics.page()` sends page views.
83
+ */
84
+ pageViews?: 'auto' | 'manual';
85
+ /** Set to `false` when the page already loads gtag.js. Defaults to `true`. */
86
+ loadScript?: boolean;
87
+ /** Your server-side GTM URL (https). Hits go there instead of Google, and events carry `event_id`. */
88
+ serverContainerUrl?: string;
89
+ /** Milliseconds tags wait for a consent update when a Consent Mode signal starts denied. Defaults to 500. */
90
+ waitForUpdate?: number;
91
+ };
92
+ export type MetaPixelConfig = {
93
+ /** Meta Pixel (dataset) ID, e.g. `1234567890123456`. */
94
+ pixelId: string;
95
+ /**
96
+ * `'auto'` (default): the Pixel sends PageView on load and on client-side navigation itself.
97
+ * `'manual'`: only `analytics.page()` sends PageView.
98
+ */
99
+ pageViews?: 'auto' | 'manual';
100
+ /** Set to `false` when the page already includes the Meta Pixel base code. Defaults to `true`. */
101
+ loadScript?: boolean;
44
102
  };
45
103
  export type AnalyticsConfig = {
46
- /** Initial consent state. Required, so every site makes an explicit choice. */
104
+ /** Initial consent for every purpose. Required, so every site makes an explicit choice. */
47
105
  consent: ConsentStatus;
106
+ /**
107
+ * Honour the browser's Global Privacy Control signal by starting the advertising signals denied. Defaults to `true`.
108
+ * An explicit `analytics.consent.update()` still wins.
109
+ */
110
+ respectGpc?: boolean;
48
111
  gtm?: GtmConfig;
112
+ ga4?: Ga4Config;
113
+ metaPixel?: MetaPixelConfig;
49
114
  /** Custom destinations, in addition to the built-in ones. */
50
115
  destinations?: Destination[];
51
116
  /** Content-Security-Policy nonce added to every script the library injects. */
@@ -59,11 +124,16 @@ export type Analytics = {
59
124
  /** Loads vendor scripts and delivers queued events. Safe to call more than once; does nothing outside the browser. */
60
125
  start(): void;
61
126
  /** Sends an event to every destination, queued until `start()`. Does nothing outside the browser. */
62
- track(name: string, params?: EventParams): void;
127
+ track(name: string, params?: EventParams, options?: TrackOptions): void;
63
128
  /** Sends a `page_view` with the current `page_location` and `page_title`, plus any params given. */
64
129
  page(params?: EventParams): void;
65
130
  /** Associates later events with a user. `userId` must not be personal data such as an email address. */
66
131
  identify(userId: string, traits?: IdentityTraits): void;
67
132
  /** Forgets the identified user, e.g. on logout. */
68
133
  reset(): void;
134
+ consent: {
135
+ /** Records the visitor's choice, e.g. from your consent banner. Omitted purposes keep their current state. */
136
+ update(update: ConsentUpdate): void;
137
+ get(): ConsentState;
138
+ };
69
139
  };
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { createAnalytics } from './core/createAnalytics.js';
2
2
  export { AnalyticsError } from './core/errors.js';
3
3
  export type { AnalyticsErrorCode } from './core/errors.js';
4
- export type { Analytics, AnalyticsConfig, AnalyticsEvent, ConsentStatus, Destination, EventParams, GtmConfig, Identity, IdentityTraits, } from './core/types.js';
4
+ export type { Analytics, AnalyticsConfig, AnalyticsEvent, ConsentState, ConsentStatus, ConsentUpdate, Destination, EventParams, Ga4Config, GtmConfig, Identity, IdentityTraits, MetaPixelConfig, TrackOptions, } from './core/types.js';
@@ -0,0 +1,6 @@
1
+ import type { Destination, Ga4Config } from '../core/types.js';
2
+ type Ga4DestinationOptions = Ga4Config & {
3
+ nonce?: string;
4
+ };
5
+ export declare const createGa4Destination: ({ measurementId, pageViews, loadScript, serverContainerUrl, waitForUpdate, nonce, }: Ga4DestinationOptions) => Destination;
6
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { ConsentState } from '../core/types.js';
2
+ type Gtag = (...args: unknown[]) => void;
3
+ export declare const dataLayer: () => unknown[];
4
+ /**
5
+ * Whether the page already loads the gtag.js library (any stream, Google- or first-party-hosted). Checked by script rather
6
+ * than by `window.gtag`, because the Consent Mode default defines the gtag stub before any library loads.
7
+ */
8
+ export declare const isGtagLibraryOnPage: () => boolean;
9
+ /** The page's `gtag()`, defining Google's official command-queue stub when the page doesn't have one yet. */
10
+ export declare const gtag: () => Gtag;
11
+ /**
12
+ * Consent Mode v2 default. It must be queued before any config or container load. When a signal starts denied, tags
13
+ * wait `waitForUpdate` ms for the visitor's choice before firing.
14
+ */
15
+ export declare const setDefaultConsent: ({ consent, waitForUpdate, }: {
16
+ consent: ConsentState;
17
+ waitForUpdate: number;
18
+ }) => void;
19
+ export declare const updateConsent: (consent: ConsentState) => void;
20
+ /** Parses an option that must be an https:// URL, failing fast with the option's name. */
21
+ export declare const toHttpsUrl: (value: string, option: string) => URL;
22
+ export {};
@@ -2,5 +2,5 @@ import type { Destination, GtmConfig } from '../core/types.js';
2
2
  type GtmDestinationOptions = GtmConfig & {
3
3
  nonce?: string;
4
4
  };
5
- export declare const createGtmDestination: ({ containerId, loadScript, nonce, }: GtmDestinationOptions) => Destination;
5
+ export declare const createGtmDestination: ({ containerId, loadScript, scriptUrl, waitForUpdate, nonce, }: GtmDestinationOptions) => Destination;
6
6
  export {};
@@ -0,0 +1,6 @@
1
+ export declare const isScriptOnPage: (src: string) => boolean;
2
+ /** Appends an async vendor script, carrying the page's CSP nonce when there is one. */
3
+ export declare const injectScript: ({ src, nonce, }: {
4
+ src: string;
5
+ nonce?: string;
6
+ }) => void;
@@ -0,0 +1,12 @@
1
+ import type { AnalyticsEvent, EventParams } from '../core/types.js';
2
+ /** GA4 recommended event → the Meta standard event with the same meaning. */
3
+ export declare const GA4_TO_META_EVENT: Readonly<Record<string, string>>;
4
+ /** GA4-style params → Meta standard params; everything else passes through. */
5
+ export declare const toMetaParams: ({ items, search_term: searchTerm, ...rest }: EventParams) => EventParams;
6
+ export type MetaCall = {
7
+ command: 'track' | 'trackCustom';
8
+ name: string;
9
+ params: EventParams;
10
+ };
11
+ /** How an event reaches the Pixel: a standard event through `track`, anything else through `trackCustom`. */
12
+ export declare const toMetaCall: ({ name, params, options, }: AnalyticsEvent) => MetaCall;
@@ -0,0 +1,6 @@
1
+ import type { Destination, MetaPixelConfig } from '../core/types.js';
2
+ type MetaPixelDestinationOptions = MetaPixelConfig & {
3
+ nonce?: string;
4
+ };
5
+ export declare const createMetaPixelDestination: ({ pixelId, pageViews, loadScript, nonce, }: MetaPixelDestinationOptions) => Destination;
6
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-marketing-tools",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.2",
4
+ "version": "1.0.0-alpha.4",
5
5
  "description": "React Marketing Tools are a set of tools to make it easier for you to implement analytics and track user journeys, interactions throughout your App. using dataLayer/Google Tag Manager, GA4 fetch directly or coming soon facebook pixel.",
6
6
  "license": "MIT",
7
7
  "author": "bronz3beard <exempli.gratia.webdesign@gmail.com> (https://www.heyrory.com/)",
@@ -63,21 +63,6 @@
63
63
  "test:watch": "vitest",
64
64
  "typecheck": "tsc --noEmit"
65
65
  },
66
- "browserslist": {
67
- "production": [
68
- ">0.2%",
69
- "not dead",
70
- "not op_mini all"
71
- ],
72
- "development": [
73
- "last 1 chrome version",
74
- "last 1 firefox version",
75
- "last 1 safari version"
76
- ]
77
- },
78
- "dependencies": {
79
- "device-detector-js": "^3.0.3"
80
- },
81
66
  "peerDependencies": {
82
67
  "react": ">=18.0.0"
83
68
  },
@@ -1,13 +0,0 @@
1
- import { TrackAnalyticsEventOptions } from '../types/index.js';
2
- /**
3
- *
4
- * @paramType {object} options
5
- * @property {object} data this is an object of any data you want to collect in analytics
6
- * @property {object} eventNameInfo
7
- * @property {string} analyticsType
8
- * @property {array | undefined} userDataKeysToHashArray default for this value is null, when used it should include an array of strings you wish to hash.
9
- * @property {boolean} dataLayerCheck
10
- * @property {object} consoleLogData (optional) is an object with the following attributes, "showGlobalVars", "showJourneyPropsPayload", "showUserProps" if any of the values are true you will be able to see the respective payload in your console.
11
- */
12
- declare const trackAnalyticsEvent: (options: TrackAnalyticsEventOptions) => Promise<void>;
13
- export { trackAnalyticsEvent };
@@ -1,3 +0,0 @@
1
- import { Ga4GoogleAnalyticsEventTracking } from '../types';
2
- declare const ga4GoogleAnalyticsEventTracking: (options: Ga4GoogleAnalyticsEventTracking) => Promise<void>;
3
- export default ga4GoogleAnalyticsEventTracking;
@@ -1,3 +0,0 @@
1
- import { HandleDataLayerPushOptions } from '../types';
2
- declare const handleDataLayerPush: (options: HandleDataLayerPushOptions) => Promise<void>;
3
- export default handleDataLayerPush;
@@ -1,41 +0,0 @@
1
- import DeviceDetector from 'device-detector-js';
2
- import { AllowedTypes, EventNameInfo } from '../types';
3
- /**
4
- *
5
- * @param {object} user the structure of this data is dictated by you, aka your API or DB.
6
- * @param {[string]} includeUserKeys is an array of strings that represent keys from your user data that you want to whitelist, the value for each key in the whitelist will be hashed.
7
- * @returns a new user object with all values hashed including only the whitelisted key value pairs
8
- */
9
- export declare const hashUserData: (user: Record<AllowedTypes, AllowedTypes>, includeUserKeys: Array<string>) => Promise<Record<AllowedTypes, AllowedTypes>>;
10
- /**
11
- *
12
- * @param {object} user the structure of this data is dictated by you, aka your API or DB.
13
- * @param {[string]} includeUserKeys is an array of strings that represent keys from your user data that you want to map over, if no keys are supplied the original passed in user data is returned.
14
- * @param {boolean} showMissingUserAttributesInConsole a boolean condition to show or hide "user" attributes that are not included in the "includeUserKeys" array, by console logging in dev tools.
15
- * @returns If no "includeUserKeys" are supplied the original ser object is returned, otherwise, a new user object with only the key value pairs that you included in your includeUserKeys array is returned.
16
- */
17
- export declare const buildNewUserData: (user: Record<AllowedTypes, AllowedTypes>, includeUserKeys: Array<string>, showMissingUserAttributesInConsole: boolean | undefined) => Record<string, string>;
18
- /**
19
- *
20
- * @paramType {Object} eventNameInfo
21
- * @property {analyticsPrefixActionList} actionPrefix either a built in value of JOURNEY | INTERACTION or a custom value added to the analyticsPrefixActionList object.
22
- * @property {string} description an OPTIONAL value passed in that is formatted like the following from 'Very descriptive description' to VERY_DESCRIPTIVE_DESCRIPTION.
23
- * @property {string} globalAppEvent the current event name for this track event selected from the analyticsGlobalEventActionList
24
- * @property {string} previousGlobalAppEvent an OPTIONAL value, the previous event name for this track event selected from the analyticsGlobalEventActionList.
25
- * @returns string eventName for this specific tracked event
26
- */
27
- export declare const buildAnalyticsEventName: (eventNameInfo: EventNameInfo) => string;
28
- /**
29
- *
30
- * @param {object} data
31
- * @param {string} globalAppEvent
32
- * @returns dataObject
33
- */
34
- export declare const buildEventDataObject: (data: Record<AllowedTypes, AllowedTypes>, globalAppEvent: string) => Promise<Record<AllowedTypes, AllowedTypes>>;
35
- /**
36
- *
37
- * @param {string} eventName
38
- * @returns a Boolean value, true if the event name is already in the dataLayer.
39
- */
40
- export declare const defaultDataLayerEventCheck: (eventName: string) => boolean;
41
- export declare const deviceDetectorInfo: (userAgent: string) => DeviceDetector.DeviceDetectorResult;
@@ -1,8 +0,0 @@
1
- import { BuildConfigOptions, Config, AnalyticsPlatform } from '../types/index.js';
2
- declare const analyticsPlatform: AnalyticsPlatform;
3
- declare const showMeBuildInAnalyticsPlatform: () => void;
4
- declare const showMeBuildInEventActionPrefixList: () => void;
5
- declare const showMeBuildInGlobalEventActionList: () => void;
6
- declare let config: Config;
7
- declare const buildConfig: (options: BuildConfigOptions) => void;
8
- export { config, analyticsPlatform, buildConfig, showMeBuildInAnalyticsPlatform, showMeBuildInGlobalEventActionList, showMeBuildInEventActionPrefixList, };
@@ -1,3 +0,0 @@
1
- import { IpInfo, ServerLocationData } from '../types';
2
- export declare const buildServerLocationData: (withServerLocationInfo: boolean | undefined, IP_INFO_TOKEN: string | undefined) => Promise<ServerLocationData | undefined>;
3
- export declare const getIpInfo: (IP_INFO_TOKEN: string) => Promise<IpInfo>;
@@ -1,130 +0,0 @@
1
- export interface AnalyticsEventActionPrefix extends Record<string, string> {
2
- }
3
- export interface AnalyticsGlobalEventAction extends Record<string, string> {
4
- }
5
- export type Event = {
6
- name: string;
7
- params: Record<AllowedTypes, AllowedTypes>;
8
- };
9
- export type GooglePayload = {
10
- non_personalized_ads: boolean;
11
- client_id: string;
12
- user_id: string;
13
- user_properties: {
14
- user_id: string;
15
- user_type: string;
16
- client_id: string;
17
- };
18
- events: Event[];
19
- };
20
- export type EventNameInfo = {
21
- eventName?: string;
22
- description?: string;
23
- actionPrefix: string;
24
- globalAppEvent: string;
25
- previousGlobalAppEvent?: string;
26
- };
27
- export type AllowedTypes = string | number;
28
- export type TrackAnalyticsEventOptions = {
29
- data: Record<AllowedTypes, AllowedTypes>;
30
- eventNameInfo: EventNameInfo;
31
- analyticsType: string;
32
- userDataKeysToHashArray?: Array<string> | null;
33
- dataLayerCheck?: boolean;
34
- consoleLogData?: object;
35
- };
36
- export type GlobalVars = {
37
- app: string;
38
- device_category?: string | undefined;
39
- device_os?: string | undefined;
40
- device_browser?: string | undefined;
41
- };
42
- export type DataLayer = {
43
- event: string;
44
- globalVars: GlobalVars;
45
- journeyProps: Record<any, any>;
46
- userProps: Record<any, any>;
47
- };
48
- export type ConsoleLogData = {
49
- showGlobalVars?: boolean;
50
- showJourneyPropsPayload?: boolean;
51
- showUserProps?: boolean;
52
- };
53
- export interface AnalyticsTrackerDataOptions {
54
- consoleLogData: ConsoleLogData | null;
55
- data: Record<any, any>;
56
- eventName: string;
57
- globalAppEvent: string;
58
- userDetails: Record<any, any>;
59
- }
60
- export interface HandleDataLayerPushOptions extends AnalyticsTrackerDataOptions {
61
- dataLayerCheck: boolean;
62
- }
63
- export type ServerLocationData = {
64
- SERVER_CITY: string;
65
- SERVER_COUNTRY: string;
66
- SERVER_HOSTNAME: string;
67
- SERVER_IP: string;
68
- SERVER_LOCATION: string;
69
- SERVER_POSTAL: string;
70
- SERVER_REGION: string;
71
- SERVER_TIMEZONE: string;
72
- };
73
- export type IpInfo = {
74
- city: string;
75
- country: string;
76
- SERVER_HOSTNAME: string;
77
- hostname: string;
78
- ip: string;
79
- loc: string;
80
- postal: string;
81
- region: string;
82
- timezone: string;
83
- };
84
- export type Ga4GoogleAnalyticsEventTracking = AnalyticsTrackerDataOptions;
85
- export type Platform = 'GOOGLE' | 'FACEBOOK' | 'DATALAYER_PUSH';
86
- export type AnalyticsPlatform = {
87
- [key in Platform]: Platform;
88
- };
89
- export type Tokens = {
90
- IP_INFO_TOKEN?: string;
91
- GA4_PUBLIC_API_SECRET?: string;
92
- GA4_PUBLIC_MEASUREMENT_ID?: string;
93
- };
94
- export type Config = {
95
- TOKENS?: Tokens;
96
- appName: string;
97
- appSessionCookieName: string;
98
- withDeviceInfo?: boolean;
99
- includeUserKeys: Array<string>;
100
- showMissingUserAttributesInConsole: boolean | undefined;
101
- withServerLocationInfo: boolean | undefined;
102
- eventActionPrefixList: AnalyticsEventActionPrefix;
103
- analyticsGlobalEventActionList: AnalyticsGlobalEventAction;
104
- };
105
- export type BuildConfigOptions = {
106
- appName: string;
107
- appSessionCookieName: string;
108
- eventActionPrefix?: AnalyticsEventActionPrefix;
109
- globalEventActionList?: AnalyticsGlobalEventAction;
110
- includeUserKeys?: Array<string> | undefined;
111
- showMissingUserAttributesInConsole?: boolean;
112
- showMeBuildInEventActionPrefixList?: boolean;
113
- TOKENS?: Tokens;
114
- withDeviceInfo?: boolean;
115
- withServerLocationInfo?: boolean;
116
- };
117
- export declare let config: Config;
118
- export type ProviderStateProps = {
119
- appName: string;
120
- appSessionCookieName: string;
121
- analyticsPlatform: AnalyticsPlatform;
122
- eventActionPrefixList: AnalyticsEventActionPrefix;
123
- analyticsGlobalEventActionList: AnalyticsGlobalEventAction;
124
- };
125
- export type ProviderApiProps = {
126
- trackAnalyticsEvent: (options: TrackAnalyticsEventOptions) => Promise<void>;
127
- showMeBuildInAnalyticsPlatform: () => void;
128
- showMeBuildInEventActionPrefixList: () => void;
129
- showMeBuildInGlobalEventActionList: () => void;
130
- };
@@ -1 +0,0 @@
1
- export declare const assertIsTrue: (condition: boolean, errorMessage?: string) => void;
@@ -1,5 +0,0 @@
1
- export declare const isClientSide: () => boolean;
2
- export declare const objectHasAttributes: (obj: object, key?: string) => boolean;
3
- export declare const replaceWhiteSpace: (stringValue: string, replaceValue: string) => string;
4
- export declare const checkAllArrayValuesReturnTrue: (valueArray: boolean[]) => boolean;
5
- export declare const hashString: (value: string) => Promise<string>;
@@ -1 +0,0 @@
1
- export declare const getCookieValueByName: (name: string) => string | undefined;