@uxf/analytics 11.120.0 → 11.122.4

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.
Files changed (2) hide show
  1. package/README.md +143 -51
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -4,6 +4,18 @@
4
4
  [![quality](https://img.shields.io/npms-io/quality-score/@uxf/analytics)](https://www.npmjs.com/package/@uxf/analytics)
5
5
  [![license](https://img.shields.io/npm/l/@uxf/analytics)](https://www.npmjs.com/package/@uxf/analytics)
6
6
 
7
+ Client-side plumbing for Google Tag Manager, GDPR cookie consent, and A/B testing in UXF Next.js apps.
8
+
9
+ ## When to use
10
+
11
+ Use this package to manage the consent cookie, inject the GTM loader with a consent-aware default state, and assign/read A/B test variants. It is not an analytics dashboard or a data-collection SDK — event data still flows through GTM/GA that you configure yourself.
12
+
13
+ Each concern is a **separate subpath import** — there is no root `@uxf/analytics` entry:
14
+
15
+ - `@uxf/analytics/consent` — read/write the cookie-consent cookie.
16
+ - `@uxf/analytics/gtm` — GTM bootstrap script and consent updates.
17
+ - `@uxf/analytics/ab-testing` — experiment assignment, provider, and hooks.
18
+
7
19
  ## Installation
8
20
 
9
21
  ```bash
@@ -14,10 +26,16 @@ yarn add @uxf/analytics
14
26
  npm install @uxf/analytics
15
27
  ```
16
28
 
29
+ Peer dependencies: `@uxf/core`, `@uxf/core-react`, and `react >=18.0.2`.
30
+
17
31
  ## Cookie consent
18
32
 
33
+ `@uxf/analytics/consent` stores the four Google consent flags plus a `version` in a base64-encoded `cookieConsent` cookie (90-day TTL by default). Bump the `version` to invalidate old consent and re-prompt users.
34
+
19
35
  ### Store consent to cookie
20
36
 
37
+ Client-only — throws if called on the server.
38
+
21
39
  ```ts
22
40
  import { storeConsentToCookie } from "@uxf/analytics/consent";
23
41
 
@@ -28,7 +46,7 @@ storeConsentToCookie(
28
46
  ad_user_data: false,
29
47
  analytics_storage: false,
30
48
  },
31
- 1 // version - this allows us to simply re-request consents from users
49
+ 1, // version
32
50
  );
33
51
  ```
34
52
 
@@ -38,50 +56,39 @@ storeConsentToCookie(
38
56
  import { readConsentFromCookie } from "@uxf/analytics/consent";
39
57
 
40
58
  const consent = readConsentFromCookie();
59
+ // { ad_personalization?, ad_storage?, ad_user_data?, analytics_storage?, version }
41
60
  ```
42
61
 
43
- ### Check if cookie consent is set
62
+ ### Check if consent is set
44
63
 
45
- This checks if the consent is already stored in cookies.
64
+ Returns `true` only when all four flags are booleans and the stored `version` matches.
46
65
 
47
- ```tsx
66
+ ```ts
48
67
  import { isConsentCookieSet } from "@uxf/analytics/consent";
49
68
 
50
- const isSet = isCookieConsentSet(
51
- null,
52
- 1 // version - this allows us to simply re-request consents from users
53
- ); // boolean
69
+ const isSet = isConsentCookieSet(null, 1); // (ctx, version) -> boolean
54
70
  ```
55
71
 
56
72
  ## GTM
57
73
 
58
74
  ### Initialize GTM
59
75
 
60
- This reads the consent from cookie and initializes the GTM dataLayer.
61
-
62
- ```tsx
63
- import { GtmScript } from "@uxf/analytics/gtm";
64
-
65
- // in your head component (not directly in _app)
66
- <GtmScript gtmId="GTM-YOURID" />
67
- ```
68
-
69
- #### or with hook
76
+ `useGtmScript` reads the consent cookie, builds the inline bootstrap script (sets the gtag default consent state, then loads GTM for the given container id), and returns it as a string. Render it in your document `<head>` — not directly in `_app`.
70
77
 
71
78
  ```tsx
72
79
  import { useGtmScript } from "@uxf/analytics/gtm";
73
80
 
74
81
  const gtmScript = useGtmScript("GTM-YOURID");
75
82
 
76
- // in your head component (not directly in _app)
77
- <script dangerouslySetInnerHTML={{ __html: gtmScript }} />
83
+ // in your head component
84
+ <script dangerouslySetInnerHTML={{ __html: gtmScript }} />;
78
85
  ```
79
86
 
80
87
  ### Update GTM consent
81
88
 
82
- This stores the consent to cookie and updates the GTM dataLayer.
89
+ Stores the consent to cookie and pushes a gtag `consent: "update"` call plus a `consent_resolved` event to the dataLayer. Client-only.
83
90
 
84
- ```tsx
91
+ ```ts
85
92
  import { updateGtmConsent } from "@uxf/analytics/gtm";
86
93
 
87
94
  updateGtmConsent(
@@ -91,22 +98,22 @@ updateGtmConsent(
91
98
  ad_user_data: false,
92
99
  analytics_storage: false,
93
100
  },
94
- 1
101
+ 1, // version
95
102
  );
96
103
  ```
97
104
 
98
- ## AB testing
105
+ ## A/B testing
99
106
 
100
- A set of components and helpers prepared to implement AB testing to React & NextJS applications.
101
-
102
- ### How to start
107
+ A set of helpers, a provider, and hooks for running A/B tests in a Next.js app. Assignment happens in the proxy/middleware (a per-experiment `uxf-experiment-*` cookie), and variants are exposed to components through `ABTestingProvider`.
103
108
 
104
109
  #### 1. Define your experiments
105
110
 
111
+ Use `as const satisfies ExperimentConfig[]` so `useABTestingVariant` can infer literal variant names.
112
+
106
113
  ```ts
107
114
  import type { ExperimentConfig } from "@uxf/analytics/ab-testing";
108
115
 
109
- const experiments = [
116
+ export const experiments = [
110
117
  {
111
118
  id: "1",
112
119
  traffic: 1,
@@ -119,31 +126,21 @@ const experiments = [
119
126
  id: "2",
120
127
  traffic: 0.5,
121
128
  variants: [
122
- { name: "Control", traffic: 0.25 },
123
- { name: "B", traffic: 0.75 },
129
+ { name: "Control", traffic: 0.5 },
130
+ { name: "B", traffic: 0.5 },
124
131
  ],
125
132
  },
126
133
  ] as const satisfies ExperimentConfig[];
127
134
  ```
128
135
 
129
- #### 2. Use the `ABTestingProvider` component
136
+ #### 2. Assign variants in `proxy.ts`
130
137
 
131
- ```tsx
132
- import { ABTestingProvider, AB_TESTING_VARIANT_PROP_NAME } from "@uxf/analytics/ab-testing";
133
-
134
- export default function App({ Component, pageProps }) {
135
- return (
136
- <ABTestingProvider experiments={props.pageProps[AB_TESTING_VARIANT_PROP_NAME]}>
137
- <Component {...pageProps} />
138
- </ABTestingProvider>
139
- );
140
- }
141
- ```
142
-
143
- #### 3. Handle AB testing in `proxy.ts`
138
+ `handleABTesting` sets a cookie per experiment (value `not-participate` for excluded users) and removes cookies for experiments no longer in the config.
144
139
 
145
140
  ```ts
146
141
  import { handleABTesting } from "@uxf/analytics/ab-testing";
142
+ import { NextRequest, NextResponse } from "next/server";
143
+ import { experiments } from "./app/examples/ab-testing/constants";
147
144
 
148
145
  export async function proxy(request: NextRequest) {
149
146
  const nextResponse = NextResponse.next();
@@ -154,25 +151,120 @@ export async function proxy(request: NextRequest) {
154
151
  }
155
152
  ```
156
153
 
157
- #### 4. Implement SSR support in page with AB testing
154
+ #### 3. Wrap the app with `ABTestingProvider`
155
+
156
+ The provider takes the assigned variants as `[experimentId, variantName][]` and fires an `experience_impression` GTM event on mount.
157
+
158
+ **App Router** — read the cookies via `next/headers` and map them with `getExperimentsFromContext`:
159
+
160
+ ```tsx
161
+ import { ABTestingProvider, getExperimentsFromContext } from "@uxf/analytics/ab-testing";
162
+ import { cookies } from "next/headers";
163
+
164
+ async function Layout(props: LayoutProps<"/examples/ab-testing">) {
165
+ return (
166
+ <ABTestingProvider
167
+ experiments={getExperimentsFromContext(
168
+ Object.fromEntries((await cookies()).getAll().map((v) => [v.name, v.value])),
169
+ )}
170
+ >
171
+ {props.children}
172
+ </ABTestingProvider>
173
+ );
174
+ }
175
+
176
+ export default Layout;
177
+ ```
178
+
179
+ **Pages Router** — inject the variants in `getServerSideProps` with `addExperimentsSSR`, then read them from `pageProps`:
158
180
 
159
181
  ```tsx
160
182
  import { addExperimentsSSR } from "@uxf/analytics/ab-testing";
183
+ import type { GetServerSideProps } from "next";
161
184
 
162
185
  export const getServerSideProps: GetServerSideProps = async (ctx) => {
163
- return addExperimentsSSR(ctx, {
164
- props: {},
165
- });
186
+ return addExperimentsSSR(ctx, { props: {} });
166
187
  };
167
188
  ```
168
189
 
169
- #### 5. Use the `useABTestingVariant` hook in your component
190
+ ```tsx
191
+ import { ABTestingProvider, AB_TESTING_VARIANT_PROP_NAME } from "@uxf/analytics/ab-testing";
192
+
193
+ export default function App({ Component, pageProps }) {
194
+ return (
195
+ <ABTestingProvider experiments={pageProps[AB_TESTING_VARIANT_PROP_NAME]}>
196
+ <Component {...pageProps} />
197
+ </ABTestingProvider>
198
+ );
199
+ }
200
+ ```
201
+
202
+ #### 4. Read the variant in a component
203
+
204
+ `useABTestingVariant` returns the variant name, or `null` when the experiment id is unknown or the user does not participate. Client component only.
170
205
 
171
206
  ```tsx
207
+ "use client";
208
+
172
209
  import { useABTestingVariant } from "@uxf/analytics/ab-testing";
210
+ import type { experiments } from "./constants";
211
+
212
+ function Page() {
213
+ const variant = useABTestingVariant<typeof experiments>("1");
173
214
 
174
- const abTestingVariant = useABTestingVariant<typeof experiments>("1");
215
+ return <div>Experiment 1 variant: {variant}</div>; // "Control" | "B" | null
216
+ }
175
217
 
176
- console.log(abTestingVariant); // "Control", "B", etc.
218
+ export default Page;
177
219
  ```
178
220
 
221
+ ## API
222
+
223
+ ### `@uxf/analytics/consent`
224
+
225
+ | Export | Signature | Description |
226
+ | --- | --- | --- |
227
+ | `storeConsentToCookie` | `(consent: CookiesConsentType, version: number, cookieTtl?: number) => void` | Writes the consent cookie (default TTL 90 days). Throws on the server. |
228
+ | `readConsentFromCookie` | `(ctx?: AnyObject \| null) => CookieConsentTypeWithVersion` | Reads and decodes the consent cookie. Pass a request-like `ctx` to read server-side. |
229
+ | `isConsentCookieSet` | `(ctx: AnyObject \| null, version: number) => boolean` | `true` when all four flags are set and the stored version matches. |
230
+ | `CookiesConsentType` | `{ ad_personalization?, ad_storage?, ad_user_data?, analytics_storage?: boolean }` | The four Google consent flags. |
231
+ | `CookieConsentTypeWithVersion` | `CookiesConsentType & { version: number }` | Shape stored in the cookie. |
232
+
233
+ ### `@uxf/analytics/gtm`
234
+
235
+ | Export | Signature | Description |
236
+ | --- | --- | --- |
237
+ | `useGtmScript` | `(gtmId: string) => string` | Builds the inline GTM bootstrap script (default consent from the cookie + container loader). |
238
+ | `updateGtmConsent` | `(consent: CookiesConsentType, version: number) => void` | Stores consent and pushes gtag `consent: "update"` + `consent_resolved`. Client-only. |
239
+ | `ConsentType` | `"granted" \| "denied"` | gtag consent value. |
240
+ | `GtmConsentData` | type | gtag consent payload / event union. |
241
+ | `GtmDataLayer` | `{ push: (gtmEventData: unknown) => void }` | `window.dataLayer` shape (augments `Window`). |
242
+
243
+ ### `@uxf/analytics/ab-testing`
244
+
245
+ | Export | Signature | Description |
246
+ | --- | --- | --- |
247
+ | `handleABTesting` | `(request, response, experiments: ExperimentConfig[], options?: { domain?: string }) => void` | Proxy/middleware: assigns and prunes experiment cookies. |
248
+ | `getExperimentVariant` | `(config: ExperimentConfig, randomNumberForTesting?: number \| null) => ExperimentVariant \| null` | Picks a variant by weighted traffic; `null` = not participating. |
249
+ | `getExperimentsFromContext` | `(cookies: Partial<{ [key: string]: string }>) => [string, string][]` | Extracts `[id, variant]` pairs from a server cookie map. |
250
+ | `getExperimentsFromClient` | `() => [string, string][]` | Extracts `[id, variant]` pairs from `document.cookie`. |
251
+ | `addExperimentsSSR` | `(ctx, pageProps) => pageProps` | Pages Router `getServerSideProps` helper; injects variants under `AB_TESTING_VARIANT_PROP_NAME`. |
252
+ | `sendABTestingEvent` | `(getExpVariantString?: GetExpVariantString) => void` | Pushes an `experience_impression` GTM event per experiment cookie. Client-only. |
253
+ | `ABTestingProvider` | `(props: { children; experiments: [string, string][]; getExpVariantString? }) => JSX` | Provides variants to the tree and fires `sendABTestingEvent` on mount. |
254
+ | `useABTesting` | `() => [string, string][]` | Returns all `[id, variant]` pairs from context. |
255
+ | `useABTestingVariant` | `<Config extends ExperimentConfig[]>(experimentId) => variantName \| null` | Returns the variant name for one experiment, or `null`. |
256
+ | `AB_TESTING_VARIANT_PROP_NAME` | `"__AB_TESTING_VARIANT__"` | Page-prop key used by `addExperimentsSSR`. |
257
+ | `EXPERIMENT_COOKIE_PREFIX` | `"uxf-experiment-"` | Prefix of every experiment cookie. |
258
+ | `ExperimentVariant` | `{ name: string; traffic: number; label?: string }` | A single variant. |
259
+ | `ExperimentConfig` | `{ id: string; traffic: number; variants: ExperimentVariant[] }` | One experiment. |
260
+ | `GetExpVariantString` | `(cookie: { name: string; value: string }) => string` | Maps a cookie to the `exp_variant_string` sent to GTM. |
261
+
262
+ ## Gotchas
263
+
264
+ - No root import — always import from `@uxf/analytics/consent`, `/gtm`, or `/ab-testing`.
265
+ - `storeConsentToCookie` throws when `window` is undefined (server). `updateGtmConsent` and `sendABTestingEvent` also run on the client only.
266
+ - `readConsentFromCookie` works on both sides — pass a request-like `ctx` to read server-side; omit it on the client.
267
+ - `ABTestingProvider`, `useABTesting`, and `useABTestingVariant` are client components (`"use client"`).
268
+ - Consent is stored base64-encoded in a single `cookieConsent` cookie; the `version` argument lets you re-request consent — `isConsentCookieSet` returns `false` on a version mismatch.
269
+ - Experiment cookies are prefixed `uxf-experiment-`; excluded users get the value `not-participate`, so a variant is only meaningful when it matches a configured `variants[].name`.
270
+ - The provider `experiments` prop is `[experimentId, variantName][]` — build it with `getExperimentsFromContext` (server) or `getExperimentsFromClient` (client), not the raw `ExperimentConfig[]`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/analytics",
3
- "version": "11.120.0",
3
+ "version": "11.122.4",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "build": "tsc -P tsconfig.json",
@@ -16,13 +16,13 @@
16
16
  "license": "ISC",
17
17
  "peerDependencies": {
18
18
  "@uxf/core": "11.114.0",
19
- "@uxf/core-react": "11.120.0",
19
+ "@uxf/core-react": "11.122.2",
20
20
  "react": ">=18.0.2"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/react": "18.3.27",
24
24
  "@uxf/core": "11.114.0",
25
- "@uxf/core-react": "11.120.0",
25
+ "@uxf/core-react": "11.122.2",
26
26
  "react": "18.3.1"
27
27
  }
28
28
  }