@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.
- package/README.md +143 -51
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@uxf/analytics)
|
|
5
5
|
[](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
|
|
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
|
|
62
|
+
### Check if consent is set
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
Returns `true` only when all four flags are booleans and the stored `version` matches.
|
|
46
65
|
|
|
47
|
-
```
|
|
66
|
+
```ts
|
|
48
67
|
import { isConsentCookieSet } from "@uxf/analytics/consent";
|
|
49
68
|
|
|
50
|
-
const isSet =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
##
|
|
105
|
+
## A/B testing
|
|
99
106
|
|
|
100
|
-
A set of
|
|
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.
|
|
123
|
-
{ name: "B", traffic: 0.
|
|
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.
|
|
136
|
+
#### 2. Assign variants in `proxy.ts`
|
|
130
137
|
|
|
131
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
return <div>Experiment 1 variant: {variant}</div>; // "Control" | "B" | null
|
|
216
|
+
}
|
|
175
217
|
|
|
176
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
+
"@uxf/core-react": "11.122.2",
|
|
26
26
|
"react": "18.3.1"
|
|
27
27
|
}
|
|
28
28
|
}
|