react-marketing-tools 0.4.4 → 1.0.0-alpha.1

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.
@@ -0,0 +1,6 @@
1
+ import type { Analytics, AnalyticsConfig } from './types.js';
2
+ /**
3
+ * Creates an analytics instance. It has no side effects until `start()`, so it is safe to create at module scope and
4
+ * during server rendering.
5
+ */
6
+ export declare const createAnalytics: (config: AnalyticsConfig) => Analytics;
@@ -0,0 +1,8 @@
1
+ export type AnalyticsErrorCode = 'invalid_event' | 'invalid_param' | 'invalid_user_id' | 'pii_redacted' | 'destination_failed';
2
+ /** Every problem the library reports to `onError`, or throws when `debug` is on. */
3
+ export declare class AnalyticsError extends Error {
4
+ readonly code: AnalyticsErrorCode;
5
+ constructor(code: AnalyticsErrorCode, message: string, options?: {
6
+ cause?: unknown;
7
+ });
8
+ }
@@ -0,0 +1,69 @@
1
+ import type { AnalyticsError } from './errors.js';
2
+ export type ConsentStatus = 'granted' | 'denied';
3
+ /** Event parameters, passed to every destination as-is. */
4
+ export type EventParams = Record<string, unknown>;
5
+ export type AnalyticsEvent = {
6
+ name: string;
7
+ params: EventParams;
8
+ /** Unique per `track()` call and shared by every destination, so vendors can deduplicate the same event. */
9
+ eventId: string;
10
+ /** Milliseconds since the Unix epoch at the moment `track()` was called. */
11
+ timestamp: number;
12
+ };
13
+ /**
14
+ * Personal data about the identified user. It is only given to destinations that match users with it (hashed by the
15
+ * vendor or on your server). It is never added to event params or the dataLayer.
16
+ */
17
+ export type IdentityTraits = {
18
+ email?: string;
19
+ phone?: string;
20
+ firstName?: string;
21
+ lastName?: string;
22
+ };
23
+ export type Identity = {
24
+ userId: string;
25
+ traits: IdentityTraits;
26
+ };
27
+ /** Somewhere events are sent. Built-in destinations are configured by key; custom ones go in `destinations`. */
28
+ export type Destination = {
29
+ name: string;
30
+ /** Called once, in the browser, by `analytics.start()`. */
31
+ start(): void;
32
+ track(event: AnalyticsEvent): void;
33
+ /** Receives `page_view` events. Destinations without it get them through `track`. */
34
+ page?(event: AnalyticsEvent): void;
35
+ identify?(identity: Identity): void;
36
+ /** Forget the identified user, e.g. on logout. */
37
+ reset?(): void;
38
+ };
39
+ export type GtmConfig = {
40
+ /** Google Tag Manager container ID, e.g. `GTM-XXXXXXX`. */
41
+ containerId: string;
42
+ /** Set to `false` when the page already includes the GTM snippet. Defaults to `true`. */
43
+ loadScript?: boolean;
44
+ };
45
+ export type AnalyticsConfig = {
46
+ /** Initial consent state. Required, so every site makes an explicit choice. */
47
+ consent: ConsentStatus;
48
+ gtm?: GtmConfig;
49
+ /** Custom destinations, in addition to the built-in ones. */
50
+ destinations?: Destination[];
51
+ /** Content-Security-Policy nonce added to every script the library injects. */
52
+ nonce?: string;
53
+ /** Throw on invalid events and personal data instead of reporting them. Turn on in development. */
54
+ debug?: boolean;
55
+ /** Receives every problem the library reports. Defaults to `console.error`. */
56
+ onError?: (error: AnalyticsError) => void;
57
+ };
58
+ export type Analytics = {
59
+ /** Loads vendor scripts and delivers queued events. Safe to call more than once; does nothing outside the browser. */
60
+ start(): void;
61
+ /** Sends an event to every destination, queued until `start()`. Does nothing outside the browser. */
62
+ track(name: string, params?: EventParams): void;
63
+ /** Sends a `page_view` with the current `page_location` and `page_title`, plus any params given. */
64
+ page(params?: EventParams): void;
65
+ /** Associates later events with a user. `userId` must not be personal data such as an email address. */
66
+ identify(userId: string, traits?: IdentityTraits): void;
67
+ /** Forgets the identified user, e.g. on logout. */
68
+ reset(): void;
69
+ };
@@ -0,0 +1,15 @@
1
+ import type { EventParams } from './types.js';
2
+ export declare const REDACTED = "[redacted]";
3
+ /** Why `name` can't be used as an event name, or `undefined` when it can. */
4
+ export declare const findEventNameProblem: (name: string) => string | undefined;
5
+ /** Param problems GA4 would handle by truncating or ignoring; reported, but the event is still sent. */
6
+ export declare const findParamProblems: (params: EventParams) => string[];
7
+ export declare const containsEmail: (value: string) => boolean;
8
+ /**
9
+ * Removes personal data from top-level params: deny-listed keys lose their whole value, and email addresses are replaced
10
+ * inside any string. Booleans and nested values are left alone.
11
+ */
12
+ export declare const redactPii: (params: EventParams) => {
13
+ params: EventParams;
14
+ redactedKeys: string[];
15
+ };
package/dist/core.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { createAnalytics } from './core/createAnalytics.js';
2
+ export { AnalyticsError } from './core/errors.js';
3
+ export type { AnalyticsErrorCode } from './core/errors.js';
4
+ export type { Analytics, AnalyticsConfig, AnalyticsEvent, ConsentStatus, Destination, EventParams, GtmConfig, Identity, IdentityTraits, } from './core/types.js';
package/dist/core.js ADDED
@@ -0,0 +1,154 @@
1
+ //#region lib/destinations/gtm.ts
2
+ var e = /^GTM-[A-Z0-9]+$/, t = () => window.dataLayer ??= [], n = (e) => Array.from(document.scripts).some((t) => t.src === e), r = ({ src: e, nonce: t }) => {
3
+ let n = document.createElement("script");
4
+ n.async = !0, n.src = e, t && n.setAttribute("nonce", t), document.head.append(n);
5
+ }, i = ({ containerId: i, loadScript: a = !0, nonce: o }) => {
6
+ if (!e.test(i)) throw Error(`[react-marketing-tools] gtm.containerId must look like "GTM-XXXXXXX" (received ${JSON.stringify(i)}).`);
7
+ let s = `https://www.googletagmanager.com/gtm.js?id=${i}`;
8
+ return {
9
+ name: "gtm",
10
+ start() {
11
+ let e = t();
12
+ a && !n(s) && (e.push({
13
+ "gtm.start": Date.now(),
14
+ event: "gtm.js"
15
+ }), r({
16
+ src: s,
17
+ nonce: o
18
+ }));
19
+ },
20
+ track({ name: e, params: n, eventId: r }) {
21
+ t().push({
22
+ ...n,
23
+ event: e,
24
+ event_id: r
25
+ });
26
+ },
27
+ identify({ userId: e }) {
28
+ t().push({
29
+ event: "identify",
30
+ user_id: e
31
+ });
32
+ },
33
+ reset() {
34
+ t().push({
35
+ event: "reset",
36
+ user_id: void 0
37
+ });
38
+ }
39
+ };
40
+ }, a = class extends Error {
41
+ code;
42
+ constructor(e, t, n) {
43
+ super(`[react-marketing-tools] ${t}`, n), this.name = "AnalyticsError", this.code = e;
44
+ }
45
+ }, o = /^[A-Za-z][A-Za-z0-9_]{0,39}$/, s = [
46
+ "google_",
47
+ "ga_",
48
+ "firebase_"
49
+ ], c = 25, l = 100, u = {
50
+ page_location: 1e3,
51
+ page_referrer: 420,
52
+ page_title: 300
53
+ }, d = [
54
+ "email",
55
+ "phone",
56
+ "first_name",
57
+ "last_name",
58
+ "address",
59
+ "password"
60
+ ], f = String.raw`[\w.+-]+(?:@|%40)[\w-]+(?:\.[\w-]+)+`, p = "[redacted]", m = (e) => {
61
+ if (!o.test(e)) return "must start with a letter, contain only letters, digits and underscores, and be at most 40 characters";
62
+ let t = s.find((t) => e.toLowerCase().startsWith(t));
63
+ return t && `must not start with the reserved prefix "${t}"`;
64
+ }, h = (e) => {
65
+ let t = m(e);
66
+ return t && `event name "${e}" ${t}`;
67
+ }, g = (e) => {
68
+ let t = Object.keys(e), n = t.flatMap((t) => {
69
+ let n = m(t);
70
+ if (n) return [`param "${t}" ${n}`];
71
+ let r = e[t], i = u[t] ?? l;
72
+ return typeof r == "string" && r.length > i ? [`param "${t}" is longer than ${i} characters`] : [];
73
+ });
74
+ return t.length > c ? [`has ${t.length} params; the limit is ${c}`, ...n] : n;
75
+ }, _ = (e) => new RegExp(f, "i").test(e), v = (e, t) => (typeof t == "string" || typeof t == "number") && d.some((t) => e.toLowerCase().includes(t)) ? p : typeof t == "string" ? t.replace(new RegExp(f, "gi"), p) : t, y = (e) => {
76
+ let t = Object.entries(e).map(([e, t]) => [
77
+ e,
78
+ t,
79
+ v(e, t)
80
+ ]);
81
+ return {
82
+ params: Object.fromEntries(t.map(([e, , t]) => [e, t])),
83
+ redactedKeys: t.filter(([, e, t]) => t !== e).map(([e]) => e)
84
+ };
85
+ }, b = () => typeof window < "u", x = () => 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)), S = (e) => {
86
+ if (e.consent !== "granted" && e.consent !== "denied") throw Error(`[react-marketing-tools] createAnalytics: "consent" must be 'granted' or 'denied' (received ${JSON.stringify(e.consent)}).`);
87
+ }, C = (e) => {
88
+ S(e);
89
+ let { debug: t = !1, onError: n = console.error } = e, r = [...e.gtm ? [i({
90
+ ...e.gtm,
91
+ nonce: e.nonce
92
+ })] : [], ...e.destinations ?? []], o = [], s = !1, c = (e) => {
93
+ if (t) throw e;
94
+ n(e);
95
+ }, l = (e) => {
96
+ for (let t of r) try {
97
+ e(t);
98
+ } catch (e) {
99
+ n(new a("destination_failed", `destination "${t.name}" failed`, { cause: e }));
100
+ }
101
+ }, u = (e) => {
102
+ s ? l(e) : o.push(e);
103
+ }, d = (e, t) => {
104
+ let n = h(e);
105
+ if (n) {
106
+ c(new a("invalid_event", n));
107
+ return;
108
+ }
109
+ for (let n of g(t)) c(new a("invalid_param", `event "${e}" ${n}`));
110
+ let { params: r, redactedKeys: i } = y(t);
111
+ return i.length > 0 && c(new a("pii_redacted", `event "${e}": personal data redacted from ${i.join(", ")}`)), {
112
+ name: e,
113
+ params: r,
114
+ eventId: x(),
115
+ timestamp: Date.now()
116
+ };
117
+ };
118
+ return {
119
+ start() {
120
+ !s && b() && (s = !0, l((e) => e.start()), o.splice(0).forEach(l));
121
+ },
122
+ track(e, t = {}) {
123
+ if (!b()) return;
124
+ let n = d(e, t);
125
+ n && u((e) => e.track(n));
126
+ },
127
+ page(e = {}) {
128
+ if (!b()) return;
129
+ let t = d("page_view", {
130
+ page_location: location.href,
131
+ page_title: document.title,
132
+ ...e
133
+ });
134
+ t && u((e) => e.page ? e.page(t) : e.track(t));
135
+ },
136
+ identify(e, t = {}) {
137
+ if (b()) {
138
+ if (!e || _(e)) {
139
+ c(new a("invalid_user_id", "identify() needs a non-empty user id that is not personal data such as an email address"));
140
+ return;
141
+ }
142
+ u((n) => n.identify?.({
143
+ userId: e,
144
+ traits: t
145
+ }));
146
+ }
147
+ },
148
+ reset() {
149
+ b() && u((e) => e.reset?.());
150
+ }
151
+ };
152
+ };
153
+ //#endregion
154
+ export { a as AnalyticsError, C as createAnalytics };
@@ -0,0 +1,6 @@
1
+ import type { Destination, GtmConfig } from '../core/types.js';
2
+ type GtmDestinationOptions = GtmConfig & {
3
+ nonce?: string;
4
+ };
5
+ export declare const createGtmDestination: ({ containerId, loadScript, nonce, }: GtmDestinationOptions) => Destination;
6
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-marketing-tools",
3
3
  "type": "module",
4
- "version": "0.4.4",
4
+ "version": "1.0.0-alpha.1",
5
5
  "description": "React Marketing Tools are a set of tools to make it easier for you to implement analytics and track user journeys, interactions throughout your App. using dataLayer/Google Tag Manager, GA4 fetch directly or coming soon facebook pixel.",
6
6
  "license": "MIT",
7
7
  "author": "bronz3beard <exempli.gratia.webdesign@gmail.com> (https://www.heyrory.com/)",
@@ -29,26 +29,30 @@
29
29
  "pixel",
30
30
  "dataLayer"
31
31
  ],
32
- "main": "./dist/react-marketing-tools.umd.cjs",
33
- "module": "./dist/react-marketing-tools.es.js",
32
+ "main": "./dist/index.js",
34
33
  "types": "./dist/index.d.ts",
35
34
  "exports": {
36
35
  ".": {
37
- "import": {
38
- "types": "./dist/index.d.ts",
39
- "default": "./dist/react-marketing-tools.es.js"
40
- },
41
- "require": {
42
- "types": "./dist/index.d.cts",
43
- "default": "./dist/react-marketing-tools.umd.cjs"
44
- }
45
- }
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./core": {
40
+ "types": "./dist/core.d.ts",
41
+ "default": "./dist/core.js"
42
+ },
43
+ "./package.json": "./package.json"
46
44
  },
47
45
  "sideEffects": false,
46
+ "engines": {
47
+ "node": ">=22.12"
48
+ },
49
+ "publishConfig": {
50
+ "tag": "next"
51
+ },
48
52
  "scripts": {
49
- "build": "vite build && tsc -p tsconfig.build.json && node scripts/emit-cts-types.mjs && node scripts/copy-legacy-umd.mjs",
53
+ "build": "vite build && tsc -p tsconfig.build.json",
50
54
  "build:watch": "vite build --watch",
51
- "check:package": "publint --strict && attw --pack .",
55
+ "check:package": "publint --strict && attw --pack . --profile esm-only",
52
56
  "check:size": "node scripts/check-size.mjs",
53
57
  "format": "prettier --write .",
54
58
  "format:check": "prettier --check .",
@@ -1,13 +0,0 @@
1
- import { TrackAnalyticsEventOptions } from '../types/index.cjs';
2
- /**
3
- *
4
- * @paramType {object} options
5
- * @property {object} data this is an object of any data you want to collect in analytics
6
- * @property {object} eventNameInfo
7
- * @property {string} analyticsType
8
- * @property {array | undefined} userDataKeysToHashArray default for this value is null, when used it should include an array of strings you wish to hash.
9
- * @property {boolean} dataLayerCheck
10
- * @property {object} consoleLogData (optional) is an object with the following attributes, "showGlobalVars", "showJourneyPropsPayload", "showUserProps" if any of the values are true you will be able to see the respective payload in your console.
11
- */
12
- declare const trackAnalyticsEvent: (options: TrackAnalyticsEventOptions) => Promise<void>;
13
- export { trackAnalyticsEvent };
@@ -1,3 +0,0 @@
1
- import { Ga4GoogleAnalyticsEventTracking } from '../types';
2
- declare const ga4GoogleAnalyticsEventTracking: (options: Ga4GoogleAnalyticsEventTracking) => Promise<void>;
3
- export default ga4GoogleAnalyticsEventTracking;
@@ -1,3 +0,0 @@
1
- import { HandleDataLayerPushOptions } from '../types';
2
- declare const handleDataLayerPush: (options: HandleDataLayerPushOptions) => Promise<void>;
3
- export default handleDataLayerPush;
@@ -1,41 +0,0 @@
1
- import DeviceDetector from 'device-detector-js';
2
- import { AllowedTypes, EventNameInfo } from '../types';
3
- /**
4
- *
5
- * @param {object} user the structure of this data is dictated by you, aka your API or DB.
6
- * @param {[string]} includeUserKeys is an array of strings that represent keys from your user data that you want to whitelist, the value for each key in the whitelist will be hashed.
7
- * @returns a new user object with all values hashed including only the whitelisted key value pairs
8
- */
9
- export declare const hashUserData: (user: Record<AllowedTypes, AllowedTypes>, includeUserKeys: Array<string>) => Promise<Record<AllowedTypes, AllowedTypes>>;
10
- /**
11
- *
12
- * @param {object} user the structure of this data is dictated by you, aka your API or DB.
13
- * @param {[string]} includeUserKeys is an array of strings that represent keys from your user data that you want to map over, if no keys are supplied the original passed in user data is returned.
14
- * @param {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.
15
- * @returns If no "includeUserKeys" are supplied the original ser object is returned, otherwise, a new user object with only the key value pairs that you included in your includeUserKeys array is returned.
16
- */
17
- export declare const buildNewUserData: (user: Record<AllowedTypes, AllowedTypes>, includeUserKeys: Array<string>, showMissingUserAttributesInConsole: boolean | undefined) => Record<string, string>;
18
- /**
19
- *
20
- * @paramType {Object} eventNameInfo
21
- * @property {analyticsPrefixActionList} actionPrefix either a built in value of JOURNEY | INTERACTION or a custom value added to the analyticsPrefixActionList object.
22
- * @property {string} description an OPTIONAL value passed in that is formatted like the following from 'Very descriptive description' to VERY_DESCRIPTIVE_DESCRIPTION.
23
- * @property {string} globalAppEvent the current event name for this track event selected from the analyticsGlobalEventActionList
24
- * @property {string} previousGlobalAppEvent an OPTIONAL value, the previous event name for this track event selected from the analyticsGlobalEventActionList.
25
- * @returns string eventName for this specific tracked event
26
- */
27
- export declare const buildAnalyticsEventName: (eventNameInfo: EventNameInfo) => string;
28
- /**
29
- *
30
- * @param {object} data
31
- * @param {string} globalAppEvent
32
- * @returns dataObject
33
- */
34
- export declare const buildEventDataObject: (data: Record<AllowedTypes, AllowedTypes>, globalAppEvent: string) => Promise<Record<AllowedTypes, AllowedTypes>>;
35
- /**
36
- *
37
- * @param {string} eventName
38
- * @returns a Boolean value, true if the event name is already in the dataLayer.
39
- */
40
- export declare const defaultDataLayerEventCheck: (eventName: string) => boolean;
41
- export declare const deviceDetectorInfo: (userAgent: string) => DeviceDetector.DeviceDetectorResult;
@@ -1,8 +0,0 @@
1
- import { BuildConfigOptions, Config, AnalyticsPlatform } from '../types/index.cjs';
2
- declare const analyticsPlatform: AnalyticsPlatform;
3
- declare const showMeBuildInAnalyticsPlatform: () => void;
4
- declare const showMeBuildInEventActionPrefixList: () => void;
5
- declare const showMeBuildInGlobalEventActionList: () => void;
6
- declare let config: Config;
7
- declare const buildConfig: (options: BuildConfigOptions) => void;
8
- export { config, analyticsPlatform, buildConfig, showMeBuildInAnalyticsPlatform, showMeBuildInGlobalEventActionList, showMeBuildInEventActionPrefixList, };
package/dist/index.d.cts DELETED
@@ -1,26 +0,0 @@
1
- import { ReactElement, ReactNode, Context } from 'react';
2
- import type { ProviderStateProps, ProviderApiProps } from './types/index.cjs';
3
- export type { Event, GooglePayload, EventNameInfo, AllowedTypes, TrackAnalyticsEventOptions, GlobalVars, DataLayer, ConsoleLogData, AnalyticsTrackerDataOptions, HandleDataLayerPushOptions, ServerLocationData, IpInfo, Ga4GoogleAnalyticsEventTracking, Platform, AnalyticsPlatform, Tokens, Config, BuildConfigOptions, ProviderStateProps, ProviderApiProps, AnalyticsEventActionPrefix, AnalyticsGlobalEventAction, } from './types/index.cjs';
4
- export { config, analyticsPlatform, buildConfig, showMeBuildInAnalyticsPlatform, showMeBuildInGlobalEventActionList, showMeBuildInEventActionPrefixList, } from './buildConfig/index.cjs';
5
- export { trackAnalyticsEvent } from './analytics/analyticsEventService.cjs';
6
- export declare const ContextState: Context<ProviderStateProps>;
7
- export declare const ContextApi: Context<ProviderApiProps>;
8
- type ReactMarketingProviderProps = {
9
- children: ReactNode;
10
- };
11
- export declare const ReactMarketingProvider: ({ children, }: ReactMarketingProviderProps) => ReactElement;
12
- /**
13
- * @property {string} appName
14
- * @property {string} appSessionCookieName
15
- * @property {AnalyticsPlatform} analyticsPlatform
16
- * @property {AnalyticsEventActionPrefix} eventActionPrefixList
17
- * @property {AnalyticsGlobalEventAction} analyticsGlobalEventActionList
18
- */
19
- export declare const useMarketingState: () => ProviderStateProps;
20
- /**
21
- * @property {function} trackAnalyticsEvent(options)
22
- * @property {function} showMeBuildInAnalyticsPlatform
23
- * @property {function} showMeBuildInEventActionPrefixList
24
- * @property {function} showMeBuildInGlobalEventActionList
25
- */
26
- export declare const useMarketingApi: () => ProviderApiProps;
@@ -1,3 +0,0 @@
1
- import { IpInfo, ServerLocationData } from '../types';
2
- export declare const buildServerLocationData: (withServerLocationInfo: boolean | undefined, IP_INFO_TOKEN: string | undefined) => Promise<ServerLocationData | undefined>;
3
- export declare const getIpInfo: (IP_INFO_TOKEN: string) => Promise<IpInfo>;