react-marketing-tools 1.0.2 → 1.1.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.
package/README.md CHANGED
@@ -1,19 +1,26 @@
1
1
  # React Marketing Tools
2
2
 
3
- [![npm](https://img.shields.io/npm/v/react-marketing-tools)](https://www.npmjs.com/package/react-marketing-tools)
4
- [![downloads](https://img.shields.io/npm/dm/react-marketing-tools)](https://www.npmjs.com/package/react-marketing-tools)
5
- [![bundle size](https://img.shields.io/bundlejs/size/react-marketing-tools?externals=react,react-dom,react/jsx-runtime)](https://bundlejs.com/?q=react-marketing-tools)
6
- [![types](https://img.shields.io/npm/types/react-marketing-tools)](https://www.npmjs.com/package/react-marketing-tools)
7
- [![node](https://img.shields.io/node/v/react-marketing-tools?logo=nodedotjs)](https://www.npmjs.com/package/react-marketing-tools)
8
- [![license](https://img.shields.io/npm/l/react-marketing-tools)](./LICENSE)
9
-
10
3
  [![CI](https://img.shields.io/github/actions/workflow/status/bronz3beard/react-marketing-tools/ci.yml?branch=main&label=CI&logo=github)](https://github.com/bronz3beard/react-marketing-tools/actions/workflows/ci.yml)
11
4
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/bronz3beard/react-marketing-tools/codeql.yml?branch=main&label=CodeQL&logo=github)](https://github.com/bronz3beard/react-marketing-tools/actions/workflows/codeql.yml)
12
5
  [![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/bronz3beard/react-marketing-tools?label=openssf%20scorecard)](https://scorecard.dev/viewer/?uri=github.com/bronz3beard/react-marketing-tools)
13
6
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14943/badge)](https://www.bestpractices.dev/projects/14943)
14
- [![last commit](https://img.shields.io/github/last-commit/bronz3beard/react-marketing-tools)](https://github.com/bronz3beard/react-marketing-tools/commits/main)
7
+ [![OpenSSF Baseline](https://www.bestpractices.dev/projects/14943/baseline)](https://www.bestpractices.dev/projects/14943)
8
+ [![provenance](https://img.shields.io/badge/provenance-signed-brightgreen)](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/releasing.md#verifying-a-release)
15
9
 
10
+ [![license](https://img.shields.io/npm/l/react-marketing-tools)](./LICENSE)
11
+ [![node](https://img.shields.io/node/v/react-marketing-tools?logo=nodedotjs)](https://www.npmjs.com/package/react-marketing-tools)
12
+ [![npm](https://img.shields.io/npm/v/react-marketing-tools)](https://www.npmjs.com/package/react-marketing-tools)
13
+ [![downloads](https://img.shields.io/npm/dm/react-marketing-tools)](https://www.npmjs.com/package/react-marketing-tools)
14
+ [![bundle size](https://img.shields.io/bundlejs/size/react-marketing-tools?externals=react,react-dom,react/jsx-runtime)](https://bundlejs.com/?q=react-marketing-tools)
15
+ [![types](https://img.shields.io/npm/types/react-marketing-tools)](https://www.npmjs.com/package/react-marketing-tools)
16
+ [![react](https://img.shields.io/npm/dependency-version/react-marketing-tools/peer/react?logo=react)](https://www.npmjs.com/package/react-marketing-tools)
17
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://www.npmjs.com/package/react-marketing-tools?activeTab=dependencies)
18
+
19
+ [![destinations](https://img.shields.io/badge/destinations-GTM%20%7C%20GA4%20%7C%20Meta%20Pixel%20%7C%20Conversions%20API-blue)](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/README.md)
20
+ [![Consent Mode](https://img.shields.io/badge/Consent%20Mode-v2-blue)](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/consent.md)
21
+ [![playground](https://img.shields.io/badge/playground-try%20it-blueviolet)](https://bronz3beard.github.io/react-marketing-tools/)
16
22
  [![AI setup prompt](https://img.shields.io/badge/AI%20setup-prompt-blueviolet)](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/agent-setup.md)
23
+ [![last commit](https://img.shields.io/github/last-commit/bronz3beard/react-marketing-tools)](https://github.com/bronz3beard/react-marketing-tools/commits/main)
17
24
  [![discussions](https://img.shields.io/github/discussions/bronz3beard/react-marketing-tools)](https://github.com/bronz3beard/react-marketing-tools/discussions)
18
25
  [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/bronz3beard/react-marketing-tools/blob/main/CONTRIBUTING.md)
19
26
 
@@ -87,7 +94,8 @@ You only configure the services you use. Many teams start with Tag Manager alone
87
94
  npm install react-marketing-tools
88
95
  ```
89
96
 
90
- Works with React 18 and 19. The package is published as ES modules, the `import` style of JavaScript, so it works in
97
+ Works with React 18 and 19. React is an optional peer: only the main `react-marketing-tools` entry uses it, so a
98
+ backend that imports just `react-marketing-tools/server` or `react-marketing-tools/core` doesn't install it. The package is published as ES modules, the `import` style of JavaScript, so it works in
91
99
  every current bundler and in Node.js 22.12 or later; it can't be loaded with `require()`.
92
100
 
93
101
  ## Set this up with an AI assistant
@@ -329,6 +337,10 @@ await sendMeasurementProtocolEvent({
329
337
  })
330
338
  ```
331
339
 
340
+ Events from an iOS or Android app go to its GA4 app stream the same way, with `firebaseAppId` and `appInstanceId` in
341
+ place of `measurementId` and `clientId`; see
342
+ [Events from a mobile app](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/measurement-protocol.md#events-from-a-mobile-app).
343
+
332
344
  The same import has `sendConversionsApiEvent` for Meta, which scrambles customer details into hashes the way Meta
333
345
  requires before sending them.
334
346
 
@@ -16,7 +16,9 @@ export type AiAgents = Record<string, string[]>;
16
16
  export type AiCrawlerReporting = 'separate-property' | {
17
17
  trafficType: string;
18
18
  };
19
- export type AiCrawlerOptions = Omit<MeasurementProtocolOptions, 'consent'> & {
19
+ export type AiCrawlerOptions = Omit<Extract<MeasurementProtocolOptions, {
20
+ measurementId: string;
21
+ }>, 'consent'> & {
20
22
  /** Your label for the crawler, from `matchAiAgent()`. */
21
23
  agent: string;
22
24
  keepSeparate: AiCrawlerReporting;
@@ -3,13 +3,27 @@ export type MeasurementProtocolEvent = {
3
3
  name: string;
4
4
  params?: EventParams;
5
5
  };
6
- export type MeasurementProtocolOptions = {
6
+ /** A GA4 web stream: events join the visitor's browser client. */
7
+ type WebStream = {
7
8
  measurementId: string;
8
- /** Keep it on your server: Google says the API secret must not be exposed in client-side code. */
9
- apiSecret: string;
10
9
  /** The visitor's GA4 client ID, from the `_ga` cookie (see `readGa4Cookies`). */
11
10
  clientId: string;
12
- /** The visitor's GA4 session ID, from the `_ga_<stream>` cookie, so events join their session. */
11
+ firebaseAppId?: never;
12
+ appInstanceId?: never;
13
+ };
14
+ /** A GA4 app stream (iOS or Android, through Firebase): events join the app installation. */
15
+ type AppStream = {
16
+ /** The Firebase App ID, e.g. `1:1234567890:android:321abc456def7890`, shown on the app stream's details. */
17
+ firebaseAppId: string;
18
+ /** The installation's ID from the Firebase SDK: `getAppInstanceId()` on Android, `appInstanceID()` on iOS. */
19
+ appInstanceId: string;
20
+ measurementId?: never;
21
+ clientId?: never;
22
+ };
23
+ export type MeasurementProtocolOptions = (WebStream | AppStream) & {
24
+ /** Created on the same stream. Keep it on your server: Google says it must not be exposed in client-side code. */
25
+ apiSecret: string;
26
+ /** The GA4 session ID, from the `_ga_<stream>` cookie or the Firebase SDK, so events join that session. */
13
27
  sessionId?: string;
14
28
  userId?: string;
15
29
  /** Up to 25 events. Each gets `session_id` and `engagement_time_msec` (1 ms unless the params set it). */
@@ -30,5 +44,6 @@ export type MeasurementProtocolResult = {
30
44
  warnings: string[];
31
45
  validationMessages?: unknown[];
32
46
  };
33
- /** Sends events to GA4 from your server, e.g. a purchase confirmed by a payment webhook. */
47
+ /** Sends events to GA4 from your server, e.g. a purchase confirmed by a payment webhook or an event relayed from an app. */
34
48
  export declare const sendMeasurementProtocolEvent: (options: MeasurementProtocolOptions) => Promise<MeasurementProtocolResult>;
49
+ export {};
package/dist/server.js CHANGED
@@ -81,17 +81,23 @@ var a = async (e) => {
81
81
  }, y = {
82
82
  global: "https://www.google-analytics.com",
83
83
  eu: "https://region1.google-analytics.com"
84
- }, b = /^G-[A-Z0-9]+$/, x = 25, S = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] sendMeasurementProtocolEvent: ${e}`), C = ({ measurementId: e, apiSecret: t, clientId: i, userId: a, events: o }) => {
85
- if (!b.test(e)) throw S(`measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)})`);
86
- if (!t) throw S("apiSecret is required");
87
- if (!i) throw S("clientId is required; read it from the _ga cookie with readGa4Cookies()");
88
- if (a !== void 0 && (!a || n(a))) throw S("userId must be your own identifier, not personal data such as an email address");
89
- if (o.length === 0 || o.length > x) throw S(`send between 1 and ${x} events per request (received ${o.length})`);
90
- for (let { name: e } of o) {
84
+ }, b = /^G-[A-Z0-9]+$/, x = /^\d+:\d+:(android|ios):[0-9a-f]+$/, S = /^[0-9a-f]{32}$/i, C = 25, w = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] sendMeasurementProtocolEvent: ${e}`), T = ({ firebaseAppId: e, appInstanceId: t }) => {
85
+ if (!x.test(e ?? "")) throw w(`firebaseAppId must look like "1:1234567890:android:321abc456def7890" (received ${JSON.stringify(e)})`);
86
+ if (!S.test(t ?? "")) throw w("appInstanceId must be the 32-character ID from the Firebase SDK (getAppInstanceId() on Android, appInstanceID() on iOS)");
87
+ }, E = ({ measurementId: e, clientId: t }) => {
88
+ if (!b.test(e ?? "")) throw w(`measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)})`);
89
+ if (!t) throw w("clientId is required; read it from the _ga cookie with readGa4Cookies()");
90
+ }, D = (e) => {
91
+ let { apiSecret: t, userId: i, events: a } = e, o = e.firebaseAppId !== void 0 || e.appInstanceId !== void 0, s = e.measurementId !== void 0 || e.clientId !== void 0;
92
+ if (o && s) throw w("pass measurementId and clientId for a web stream, or firebaseAppId and appInstanceId for an app stream, not both");
93
+ if (o ? T(e) : E(e), !t) throw w("apiSecret is required");
94
+ if (i !== void 0 && (!i || n(i))) throw w("userId must be your own identifier, not personal data such as an email address");
95
+ if (a.length === 0 || a.length > C) throw w(`send between 1 and ${C} events per request (received ${a.length})`);
96
+ for (let { name: e } of a) {
91
97
  let t = r(e);
92
- if (t) throw S(t);
98
+ if (t) throw w(t);
93
99
  }
94
- }, w = ({ event: { name: n, params: r = {} }, sessionId: i, warnings: a }) => {
100
+ }, O = ({ event: { name: n, params: r = {} }, sessionId: i, warnings: a }) => {
95
101
  for (let e of t(r)) a.push(`event "${n}" ${e}`);
96
102
  let { params: o, redactedKeys: s } = e(r);
97
103
  return s.length > 0 && a.push(`event "${n}": personal data redacted from ${s.join(", ")}`), {
@@ -102,48 +108,56 @@ var a = async (e) => {
102
108
  ...o
103
109
  }
104
110
  };
105
- }, T = ({ adUserData: e, adPersonalization: t }) => ({
111
+ }, k = ({ adUserData: e, adPersonalization: t }) => ({
106
112
  ...e && { ad_user_data: e.toUpperCase() },
107
113
  ...t && { ad_personalization: t.toUpperCase() }
108
- }), E = async (e) => {
109
- C(e);
110
- let { measurementId: t, apiSecret: n, clientId: r, sessionId: i, userId: a, events: o, consent: s, validate: c = !1, region: l = "global" } = e, u = [], d = {
111
- client_id: r,
112
- ...a && { user_id: a },
113
- ...s && { consent: T(s) },
114
- events: o.map((e) => w({
114
+ }), A = (e) => e.firebaseAppId === void 0 ? {
115
+ param: "measurement_id",
116
+ id: e.measurementId,
117
+ body: { client_id: e.clientId }
118
+ } : {
119
+ param: "firebase_app_id",
120
+ id: e.firebaseAppId,
121
+ body: { app_instance_id: e.appInstanceId }
122
+ }, j = async (e) => {
123
+ D(e);
124
+ let { apiSecret: t, sessionId: n, userId: r, events: i, consent: a, validate: o = !1, region: s = "global" } = e, c = A(e), l = [], u = {
125
+ ...c.body,
126
+ ...r && { user_id: r },
127
+ ...a && { consent: k(a) },
128
+ events: i.map((e) => O({
115
129
  event: e,
116
- sessionId: i,
117
- warnings: u
130
+ sessionId: n,
131
+ warnings: l
118
132
  }))
119
- }, f = new URL(c ? "/debug/mp/collect" : "/mp/collect", y[l]);
120
- f.searchParams.set("measurement_id", t), f.searchParams.set("api_secret", n);
121
- let p = await fetch(f, {
133
+ }, d = new URL(o ? "/debug/mp/collect" : "/mp/collect", y[s]);
134
+ d.searchParams.set(c.param, c.id), d.searchParams.set("api_secret", t);
135
+ let f = await fetch(d, {
122
136
  method: "POST",
123
137
  headers: { "content-type": "application/json" },
124
- body: JSON.stringify(d)
138
+ body: JSON.stringify(u)
125
139
  });
126
- if (!c) return {
127
- ok: p.ok,
128
- status: p.status,
129
- warnings: u
140
+ if (!o) return {
141
+ ok: f.ok,
142
+ status: f.status,
143
+ warnings: l
130
144
  };
131
- let { validationMessages: m = [] } = await p.json().catch(() => ({}));
145
+ let { validationMessages: p = [] } = await f.json().catch(() => ({}));
132
146
  return {
133
- ok: p.ok && m.length === 0,
134
- status: p.status,
135
- warnings: u,
136
- validationMessages: m
147
+ ok: f.ok && p.length === 0,
148
+ status: f.status,
149
+ warnings: l,
150
+ validationMessages: p
137
151
  };
138
- }, D = ({ userAgent: e, agents: t }) => {
152
+ }, M = ({ userAgent: e, agents: t }) => {
139
153
  let n = e.toLowerCase();
140
154
  return Object.entries(t).find(([, e]) => e.some((e) => n.includes(e.toLowerCase())))?.[0];
141
- }, O = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] sendAiCrawlerEvent: ${e}`), k = ["traffic_type", "ai_agent"], A = async (e) => {
155
+ }, N = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] sendAiCrawlerEvent: ${e}`), P = ["traffic_type", "ai_agent"], F = async (e) => {
142
156
  let { agent: t, keepSeparate: n, events: r, ...i } = e;
143
- if (!t) throw O("agent is required");
157
+ if (!t) throw N("agent is required");
144
158
  let a = n === "separate-property" ? "ai_crawler" : n?.trafficType;
145
- if (!a) throw O("keepSeparate is required: 'separate-property' when measurementId is a crawler-only property, or { trafficType } matching a GA4 data filter. Crawler hits must never land in a visitor property untagged.");
146
- let o = r.flatMap((e) => k.filter((t) => e.params?.[t] !== void 0).map((t) => `event "${e.name}" param "${t}" was replaced: sendAiCrawlerEvent sets it`)), s = await E({
159
+ if (!a) throw N("keepSeparate is required: 'separate-property' when measurementId is a crawler-only property, or { trafficType } matching a GA4 data filter. Crawler hits must never land in a visitor property untagged.");
160
+ let o = r.flatMap((e) => P.filter((t) => e.params?.[t] !== void 0).map((t) => `event "${e.name}" param "${t}" was replaced: sendAiCrawlerEvent sets it`)), s = await j({
147
161
  ...i,
148
162
  events: r.map((e) => ({
149
163
  ...e,
@@ -158,7 +172,7 @@ var a = async (e) => {
158
172
  ...s,
159
173
  warnings: [...o, ...s.warnings]
160
174
  };
161
- }, j = 16384, M = /^\d+$/, N = [
175
+ }, I = 16384, L = /^\d+$/, R = [
162
176
  "externalId",
163
177
  "email",
164
178
  "phone",
@@ -166,44 +180,44 @@ var a = async (e) => {
166
180
  "lastName",
167
181
  "fbc",
168
182
  "fbp"
169
- ], P = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] createTrackHandler: ${e}`), F = ({ allowedOrigins: e, meta: t }) => {
170
- if (e.length === 0) throw P("allowedOrigins needs at least one origin, e.g. 'https://shop.example.com'");
171
- for (let t of e) if (!URL.canParse(t) || new URL(t).origin !== t) throw P(`allowedOrigins entries must be origins such as 'https://shop.example.com', without a path or trailing slash (received ${JSON.stringify(t)})`);
172
- if (!M.test(t.pixelId)) throw P(`meta.pixelId must be the numeric pixel ID (received ${JSON.stringify(t.pixelId)})`);
173
- if (!t.accessToken) throw P("meta.accessToken is required");
174
- }, I = (e, t) => new Response(t, { status: e }), L = async (e) => {
175
- if (Number(e.headers.get("content-length")) > j) return;
183
+ ], z = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] createTrackHandler: ${e}`), B = ({ allowedOrigins: e, meta: t }) => {
184
+ if (e.length === 0) throw z("allowedOrigins needs at least one origin, e.g. 'https://shop.example.com'");
185
+ for (let t of e) if (!URL.canParse(t) || new URL(t).origin !== t) throw z(`allowedOrigins entries must be origins such as 'https://shop.example.com', without a path or trailing slash (received ${JSON.stringify(t)})`);
186
+ if (!L.test(t.pixelId)) throw z(`meta.pixelId must be the numeric pixel ID (received ${JSON.stringify(t.pixelId)})`);
187
+ if (!t.accessToken) throw z("meta.accessToken is required");
188
+ }, V = (e, t) => new Response(t, { status: e }), H = async (e) => {
189
+ if (Number(e.headers.get("content-length")) > I) return;
176
190
  if (!e.body) return "";
177
191
  let t = e.body.getReader(), n = new TextDecoder(), r = "", i = 0;
178
192
  for (;;) {
179
193
  let { done: e, value: a } = await t.read();
180
194
  if (e) return r + n.decode();
181
- if (i += a.byteLength, i > j) {
195
+ if (i += a.byteLength, i > I) {
182
196
  await t.cancel();
183
197
  return;
184
198
  }
185
199
  r += n.decode(a, { stream: !0 });
186
200
  }
187
- }, R = (e) => typeof e == "object" && !!e && !Array.isArray(e), z = (e) => typeof e == "string" && e.length > 0, B = (e) => typeof e == "string" && URL.canParse(e) && ["http:", "https:"].includes(new URL(e).protocol), V = (e) => {
201
+ }, U = (e) => typeof e == "object" && !!e && !Array.isArray(e), W = (e) => typeof e == "string" && e.length > 0, G = (e) => typeof e == "string" && URL.canParse(e) && ["http:", "https:"].includes(new URL(e).protocol), K = (e) => {
188
202
  let t;
189
203
  try {
190
204
  t = JSON.parse(e);
191
205
  } catch {
192
206
  return;
193
207
  }
194
- if (!R(t) || t.v !== 1) return;
208
+ if (!U(t) || t.v !== 1) return;
195
209
  let { eventName: n, eventId: r, customData: i, eventSourceUrl: a, consent: o, userData: s } = t;
196
- if (z(n) && z(r) && R(i) && B(a) && R(o) && (o.adUserData === "granted" || o.adUserData === "denied") && R(s) && !N.some((e) => s[e] !== void 0 && typeof s[e] != "string")) return {
210
+ if (W(n) && W(r) && U(i) && G(a) && U(o) && (o.adUserData === "granted" || o.adUserData === "denied") && U(s) && !R.some((e) => s[e] !== void 0 && typeof s[e] != "string")) return {
197
211
  v: 1,
198
212
  eventName: n,
199
213
  eventId: r,
200
214
  customData: i,
201
215
  eventSourceUrl: a,
202
216
  consent: { adUserData: o.adUserData },
203
- userData: Object.fromEntries(N.filter((e) => s[e] !== void 0).map((e) => [e, s[e]]))
217
+ userData: Object.fromEntries(R.filter((e) => s[e] !== void 0).map((e) => [e, s[e]]))
204
218
  };
205
- }, H = (e) => {
206
- F(e);
219
+ }, q = (e) => {
220
+ B(e);
207
221
  let { meta: t, onError: n = console.error } = e, r = new Set(e.allowedOrigins);
208
222
  return async (e) => {
209
223
  if (e.method !== "POST") return new Response("Use POST", {
@@ -211,14 +225,14 @@ var a = async (e) => {
211
225
  headers: { allow: "POST" }
212
226
  });
213
227
  let i = e.headers.get("origin");
214
- if (!i || !r.has(i)) return I(403, "Origin not allowed");
215
- let a = await L(e);
216
- if (a === void 0) return I(413, "Body too large");
217
- let o = V(a);
218
- if (!o) return I(400, "Invalid event");
228
+ if (!i || !r.has(i)) return V(403, "Origin not allowed");
229
+ let a = await H(e);
230
+ if (a === void 0) return V(413, "Body too large");
231
+ let o = K(a);
232
+ if (!o) return V(400, "Invalid event");
219
233
  let s = e.headers.get("user-agent");
220
- if (!s) return I(400, "Missing User-Agent");
221
- if (o.consent.adUserData !== "granted") return I(204);
234
+ if (!s) return V(400, "Missing User-Agent");
235
+ if (o.consent.adUserData !== "granted") return V(204);
222
236
  try {
223
237
  let r = await v({
224
238
  ...t,
@@ -234,14 +248,14 @@ var a = async (e) => {
234
248
  customData: o.customData
235
249
  }]
236
250
  });
237
- if (r.ok) return I(204);
251
+ if (r.ok) return V(204);
238
252
  n(Error(`[react-marketing-tools] createTrackHandler: Meta rejected "${o.eventName}" (HTTP ${r.status})`, { cause: r.body }));
239
253
  } catch (e) {
240
254
  n(Error(`[react-marketing-tools] createTrackHandler: could not send "${o.eventName}" to Meta`, { cause: e }));
241
255
  }
242
- return I(502, "Meta did not accept the event");
256
+ return V(502, "Meta did not accept the event");
243
257
  };
244
- }, U = ({ cookieHeader: e, measurementId: t }) => {
258
+ }, J = ({ cookieHeader: e, measurementId: t }) => {
245
259
  let n = /^GA\d\.\d\.(\d+\.\d+)$/.exec(i(e, "_ga") ?? "")?.[1], r = i(e, `_ga_${t.replace(/^G-/, "")}`) ?? "", a = /^GS1\.\d\.(\d+)\./.exec(r)?.[1] ?? /^GS2\.\d\.s(\d+)/.exec(r)?.[1];
246
260
  return {
247
261
  ...n && { clientId: n },
@@ -249,4 +263,4 @@ var a = async (e) => {
249
263
  };
250
264
  };
251
265
  //#endregion
252
- export { f as META_GRAPH_API_VERSION, H as createTrackHandler, D as matchAiAgent, U as readGa4Cookies, A as sendAiCrawlerEvent, v as sendConversionsApiEvent, E as sendMeasurementProtocolEvent };
266
+ export { f as META_GRAPH_API_VERSION, q as createTrackHandler, M as matchAiAgent, J as readGa4Cookies, F as sendAiCrawlerEvent, v as sendConversionsApiEvent, j as sendMeasurementProtocolEvent };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-marketing-tools",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.1.0",
5
5
  "description": "One track() call for Google Tag Manager, Google Analytics 4 and the Meta Pixel, with Consent Mode v2, UTM attribution and personal-data redaction built in. React bindings included.",
6
6
  "license": "MIT",
7
7
  "author": "bronz3beard <exempli.gratia.webdesign@gmail.com> (https://www.heyrory.com/)",
@@ -101,6 +101,9 @@
101
101
  "@fingerprintjs/fingerprintjs": {
102
102
  "optional": true
103
103
  },
104
+ "react": {
105
+ "optional": true
106
+ },
104
107
  "web-vitals": {
105
108
  "optional": true
106
109
  }
@@ -113,6 +116,7 @@
113
116
  "@types/react": "^19.3.0",
114
117
  "@types/react-dom": "^19.3.0",
115
118
  "@typescript/native": "npm:typescript@^7.0.2",
119
+ "@vitest/coverage-v8": "^5.0.1",
116
120
  "eslint": "^10.11.0",
117
121
  "eslint-config-prettier": "^10.1.8",
118
122
  "eslint-plugin-react-hooks": "^7.1.1",