react-marketing-tools 1.0.0-alpha.3 → 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.
@@ -118,59 +118,151 @@ var e = [
118
118
  });
119
119
  }
120
120
  };
121
- }, b = 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 {
122
214
  code;
123
215
  constructor(e, t, n) {
124
216
  super(`[react-marketing-tools] ${t}`, n), this.name = "AnalyticsError", this.code = e;
125
217
  }
126
- }, x = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, S = [
218
+ }, P = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, F = [
127
219
  "google_",
128
220
  "ga_",
129
221
  "firebase_"
130
- ], C = 25, w = 100, T = {
222
+ ], I = 25, L = 100, R = {
131
223
  page_location: 1e3,
132
224
  page_referrer: 420,
133
225
  page_title: 300
134
- }, E = [
226
+ }, z = [
135
227
  "email",
136
228
  "phone",
137
229
  "first_name",
138
230
  "last_name",
139
231
  "address",
140
232
  "password"
141
- ], D = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, O = "[redacted]", k = (e) => {
142
- if (!x.test(e)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
143
- 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));
144
236
  return t && `must not start with the reserved prefix "${t}"`;
145
- }, A = (e) => {
146
- let t = k(e);
237
+ }, U = (e) => {
238
+ let t = H(e);
147
239
  return t && `event name "${e}" ${t}`;
148
- }, j = (e) => {
240
+ }, W = (e) => {
149
241
  let t = Object.keys(e), n = t.flatMap((t) => {
150
- let n = k(t);
242
+ let n = H(t);
151
243
  if (n) return [`param "${t}" ${n}`];
152
- let r = e[t], i = T[t] ?? w;
244
+ let r = e[t], i = R[t] ?? L;
153
245
  return typeof r == "string" && r.length > i ? [`param "${t}" is longer than ${i} characters`] : [];
154
246
  });
155
- return t.length > C ? [`has ${t.length} params; the limit is ${C}`, ...n] : n;
156
- }, M = (e) => new RegExp(D, "i").test(e), N = (e, t) => (typeof t == "string" || typeof t == "number") && E.some((t) => e.toLowerCase().includes(t)) ? O : typeof t == "string" ? t.replace(new RegExp(D, "gi"), O) : t, P = (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) => {
157
249
  let t = Object.entries(e).map(([e, t]) => [
158
250
  e,
159
251
  t,
160
- N(e, t)
252
+ K(e, t)
161
253
  ]);
162
254
  return {
163
255
  params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
164
256
  redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
165
257
  };
166
- }, F = () => typeof window < "u", I = () => 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)), L = (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) => {
167
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)}).`);
168
- }, R = (i) => {
169
- L(i);
260
+ }, Z = (i) => {
261
+ X(i);
170
262
  let { debug: a = !1, onError: s = console.error, respectGpc: c = !0 } = i, l = n({
171
263
  consent: i.consent,
172
264
  gpc: c && o()
173
- }), u = l, d = [
265
+ }), u = l, d, f = [
174
266
  ...i.gtm ? [y({
175
267
  ...i.gtm,
176
268
  nonce: i.nonce
@@ -179,80 +271,101 @@ var e = [
179
271
  ...i.ga4,
180
272
  nonce: i.nonce
181
273
  })] : [],
274
+ ...i.metaPixel ? [M({
275
+ ...i.metaPixel,
276
+ nonce: i.nonce
277
+ })] : [],
182
278
  ...i.destinations ?? []
183
- ], f = [], p = !1, m = (e) => {
279
+ ], p = [], m = !1, h = (e) => {
184
280
  if (a) throw e;
185
281
  s(e);
186
- }, h = (e) => {
187
- for (let t of d) try {
282
+ }, _ = (e) => {
283
+ for (let t of f) try {
188
284
  e(t);
189
285
  } catch (e) {
190
- s(new b("destination_failed", `destination "${t.name}" failed`, { cause: e }));
286
+ s(new N("destination_failed", `destination "${t.name}" failed`, { cause: e }));
191
287
  }
192
- }, _ = (e) => {
193
- p ? h(e) : f.push(e);
194
- }, v = (e, t) => {
195
- let n = A(e);
196
- if (n) {
197
- m(new b("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));
198
294
  return;
199
295
  }
200
- for (let n of j(t)) m(new b("invalid_param", `event "${e}" ${n}`));
201
- let { params: r, redactedKeys: i } = P(t);
202
- return i.length > 0 && m(new b("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(", ")}`)), {
203
299
  name: e,
204
- params: r,
205
- eventId: I(),
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(),
206
309
  timestamp: Date.now()
207
310
  };
208
311
  };
209
312
  return {
210
313
  start() {
211
- !p && F() && (p = !0, h((e) => e.start({ consent: l })), f.splice(0).forEach(h));
314
+ !m && J() && (m = !0, _((e) => e.start({
315
+ consent: l,
316
+ identity: d
317
+ })), p.splice(0).forEach(_));
212
318
  },
213
- track(e, t = {}) {
214
- if (!F()) return;
215
- let n = v(e, t);
216
- n && _((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));
217
327
  },
218
328
  page(e = {}) {
219
- if (!F()) return;
220
- let t = v("page_view", {
221
- page_location: location.href,
222
- page_title: document.title,
223
- ...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
+ }
224
337
  });
225
- t && _((e) => e.page ? e.page(t) : e.track(t));
338
+ t && v((e) => e.page ? e.page(t) : e.track(t));
226
339
  },
227
340
  identify(e, t = {}) {
228
- if (F()) {
229
- if (!e || M(e)) {
230
- m(new b("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
231
- return;
232
- }
233
- _((n) => n.identify?.({
234
- userId: e,
235
- traits: t
236
- }));
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;
237
345
  }
346
+ let n = {
347
+ userId: e,
348
+ traits: t
349
+ };
350
+ d = n, v((e) => e.identify?.(n));
238
351
  },
239
352
  reset() {
240
- F() && _((e) => e.reset?.());
353
+ J() && (d = void 0, v((e) => e.reset?.()));
241
354
  },
242
355
  consent: {
243
356
  update(n) {
244
- if (!F()) return;
357
+ if (!J()) return;
245
358
  let i = Object.entries(n).filter(([n, r]) => r !== void 0 && !(e.includes(n) && t(r)));
246
359
  if (i.length > 0) {
247
- m(new b("invalid_consent", `consent.update() ignored: ${i.map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(", ")}. Use analytics, ads, adUserData or adPersonalization with 'granted' or 'denied'.`));
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'.`));
248
361
  return;
249
362
  }
250
363
  let a = r(u, n);
251
- u = a, _((e) => e.consent?.(a));
364
+ u = a, v((e) => e.consent?.(a));
252
365
  },
253
366
  get: () => u
254
367
  }
255
368
  };
256
369
  };
257
370
  //#endregion
258
- export { b as n, R as t };
371
+ export { N as n, Z as t };
@@ -13,9 +13,18 @@ export type ConsentState = {
13
13
  export type ConsentUpdate = Partial<ConsentState>;
14
14
  /** Event parameters, passed to every destination as-is. */
15
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
+ };
16
24
  export type AnalyticsEvent = {
17
25
  name: string;
18
26
  params: EventParams;
27
+ options?: TrackOptions;
19
28
  /** Unique per `track()` call and shared by every destination, so vendors can deduplicate the same event. */
20
29
  eventId: string;
21
30
  /** Milliseconds since the Unix epoch at the moment `track()` was called. */
@@ -38,9 +47,13 @@ export type Identity = {
38
47
  /** Somewhere events are sent. Built-in destinations are configured by key; custom ones go in `destinations`. */
39
48
  export type Destination = {
40
49
  name: string;
41
- /** Called once, in the browser, by `analytics.start()`, with the consent state the page started with. */
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
+ */
42
54
  start(context: {
43
55
  consent: ConsentState;
56
+ identity?: Identity;
44
57
  }): void;
45
58
  track(event: AnalyticsEvent): void;
46
59
  /** Receives every consent change, in order with events. */
@@ -76,6 +89,17 @@ export type Ga4Config = {
76
89
  /** Milliseconds tags wait for a consent update when a Consent Mode signal starts denied. Defaults to 500. */
77
90
  waitForUpdate?: number;
78
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;
102
+ };
79
103
  export type AnalyticsConfig = {
80
104
  /** Initial consent for every purpose. Required, so every site makes an explicit choice. */
81
105
  consent: ConsentStatus;
@@ -86,6 +110,7 @@ export type AnalyticsConfig = {
86
110
  respectGpc?: boolean;
87
111
  gtm?: GtmConfig;
88
112
  ga4?: Ga4Config;
113
+ metaPixel?: MetaPixelConfig;
89
114
  /** Custom destinations, in addition to the built-in ones. */
90
115
  destinations?: Destination[];
91
116
  /** Content-Security-Policy nonce added to every script the library injects. */
@@ -99,7 +124,7 @@ export type Analytics = {
99
124
  /** Loads vendor scripts and delivers queued events. Safe to call more than once; does nothing outside the browser. */
100
125
  start(): void;
101
126
  /** Sends an event to every destination, queued until `start()`. Does nothing outside the browser. */
102
- track(name: string, params?: EventParams): void;
127
+ track(name: string, params?: EventParams, options?: TrackOptions): void;
103
128
  /** Sends a `page_view` with the current `page_location` and `page_title`, plus any params given. */
104
129
  page(params?: EventParams): void;
105
130
  /** Associates later events with a user. `userId` must not be personal data such as an email address. */
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, ConsentState, ConsentStatus, ConsentUpdate, Destination, EventParams, Ga4Config, 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,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.3",
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/)",