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.
- package/dist/chunks/core.js +172 -59
- package/dist/core/types.d.ts +27 -2
- package/dist/core.d.ts +1 -1
- package/dist/destinations/metaEventMap.d.ts +12 -0
- package/dist/destinations/metaPixel.d.ts +6 -0
- package/package.json +1 -1
package/dist/chunks/core.js
CHANGED
|
@@ -118,59 +118,151 @@ var e = [
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
|
-
}, b =
|
|
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
|
-
},
|
|
218
|
+
}, P = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, F = [
|
|
127
219
|
"google_",
|
|
128
220
|
"ga_",
|
|
129
221
|
"firebase_"
|
|
130
|
-
],
|
|
222
|
+
], I = 25, L = 100, R = {
|
|
131
223
|
page_location: 1e3,
|
|
132
224
|
page_referrer: 420,
|
|
133
225
|
page_title: 300
|
|
134
|
-
},
|
|
226
|
+
}, z = [
|
|
135
227
|
"email",
|
|
136
228
|
"phone",
|
|
137
229
|
"first_name",
|
|
138
230
|
"last_name",
|
|
139
231
|
"address",
|
|
140
232
|
"password"
|
|
141
|
-
],
|
|
142
|
-
if (!
|
|
143
|
-
let 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
|
-
},
|
|
146
|
-
let t =
|
|
237
|
+
}, U = (e) => {
|
|
238
|
+
let t = H(e);
|
|
147
239
|
return t && `event name "${e}" ${t}`;
|
|
148
|
-
},
|
|
240
|
+
}, W = (e) => {
|
|
149
241
|
let t = Object.keys(e), n = t.flatMap((t) => {
|
|
150
|
-
let n =
|
|
242
|
+
let n = H(t);
|
|
151
243
|
if (n) return [`param "${t}" ${n}`];
|
|
152
|
-
let r = e[t], i =
|
|
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 >
|
|
156
|
-
},
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
169
|
-
|
|
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
|
-
],
|
|
279
|
+
], p = [], m = !1, h = (e) => {
|
|
184
280
|
if (a) throw e;
|
|
185
281
|
s(e);
|
|
186
|
-
},
|
|
187
|
-
for (let t of
|
|
282
|
+
}, _ = (e) => {
|
|
283
|
+
for (let t of f) try {
|
|
188
284
|
e(t);
|
|
189
285
|
} catch (e) {
|
|
190
|
-
s(new
|
|
286
|
+
s(new N("destination_failed", `destination "${t.name}" failed`, { cause: e }));
|
|
191
287
|
}
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
},
|
|
195
|
-
let
|
|
196
|
-
if (
|
|
197
|
-
|
|
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
|
|
201
|
-
let
|
|
202
|
-
return
|
|
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:
|
|
205
|
-
|
|
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
|
-
!
|
|
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 (!
|
|
215
|
-
let
|
|
216
|
-
|
|
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 (!
|
|
220
|
-
let t =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 &&
|
|
338
|
+
t && v((e) => e.page ? e.page(t) : e.track(t));
|
|
226
339
|
},
|
|
227
340
|
identify(e, t = {}) {
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
353
|
+
J() && (d = void 0, v((e) => e.reset?.()));
|
|
241
354
|
},
|
|
242
355
|
consent: {
|
|
243
356
|
update(n) {
|
|
244
|
-
if (!
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
371
|
+
export { N as n, Z as t };
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|
|
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/)",
|