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 +82 -219
- package/dist/attribution/attribution.d.ts +21 -0
- package/dist/attribution/tracker.d.ts +21 -0
- package/dist/chunks/core.js +265 -157
- package/dist/core/types.d.ts +31 -0
- package/dist/core.d.ts +1 -1
- package/dist/shared/cookies.d.ts +5 -0
- package/package.json +16 -11
package/README.md
CHANGED
|
@@ -1,226 +1,89 @@
|
|
|
1
|
+
# React Marketing Tools
|
|
1
2
|
|
|
2
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/react-marketing-tools?activeTab=versions)
|
|
4
|
+
[](./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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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>;
|
package/dist/chunks/core.js
CHANGED
|
@@ -1,10 +1,124 @@
|
|
|
1
|
-
//#region lib/core/
|
|
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
|
-
],
|
|
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
|
-
},
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
...
|
|
30
|
-
...
|
|
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
|
-
},
|
|
33
|
-
|
|
34
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
42
|
-
if (!
|
|
43
|
-
r !== void 0 &&
|
|
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
|
-
|
|
165
|
+
M({
|
|
52
166
|
consent: s,
|
|
53
167
|
waitForUpdate: i
|
|
54
|
-
}),
|
|
168
|
+
}), ie() || (j()("js", /* @__PURE__ */ new Date()), n && I({
|
|
55
169
|
src: o,
|
|
56
170
|
nonce: a
|
|
57
|
-
})),
|
|
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
|
-
|
|
177
|
+
j()("event", e, s(t, n));
|
|
64
178
|
},
|
|
65
179
|
consent(e) {
|
|
66
|
-
|
|
180
|
+
N(e);
|
|
67
181
|
},
|
|
68
182
|
page({ params: e, eventId: n }) {
|
|
69
|
-
t === "manual" &&
|
|
183
|
+
t === "manual" && j()("event", "page_view", s(e, n));
|
|
70
184
|
},
|
|
71
185
|
identify({ userId: e }) {
|
|
72
|
-
|
|
186
|
+
j()("set", { user_id: e });
|
|
73
187
|
},
|
|
74
188
|
reset() {
|
|
75
|
-
|
|
189
|
+
j()("set", { user_id: null });
|
|
76
190
|
}
|
|
77
191
|
};
|
|
78
|
-
},
|
|
79
|
-
let n =
|
|
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
|
-
},
|
|
82
|
-
if (!
|
|
83
|
-
let a =
|
|
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
|
-
|
|
201
|
+
M({
|
|
88
202
|
consent: e,
|
|
89
203
|
waitForUpdate: r
|
|
90
|
-
}), t && !
|
|
204
|
+
}), t && !F(a) && (A().push({
|
|
91
205
|
"gtm.start": Date.now(),
|
|
92
206
|
event: "gtm.js"
|
|
93
|
-
}),
|
|
207
|
+
}), I({
|
|
94
208
|
src: a,
|
|
95
209
|
nonce: i
|
|
96
210
|
}));
|
|
97
211
|
},
|
|
98
|
-
track({ name: e, params: t, eventId: n }) {
|
|
99
|
-
|
|
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
|
-
|
|
221
|
+
N(e);
|
|
107
222
|
},
|
|
108
223
|
identify({ userId: e }) {
|
|
109
|
-
|
|
224
|
+
A().push({
|
|
110
225
|
event: "identify",
|
|
111
226
|
user_id: e
|
|
112
227
|
});
|
|
113
228
|
},
|
|
114
229
|
reset() {
|
|
115
|
-
|
|
230
|
+
A().push({
|
|
116
231
|
event: "reset",
|
|
117
232
|
user_id: void 0
|
|
118
233
|
});
|
|
119
234
|
}
|
|
120
235
|
};
|
|
121
|
-
},
|
|
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
|
-
]),
|
|
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
|
-
},
|
|
151
|
-
let t = e.filter(
|
|
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
|
-
},
|
|
276
|
+
}, K = ({ items: e, search_term: t, ...n }) => ({
|
|
162
277
|
...n,
|
|
163
278
|
...typeof t == "string" ? { search_string: t } : {},
|
|
164
|
-
...Array.isArray(e) ?
|
|
165
|
-
}),
|
|
166
|
-
let r = n?.meta, i = r?.event ??
|
|
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:
|
|
283
|
+
command: H.has(i) ? "track" : "trackCustom",
|
|
169
284
|
name: i,
|
|
170
285
|
params: {
|
|
171
|
-
...
|
|
286
|
+
...K(t),
|
|
172
287
|
...r?.params
|
|
173
288
|
}
|
|
174
289
|
};
|
|
175
|
-
},
|
|
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
|
-
},
|
|
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)),
|
|
192
|
-
if (!
|
|
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 =
|
|
197
|
-
t === "manual" && (o.disablePushState = !0),
|
|
198
|
-
src:
|
|
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 } =
|
|
204
|
-
|
|
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" &&
|
|
322
|
+
t === "manual" && Y()("track", "PageView", {}, { eventID: e });
|
|
208
323
|
},
|
|
209
324
|
consent(e) {
|
|
210
|
-
|
|
325
|
+
Y()("consent", X(e));
|
|
211
326
|
}
|
|
212
327
|
};
|
|
213
|
-
},
|
|
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
|
-
},
|
|
219
|
-
"
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
},
|
|
261
|
-
|
|
262
|
-
let { debug:
|
|
263
|
-
consent:
|
|
264
|
-
gpc:
|
|
265
|
-
}),
|
|
266
|
-
...
|
|
267
|
-
...
|
|
268
|
-
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
|
-
...
|
|
271
|
-
...
|
|
272
|
-
nonce:
|
|
350
|
+
...e.ga4 ? [R({
|
|
351
|
+
...e.ga4,
|
|
352
|
+
nonce: e.nonce
|
|
273
353
|
})] : [],
|
|
274
|
-
...
|
|
275
|
-
...
|
|
276
|
-
nonce:
|
|
354
|
+
...e.metaPixel ? [ce({
|
|
355
|
+
...e.metaPixel,
|
|
356
|
+
nonce: e.nonce
|
|
277
357
|
})] : [],
|
|
278
|
-
...
|
|
279
|
-
],
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
372
|
+
n(new Z("destination_failed", `destination "${t.name}" failed`, { cause: e }));
|
|
287
373
|
}
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
},
|
|
291
|
-
let r =
|
|
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
|
-
|
|
379
|
+
v(new Z("invalid_event", r));
|
|
294
380
|
return;
|
|
295
381
|
}
|
|
296
|
-
for (let n of
|
|
297
|
-
let i =
|
|
298
|
-
|
|
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:
|
|
397
|
+
eventId: ue(),
|
|
309
398
|
timestamp: Date.now()
|
|
310
399
|
};
|
|
311
400
|
};
|
|
312
401
|
return {
|
|
313
402
|
start() {
|
|
314
|
-
!
|
|
315
|
-
consent:
|
|
316
|
-
identity:
|
|
317
|
-
})),
|
|
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 (
|
|
321
|
-
let r =
|
|
409
|
+
if (!$()) return;
|
|
410
|
+
let r = x({
|
|
322
411
|
name: e,
|
|
323
412
|
params: t,
|
|
324
413
|
options: n
|
|
325
414
|
});
|
|
326
|
-
r &&
|
|
415
|
+
r && b((e) => e.track(r));
|
|
327
416
|
},
|
|
328
417
|
page(e = {}) {
|
|
329
|
-
if (
|
|
330
|
-
|
|
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 &&
|
|
431
|
+
t && b((e) => e.page ? e.page(t) : e.track(t));
|
|
339
432
|
},
|
|
340
433
|
identify(e, t = {}) {
|
|
341
|
-
if (
|
|
342
|
-
if (!e ||
|
|
343
|
-
|
|
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
|
-
|
|
443
|
+
o = n, b((e) => e.identify?.(n));
|
|
351
444
|
},
|
|
352
445
|
reset() {
|
|
353
|
-
|
|
446
|
+
$() && (o = void 0, b((e) => e.reset?.()));
|
|
354
447
|
},
|
|
355
448
|
consent: {
|
|
356
|
-
update(
|
|
357
|
-
if (
|
|
358
|
-
let
|
|
359
|
-
if (
|
|
360
|
-
|
|
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 =
|
|
364
|
-
|
|
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: () =>
|
|
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 {
|
|
479
|
+
export { Z as n, fe as t };
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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';
|
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.
|
|
5
|
-
"description": "
|
|
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
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
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",
|