react-marketing-tools 1.0.0-alpha.5 → 1.0.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,87 +1,401 @@
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
4
  [![license](https://img.shields.io/npm/l/react-marketing-tools)](./LICENSE)
5
5
 
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.
6
+ Send one event from your React app and it reaches Google Tag Manager, Google Analytics 4 and the Meta Pixel at once.
7
+ The library keeps the visitor's privacy choices, remembers which campaign brought them, strips personal data out of
8
+ events, and can send the same events again from your server so they still arrive when a browser blocks tracking.
8
9
 
9
- > **1.0 is in alpha** on the `next` tag. `npm install react-marketing-tools` still installs 0.4.x, whose API 1.0
10
- > replaces; see the [changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md).
10
+ **[Try it in the playground](https://bronz3beard.github.io/react-marketing-tools/)**: press a button and see exactly
11
+ what each of those services would receive. Nothing is sent anywhere.
12
+
13
+ > **Setting this up with an AI assistant?** There's a prompt written for that:
14
+ > [Set this up with an AI assistant](#set-this-up-with-an-ai-assistant). It interviews you, writes the wiring, and
15
+ > tells you what to click in each service.
16
+
17
+ > **Coming from 0.4?** 1.0 is a rewrite, and the API is different. The
18
+ > [migration guide](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md) maps the old
19
+ > names to the new ones, and the
20
+ > [changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md) lists everything that
21
+ > changed.
22
+
23
+ ## Contents
24
+
25
+ - [What you need before you start](#what-you-need-before-you-start)
26
+ - [Install](#install)
27
+ - [Set this up with an AI assistant](#set-this-up-with-an-ai-assistant)
28
+ - [Quick start](#quick-start)
29
+ - [1. Create the analytics instance](#1-create-the-analytics-instance)
30
+ - [2. Hand it to your app](#2-hand-it-to-your-app)
31
+ - [3. Track an event](#3-track-an-event)
32
+ - [Track clicks without writing code](#track-clicks-without-writing-code)
33
+ - [Follow a multi-step flow](#follow-a-multi-step-flow)
34
+ - [Tell the library who the visitor is](#tell-the-library-who-the-visitor-is)
35
+ - [Record what the visitor consented to](#record-what-the-visitor-consented-to)
36
+ - [Recognise a returning visitor](#recognise-a-returning-visitor)
37
+ - [Report how fast your pages are](#report-how-fast-your-pages-are)
38
+ - [Send events from your server](#send-events-from-your-server)
39
+ - [A purchase confirmed by a payment webhook](#a-purchase-confirmed-by-a-payment-webhook)
40
+ - [Send the Pixel's events from your server too](#send-the-pixels-events-from-your-server-too)
41
+ - [Use it without React](#use-it-without-react)
42
+ - [What each service receives](#what-each-service-receives)
43
+ - [Documentation](#documentation)
44
+ - [License](#license)
45
+
46
+ ## What you need before you start
47
+
48
+ This library sends events to accounts you already have. It doesn't create or configure them for you, and it isn't a
49
+ replacement for them. Before you install it, set up the ones you want to use:
50
+
51
+ | You want | What you need first | What the library needs from it |
52
+ | --- | --- | --- |
53
+ | Google Tag Manager | A container, with the tags and triggers that decide what happens to each event | The container ID, like `GTM-XXXXXXX` |
54
+ | Google Analytics 4 | A property with a web data stream | The measurement ID, like `G-XXXXXXX` |
55
+ | Meta Pixel | A dataset (pixel) in Meta Events Manager | The pixel ID, a long number |
56
+ | Events sent from your server to GA4 | An API secret on that same data stream | The secret, kept on your server |
57
+ | Events sent from your server to Meta | A Conversions API access token in Events Manager | The token, kept on your server |
58
+
59
+ Two things worth knowing:
60
+
61
+ - **With Tag Manager, your container still decides what happens.** The library puts each event into the dataLayer, the
62
+ list of events Tag Manager watches. Until you add a trigger and a tag for an event name, the event is recorded but
63
+ goes nowhere.
64
+ - **With Google Analytics 4, events arrive on their own.** They show up as events with the name you tracked. Using the
65
+ names Google recommends, such as `purchase` or `sign_up`, fills in GA4's built-in reports; your own names appear in
66
+ reports and explorations once you use them.
67
+
68
+ You only configure the services you use. Many teams start with Tag Manager alone and add the rest later.
11
69
 
12
70
  ## Install
13
71
 
14
72
  ```sh
15
- npm install react-marketing-tools@next
73
+ npm install react-marketing-tools
16
74
  ```
17
75
 
18
- Requires React 18 or 19. The package is ESM-only; server rendering needs Node.js 22.12 or later.
76
+ Works with React 18 and 19. The package is published as ES modules, the `import` style of JavaScript, so it works in
77
+ every current bundler and in Node.js 22.12 or later; it can't be loaded with `require()`.
78
+
79
+ ## Set this up with an AI assistant
80
+
81
+ There's a prompt you can paste into any AI coding assistant. It reads your project, asks what you want to measure and
82
+ which analytics accounts you have, then writes the setup, a checklist of what to click in Google Tag Manager, Google
83
+ Analytics and Meta, and the steps to check it all worked.
84
+
85
+ **[Get the prompt](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/agent-setup.md)**, paste it into
86
+ your assistant, and answer its questions. That page also lists the six things to check before you trust what it wrote,
87
+ and an `AGENTS.md` block to keep in your repo so later sessions don't re-guess the conventions this one chose.
88
+
89
+ The prompt is tested: three assistants set up a Next.js app and a Vite app from scratch, and what they got wrong is
90
+ what those rules are there to prevent.
19
91
 
20
- ## Usage
92
+ Working by hand is just as quick for a simple setup: the [quick start](#quick-start) below, or the
93
+ [integration walkthrough](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/integration-walkthrough.md)
94
+ for an app that already has analytics in it.
21
95
 
22
- Create one instance:
96
+ ## Quick start
97
+
98
+ Adding this to an app that already exists, with analytics already in it? Follow the
99
+ [integration walkthrough](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/integration-walkthrough.md)
100
+ instead: same steps, plus how to prove events arrive and how to retire the library you're replacing.
101
+
102
+ ### 1. Create the analytics instance
23
103
 
24
104
  ```ts
25
105
  // analytics.ts
26
106
  import { createAnalytics } from 'react-marketing-tools'
27
107
 
28
108
  export const analytics = createAnalytics({
29
- consent: 'denied', // until your consent banner records the visitor's choice
109
+ // Nothing is stored or sent until you record the visitor's choice; see "Record what the visitor consented to".
110
+ consent: 'denied',
30
111
  gtm: { containerId: 'GTM-XXXXXXX' },
31
112
  ga4: { measurementId: 'G-XXXXXXX' },
32
113
  metaPixel: { pixelId: '1234567890123456' },
33
114
  })
34
115
  ```
35
116
 
36
- Provide it to your app:
117
+ Creating the instance does nothing on its own: no scripts load and no cookies are written until your app starts it,
118
+ which the provider below does. That makes this file safe to import anywhere, including in server-rendered pages.
119
+
120
+ ### 2. Hand it to your app
37
121
 
38
122
  ```tsx
39
123
  // main.tsx
124
+ import { StrictMode } from 'react'
125
+ import { createRoot } from 'react-dom/client'
40
126
  import { AnalyticsProvider } from 'react-marketing-tools'
41
127
  import { analytics } from './analytics'
128
+ import { App } from './App'
42
129
 
43
130
  createRoot(document.getElementById('root')!).render(
44
- <AnalyticsProvider analytics={analytics}>
45
- <App />
46
- </AnalyticsProvider>,
131
+ <StrictMode>
132
+ <AnalyticsProvider analytics={analytics}>
133
+ <App />
134
+ </AnalyticsProvider>
135
+ </StrictMode>,
47
136
  )
48
137
  ```
49
138
 
50
- Track from any component:
139
+ The provider loads the vendor scripts once your app is running in the browser. Anything you track before that is held
140
+ and sent in order afterwards, so you never lose an event that happened during startup.
141
+
142
+ ### 3. Track an event
51
143
 
52
144
  ```tsx
145
+ // SignUpButton.tsx
53
146
  import { useAnalytics } from 'react-marketing-tools'
54
147
 
55
148
  export const SignUpButton = () => {
56
149
  const { track } = useAnalytics()
57
150
 
58
- // Reaches GTM, GA4 and Meta (as CompleteRegistration) with one shared event_id
59
- return <button onClick={() => track('sign_up', { method: 'google' })}>Sign up</button>
151
+ return (
152
+ <button onClick={() => track('sign_up', { method: 'google' })}>
153
+ Sign up
154
+ </button>
155
+ )
60
156
  }
61
157
  ```
62
158
 
63
- The same instance identifies users and records consent:
159
+ That one call reaches all three services: Tag Manager gets a `sign_up` event, Google Analytics 4 gets `sign_up`, and
160
+ the Meta Pixel gets `CompleteRegistration`, the name Meta uses for the same thing. All three carry the same event ID,
161
+ so when the same action arrives twice, from the browser and from your server, Meta counts it once.
162
+
163
+ ## Track clicks without writing code
164
+
165
+ Turn it on once, and then mark the elements you care about. No analytics code in your components:
166
+
167
+ ```ts
168
+ // analytics.ts
169
+ import { createAnalytics } from 'react-marketing-tools'
170
+
171
+ export const analytics = createAnalytics({
172
+ consent: 'denied',
173
+ gtm: { containerId: 'GTM-XXXXXXX' },
174
+ autocapture: { clicks: true },
175
+ })
176
+ ```
177
+
178
+ ```tsx
179
+ // PricingPage.tsx
180
+ export const PricingPage = () => (
181
+ <section>
182
+ <h1>Pricing</h1>
183
+ <button
184
+ data-analytics-event="cta_click"
185
+ data-analytics-param-location="pricing_header"
186
+ data-analytics-param-plan="pro"
187
+ >
188
+ Start free trial
189
+ </button>
190
+ </section>
191
+ )
192
+ ```
193
+
194
+ A click anywhere inside that button, including on an icon or text inside it, sends the same event as
195
+ `track('cta_click', { location: 'pricing_header', plan: 'pro' })`.
196
+
197
+ - `data-analytics-event` is the event name. Every `data-analytics-param-*` attribute becomes one detail on the event,
198
+ with dashes turned into underscores: `data-analytics-param-button-text` arrives as `button_text`.
199
+ - Values are text. For numbers, such as a price you want to add up, call `track()` instead.
200
+ - It works for elements added later, for example after a route change, and for links as well as buttons.
201
+ - If several marked elements are nested, the closest one to the click wins.
202
+
203
+ ## Follow a multi-step flow
204
+
205
+ A journey groups the steps of a flow such as a checkout, so you can see where people drop out:
206
+
207
+ ```tsx
208
+ // Checkout.tsx
209
+ import { useState } from 'react'
210
+ import { useAnalytics } from 'react-marketing-tools'
211
+
212
+ export const Checkout = () => {
213
+ const { journey } = useAnalytics()
214
+ // Create it once for this flow, not on every render: each journey has its own ID.
215
+ const [checkout] = useState(() => journey('checkout'))
216
+
217
+ return (
218
+ <>
219
+ <button onClick={() => checkout.step('shipping')}>Continue to payment</button>
220
+ <button onClick={() => checkout.complete({ value: 42, currency: 'USD' })}>Pay</button>
221
+ <button onClick={() => checkout.abandon('changed_mind')}>Cancel</button>
222
+ </>
223
+ )
224
+ }
225
+ ```
226
+
227
+ The first call also sends a `journey_start` event, and every event in the flow carries the same journey ID. In Google
228
+ Analytics 4 you can then build a funnel from `journey_start`, `journey_step` and `journey_complete`.
229
+
230
+ ## Tell the library who the visitor is
231
+
232
+ ```ts
233
+ // after your sign-in code succeeds
234
+ import { analytics } from './analytics'
235
+
236
+ analytics.identify('user-42', { email: 'ada@example.com' })
237
+ ```
238
+
239
+ The ID is your own user ID: it goes to Tag Manager and Google Analytics 4 so you can join sessions to accounts. The
240
+ email address is not sent to Google. It goes only to Meta, which uses contact details to match a visitor to a Facebook
241
+ or Instagram account, and it's scrambled into an unreadable fingerprint (a hash) before it's sent. Call
242
+ `analytics.reset()` when someone signs out.
243
+
244
+ ## Record what the visitor consented to
245
+
246
+ ```ts
247
+ // from your cookie banner, when the visitor answers
248
+ import { analytics } from './analytics'
249
+
250
+ analytics.consent.update({ analytics: 'granted', ads: 'granted' })
251
+ ```
252
+
253
+ Until this is called, the instance follows the `consent` value you passed when you created it. The library passes the
254
+ choice to Google's Consent Mode, which is how Google's tags are told what a visitor agreed to, and to the Meta Pixel,
255
+ which holds events until it's allowed to send them. The same choice controls what the library itself stores in the
256
+ browser. Both parts of the choice can be set separately if your banner asks separately.
257
+
258
+ ## Recognise a returning visitor
259
+
260
+ ```ts
261
+ import { analytics } from './analytics'
262
+
263
+ // inside an async function, any time after your app has started
264
+ const visitorId = await analytics.getVisitorId()
265
+ ```
266
+
267
+ This is a random ID kept in the browser, given only to visitors who consented to analytics, so you can join a person's
268
+ visits in your own systems. It's never sent to Google Analytics. You can swap it for a browser fingerprint if you want
269
+ to recognise people who clear their cookies; see [Visitor ID](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/visitor-id.md).
270
+
271
+ ## Report how fast your pages are
272
+
273
+ Core Web Vitals are Google's three measures of page experience: how fast the main content appears, how quickly the page
274
+ responds to a tap or click, and how much the layout jumps around. Install Google's measuring library and pass it your
275
+ instance:
276
+
277
+ ```sh
278
+ npm install web-vitals@^6
279
+ ```
280
+
281
+ ```ts
282
+ // main.tsx, after the render call
283
+ import { trackWebVitals } from 'react-marketing-tools/web-vitals'
284
+ import { analytics } from './analytics'
285
+
286
+ void trackWebVitals(analytics)
287
+ ```
288
+
289
+ Each measurement arrives as an event named `LCP`, `INP` or `CLS` in Google Analytics 4 and Tag Manager. They're never
290
+ sent to Meta.
291
+
292
+ ## Send events from your server
293
+
294
+ Some things happen where the browser can't see them, such as a payment your payment provider confirms minutes later.
295
+ Other events simply never make it, because an extension or browser setting blocks the tracking scripts. For both, send
296
+ the event from your server.
297
+
298
+ ### A purchase confirmed by a payment webhook
64
299
 
65
300
  ```ts
66
- analytics.identify('user-42', { email: 'ada@example.com' }) // user id for GTM and GA4, advanced matching for Meta
67
- analytics.consent.update({ analytics: 'granted', ads: 'granted' }) // Google Consent Mode v2 and Meta consent
301
+ // inside your payment webhook, where `order` is the order you just confirmed
302
+ import { sendMeasurementProtocolEvent } from 'react-marketing-tools/server'
303
+
304
+ await sendMeasurementProtocolEvent({
305
+ measurementId: 'G-XXXXXXX',
306
+ apiSecret: process.env.GA4_API_SECRET!,
307
+ // Saved when the visitor started checkout, with readGa4Cookies() from the same import.
308
+ clientId: order.ga4ClientId,
309
+ events: [
310
+ {
311
+ name: 'purchase',
312
+ params: { transaction_id: order.id, value: order.total, currency: 'USD' },
313
+ },
314
+ ],
315
+ })
68
316
  ```
69
317
 
70
- Configure only the destinations you use. Without React, import `createAnalytics` from `react-marketing-tools/core` and
71
- call `analytics.start()` yourself.
318
+ The same import has `sendConversionsApiEvent` for Meta, which scrambles customer details into hashes the way Meta
319
+ requires before sending them.
320
+
321
+ ### Send the Pixel's events from your server too
322
+
323
+ Point the page at an address on your own site, and mount the handler there. Every event the Pixel receives is then sent
324
+ from your server as well, and Meta counts each one once:
325
+
326
+ ```ts
327
+ // analytics.ts
328
+ import { createAnalytics } from 'react-marketing-tools'
329
+
330
+ export const analytics = createAnalytics({
331
+ consent: 'denied',
332
+ metaPixel: { pixelId: '1234567890123456' },
333
+ server: { endpoint: '/api/track' },
334
+ })
335
+ ```
336
+
337
+ ```ts
338
+ // app/api/track/route.ts — a Next.js route; any server that speaks Request and Response works
339
+ import { createTrackHandler } from 'react-marketing-tools/server'
340
+
341
+ export const POST = createTrackHandler({
342
+ allowedOrigins: ['https://shop.example.com'],
343
+ meta: {
344
+ pixelId: '1234567890123456',
345
+ accessToken: process.env.META_CAPI_TOKEN!,
346
+ },
347
+ })
348
+ ```
349
+
350
+ ## Use it without React
351
+
352
+ ```ts
353
+ // analytics.ts
354
+ import { createAnalytics } from 'react-marketing-tools/core'
355
+
356
+ export const analytics = createAnalytics({
357
+ consent: 'granted',
358
+ gtm: { containerId: 'GTM-XXXXXXX' },
359
+ })
360
+
361
+ analytics.start() // the React provider does this for you
362
+ analytics.track('sign_up', { method: 'google' })
363
+ ```
364
+
365
+ ## What each service receives
366
+
367
+ | | Google Tag Manager | Google Analytics 4 | Meta Pixel | Your server, for Meta |
368
+ | --- | --- | --- | --- | --- |
369
+ | `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 |
370
+ | `identify()` | your user ID | your user ID | contact details, hashed by the Pixel | contact details, hashed by your server |
371
+ | `consent.update()` | Google Consent Mode | Google Consent Mode | permission to send, or to hold | only sends with permission to share data with ad platforms |
372
+ | 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 |
72
373
 
73
374
  ## Documentation
74
375
 
376
+ - [All docs](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/README.md)
75
377
  - [Getting started](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/getting-started.md)
76
- - [React](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/react.md): provider, hook, Next.js App Router, single-page apps
77
- - [Tracking events](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/tracking-events.md): naming rules, page views, users, personal data, errors
378
+ - [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
379
+ - [React](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/react.md): provider, hook, single-page apps
380
+ - [Next.js](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/nextjs.md): a complete App Router setup, client and server
381
+ - [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
78
382
  - [Configuration](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/configuration.md)
79
383
  - [Consent](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/consent.md): Consent Mode v2 and Global Privacy Control
80
384
  - [Attribution](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/attribution-utm.md): UTM params and ad click IDs
385
+ - [Visitor ID](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/visitor-id.md): a stable ID for consenting visitors, random or fingerprint
81
386
  - [Google Tag Manager](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-tag-manager.md)
82
387
  - [Google Analytics 4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-analytics-4.md)
83
388
  - [Meta Pixel](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-pixel.md)
389
+ - [Other tools](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/custom-destinations.md): PostHog, Umami, Plausible, your own endpoint
390
+ - [Measuring AI activity](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/ai-traffic.md): AI features, visits from AI assistants, AI crawlers
84
391
  - [Server-side tagging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/server-side-tagging.md)
392
+ - [GA4 Measurement Protocol](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/measurement-protocol.md): GA4 events from your server
393
+ - [Meta Conversions API](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-conversions-api.md): Meta events from your server
394
+ - [Error tracking](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/error-tracking.md): report errors to Google Analytics 4
395
+ - [Debugging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/debugging.md): see what's sent, and fix common problems
396
+ - [Migrating from 0.4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md)
397
+ - [API summary](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/api-summary.md): every option, method and limit on one page
398
+ - [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
85
399
  - [Changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md)
86
400
 
87
401
  ## 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;
@@ -0,0 +1,11 @@
1
+ import type { EventParams } from '../core/types.js';
2
+ /**
3
+ * The event a click asks for: the nearest element around the click target with `data-analytics-event`, and its
4
+ * `data-analytics-param-*` attributes as params (`data-analytics-param-button-text` → `button_text`).
5
+ */
6
+ export declare const readClickEvent: (target: EventTarget | null) => {
7
+ name: string;
8
+ params: EventParams;
9
+ } | undefined;
10
+ /** Tracks clicks on marked elements through one listener on the document, so elements added later are covered too. */
11
+ export declare const captureClicks: (track: (name: string, params: EventParams) => void) => void;
@@ -0,0 +1,49 @@
1
+ //#region lib/core/validate.ts
2
+ var e = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, t = [
3
+ "google_",
4
+ "ga_",
5
+ "firebase_"
6
+ ], n = 25, r = 100, i = {
7
+ page_location: 1e3,
8
+ page_referrer: 420,
9
+ page_title: 300
10
+ }, a = [
11
+ "email",
12
+ "phone",
13
+ "first_name",
14
+ "last_name",
15
+ "address",
16
+ "password"
17
+ ], o = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, s = "[redacted]", c = (n) => {
18
+ if (!e.test(n)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
19
+ let r = t.find((e) => n.toLowerCase().startsWith(e));
20
+ return r && `must not start with the reserved prefix "${r}"`;
21
+ }, l = (e) => {
22
+ let t = c(e);
23
+ return t && `event name "${e}" ${t}`;
24
+ }, u = (e) => {
25
+ let t = Object.keys(e), a = t.flatMap((t) => {
26
+ let n = c(t);
27
+ if (n) return [`param "${t}" ${n}`];
28
+ let a = e[t], o = i[t] ?? r;
29
+ return typeof a == "string" && a.length > o ? [`param "${t}" is longer than ${o} characters`] : [];
30
+ });
31
+ return t.length > n ? [`has ${t.length} params; the limit is ${n}`, ...a] : a;
32
+ }, d = (e) => new RegExp(o, "i").test(e), f = (e, t) => (typeof t == "string" || typeof t == "number") && a.some((t) => e.toLowerCase().includes(t)) ? s : typeof t == "string" ? t.replace(new RegExp(o, "gi"), s) : t, p = (e) => {
33
+ let t = Object.entries(e).map(([e, t]) => [
34
+ e,
35
+ t,
36
+ f(e, t)
37
+ ]);
38
+ return {
39
+ params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
40
+ redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
41
+ };
42
+ }, m = (e, t) => {
43
+ for (let n of e.split(";")) {
44
+ let e = n.indexOf("=");
45
+ if (e !== -1 && n.slice(0, e).trim() === t) return n.slice(e + 1).trim();
46
+ }
47
+ };
48
+ //#endregion
49
+ export { p as a, u as i, d as n, l as r, m as t };