react-marketing-tools 1.0.0-alpha.3 → 1.0.0-alpha.5

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,226 +1,89 @@
1
+ # React Marketing Tools
1
2
 
2
- # React Marketing Tools   [![npm version](https://badge.fury.io/js/react-marketing-tools.svg)](https://badge.fury.io/js/react-marketing-tools)
3
-
4
- React Marketing Tools are a set of tools to make it easier for you to implement analytics and track user journeys, interactions throughout your App. using dataLayer/Google Tag Manager, GA4 fetch directly or coming soon facebook pixel.
5
-
6
- * [React Marketing Tools Demo](https://codepen.io/bronz3beard/pen/yLZmMeg)
7
- * [Detailed Blog post on React Marketing Tools Implementation](https://blog.heyrory.com/google-analytics-4-google-tag-manager)
8
-
9
-
10
- # PR's
11
- - Have a look at the [PR template doc](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs) for best approach to getting your pr merged.
12
-
13
- # CHANGELOG
14
- - You can view it [here](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md)
15
-
16
- # Usage and setup examples.
17
-
18
- ### Setup configuration
19
- - Setting up config without provider is also an option, in this case you will only need to import _buildConfig_
20
- ```js
21
- import React from 'react'
22
- import ReactDOM from 'react-dom/client'
23
- import {
24
- ReactMarketingProvider,
25
- buildConfig,
26
- BuildConfigOptions,
27
- Tokens,
28
- } from 'react-marketing-tools'
29
- import App from './App'
30
-
31
- /*
32
- TOKENS are optional
33
- const TOKENS: Tokens = {
34
-
35
- // if withServerLocationInfo is true you must supply this token.
36
- IP_INFO_TOKEN: 'SOME_TOKEN',
37
-
38
- // if analyticsType = analyticsPlatform.GOOGLE the below tokens must be supplied.
39
- GA4_PUBLIC_API_SECRET: 'SOME_TOKEN',
40
- GA4_PUBLIC_MEASUREMENT_ID: 'SOME_TOKEN',
41
- }
42
- */
43
-
44
- // These are the keys for the values you want to include from your user data
45
- // these must be included for any user data to be collected by analytics event if user data is hardcoded when passed in.
46
- const includeUserKeys = [
47
- 'firstName',
48
- 'lastName',
49
- ]
50
-
51
- const analyticsConfig: BuildConfigOptions = {
52
- appName: 'my-awesome-app', // required
53
- appSessionCookieName: 'APP_SESSION',
54
- eventActionPrefix: { // this will extend the default values of eventActionPrefix
55
- ACTION: 'ACTION',
56
- OTHER_EVENT_NAME_TYPE: 'OTHER_EVENT_NAME_TYPE'
57
- },
58
- globalEventActionList: { // this will extend the default values of globalEventActionList
59
- SIGN_IN: 'SIGN_IN',
60
- SIGN_UP: 'SIGN_UP',
61
- IMPORTANT_BUTTON_CLICKED: 'IMPORTANT_BUTTON_CLICKED'
62
- },
63
- // TOKENS // (optional),
64
- includeUserKeys,
65
- showMissingUserAttributesInConsole: false, // a boolean condition to show or hide "user" attributes that are not included in the "includeUserKeys" array, by console logging in dev tools.
66
- withDeviceInfo: true, // (optional) has default value
67
- withServerLocationInfo: false, // (optional) has default value
68
- }
69
-
70
- /**
71
- * @type {Object} buildConfig -> options: all attributes of the options object must have a value, other than withDeviceInfo.
72
- * @property {string} appName: the name of your app this value must be passed in.
73
- * @property {string} appSessionCookieName This is used to get the cookie from storage based on a key you use, the value from the cookie will be used in "client_id:"
74
- * @property {Object} eventActionPrefix: is a { key: 'value' } object that allows you to extend "analyticsEventActionPrefixList" object with custom eventActionPrefix. To see the build in list call the function showMeBuildInEventActionPrefixList().
75
- * @property {Object} globalEventActionList: is a { key: 'value' } object that allows you to extend "analyticsGlobalEventActionList" object with custom eventActionNames. To see the build in list call the function showMeBuildInGlobalEventActionList().
76
- * @property {Array} includeUserKeys: is an array of strings that represent keys from your user data that you want to whitelist, user data you wan to hash.
77
- * @property {Boolean} showMissingUserAttributesInConsole a boolean condition to show or hide "user" attributes that are not included in the "includeUserKeys" array, by console logging in dev tools.
78
- * @property {Object} TOKENS: is a { key: 'value' } object that includes the following keys, IP_INFO_TOKEN, GA4_PUBLIC_API_SECRET, GA4_PUBLIC_MEASUREMENT_ID, depending on if you need these features enabled.
79
- * @property {Boolean} withDeviceInfo: if you want device information added to "globalVars" set this to true false by default.
80
- * @property {Boolean} withServerLocationInfo: if you want server information added to "journeyProps" set this to true false by default.
81
- */
82
- buildConfig(analyticsConfig)
83
-
84
- ReactDOM.createRoot(document.getElementById('root')).render(
85
- <React.StrictMode>
86
- <ReactMarketingProvider>
87
- <App />
88
- </ReactMarketingProvider>
89
- </React.StrictMode>
90
- )
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)
4
+ [![license](https://img.shields.io/npm/l/react-marketing-tools)](./LICENSE)
91
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.
8
+
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).
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install react-marketing-tools@next
16
+ ```
17
+
18
+ Requires React 18 or 19. The package is ESM-only; server rendering needs Node.js 22.12 or later.
19
+
20
+ ## Usage
21
+
22
+ Create one instance:
23
+
24
+ ```ts
25
+ // analytics.ts
26
+ import { createAnalytics } from 'react-marketing-tools'
27
+
28
+ export const analytics = createAnalytics({
29
+ consent: 'denied', // until your consent banner records the visitor's choice
30
+ gtm: { containerId: 'GTM-XXXXXXX' },
31
+ ga4: { measurementId: 'G-XXXXXXX' },
32
+ metaPixel: { pixelId: '1234567890123456' },
33
+ })
34
+ ```
35
+
36
+ Provide it to your app:
37
+
38
+ ```tsx
39
+ // main.tsx
40
+ import { AnalyticsProvider } from 'react-marketing-tools'
41
+ import { analytics } from './analytics'
42
+
43
+ createRoot(document.getElementById('root')!).render(
44
+ <AnalyticsProvider analytics={analytics}>
45
+ <App />
46
+ </AnalyticsProvider>,
47
+ )
92
48
  ```
93
49
 
94
- ## Usage with Provider
95
- ```js
96
- import { useState, useEffect, useCallback, useRef } from 'react'
97
- import {
98
- useMarketingApi,
99
- useMarketingState,
100
- EventNameInfo,
101
- TrackAnalyticsEventOptions,
102
- ProviderApiProps,
103
- ProviderStateProps,
104
- } from 'react-marketing-tools'
105
-
106
- function App() {
107
- const [count, setCount] = useState(0)
108
- const {
109
- analyticsPlatform,
110
- appSessionCookieName,
111
- eventActionPrefixList,
112
- analyticsGlobalEventActionList,
113
- }: ProviderStateProps = useMarketingState()
114
- const {
115
- trackAnalyticsEvent
116
- /*
117
- NOTE: If you want to see built in config items and your added items, use one of the following functions to the body of your functional component or useEffect/function
118
- call it like so showMeBuildInAnalyticsPlatform() then check your console, in dev tools.
119
- showMeBuildInAnalyticsPlatform,
120
- showMeBuildInEventActionPrefixList,
121
- showMeBuildInGlobalEventActionList,
122
- */
123
- }: ProviderApiProps = useMarketingApi()
124
-
125
- useEffect(function appLoadPageLandingWelcome() {
126
- // create session cookie, useful for unauthenticated user tracking and other things
127
- document.cookie = `${appSessionCookieName}=${uuid()};max-age=${70};SameSite=Strict;Secure`
128
-
129
- const sendAnalyticsEvent = async () => {
130
- const eventNameInfo: EventNameInfo = {
131
- // If this is not passed in the event name will be a combination of actionPrefix & globalAppEvent
132
- // J_UNAUTHENTICATED
133
- eventName: 'Welcome Landing',
134
- actionPrefix: eventActionPrefixList.JOURNEY,
135
- globalAppEvent: analyticsGlobalEventActionList.UNAUTHENTICATED,
136
- }
137
-
138
- const trackingData: TrackAnalyticsEventOptions = {
139
- data: {},
140
- eventNameInfo,
141
- analyticsType: analyticsPlatform.DATALAYER_PUSH,
142
- dataLayerCheck: true,
143
- userDataKeysToHashArray: null,
144
- }
145
-
146
- await trackAnalyticsEvent(trackingData)
147
- }
148
-
149
- sendAnalyticsEvent()
150
- }, [])
151
-
152
- const handleButtonClick = useCallback(async () => {
153
- const countActual = count + 1
154
- setCount(countActual)
155
-
156
- const eventNameInfo: EventNameInfo = {
157
- // If this is not passed in the event name will be a combination of actionPrefix & globalAppEvent
158
- // I_AUTHENTICATED
159
- eventName: 'count button click',
160
- actionPrefix: eventActionPrefixList.INTERACTION,
161
- globalAppEvent: analyticsGlobalEventActionList.AUTHENTICATED,
162
- previousGlobalAppEvent: analyticsGlobalEventActionList.UNAUTHENTICATED,
163
- }
164
-
165
- const trackingData: TrackAnalyticsEventOptions = {
166
- data: {
167
- count: countActual,
168
- // firstName: 'bob',
169
- lastName: 'yeah nah',
170
- email: 'yeahnah@gmail.com',
171
- },
172
- eventNameInfo,
173
- analyticsType: analyticsPlatform.DATALAYER_PUSH,
174
- consoleLogData: {
175
- showJourneyPropsPayload: true,
176
- },
177
- dataLayerCheck: false,
178
- userDataKeysToHashArray: null,
179
- }
180
-
181
- await trackAnalyticsEvent(trackingData)
182
- }, [count])
183
-
184
- /* Example: use GA4 directly
185
- const handleButtonClick = useCallback(async () => {
186
- const countActual = count + 1
187
-
188
- setCount(countActual)
189
-
190
- const eventNameInfo: EventNameInfo = {
191
- eventName: 'count button click',
192
- actionPrefix: eventActionPrefixList.INTERACTION,
193
- globalAppEvent: analyticsGlobalEventActionList.AUTHENTICATED,
194
- previousGlobalAppEvent: analyticsGlobalEventActionList.UNAUTHENTICATED,
195
- }
196
-
197
- const trackingData: TrackAnalyticsEventOptions = {
198
- data: {
199
- count: countActual,
200
- firstName: 'bob',
201
- lastName: 'yeah nah',
202
- email: 'yeahnah@gmail.com',
203
- },
204
- eventNameInfo,
205
- analyticsType: analyticsPlatform.GOOGLE,
206
- userDataKeysToHashArray: ['email', 'firstName', 'lastName'],
207
- consoleLogData: {
208
- showJourneyPropsPayload: true,
209
- },
210
- }
211
-
212
- await trackAnalyticsEvent(trackingData)
213
- }, [count])
214
- */
215
-
216
- ...
217
-
218
- return (
219
- ...
220
- )
221
- }
50
+ Track from any component:
51
+
52
+ ```tsx
53
+ import { useAnalytics } from 'react-marketing-tools'
222
54
 
55
+ export const SignUpButton = () => {
56
+ const { track } = useAnalytics()
57
+
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>
60
+ }
223
61
  ```
224
62
 
225
- ## Usage without Provider
226
- > To view implementation and usage without the React Context/Provider visit this [codepen](https://codepen.io/roryfn/pen/OJBGvMG)
63
+ The same instance identifies users and records consent:
64
+
65
+ ```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
68
+ ```
69
+
70
+ Configure only the destinations you use. Without React, import `createAnalytics` from `react-marketing-tools/core` and
71
+ call `analytics.start()` yourself.
72
+
73
+ ## Documentation
74
+
75
+ - [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
78
+ - [Configuration](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/configuration.md)
79
+ - [Consent](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/consent.md): Consent Mode v2 and Global Privacy Control
80
+ - [Attribution](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/attribution-utm.md): UTM params and ad click IDs
81
+ - [Google Tag Manager](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-tag-manager.md)
82
+ - [Google Analytics 4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-analytics-4.md)
83
+ - [Meta Pixel](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-pixel.md)
84
+ - [Server-side tagging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/server-side-tagging.md)
85
+ - [Changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md)
86
+
87
+ ## License
88
+
89
+ [MIT](./LICENSE)
@@ -0,0 +1,21 @@
1
+ import type { Attribution, CampaignParam } from '../core/types.js';
2
+ export declare const CAMPAIGN_PARAMS: readonly CampaignParam[];
3
+ /**
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`).
6
+ */
7
+ export declare const parseAttribution: ({ url, referrer, capturedAt, }: {
8
+ url: string;
9
+ referrer?: string;
10
+ capturedAt: number;
11
+ }) => Attribution | undefined;
12
+ /** Stored attribution is untrusted input: anything that isn't a well-formed touch is discarded. */
13
+ export declare const toAttribution: (value: unknown) => Attribution | undefined;
14
+ /**
15
+ * Meta's click ID for the Conversions API: the Pixel's `_fbc` cookie as-is (Meta may append to it), otherwise built from
16
+ * the touch's `fbclid` in Meta's documented `fb.1.<creation time ms>.<fbclid>` format.
17
+ */
18
+ export declare const deriveFbc: ({ fbcCookie, touch, }: {
19
+ fbcCookie?: string;
20
+ touch?: Attribution;
21
+ }) => string | undefined;
@@ -0,0 +1,21 @@
1
+ import type { Attribution } from '../core/types.js';
2
+ /**
3
+ * First and last campaign touch. The caller decides when storage may be used: reading or writing device storage both
4
+ * need analytics consent (ePrivacy Directive Art. 5(3)).
5
+ */
6
+ export declare const createAttributionTracker: ({ ttlDays }: {
7
+ ttlDays: number;
8
+ }) => {
9
+ /** Records a touch in memory. */
10
+ observe(touch: Attribution | undefined): void;
11
+ /** Merges touches stored on earlier visits: the oldest unexpired first touch wins; this session's last touch wins. */
12
+ restore(): void;
13
+ persist(): void;
14
+ /** Erases stored touches, e.g. when analytics consent is withdrawn. In-memory touches are kept for this page. */
15
+ erase(): void;
16
+ get: () => {
17
+ firstTouch: Attribution | undefined;
18
+ lastTouch: Attribution | undefined;
19
+ };
20
+ };
21
+ export type AttributionTracker = ReturnType<typeof createAttributionTracker>;
@@ -1,10 +1,124 @@
1
- //#region lib/core/consent.ts
2
- var e = [
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
+ }, ee = (e) => {
22
+ let t = c(e);
23
+ return t && `event name "${e}" ${t}`;
24
+ }, l = (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
+ }, u = (e) => new RegExp(o, "i").test(e), d = (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, f = (e) => {
33
+ let t = Object.entries(e).map(([e, t]) => [
34
+ e,
35
+ t,
36
+ d(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
+ }, p = [
43
+ "utm_source",
44
+ "utm_medium",
45
+ "utm_campaign",
46
+ "utm_term",
47
+ "utm_content",
48
+ "utm_id",
49
+ "utm_source_platform",
50
+ "utm_creative_format",
51
+ "utm_marketing_tactic",
52
+ "gclid",
53
+ "gbraid",
54
+ "wbraid",
55
+ "dclid",
56
+ "fbclid",
57
+ "msclkid",
58
+ "ttclid",
59
+ "li_fat_id",
60
+ "twclid"
61
+ ], m = 100, h = (e) => {
62
+ if (!URL.canParse(e)) return;
63
+ let { origin: t, pathname: n } = new URL(e);
64
+ return t + n;
65
+ }, g = ({ url: e, referrer: t, capturedAt: n }) => {
66
+ if (!URL.canParse(e)) return;
67
+ let { searchParams: r } = new URL(e), i = p.flatMap((e) => {
68
+ let t = r.get(e)?.trim();
69
+ return t ? [[e, t]] : [];
70
+ });
71
+ if (i.length === 0) return;
72
+ let { params: a } = f(Object.fromEntries(i)), o = Object.fromEntries(Object.entries(a).map(([e, t]) => [e, String(t).slice(0, m)])), s = t ? h(t) : void 0;
73
+ return {
74
+ ...o,
75
+ landing_page: h(e) ?? e,
76
+ ...s ? { referrer: s } : {},
77
+ captured_at: n
78
+ };
79
+ }, _ = (e) => typeof e == "object" && !!e, v = (e) => {
80
+ if (_(e) && typeof e.captured_at == "number" && typeof e.landing_page == "string") return {
81
+ ...Object.fromEntries(p.flatMap((t) => typeof e[t] == "string" ? [[t, e[t]]] : [])),
82
+ landing_page: e.landing_page,
83
+ ...typeof e.referrer == "string" ? { referrer: e.referrer } : {},
84
+ captured_at: e.captured_at
85
+ };
86
+ }, te = ({ fbcCookie: e, touch: t }) => e ?? (t?.fbclid ? `fb.1.${t.captured_at}.${t.fbclid}` : void 0), y = "rmt:attribution:first", b = "rmt:attribution:last", x = 864e5, S = (e, t) => {
87
+ try {
88
+ return t(window[e]);
89
+ } catch {
90
+ return;
91
+ }
92
+ }, C = (e, t) => S(e, (e) => {
93
+ let n = e.getItem(t);
94
+ return n === null ? void 0 : v(JSON.parse(n));
95
+ }), ne = ({ ttlDays: e }) => {
96
+ let t, n, r = (t) => Date.now() - t.captured_at < e * x;
97
+ return {
98
+ observe(e) {
99
+ e && (t ??= e, n = e);
100
+ },
101
+ restore() {
102
+ let e = C("localStorage", y);
103
+ e && r(e) && (!t || e.captured_at <= t.captured_at) && (t = e), n ??= C("sessionStorage", b);
104
+ },
105
+ persist() {
106
+ S("localStorage", (e) => t && r(t) ? e.setItem(y, JSON.stringify(t)) : e.removeItem(y)), n && S("sessionStorage", (e) => e.setItem(b, JSON.stringify(n)));
107
+ },
108
+ erase() {
109
+ S("localStorage", (e) => e.removeItem(y)), S("sessionStorage", (e) => e.removeItem(b));
110
+ },
111
+ get: () => ({
112
+ firstTouch: t,
113
+ lastTouch: n
114
+ })
115
+ };
116
+ }, w = [
3
117
  "analytics",
4
118
  "ads",
5
119
  "adUserData",
6
120
  "adPersonalization"
7
- ], t = (e) => e === "granted" || e === "denied", n = ({ consent: e, gpc: t }) => {
121
+ ], T = (e) => e === "granted" || e === "denied", E = ({ consent: e, gpc: t }) => {
8
122
  let n = t ? "denied" : e;
9
123
  return {
10
124
  analytics: e,
@@ -12,35 +126,35 @@ var e = [
12
126
  adUserData: n,
13
127
  adPersonalization: n
14
128
  };
15
- }, r = (e, t) => ({
129
+ }, D = (e, t) => ({
16
130
  analytics: t.analytics ?? e.analytics,
17
131
  ads: t.ads ?? e.ads,
18
132
  adUserData: t.adUserData ?? t.ads ?? e.adUserData,
19
133
  adPersonalization: t.adPersonalization ?? t.ads ?? e.adPersonalization
20
- }), i = (e) => ({
134
+ }), O = (e) => ({
21
135
  analytics_storage: e.analytics,
22
136
  ad_storage: e.ads,
23
137
  ad_user_data: e.adUserData,
24
138
  ad_personalization: e.adPersonalization
25
- }), a = (e) => Object.values(e).includes("denied"), o = () => typeof navigator < "u" && navigator.globalPrivacyControl === !0, s = () => window.dataLayer ??= [], c = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/gtag/js")), l = () => window.gtag ??= function() {
26
- s().push(arguments);
27
- }, u = ({ consent: e, waitForUpdate: t }) => {
28
- l()("consent", "default", {
29
- ...i(e),
30
- ...a(e) ? { wait_for_update: t } : {}
139
+ }), k = (e) => Object.values(e).includes("denied"), re = () => typeof navigator < "u" && navigator.globalPrivacyControl === !0, A = () => window.dataLayer ??= [], ie = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/gtag/js")), j = () => window.gtag ??= function() {
140
+ A().push(arguments);
141
+ }, M = ({ consent: e, waitForUpdate: t }) => {
142
+ j()("consent", "default", {
143
+ ...O(e),
144
+ ...k(e) ? { wait_for_update: t } : {}
31
145
  });
32
- }, d = (e) => {
33
- l()("consent", "update", i(e));
34
- }, f = (e, t) => {
146
+ }, N = (e) => {
147
+ j()("consent", "update", O(e));
148
+ }, P = (e, t) => {
35
149
  let n = URL.canParse(e) ? new URL(e) : void 0;
36
150
  if (n?.protocol !== "https:") throw Error(`[react-marketing-tools] ${t} must be an https:// URL (received ${JSON.stringify(e)}).`);
37
151
  return n;
38
- }, p = (e) => Array.from(document.scripts).some((t) => t.src === e), m = ({ src: e, nonce: t }) => {
152
+ }, F = (e) => Array.from(document.scripts).some((t) => t.src === e), I = ({ src: e, nonce: t }) => {
39
153
  let n = document.createElement("script");
40
154
  n.async = !0, n.src = e, t && n.setAttribute("nonce", t), document.head.append(n);
41
- }, h = /^G-[A-Z0-9]+$/, g = ({ measurementId: e, pageViews: t = "auto", loadScript: n = !0, serverContainerUrl: r, waitForUpdate: i = 500, nonce: a }) => {
42
- if (!h.test(e)) throw Error(`[react-marketing-tools] ga4.measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)}).`);
43
- r !== void 0 && f(r, "ga4.serverContainerUrl");
155
+ }, L = /^G-[A-Z0-9]+$/, R = ({ measurementId: e, pageViews: t = "auto", loadScript: n = !0, serverContainerUrl: r, waitForUpdate: i = 500, nonce: a }) => {
156
+ if (!L.test(e)) throw Error(`[react-marketing-tools] ga4.measurementId must look like "G-XXXXXXX" (received ${JSON.stringify(e)}).`);
157
+ r !== void 0 && P(r, "ga4.serverContainerUrl");
44
158
  let o = `https://www.googletagmanager.com/gtag/js?id=${e}`, s = (e, t) => r ? {
45
159
  ...e,
46
160
  event_id: t
@@ -48,211 +162,318 @@ var e = [
48
162
  return {
49
163
  name: "ga4",
50
164
  start({ consent: s }) {
51
- u({
165
+ M({
52
166
  consent: s,
53
167
  waitForUpdate: i
54
- }), c() || (l()("js", /* @__PURE__ */ new Date()), n && m({
168
+ }), ie() || (j()("js", /* @__PURE__ */ new Date()), n && I({
55
169
  src: o,
56
170
  nonce: a
57
- })), l()("config", e, {
171
+ })), j()("config", e, {
58
172
  ...t === "manual" ? { send_page_view: !1 } : {},
59
173
  ...r ? { server_container_url: r } : {}
60
174
  });
61
175
  },
62
176
  track({ name: e, params: t, eventId: n }) {
63
- l()("event", e, s(t, n));
177
+ j()("event", e, s(t, n));
64
178
  },
65
179
  consent(e) {
66
- d(e);
180
+ N(e);
67
181
  },
68
182
  page({ params: e, eventId: n }) {
69
- t === "manual" && l()("event", "page_view", s(e, n));
183
+ t === "manual" && j()("event", "page_view", s(e, n));
70
184
  },
71
185
  identify({ userId: e }) {
72
- l()("set", { user_id: e });
186
+ j()("set", { user_id: e });
73
187
  },
74
188
  reset() {
75
- l()("set", { user_id: null });
189
+ j()("set", { user_id: null });
76
190
  }
77
191
  };
78
- }, _ = /^GTM-[A-Z0-9]+$/, v = (e, t) => {
79
- let n = f(t ?? "https://www.googletagmanager.com/gtm.js", "gtm.scriptUrl");
192
+ }, z = /^GTM-[A-Z0-9]+$/, B = (e, t) => {
193
+ let n = P(t ?? "https://www.googletagmanager.com/gtm.js", "gtm.scriptUrl");
80
194
  return n.searchParams.set("id", e), n.href;
81
- }, y = ({ containerId: e, loadScript: t = !0, scriptUrl: n, waitForUpdate: r = 500, nonce: i }) => {
82
- if (!_.test(e)) throw Error(`[react-marketing-tools] gtm.containerId must look like "GTM-XXXXXXX" (received ${JSON.stringify(e)}).`);
83
- let a = v(e, n);
195
+ }, V = ({ containerId: e, loadScript: t = !0, scriptUrl: n, waitForUpdate: r = 500, nonce: i }) => {
196
+ if (!z.test(e)) throw Error(`[react-marketing-tools] gtm.containerId must look like "GTM-XXXXXXX" (received ${JSON.stringify(e)}).`);
197
+ let a = B(e, n);
84
198
  return {
85
199
  name: "gtm",
86
200
  start({ consent: e }) {
87
- u({
201
+ M({
88
202
  consent: e,
89
203
  waitForUpdate: r
90
- }), t && !p(a) && (s().push({
204
+ }), t && !F(a) && (A().push({
91
205
  "gtm.start": Date.now(),
92
206
  event: "gtm.js"
93
- }), m({
207
+ }), I({
94
208
  src: a,
95
209
  nonce: i
96
210
  }));
97
211
  },
98
- track({ name: e, params: t, eventId: n }) {
99
- s().push({
212
+ track({ name: e, params: t, eventId: n, attribution: r }) {
213
+ A().push({
100
214
  ...t,
215
+ ...r && { attribution: r },
101
216
  event: e,
102
217
  event_id: n
103
218
  });
104
219
  },
105
220
  consent(e) {
106
- d(e);
221
+ N(e);
107
222
  },
108
223
  identify({ userId: e }) {
109
- s().push({
224
+ A().push({
110
225
  event: "identify",
111
226
  user_id: e
112
227
  });
113
228
  },
114
229
  reset() {
115
- s().push({
230
+ A().push({
116
231
  event: "reset",
117
232
  user_id: void 0
118
233
  });
119
234
  }
120
235
  };
121
- }, b = class extends Error {
236
+ }, H = /* @__PURE__ */ new Set([
237
+ "AddPaymentInfo",
238
+ "AddToCart",
239
+ "AddToWishlist",
240
+ "CompleteRegistration",
241
+ "Contact",
242
+ "CustomizeProduct",
243
+ "Donate",
244
+ "FindLocation",
245
+ "InitiateCheckout",
246
+ "Lead",
247
+ "PageView",
248
+ "Purchase",
249
+ "Schedule",
250
+ "Search",
251
+ "StartTrial",
252
+ "SubmitApplication",
253
+ "Subscribe",
254
+ "ViewContent"
255
+ ]), U = {
256
+ purchase: "Purchase",
257
+ sign_up: "CompleteRegistration",
258
+ generate_lead: "Lead",
259
+ add_to_cart: "AddToCart",
260
+ begin_checkout: "InitiateCheckout",
261
+ view_item: "ViewContent",
262
+ search: "Search",
263
+ add_payment_info: "AddPaymentInfo",
264
+ add_to_wishlist: "AddToWishlist"
265
+ }, W = (e) => typeof e == "object" && !!e, G = (e) => {
266
+ let t = e.filter(W).filter((e) => e.item_id !== void 0).map((e) => ({
267
+ id: String(e.item_id),
268
+ quantity: typeof e.quantity == "number" ? e.quantity : 1
269
+ }));
270
+ return {
271
+ content_ids: t.map((e) => e.id),
272
+ contents: t,
273
+ num_items: t.reduce((e, t) => e + t.quantity, 0),
274
+ content_type: "product"
275
+ };
276
+ }, K = ({ items: e, search_term: t, ...n }) => ({
277
+ ...n,
278
+ ...typeof t == "string" ? { search_string: t } : {},
279
+ ...Array.isArray(e) ? G(e) : {}
280
+ }), q = ({ name: e, params: t, options: n }) => {
281
+ let r = n?.meta, i = r?.event ?? U[e] ?? e;
282
+ return {
283
+ command: H.has(i) ? "track" : "trackCustom",
284
+ name: i,
285
+ params: {
286
+ ...K(t),
287
+ ...r?.params
288
+ }
289
+ };
290
+ }, J = /^\d+$/, ae = "https://connect.facebook.net/en_US/fbevents.js", Y = () => {
291
+ let e = window;
292
+ if (!e.fbq) {
293
+ let t = function() {
294
+ let e = arguments;
295
+ t.callMethod ? t.callMethod.call(t, ...e) : t.queue.push(e);
296
+ };
297
+ t.push = t, t.loaded = !0, t.version = "2.0", t.queue = [], e.fbq = t, e._fbq ??= t;
298
+ }
299
+ return e.fbq;
300
+ }, oe = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/fbevents.js")), se = ({ userId: e, traits: t }) => Object.fromEntries(Object.entries({
301
+ external_id: e,
302
+ em: t.email,
303
+ ph: t.phone,
304
+ fn: t.firstName,
305
+ ln: t.lastName
306
+ }).filter(([, e]) => e !== void 0)), X = (e) => e.adUserData === "granted" ? "grant" : "revoke", ce = ({ pixelId: e, pageViews: t = "auto", loadScript: n = !0, nonce: r }) => {
307
+ if (!J.test(e)) throw Error(`[react-marketing-tools] metaPixel.pixelId must be the numeric pixel ID (received ${JSON.stringify(e)}).`);
308
+ return {
309
+ name: "metaPixel",
310
+ start({ consent: i, identity: a }) {
311
+ let o = Y();
312
+ t === "manual" && (o.disablePushState = !0), X(i) === "revoke" && o("consent", "revoke"), a ? o("init", e, se(a)) : o("init", e), t === "auto" && o("track", "PageView"), n && !oe() && I({
313
+ src: ae,
314
+ nonce: r
315
+ });
316
+ },
317
+ track(e) {
318
+ let { command: t, name: n, params: r } = q(e);
319
+ Y()(t, n, r, { eventID: e.eventId });
320
+ },
321
+ page({ eventId: e }) {
322
+ t === "manual" && Y()("track", "PageView", {}, { eventID: e });
323
+ },
324
+ consent(e) {
325
+ Y()("consent", X(e));
326
+ }
327
+ };
328
+ }, Z = class extends Error {
122
329
  code;
123
330
  constructor(e, t, n) {
124
331
  super(`[react-marketing-tools] ${t}`, n), this.name = "AnalyticsError", this.code = e;
125
332
  }
126
- }, x = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, S = [
127
- "google_",
128
- "ga_",
129
- "firebase_"
130
- ], C = 25, w = 100, T = {
131
- page_location: 1e3,
132
- page_referrer: 420,
133
- page_title: 300
134
- }, E = [
135
- "email",
136
- "phone",
137
- "first_name",
138
- "last_name",
139
- "address",
140
- "password"
141
- ], D = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, O = "[redacted]", k = (e) => {
142
- if (!x.test(e)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
143
- let t = S.find((t) => e.toLowerCase().startsWith(t));
144
- return t && `must not start with the reserved prefix "${t}"`;
145
- }, A = (e) => {
146
- let t = k(e);
147
- return t && `event name "${e}" ${t}`;
148
- }, j = (e) => {
149
- let t = Object.keys(e), n = t.flatMap((t) => {
150
- let n = k(t);
151
- if (n) return [`param "${t}" ${n}`];
152
- let r = e[t], i = T[t] ?? w;
153
- return typeof r == "string" && r.length > i ? [`param "${t}" is longer than ${i} characters`] : [];
154
- });
155
- return t.length > C ? [`has ${t.length} params; the limit is ${C}`, ...n] : n;
156
- }, M = (e) => new RegExp(D, "i").test(e), N = (e, t) => (typeof t == "string" || typeof t == "number") && E.some((t) => e.toLowerCase().includes(t)) ? O : typeof t == "string" ? t.replace(new RegExp(D, "gi"), O) : t, P = (e) => {
157
- let t = Object.entries(e).map(([e, t]) => [
158
- e,
159
- t,
160
- N(e, t)
161
- ]);
162
- return {
163
- params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
164
- redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
165
- };
166
- }, F = () => typeof window < "u", I = () => typeof crypto.randomUUID == "function" ? crypto.randomUUID() : "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (e) => (Number(e) ^ crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(1))[0] & 15 >> Number(e) / 4).toString(16)), L = (e) => {
333
+ }, Q = (e, t) => {
334
+ for (let n of e.split(";")) {
335
+ let e = n.indexOf("=");
336
+ if (e !== -1 && n.slice(0, e).trim() === t) return n.slice(e + 1).trim();
337
+ }
338
+ }, $ = () => typeof window < "u", le = 90, ue = () => 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)), de = (e) => {
167
339
  if (e.consent !== "granted" && e.consent !== "denied") throw Error(`[react-marketing-tools] createAnalytics: "consent" must be 'granted' or 'denied' (received ${JSON.stringify(e.consent)}).`);
168
- }, R = (i) => {
169
- L(i);
170
- let { debug: a = !1, onError: s = console.error, respectGpc: c = !0 } = i, l = n({
171
- consent: i.consent,
172
- gpc: c && o()
173
- }), u = l, d = [
174
- ...i.gtm ? [y({
175
- ...i.gtm,
176
- nonce: i.nonce
340
+ }, fe = (e) => {
341
+ de(e);
342
+ let { debug: t = !1, onError: n = console.error, respectGpc: r = !0 } = e, i = E({
343
+ consent: e.consent,
344
+ gpc: r && re()
345
+ }), a = i, o, s = [
346
+ ...e.gtm ? [V({
347
+ ...e.gtm,
348
+ nonce: e.nonce
349
+ })] : [],
350
+ ...e.ga4 ? [R({
351
+ ...e.ga4,
352
+ nonce: e.nonce
177
353
  })] : [],
178
- ...i.ga4 ? [g({
179
- ...i.ga4,
180
- nonce: i.nonce
354
+ ...e.metaPixel ? [ce({
355
+ ...e.metaPixel,
356
+ nonce: e.nonce
181
357
  })] : [],
182
- ...i.destinations ?? []
183
- ], f = [], p = !1, m = (e) => {
184
- if (a) throw e;
185
- s(e);
186
- }, h = (e) => {
187
- for (let t of d) try {
358
+ ...e.destinations ?? []
359
+ ], c = [], d = !1, p = e.attribution === !1 ? void 0 : ne({ ttlDays: typeof e.attribution == "object" && e.attribution.ttlDays || le }), m = !1, h = () => {
360
+ m || (m = !0, p?.observe(g({
361
+ url: location.href,
362
+ referrer: document.referrer,
363
+ capturedAt: Date.now()
364
+ })));
365
+ }, _ = () => d && a.analytics === "granted", v = (e) => {
366
+ if (t) throw e;
367
+ n(e);
368
+ }, y = (e) => {
369
+ for (let t of s) try {
188
370
  e(t);
189
371
  } catch (e) {
190
- s(new b("destination_failed", `destination "${t.name}" failed`, { cause: e }));
372
+ n(new Z("destination_failed", `destination "${t.name}" failed`, { cause: e }));
191
373
  }
192
- }, _ = (e) => {
193
- p ? h(e) : f.push(e);
194
- }, v = (e, t) => {
195
- let n = A(e);
196
- if (n) {
197
- m(new b("invalid_event", n));
374
+ }, b = (e) => {
375
+ d ? y(e) : c.push(e);
376
+ }, x = ({ name: e, params: t, options: n }) => {
377
+ let r = ee(e);
378
+ if (r) {
379
+ v(new Z("invalid_event", r));
198
380
  return;
199
381
  }
200
- for (let n of j(t)) m(new b("invalid_param", `event "${e}" ${n}`));
201
- let { params: r, redactedKeys: i } = P(t);
202
- return i.length > 0 && m(new b("pii_redacted", `event "${e}": personal data redacted from ${i.join(", ")}`)), {
382
+ for (let n of l(t)) v(new Z("invalid_param", `event "${e}" ${n}`));
383
+ let i = f(t), a = n?.meta?.params && f(n.meta.params), o = [...i.redactedKeys, ...a?.redactedKeys.map((e) => `meta.${e}`) ?? []];
384
+ o.length > 0 && v(new Z("pii_redacted", `event "${e}": personal data redacted from ${o.join(", ")}`)), h();
385
+ let s = p?.get().lastTouch;
386
+ return {
203
387
  name: e,
204
- params: r,
205
- eventId: I(),
388
+ params: i.params,
389
+ ...s && { attribution: s },
390
+ ...n && { options: a ? {
391
+ ...n,
392
+ meta: {
393
+ ...n.meta,
394
+ params: a.params
395
+ }
396
+ } : n },
397
+ eventId: ue(),
206
398
  timestamp: Date.now()
207
399
  };
208
400
  };
209
401
  return {
210
402
  start() {
211
- !p && F() && (p = !0, h((e) => e.start({ consent: l })), f.splice(0).forEach(h));
403
+ !d && $() && (d = !0, h(), _() && (p?.restore(), p?.persist()), y((e) => e.start({
404
+ consent: i,
405
+ identity: o
406
+ })), c.splice(0).forEach(y));
212
407
  },
213
- track(e, t = {}) {
214
- if (!F()) return;
215
- let n = v(e, t);
216
- n && _((e) => e.track(n));
408
+ track(e, t = {}, n) {
409
+ if (!$()) return;
410
+ let r = x({
411
+ name: e,
412
+ params: t,
413
+ options: n
414
+ });
415
+ r && b((e) => e.track(r));
217
416
  },
218
417
  page(e = {}) {
219
- if (!F()) return;
220
- let t = v("page_view", {
221
- page_location: location.href,
222
- page_title: document.title,
223
- ...e
418
+ if (!$()) return;
419
+ h(), p?.observe(g({
420
+ url: location.href,
421
+ capturedAt: Date.now()
422
+ })), _() && p?.persist();
423
+ let t = x({
424
+ name: "page_view",
425
+ params: {
426
+ page_location: location.href,
427
+ page_title: document.title,
428
+ ...e
429
+ }
224
430
  });
225
- t && _((e) => e.page ? e.page(t) : e.track(t));
431
+ t && b((e) => e.page ? e.page(t) : e.track(t));
226
432
  },
227
433
  identify(e, t = {}) {
228
- if (F()) {
229
- if (!e || M(e)) {
230
- m(new b("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
231
- return;
232
- }
233
- _((n) => n.identify?.({
234
- userId: e,
235
- traits: t
236
- }));
434
+ if (!$()) return;
435
+ if (!e || u(e)) {
436
+ v(new Z("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
437
+ return;
237
438
  }
439
+ let n = {
440
+ userId: e,
441
+ traits: t
442
+ };
443
+ o = n, b((e) => e.identify?.(n));
238
444
  },
239
445
  reset() {
240
- F() && _((e) => e.reset?.());
446
+ $() && (o = void 0, b((e) => e.reset?.()));
241
447
  },
242
448
  consent: {
243
- update(n) {
244
- if (!F()) return;
245
- let i = Object.entries(n).filter(([n, r]) => r !== void 0 && !(e.includes(n) && t(r)));
246
- if (i.length > 0) {
247
- m(new b("invalid_consent", `consent.update() ignored: ${i.map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(", ")}. Use analytics, ads, adUserData or adPersonalization with 'granted' or 'denied'.`));
449
+ update(e) {
450
+ if (!$()) return;
451
+ let t = Object.entries(e).filter(([e, t]) => t !== void 0 && !(w.includes(e) && T(t)));
452
+ if (t.length > 0) {
453
+ v(new Z("invalid_consent", `consent.update() ignored: ${t.map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(", ")}. Use analytics, ads, adUserData or adPersonalization with 'granted' or 'denied'.`));
248
454
  return;
249
455
  }
250
- let a = r(u, n);
251
- u = a, _((e) => e.consent?.(a));
456
+ let n = a, r = D(a, e);
457
+ a = r, n.analytics === "granted" && r.analytics === "denied" ? p?.erase() : _() && (p?.restore(), p?.persist()), b((e) => e.consent?.(r));
252
458
  },
253
- get: () => u
459
+ get: () => a
460
+ },
461
+ getAttribution() {
462
+ if (!$()) return {};
463
+ h();
464
+ let e = p?.get() ?? {};
465
+ if (a.adUserData !== "granted") return e;
466
+ let t = Q(document.cookie, "_fbp"), n = te({
467
+ fbcCookie: Q(document.cookie, "_fbc"),
468
+ touch: e.lastTouch
469
+ });
470
+ return {
471
+ ...e,
472
+ ...n && { fbc: n },
473
+ ...t && { fbp: t }
474
+ };
254
475
  }
255
476
  };
256
477
  };
257
478
  //#endregion
258
- export { b as n, R as t };
479
+ export { Z as n, fe as t };
@@ -13,9 +13,40 @@ export type ConsentState = {
13
13
  export type ConsentUpdate = Partial<ConsentState>;
14
14
  /** Event parameters, passed to every destination as-is. */
15
15
  export type EventParams = Record<string, unknown>;
16
+ /** Per-call adjustments for one destination, when the automatic mapping isn't what you want. */
17
+ export type TrackOptions = {
18
+ /** Meta Pixel: send as this event name (standard or custom), with these params merged over the mapped ones. */
19
+ meta?: {
20
+ event?: string;
21
+ params?: EventParams;
22
+ };
23
+ };
24
+ 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';
25
+ /** Where a visit came from: the campaign params and ad click IDs of its landing URL. */
26
+ export type Attribution = Partial<Record<CampaignParam, string>> & {
27
+ /** Origin and path of the landing page, without its query string. */
28
+ landing_page: string;
29
+ /** Origin and path of the referring page, when there was one. */
30
+ referrer?: string;
31
+ /** Milliseconds since the Unix epoch when the visit was captured. */
32
+ captured_at: number;
33
+ };
34
+ export type AttributionSnapshot = {
35
+ /** The first campaign touch within the attribution window. */
36
+ firstTouch?: Attribution;
37
+ /** The most recent campaign touch in this browser session. */
38
+ lastTouch?: Attribution;
39
+ /** Meta click ID (`_fbc`), for the Conversions API. Only with `adUserData` consent. */
40
+ fbc?: string;
41
+ /** Meta browser ID (`_fbp`), for the Conversions API. Only with `adUserData` consent. */
42
+ fbp?: string;
43
+ };
16
44
  export type AnalyticsEvent = {
17
45
  name: string;
18
46
  params: EventParams;
47
+ options?: TrackOptions;
48
+ /** The last campaign touch when the event was tracked. */
49
+ attribution?: Attribution;
19
50
  /** Unique per `track()` call and shared by every destination, so vendors can deduplicate the same event. */
20
51
  eventId: string;
21
52
  /** Milliseconds since the Unix epoch at the moment `track()` was called. */
@@ -38,9 +69,13 @@ export type Identity = {
38
69
  /** Somewhere events are sent. Built-in destinations are configured by key; custom ones go in `destinations`. */
39
70
  export type Destination = {
40
71
  name: string;
41
- /** Called once, in the browser, by `analytics.start()`, with the consent state the page started with. */
72
+ /**
73
+ * Called once, in the browser, by `analytics.start()`, with the consent state the page started with and the user
74
+ * identified before start, if any. Some vendors (the Meta Pixel) only accept user data when they initialise.
75
+ */
42
76
  start(context: {
43
77
  consent: ConsentState;
78
+ identity?: Identity;
44
79
  }): void;
45
80
  track(event: AnalyticsEvent): void;
46
81
  /** Receives every consent change, in order with events. */
@@ -76,6 +111,17 @@ export type Ga4Config = {
76
111
  /** Milliseconds tags wait for a consent update when a Consent Mode signal starts denied. Defaults to 500. */
77
112
  waitForUpdate?: number;
78
113
  };
114
+ export type MetaPixelConfig = {
115
+ /** Meta Pixel (dataset) ID, e.g. `1234567890123456`. */
116
+ pixelId: string;
117
+ /**
118
+ * `'auto'` (default): the Pixel sends PageView on load and on client-side navigation itself.
119
+ * `'manual'`: only `analytics.page()` sends PageView.
120
+ */
121
+ pageViews?: 'auto' | 'manual';
122
+ /** Set to `false` when the page already includes the Meta Pixel base code. Defaults to `true`. */
123
+ loadScript?: boolean;
124
+ };
79
125
  export type AnalyticsConfig = {
80
126
  /** Initial consent for every purpose. Required, so every site makes an explicit choice. */
81
127
  consent: ConsentStatus;
@@ -84,8 +130,16 @@ export type AnalyticsConfig = {
84
130
  * An explicit `analytics.consent.update()` still wins.
85
131
  */
86
132
  respectGpc?: boolean;
133
+ /**
134
+ * Capture UTM params and ad click IDs from landing URLs. Defaults to `true`. First and last touch are stored in the
135
+ * browser only with analytics consent; `ttlDays` (default 90) is how long a first touch is kept.
136
+ */
137
+ attribution?: boolean | {
138
+ ttlDays?: number;
139
+ };
87
140
  gtm?: GtmConfig;
88
141
  ga4?: Ga4Config;
142
+ metaPixel?: MetaPixelConfig;
89
143
  /** Custom destinations, in addition to the built-in ones. */
90
144
  destinations?: Destination[];
91
145
  /** Content-Security-Policy nonce added to every script the library injects. */
@@ -99,13 +153,15 @@ export type Analytics = {
99
153
  /** Loads vendor scripts and delivers queued events. Safe to call more than once; does nothing outside the browser. */
100
154
  start(): void;
101
155
  /** Sends an event to every destination, queued until `start()`. Does nothing outside the browser. */
102
- track(name: string, params?: EventParams): void;
156
+ track(name: string, params?: EventParams, options?: TrackOptions): void;
103
157
  /** Sends a `page_view` with the current `page_location` and `page_title`, plus any params given. */
104
158
  page(params?: EventParams): void;
105
159
  /** Associates later events with a user. `userId` must not be personal data such as an email address. */
106
160
  identify(userId: string, traits?: IdentityTraits): void;
107
161
  /** Forgets the identified user, e.g. on logout. */
108
162
  reset(): void;
163
+ /** Campaign attribution for this visitor. Empty outside the browser. */
164
+ getAttribution(): AttributionSnapshot;
109
165
  consent: {
110
166
  /** Records the visitor's choice, e.g. from your consent banner. Omitted purposes keep their current state. */
111
167
  update(update: ConsentUpdate): void;
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { createAnalytics } from './core/createAnalytics.js';
2
2
  export { AnalyticsError } from './core/errors.js';
3
3
  export type { AnalyticsErrorCode } from './core/errors.js';
4
- export type { Analytics, AnalyticsConfig, AnalyticsEvent, ConsentState, ConsentStatus, ConsentUpdate, Destination, EventParams, Ga4Config, GtmConfig, Identity, IdentityTraits, } from './core/types.js';
4
+ export type { Analytics, AnalyticsConfig, AnalyticsEvent, Attribution, AttributionSnapshot, CampaignParam, ConsentState, ConsentStatus, ConsentUpdate, Destination, EventParams, Ga4Config, GtmConfig, Identity, IdentityTraits, MetaPixelConfig, TrackOptions, } from './core/types.js';
@@ -0,0 +1,12 @@
1
+ import type { AnalyticsEvent, EventParams } from '../core/types.js';
2
+ /** GA4 recommended event → the Meta standard event with the same meaning. */
3
+ export declare const GA4_TO_META_EVENT: Readonly<Record<string, string>>;
4
+ /** GA4-style params → Meta standard params; everything else passes through. */
5
+ export declare const toMetaParams: ({ items, search_term: searchTerm, ...rest }: EventParams) => EventParams;
6
+ export type MetaCall = {
7
+ command: 'track' | 'trackCustom';
8
+ name: string;
9
+ params: EventParams;
10
+ };
11
+ /** How an event reaches the Pixel: a standard event through `track`, anything else through `trackCustom`. */
12
+ export declare const toMetaCall: ({ name, params, options, }: AnalyticsEvent) => MetaCall;
@@ -0,0 +1,6 @@
1
+ import type { Destination, MetaPixelConfig } from '../core/types.js';
2
+ type MetaPixelDestinationOptions = MetaPixelConfig & {
3
+ nonce?: string;
4
+ };
5
+ export declare const createMetaPixelDestination: ({ pixelId, pageViews, loadScript, nonce, }: MetaPixelDestinationOptions) => Destination;
6
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Reads one cookie from a `document.cookie` string or an HTTP `Cookie` header. Values are returned as stored; an
3
+ * `=` inside a value is kept.
4
+ */
5
+ export declare const parseCookie: (cookies: string, name: string) => string | undefined;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "react-marketing-tools",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.3",
5
- "description": "React Marketing Tools are a set of tools to make it easier for you to implement analytics and track user journeys, interactions throughout your App. using dataLayer/Google Tag Manager, GA4 fetch directly or coming soon facebook pixel.",
4
+ "version": "1.0.0-alpha.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
8
  "homepage": "https://www.heyrory.com/",
@@ -17,17 +17,22 @@
17
17
  "dist"
18
18
  ],
19
19
  "keywords": [
20
- "javascript",
21
20
  "react",
22
- "marketing",
23
- "tools",
24
- "marketing tools",
25
21
  "analytics",
26
- "google",
27
- "facebook",
28
- "GA4",
29
- "pixel",
30
- "dataLayer"
22
+ "marketing",
23
+ "google-tag-manager",
24
+ "gtm",
25
+ "datalayer",
26
+ "google-analytics",
27
+ "ga4",
28
+ "gtag",
29
+ "meta-pixel",
30
+ "facebook-pixel",
31
+ "consent-mode",
32
+ "gdpr",
33
+ "utm",
34
+ "attribution",
35
+ "server-side-tagging"
31
36
  ],
32
37
  "main": "./dist/index.js",
33
38
  "types": "./dist/index.d.ts",