@dotcms/experiments 0.0.1-alpha.38 → 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,716 @@
1
+ import { EventPayload, jitsuClient, JitsuClient } from '@jitsu/sdk-js';
2
+
3
+ import {
4
+ API_EXPERIMENTS_URL,
5
+ DEBUG_LEVELS,
6
+ EXPERIMENT_ALREADY_CHECKED_KEY,
7
+ EXPERIMENT_DB_KEY_PATH,
8
+ EXPERIMENT_DB_STORE_NAME,
9
+ EXPERIMENT_DEFAULT_VARIANT_NAME,
10
+ EXPERIMENT_QUERY_PARAM_KEY,
11
+ PAGE_VIEW_EVENT_NAME
12
+ } from './shared/constants';
13
+ import {
14
+ AssignedExperiments,
15
+ DotExperimentConfig,
16
+ Experiment,
17
+ FetchExperiments,
18
+ Variant
19
+ } from './shared/models';
20
+ import {
21
+ getExperimentsIds,
22
+ parseData,
23
+ parseDataForAnalytics,
24
+ verifyRegex
25
+ } from './shared/parser/parser';
26
+ import { IndexDBDatabaseHandler } from './shared/persistence/index-db-database-handler';
27
+ import { DotLogger } from './shared/utils/DotLogger';
28
+ import {
29
+ checkFlagExperimentAlreadyChecked,
30
+ defaultRedirectFn,
31
+ getFullUrl,
32
+ isDataCreateValid,
33
+ objectsAreEqual,
34
+ updateUrlWithExperimentVariant
35
+ } from './shared/utils/utils';
36
+
37
+ /**
38
+ * `DotExperiments` is a Typescript class to handles all operations related to fetching, storing, parsing, and navigating
39
+ * data for Experiments (A/B Testing).
40
+ *
41
+ * It requires a configuration object for instantiation, please instance it using the method `getInstance` sending
42
+ * an object with `api-key`, `server` and `debug`.
43
+ *
44
+ * Here's an example of how you can instantiate DotExperiments class:
45
+ * @example
46
+ * ```typescript
47
+ * const instance = DotExperiments.getInstance({
48
+ * server: "yourServerUrl",
49
+ * "api-key": "yourApiKey"
50
+ * });
51
+ * ```
52
+ *
53
+ * @export
54
+ * @class DotExperiments
55
+ *
56
+ */
57
+ export class DotExperiments {
58
+ /**
59
+ * The instance of the DotExperiments class.
60
+ * @private
61
+ */
62
+ private static instance: DotExperiments;
63
+ /**
64
+ * Represents the default configuration for the DotExperiment library.
65
+ * @property {boolean} trackPageView - Specifies whether to track page view or not. Default value is true.
66
+ */
67
+ private static readonly defaultConfig: Partial<DotExperimentConfig> = {
68
+ // By default, we track the page view
69
+ trackPageView: true,
70
+ // By default, debug is off
71
+ debug: false
72
+ };
73
+ /**
74
+ * Represents the promise for the initialization process.
75
+ * @private
76
+ */
77
+ private initializationPromise: Promise<void> | null = null;
78
+ /**
79
+ * Represents the analytics client for Analytics.
80
+ * @private
81
+ */
82
+ private analytics!: JitsuClient;
83
+ /**
84
+ * Class representing a database handler for IndexDB.
85
+ * @class
86
+ */
87
+ private persistenceHandler!: IndexDBDatabaseHandler;
88
+ /**
89
+ * Represents the stored data in the IndexedDB.
90
+ * @private
91
+ */
92
+ private experimentsAssigned: Experiment[] = [];
93
+ /**
94
+ * A logger utility for logging messages.
95
+ *
96
+ * @class
97
+ */
98
+ private logger!: DotLogger;
99
+ /**
100
+ * Represents the current location.
101
+ * @private
102
+ */
103
+ // eslint-disable-next-line no-restricted-globals
104
+ private currentLocation: Location = location;
105
+
106
+ /**
107
+ * Represents the previous location.
108
+ *
109
+ * @type {string}
110
+ */
111
+ private prevLocation = '';
112
+
113
+ private constructor(private readonly config: DotExperimentConfig) {
114
+ // merge default config and the config to the instance
115
+ this.config = { ...DotExperiments.defaultConfig, ...config };
116
+
117
+ if (!this.config['server']) {
118
+ throw new Error('`server` must be provided and should not be empty!');
119
+ }
120
+
121
+ if (!this.config['apiKey']) {
122
+ throw new Error('`apiKey` must be provided and should not be empty!');
123
+ }
124
+
125
+ this.logger = new DotLogger(this.config.debug, 'DotExperiment');
126
+ }
127
+
128
+ /**
129
+ * Retrieves the array of experiments assigned to an instance of the class.
130
+ *
131
+ * @return {Experiment[]} An array containing the experiments assigned to the instance.
132
+ */
133
+ public get experiments(): Experiment[] {
134
+ return this.experimentsAssigned;
135
+ }
136
+
137
+ /**
138
+ * Returns a custom redirect function. If a custom redirect function is not configured,
139
+ * the default redirect function will be used.
140
+ *
141
+ * @return {function} A function that accepts a URL string parameter and performs a redirect.
142
+ * If no parameter is provided, the function will not perform any action.
143
+ */
144
+ public get customRedirectFn(): (url: string) => void {
145
+ return this.config.redirectFn ?? defaultRedirectFn;
146
+ }
147
+
148
+ /**
149
+ * Retrieves the current location.
150
+ *
151
+ * @returns {Location} The current location.
152
+ */
153
+ public get location(): Location {
154
+ return this.currentLocation;
155
+ }
156
+
157
+ /**
158
+ * Retrieves instance of DotExperiments class if it doesn't exist create a new one.
159
+ * If the instance does not exist, it creates a new instance with the provided configuration and calls the `getExperimentData` method.
160
+ *
161
+ * @param {DotExperimentConfig} config - The configuration object for initializing the DotExperiments instance.
162
+ * @return {DotExperiments} - The instance of the DotExperiments class.
163
+ */
164
+ public static getInstance(config?: DotExperimentConfig): DotExperiments {
165
+ if (!DotExperiments.instance) {
166
+ if (!config) {
167
+ throw new Error('Configuration is required to create a new instance.');
168
+ }
169
+
170
+ DotExperiments.instance = new DotExperiments(config);
171
+ DotExperiments.instance.initialize();
172
+
173
+ DotExperiments.instance.logger.log(
174
+ 'Instance created with configuration: ' + JSON.stringify(config)
175
+ );
176
+ } else {
177
+ DotExperiments.instance.logger.log('Instance of DotExperiments already exist');
178
+ }
179
+
180
+ return DotExperiments.instance;
181
+ }
182
+
183
+ /**
184
+ * Waits for the initialization process to be completed.
185
+ *
186
+ * @return {Promise<void>} A Promise that resolves when the initialization is ready.
187
+ */
188
+ public ready(): Promise<void> {
189
+ return this.initializationPromise ?? Promise.resolve();
190
+ }
191
+
192
+ /**
193
+ * This method appends variant parameters to navigation links based on the provided navClass.
194
+ *
195
+ * Note: In order for this method's functionality to apply, you need to define a class for the navigation elements (anchors)
196
+ * to which you would like this functionality applied and pass it as an argument when calling this method.
197
+ *
198
+ * @param {string} navClass - The class of the navigation elements to which variant parameters should be appended. Such elements should be anchors (`<a>`).
199
+ *
200
+ * @example
201
+ * <ul class="navbar-nav me-auto mb-2 mb-md-0">
202
+ * <li class="nav-item">
203
+ * <a class="nav-link " aria-current="page" href="/">Home</a>
204
+ * </li>
205
+ * <li class="nav-item ">
206
+ * <a class="nav-link active" href="/blog">Travel Blog</a>
207
+ * </li>
208
+ * <li class="nav-item">
209
+ * <a class="nav-link" href="/destinations">Destinations</a>
210
+ * </li>
211
+ * </ul>
212
+ *
213
+ * dotExperiment.ready().then(() => {
214
+ * dotExperiment.appendVariantParams('.navbar-nav .nav-link');
215
+ * });
216
+ * appendVariantParams('nav-item-class');
217
+ *
218
+ * @returns {void}
219
+ */
220
+ public appendVariantParams(navClass: string): void {
221
+ // Todo: Add a config for the standalone pages
222
+ // This is only for standalone pages
223
+ const navItems: NodeListOf<HTMLAnchorElement> = document.querySelectorAll(navClass);
224
+
225
+ if (navItems.length > 0) {
226
+ navItems.forEach((link) => {
227
+ const href =
228
+ getFullUrl(this.currentLocation, link.getAttribute('href') || '') ?? '';
229
+
230
+ const variant = this.getVariantFromHref(href);
231
+
232
+ if (variant !== null && href !== null) {
233
+ link.href = updateUrlWithExperimentVariant(href, variant);
234
+ }
235
+ });
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Retrieves the current debug status.
241
+ *
242
+ * @private
243
+ * @returns {boolean} - The debug status.
244
+ */
245
+ public getIsDebugActive(): boolean {
246
+ return this.config.debug;
247
+ }
248
+
249
+ /**
250
+ * Updates the current location and checks if a variant should be applied.
251
+ * Redirects to the variant URL if necessary.
252
+ *
253
+ * @param {Location} location - The new location.
254
+ * @param redirectFunction
255
+ */
256
+ public async locationChanged(
257
+ location: Location,
258
+ redirectFunction?: (url: string) => void
259
+ ): Promise<void> {
260
+ this.logger.group('Location Changed Process');
261
+ this.logger.time('Total location changed');
262
+
263
+ this.currentLocation = location;
264
+ await this.verifyExperimentData();
265
+
266
+ const variantAssigned = this.getVariantFromHref(location.href);
267
+
268
+ if (variantAssigned && variantAssigned.name !== EXPERIMENT_DEFAULT_VARIANT_NAME) {
269
+ const searchParams = new URLSearchParams(location.search);
270
+
271
+ const currentVariant = searchParams.get(EXPERIMENT_QUERY_PARAM_KEY);
272
+
273
+ if (currentVariant !== variantAssigned.name) {
274
+ const variantUrl = updateUrlWithExperimentVariant(location, variantAssigned);
275
+
276
+ if (redirectFunction) {
277
+ this.logger.log(
278
+ `Page redirected to ${variantUrl} using the provided redirect function.`
279
+ );
280
+ this.logger.timeEnd('Total location changed');
281
+ this.logger.groupEnd();
282
+ redirectFunction(variantUrl);
283
+ } else {
284
+ this.logger.log(
285
+ `Page redirected to ${variantUrl} using the default redirect function.`
286
+ );
287
+
288
+ defaultRedirectFn(variantUrl);
289
+ }
290
+ } else {
291
+ this.logger.log(`No redirection needed.`);
292
+ }
293
+ } else {
294
+ this.logger.log(`No experiment variant matched for the current location.`);
295
+ }
296
+
297
+ this.logger.timeEnd('Total location changed');
298
+ this.logger.groupEnd();
299
+ }
300
+
301
+ /**
302
+ * Tracks a page view event in the analytics system.
303
+ *
304
+ * @return {void}
305
+ */
306
+ public trackPageView(): void {
307
+ this.track(PAGE_VIEW_EVENT_NAME);
308
+ this.prevLocation = this.currentLocation.href;
309
+ }
310
+
311
+ /**
312
+ * This method is used to retrieve the variant associated with a given URL.
313
+ *
314
+ * It checks if the URL is part of an experiment by verifying it against an experiment's regex. If the URL matches the regex of an experiment,
315
+ * it returns the variant attached to that experiment; otherwise, it returns null.
316
+ *
317
+ * @param {string | null} path - The URL to check for a variant. This should be the path of the URL.
318
+ *
319
+ * @returns {Variant | null} The variant associated with the URL if it exists, null otherwise.
320
+ */
321
+ public getVariantFromHref(path: string | null): Variant | null {
322
+ const experiment = this.experimentsAssigned.find((experiment) => {
323
+ const url = getFullUrl(this.currentLocation, path) ?? '';
324
+
325
+ return verifyRegex(experiment.regexs.isExperimentPage, url);
326
+ });
327
+
328
+ return experiment?.variant || null;
329
+ }
330
+
331
+ /**
332
+ * Returns the experiment variant name as a URL search parameter.
333
+ *
334
+ * @param {string|null} path - The path to the current page.
335
+ * @returns {URLSearchParams} - The URL search parameters containing the experiment variant name.
336
+ */
337
+ public getVariantAsQueryParam(path: string | null): URLSearchParams {
338
+ let params: Record<string, string> = {};
339
+
340
+ if (this.experimentsAssigned && path) {
341
+ const experiment = this.experimentsAssigned.find((experiment) => {
342
+ const url = getFullUrl(this.currentLocation, path) ?? '';
343
+
344
+ return verifyRegex(experiment.regexs.isExperimentPage, url);
345
+ });
346
+
347
+ if (experiment && experiment.variant.name !== EXPERIMENT_DEFAULT_VARIANT_NAME) {
348
+ params = { [EXPERIMENT_QUERY_PARAM_KEY]: experiment.variant.name };
349
+ }
350
+ }
351
+
352
+ return new URLSearchParams(params);
353
+ }
354
+
355
+ /**
356
+ * Determines whether a page view should be tracked.
357
+ *
358
+ * @private
359
+ * @returns {boolean} True if a page view should be tracked, otherwise false.
360
+ */
361
+ private shouldTrackPageView(): boolean {
362
+ if (!this.config.trackPageView) {
363
+ this.logger.log(
364
+ `No send pageView. Tracking disabled. Config: ${this.config.trackPageView}.`
365
+ );
366
+
367
+ return false;
368
+ }
369
+
370
+ if (this.experimentsAssigned.length === 0) {
371
+ this.logger.log(`No send pageView. No experiments to track.`);
372
+
373
+ return false;
374
+ }
375
+
376
+ // If the previous location is the same as the current location, we don't need to track the page view
377
+ if (this.prevLocation === this.currentLocation.href) {
378
+ this.logger.log(`No send pageView. Same location.`);
379
+
380
+ return false;
381
+ }
382
+
383
+ return true;
384
+ }
385
+
386
+ /**
387
+ * Tracks an event using the analytics service.
388
+ *
389
+ * @param {string} typeName - The type of event to track.
390
+ * @param {EventPayload} [payload] - Optional payload associated with the event.
391
+ * @return {void}
392
+ */
393
+ private track(typeName: string, payload?: EventPayload): void {
394
+ this.analytics.track(typeName, payload).then(() => {
395
+ this.logger.log(`${typeName} event sent`);
396
+ });
397
+ }
398
+
399
+ /**
400
+ * Initializes the application using lazy initialization. This method performs
401
+ * necessary setup steps and should be invoked to ensure proper execution of the application.
402
+ *
403
+ * Note: This method uses lazy initialization. Make sure to call this method to ensure
404
+ * the application works correctly.
405
+ *
406
+ * @return {Promise<void>} A promise that resolves when the initialization is complete.
407
+ */
408
+ private async initialize(): Promise<void> {
409
+ if (this.initializationPromise != null) {
410
+ // The initialization promise already exists, so we don't need to initialize again.
411
+ // Wait for the current initialization to complete.
412
+ await this.initializationPromise;
413
+ } else {
414
+ // First time initialization or after the promise has been explicitly set to null elsewhere.
415
+ this.initializationPromise = (async () => {
416
+ this.logger.group('Initialization Process');
417
+ this.logger.time('Total Initialization');
418
+ // Load the database handler
419
+ this.initializeDatabaseHandler();
420
+ // Initialize the analytics client
421
+ this.initAnalyticsClient();
422
+ // Retrieve persisted data
423
+ this.experimentsAssigned = await this.getPersistedData();
424
+
425
+ await this.verifyExperimentData();
426
+
427
+ this.logger.timeEnd('Total Initialization');
428
+ this.logger.groupEnd();
429
+ })();
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Fetches experiments from the server.
435
+ *
436
+ * @private
437
+ * @returns {Promise<AssignedExperiments>} - The entity object returned from the server.
438
+ * @throws {Error} - If an HTTP error occurs or an error occurs during the fetch request.
439
+ */
440
+ private async getExperimentsFromServer(): Promise<
441
+ Pick<AssignedExperiments, 'excludedExperimentIdsEnded' | 'experiments'>
442
+ > {
443
+ this.logger.group('Fetch Experiments');
444
+ this.logger.time('Fetch Time');
445
+
446
+ try {
447
+ const body = {
448
+ exclude: this.experimentsAssigned ? getExperimentsIds(this.experimentsAssigned) : []
449
+ };
450
+
451
+ const response: Response = await fetch(`${this.config.server}/${API_EXPERIMENTS_URL}`, {
452
+ method: 'POST',
453
+ headers: {
454
+ Accept: 'application/json',
455
+ 'Content-Type': 'application/json'
456
+ },
457
+ body: JSON.stringify(body)
458
+ });
459
+
460
+ if (!response.ok) {
461
+ const responseText = await response.text();
462
+
463
+ throw new Error(`HTTP error! status: ${response.status}, body: ${responseText}`);
464
+ }
465
+
466
+ const responseJson = await response.json();
467
+
468
+ const experiments = responseJson?.entity?.experiments ?? [];
469
+
470
+ const excludedExperimentIdsEnded =
471
+ responseJson?.entity?.excludedExperimentIdsEnded ?? [];
472
+
473
+ this.logger.log(`Experiment data get successfully `);
474
+
475
+ this.persistenceHandler.setFlagExperimentAlreadyChecked();
476
+ this.persistenceHandler.setFetchExpiredTime();
477
+
478
+ return { experiments, excludedExperimentIdsEnded };
479
+ } catch (error) {
480
+ this.logger.error(
481
+ `An error occurred while trying to fetch the experiments: ${
482
+ (error as Error).message
483
+ }`
484
+ );
485
+ throw error;
486
+ } finally {
487
+ this.logger.timeEnd('Fetch Time');
488
+ this.logger.groupEnd();
489
+ }
490
+ }
491
+
492
+ /**
493
+ * This method is responsible for retrieving and persisting experiment data from the server to the local indexDB database.
494
+ *
495
+ * - Checks whether making a request to the server to fetch experiment data is required.
496
+ * - Sends the request to the server for data if required.
497
+ * - Parses the fetched data to the form required for storage in the database.
498
+ * - Persists the data in the indexDB database.
499
+ *
500
+ * @private
501
+ * @method verifyExperimentData
502
+ * @async
503
+ * @throws {Error} Throws an error with details if there is any failure in loading the experiments or during their persistence in indexDB.
504
+ *
505
+ * @returns {Promise<void>} An empty promise that fulfills once the experiment data has been successfully loaded and persisted.
506
+ */
507
+ private async verifyExperimentData(): Promise<void> {
508
+ try {
509
+ let fetchedExperiments: FetchExperiments = {
510
+ excludedExperimentIdsEnded: [],
511
+ experiments: []
512
+ };
513
+
514
+ const storedExperiments: Experiment[] = this.experimentsAssigned
515
+ ? this.experimentsAssigned
516
+ : [];
517
+
518
+ // Checks whether fetching experiment data from the server is necessary.
519
+ if (this.shouldFetchNewData()) {
520
+ fetchedExperiments = await this.getExperimentsFromServer();
521
+ }
522
+
523
+ const dataToPersist: Experiment[] = parseData(fetchedExperiments, storedExperiments);
524
+
525
+ // If my stored data is equal to my parsed data, I don't need to persist again
526
+ if (!objectsAreEqual(dataToPersist, storedExperiments)) {
527
+ this.experimentsAssigned = await this.persistExperiments(dataToPersist);
528
+ }
529
+
530
+ this.refreshAnalyticsForCurrentLocation();
531
+ } catch (e) {
532
+ throw Error(`Error persisting experiments to indexDB, ${e}`);
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Persists the parsed experiment data into the indexDB database.
538
+ *
539
+ * The method does the following:
540
+ * - Receives the parsed data.
541
+ * - Updates the creation date.
542
+ * - Clears existing data from the indexDB database.
543
+ * - Stores the new data in the indexDB database.
544
+ *
545
+ * If there are no experiments in the received data, the method will not attempt to clear or persist anything and will return immediately.
546
+ *.
547
+ *
548
+ * @note This method utilizes Promises for the asynchronous handling of data persistence. Errors during data persistence are caught and logged, but not re-thrown.
549
+ *
550
+ * @private
551
+ * @method persistExperiments
552
+ * @throws Nothing – Errors are caught and logged, but not re-thrown.
553
+ *
554
+ * @param experiments
555
+ */
556
+ private async persistExperiments(experiments: Experiment[]): Promise<Experiment[]> {
557
+ if (!experiments.length) {
558
+ return [];
559
+ }
560
+
561
+ this.logger.group('Persisting Experiments');
562
+ this.logger.time('Persistence Time');
563
+
564
+ try {
565
+ await this.persistenceHandler.persistData(experiments);
566
+ this.logger.log('Experiment data stored successfully');
567
+
568
+ return experiments;
569
+ } catch (onerror) {
570
+ this.logger.log(`Error storing data: ${onerror}`);
571
+
572
+ return Promise.reject(`Error storing data: ${onerror}`);
573
+ } finally {
574
+ this.logger.timeEnd('Persistence Time');
575
+ this.logger.groupEnd();
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Initializes the database handler.
581
+ *
582
+ * This private method instantiates the class handling the IndexDB database
583
+ * and assigns this instance to 'persistenceHandler'.
584
+ *
585
+ * @private
586
+ */
587
+ private initializeDatabaseHandler(): void {
588
+ this.persistenceHandler = new IndexDBDatabaseHandler({
589
+ db_store: EXPERIMENT_DB_STORE_NAME,
590
+ db_name: EXPERIMENT_DB_STORE_NAME,
591
+ db_key_path: EXPERIMENT_DB_KEY_PATH
592
+ });
593
+ }
594
+
595
+ /**
596
+ * Initializes the Jitsu analytics client.
597
+ *
598
+ * This private method sets up the Jitsu client responsible for sending events
599
+ * to the server with the provided configuration. It also uses the parsed data
600
+ * and registers it as global within Jitsu.
601
+ *
602
+ * @private
603
+ */
604
+ private initAnalyticsClient(): void {
605
+ try {
606
+ if (!this.analytics) {
607
+ this.analytics = jitsuClient({
608
+ key: this.config['apiKey'],
609
+ tracking_host: this.config['server'],
610
+ log_level: this.config['debug'] ? DEBUG_LEVELS.WARN : DEBUG_LEVELS.NONE
611
+ });
612
+ this.logger.log('Analytics client initialized successfully.');
613
+ }
614
+ } catch (error) {
615
+ this.logger.log(`Error creating/updating analytics client: ${error}`);
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Updates the analytics client's data using the experiments data
621
+ * currently available in the IndexDB database, based on the current location.
622
+ *
623
+ * Retrieves and processes the experiments information according to the
624
+ * current location into a suitable format for `analytics.set()`.
625
+ *
626
+ * @private
627
+ * @method refreshAnalyticsForCurrentLocation
628
+ * @returns {void}
629
+ */
630
+ private refreshAnalyticsForCurrentLocation(): void {
631
+ const experimentsData = this.experimentsAssigned ?? [];
632
+
633
+ if (experimentsData.length > 0) {
634
+ const { experiments } = parseDataForAnalytics(experimentsData, this.currentLocation);
635
+
636
+ this.analytics.set({ experiments });
637
+
638
+ this.logger.log('Analytics client updated with experiments data.');
639
+ } else {
640
+ this.analytics.set({ experiments: [] });
641
+ this.logger.log('No experiments data available to update analytics client.');
642
+ }
643
+
644
+ // trigger the page view event
645
+ if (this.shouldTrackPageView()) {
646
+ this.trackPageView();
647
+
648
+ return;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Determines whether analytics should be checked.
654
+ *
655
+ * @private
656
+ * @returns {Promise<boolean>} A boolean value indicating whether analytics should be checked.
657
+ */
658
+ private shouldFetchNewData(): boolean {
659
+ // If the user close the tab, reload the data
660
+ if (!checkFlagExperimentAlreadyChecked()) {
661
+ this.logger.log(
662
+ `${EXPERIMENT_ALREADY_CHECKED_KEY} not found, fetch data from Analytics`
663
+ );
664
+
665
+ return true;
666
+ }
667
+
668
+ if (!this.experimentsAssigned || this.experimentsAssigned.length === 0) {
669
+ this.logger.log(`No experiments assigned to the client, fetch data from Analytics`);
670
+
671
+ return true;
672
+ }
673
+
674
+ if (!isDataCreateValid()) {
675
+ this.logger.log(
676
+ `The validity period of the persistence has passed, fetch data from Analytics`
677
+ );
678
+
679
+ return true;
680
+ }
681
+
682
+ this.logger.log(`Not should Check Analytics by now...`);
683
+
684
+ return false;
685
+ }
686
+
687
+ /**
688
+ * Retrieves persisted data from the database.
689
+ *
690
+ * @private
691
+ * @returns {Promise<void>} A promise that resolves with no value.
692
+ */
693
+ private async getPersistedData(): Promise<Experiment[]> {
694
+ this.logger.group('Loading Persisted Data');
695
+ this.logger.time('Loading Time');
696
+
697
+ let storedData: Experiment[] = [];
698
+
699
+ try {
700
+ storedData = await this.persistenceHandler.getData<Experiment[]>();
701
+
702
+ if (storedData) {
703
+ this.logger.log(`Data persisted loaded, ${storedData.length} experiments loaded`);
704
+ } else {
705
+ this.logger.log(`No persisted data found, let's fetch from the server.`);
706
+ }
707
+ } catch (error) {
708
+ this.logger.warn(`Error loading persisted data: ${error}`);
709
+ } finally {
710
+ this.logger.timeEnd('Loading Time');
711
+ this.logger.groupEnd();
712
+ }
713
+
714
+ return storedData;
715
+ }
716
+ }