react-marketing-tools 1.0.0-alpha.4 → 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,77 +162,78 @@ 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 = /* @__PURE__ */ new Set([
236
+ }, H = /* @__PURE__ */ new Set([
122
237
  "AddPaymentInfo",
123
238
  "AddToCart",
124
239
  "AddToWishlist",
@@ -137,7 +252,7 @@ var e = [
137
252
  "SubmitApplication",
138
253
  "Subscribe",
139
254
  "ViewContent"
140
- ]), x = {
255
+ ]), U = {
141
256
  purchase: "Purchase",
142
257
  sign_up: "CompleteRegistration",
143
258
  generate_lead: "Lead",
@@ -147,8 +262,8 @@ var e = [
147
262
  search: "Search",
148
263
  add_payment_info: "AddPaymentInfo",
149
264
  add_to_wishlist: "AddToWishlist"
150
- }, S = (e) => typeof e == "object" && !!e, C = (e) => {
151
- let t = e.filter(S).filter((e) => e.item_id !== void 0).map((e) => ({
265
+ }, W = (e) => typeof e == "object" && !!e, G = (e) => {
266
+ let t = e.filter(W).filter((e) => e.item_id !== void 0).map((e) => ({
152
267
  id: String(e.item_id),
153
268
  quantity: typeof e.quantity == "number" ? e.quantity : 1
154
269
  }));
@@ -158,21 +273,21 @@ var e = [
158
273
  num_items: t.reduce((e, t) => e + t.quantity, 0),
159
274
  content_type: "product"
160
275
  };
161
- }, w = ({ items: e, search_term: t, ...n }) => ({
276
+ }, K = ({ items: e, search_term: t, ...n }) => ({
162
277
  ...n,
163
278
  ...typeof t == "string" ? { search_string: t } : {},
164
- ...Array.isArray(e) ? C(e) : {}
165
- }), T = ({ name: e, params: t, options: n }) => {
166
- let r = n?.meta, i = r?.event ?? x[e] ?? e;
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;
167
282
  return {
168
- command: b.has(i) ? "track" : "trackCustom",
283
+ command: H.has(i) ? "track" : "trackCustom",
169
284
  name: i,
170
285
  params: {
171
- ...w(t),
286
+ ...K(t),
172
287
  ...r?.params
173
288
  }
174
289
  };
175
- }, E = /^\d+$/, D = "https://connect.facebook.net/en_US/fbevents.js", O = () => {
290
+ }, J = /^\d+$/, ae = "https://connect.facebook.net/en_US/fbevents.js", Y = () => {
176
291
  let e = window;
177
292
  if (!e.fbq) {
178
293
  let t = function() {
@@ -182,122 +297,96 @@ var e = [
182
297
  t.push = t, t.loaded = !0, t.version = "2.0", t.queue = [], e.fbq = t, e._fbq ??= t;
183
298
  }
184
299
  return e.fbq;
185
- }, k = () => Array.from(document.scripts).some((e) => URL.canParse(e.src) && new URL(e.src).pathname.endsWith("/fbevents.js")), A = ({ userId: e, traits: t }) => Object.fromEntries(Object.entries({
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({
186
301
  external_id: e,
187
302
  em: t.email,
188
303
  ph: t.phone,
189
304
  fn: t.firstName,
190
305
  ln: t.lastName
191
- }).filter(([, e]) => e !== void 0)), j = (e) => e.adUserData === "granted" ? "grant" : "revoke", M = ({ pixelId: e, pageViews: t = "auto", loadScript: n = !0, nonce: r }) => {
192
- if (!E.test(e)) throw Error(`[react-marketing-tools] metaPixel.pixelId must be the numeric pixel ID (received ${JSON.stringify(e)}).`);
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)}).`);
193
308
  return {
194
309
  name: "metaPixel",
195
310
  start({ consent: i, identity: a }) {
196
- let o = O();
197
- t === "manual" && (o.disablePushState = !0), j(i) === "revoke" && o("consent", "revoke"), a ? o("init", e, A(a)) : o("init", e), t === "auto" && o("track", "PageView"), n && !k() && m({
198
- src: D,
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,
199
314
  nonce: r
200
315
  });
201
316
  },
202
317
  track(e) {
203
- let { command: t, name: n, params: r } = T(e);
204
- O()(t, n, r, { eventID: e.eventId });
318
+ let { command: t, name: n, params: r } = q(e);
319
+ Y()(t, n, r, { eventID: e.eventId });
205
320
  },
206
321
  page({ eventId: e }) {
207
- t === "manual" && O()("track", "PageView", {}, { eventID: e });
322
+ t === "manual" && Y()("track", "PageView", {}, { eventID: e });
208
323
  },
209
324
  consent(e) {
210
- O()("consent", j(e));
325
+ Y()("consent", X(e));
211
326
  }
212
327
  };
213
- }, N = class extends Error {
328
+ }, Z = class extends Error {
214
329
  code;
215
330
  constructor(e, t, n) {
216
331
  super(`[react-marketing-tools] ${t}`, n), this.name = "AnalyticsError", this.code = e;
217
332
  }
218
- }, P = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, F = [
219
- "google_",
220
- "ga_",
221
- "firebase_"
222
- ], I = 25, L = 100, R = {
223
- page_location: 1e3,
224
- page_referrer: 420,
225
- page_title: 300
226
- }, z = [
227
- "email",
228
- "phone",
229
- "first_name",
230
- "last_name",
231
- "address",
232
- "password"
233
- ], B = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, V = "[redacted]", H = (e) => {
234
- if (!P.test(e)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
235
- let t = F.find((t) => e.toLowerCase().startsWith(t));
236
- return t && `must not start with the reserved prefix "${t}"`;
237
- }, U = (e) => {
238
- let t = H(e);
239
- return t && `event name "${e}" ${t}`;
240
- }, W = (e) => {
241
- let t = Object.keys(e), n = t.flatMap((t) => {
242
- let n = H(t);
243
- if (n) return [`param "${t}" ${n}`];
244
- let r = e[t], i = R[t] ?? L;
245
- return typeof r == "string" && r.length > i ? [`param "${t}" is longer than ${i} characters`] : [];
246
- });
247
- return t.length > I ? [`has ${t.length} params; the limit is ${I}`, ...n] : n;
248
- }, G = (e) => new RegExp(B, "i").test(e), K = (e, t) => (typeof t == "string" || typeof t == "number") && z.some((t) => e.toLowerCase().includes(t)) ? V : typeof t == "string" ? t.replace(new RegExp(B, "gi"), V) : t, q = (e) => {
249
- let t = Object.entries(e).map(([e, t]) => [
250
- e,
251
- t,
252
- K(e, t)
253
- ]);
254
- return {
255
- params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
256
- redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
257
- };
258
- }, J = () => typeof window < "u", Y = () => typeof crypto.randomUUID == "function" ? crypto.randomUUID() : "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (e) => (Number(e) ^ crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(1))[0] & 15 >> Number(e) / 4).toString(16)), X = (e) => {
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) => {
259
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)}).`);
260
- }, Z = (i) => {
261
- X(i);
262
- let { debug: a = !1, onError: s = console.error, respectGpc: c = !0 } = i, l = n({
263
- consent: i.consent,
264
- gpc: c && o()
265
- }), u = l, d, f = [
266
- ...i.gtm ? [y({
267
- ...i.gtm,
268
- 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
269
349
  })] : [],
270
- ...i.ga4 ? [g({
271
- ...i.ga4,
272
- nonce: i.nonce
350
+ ...e.ga4 ? [R({
351
+ ...e.ga4,
352
+ nonce: e.nonce
273
353
  })] : [],
274
- ...i.metaPixel ? [M({
275
- ...i.metaPixel,
276
- nonce: i.nonce
354
+ ...e.metaPixel ? [ce({
355
+ ...e.metaPixel,
356
+ nonce: e.nonce
277
357
  })] : [],
278
- ...i.destinations ?? []
279
- ], p = [], m = !1, h = (e) => {
280
- if (a) throw e;
281
- s(e);
282
- }, _ = (e) => {
283
- for (let t of f) 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 {
284
370
  e(t);
285
371
  } catch (e) {
286
- s(new N("destination_failed", `destination "${t.name}" failed`, { cause: e }));
372
+ n(new Z("destination_failed", `destination "${t.name}" failed`, { cause: e }));
287
373
  }
288
- }, v = (e) => {
289
- m ? _(e) : p.push(e);
290
- }, b = ({ name: e, params: t, options: n }) => {
291
- let r = U(e);
374
+ }, b = (e) => {
375
+ d ? y(e) : c.push(e);
376
+ }, x = ({ name: e, params: t, options: n }) => {
377
+ let r = ee(e);
292
378
  if (r) {
293
- h(new N("invalid_event", r));
379
+ v(new Z("invalid_event", r));
294
380
  return;
295
381
  }
296
- for (let n of W(t)) h(new N("invalid_param", `event "${e}" ${n}`));
297
- let i = q(t), a = n?.meta?.params && q(n.meta.params), o = [...i.redactedKeys, ...a?.redactedKeys.map((e) => `meta.${e}`) ?? []];
298
- return o.length > 0 && h(new N("pii_redacted", `event "${e}": personal data redacted from ${o.join(", ")}`)), {
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 {
299
387
  name: e,
300
388
  params: i.params,
389
+ ...s && { attribution: s },
301
390
  ...n && { options: a ? {
302
391
  ...n,
303
392
  meta: {
@@ -305,29 +394,33 @@ var e = [
305
394
  params: a.params
306
395
  }
307
396
  } : n },
308
- eventId: Y(),
397
+ eventId: ue(),
309
398
  timestamp: Date.now()
310
399
  };
311
400
  };
312
401
  return {
313
402
  start() {
314
- !m && J() && (m = !0, _((e) => e.start({
315
- consent: l,
316
- identity: d
317
- })), p.splice(0).forEach(_));
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));
318
407
  },
319
408
  track(e, t = {}, n) {
320
- if (!J()) return;
321
- let r = b({
409
+ if (!$()) return;
410
+ let r = x({
322
411
  name: e,
323
412
  params: t,
324
413
  options: n
325
414
  });
326
- r && v((e) => e.track(r));
415
+ r && b((e) => e.track(r));
327
416
  },
328
417
  page(e = {}) {
329
- if (!J()) return;
330
- let t = b({
418
+ if (!$()) return;
419
+ h(), p?.observe(g({
420
+ url: location.href,
421
+ capturedAt: Date.now()
422
+ })), _() && p?.persist();
423
+ let t = x({
331
424
  name: "page_view",
332
425
  params: {
333
426
  page_location: location.href,
@@ -335,37 +428,52 @@ var e = [
335
428
  ...e
336
429
  }
337
430
  });
338
- t && v((e) => e.page ? e.page(t) : e.track(t));
431
+ t && b((e) => e.page ? e.page(t) : e.track(t));
339
432
  },
340
433
  identify(e, t = {}) {
341
- if (!J()) return;
342
- if (!e || G(e)) {
343
- h(new N("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
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"));
344
437
  return;
345
438
  }
346
439
  let n = {
347
440
  userId: e,
348
441
  traits: t
349
442
  };
350
- d = n, v((e) => e.identify?.(n));
443
+ o = n, b((e) => e.identify?.(n));
351
444
  },
352
445
  reset() {
353
- J() && (d = void 0, v((e) => e.reset?.()));
446
+ $() && (o = void 0, b((e) => e.reset?.()));
354
447
  },
355
448
  consent: {
356
- update(n) {
357
- if (!J()) return;
358
- let i = Object.entries(n).filter(([n, r]) => r !== void 0 && !(e.includes(n) && t(r)));
359
- if (i.length > 0) {
360
- h(new N("invalid_consent", `consent.update() ignored: ${i.map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(", ")}. Use analytics, ads, adUserData or adPersonalization with 'granted' or 'denied'.`));
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'.`));
361
454
  return;
362
455
  }
363
- let a = r(u, n);
364
- u = a, v((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));
365
458
  },
366
- 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
+ };
367
475
  }
368
476
  };
369
477
  };
370
478
  //#endregion
371
- export { N as n, Z as t };
479
+ export { Z as n, fe as t };
@@ -21,10 +21,32 @@ export type TrackOptions = {
21
21
  params?: EventParams;
22
22
  };
23
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
+ };
24
44
  export type AnalyticsEvent = {
25
45
  name: string;
26
46
  params: EventParams;
27
47
  options?: TrackOptions;
48
+ /** The last campaign touch when the event was tracked. */
49
+ attribution?: Attribution;
28
50
  /** Unique per `track()` call and shared by every destination, so vendors can deduplicate the same event. */
29
51
  eventId: string;
30
52
  /** Milliseconds since the Unix epoch at the moment `track()` was called. */
@@ -108,6 +130,13 @@ export type AnalyticsConfig = {
108
130
  * An explicit `analytics.consent.update()` still wins.
109
131
  */
110
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
+ };
111
140
  gtm?: GtmConfig;
112
141
  ga4?: Ga4Config;
113
142
  metaPixel?: MetaPixelConfig;
@@ -131,6 +160,8 @@ export type Analytics = {
131
160
  identify(userId: string, traits?: IdentityTraits): void;
132
161
  /** Forgets the identified user, e.g. on logout. */
133
162
  reset(): void;
163
+ /** Campaign attribution for this visitor. Empty outside the browser. */
164
+ getAttribution(): AttributionSnapshot;
134
165
  consent: {
135
166
  /** Records the visitor's choice, e.g. from your consent banner. Omitted purposes keep their current state. */
136
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, MetaPixelConfig, TrackOptions, } 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,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.4",
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",