react-marketing-tools 1.0.0-alpha.0 → 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,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
+ }
@@ -1,3 +1,4 @@
1
+ import type { AnalyticsError } from './errors.js';
1
2
  export type ConsentStatus = 'granted' | 'denied';
2
3
  /** Event parameters, passed to every destination as-is. */
3
4
  export type EventParams = Record<string, unknown>;
@@ -9,12 +10,31 @@ export type AnalyticsEvent = {
9
10
  /** Milliseconds since the Unix epoch at the moment `track()` was called. */
10
11
  timestamp: number;
11
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
+ };
12
27
  /** Somewhere events are sent. Built-in destinations are configured by key; custom ones go in `destinations`. */
13
28
  export type Destination = {
14
29
  name: string;
15
30
  /** Called once, in the browser, by `analytics.start()`. */
16
31
  start(): void;
17
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;
18
38
  };
19
39
  export type GtmConfig = {
20
40
  /** Google Tag Manager container ID, e.g. `GTM-XXXXXXX`. */
@@ -30,10 +50,20 @@ export type AnalyticsConfig = {
30
50
  destinations?: Destination[];
31
51
  /** Content-Security-Policy nonce added to every script the library injects. */
32
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;
33
57
  };
34
58
  export type Analytics = {
35
59
  /** Loads vendor scripts and delivers queued events. Safe to call more than once; does nothing outside the browser. */
36
60
  start(): void;
37
61
  /** Sends an event to every destination, queued until `start()`. Does nothing outside the browser. */
38
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;
39
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 CHANGED
@@ -1,2 +1,4 @@
1
1
  export { createAnalytics } from './core/createAnalytics.js';
2
- export type { Analytics, AnalyticsConfig, AnalyticsEvent, ConsentStatus, Destination, EventParams, GtmConfig, } from './core/types.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 CHANGED
@@ -23,37 +23,132 @@ var e = /^GTM-[A-Z0-9]+$/, t = () => window.dataLayer ??= [], n = (e) => Array.f
23
23
  event: e,
24
24
  event_id: r
25
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
+ });
26
38
  }
27
39
  };
28
- }, a = () => typeof window < "u", o = () => 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) => {
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) => {
29
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)}).`);
30
- }, c = (e) => {
31
- s(e);
32
- let t = [...e.gtm ? [i({
87
+ }, C = (e) => {
88
+ S(e);
89
+ let { debug: t = !1, onError: n = console.error } = e, r = [...e.gtm ? [i({
33
90
  ...e.gtm,
34
91
  nonce: e.nonce
35
- })] : [], ...e.destinations ?? []], n = [], r = !1, c = (e) => {
36
- for (let n of t) n.track(e);
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
+ };
37
117
  };
38
118
  return {
39
119
  start() {
40
- if (!r && a()) {
41
- r = !0;
42
- for (let e of t) e.start();
43
- n.splice(0).forEach(c);
44
- }
120
+ !s && b() && (s = !0, l((e) => e.start()), o.splice(0).forEach(l));
45
121
  },
46
122
  track(e, t = {}) {
47
- if (!a()) return;
48
- let i = {
49
- name: e,
50
- params: t,
51
- eventId: o(),
52
- timestamp: Date.now()
53
- };
54
- r ? c(i) : n.push(i);
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?.());
55
150
  }
56
151
  };
57
152
  };
58
153
  //#endregion
59
- export { c as createAnalytics };
154
+ export { a as AnalyticsError, C as createAnalytics };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-marketing-tools",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.0",
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/)",