@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.
Files changed (48) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +26 -0
  3. package/jest.config.ts +11 -0
  4. package/package.json +4 -8
  5. package/project.json +55 -0
  6. package/src/lib/components/{DotExperimentHandlingComponent.d.ts → DotExperimentHandlingComponent.tsx} +20 -3
  7. package/src/lib/components/DotExperimentsProvider.spec.tsx +62 -0
  8. package/src/lib/components/{DotExperimentsProvider.d.ts → DotExperimentsProvider.tsx} +41 -3
  9. package/src/lib/components/withExperiments.tsx +52 -0
  10. package/src/lib/contexts/DotExperimentsContext.spec.tsx +42 -0
  11. package/src/lib/contexts/{DotExperimentsContext.d.ts → DotExperimentsContext.tsx} +5 -2
  12. package/src/lib/dot-experiments.spec.ts +285 -0
  13. package/src/lib/dot-experiments.ts +716 -0
  14. package/src/lib/hooks/useExperimentVariant.spec.tsx +111 -0
  15. package/src/lib/hooks/useExperimentVariant.ts +55 -0
  16. package/src/lib/hooks/useExperiments.ts +90 -0
  17. package/src/lib/shared/{constants.d.ts → constants.ts} +35 -18
  18. package/src/lib/shared/mocks/mock.ts +209 -0
  19. package/src/lib/shared/{models.d.ts → models.ts} +35 -2
  20. package/src/lib/shared/parser/parse.spec.ts +187 -0
  21. package/src/lib/shared/parser/parser.ts +171 -0
  22. package/src/lib/shared/persistence/index-db-database-handler.spec.ts +100 -0
  23. package/src/lib/shared/persistence/index-db-database-handler.ts +218 -0
  24. package/src/lib/shared/utils/DotLogger.ts +57 -0
  25. package/src/lib/shared/utils/memoize.spec.ts +49 -0
  26. package/src/lib/shared/utils/memoize.ts +49 -0
  27. package/src/lib/shared/utils/utils.spec.ts +142 -0
  28. package/src/lib/shared/utils/utils.ts +203 -0
  29. package/src/lib/standalone.spec.ts +36 -0
  30. package/src/lib/standalone.ts +28 -0
  31. package/tsconfig.json +20 -0
  32. package/tsconfig.lib.json +20 -0
  33. package/tsconfig.spec.json +9 -0
  34. package/vite.config.ts +41 -0
  35. package/index.esm.d.ts +0 -1
  36. package/index.esm.js +0 -7174
  37. package/src/lib/components/withExperiments.d.ts +0 -20
  38. package/src/lib/dot-experiments.d.ts +0 -289
  39. package/src/lib/hooks/useExperimentVariant.d.ts +0 -21
  40. package/src/lib/hooks/useExperiments.d.ts +0 -14
  41. package/src/lib/shared/mocks/mock.d.ts +0 -43
  42. package/src/lib/shared/parser/parser.d.ts +0 -54
  43. package/src/lib/shared/persistence/index-db-database-handler.d.ts +0 -87
  44. package/src/lib/shared/utils/DotLogger.d.ts +0 -15
  45. package/src/lib/shared/utils/memoize.d.ts +0 -7
  46. package/src/lib/shared/utils/utils.d.ts +0 -73
  47. package/src/lib/standalone.d.ts +0 -7
  48. /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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "types": ["jest", "node"]
7
+ },
8
+ "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
9
+ }
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";