@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
|
@@ -26,6 +26,7 @@ export interface DotExperimentConfig {
|
|
|
26
26
|
*/
|
|
27
27
|
redirectFn?: (url: string) => void;
|
|
28
28
|
}
|
|
29
|
+
|
|
29
30
|
/**
|
|
30
31
|
* Represents the configuration for the LookBackWindow.
|
|
31
32
|
*
|
|
@@ -36,15 +37,18 @@ export interface LookBackWindow {
|
|
|
36
37
|
* Defines the time period in milliseconds when the LookBackWindow should expire.
|
|
37
38
|
*/
|
|
38
39
|
expireMillis: number;
|
|
40
|
+
|
|
39
41
|
/**
|
|
40
42
|
* A value indicating the expiration timestamp of the experiment.
|
|
41
43
|
*/
|
|
42
44
|
expireTime?: number;
|
|
45
|
+
|
|
43
46
|
/**
|
|
44
47
|
* Represents the associated value with the LookBackWindow.
|
|
45
48
|
*/
|
|
46
49
|
value: string;
|
|
47
50
|
}
|
|
51
|
+
|
|
48
52
|
/**
|
|
49
53
|
* Represents the configurations for checking whether the current page is an Experiment page, or a target.
|
|
50
54
|
*
|
|
@@ -55,11 +59,13 @@ interface Regexs {
|
|
|
55
59
|
* A regular expression for validating if the page is an Experiment page.
|
|
56
60
|
*/
|
|
57
61
|
isExperimentPage: string;
|
|
62
|
+
|
|
58
63
|
/**
|
|
59
64
|
* A regular expression for validating if the page is the target page. This can be null.
|
|
60
65
|
*/
|
|
61
66
|
isTargetPage: string | null;
|
|
62
67
|
}
|
|
68
|
+
|
|
63
69
|
/**
|
|
64
70
|
* Represents a variant that is applied when a request is made.
|
|
65
71
|
*
|
|
@@ -70,11 +76,13 @@ export interface Variant {
|
|
|
70
76
|
* The name of the variant.
|
|
71
77
|
*/
|
|
72
78
|
name: string;
|
|
79
|
+
|
|
73
80
|
/**
|
|
74
81
|
* The fully qualified URL where the variant is being applied, with query parameters already set.
|
|
75
82
|
*/
|
|
76
83
|
url: string;
|
|
77
84
|
}
|
|
85
|
+
|
|
78
86
|
/**
|
|
79
87
|
* Represents an experiment with all its configurations.
|
|
80
88
|
*
|
|
@@ -85,31 +93,38 @@ export interface Experiment {
|
|
|
85
93
|
* The unique identifier for the experiment.
|
|
86
94
|
*/
|
|
87
95
|
id: string;
|
|
96
|
+
|
|
88
97
|
/**
|
|
89
98
|
* The lookback window object for the experiment.
|
|
90
99
|
*/
|
|
91
100
|
lookBackWindow: LookBackWindow;
|
|
101
|
+
|
|
92
102
|
/**
|
|
93
103
|
* The name of the experiment.
|
|
94
104
|
*/
|
|
95
105
|
name: string;
|
|
106
|
+
|
|
96
107
|
/**
|
|
97
108
|
* The URL of the page where the experiment is applied.
|
|
98
109
|
*/
|
|
99
110
|
pageUrl: string;
|
|
111
|
+
|
|
100
112
|
/**
|
|
101
113
|
* The object containing regular expressions for validating pages.
|
|
102
114
|
*/
|
|
103
115
|
regexs: Regexs;
|
|
116
|
+
|
|
104
117
|
/**
|
|
105
118
|
* The unique running identifier for the experiment.
|
|
106
119
|
*/
|
|
107
120
|
runningId: string;
|
|
121
|
+
|
|
108
122
|
/**
|
|
109
123
|
* The variant applied to the user making the request.
|
|
110
124
|
*/
|
|
111
125
|
variant: Variant;
|
|
112
126
|
}
|
|
127
|
+
|
|
113
128
|
/**
|
|
114
129
|
* Represents the experiments assigned and their details.
|
|
115
130
|
*
|
|
@@ -120,23 +135,31 @@ export interface AssignedExperiments {
|
|
|
120
135
|
* The ids of the experiments that are excluded in the assignment.
|
|
121
136
|
*/
|
|
122
137
|
excludedExperimentIds: string[];
|
|
138
|
+
|
|
123
139
|
/**
|
|
124
140
|
* An array representing the assigned experiments.
|
|
125
141
|
*/
|
|
126
142
|
experiments: Experiment[];
|
|
143
|
+
|
|
127
144
|
/**
|
|
128
145
|
* The ids of the experiments included in the assignment.
|
|
129
146
|
*/
|
|
130
147
|
includedExperimentIds: string[];
|
|
148
|
+
|
|
131
149
|
/**
|
|
132
150
|
* The ids of the experiments that are excluded in the request and have ended.
|
|
133
151
|
*/
|
|
134
152
|
excludedExperimentIdsEnded: string[];
|
|
135
153
|
}
|
|
154
|
+
|
|
136
155
|
/**
|
|
137
156
|
* Represents the response from the backend when fetching an experiment and the excludedExperimentIdsEnded.
|
|
138
157
|
*/
|
|
139
|
-
export type FetchExperiments = Pick<
|
|
158
|
+
export type FetchExperiments = Pick<
|
|
159
|
+
AssignedExperiments,
|
|
160
|
+
'excludedExperimentIdsEnded' | 'experiments'
|
|
161
|
+
>;
|
|
162
|
+
|
|
140
163
|
/**
|
|
141
164
|
* Represents the response from backend holding information about running experiments and variant assignment.
|
|
142
165
|
*
|
|
@@ -147,19 +170,23 @@ export interface IsUserIncludedApiResponse {
|
|
|
147
170
|
* The object holding all experiment-related information.
|
|
148
171
|
*/
|
|
149
172
|
entity: AssignedExperiments;
|
|
173
|
+
|
|
150
174
|
/**
|
|
151
175
|
* An array holding possible error messages.
|
|
152
176
|
*/
|
|
153
177
|
errors: string[];
|
|
178
|
+
|
|
154
179
|
/**
|
|
155
180
|
* A map that holds internationalization (i18n) messages.
|
|
156
181
|
*/
|
|
157
182
|
i18nMessagesMap: Record<string, string>;
|
|
183
|
+
|
|
158
184
|
/**
|
|
159
185
|
* An array of generic messages.
|
|
160
186
|
*/
|
|
161
187
|
messages: string[];
|
|
162
188
|
}
|
|
189
|
+
|
|
163
190
|
/**
|
|
164
191
|
* Represents a single experiment event.
|
|
165
192
|
*
|
|
@@ -170,27 +197,33 @@ export interface ExperimentEvent {
|
|
|
170
197
|
* The name or identifier of the experiment.
|
|
171
198
|
*/
|
|
172
199
|
experiment: string;
|
|
200
|
+
|
|
173
201
|
/**
|
|
174
202
|
* The unique running identifier of the experiment.
|
|
175
203
|
*/
|
|
176
204
|
runningId: string;
|
|
205
|
+
|
|
177
206
|
/**
|
|
178
207
|
* A flag that determines if the current page is the one where the experiment is conducted.
|
|
179
208
|
*/
|
|
180
209
|
isExperimentPage: boolean;
|
|
210
|
+
|
|
181
211
|
/**
|
|
182
212
|
* A flag that determines if the current page is the target page of the experiment.
|
|
183
213
|
*/
|
|
184
214
|
isTargetPage: boolean;
|
|
215
|
+
|
|
185
216
|
/**
|
|
186
217
|
* Represents the time period for which the experiment is valid or should be considered.
|
|
187
218
|
*/
|
|
188
219
|
lookBackWindow: string;
|
|
220
|
+
|
|
189
221
|
/**
|
|
190
222
|
* Represents the variant of the experiment to specify different versions of an experiment.
|
|
191
223
|
*/
|
|
192
224
|
variant: string;
|
|
193
225
|
}
|
|
226
|
+
|
|
194
227
|
/**
|
|
195
228
|
* Represents parsed data of an experiment that is ready to be sent to Analytics.
|
|
196
229
|
*
|
|
@@ -201,9 +234,9 @@ export interface ExperimentParsed {
|
|
|
201
234
|
* The URL of the experiment. It helps track the location or origin of the experiment.
|
|
202
235
|
*/
|
|
203
236
|
href: string;
|
|
237
|
+
|
|
204
238
|
/**
|
|
205
239
|
* An array holding details of individual experiments being conducted.
|
|
206
240
|
*/
|
|
207
241
|
experiments: ExperimentEvent[];
|
|
208
242
|
}
|
|
209
|
-
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { parseData, parseDataForAnalytics } from './parser';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IsUserIncludedResponse,
|
|
5
|
+
LocationMock,
|
|
6
|
+
MOCK_CURRENT_TIMESTAMP,
|
|
7
|
+
MockDataStoredIndexDB,
|
|
8
|
+
MockDataStoredIndexDBWithNew,
|
|
9
|
+
MockDataStoredIndexDBWithNew15DaysLater,
|
|
10
|
+
NewIsUserIncludedResponse,
|
|
11
|
+
TIME_15_DAYS_MILLISECONDS,
|
|
12
|
+
TIME_5_DAYS_MILLISECONDS
|
|
13
|
+
} from '../mocks/mock';
|
|
14
|
+
import { Experiment, ExperimentParsed, FetchExperiments } from '../models';
|
|
15
|
+
|
|
16
|
+
const assignedExperiments: Experiment[] = IsUserIncludedResponse.entity.experiments;
|
|
17
|
+
|
|
18
|
+
const experimentMock = IsUserIncludedResponse.entity.experiments[0];
|
|
19
|
+
|
|
20
|
+
describe('Parsers', () => {
|
|
21
|
+
describe('parseData For Analytics', () => {
|
|
22
|
+
it('returns `isExperimentPage` true and `isTargetPage` false when location is /blog', () => {
|
|
23
|
+
const expectedURL = 'http://localhost/blog';
|
|
24
|
+
|
|
25
|
+
const expectedExperimentsParsed: ExperimentParsed = {
|
|
26
|
+
href: expectedURL,
|
|
27
|
+
experiments: [
|
|
28
|
+
{
|
|
29
|
+
experiment: experimentMock.id,
|
|
30
|
+
runningId: experimentMock.runningId,
|
|
31
|
+
variant: experimentMock.variant.name,
|
|
32
|
+
lookBackWindow: experimentMock.lookBackWindow.value,
|
|
33
|
+
isExperimentPage: true,
|
|
34
|
+
isTargetPage: false
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const location: Location = { ...LocationMock, href: expectedURL };
|
|
40
|
+
|
|
41
|
+
const parsedData: ExperimentParsed = parseDataForAnalytics(
|
|
42
|
+
assignedExperiments,
|
|
43
|
+
location
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(parsedData).toStrictEqual(expectedExperimentsParsed);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns `isExperimentPage` false and `isTargetPage` true when location is /destinations', () => {
|
|
50
|
+
const expectedURL = 'http://localhost/destinations';
|
|
51
|
+
|
|
52
|
+
const expectedExperimentsParsed: ExperimentParsed = {
|
|
53
|
+
href: expectedURL,
|
|
54
|
+
experiments: [
|
|
55
|
+
{
|
|
56
|
+
experiment: experimentMock.id,
|
|
57
|
+
runningId: experimentMock.runningId,
|
|
58
|
+
variant: experimentMock.variant.name,
|
|
59
|
+
lookBackWindow: experimentMock.lookBackWindow.value,
|
|
60
|
+
isExperimentPage: false,
|
|
61
|
+
isTargetPage: true
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const location: Location = { ...LocationMock, href: expectedURL };
|
|
67
|
+
|
|
68
|
+
const parsedData: ExperimentParsed = parseDataForAnalytics(
|
|
69
|
+
assignedExperiments,
|
|
70
|
+
location
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(parsedData).toStrictEqual(expectedExperimentsParsed);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns `isExperimentPage` false and `isTargetPage` false when location is /other-url', () => {
|
|
77
|
+
const expectedURL = 'http://localhost/other-url';
|
|
78
|
+
|
|
79
|
+
const expectedExperimentsParsed: ExperimentParsed = {
|
|
80
|
+
href: expectedURL,
|
|
81
|
+
experiments: [
|
|
82
|
+
{
|
|
83
|
+
experiment: experimentMock.id,
|
|
84
|
+
runningId: experimentMock.runningId,
|
|
85
|
+
variant: experimentMock.variant.name,
|
|
86
|
+
lookBackWindow: experimentMock.lookBackWindow.value,
|
|
87
|
+
isExperimentPage: false,
|
|
88
|
+
isTargetPage: false
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const location: Location = { ...LocationMock, href: expectedURL };
|
|
94
|
+
|
|
95
|
+
const parsedData: ExperimentParsed = parseDataForAnalytics(
|
|
96
|
+
assignedExperiments,
|
|
97
|
+
location
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(parsedData).toStrictEqual(expectedExperimentsParsed);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('parseData For Store', () => {
|
|
105
|
+
const mockNow = jest.spyOn(Date, 'now');
|
|
106
|
+
|
|
107
|
+
mockNow.mockImplementation(() => MOCK_CURRENT_TIMESTAMP);
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
jest.clearAllMocks();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle case where only NEW data is available', () => {
|
|
114
|
+
// First request, expire in now + experiment.lookBackWindow.expireMillis
|
|
115
|
+
const newData: FetchExperiments = {
|
|
116
|
+
experiments: IsUserIncludedResponse.entity.experiments,
|
|
117
|
+
excludedExperimentIdsEnded: []
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const dataFromIndexDB: Experiment[] | undefined = undefined;
|
|
121
|
+
|
|
122
|
+
const parsedData = parseData(newData, dataFromIndexDB);
|
|
123
|
+
|
|
124
|
+
expect(parsedData).toStrictEqual(MockDataStoredIndexDB);
|
|
125
|
+
expect(newData.experiments.length).toBe(parsedData.length);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle case where only OLD data is available', () => {
|
|
129
|
+
//No new request, not touch anything if not expired
|
|
130
|
+
|
|
131
|
+
const newData: FetchExperiments = {
|
|
132
|
+
experiments: [],
|
|
133
|
+
excludedExperimentIdsEnded: []
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const dataFromIndexDB: Experiment[] | undefined = MockDataStoredIndexDB;
|
|
137
|
+
|
|
138
|
+
const parsedData = parseData(newData, dataFromIndexDB);
|
|
139
|
+
|
|
140
|
+
expect(parsedData).toStrictEqual(MockDataStoredIndexDB);
|
|
141
|
+
expect(MockDataStoredIndexDB.length).toBe(parsedData.length);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle case where both OLD and NEW data are available', () => {
|
|
145
|
+
//new request, stored data + new data. No delete anything only 5 days passed, 2 to store
|
|
146
|
+
|
|
147
|
+
const nowPlus5Days = MOCK_CURRENT_TIMESTAMP + TIME_5_DAYS_MILLISECONDS;
|
|
148
|
+
|
|
149
|
+
mockNow.mockImplementation(() => nowPlus5Days);
|
|
150
|
+
|
|
151
|
+
const newData: FetchExperiments = {
|
|
152
|
+
experiments: NewIsUserIncludedResponse.entity.experiments,
|
|
153
|
+
excludedExperimentIdsEnded: [
|
|
154
|
+
...NewIsUserIncludedResponse.entity.excludedExperimentIdsEnded
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const dataFromIndexDB: Experiment[] | undefined = MockDataStoredIndexDB;
|
|
159
|
+
|
|
160
|
+
const parsedData = parseData(newData, dataFromIndexDB);
|
|
161
|
+
|
|
162
|
+
expect(parsedData).toStrictEqual(MockDataStoredIndexDBWithNew);
|
|
163
|
+
|
|
164
|
+
expect(parsedData.length).toBe(MockDataStoredIndexDBWithNew.length);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should remove from stored experiment the experiments expired', () => {
|
|
168
|
+
// no new request, 15 days later, so expireTime is 15 days from MOCK_CURRENT_TIMESTAMP
|
|
169
|
+
// 1st experiment expired, so only 1 to store
|
|
170
|
+
const now15Days = MOCK_CURRENT_TIMESTAMP + TIME_15_DAYS_MILLISECONDS;
|
|
171
|
+
|
|
172
|
+
mockNow.mockImplementation(() => now15Days);
|
|
173
|
+
|
|
174
|
+
const newData: FetchExperiments = {
|
|
175
|
+
experiments: [],
|
|
176
|
+
excludedExperimentIdsEnded: []
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const dataFromIndexDB: Experiment[] | undefined = MockDataStoredIndexDBWithNew;
|
|
180
|
+
|
|
181
|
+
const parsedData = parseData(newData, dataFromIndexDB);
|
|
182
|
+
|
|
183
|
+
expect(parsedData.length).toBe(MockDataStoredIndexDBWithNew15DaysLater.length);
|
|
184
|
+
expect(parsedData).toStrictEqual(MockDataStoredIndexDBWithNew15DaysLater);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Experiment, ExperimentParsed, FetchExperiments } from '../models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This arrow function parses a given set of assigned experiments for analytics.
|
|
5
|
+
*
|
|
6
|
+
* This process involves iterating over the experiments, which are currently in the "Running" state as received from the DotCMS endpoint,
|
|
7
|
+
* analyzing each experiment's relevant data such as running ID, variant name, and look back window value.
|
|
8
|
+
* It also performs regular expression verification for both 'isExperimentPage' and 'isTargetPage' against the current URL.
|
|
9
|
+
*
|
|
10
|
+
* The parsed data is useful for tracking and understanding the user's interaction with the experiment-targeted components during their visit.
|
|
11
|
+
*
|
|
12
|
+
* Contains an object with experiments information.
|
|
13
|
+
*
|
|
14
|
+
* @param experiments
|
|
15
|
+
* @param {Location} location - This parameter is the object representing the current location (URL) of the user.
|
|
16
|
+
* Mostly employed for matching the regular expressions to detect whether the current page is an 'ExperimentPage' or a 'TargetPage'.
|
|
17
|
+
*
|
|
18
|
+
* @returns {ExperimentParsed} - The function returns an object with the original URL and an array of each experiment's comprehensive detail.
|
|
19
|
+
* The return object is suitable for further analytical operations. Each experiment's detail includes the experiment ID, running ID, variant name,
|
|
20
|
+
* look back window value, and booleans that represent whether current URL is 'isExperimentPage' or 'isTargetPage' for the respective experiment.
|
|
21
|
+
*/
|
|
22
|
+
export const parseDataForAnalytics = (
|
|
23
|
+
experiments: Experiment[],
|
|
24
|
+
location: Location
|
|
25
|
+
): ExperimentParsed => {
|
|
26
|
+
const currentHref = location.href;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
href: currentHref,
|
|
30
|
+
experiments: experiments.map((experiment) => ({
|
|
31
|
+
experiment: experiment.id,
|
|
32
|
+
runningId: experiment.runningId,
|
|
33
|
+
variant: experiment.variant.name,
|
|
34
|
+
lookBackWindow: experiment.lookBackWindow.value,
|
|
35
|
+
isExperimentPage: verifyRegex(experiment.regexs.isExperimentPage, currentHref),
|
|
36
|
+
isTargetPage: verifyRegex(experiment.regexs.isTargetPage, currentHref)
|
|
37
|
+
}))
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This utility function performs regular expression (regex) matching on a supplied URL.
|
|
43
|
+
*
|
|
44
|
+
* @param {string | null} regexToCheck - The regular expression to match against the URL.
|
|
45
|
+
* @param {string} href - This is the target URL, which is aimed to be matched against the provided regular expression.
|
|
46
|
+
* @returns {boolean} -The function returns a Boolean value.
|
|
47
|
+
*/
|
|
48
|
+
export const verifyRegex = (regexToCheck: string | null, href: string): boolean => {
|
|
49
|
+
if (regexToCheck === null || href === null) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const regexExp = new RegExp(regexToCheck);
|
|
55
|
+
|
|
56
|
+
const url = new URL(href);
|
|
57
|
+
|
|
58
|
+
const sanitizedHref = `${url.origin}${url.pathname.toLowerCase()}${url.search}`;
|
|
59
|
+
|
|
60
|
+
return regexExp.test(sanitizedHref);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(`The regex ${regexToCheck} it is not a valid regex to check. ${error}`);
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function merges newly fetched data with the data stored from IndexedDB, preparing it for re-storage in IndexedDB.
|
|
70
|
+
*
|
|
71
|
+
* @param { AssignedExperiments | null } fetchExperiments - The experiment data fetched from the API.
|
|
72
|
+
* @param { AssignedExperiments | null } storedExperiments - The experiment data currently stored in IndexedDB.
|
|
73
|
+
*
|
|
74
|
+
* @returns { AssignedExperiments } - The parsed experiment data ready for storing.
|
|
75
|
+
*
|
|
76
|
+
* Following cases are handled -
|
|
77
|
+
* 1) When new Data is received without Old data. This is assumed to be the first time data is received.
|
|
78
|
+
* 2) When only old data is received, implying that the timestamp hasn't expired and the data is available in IndexedDB.
|
|
79
|
+
* The data is verified and any expired experiment is removed before being assigned to dataToStorage.
|
|
80
|
+
* 3) When both old data and new data is present. This implies that the record existed in IndexedDB but was fetched because the flag had expired.
|
|
81
|
+
* Additional operations are performed to add expiry time to experiments, merging all experiments, and storing them.
|
|
82
|
+
*
|
|
83
|
+
* There could be scenarios where none of these conditions are met, in that case, dataToStorage will be the default empty object.
|
|
84
|
+
*/
|
|
85
|
+
export const parseData = (
|
|
86
|
+
fetchExperiments: FetchExperiments,
|
|
87
|
+
storedExperiments: Experiment[] | undefined
|
|
88
|
+
): Experiment[] => {
|
|
89
|
+
let dataToStorage: Experiment[] = {} as Experiment[];
|
|
90
|
+
|
|
91
|
+
const { excludedExperimentIdsEnded } = fetchExperiments;
|
|
92
|
+
|
|
93
|
+
if (fetchExperiments && !storedExperiments) {
|
|
94
|
+
// TODO: Use fetchExperiment instead fetchExperimentsNoNoneExperimentID when the endpoint dont retrieve NONE experiment
|
|
95
|
+
// https://github.com/dotCMS/core/issues/27905
|
|
96
|
+
const fetchExperimentsNoNoneExperimentID: Experiment[] = fetchExperiments.experiments
|
|
97
|
+
? fetchExperiments.experiments.filter((experiment) => experiment.id !== 'NONE')
|
|
98
|
+
: [];
|
|
99
|
+
|
|
100
|
+
dataToStorage = addExpireTimeToExperiments(fetchExperimentsNoNoneExperimentID);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!fetchExperiments && storedExperiments) {
|
|
104
|
+
dataToStorage = getUnexpiredExperiments(storedExperiments, excludedExperimentIdsEnded);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (fetchExperiments && storedExperiments) {
|
|
108
|
+
// TODO: Use fetchExperiment instead fetchExperimentsNoNoneExperimentID when the endpoint dont retrieve NONE experiment
|
|
109
|
+
// https://github.com/dotCMS/core/issues/27905
|
|
110
|
+
const fetchExperimentsNoNoneExperimentID: Experiment[] = fetchExperiments.experiments
|
|
111
|
+
? fetchExperiments.experiments.filter((experiment) => experiment.id !== 'NONE')
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
dataToStorage = [
|
|
115
|
+
...addExpireTimeToExperiments(fetchExperimentsNoNoneExperimentID),
|
|
116
|
+
...getUnexpiredExperiments(storedExperiments, excludedExperimentIdsEnded)
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return dataToStorage;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Retrieves the array of experiment IDs from the given AssignedExperiments..
|
|
125
|
+
*
|
|
126
|
+
* @returns {string[]} Returns an array of experiment IDs if available, otherwise an empty array.
|
|
127
|
+
* @param experiments
|
|
128
|
+
*/
|
|
129
|
+
export const getExperimentsIds = (experiments: Experiment[]): string[] =>
|
|
130
|
+
experiments.map((experiment: Experiment) => experiment.id) || [];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Sets the expire time for new experiments based on the current time.
|
|
134
|
+
* The expire time is calculated by adding the expireMillis value of each experiment's lookBackWindow to the current time (Date.now()).
|
|
135
|
+
*
|
|
136
|
+
* @param {Array<Experiment>} experiments - An array of experiments to set the expire time for.
|
|
137
|
+
* @returns {Array<Experiment>} - An updated array of experiments with expire time set.
|
|
138
|
+
*/
|
|
139
|
+
const addExpireTimeToExperiments = (experiments: Experiment[]): Experiment[] => {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
|
|
142
|
+
return experiments.map((experiment) => ({
|
|
143
|
+
...experiment,
|
|
144
|
+
lookBackWindow: {
|
|
145
|
+
...experiment.lookBackWindow,
|
|
146
|
+
expireTime: now + experiment.lookBackWindow.expireMillis
|
|
147
|
+
}
|
|
148
|
+
}));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns an array of experiments that have not expired yet.
|
|
153
|
+
*
|
|
154
|
+
* @param {Experiment[]} experiments - An array of experiments to filter.
|
|
155
|
+
* @param excludedExperimentIdsEnded - Array of Experiments ids that have been manually ended.
|
|
156
|
+
* @returns {Experiment[]} An array of unexpired experiments.
|
|
157
|
+
*/
|
|
158
|
+
const getUnexpiredExperiments = (
|
|
159
|
+
experiments: Experiment[],
|
|
160
|
+
excludedExperimentIdsEnded: string[]
|
|
161
|
+
): Experiment[] => {
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
|
|
164
|
+
return experiments.filter((experiment) => {
|
|
165
|
+
const expireTime = experiment.lookBackWindow?.expireTime;
|
|
166
|
+
|
|
167
|
+
return expireTime
|
|
168
|
+
? expireTime > now && !excludedExperimentIdsEnded.includes(experiment.id)
|
|
169
|
+
: false;
|
|
170
|
+
});
|
|
171
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fakeIndexedDB from 'fake-indexeddb';
|
|
2
|
+
|
|
3
|
+
import { IndexDBDatabaseHandler } from './index-db-database-handler';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
EXPERIMENT_ALREADY_CHECKED_KEY,
|
|
7
|
+
EXPERIMENT_DB_KEY_PATH,
|
|
8
|
+
EXPERIMENT_DB_STORE_NAME
|
|
9
|
+
} from '../constants';
|
|
10
|
+
import { IsUserIncludedResponse } from '../mocks/mock';
|
|
11
|
+
import { checkFlagExperimentAlreadyChecked } from '../utils/utils';
|
|
12
|
+
|
|
13
|
+
if (!globalThis.structuredClone) {
|
|
14
|
+
globalThis.structuredClone = function (obj) {
|
|
15
|
+
return JSON.parse(JSON.stringify(obj));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let persistDatabaseHandler: IndexDBDatabaseHandler;
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
Object.defineProperty(window, 'indexedDB', {
|
|
23
|
+
writable: true,
|
|
24
|
+
value: fakeIndexedDB
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
persistDatabaseHandler = new IndexDBDatabaseHandler({
|
|
28
|
+
db_store: EXPERIMENT_DB_STORE_NAME,
|
|
29
|
+
db_name: EXPERIMENT_DB_STORE_NAME,
|
|
30
|
+
db_key_path: EXPERIMENT_DB_KEY_PATH
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('IndexedDB tests', () => {
|
|
35
|
+
it('saveData successfully saves data to the store', async () => {
|
|
36
|
+
const key = await persistDatabaseHandler.persistData(IsUserIncludedResponse.entity);
|
|
37
|
+
|
|
38
|
+
expect(key).toBe(EXPERIMENT_DB_KEY_PATH);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('getDataByKey successfully retrieves data from the store', async () => {
|
|
42
|
+
await persistDatabaseHandler.persistData(IsUserIncludedResponse.entity);
|
|
43
|
+
|
|
44
|
+
const data = await persistDatabaseHandler.getData();
|
|
45
|
+
|
|
46
|
+
expect(data).toEqual(IsUserIncludedResponse.entity);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('SessionStorage EXPERIMENT_ALREADY_CHECKED_KEY handle', () => {
|
|
51
|
+
Object.defineProperty(window, 'sessionStorage', {
|
|
52
|
+
value: {
|
|
53
|
+
setItem: jest.fn(),
|
|
54
|
+
getItem: jest.fn()
|
|
55
|
+
},
|
|
56
|
+
writable: true
|
|
57
|
+
});
|
|
58
|
+
it('should set true to sessionStorage key `EXPERIMENT_ALREADY_CHECKED_KEY` ', () => {
|
|
59
|
+
const value = 'true';
|
|
60
|
+
|
|
61
|
+
persistDatabaseHandler.setFlagExperimentAlreadyChecked();
|
|
62
|
+
|
|
63
|
+
expect(window.sessionStorage.setItem).toHaveBeenLastCalledWith(
|
|
64
|
+
EXPERIMENT_ALREADY_CHECKED_KEY,
|
|
65
|
+
value
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should set true to sessionStorage key `EXPERIMENT_ALREADY_CHECKED_KEY` ', () => {
|
|
70
|
+
const value = 'true';
|
|
71
|
+
|
|
72
|
+
persistDatabaseHandler.setFlagExperimentAlreadyChecked();
|
|
73
|
+
|
|
74
|
+
expect(window.sessionStorage.setItem).toHaveBeenLastCalledWith(
|
|
75
|
+
EXPERIMENT_ALREADY_CHECKED_KEY,
|
|
76
|
+
value
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('checkFlagExperimentAlreadyChecked', () => {
|
|
81
|
+
const getItemMock = window.sessionStorage.getItem as jest.MockedFunction<
|
|
82
|
+
typeof window.sessionStorage.getItem
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
const testCases = [
|
|
86
|
+
{ value: '', expected: false, description: 'sessionStorage value is ""' },
|
|
87
|
+
{ value: 'true', expected: true, description: 'sessionStorage value is "true"' },
|
|
88
|
+
{ value: null, expected: false, description: 'sessionStorage value is null' }
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
testCases.forEach(({ description, value, expected }) => {
|
|
92
|
+
it(`returns ${expected} when ${description}`, () => {
|
|
93
|
+
getItemMock.mockReturnValue(value);
|
|
94
|
+
|
|
95
|
+
expect(checkFlagExperimentAlreadyChecked()).toBe(expected);
|
|
96
|
+
expect(getItemMock).toHaveBeenCalledWith(EXPERIMENT_ALREADY_CHECKED_KEY);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|