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 +82 -219
- package/dist/attribution/attribution.d.ts +21 -0
- package/dist/attribution/tracker.d.ts +21 -0
- package/dist/chunks/core.js +362 -141
- package/dist/core/types.d.ts +58 -2
- package/dist/core.d.ts +1 -1
- package/dist/destinations/metaEventMap.d.ts +12 -0
- package/dist/destinations/metaPixel.d.ts +6 -0
- 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,211 +162,318 @@ 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([
|
|
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
|
-
},
|
|
127
|
-
"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
},
|
|
169
|
-
|
|
170
|
-
let { debug:
|
|
171
|
-
consent:
|
|
172
|
-
gpc:
|
|
173
|
-
}),
|
|
174
|
-
...
|
|
175
|
-
...
|
|
176
|
-
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
|
-
...
|
|
179
|
-
...
|
|
180
|
-
nonce:
|
|
354
|
+
...e.metaPixel ? [ce({
|
|
355
|
+
...e.metaPixel,
|
|
356
|
+
nonce: e.nonce
|
|
181
357
|
})] : [],
|
|
182
|
-
...
|
|
183
|
-
],
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
372
|
+
n(new Z("destination_failed", `destination "${t.name}" failed`, { cause: e }));
|
|
191
373
|
}
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
},
|
|
195
|
-
let
|
|
196
|
-
if (
|
|
197
|
-
|
|
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
|
|
201
|
-
let
|
|
202
|
-
|
|
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:
|
|
205
|
-
|
|
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
|
-
!
|
|
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 (
|
|
215
|
-
let
|
|
216
|
-
|
|
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 (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 &&
|
|
431
|
+
t && b((e) => e.page ? e.page(t) : e.track(t));
|
|
226
432
|
},
|
|
227
433
|
identify(e, t = {}) {
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
446
|
+
$() && (o = void 0, b((e) => e.reset?.()));
|
|
241
447
|
},
|
|
242
448
|
consent: {
|
|
243
|
-
update(
|
|
244
|
-
if (
|
|
245
|
-
let
|
|
246
|
-
if (
|
|
247
|
-
|
|
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 =
|
|
251
|
-
|
|
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: () =>
|
|
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 {
|
|
479
|
+
export { Z as n, fe as t };
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 {};
|
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",
|