@dotcms/experiments 0.0.1-alpha.37 → 0.0.1-alpha.39
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/.babelrc +12 -0
- package/.eslintrc.json +26 -0
- package/jest.config.ts +11 -0
- package/package.json +4 -8
- package/project.json +55 -0
- package/src/lib/components/{DotExperimentHandlingComponent.d.ts → DotExperimentHandlingComponent.tsx} +20 -3
- package/src/lib/components/DotExperimentsProvider.spec.tsx +62 -0
- package/src/lib/components/{DotExperimentsProvider.d.ts → DotExperimentsProvider.tsx} +41 -3
- package/src/lib/components/withExperiments.tsx +52 -0
- package/src/lib/contexts/DotExperimentsContext.spec.tsx +42 -0
- package/src/lib/contexts/{DotExperimentsContext.d.ts → DotExperimentsContext.tsx} +5 -2
- package/src/lib/dot-experiments.spec.ts +285 -0
- package/src/lib/dot-experiments.ts +716 -0
- package/src/lib/hooks/useExperimentVariant.spec.tsx +111 -0
- package/src/lib/hooks/useExperimentVariant.ts +55 -0
- package/src/lib/hooks/useExperiments.ts +90 -0
- package/src/lib/shared/{constants.d.ts → constants.ts} +35 -18
- package/src/lib/shared/mocks/mock.ts +209 -0
- package/src/lib/shared/{models.d.ts → models.ts} +35 -2
- package/src/lib/shared/parser/parse.spec.ts +187 -0
- package/src/lib/shared/parser/parser.ts +171 -0
- package/src/lib/shared/persistence/index-db-database-handler.spec.ts +100 -0
- package/src/lib/shared/persistence/index-db-database-handler.ts +218 -0
- package/src/lib/shared/utils/DotLogger.ts +57 -0
- package/src/lib/shared/utils/memoize.spec.ts +49 -0
- package/src/lib/shared/utils/memoize.ts +49 -0
- package/src/lib/shared/utils/utils.spec.ts +142 -0
- package/src/lib/shared/utils/utils.ts +203 -0
- package/src/lib/standalone.spec.ts +36 -0
- package/src/lib/standalone.ts +28 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +20 -0
- package/tsconfig.spec.json +9 -0
- package/vite.config.ts +41 -0
- package/index.esm.d.ts +0 -1
- package/index.esm.js +0 -7174
- package/src/lib/components/withExperiments.d.ts +0 -20
- package/src/lib/dot-experiments.d.ts +0 -289
- package/src/lib/hooks/useExperimentVariant.d.ts +0 -21
- package/src/lib/hooks/useExperiments.d.ts +0 -14
- package/src/lib/shared/mocks/mock.d.ts +0 -43
- package/src/lib/shared/parser/parser.d.ts +0 -54
- package/src/lib/shared/persistence/index-db-database-handler.d.ts +0 -87
- package/src/lib/shared/utils/DotLogger.d.ts +0 -15
- package/src/lib/shared/utils/memoize.d.ts +0 -7
- package/src/lib/shared/utils/utils.d.ts +0 -73
- package/src/lib/standalone.d.ts +0 -7
- /package/src/{index.d.ts → index.ts} +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXPERIMENT_ALLOWED_DATA_ATTRIBUTES,
|
|
3
|
+
EXPERIMENT_ALREADY_CHECKED_KEY,
|
|
4
|
+
EXPERIMENT_FETCH_EXPIRE_TIME_KEY,
|
|
5
|
+
EXPERIMENT_QUERY_PARAM_KEY,
|
|
6
|
+
EXPERIMENT_SCRIPT_FILE_NAME
|
|
7
|
+
} from '../constants';
|
|
8
|
+
import { DotExperimentConfig, Experiment, Variant } from '../models';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the first script element that includes the experiment script identifier.
|
|
12
|
+
*
|
|
13
|
+
* @return {HTMLScriptElement|undefined} - The found script element or undefined if none is found.
|
|
14
|
+
*/
|
|
15
|
+
export const getExperimentScriptTag = (): HTMLScriptElement => {
|
|
16
|
+
const experimentScript = Array.from(document.getElementsByTagName('script')).find((script) =>
|
|
17
|
+
script.src.includes(EXPERIMENT_SCRIPT_FILE_NAME)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!experimentScript) {
|
|
21
|
+
throw new Error('Experiment script not found');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return experimentScript;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Retrieves experiment attributes from a given script element.
|
|
29
|
+
*
|
|
30
|
+
*
|
|
31
|
+
* @return {DotExperimentConfig | null} - The experiment attributes or null if there are no valid attributes present.
|
|
32
|
+
*/
|
|
33
|
+
export const getDataExperimentAttributes = (location: Location): DotExperimentConfig | null => {
|
|
34
|
+
const script = getExperimentScriptTag();
|
|
35
|
+
|
|
36
|
+
const defaultExperimentAttributes: DotExperimentConfig = {
|
|
37
|
+
apiKey: '',
|
|
38
|
+
server: location.href,
|
|
39
|
+
debug: false
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let experimentAttribute: Partial<DotExperimentConfig> = {};
|
|
43
|
+
|
|
44
|
+
if (!script.hasAttribute('data-experiment-api-key')) {
|
|
45
|
+
throw new Error('You need specify the `data-experiment-api-key`');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Array.from(script.attributes).forEach((attr) => {
|
|
49
|
+
if (EXPERIMENT_ALLOWED_DATA_ATTRIBUTES.includes(attr.name)) {
|
|
50
|
+
// Server of dotCMS
|
|
51
|
+
if (attr.name === 'data-experiment-server') {
|
|
52
|
+
experimentAttribute = { ...experimentAttribute, server: attr.value };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Api Key for Analytics App
|
|
56
|
+
if (attr.name === 'data-experiment-api-key') {
|
|
57
|
+
experimentAttribute = { ...experimentAttribute, apiKey: attr.value };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Show debug
|
|
61
|
+
if (attr.name === 'data-experiment-debug') {
|
|
62
|
+
experimentAttribute = {
|
|
63
|
+
...experimentAttribute,
|
|
64
|
+
debug: true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { ...defaultExperimentAttributes, ...experimentAttribute };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Retrieves the data attributes from the experiment script tag.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* Given the custom script tag in your HTML:
|
|
78
|
+
* <script src="/dot-experiments.iife.js"
|
|
79
|
+
* defer=""
|
|
80
|
+
* data-experiment-api-key="api-token"
|
|
81
|
+
* data-experiment-server="http://localhost:8080/"
|
|
82
|
+
* data-experiment-debug>
|
|
83
|
+
* </script>
|
|
84
|
+
*
|
|
85
|
+
* @returns {DotExperimentConfig | null} The data attributes of the experiment script tag, or null if no experiment script is found.
|
|
86
|
+
*/
|
|
87
|
+
export const getScriptDataAttributes = (location: Location): DotExperimentConfig | null => {
|
|
88
|
+
const dataExperimentAttributes = getDataExperimentAttributes(location);
|
|
89
|
+
|
|
90
|
+
if (dataExperimentAttributes) {
|
|
91
|
+
return dataExperimentAttributes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Checks the flag indicating whether the experiment has already been checked.
|
|
99
|
+
*
|
|
100
|
+
* @function checkFlagExperimentAlreadyChecked
|
|
101
|
+
* @returns {boolean} - returns true if experiment has already been checked, otherwise false.
|
|
102
|
+
*/
|
|
103
|
+
export const checkFlagExperimentAlreadyChecked = (): boolean => {
|
|
104
|
+
const flag = sessionStorage.getItem(EXPERIMENT_ALREADY_CHECKED_KEY);
|
|
105
|
+
|
|
106
|
+
return flag === 'true';
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Checks if the data needs to be invalidated based on the creation date.
|
|
111
|
+
*
|
|
112
|
+
* @returns {boolean} - True if the data needs to be invalidated, false otherwise.
|
|
113
|
+
*/
|
|
114
|
+
export const isDataCreateValid = (): boolean => {
|
|
115
|
+
try {
|
|
116
|
+
const timeValidUntil = Number(localStorage.getItem(EXPERIMENT_FETCH_EXPIRE_TIME_KEY));
|
|
117
|
+
|
|
118
|
+
if (isNaN(timeValidUntil)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
return timeValidUntil > now;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Ad to an absolute path the baseUrl depending on the location.
|
|
132
|
+
*
|
|
133
|
+
* @param {string | null} absolutePath - The absolute path of the URL.
|
|
134
|
+
* @param {Location} location - The location object representing the current URL.
|
|
135
|
+
* @returns {string | null} - The full URL or null if absolutePath is null.
|
|
136
|
+
*/
|
|
137
|
+
export const getFullUrl = (location: Location, absolutePath: string | null): string | null => {
|
|
138
|
+
if (absolutePath === null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!isFullUrl(absolutePath)) {
|
|
143
|
+
const baseUrl = location.origin;
|
|
144
|
+
|
|
145
|
+
return `${baseUrl}${absolutePath}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return absolutePath;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const isFullUrl = (url: string): boolean => {
|
|
152
|
+
const pattern = /^https?:\/\//i;
|
|
153
|
+
|
|
154
|
+
return pattern.test(url);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Updates the URL with the queryParam with the experiment variant name.
|
|
159
|
+
*
|
|
160
|
+
* @param {Location|string} location - The current location object or the URL string.
|
|
161
|
+
* @param {Variant} variant - The experiment variant to update the URL with.
|
|
162
|
+
* @returns {string} The updated URL string.
|
|
163
|
+
*/
|
|
164
|
+
export const updateUrlWithExperimentVariant = (
|
|
165
|
+
location: Location | string,
|
|
166
|
+
variant: Variant | null
|
|
167
|
+
): string => {
|
|
168
|
+
const href = typeof location === 'string' ? location : location.href;
|
|
169
|
+
|
|
170
|
+
const url = new URL(href);
|
|
171
|
+
|
|
172
|
+
if (variant !== null) {
|
|
173
|
+
const params = url.searchParams;
|
|
174
|
+
|
|
175
|
+
params.set(EXPERIMENT_QUERY_PARAM_KEY, variant.name);
|
|
176
|
+
url.search = params.toString();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return url.toString();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if two arrays of Experiment objects are equal.
|
|
184
|
+
*
|
|
185
|
+
* @param {Experiment[]} obj1 - The first array of Experiment objects.
|
|
186
|
+
* @param {Experiment[]} obj2 - The second array of Experiment objects.
|
|
187
|
+
* @return {boolean} - True if the arrays are equal, false otherwise.
|
|
188
|
+
*/
|
|
189
|
+
export const objectsAreEqual = (obj1: Experiment[], obj2: Experiment[]): boolean => {
|
|
190
|
+
if (obj1.length === 0 && obj2.length === 0) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A function to redirect the user to a new URL.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} href - The URL to redirect to.
|
|
201
|
+
* @returns {void}
|
|
202
|
+
*/
|
|
203
|
+
export const defaultRedirectFn = (href: string) => (window.location.href = href);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DotExperiments } from './dot-experiments';
|
|
2
|
+
import { EXPERIMENT_WINDOWS_KEY } from './shared/constants';
|
|
3
|
+
import { getScriptDataAttributes } from './shared/utils/utils';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
[EXPERIMENT_WINDOWS_KEY]: DotExperiments;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
jest.mock('./shared/utils/utils', () => ({
|
|
12
|
+
getScriptDataAttributes: jest.fn().mockReturnValue({ server: 'http://localhost' }),
|
|
13
|
+
Logger: jest.fn()
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('IIFE Execution', () => {
|
|
17
|
+
it('should call getScriptDataAttributes and set window[EXPERIMENT_WINDOWS_KEY]', () => {
|
|
18
|
+
const fakeInstance = {
|
|
19
|
+
initialize: jest.fn()
|
|
20
|
+
} as unknown as DotExperiments;
|
|
21
|
+
|
|
22
|
+
const getInstanceMock = jest
|
|
23
|
+
.spyOn(DotExperiments, 'getInstance')
|
|
24
|
+
.mockReturnValue(fakeInstance);
|
|
25
|
+
|
|
26
|
+
require('./standalone');
|
|
27
|
+
|
|
28
|
+
expect(getScriptDataAttributes).toHaveBeenCalled();
|
|
29
|
+
|
|
30
|
+
expect(getInstanceMock).toHaveBeenCalledWith({ server: 'http://localhost' });
|
|
31
|
+
expect(getInstanceMock).toHaveBeenCalled();
|
|
32
|
+
|
|
33
|
+
expect(window[EXPERIMENT_WINDOWS_KEY]).toBeDefined();
|
|
34
|
+
expect(window[EXPERIMENT_WINDOWS_KEY]).toEqual(fakeInstance);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DotExperiments } from './dot-experiments';
|
|
2
|
+
import { EXPERIMENT_WINDOWS_KEY } from './shared/constants';
|
|
3
|
+
import { getScriptDataAttributes } from './shared/utils/utils';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
[EXPERIMENT_WINDOWS_KEY]: DotExperiments;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This file sets up everything necessary to run an Experiment in Standalone Mode(Immediately Invoked Function Expressions).
|
|
13
|
+
* It checks which experiments are currently running, and generates the essential data
|
|
14
|
+
* needed for storing in DotCMS for subsequent A/B Testing validation.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
if (window) {
|
|
18
|
+
// TODO: make this file buildable by task and publish to dotCMS/src/main/webapp/html
|
|
19
|
+
try {
|
|
20
|
+
const dataAttributes = getScriptDataAttributes(window.location);
|
|
21
|
+
|
|
22
|
+
if (dataAttributes) {
|
|
23
|
+
window[EXPERIMENT_WINDOWS_KEY] = DotExperiments.getInstance({ ...dataAttributes });
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`Error instancing DotExperiments: ${error}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"esModuleInterop": false,
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"strict": true
|
|
9
|
+
},
|
|
10
|
+
"files": [],
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"references": [
|
|
13
|
+
{
|
|
14
|
+
"path": "./tsconfig.lib.json"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "./tsconfig.spec.json"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"types": ["node", "vite/client"]
|
|
7
|
+
},
|
|
8
|
+
"exclude": [
|
|
9
|
+
"jest.config.ts",
|
|
10
|
+
"src/**/*.spec.ts",
|
|
11
|
+
"src/**/*.test.ts",
|
|
12
|
+
"src/**/*.spec.tsx",
|
|
13
|
+
"src/**/*.test.tsx",
|
|
14
|
+
"src/**/*.spec.js",
|
|
15
|
+
"src/**/*.test.js",
|
|
16
|
+
"src/**/*.spec.jsx",
|
|
17
|
+
"src/**/*.test.jsx"
|
|
18
|
+
],
|
|
19
|
+
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
20
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import dts from 'vite-plugin-dts';
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
root: __dirname,
|
|
9
|
+
cacheDir: '../../../node_modules/.vite/libs/sdk/experiments',
|
|
10
|
+
|
|
11
|
+
plugins: [
|
|
12
|
+
nxViteTsPaths(),
|
|
13
|
+
dts({
|
|
14
|
+
entryRoot: 'src',
|
|
15
|
+
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
|
|
16
|
+
skipDiagnostics: true
|
|
17
|
+
})
|
|
18
|
+
],
|
|
19
|
+
|
|
20
|
+
// Configuration for building the DotExperiment as IIFE file to use in
|
|
21
|
+
// plain HTML or other projects.
|
|
22
|
+
build: {
|
|
23
|
+
outDir: '../../../dist/libs/sdk/experiments',
|
|
24
|
+
reportCompressedSize: true,
|
|
25
|
+
emptyOutDir: true,
|
|
26
|
+
commonjsOptions: {
|
|
27
|
+
transformMixedEsModules: true
|
|
28
|
+
},
|
|
29
|
+
minify: 'terser',
|
|
30
|
+
lib: {
|
|
31
|
+
entry: 'src/lib/standalone.ts',
|
|
32
|
+
name: 'DotExperiment',
|
|
33
|
+
fileName: 'dot-experiments.min',
|
|
34
|
+
formats: ['iife']
|
|
35
|
+
},
|
|
36
|
+
rollupOptions: {
|
|
37
|
+
// External packages that should not be bundled into your library.
|
|
38
|
+
external: []
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
package/index.esm.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./src/index";
|