@uxf/analytics 11.52.2 → 11.53.0

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
@@ -59,12 +59,21 @@ const isSet = isCookieConsentSet(
59
59
 
60
60
  This reads the consent from cookie and initializes the GTM dataLayer.
61
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
70
+
62
71
  ```tsx
63
72
  import { useGtmScript } from "@uxf/analytics/gtm";
64
73
 
65
74
  const gtmScript = useGtmScript("GTM-YOURID");
66
75
 
67
- // in your head component
76
+ // in your head component (not directly in _app)
68
77
  <script dangerouslySetInnerHTML={{ __html: gtmScript }} />
69
78
  ```
70
79
 
@@ -84,4 +93,85 @@ updateGtmConsent(
84
93
  },
85
94
  1
86
95
  );
87
- ```
96
+ ```
97
+
98
+ ## AB testing
99
+
100
+ A set of components and helpers prepared to implement AB testing to React & NextJS applications.
101
+
102
+ ### How to start
103
+
104
+ #### 1. Define your experiments
105
+
106
+ ```ts
107
+ import type { ExperimentConfig } from "@uxf/analytics/ab-testing";
108
+
109
+ const experiments = [
110
+ {
111
+ id: "1",
112
+ traffic: 1,
113
+ variants: [
114
+ { name: "Control", traffic: 0.5 },
115
+ { name: "B", traffic: 0.5 },
116
+ ],
117
+ },
118
+ {
119
+ id: "2",
120
+ traffic: 0.5,
121
+ variants: [
122
+ { name: "Control", traffic: 0.25 },
123
+ { name: "B", traffic: 0.75 },
124
+ ],
125
+ },
126
+ ] as const satisfies ExperimentConfig[];
127
+ ```
128
+
129
+ #### 2. Use the `ABTestingProvider` component
130
+
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 `middleware.ts`
144
+
145
+ ```ts
146
+ import { handleABTesting } from "@uxf/analytics/ab-testing";
147
+
148
+ export async function middleware(request: NextRequest) {
149
+ const nextResponse = NextResponse.next();
150
+
151
+ handleABTesting(request, nextResponse, experiments);
152
+
153
+ return nextResponse;
154
+ }
155
+ ```
156
+
157
+ #### 4. Implement SSR support in page with AB testing
158
+
159
+ ```tsx
160
+ import { addExperimentsSSR } from "@uxf/analytics/ab-testing";
161
+
162
+ export const getServerSideProps: GetServerSideProps = async (ctx) => {
163
+ return addExperimentsSSR(ctx, {
164
+ props: {},
165
+ });
166
+ };
167
+ ```
168
+
169
+ #### 5. Use the `useABTestingVariant` hook in your component
170
+
171
+ ```tsx
172
+ import { useABTestingVariant } from "@uxf/analytics/ab-testing";
173
+
174
+ const abTestingVariant = useABTestingVariant<typeof experiments>("1");
175
+
176
+ console.log(abTestingVariant); // "Control", "B", etc.
177
+ ```
@@ -0,0 +1,4 @@
1
+ export * from "../src/ab-testing/ab-testing";
2
+ export * from "../src/ab-testing/ab-testing-context";
3
+ export * from "../src/ab-testing/constants";
4
+ export type * from "../src/ab-testing/types";
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("../src/ab-testing/ab-testing"), exports);
18
+ __exportStar(require("../src/ab-testing/ab-testing-context"), exports);
19
+ __exportStar(require("../src/ab-testing/constants"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/analytics",
3
- "version": "11.52.2",
3
+ "version": "11.53.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "build": "tsc -P tsconfig.json",
@@ -15,7 +15,7 @@
15
15
  "author": "",
16
16
  "license": "ISC",
17
17
  "dependencies": {
18
- "@uxf/core": "11.52.2"
18
+ "@uxf/core": "11.53.0"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=18.0.2"
@@ -0,0 +1,10 @@
1
+ import React, { type ReactNode } from "react";
2
+ import type { ExperimentConfig } from "./types";
3
+ interface Props {
4
+ experiments: [string, string][];
5
+ children: ReactNode;
6
+ }
7
+ export declare function ABTestingProvider(props: Props): React.JSX.Element;
8
+ export declare function useABTesting(): [string, string][];
9
+ export declare function useABTestingVariant<Config extends ExperimentConfig[]>(experimentId: Config[number]["id"]): Config[number]["variants"][number]["name"] | null;
10
+ export {};
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ "use client";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || function (mod) {
20
+ if (mod && mod.__esModule) return mod;
21
+ var result = {};
22
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
+ __setModuleDefault(result, mod);
24
+ return result;
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.ABTestingProvider = ABTestingProvider;
28
+ exports.useABTesting = useABTesting;
29
+ exports.useABTestingVariant = useABTestingVariant;
30
+ const use_on_mount_1 = require("@uxf/core-react/hooks/use-on-mount");
31
+ const react_1 = __importStar(require("react"));
32
+ const ab_testing_1 = require("./ab-testing");
33
+ const ABTestingContext = (0, react_1.createContext)([]);
34
+ function ABTestingProvider(props) {
35
+ (0, use_on_mount_1.useOnMount)(() => {
36
+ (0, ab_testing_1.sendABTestingEvent)();
37
+ });
38
+ return react_1.default.createElement(ABTestingContext.Provider, { value: props.experiments }, props.children);
39
+ }
40
+ function useABTesting() {
41
+ return (0, react_1.useContext)(ABTestingContext);
42
+ }
43
+ function useABTestingVariant(experimentId) {
44
+ var _a;
45
+ const experiments = useABTesting();
46
+ const experiment = experiments.find(([name]) => name === experimentId);
47
+ return ((_a = experiment === null || experiment === void 0 ? void 0 : experiment.at(1)) !== null && _a !== void 0 ? _a : null);
48
+ }
@@ -0,0 +1,36 @@
1
+ import type { ExperimentConfig, ExperimentVariant } from "./types";
2
+ type NextRequest = {
3
+ cookies: {
4
+ get: (cookieName: string) => {
5
+ name: string;
6
+ value: string;
7
+ } | undefined;
8
+ };
9
+ };
10
+ type NextResponse = {
11
+ cookies: {
12
+ set: (cookie: {
13
+ name: string;
14
+ value: string;
15
+ maxAge: number;
16
+ }) => void;
17
+ };
18
+ };
19
+ type GetServerSidePropsContext = {
20
+ req: {
21
+ cookies: Partial<{
22
+ [key: string]: string;
23
+ }>;
24
+ };
25
+ };
26
+ type GetServerSidePropsResult<P> = {
27
+ props: P;
28
+ };
29
+ export declare function getExperimentVariant(config: ExperimentConfig, randomNumberForTesting?: number | null): ExperimentVariant | null;
30
+ export declare function handleABTesting(request: NextRequest, response: NextResponse, experiments: ExperimentConfig[]): void;
31
+ export declare function getExperimentsFromContext(cookies: Partial<{
32
+ [p: string]: string;
33
+ }>): [string, string][];
34
+ export declare function addExperimentsSSR<P>(ctx: GetServerSidePropsContext, pageProps: GetServerSidePropsResult<P>): GetServerSidePropsResult<P>;
35
+ export declare function sendABTestingEvent(): void;
36
+ export {};
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getExperimentVariant = getExperimentVariant;
4
+ exports.handleABTesting = handleABTesting;
5
+ exports.getExperimentsFromContext = getExperimentsFromContext;
6
+ exports.addExperimentsSSR = addExperimentsSSR;
7
+ exports.sendABTestingEvent = sendABTestingEvent;
8
+ const is_not_nil_1 = require("@uxf/core/utils/is-not-nil");
9
+ const gtm_event_1 = require("../gtm/gtm-event");
10
+ const constants_1 = require("./constants");
11
+ function getExperimentVariant(config, randomNumberForTesting = null) {
12
+ const randomNumber = randomNumberForTesting !== null && randomNumberForTesting !== void 0 ? randomNumberForTesting : Math.random();
13
+ const trafficWithoutABTesting = Math.fround(1 - config.traffic);
14
+ if (randomNumber <= trafficWithoutABTesting) {
15
+ return null;
16
+ }
17
+ let sumTraffic = trafficWithoutABTesting;
18
+ for (const variant of config.variants) {
19
+ sumTraffic += variant.traffic * config.traffic;
20
+ if (randomNumber <= sumTraffic) {
21
+ return variant;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function getCookieName(experimentId) {
27
+ return `${constants_1.EXPERIMENT_COOKIE_PREFIX}${experimentId}`;
28
+ }
29
+ function handleExperimentCookie(request, response, experimentConfig) {
30
+ var _a;
31
+ const cookieValue = request.cookies.get(getCookieName(experimentConfig.id));
32
+ if (cookieValue) {
33
+ return;
34
+ }
35
+ const experimentVariant = getExperimentVariant(experimentConfig);
36
+ response.cookies.set({
37
+ name: getCookieName(experimentConfig.id),
38
+ value: (_a = experimentVariant === null || experimentVariant === void 0 ? void 0 : experimentVariant.name) !== null && _a !== void 0 ? _a : "not-participate",
39
+ maxAge: 60 * 60 * 24 * 365, // 365 days
40
+ });
41
+ }
42
+ function handleABTesting(request, response, experiments) {
43
+ experiments.forEach((experimentConfig) => handleExperimentCookie(request, response, experimentConfig));
44
+ }
45
+ function getExperimentsFromContext(cookies) {
46
+ return Object.entries(cookies)
47
+ .filter(([cookieName]) => cookieName.startsWith(constants_1.EXPERIMENT_COOKIE_PREFIX))
48
+ .filter(([, cookieValue]) => (0, is_not_nil_1.isNotNil)(cookieValue))
49
+ .map(([cookieName, cookieValue]) => [cookieName.replace(constants_1.EXPERIMENT_COOKIE_PREFIX, ""), cookieValue]);
50
+ }
51
+ function addExperimentsSSR(ctx, pageProps) {
52
+ if (pageProps.props !== undefined) {
53
+ pageProps.props[constants_1.AB_TESTING_VARIANT_PROP_NAME] = getExperimentsFromContext(ctx.req.cookies);
54
+ }
55
+ return pageProps;
56
+ }
57
+ function sendABTestingEvent() {
58
+ if (typeof document === "undefined") {
59
+ // eslint-disable-next-line no-console
60
+ console.error("Document is undefined. This method can be called only on client.");
61
+ return;
62
+ }
63
+ if (typeof window.dataLayer === "undefined") {
64
+ // eslint-disable-next-line no-console
65
+ console.error("DataLayer is undefined. GTM must be implemented.");
66
+ return;
67
+ }
68
+ decodeURIComponent(document.cookie)
69
+ .split(";")
70
+ .map((cookie) => cookie.trim().split("="))
71
+ .map(([cookieName, cookieValue]) => ({ name: cookieName, value: cookieValue }))
72
+ .forEach((cookie) => {
73
+ if (cookie.name.startsWith(constants_1.EXPERIMENT_COOKIE_PREFIX) && typeof window !== "undefined") {
74
+ (0, gtm_event_1.gtmEvent)({
75
+ event: "experience_impression",
76
+ exp_variant_string: `${cookie.name}-${cookie.value}`,
77
+ });
78
+ }
79
+ });
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ab_testing_1 = require("./ab-testing");
4
+ const A = { name: "A", traffic: 0.1 };
5
+ const B = { name: "B", traffic: 0.3 };
6
+ const C = { name: "C", traffic: 0.6 };
7
+ const config = {
8
+ id: "experiment-1",
9
+ traffic: 0.8,
10
+ variants: [A, B, C],
11
+ };
12
+ /**
13
+ * 100% --- 20% | 0.20
14
+ * \-- 80% --- 10% (0.8 * 0.1 = 0.08) | 0.28
15
+ * \-- 30% (0.8 * 0.3 = 0.24) | 0.52
16
+ * \- 60% (0.8 * 0.6 = 0.48) | 0.100
17
+ */
18
+ test("getExperimentVariant", () => {
19
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0)).toBe(null);
20
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.1)).toBe(null);
21
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.2)).toBe(null);
22
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.21)).toBe(A);
23
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.28)).toBe(A);
24
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.29)).toBe(B);
25
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.52)).toBe(B);
26
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.53)).toBe(C);
27
+ expect((0, ab_testing_1.getExperimentVariant)(config, 0.99)).toBe(C);
28
+ expect((0, ab_testing_1.getExperimentVariant)(config, 1)).toBe(C);
29
+ // tohle by nemělo nastat
30
+ expect((0, ab_testing_1.getExperimentVariant)(config, 1.1)).toBe(null);
31
+ });
@@ -0,0 +1,2 @@
1
+ export declare const EXPERIMENT_COOKIE_PREFIX = "uxf-experiment-";
2
+ export declare const AB_TESTING_VARIANT_PROP_NAME = "__AB_TESTING_VARIANT__";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AB_TESTING_VARIANT_PROP_NAME = exports.EXPERIMENT_COOKIE_PREFIX = void 0;
4
+ exports.EXPERIMENT_COOKIE_PREFIX = "uxf-experiment-";
5
+ exports.AB_TESTING_VARIANT_PROP_NAME = "__AB_TESTING_VARIANT__";
@@ -0,0 +1,10 @@
1
+ export interface ExperimentVariant {
2
+ name: string;
3
+ traffic: number;
4
+ label?: string;
5
+ }
6
+ export interface ExperimentConfig {
7
+ id: string;
8
+ traffic: number;
9
+ variants: Array<ExperimentVariant>;
10
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function gtmEvent(gtmEventData: any): void;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gtmEvent = gtmEvent;
4
+ function gtmEvent(gtmEventData) {
5
+ if (typeof window.dataLayer === "undefined") {
6
+ window.dataLayer = [];
7
+ }
8
+ window.dataLayer.push(gtmEventData);
9
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ interface Props {
3
+ gtmId: string;
4
+ }
5
+ export declare function GtmScript(props: Props): React.JSX.Element;
6
+ export {};
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GtmScript = GtmScript;
7
+ const is_server_1 = require("@uxf/core/utils/is-server");
8
+ const react_1 = __importDefault(require("react"));
9
+ const use_gtm_script_1 = require("./use-gtm-script");
10
+ function GtmScript(props) {
11
+ const gtmScript = (0, use_gtm_script_1.useGtmScript)(props.gtmId);
12
+ return react_1.default.createElement(react_1.default.Fragment, null, !is_server_1.isServer && react_1.default.createElement("script", { dangerouslySetInnerHTML: { __html: gtmScript } }));
13
+ }