react-marketing-tools 1.0.0-alpha.4 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,226 +1,161 @@
1
+ # React Marketing Tools
1
2
 
2
- # React Marketing Tools   [![npm version](https://badge.fury.io/js/react-marketing-tools.svg)](https://badge.fury.io/js/react-marketing-tools)
3
-
4
- React Marketing Tools are a set of tools to make it easier for you to implement analytics and track user journeys, interactions throughout your App. using dataLayer/Google Tag Manager, GA4 fetch directly or coming soon facebook pixel.
5
-
6
- * [React Marketing Tools Demo](https://codepen.io/bronz3beard/pen/yLZmMeg)
7
- * [Detailed Blog post on React Marketing Tools Implementation](https://blog.heyrory.com/google-analytics-4-google-tag-manager)
8
-
9
-
10
- # PR's
11
- - Have a look at the [PR template doc](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs) for best approach to getting your pr merged.
12
-
13
- # CHANGELOG
14
- - You can view it [here](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md)
15
-
16
- # Usage and setup examples.
17
-
18
- ### Setup configuration
19
- - Setting up config without provider is also an option, in this case you will only need to import _buildConfig_
20
- ```js
21
- import React from 'react'
22
- import ReactDOM from 'react-dom/client'
23
- import {
24
- ReactMarketingProvider,
25
- buildConfig,
26
- BuildConfigOptions,
27
- Tokens,
28
- } from 'react-marketing-tools'
29
- import App from './App'
30
-
31
- /*
32
- TOKENS are optional
33
- const TOKENS: Tokens = {
34
-
35
- // if withServerLocationInfo is true you must supply this token.
36
- IP_INFO_TOKEN: 'SOME_TOKEN',
37
-
38
- // if analyticsType = analyticsPlatform.GOOGLE the below tokens must be supplied.
39
- GA4_PUBLIC_API_SECRET: 'SOME_TOKEN',
40
- GA4_PUBLIC_MEASUREMENT_ID: 'SOME_TOKEN',
41
- }
42
- */
43
-
44
- // These are the keys for the values you want to include from your user data
45
- // these must be included for any user data to be collected by analytics event if user data is hardcoded when passed in.
46
- const includeUserKeys = [
47
- 'firstName',
48
- 'lastName',
49
- ]
50
-
51
- const analyticsConfig: BuildConfigOptions = {
52
- appName: 'my-awesome-app', // required
53
- appSessionCookieName: 'APP_SESSION',
54
- eventActionPrefix: { // this will extend the default values of eventActionPrefix
55
- ACTION: 'ACTION',
56
- OTHER_EVENT_NAME_TYPE: 'OTHER_EVENT_NAME_TYPE'
57
- },
58
- globalEventActionList: { // this will extend the default values of globalEventActionList
59
- SIGN_IN: 'SIGN_IN',
60
- SIGN_UP: 'SIGN_UP',
61
- IMPORTANT_BUTTON_CLICKED: 'IMPORTANT_BUTTON_CLICKED'
62
- },
63
- // TOKENS // (optional),
64
- includeUserKeys,
65
- showMissingUserAttributesInConsole: false, // a boolean condition to show or hide "user" attributes that are not included in the "includeUserKeys" array, by console logging in dev tools.
66
- withDeviceInfo: true, // (optional) has default value
67
- withServerLocationInfo: false, // (optional) has default value
68
- }
69
-
70
- /**
71
- * @type {Object} buildConfig -> options: all attributes of the options object must have a value, other than withDeviceInfo.
72
- * @property {string} appName: the name of your app this value must be passed in.
73
- * @property {string} appSessionCookieName This is used to get the cookie from storage based on a key you use, the value from the cookie will be used in "client_id:"
74
- * @property {Object} eventActionPrefix: is a { key: 'value' } object that allows you to extend "analyticsEventActionPrefixList" object with custom eventActionPrefix. To see the build in list call the function showMeBuildInEventActionPrefixList().
75
- * @property {Object} globalEventActionList: is a { key: 'value' } object that allows you to extend "analyticsGlobalEventActionList" object with custom eventActionNames. To see the build in list call the function showMeBuildInGlobalEventActionList().
76
- * @property {Array} includeUserKeys: is an array of strings that represent keys from your user data that you want to whitelist, user data you wan to hash.
77
- * @property {Boolean} showMissingUserAttributesInConsole a boolean condition to show or hide "user" attributes that are not included in the "includeUserKeys" array, by console logging in dev tools.
78
- * @property {Object} TOKENS: is a { key: 'value' } object that includes the following keys, IP_INFO_TOKEN, GA4_PUBLIC_API_SECRET, GA4_PUBLIC_MEASUREMENT_ID, depending on if you need these features enabled.
79
- * @property {Boolean} withDeviceInfo: if you want device information added to "globalVars" set this to true false by default.
80
- * @property {Boolean} withServerLocationInfo: if you want server information added to "journeyProps" set this to true false by default.
81
- */
82
- buildConfig(analyticsConfig)
83
-
84
- ReactDOM.createRoot(document.getElementById('root')).render(
85
- <React.StrictMode>
86
- <ReactMarketingProvider>
87
- <App />
88
- </ReactMarketingProvider>
89
- </React.StrictMode>
90
- )
3
+ [![npm next](https://img.shields.io/npm/v/react-marketing-tools/next?label=npm%40next)](https://www.npmjs.com/package/react-marketing-tools?activeTab=versions)
4
+ [![license](https://img.shields.io/npm/l/react-marketing-tools)](./LICENSE)
91
5
 
6
+ One `track()` call for Google Tag Manager, Google Analytics 4 and the Meta Pixel, with Consent Mode v2, UTM attribution
7
+ and personal-data redaction built in.
8
+
9
+ **[Try it in the playground](https://bronz3beard.github.io/react-marketing-tools/)**: see what each vendor receives for
10
+ every event, without sending anything.
11
+
12
+ > **1.0 is in beta** on the `next` tag. `npm install react-marketing-tools` still installs 0.4.x, whose API 1.0
13
+ > replaces; see the [migration guide](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md)
14
+ > and the [changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md).
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ npm install react-marketing-tools@next
20
+ ```
21
+
22
+ Requires React 18 or 19. The package is ESM-only; server rendering needs Node.js 22.12 or later.
23
+
24
+ ## Usage
25
+
26
+ Create one instance:
27
+
28
+ ```ts
29
+ // analytics.ts
30
+ import { createAnalytics } from 'react-marketing-tools'
31
+
32
+ export const analytics = createAnalytics({
33
+ consent: 'denied', // until your consent banner records the visitor's choice
34
+ gtm: { containerId: 'GTM-XXXXXXX' },
35
+ ga4: { measurementId: 'G-XXXXXXX' },
36
+ metaPixel: { pixelId: '1234567890123456' },
37
+ })
92
38
  ```
93
39
 
94
- ## Usage with Provider
95
- ```js
96
- import { useState, useEffect, useCallback, useRef } from 'react'
97
- import {
98
- useMarketingApi,
99
- useMarketingState,
100
- EventNameInfo,
101
- TrackAnalyticsEventOptions,
102
- ProviderApiProps,
103
- ProviderStateProps,
104
- } from 'react-marketing-tools'
105
-
106
- function App() {
107
- const [count, setCount] = useState(0)
108
- const {
109
- analyticsPlatform,
110
- appSessionCookieName,
111
- eventActionPrefixList,
112
- analyticsGlobalEventActionList,
113
- }: ProviderStateProps = useMarketingState()
114
- const {
115
- trackAnalyticsEvent
116
- /*
117
- NOTE: If you want to see built in config items and your added items, use one of the following functions to the body of your functional component or useEffect/function
118
- call it like so showMeBuildInAnalyticsPlatform() then check your console, in dev tools.
119
- showMeBuildInAnalyticsPlatform,
120
- showMeBuildInEventActionPrefixList,
121
- showMeBuildInGlobalEventActionList,
122
- */
123
- }: ProviderApiProps = useMarketingApi()
124
-
125
- useEffect(function appLoadPageLandingWelcome() {
126
- // create session cookie, useful for unauthenticated user tracking and other things
127
- document.cookie = `${appSessionCookieName}=${uuid()};max-age=${70};SameSite=Strict;Secure`
128
-
129
- const sendAnalyticsEvent = async () => {
130
- const eventNameInfo: EventNameInfo = {
131
- // If this is not passed in the event name will be a combination of actionPrefix & globalAppEvent
132
- // J_UNAUTHENTICATED
133
- eventName: 'Welcome Landing',
134
- actionPrefix: eventActionPrefixList.JOURNEY,
135
- globalAppEvent: analyticsGlobalEventActionList.UNAUTHENTICATED,
136
- }
137
-
138
- const trackingData: TrackAnalyticsEventOptions = {
139
- data: {},
140
- eventNameInfo,
141
- analyticsType: analyticsPlatform.DATALAYER_PUSH,
142
- dataLayerCheck: true,
143
- userDataKeysToHashArray: null,
144
- }
145
-
146
- await trackAnalyticsEvent(trackingData)
147
- }
148
-
149
- sendAnalyticsEvent()
150
- }, [])
151
-
152
- const handleButtonClick = useCallback(async () => {
153
- const countActual = count + 1
154
- setCount(countActual)
155
-
156
- const eventNameInfo: EventNameInfo = {
157
- // If this is not passed in the event name will be a combination of actionPrefix & globalAppEvent
158
- // I_AUTHENTICATED
159
- eventName: 'count button click',
160
- actionPrefix: eventActionPrefixList.INTERACTION,
161
- globalAppEvent: analyticsGlobalEventActionList.AUTHENTICATED,
162
- previousGlobalAppEvent: analyticsGlobalEventActionList.UNAUTHENTICATED,
163
- }
164
-
165
- const trackingData: TrackAnalyticsEventOptions = {
166
- data: {
167
- count: countActual,
168
- // firstName: 'bob',
169
- lastName: 'yeah nah',
170
- email: 'yeahnah@gmail.com',
171
- },
172
- eventNameInfo,
173
- analyticsType: analyticsPlatform.DATALAYER_PUSH,
174
- consoleLogData: {
175
- showJourneyPropsPayload: true,
176
- },
177
- dataLayerCheck: false,
178
- userDataKeysToHashArray: null,
179
- }
180
-
181
- await trackAnalyticsEvent(trackingData)
182
- }, [count])
183
-
184
- /* Example: use GA4 directly
185
- const handleButtonClick = useCallback(async () => {
186
- const countActual = count + 1
187
-
188
- setCount(countActual)
189
-
190
- const eventNameInfo: EventNameInfo = {
191
- eventName: 'count button click',
192
- actionPrefix: eventActionPrefixList.INTERACTION,
193
- globalAppEvent: analyticsGlobalEventActionList.AUTHENTICATED,
194
- previousGlobalAppEvent: analyticsGlobalEventActionList.UNAUTHENTICATED,
195
- }
196
-
197
- const trackingData: TrackAnalyticsEventOptions = {
198
- data: {
199
- count: countActual,
200
- firstName: 'bob',
201
- lastName: 'yeah nah',
202
- email: 'yeahnah@gmail.com',
203
- },
204
- eventNameInfo,
205
- analyticsType: analyticsPlatform.GOOGLE,
206
- userDataKeysToHashArray: ['email', 'firstName', 'lastName'],
207
- consoleLogData: {
208
- showJourneyPropsPayload: true,
209
- },
210
- }
211
-
212
- await trackAnalyticsEvent(trackingData)
213
- }, [count])
214
- */
215
-
216
- ...
217
-
218
- return (
219
- ...
220
- )
221
- }
40
+ Provide it to your app:
41
+
42
+ ```tsx
43
+ // main.tsx
44
+ import { AnalyticsProvider } from 'react-marketing-tools'
45
+ import { analytics } from './analytics'
46
+
47
+ createRoot(document.getElementById('root')!).render(
48
+ <AnalyticsProvider analytics={analytics}>
49
+ <App />
50
+ </AnalyticsProvider>,
51
+ )
52
+ ```
53
+
54
+ Track from any component:
55
+
56
+ ```tsx
57
+ import { useAnalytics } from 'react-marketing-tools'
58
+
59
+ export const SignUpButton = () => {
60
+ const { track } = useAnalytics()
61
+
62
+ // Reaches GTM, GA4 and Meta (as CompleteRegistration) with one shared event_id
63
+ return <button onClick={() => track('sign_up', { method: 'google' })}>Sign up</button>
64
+ }
65
+ ```
66
+
67
+ Or without code, with `autocapture: { clicks: true }` in the config:
68
+
69
+ ```html
70
+ <button data-analytics-event="cta_click" data-analytics-param-location="hero">Start</button>
71
+ ```
72
+
73
+ Follow a multi-step flow as a funnel:
74
+
75
+ ```ts
76
+ const checkout = analytics.journey('checkout')
77
+ checkout.step('shipping') // journey_start, then journey_step
78
+ checkout.complete({ value: 42, currency: 'USD' }) // journey_complete
79
+ ```
80
+
81
+ Report Core Web Vitals (LCP, INP, CLS) to GA4 and Tag Manager, after `npm install web-vitals@^6`:
82
+
83
+ ```ts
84
+ import { trackWebVitals } from 'react-marketing-tools/web-vitals'
85
+
86
+ void trackWebVitals(analytics)
87
+ ```
88
+
89
+ The same instance identifies users, records consent and gives a consenting visitor a stable ID:
90
+
91
+ ```ts
92
+ analytics.identify('user-42', { email: 'ada@example.com' }) // user id for GTM and GA4, advanced matching for Meta
93
+ analytics.consent.update({ analytics: 'granted', ads: 'granted' }) // Google Consent Mode v2 and Meta consent
94
+ const visitorId = await analytics.getVisitorId() // random by default, or a fingerprint; never sent to GA4
95
+ ```
96
+
97
+ Configure only the destinations you use. Without React, import `createAnalytics` from `react-marketing-tools/core` and
98
+ call `analytics.start()` yourself.
99
+
100
+ Send events that happen on your server, such as a purchase confirmed by a payment webhook, from
101
+ `react-marketing-tools/server`:
102
+
103
+ ```ts
104
+ import { sendMeasurementProtocolEvent } from 'react-marketing-tools/server'
105
+
106
+ await sendMeasurementProtocolEvent({
107
+ measurementId: 'G-XXXXXXX',
108
+ apiSecret: process.env.GA4_API_SECRET!,
109
+ clientId: order.ga4ClientId, // saved at checkout with readGa4Cookies()
110
+ events: [{ name: 'purchase', params: { transaction_id: order.id, value: 42, currency: 'USD' } }],
111
+ })
112
+ ```
113
+
114
+ `sendConversionsApiEvent` does the same for Meta, hashing customer information as Meta requires. To have Meta receive
115
+ the events the browser Pixel misses, counted once, relay them through your server:
116
+
117
+ ```ts
118
+ // analytics.ts
119
+ createAnalytics({ /* …as above */ server: { endpoint: '/api/track' } })
120
+
121
+ // app/api/track/route.ts (Next.js; any Request → Response server works)
122
+ import { createTrackHandler } from 'react-marketing-tools/server'
222
123
 
124
+ export const POST = createTrackHandler({
125
+ allowedOrigins: ['https://shop.example.com'],
126
+ meta: { pixelId: '1234567890123456', accessToken: process.env.META_CAPI_TOKEN! },
127
+ })
223
128
  ```
224
129
 
225
- ## Usage without Provider
226
- > To view implementation and usage without the React Context/Provider visit this [codepen](https://codepen.io/roryfn/pen/OJBGvMG)
130
+ ## What each destination receives
131
+
132
+ | | Google Tag Manager | Google Analytics 4 | Meta Pixel | Conversions API relay |
133
+ | --- | --- | --- | --- | --- |
134
+ | `track()` | dataLayer push with `event_id` | gtag.js event | standard or custom event with `eventID` | the same event from your server |
135
+ | `identify()` | `user_id` | `user_id` | advanced matching | user data, hashed on your server |
136
+ | `consent.update()` | Consent Mode v2 | Consent Mode v2 | `grant` / `revoke` | only with `adUserData` |
137
+ | Attribution | `attribution` (last touch) | read from the page URL | `fbc` | `fbc`, `fbp` |
138
+
139
+ ## Documentation
140
+
141
+ - [All docs](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/README.md)
142
+ - [Getting started](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/getting-started.md)
143
+ - [React](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/react.md): provider, hook, Next.js App Router, single-page apps
144
+ - [Tracking events](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/tracking-events.md): naming rules, page views, journeys, click autocapture, Web Vitals, users, personal data, errors
145
+ - [Configuration](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/configuration.md)
146
+ - [Consent](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/consent.md): Consent Mode v2 and Global Privacy Control
147
+ - [Attribution](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/attribution-utm.md): UTM params and ad click IDs
148
+ - [Visitor ID](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/visitor-id.md): a stable ID for consenting visitors, random or fingerprint
149
+ - [Google Tag Manager](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-tag-manager.md)
150
+ - [Google Analytics 4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/google-analytics-4.md)
151
+ - [Meta Pixel](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-pixel.md)
152
+ - [Server-side tagging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/server-side-tagging.md)
153
+ - [GA4 Measurement Protocol](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/measurement-protocol.md): GA4 events from your server
154
+ - [Meta Conversions API](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/meta-conversions-api.md): Meta events from your server
155
+ - [Debugging](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/debugging.md): see what's sent, and fix common problems
156
+ - [Migrating from 0.4](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/migration-v1.md)
157
+ - [Changelog](https://github.com/bronz3beard/react-marketing-tools/blob/main/docs/CHANGELOG.md)
158
+
159
+ ## License
160
+
161
+ [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>;
@@ -0,0 +1,11 @@
1
+ import type { EventParams } from '../core/types.js';
2
+ /**
3
+ * The event a click asks for: the nearest element around the click target with `data-analytics-event`, and its
4
+ * `data-analytics-param-*` attributes as params (`data-analytics-param-button-text` → `button_text`).
5
+ */
6
+ export declare const readClickEvent: (target: EventTarget | null) => {
7
+ name: string;
8
+ params: EventParams;
9
+ } | undefined;
10
+ /** Tracks clicks on marked elements through one listener on the document, so elements added later are covered too. */
11
+ export declare const captureClicks: (track: (name: string, params: EventParams) => void) => void;
@@ -0,0 +1,49 @@
1
+ //#region lib/core/validate.ts
2
+ var e = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, t = [
3
+ "google_",
4
+ "ga_",
5
+ "firebase_"
6
+ ], n = 25, r = 100, i = {
7
+ page_location: 1e3,
8
+ page_referrer: 420,
9
+ page_title: 300
10
+ }, a = [
11
+ "email",
12
+ "phone",
13
+ "first_name",
14
+ "last_name",
15
+ "address",
16
+ "password"
17
+ ], o = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, s = "[redacted]", c = (n) => {
18
+ if (!e.test(n)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
19
+ let r = t.find((e) => n.toLowerCase().startsWith(e));
20
+ return r && `must not start with the reserved prefix "${r}"`;
21
+ }, l = (e) => {
22
+ let t = c(e);
23
+ return t && `event name "${e}" ${t}`;
24
+ }, u = (e) => {
25
+ let t = Object.keys(e), a = t.flatMap((t) => {
26
+ let n = c(t);
27
+ if (n) return [`param "${t}" ${n}`];
28
+ let a = e[t], o = i[t] ?? r;
29
+ return typeof a == "string" && a.length > o ? [`param "${t}" is longer than ${o} characters`] : [];
30
+ });
31
+ return t.length > n ? [`has ${t.length} params; the limit is ${n}`, ...a] : a;
32
+ }, d = (e) => new RegExp(o, "i").test(e), f = (e, t) => (typeof t == "string" || typeof t == "number") && a.some((t) => e.toLowerCase().includes(t)) ? s : typeof t == "string" ? t.replace(new RegExp(o, "gi"), s) : t, p = (e) => {
33
+ let t = Object.entries(e).map(([e, t]) => [
34
+ e,
35
+ t,
36
+ f(e, t)
37
+ ]);
38
+ return {
39
+ params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
40
+ redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
41
+ };
42
+ }, m = (e, t) => {
43
+ for (let n of e.split(";")) {
44
+ let e = n.indexOf("=");
45
+ if (e !== -1 && n.slice(0, e).trim() === t) return n.slice(e + 1).trim();
46
+ }
47
+ };
48
+ //#endregion
49
+ export { p as a, u as i, d as n, l as r, m as t };