react-marketing-tools 1.0.0-beta.3 → 1.0.1

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,146 +1,397 @@
1
1
  # React Marketing Tools
2
2
 
3
- [![npm next](https://img.shields.io/npm/v/react-marketing-tools/next?label=npm%40next)](https://www.npmjs.com/package/react-marketing-tools?activeTab=versions)
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)
4
8
  [![license](https://img.shields.io/npm/l/react-marketing-tools)](./LICENSE)
5
9
 
6
- One `track()` call for Google Tag Manager, Google Analytics 4 and the Meta Pixel, with Consent Mode v2, UTM attribution
7
- and personal-data redaction built in.
8
-
9
- **[Try it in the playground](https://bronz3beard.github.io/react-marketing-tools/)**: see what each vendor receives for
10
- every event, without sending anything.
11
-
12
- > **1.0 is in beta** on the `next` tag. `npm install react-marketing-tools` still installs 0.4.x, whose API 1.0
13
- > replaces; see the [migration guide](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md)
14
- > and the [changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md).
10
+ [![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
+ [![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
+ [![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
+ [![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)
15
+
16
+ [![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)
17
+ [![discussions](https://img.shields.io/github/discussions/bronz3beard/react-marketing-tools)](https://github.com/bronz3beard/react-marketing-tools/discussions)
18
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/bronz3beard/react-marketing-tools/blob/main/CONTRIBUTING.md)
19
+
20
+ Send one event from your React app and it reaches Google Tag Manager, Google Analytics 4 and the Meta Pixel at once.
21
+ The library keeps the visitor's privacy choices, remembers which campaign brought them, strips personal data out of
22
+ events, and can send the same events again from your server so they still arrive when a browser blocks tracking.
23
+
24
+ **[Try it in the playground](https://bronz3beard.github.io/react-marketing-tools/)**: press a button and see exactly
25
+ what each of those services would receive. Nothing is sent anywhere.
26
+
27
+ > **Setting this up with an AI assistant?** There's a prompt written for that:
28
+ > [Set this up with an AI assistant](#set-this-up-with-an-ai-assistant). It interviews you, writes the wiring, and
29
+ > tells you what to click in each service.
30
+
31
+ > **Coming from 0.4?** 1.0 is a rewrite, and the API is different. The
32
+ > [migration guide](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md) maps the old
33
+ > names to the new ones, and the
34
+ > [changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md) lists everything that
35
+ > changed.
36
+
37
+ ## Contents
38
+
39
+ - [What you need before you start](#what-you-need-before-you-start)
40
+ - [Install](#install)
41
+ - [Set this up with an AI assistant](#set-this-up-with-an-ai-assistant)
42
+ - [Quick start](#quick-start)
43
+ - [1. Create the analytics instance](#1-create-the-analytics-instance)
44
+ - [2. Hand it to your app](#2-hand-it-to-your-app)
45
+ - [3. Track an event](#3-track-an-event)
46
+ - [Track clicks without writing code](#track-clicks-without-writing-code)
47
+ - [Follow a multi-step flow](#follow-a-multi-step-flow)
48
+ - [Tell the library who the visitor is](#tell-the-library-who-the-visitor-is)
49
+ - [Record what the visitor consented to](#record-what-the-visitor-consented-to)
50
+ - [Recognise a returning visitor](#recognise-a-returning-visitor)
51
+ - [Report how fast your pages are](#report-how-fast-your-pages-are)
52
+ - [Send events from your server](#send-events-from-your-server)
53
+ - [A purchase confirmed by a payment webhook](#a-purchase-confirmed-by-a-payment-webhook)
54
+ - [Send the Pixel's events from your server too](#send-the-pixels-events-from-your-server-too)
55
+ - [Use it without React](#use-it-without-react)
56
+ - [What each service receives](#what-each-service-receives)
57
+ - [Documentation](#documentation)
58
+ - [License](#license)
59
+
60
+ ## What you need before you start
61
+
62
+ This library sends events to accounts you already have. It doesn't create or configure them for you, and it isn't a
63
+ replacement for them. Before you install it, set up the ones you want to use:
64
+
65
+ | You want | What you need first | What the library needs from it |
66
+ | --- | --- | --- |
67
+ | Google Tag Manager | A container, with the tags and triggers that decide what happens to each event | The container ID, like `GTM-XXXXXXX` |
68
+ | Google Analytics 4 | A property with a web data stream | The measurement ID, like `G-XXXXXXX` |
69
+ | Meta Pixel | A dataset (pixel) in Meta Events Manager | The pixel ID, a long number |
70
+ | Events sent from your server to GA4 | An API secret on that same data stream | The secret, kept on your server |
71
+ | Events sent from your server to Meta | A Conversions API access token in Events Manager | The token, kept on your server |
72
+
73
+ Two things worth knowing:
74
+
75
+ - **With Tag Manager, your container still decides what happens.** The library puts each event into the dataLayer, the
76
+ list of events Tag Manager watches. Until you add a trigger and a tag for an event name, the event is recorded but
77
+ goes nowhere.
78
+ - **With Google Analytics 4, events arrive on their own.** They show up as events with the name you tracked. Using the
79
+ names Google recommends, such as `purchase` or `sign_up`, fills in GA4's built-in reports; your own names appear in
80
+ reports and explorations once you use them.
81
+
82
+ You only configure the services you use. Many teams start with Tag Manager alone and add the rest later.
15
83
 
16
84
  ## Install
17
85
 
18
86
  ```sh
19
- npm install react-marketing-tools@next
87
+ npm install react-marketing-tools
20
88
  ```
21
89
 
22
- Requires React 18 or 19. The package is ESM-only; server rendering needs Node.js 22.12 or later.
90
+ Works with React 18 and 19. The package is published as ES modules, the `import` style of JavaScript, so it works in
91
+ every current bundler and in Node.js 22.12 or later; it can't be loaded with `require()`.
92
+
93
+ ## Set this up with an AI assistant
94
+
95
+ There's a prompt you can paste into any AI coding assistant. It reads your project, asks what you want to measure and
96
+ which analytics accounts you have, then writes the setup, a checklist of what to click in Google Tag Manager, Google
97
+ Analytics and Meta, and the steps to check it all worked.
98
+
99
+ **[Get the prompt](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/agent-setup.md)**, paste it into
100
+ your assistant, and answer its questions. That page also lists the six things to check before you trust what it wrote,
101
+ and an `AGENTS.md` block to keep in your repo so later sessions don't re-guess the conventions this one chose.
23
102
 
24
- ## Usage
103
+ The prompt is tested: three assistants set up a Next.js app and a Vite app from scratch, and what they got wrong is
104
+ what those rules are there to prevent.
25
105
 
26
- Create one instance:
106
+ Working by hand is just as quick for a simple setup: the [quick start](#quick-start) below, or the
107
+ [integration walkthrough](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/integration-walkthrough.md)
108
+ for an app that already has analytics in it.
109
+
110
+ ## Quick start
111
+
112
+ Adding this to an app that already exists, with analytics already in it? Follow the
113
+ [integration walkthrough](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/integration-walkthrough.md)
114
+ instead: same steps, plus how to prove events arrive and how to retire the library you're replacing.
115
+
116
+ ### 1. Create the analytics instance
27
117
 
28
118
  ```ts
29
119
  // analytics.ts
30
120
  import { createAnalytics } from 'react-marketing-tools'
31
121
 
32
122
  export const analytics = createAnalytics({
33
- consent: 'denied', // until your consent banner records the visitor's choice
123
+ // Nothing is stored or sent until you record the visitor's choice; see "Record what the visitor consented to".
124
+ consent: 'denied',
34
125
  gtm: { containerId: 'GTM-XXXXXXX' },
35
126
  ga4: { measurementId: 'G-XXXXXXX' },
36
127
  metaPixel: { pixelId: '1234567890123456' },
37
128
  })
38
129
  ```
39
130
 
40
- Provide it to your app:
131
+ Creating the instance does nothing on its own: no scripts load and no cookies are written until your app starts it,
132
+ which the provider below does. That makes this file safe to import anywhere, including in server-rendered pages.
133
+
134
+ ### 2. Hand it to your app
41
135
 
42
136
  ```tsx
43
137
  // main.tsx
138
+ import { StrictMode } from 'react'
139
+ import { createRoot } from 'react-dom/client'
44
140
  import { AnalyticsProvider } from 'react-marketing-tools'
45
141
  import { analytics } from './analytics'
142
+ import { App } from './App'
46
143
 
47
144
  createRoot(document.getElementById('root')!).render(
48
- <AnalyticsProvider analytics={analytics}>
49
- <App />
50
- </AnalyticsProvider>,
145
+ <StrictMode>
146
+ <AnalyticsProvider analytics={analytics}>
147
+ <App />
148
+ </AnalyticsProvider>
149
+ </StrictMode>,
51
150
  )
52
151
  ```
53
152
 
54
- Track from any component:
153
+ The provider loads the vendor scripts once your app is running in the browser. Anything you track before that is held
154
+ and sent in order afterwards, so you never lose an event that happened during startup.
155
+
156
+ ### 3. Track an event
55
157
 
56
158
  ```tsx
159
+ // SignUpButton.tsx
57
160
  import { useAnalytics } from 'react-marketing-tools'
58
161
 
59
162
  export const SignUpButton = () => {
60
163
  const { track } = useAnalytics()
61
164
 
62
- // Reaches GTM, GA4 and Meta (as CompleteRegistration) with one shared event_id
63
- return <button onClick={() => track('sign_up', { method: 'google' })}>Sign up</button>
165
+ return (
166
+ <button onClick={() => track('sign_up', { method: 'google' })}>
167
+ Sign up
168
+ </button>
169
+ )
64
170
  }
65
171
  ```
66
172
 
67
- Or without code, with `autocapture: { clicks: true }` in the config:
173
+ That one call reaches all three services: Tag Manager gets a `sign_up` event, Google Analytics 4 gets `sign_up`, and
174
+ the Meta Pixel gets `CompleteRegistration`, the name Meta uses for the same thing. All three carry the same event ID,
175
+ so when the same action arrives twice, from the browser and from your server, Meta counts it once.
68
176
 
69
- ```html
70
- <button data-analytics-event="cta_click" data-analytics-param-location="hero">Start</button>
177
+ ## Track clicks without writing code
178
+
179
+ Turn it on once, and then mark the elements you care about. No analytics code in your components:
180
+
181
+ ```ts
182
+ // analytics.ts
183
+ import { createAnalytics } from 'react-marketing-tools'
184
+
185
+ export const analytics = createAnalytics({
186
+ consent: 'denied',
187
+ gtm: { containerId: 'GTM-XXXXXXX' },
188
+ autocapture: { clicks: true },
189
+ })
71
190
  ```
72
191
 
73
- Follow a multi-step flow as a funnel:
192
+ ```tsx
193
+ // PricingPage.tsx
194
+ export const PricingPage = () => (
195
+ <section>
196
+ <h1>Pricing</h1>
197
+ <button
198
+ data-analytics-event="cta_click"
199
+ data-analytics-param-location="pricing_header"
200
+ data-analytics-param-plan="pro"
201
+ >
202
+ Start free trial
203
+ </button>
204
+ </section>
205
+ )
206
+ ```
207
+
208
+ A click anywhere inside that button, including on an icon or text inside it, sends the same event as
209
+ `track('cta_click', { location: 'pricing_header', plan: 'pro' })`.
210
+
211
+ - `data-analytics-event` is the event name. Every `data-analytics-param-*` attribute becomes one detail on the event,
212
+ with dashes turned into underscores: `data-analytics-param-button-text` arrives as `button_text`.
213
+ - Values are text. For numbers, such as a price you want to add up, call `track()` instead.
214
+ - It works for elements added later, for example after a route change, and for links as well as buttons.
215
+ - If several marked elements are nested, the closest one to the click wins.
216
+
217
+ ## Follow a multi-step flow
218
+
219
+ A journey groups the steps of a flow such as a checkout, so you can see where people drop out:
220
+
221
+ ```tsx
222
+ // Checkout.tsx
223
+ import { useState } from 'react'
224
+ import { useAnalytics } from 'react-marketing-tools'
225
+
226
+ export const Checkout = () => {
227
+ const { journey } = useAnalytics()
228
+ // Create it once for this flow, not on every render: each journey has its own ID.
229
+ const [checkout] = useState(() => journey('checkout'))
230
+
231
+ return (
232
+ <>
233
+ <button onClick={() => checkout.step('shipping')}>Continue to payment</button>
234
+ <button onClick={() => checkout.complete({ value: 42, currency: 'USD' })}>Pay</button>
235
+ <button onClick={() => checkout.abandon('changed_mind')}>Cancel</button>
236
+ </>
237
+ )
238
+ }
239
+ ```
240
+
241
+ The first call also sends a `journey_start` event, and every event in the flow carries the same journey ID. In Google
242
+ Analytics 4 you can then build a funnel from `journey_start`, `journey_step` and `journey_complete`.
243
+
244
+ ## Tell the library who the visitor is
74
245
 
75
246
  ```ts
76
- const checkout = analytics.journey('checkout')
77
- checkout.step('shipping') // journey_start, then journey_step
78
- checkout.complete({ value: 42, currency: 'USD' }) // journey_complete
247
+ // after your sign-in code succeeds
248
+ import { analytics } from './analytics'
249
+
250
+ analytics.identify('user-42', { email: 'ada@example.com' })
79
251
  ```
80
252
 
81
- Report Core Web Vitals (LCP, INP, CLS) to GA4 and Tag Manager, after `npm install web-vitals@^6`:
253
+ The ID is your own user ID: it goes to Tag Manager and Google Analytics 4 so you can join sessions to accounts. The
254
+ email address is not sent to Google. It goes only to Meta, which uses contact details to match a visitor to a Facebook
255
+ or Instagram account, and it's scrambled into an unreadable fingerprint (a hash) before it's sent. Call
256
+ `analytics.reset()` when someone signs out.
257
+
258
+ ## Record what the visitor consented to
82
259
 
83
260
  ```ts
84
- import { trackWebVitals } from 'react-marketing-tools/web-vitals'
261
+ // from your cookie banner, when the visitor answers
262
+ import { analytics } from './analytics'
85
263
 
86
- void trackWebVitals(analytics)
264
+ analytics.consent.update({ analytics: 'granted', ads: 'granted' })
87
265
  ```
88
266
 
89
- The same instance identifies users, records consent and gives a consenting visitor a stable ID:
267
+ Until this is called, the instance follows the `consent` value you passed when you created it. The library passes the
268
+ choice to Google's Consent Mode, which is how Google's tags are told what a visitor agreed to, and to the Meta Pixel,
269
+ which holds events until it's allowed to send them. The same choice controls what the library itself stores in the
270
+ browser. Both parts of the choice can be set separately if your banner asks separately.
271
+
272
+ ## Recognise a returning visitor
90
273
 
91
274
  ```ts
92
- analytics.identify('user-42', { email: 'ada@example.com' }) // user id for GTM and GA4, advanced matching for Meta
93
- analytics.consent.update({ analytics: 'granted', ads: 'granted' }) // Google Consent Mode v2 and Meta consent
94
- const visitorId = await analytics.getVisitorId() // random by default, or a fingerprint; never sent to GA4
275
+ import { analytics } from './analytics'
276
+
277
+ // inside an async function, any time after your app has started
278
+ const visitorId = await analytics.getVisitorId()
279
+ ```
280
+
281
+ This is a random ID kept in the browser, given only to visitors who consented to analytics, so you can join a person's
282
+ visits in your own systems. It's never sent to Google Analytics. You can swap it for a browser fingerprint if you want
283
+ to recognise people who clear their cookies; see [Visitor ID](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/visitor-id.md).
284
+
285
+ ## Report how fast your pages are
286
+
287
+ Core Web Vitals are Google's three measures of page experience: how fast the main content appears, how quickly the page
288
+ responds to a tap or click, and how much the layout jumps around. Install Google's measuring library and pass it your
289
+ instance:
290
+
291
+ ```sh
292
+ npm install web-vitals@^6
95
293
  ```
96
294
 
97
- Configure only the destinations you use. Without React, import `createAnalytics` from `react-marketing-tools/core` and
98
- call `analytics.start()` yourself.
295
+ ```ts
296
+ // main.tsx, after the render call
297
+ import { trackWebVitals } from 'react-marketing-tools/web-vitals'
298
+ import { analytics } from './analytics'
299
+
300
+ void trackWebVitals(analytics)
301
+ ```
302
+
303
+ Each measurement arrives as an event named `LCP`, `INP` or `CLS` in Google Analytics 4 and Tag Manager. They're never
304
+ sent to Meta.
305
+
306
+ ## Send events from your server
307
+
308
+ Some things happen where the browser can't see them, such as a payment your payment provider confirms minutes later.
309
+ Other events simply never make it, because an extension or browser setting blocks the tracking scripts. For both, send
310
+ the event from your server.
99
311
 
100
- Send events that happen on your server, such as a purchase confirmed by a payment webhook, from
101
- `react-marketing-tools/server`:
312
+ ### A purchase confirmed by a payment webhook
102
313
 
103
314
  ```ts
315
+ // inside your payment webhook, where `order` is the order you just confirmed
104
316
  import { sendMeasurementProtocolEvent } from 'react-marketing-tools/server'
105
317
 
106
318
  await sendMeasurementProtocolEvent({
107
319
  measurementId: 'G-XXXXXXX',
108
320
  apiSecret: process.env.GA4_API_SECRET!,
109
- clientId: order.ga4ClientId, // saved at checkout with readGa4Cookies()
110
- events: [{ name: 'purchase', params: { transaction_id: order.id, value: 42, currency: 'USD' } }],
321
+ // Saved when the visitor started checkout, with readGa4Cookies() from the same import.
322
+ clientId: order.ga4ClientId,
323
+ events: [
324
+ {
325
+ name: 'purchase',
326
+ params: { transaction_id: order.id, value: order.total, currency: 'USD' },
327
+ },
328
+ ],
111
329
  })
112
330
  ```
113
331
 
114
- `sendConversionsApiEvent` does the same for Meta, hashing customer information as Meta requires. To have Meta receive
115
- the events the browser Pixel misses, counted once, relay them through your server:
332
+ The same import has `sendConversionsApiEvent` for Meta, which scrambles customer details into hashes the way Meta
333
+ requires before sending them.
334
+
335
+ ### Send the Pixel's events from your server too
336
+
337
+ Point the page at an address on your own site, and mount the handler there. Every event the Pixel receives is then sent
338
+ from your server as well, and Meta counts each one once:
116
339
 
117
340
  ```ts
118
341
  // analytics.ts
119
- createAnalytics({ /* …as above */ server: { endpoint: '/api/track' } })
342
+ import { createAnalytics } from 'react-marketing-tools'
343
+
344
+ export const analytics = createAnalytics({
345
+ consent: 'denied',
346
+ metaPixel: { pixelId: '1234567890123456' },
347
+ server: { endpoint: '/api/track' },
348
+ })
349
+ ```
120
350
 
121
- // app/api/track/route.ts (Next.js; any Request → Response server works)
351
+ ```ts
352
+ // app/api/track/route.ts — a Next.js route; any server that speaks Request and Response works
122
353
  import { createTrackHandler } from 'react-marketing-tools/server'
123
354
 
124
355
  export const POST = createTrackHandler({
125
356
  allowedOrigins: ['https://shop.example.com'],
126
- meta: { pixelId: '1234567890123456', accessToken: process.env.META_CAPI_TOKEN! },
357
+ meta: {
358
+ pixelId: '1234567890123456',
359
+ accessToken: process.env.META_CAPI_TOKEN!,
360
+ },
127
361
  })
128
362
  ```
129
363
 
130
- ## What each destination receives
364
+ ## Use it without React
365
+
366
+ ```ts
367
+ // analytics.ts
368
+ import { createAnalytics } from 'react-marketing-tools/core'
369
+
370
+ export const analytics = createAnalytics({
371
+ consent: 'granted',
372
+ gtm: { containerId: 'GTM-XXXXXXX' },
373
+ })
374
+
375
+ analytics.start() // the React provider does this for you
376
+ analytics.track('sign_up', { method: 'google' })
377
+ ```
378
+
379
+ ## What each service receives
131
380
 
132
- | | Google Tag Manager | Google Analytics 4 | Meta Pixel | Conversions API relay |
381
+ | | Google Tag Manager | Google Analytics 4 | Meta Pixel | Your server, for Meta |
133
382
  | --- | --- | --- | --- | --- |
134
- | `track()` | dataLayer push with `event_id` | gtag.js event | standard or custom event with `eventID` | the same event from your server |
135
- | `identify()` | `user_id` | `user_id` | advanced matching | user data, hashed on your server |
136
- | `consent.update()` | Consent Mode v2 | Consent Mode v2 | `grant` / `revoke` | only with `adUserData` |
137
- | Attribution | `attribution` (last touch) | read from the page URL | `fbc` | `fbc`, `fbp` |
383
+ | `track()` | the event in the dataLayer, with an event ID | the event through Google's tag | Meta's name for the event, with the same event ID | the same event again, so it still arrives when the browser is blocked |
384
+ | `identify()` | your user ID | your user ID | contact details, hashed by the Pixel | contact details, hashed by your server |
385
+ | `consent.update()` | Google Consent Mode | Google Consent Mode | permission to send, or to hold | only sends with permission to share data with ad platforms |
386
+ | Campaign the visitor came from | the campaign on every event | read from the page address by Google's tag | the Meta click ID | the Meta click and browser IDs |
138
387
 
139
388
  ## Documentation
140
389
 
141
390
  - [All docs](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/README.md)
142
391
  - [Getting started](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/getting-started.md)
143
- - [React](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/react.md): provider, hook, Next.js App Router, single-page apps
392
+ - [Integration walkthrough](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/integration-walkthrough.md): add it to an existing app, step by step, and prove events arrive
393
+ - [React](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/react.md): provider, hook, single-page apps
394
+ - [Next.js](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/nextjs.md): a complete App Router setup, client and server
144
395
  - [Tracking events](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/tracking-events.md): naming rules, page views, journeys, click autocapture, Web Vitals, users, personal data, errors
145
396
  - [Configuration](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/configuration.md)
146
397
  - [Consent](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/consent.md): Consent Mode v2 and Global Privacy Control
@@ -149,11 +400,16 @@ export const POST = createTrackHandler({
149
400
  - [Google Tag Manager](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-tag-manager.md)
150
401
  - [Google Analytics 4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-analytics-4.md)
151
402
  - [Meta Pixel](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-pixel.md)
403
+ - [Other tools](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/custom-destinations.md): PostHog, Umami, Plausible, your own endpoint
404
+ - [Measuring AI activity](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/ai-traffic.md): AI features, visits from AI assistants, AI crawlers
152
405
  - [Server-side tagging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/server-side-tagging.md)
153
406
  - [GA4 Measurement Protocol](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/measurement-protocol.md): GA4 events from your server
154
407
  - [Meta Conversions API](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-conversions-api.md): Meta events from your server
408
+ - [Error tracking](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/error-tracking.md): report errors to Google Analytics 4
155
409
  - [Debugging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/debugging.md): see what's sent, and fix common problems
156
410
  - [Migrating from 0.4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md)
411
+ - [API summary](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/api-summary.md): every option, method and limit on one page
412
+ - [Set up with an AI assistant](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/agent-setup.md): a prompt that interviews you and writes the setup
157
413
  - [Changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md)
158
414
 
159
415
  ## License
@@ -1,13 +1,20 @@
1
- import type { Attribution, CampaignParam } from '../core/types.js';
1
+ import type { AiSources, Attribution, CampaignParam } from '../core/types.js';
2
2
  export declare const CAMPAIGN_PARAMS: readonly CampaignParam[];
3
+ /** Your label for the AI assistant a referrer belongs to: an exact hostname match, or a subdomain of one. */
4
+ export declare const matchAiSource: ({ referrer, aiSources, }: {
5
+ referrer?: string;
6
+ aiSources?: AiSources;
7
+ }) => string | undefined;
3
8
  /**
4
- * The campaign behind a visit, from its landing URL. `undefined` when the URL carries no campaign params, so a plain
5
- * navigation never replaces an earlier touch. Values are email-redacted (email tools put addresses in `utm_term`).
9
+ * The campaign behind a visit, from its landing URL, or the AI assistant that sent it. `undefined` when the URL carries
10
+ * no campaign params and the referrer isn't one of `aiSources`, so a plain navigation never replaces an earlier touch.
11
+ * Values are email-redacted (email tools put addresses in `utm_term`).
6
12
  */
7
- export declare const parseAttribution: ({ url, referrer, capturedAt, }: {
13
+ export declare const parseAttribution: ({ url, referrer, capturedAt, aiSources, }: {
8
14
  url: string;
9
15
  referrer?: string;
10
16
  capturedAt: number;
17
+ aiSources?: AiSources;
11
18
  }) => Attribution | undefined;
12
19
  /** Stored attribution is untrusted input: anything that isn't a well-formed touch is discarded. */
13
20
  export declare const toAttribution: (value: unknown) => Attribution | undefined;
@@ -23,75 +23,87 @@ var a = [
23
23
  if (!URL.canParse(e)) return;
24
24
  let { origin: t, pathname: n } = new URL(e);
25
25
  return t + n;
26
- }, c = ({ url: t, referrer: n, capturedAt: r }) => {
26
+ }, c = ({ referrer: e, aiSources: t }) => {
27
+ if (!e || !t || !URL.canParse(e)) return;
28
+ let n = new URL(e).hostname.toLowerCase();
29
+ return Object.entries(t).find(([, e]) => e.some((e) => {
30
+ let t = e.toLowerCase();
31
+ return n === t || n.endsWith(`.${t}`);
32
+ }))?.[0];
33
+ }, l = ({ url: t, referrer: n, capturedAt: r, aiSources: i }) => {
27
34
  if (!URL.canParse(t)) return;
28
- let { searchParams: i } = new URL(t), c = a.flatMap((e) => {
29
- let t = i.get(e)?.trim();
35
+ let { searchParams: l } = new URL(t), u = a.flatMap((e) => {
36
+ let t = l.get(e)?.trim();
30
37
  return t ? [[e, t]] : [];
38
+ }), d = c({
39
+ referrer: n,
40
+ aiSources: i
31
41
  });
32
- if (c.length === 0) return;
33
- let { params: l } = e(Object.fromEntries(c)), u = Object.fromEntries(Object.entries(l).map(([e, t]) => [e, String(t).slice(0, o)])), d = n ? s(n) : void 0;
42
+ if (u.length === 0 && !d) return;
43
+ let { params: f } = e(Object.fromEntries(u)), p = Object.fromEntries(Object.entries(f).map(([e, t]) => [e, String(t).slice(0, o)])), m = n ? s(n) : void 0;
34
44
  return {
35
- ...u,
45
+ ...p,
36
46
  landing_page: s(t) ?? t,
37
- ...d ? { referrer: d } : {},
47
+ ...m ? { referrer: m } : {},
48
+ ...d ? { ai_source: d } : {},
38
49
  captured_at: r
39
50
  };
40
- }, l = (e) => typeof e == "object" && !!e, u = (e) => {
41
- if (l(e) && typeof e.captured_at == "number" && typeof e.landing_page == "string") return {
51
+ }, u = (e) => typeof e == "object" && !!e, d = (e) => {
52
+ if (u(e) && typeof e.captured_at == "number" && typeof e.landing_page == "string") return {
42
53
  ...Object.fromEntries(a.flatMap((t) => typeof e[t] == "string" ? [[t, e[t]]] : [])),
43
54
  landing_page: e.landing_page,
44
55
  ...typeof e.referrer == "string" ? { referrer: e.referrer } : {},
56
+ ...typeof e.ai_source == "string" ? { ai_source: e.ai_source } : {},
45
57
  captured_at: e.captured_at
46
58
  };
47
- }, d = ({ fbcCookie: e, touch: t }) => e ?? (t?.fbclid ? `fb.1.${t.captured_at}.${t.fbclid}` : void 0), f = "rmt:attribution:first", p = "rmt:attribution:last", m = 864e5, h = (e, t) => {
59
+ }, f = ({ fbcCookie: e, touch: t }) => e ?? (t?.fbclid ? `fb.1.${t.captured_at}.${t.fbclid}` : void 0), p = "rmt:attribution:first", m = "rmt:attribution:last", h = 864e5, g = (e, t) => {
48
60
  try {
49
61
  return t(window[e]);
50
62
  } catch {
51
63
  return;
52
64
  }
53
- }, g = (e, t) => h(e, (e) => {
65
+ }, _ = (e, t) => g(e, (e) => {
54
66
  let n = e.getItem(t);
55
- return n === null ? void 0 : u(JSON.parse(n));
67
+ return n === null ? void 0 : d(JSON.parse(n));
56
68
  }), ee = ({ ttlDays: e }) => {
57
- let t, n, r = (t) => Date.now() - t.captured_at < e * m;
69
+ let t, n, r = (t) => Date.now() - t.captured_at < e * h;
58
70
  return {
59
71
  observe(e) {
60
72
  e && (t ??= e, n = e);
61
73
  },
62
74
  restore() {
63
- let e = g("localStorage", f);
64
- e && r(e) && (!t || e.captured_at <= t.captured_at) && (t = e), n ??= g("sessionStorage", p);
75
+ let e = _("localStorage", p);
76
+ e && r(e) && (!t || e.captured_at <= t.captured_at) && (t = e), n ??= _("sessionStorage", m);
65
77
  },
66
78
  persist() {
67
- h("localStorage", (e) => t && r(t) ? e.setItem(f, JSON.stringify(t)) : e.removeItem(f)), n && h("sessionStorage", (e) => e.setItem(p, JSON.stringify(n)));
79
+ g("localStorage", (e) => t && r(t) ? e.setItem(p, JSON.stringify(t)) : e.removeItem(p)), n && g("sessionStorage", (e) => e.setItem(m, JSON.stringify(n)));
68
80
  },
69
81
  erase() {
70
- h("localStorage", (e) => e.removeItem(f)), h("sessionStorage", (e) => e.removeItem(p));
82
+ g("localStorage", (e) => e.removeItem(p)), g("sessionStorage", (e) => e.removeItem(m));
71
83
  },
72
84
  get: () => ({
73
85
  firstTouch: t,
74
86
  lastTouch: n
75
87
  })
76
88
  };
77
- }, _ = "data-analytics-event", v = "data-analytics-param-", y = (e) => {
89
+ }, v = "data-analytics-event", y = "data-analytics-param-", b = (e) => {
78
90
  if (!(e instanceof Element)) return;
79
- let t = e.closest(`[${_}]`), n = t?.getAttribute(_);
91
+ let t = e.closest(`[${v}]`), n = t?.getAttribute(v);
80
92
  if (t && n) return {
81
93
  name: n,
82
- params: Object.fromEntries(Array.from(t.attributes).filter((e) => e.name.startsWith(v)).map((e) => [e.name.slice(21).replaceAll("-", "_"), e.value]))
94
+ params: Object.fromEntries(Array.from(t.attributes).filter((e) => e.name.startsWith(y)).map((e) => [e.name.slice(21).replaceAll("-", "_"), e.value]))
83
95
  };
84
- }, b = (e) => {
96
+ }, te = (e) => {
85
97
  document.addEventListener("click", (t) => {
86
- let n = y(t.target);
98
+ let n = b(t.target);
87
99
  n && e(n.name, n.params);
88
100
  }, { capture: !0 });
89
- }, x = [
101
+ }, ne = [
90
102
  "analytics",
91
103
  "ads",
92
104
  "adUserData",
93
105
  "adPersonalization"
94
- ], te = (e) => e === "granted" || e === "denied", ne = ({ consent: e, gpc: t }) => {
106
+ ], x = (e) => e === "granted" || e === "denied", S = ({ consent: e, gpc: t }) => {
95
107
  let n = t ? "denied" : e;
96
108
  return {
97
109
  analytics: e,
@@ -99,7 +111,7 @@ var a = [
99
111
  adUserData: n,
100
112
  adPersonalization: n
101
113
  };
102
- }, S = (e, t) => ({
114
+ }, re = (e, t) => ({
103
115
  analytics: t.analytics ?? e.analytics,
104
116
  ads: t.ads ?? e.ads,
105
117
  adUserData: t.adUserData ?? t.ads ?? e.adUserData,
@@ -109,7 +121,7 @@ var a = [
109
121
  ad_storage: e.ads,
110
122
  ad_user_data: e.adUserData,
111
123
  ad_personalization: e.adPersonalization
112
- }), w = (e) => Object.values(e).includes("denied"), re = () => typeof navigator < "u" && navigator.globalPrivacyControl === !0, T = () => window.dataLayer ??= [], E = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/gtag/js")), D = () => window.gtag ??= function() {
124
+ }), w = (e) => Object.values(e).includes("denied"), ie = () => typeof navigator < "u" && navigator.globalPrivacyControl === !0, T = () => window.dataLayer ??= [], E = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/gtag/js")), D = () => window.gtag ??= function() {
113
125
  T().push(arguments);
114
126
  }, O = ({ consent: e, waitForUpdate: t }) => {
115
127
  D()("consent", "default", {
@@ -125,8 +137,8 @@ var a = [
125
137
  }, j = (e) => Array.from(document.scripts).some((t) => t.src === e), M = ({ src: e, nonce: t }) => {
126
138
  let n = document.createElement("script");
127
139
  n.async = !0, n.src = e, t && n.setAttribute("nonce", t), document.head.append(n);
128
- }, ie = /^G-[A-Z0-9]+$/, ae = ({ measurementId: e, pageViews: t = "auto", loadScript: n = !0, serverContainerUrl: r, waitForUpdate: i = 500, nonce: a }) => {
129
- if (!ie.test(e)) throw Error(`[react-marketing-tools] ga4.measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)}).`);
140
+ }, ae = /^G-[A-Z0-9]+$/, oe = ({ measurementId: e, pageViews: t = "auto", loadScript: n = !0, serverContainerUrl: r, waitForUpdate: i = 500, nonce: a }) => {
141
+ if (!ae.test(e)) throw Error(`[react-marketing-tools] ga4.measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)}).`);
130
142
  r !== void 0 && A(r, "ga4.serverContainerUrl");
131
143
  let o = `https://www.googletagmanager.com/gtag/js?id=${e}`, s = (e, t) => r ? {
132
144
  ...e,
@@ -276,7 +288,7 @@ var a = [
276
288
  ph: t.phone,
277
289
  fn: t.firstName,
278
290
  ln: t.lastName
279
- }).filter(([, e]) => e !== void 0)), q = (e) => e.adUserData === "granted" ? "grant" : "revoke", oe = ({ pixelId: e, pageViews: t = "auto", loadScript: n = !0, nonce: r }) => {
291
+ }).filter(([, e]) => e !== void 0)), q = (e) => e.adUserData === "granted" ? "grant" : "revoke", se = ({ pixelId: e, pageViews: t = "auto", loadScript: n = !0, nonce: r }) => {
280
292
  if (!H.test(e)) throw Error(`[react-marketing-tools] metaPixel.pixelId must be the numeric pixel ID (received ${JSON.stringify(e)}).`);
281
293
  return {
282
294
  name: "metaPixel",
@@ -304,10 +316,10 @@ var a = [
304
316
  constructor(e, t, n) {
305
317
  super(`[react-marketing-tools] ${t}`, n), this.name = "AnalyticsError", this.code = e;
306
318
  }
307
- }, se = (e) => e.startsWith("/") && !e.startsWith("//") || URL.canParse(e) && new URL(e).protocol === "https:", ce = ({ endpoint: e, relayPageViews: t, visitorId: n, onError: r }) => {
308
- if (!se(e)) throw Error(`[react-marketing-tools] server.endpoint must be a path on your site, such as '/api/track', or an https:// URL (received ${JSON.stringify(e)}).`);
319
+ }, ce = (e) => e.startsWith("/") && !e.startsWith("//") || URL.canParse(e) && new URL(e).protocol === "https:", le = ({ endpoint: e, relayPageViews: t, visitorId: n, onError: r }) => {
320
+ if (!ce(e)) throw Error(`[react-marketing-tools] server.endpoint must be a path on your site, such as '/api/track', or an https:// URL (received ${JSON.stringify(e)}).`);
309
321
  let a = "denied", o, s = (e) => {
310
- let t = i(document.cookie, "_fbp"), r = d({
322
+ let t = i(document.cookie, "_fbp"), r = f({
311
323
  fbcCookie: i(document.cookie, "_fbc"),
312
324
  touch: e.attribution
313
325
  }), a = o?.userId ?? n();
@@ -367,31 +379,31 @@ var a = [
367
379
  o = void 0;
368
380
  }
369
381
  };
370
- }, 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 = "rmt:vid", le = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, Z = () => {
382
+ }, 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 = "rmt:vid", ue = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, de = () => {
371
383
  try {
372
384
  let e = localStorage.getItem(X);
373
- return e !== null && le.test(e) ? e : void 0;
385
+ return e !== null && ue.test(e) ? e : void 0;
374
386
  } catch {
375
387
  return;
376
388
  }
377
- }, Q = (e) => {
389
+ }, Z = (e) => {
378
390
  try {
379
391
  e ? localStorage.setItem(X, e) : localStorage.removeItem(X);
380
392
  } catch {}
381
- }, ue = () => {
393
+ }, fe = () => {
382
394
  let e, t = (t) => {
383
395
  if (t.analytics !== "granted") return;
384
- let n = Z();
385
- return n ? e = n : (e ??= Y(), Q(e), e);
396
+ let n = de();
397
+ return n ? e = n : (e ??= Y(), Z(e), e);
386
398
  };
387
399
  return {
388
400
  peek: t,
389
401
  get: (e) => Promise.resolve(t(e)),
390
402
  erase() {
391
- e = void 0, Q(void 0);
403
+ e = void 0, Z(void 0);
392
404
  }
393
405
  };
394
- }, de = ({ fingerprint: e, onError: t }) => {
406
+ }, pe = ({ fingerprint: e, onError: t }) => {
395
407
  let n, r, i = (e) => e.analytics === "granted" && e.ads === "granted";
396
408
  return {
397
409
  peek: (e) => i(e) ? r : void 0,
@@ -407,16 +419,16 @@ var a = [
407
419
  n = void 0, r = void 0;
408
420
  }
409
421
  };
410
- }, fe = ({ config: e, onError: t }) => {
422
+ }, me = ({ config: e, onError: t }) => {
411
423
  if (e !== !1) {
412
- if (e === "random") return ue();
413
- if (typeof e?.fingerprint == "function") return de({
424
+ if (e === "random") return fe();
425
+ if (typeof e?.fingerprint == "function") return pe({
414
426
  fingerprint: e.fingerprint,
415
427
  onError: t
416
428
  });
417
429
  throw Error(`[react-marketing-tools] visitorId must be 'random', false or { fingerprint: () => Promise<string> } (received ${JSON.stringify(e)}).`);
418
430
  }
419
- }, pe = ({ name: e, track: t, fail: n }) => {
431
+ }, he = ({ name: e, track: t, fail: n }) => {
420
432
  let r = {
421
433
  journey_id: Y(),
422
434
  journey_name: e
@@ -446,14 +458,14 @@ var a = [
446
458
  }));
447
459
  }
448
460
  };
449
- }, $ = () => typeof window < "u", me = 90, he = (e) => {
461
+ }, Q = () => typeof window < "u", ge = 90, $ = (e) => {
450
462
  if (e.consent !== "granted" && e.consent !== "denied") throw Error(`[react-marketing-tools] createAnalytics: "consent" must be 'granted' or 'denied' (received ${JSON.stringify(e.consent)}).`);
451
- }, ge = (a) => {
452
- he(a);
453
- let { debug: o = !1, onError: s = console.error, respectGpc: l = !0 } = a, u = ne({
463
+ }, _e = (a) => {
464
+ $(a);
465
+ let { debug: o = !1, onError: s = console.error, respectGpc: c = !0 } = a, u = S({
454
466
  consent: a.consent,
455
- gpc: l && re()
456
- }), f = u, p, m = fe({
467
+ gpc: c && ie()
468
+ }), d = u, p, m = me({
457
469
  config: a.visitorId ?? "random",
458
470
  onError: s
459
471
  }), h = () => {}, g = new Promise((e) => {
@@ -463,29 +475,30 @@ var a = [
463
475
  ...a.gtm,
464
476
  nonce: a.nonce
465
477
  })] : [],
466
- ...a.ga4 ? [ae({
478
+ ...a.ga4 ? [oe({
467
479
  ...a.ga4,
468
480
  nonce: a.nonce
469
481
  })] : [],
470
- ...a.metaPixel ? [oe({
482
+ ...a.metaPixel ? [se({
471
483
  ...a.metaPixel,
472
484
  nonce: a.nonce
473
485
  })] : [],
474
- ...a.server ? [ce({
486
+ ...a.server ? [le({
475
487
  ...a.server,
476
488
  relayPageViews: !a.metaPixel || a.metaPixel.pageViews === "manual",
477
- visitorId: () => m?.peek(f),
489
+ visitorId: () => m?.peek(d),
478
490
  onError: s
479
491
  })] : [],
480
492
  ...a.destinations ?? []
481
- ], v = [], y = !1, C = a.attribution === !1 ? void 0 : ee({ ttlDays: typeof a.attribution == "object" && a.attribution.ttlDays || me }), w = !1, T = () => {
482
- w || (w = !0, C?.observe(c({
493
+ ], v = [], y = !1, b = a.attribution === !1 ? void 0 : ee({ ttlDays: typeof a.attribution == "object" && a.attribution.ttlDays || ge }), C = typeof a.attribution == "object" ? a.attribution.aiSources : void 0, w = !1, T = () => {
494
+ w || (w = !0, b?.observe(l({
483
495
  url: location.href,
484
496
  referrer: document.referrer,
485
- capturedAt: Date.now()
497
+ capturedAt: Date.now(),
498
+ aiSources: C
486
499
  })));
487
- }, E = () => y && f.analytics === "granted", D = () => {
488
- y && a.server && m?.get(f);
500
+ }, E = () => y && d.analytics === "granted", D = () => {
501
+ y && a.server && m?.get(d);
489
502
  }, O = (e) => {
490
503
  if (o) throw e;
491
504
  s(e);
@@ -506,7 +519,7 @@ var a = [
506
519
  for (let e of t(i)) O(new J("invalid_param", `event "${n}" ${e}`));
507
520
  let s = e(i), c = a?.meta || void 0, l = c?.params && e(c.params), u = [...s.redactedKeys, ...l?.redactedKeys.map((e) => `meta.${e}`) ?? []];
508
521
  u.length > 0 && O(new J("pii_redacted", `event "${n}": personal data redacted from ${u.join(", ")}`)), T();
509
- let d = C?.get().lastTouch;
522
+ let d = b?.get().lastTouch;
510
523
  return {
511
524
  name: n,
512
525
  params: s.params,
@@ -522,7 +535,7 @@ var a = [
522
535
  timestamp: Date.now()
523
536
  };
524
537
  }, M = (e, t = {}, n) => {
525
- if (!$()) return;
538
+ if (!Q()) return;
526
539
  let r = j({
527
540
  name: e,
528
541
  params: t,
@@ -532,23 +545,24 @@ var a = [
532
545
  };
533
546
  return {
534
547
  start() {
535
- !y && $() && (y = !0, T(), E() && (C?.restore(), C?.persist()), D(), k((e) => e.start({
548
+ !y && Q() && (y = !0, T(), E() && (b?.restore(), b?.persist()), D(), k((e) => e.start({
536
549
  consent: u,
537
550
  identity: p
538
- })), v.splice(0).forEach(k), a.autocapture?.clicks && b(M), h());
551
+ })), v.splice(0).forEach(k), a.autocapture?.clicks && te(M), h());
539
552
  },
540
553
  track: M,
541
- journey: (e) => pe({
554
+ journey: (e) => he({
542
555
  name: e,
543
556
  track: M,
544
557
  fail: O
545
558
  }),
546
559
  page(e = {}) {
547
- if (!$()) return;
548
- T(), C?.observe(c({
560
+ if (!Q()) return;
561
+ T(), b?.observe(l({
549
562
  url: location.href,
550
- capturedAt: Date.now()
551
- })), E() && C?.persist();
563
+ capturedAt: Date.now(),
564
+ aiSources: C
565
+ })), E() && b?.persist();
552
566
  let t = j({
553
567
  name: "page_view",
554
568
  params: {
@@ -560,7 +574,7 @@ var a = [
560
574
  t && A((e) => e.page ? e.page(t) : e.track(t));
561
575
  },
562
576
  identify(e, t = {}) {
563
- if (!$()) return;
577
+ if (!Q()) return;
564
578
  if (!e || n(e)) {
565
579
  O(new J("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
566
580
  return;
@@ -572,27 +586,27 @@ var a = [
572
586
  p = r, A((e) => e.identify?.(r));
573
587
  },
574
588
  reset() {
575
- $() && (p = void 0, A((e) => e.reset?.()));
589
+ Q() && (p = void 0, A((e) => e.reset?.()));
576
590
  },
577
591
  consent: {
578
592
  update(e) {
579
- if (!$()) return;
580
- let t = Object.entries(e).filter(([e, t]) => t !== void 0 && !(x.includes(e) && te(t)));
593
+ if (!Q()) return;
594
+ let t = Object.entries(e).filter(([e, t]) => t !== void 0 && !(ne.includes(e) && x(t)));
581
595
  if (t.length > 0) {
582
596
  O(new J("invalid_consent", `consent.update() ignored: ${t.map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(", ")}. Use analytics, ads, adUserData or adPersonalization with 'granted' or 'denied'.`));
583
597
  return;
584
598
  }
585
- let n = f, r = S(f, e);
586
- f = r, n.analytics === "granted" && r.analytics === "denied" ? (C?.erase(), m?.erase()) : E() && (C?.restore(), C?.persist()), D(), A((e) => e.consent?.(r));
599
+ let n = d, r = re(d, e);
600
+ d = r, n.analytics === "granted" && r.analytics === "denied" ? (b?.erase(), m?.erase()) : E() && (b?.restore(), b?.persist()), D(), A((e) => e.consent?.(r));
587
601
  },
588
- get: () => f
602
+ get: () => d
589
603
  },
590
604
  getAttribution() {
591
- if (!$()) return {};
605
+ if (!Q()) return {};
592
606
  T();
593
- let e = C?.get() ?? {};
594
- if (f.adUserData !== "granted") return e;
595
- let t = i(document.cookie, "_fbp"), n = d({
607
+ let e = b?.get() ?? {};
608
+ if (d.adUserData !== "granted") return e;
609
+ let t = i(document.cookie, "_fbp"), n = f({
596
610
  fbcCookie: i(document.cookie, "_fbc"),
597
611
  touch: e.lastTouch
598
612
  });
@@ -603,9 +617,9 @@ var a = [
603
617
  };
604
618
  },
605
619
  getVisitorId() {
606
- return !$() || !m ? Promise.resolve(void 0) : g.then(() => m.get(f));
620
+ return !Q() || !m ? Promise.resolve(void 0) : g.then(() => m.get(d));
607
621
  }
608
622
  };
609
623
  };
610
624
  //#endregion
611
- export { J as n, ge as t };
625
+ export { J as n, _e as t };
@@ -25,12 +25,20 @@ export type TrackOptions = {
25
25
  };
26
26
  };
27
27
  export type CampaignParam = 'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_term' | 'utm_content' | 'utm_id' | 'utm_source_platform' | 'utm_creative_format' | 'utm_marketing_tactic' | 'gclid' | 'gbraid' | 'wbraid' | 'dclid' | 'fbclid' | 'msclkid' | 'ttclid' | 'li_fat_id' | 'twclid';
28
+ /**
29
+ * Labels for visits that arrive from AI assistants, as `{ label: [hostname, …] }`, for example
30
+ * `{ chatgpt: ['chatgpt.com', 'chat.openai.com'], perplexity: ['perplexity.ai'] }`. A referrer matches a hostname
31
+ * exactly or as a subdomain of it. The list is yours to keep up to date: the library ships none.
32
+ */
33
+ export type AiSources = Record<string, string[]>;
28
34
  /** Where a visit came from: the campaign params and ad click IDs of its landing URL. */
29
35
  export type Attribution = Partial<Record<CampaignParam, string>> & {
30
36
  /** Origin and path of the landing page, without its query string. */
31
37
  landing_page: string;
32
38
  /** Origin and path of the referring page, when there was one. */
33
39
  referrer?: string;
40
+ /** Your label for the AI assistant this visit came from, when the referrer matched `attribution.aiSources`. */
41
+ ai_source?: string;
34
42
  /** Milliseconds since the Unix epoch when the visit was captured. */
35
43
  captured_at: number;
36
44
  };
@@ -162,10 +170,12 @@ export type AnalyticsConfig = {
162
170
  respectGpc?: boolean;
163
171
  /**
164
172
  * Capture UTM params and ad click IDs from landing URLs. Defaults to `true`. First and last touch are stored in the
165
- * browser only with analytics consent; `ttlDays` (default 90) is how long a first touch is kept.
173
+ * browser only with analytics consent; `ttlDays` (default 90) is how long a first touch is kept. `aiSources` labels
174
+ * visits that arrive from AI assistants.
166
175
  */
167
176
  attribution?: boolean | {
168
177
  ttlDays?: number;
178
+ aiSources?: AiSources;
169
179
  };
170
180
  /** A stable ID for this visitor, only with consent and never sent to GA4. Defaults to `'random'`. */
171
181
  visitorId?: VisitorIdConfig;
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, Attribution, AttributionSnapshot, CampaignParam, ConsentState, ConsentStatus, ConsentUpdate, Destination, EventParams, Ga4Config, GtmConfig, Identity, IdentityTraits, Journey, MetaPixelConfig, ServerRelayConfig, TrackOptions, VisitorIdConfig, } from './core/types.js';
4
+ export type { AiSources, Analytics, AnalyticsConfig, AnalyticsEvent, Attribution, AttributionSnapshot, CampaignParam, ConsentState, ConsentStatus, ConsentUpdate, Destination, EventParams, Ga4Config, GtmConfig, Identity, IdentityTraits, Journey, MetaPixelConfig, ServerRelayConfig, TrackOptions, VisitorIdConfig, } from './core/types.js';
@@ -0,0 +1,37 @@
1
+ import type { MeasurementProtocolOptions, MeasurementProtocolResult } from './measurementProtocol.js';
2
+ /**
3
+ * The AI crawlers you want to recognise, as `{ label: [user-agent fragment, …] }`, for example
4
+ * `{ gptbot: ['GPTBot'], claudebot: ['ClaudeBot'] }`. Matching ignores case. The list is yours to keep up to date:
5
+ * crawlers come and go, so the library ships none.
6
+ */
7
+ export type AiAgents = Record<string, string[]>;
8
+ /**
9
+ * How this traffic is kept out of your visitor reports. Crawler hits are not people, and mixing them in inflates every
10
+ * number you report.
11
+ *
12
+ * - `'separate-property'`: `measurementId` is a GA4 property used only for crawlers.
13
+ * - `{ trafficType }`: the events carry that `traffic_type` value, and you exclude it with a data filter in
14
+ * **Admin → Data settings → Data filters** of the property you send to.
15
+ */
16
+ export type AiCrawlerReporting = 'separate-property' | {
17
+ trafficType: string;
18
+ };
19
+ export type AiCrawlerOptions = Omit<MeasurementProtocolOptions, 'consent'> & {
20
+ /** Your label for the crawler, from `matchAiAgent()`. */
21
+ agent: string;
22
+ keepSeparate: AiCrawlerReporting;
23
+ };
24
+ /** Your label for the crawler behind a user agent, or undefined when it isn't one you listed. */
25
+ export declare const matchAiAgent: ({ userAgent, agents, }: {
26
+ userAgent: string;
27
+ agents: AiAgents;
28
+ }) => string | undefined;
29
+ /**
30
+ * Sends a crawler's visit to GA4, tagged so it can never be mistaken for a person: every event carries your `ai_agent`
31
+ * label and a `traffic_type`. You must say how the traffic is kept separate, because crawler hits in a visitor property
32
+ * quietly ruin sessions, engagement and conversion rates.
33
+ *
34
+ * Crawlers have no GA4 cookie, so pick the `clientId` yourself: one per crawler (`crawler.gptbot`) counts each crawler
35
+ * as one visitor, a random one per request counts every fetch separately.
36
+ */
37
+ export declare const sendAiCrawlerEvent: (options: AiCrawlerOptions) => Promise<MeasurementProtocolResult>;
package/dist/server.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { META_GRAPH_API_VERSION, sendConversionsApiEvent, } from './server/conversionsApi.js';
2
2
  export type { ConversionsApiEvent, ConversionsApiOptions, ConversionsApiResult, } from './server/conversionsApi.js';
3
+ export { matchAiAgent, sendAiCrawlerEvent } from './server/aiCrawlers.js';
4
+ export type { AiAgents, AiCrawlerOptions, AiCrawlerReporting, } from './server/aiCrawlers.js';
3
5
  export { createTrackHandler } from './server/createTrackHandler.js';
4
6
  export type { TrackHandlerOptions } from './server/createTrackHandler.js';
5
7
  export { readGa4Cookies } from './server/ga4Cookies.js';
package/dist/server.js CHANGED
@@ -78,7 +78,87 @@ var a = async (e) => {
78
78
  body: u,
79
79
  warnings: o
80
80
  };
81
- }, y = 16384, b = /^\d+$/, x = [
81
+ }, y = {
82
+ global: "https://www.google-analytics.com",
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) {
91
+ let t = r(e);
92
+ if (t) throw S(t);
93
+ }
94
+ }, w = ({ event: { name: n, params: r = {} }, sessionId: i, warnings: a }) => {
95
+ for (let e of t(r)) a.push(`event "${n}" ${e}`);
96
+ let { params: o, redactedKeys: s } = e(r);
97
+ return s.length > 0 && a.push(`event "${n}": personal data redacted from ${s.join(", ")}`), {
98
+ name: n,
99
+ params: {
100
+ ...i && { session_id: i },
101
+ engagement_time_msec: 1,
102
+ ...o
103
+ }
104
+ };
105
+ }, T = ({ adUserData: e, adPersonalization: t }) => ({
106
+ ...e && { ad_user_data: e.toUpperCase() },
107
+ ...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({
115
+ event: e,
116
+ sessionId: i,
117
+ warnings: u
118
+ }))
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, {
122
+ method: "POST",
123
+ headers: { "content-type": "application/json" },
124
+ body: JSON.stringify(d)
125
+ });
126
+ if (!c) return {
127
+ ok: p.ok,
128
+ status: p.status,
129
+ warnings: u
130
+ };
131
+ let { validationMessages: m = [] } = await p.json().catch(() => ({}));
132
+ return {
133
+ ok: p.ok && m.length === 0,
134
+ status: p.status,
135
+ warnings: u,
136
+ validationMessages: m
137
+ };
138
+ }, D = ({ userAgent: e, agents: t }) => {
139
+ let n = e.toLowerCase();
140
+ 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) => {
142
+ let { agent: t, keepSeparate: n, events: r, ...i } = e;
143
+ if (!t) throw O("agent is required");
144
+ 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({
147
+ ...i,
148
+ events: r.map((e) => ({
149
+ ...e,
150
+ params: {
151
+ ...e.params,
152
+ traffic_type: a,
153
+ ai_agent: t
154
+ }
155
+ }))
156
+ });
157
+ return {
158
+ ...s,
159
+ warnings: [...o, ...s.warnings]
160
+ };
161
+ }, j = 16384, M = /^\d+$/, N = [
82
162
  "externalId",
83
163
  "email",
84
164
  "phone",
@@ -86,44 +166,44 @@ var a = async (e) => {
86
166
  "lastName",
87
167
  "fbc",
88
168
  "fbp"
89
- ], S = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] createTrackHandler: ${e}`), C = ({ allowedOrigins: e, meta: t }) => {
90
- if (e.length === 0) throw S("allowedOrigins needs at least one origin, e.g. 'https://shop.example.com'");
91
- for (let t of e) if (!URL.canParse(t) || new URL(t).origin !== t) throw S(`allowedOrigins entries must be origins such as 'https://shop.example.com', without a path or trailing slash (received ${JSON.stringify(t)})`);
92
- if (!b.test(t.pixelId)) throw S(`meta.pixelId must be the numeric pixel ID (received ${JSON.stringify(t.pixelId)})`);
93
- if (!t.accessToken) throw S("meta.accessToken is required");
94
- }, w = (e, t) => new Response(t, { status: e }), T = async (e) => {
95
- if (Number(e.headers.get("content-length")) > y) return;
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;
96
176
  if (!e.body) return "";
97
177
  let t = e.body.getReader(), n = new TextDecoder(), r = "", i = 0;
98
178
  for (;;) {
99
179
  let { done: e, value: a } = await t.read();
100
180
  if (e) return r + n.decode();
101
- if (i += a.byteLength, i > y) {
181
+ if (i += a.byteLength, i > j) {
102
182
  await t.cancel();
103
183
  return;
104
184
  }
105
185
  r += n.decode(a, { stream: !0 });
106
186
  }
107
- }, E = (e) => typeof e == "object" && !!e && !Array.isArray(e), D = (e) => typeof e == "string" && e.length > 0, O = (e) => typeof e == "string" && URL.canParse(e) && ["http:", "https:"].includes(new URL(e).protocol), k = (e) => {
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) => {
108
188
  let t;
109
189
  try {
110
190
  t = JSON.parse(e);
111
191
  } catch {
112
192
  return;
113
193
  }
114
- if (!E(t) || t.v !== 1) return;
194
+ if (!R(t) || t.v !== 1) return;
115
195
  let { eventName: n, eventId: r, customData: i, eventSourceUrl: a, consent: o, userData: s } = t;
116
- if (D(n) && D(r) && E(i) && O(a) && E(o) && (o.adUserData === "granted" || o.adUserData === "denied") && E(s) && !x.some((e) => s[e] !== void 0 && typeof s[e] != "string")) return {
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 {
117
197
  v: 1,
118
198
  eventName: n,
119
199
  eventId: r,
120
200
  customData: i,
121
201
  eventSourceUrl: a,
122
202
  consent: { adUserData: o.adUserData },
123
- userData: Object.fromEntries(x.filter((e) => s[e] !== void 0).map((e) => [e, s[e]]))
203
+ userData: Object.fromEntries(N.filter((e) => s[e] !== void 0).map((e) => [e, s[e]]))
124
204
  };
125
- }, A = (e) => {
126
- C(e);
205
+ }, H = (e) => {
206
+ F(e);
127
207
  let { meta: t, onError: n = console.error } = e, r = new Set(e.allowedOrigins);
128
208
  return async (e) => {
129
209
  if (e.method !== "POST") return new Response("Use POST", {
@@ -131,14 +211,14 @@ var a = async (e) => {
131
211
  headers: { allow: "POST" }
132
212
  });
133
213
  let i = e.headers.get("origin");
134
- if (!i || !r.has(i)) return w(403, "Origin not allowed");
135
- let a = await T(e);
136
- if (a === void 0) return w(413, "Body too large");
137
- let o = k(a);
138
- if (!o) return w(400, "Invalid event");
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");
139
219
  let s = e.headers.get("user-agent");
140
- if (!s) return w(400, "Missing User-Agent");
141
- if (o.consent.adUserData !== "granted") return w(204);
220
+ if (!s) return I(400, "Missing User-Agent");
221
+ if (o.consent.adUserData !== "granted") return I(204);
142
222
  try {
143
223
  let r = await v({
144
224
  ...t,
@@ -154,76 +234,19 @@ var a = async (e) => {
154
234
  customData: o.customData
155
235
  }]
156
236
  });
157
- if (r.ok) return w(204);
237
+ if (r.ok) return I(204);
158
238
  n(Error(`[react-marketing-tools] createTrackHandler: Meta rejected "${o.eventName}" (HTTP ${r.status})`, { cause: r.body }));
159
239
  } catch (e) {
160
240
  n(Error(`[react-marketing-tools] createTrackHandler: could not send "${o.eventName}" to Meta`, { cause: e }));
161
241
  }
162
- return w(502, "Meta did not accept the event");
242
+ return I(502, "Meta did not accept the event");
163
243
  };
164
- }, j = ({ cookieHeader: e, measurementId: t }) => {
244
+ }, U = ({ cookieHeader: e, measurementId: t }) => {
165
245
  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];
166
246
  return {
167
247
  ...n && { clientId: n },
168
248
  ...a && { sessionId: a }
169
249
  };
170
- }, M = {
171
- global: "https://www.google-analytics.com",
172
- eu: "https://region1.google-analytics.com"
173
- }, N = /^G-[A-Z0-9]+$/, P = 25, F = (e) => /* @__PURE__ */ TypeError(`[react-marketing-tools] sendMeasurementProtocolEvent: ${e}`), I = ({ measurementId: e, apiSecret: t, clientId: i, userId: a, events: o }) => {
174
- if (!N.test(e)) throw F(`measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)})`);
175
- if (!t) throw F("apiSecret is required");
176
- if (!i) throw F("clientId is required; read it from the _ga cookie with readGa4Cookies()");
177
- if (a !== void 0 && (!a || n(a))) throw F("userId must be your own identifier, not personal data such as an email address");
178
- if (o.length === 0 || o.length > P) throw F(`send between 1 and ${P} events per request (received ${o.length})`);
179
- for (let { name: e } of o) {
180
- let t = r(e);
181
- if (t) throw F(t);
182
- }
183
- }, L = ({ event: { name: n, params: r = {} }, sessionId: i, warnings: a }) => {
184
- for (let e of t(r)) a.push(`event "${n}" ${e}`);
185
- let { params: o, redactedKeys: s } = e(r);
186
- return s.length > 0 && a.push(`event "${n}": personal data redacted from ${s.join(", ")}`), {
187
- name: n,
188
- params: {
189
- ...i && { session_id: i },
190
- engagement_time_msec: 1,
191
- ...o
192
- }
193
- };
194
- }, R = ({ adUserData: e, adPersonalization: t }) => ({
195
- ...e && { ad_user_data: e.toUpperCase() },
196
- ...t && { ad_personalization: t.toUpperCase() }
197
- }), z = async (e) => {
198
- I(e);
199
- let { measurementId: t, apiSecret: n, clientId: r, sessionId: i, userId: a, events: o, consent: s, validate: c = !1, region: l = "global" } = e, u = [], d = {
200
- client_id: r,
201
- ...a && { user_id: a },
202
- ...s && { consent: R(s) },
203
- events: o.map((e) => L({
204
- event: e,
205
- sessionId: i,
206
- warnings: u
207
- }))
208
- }, f = new URL(c ? "/debug/mp/collect" : "/mp/collect", M[l]);
209
- f.searchParams.set("measurement_id", t), f.searchParams.set("api_secret", n);
210
- let p = await fetch(f, {
211
- method: "POST",
212
- headers: { "content-type": "application/json" },
213
- body: JSON.stringify(d)
214
- });
215
- if (!c) return {
216
- ok: p.ok,
217
- status: p.status,
218
- warnings: u
219
- };
220
- let { validationMessages: m = [] } = await p.json().catch(() => ({}));
221
- return {
222
- ok: p.ok && m.length === 0,
223
- status: p.status,
224
- warnings: u,
225
- validationMessages: m
226
- };
227
250
  };
228
251
  //#endregion
229
- export { f as META_GRAPH_API_VERSION, A as createTrackHandler, j as readGa4Cookies, v as sendConversionsApiEvent, z as sendMeasurementProtocolEvent };
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 };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "react-marketing-tools",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.3",
4
+ "version": "1.0.1",
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/)",
8
- "homepage": "https://www.heyrory.com/",
8
+ "homepage": "https://bronz3beard.github.io/react-marketing-tools/",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/bronz3beard/react-marketing-tools.git"
@@ -32,7 +32,14 @@
32
32
  "gdpr",
33
33
  "utm",
34
34
  "attribution",
35
- "server-side-tagging"
35
+ "server-side-tagging",
36
+ "conversions-api",
37
+ "measurement-protocol",
38
+ "web-vitals",
39
+ "consent",
40
+ "privacy",
41
+ "nextjs",
42
+ "typescript"
36
43
  ],
37
44
  "main": "./dist/index.js",
38
45
  "types": "./dist/index.d.ts",
@@ -69,6 +76,7 @@
69
76
  "scripts": {
70
77
  "build": "vite build && tsc -p tsconfig.build.json",
71
78
  "build:watch": "vite build --watch",
79
+ "check:api-summary": "node scripts/check-api-summary.mjs",
72
80
  "check:links": "node scripts/check-links.mjs",
73
81
  "check:package": "publint --strict && attw --pack . --profile esm-only",
74
82
  "check:size": "node scripts/check-size.mjs",
@@ -79,6 +87,7 @@
79
87
  "lint": "eslint .",
80
88
  "mock:publish": "npm pack --dry-run",
81
89
  "prepublishOnly": "npm run build && npm run check:size",
90
+ "release": "node scripts/release.mjs",
82
91
  "test": "vitest run",
83
92
  "test:watch": "vitest",
84
93
  "typecheck": "tsc --noEmit"
@@ -107,6 +116,7 @@
107
116
  "eslint": "^10.11.0",
108
117
  "eslint-config-prettier": "^10.1.8",
109
118
  "eslint-plugin-react-hooks": "^7.1.1",
119
+ "fast-check": "^4.10.2",
110
120
  "jsdom": "^30.1.0",
111
121
  "prettier": "^3.9.8",
112
122
  "publint": "^0.3.24",