@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
@@ -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<AssignedExperiments, 'excludedExperimentIdsEnded' | 'experiments'>;
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
+ });