@ninetailed/experience.js 7.11.0 → 7.12.0-beta.0

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/index.esm.js CHANGED
@@ -54,6 +54,7 @@ const ANONYMOUS_ID = '__nt_anonymous_id__';
54
54
  const DEBUG_FLAG = '__nt_debug__';
55
55
  const PROFILE_FALLBACK_CACHE = '__nt_profile__';
56
56
  const EXPERIENCES_FALLBACK_CACHE = '__nt_experiences__';
57
+ const CHANGES_FALLBACK_CACHE = '__nt_changes__';
57
58
  const PROFILE_CHANGE = 'profile-change';
58
59
  const PROFILE_RESET = 'profile-reset';
59
60
  const CONSENT = '__nt-consent__';
@@ -110,14 +111,12 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
110
111
  instance.dispatch({
111
112
  type: PROFILE_RESET
112
113
  });
113
- instance.storage.removeItem(ANONYMOUS_ID);
114
- instance.storage.removeItem(PROFILE_FALLBACK_CACHE);
115
- instance.storage.removeItem(EXPERIENCES_FALLBACK_CACHE);
114
+ _this.clearCaches();
116
115
  logger.debug('Removed old profile data from localstorage.');
117
116
  if (typeof _this.onInitProfileId === 'function') {
118
117
  const profileId = await _this.onInitProfileId(undefined);
119
118
  if (typeof profileId === 'string') {
120
- instance.storage.setItem(ANONYMOUS_ID, profileId);
119
+ _this.setAnonymousId(profileId);
121
120
  }
122
121
  }
123
122
  await _this.ninetailed.track('nt_reset');
@@ -163,13 +162,13 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
163
162
  const legacyAnonymousId = instance.storage.getItem(LEGACY_ANONYMOUS_ID);
164
163
  if (legacyAnonymousId) {
165
164
  logger.debug('Found legacy anonymousId, migrating to new one.', legacyAnonymousId);
166
- instance.storage.setItem(ANONYMOUS_ID, legacyAnonymousId);
165
+ this.setAnonymousId(legacyAnonymousId);
167
166
  instance.storage.removeItem(LEGACY_ANONYMOUS_ID);
168
167
  }
169
168
  if (typeof this.onInitProfileId === 'function') {
170
- const profileId = await this.onInitProfileId(instance.storage.getItem(ANONYMOUS_ID));
169
+ const profileId = await this.onInitProfileId(this.getAnonymousId());
171
170
  if (typeof profileId === 'string') {
172
- instance.storage.setItem(ANONYMOUS_ID, profileId);
171
+ this.setAnonymousId(profileId);
173
172
  }
174
173
  }
175
174
  logger.debug('Ninetailed Core plugin initialized.');
@@ -267,6 +266,61 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
267
266
  }
268
267
  return this._instance;
269
268
  }
269
+ getAnonymousId() {
270
+ var _ref;
271
+ return (_ref = this.instance.storage.getItem(ANONYMOUS_ID)) != null ? _ref : undefined;
272
+ }
273
+ setAnonymousId(id) {
274
+ this.instance.storage.setItem(ANONYMOUS_ID, id);
275
+ }
276
+ clearAnonymousId() {
277
+ this.instance.storage.removeItem(ANONYMOUS_ID);
278
+ }
279
+ setFallbackProfile(profile) {
280
+ this.instance.storage.setItem(PROFILE_FALLBACK_CACHE, profile);
281
+ }
282
+ getFallbackProfile() {
283
+ var _ref2;
284
+ return (_ref2 = this.instance.storage.getItem(PROFILE_FALLBACK_CACHE)) != null ? _ref2 : undefined;
285
+ }
286
+ clearFallbackProfile() {
287
+ this.instance.storage.removeItem(PROFILE_FALLBACK_CACHE);
288
+ }
289
+ setFallbackExperiences(experiences) {
290
+ this.instance.storage.setItem(EXPERIENCES_FALLBACK_CACHE, experiences);
291
+ }
292
+ getFallbackExperiences() {
293
+ return this.instance.storage.getItem(EXPERIENCES_FALLBACK_CACHE) || [];
294
+ }
295
+ clearFallbackExperiences() {
296
+ this.instance.storage.removeItem(EXPERIENCES_FALLBACK_CACHE);
297
+ }
298
+ setFallbackChanges(changes) {
299
+ this.instance.storage.setItem(CHANGES_FALLBACK_CACHE, changes);
300
+ }
301
+ getFallbackChanges() {
302
+ return this.instance.storage.getItem(CHANGES_FALLBACK_CACHE) || [];
303
+ }
304
+ clearFallbackChanges() {
305
+ this.instance.storage.removeItem(CHANGES_FALLBACK_CACHE);
306
+ }
307
+ clearCaches() {
308
+ this.clearAnonymousId();
309
+ this.clearFallbackProfile();
310
+ this.clearFallbackExperiences();
311
+ this.clearFallbackChanges();
312
+ }
313
+ populateCaches({
314
+ experiences,
315
+ profile,
316
+ anonymousId,
317
+ changes
318
+ }) {
319
+ this.setAnonymousId(anonymousId);
320
+ this.setFallbackProfile(profile);
321
+ this.setFallbackExperiences(experiences);
322
+ this.setFallbackChanges(changes);
323
+ }
270
324
  async _flush() {
271
325
  const events = Object.assign([], this.queue);
272
326
  logger.info('Start flushing events.');
@@ -277,10 +331,11 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
277
331
  };
278
332
  }
279
333
  try {
280
- const anonymousId = this.instance.storage.getItem(ANONYMOUS_ID);
334
+ const anonymousId = this.getAnonymousId();
281
335
  const {
282
336
  profile,
283
- experiences
337
+ experiences,
338
+ changes
284
339
  } = await this.apiClient.upsertProfile({
285
340
  profileId: anonymousId,
286
341
  events
@@ -288,15 +343,20 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
288
343
  locale: this.locale,
289
344
  enabledFeatures: this.enabledFeatures
290
345
  });
291
- this.instance.storage.setItem(ANONYMOUS_ID, profile.id);
292
- this.instance.storage.setItem(PROFILE_FALLBACK_CACHE, profile);
293
- this.instance.storage.setItem(EXPERIENCES_FALLBACK_CACHE, experiences);
346
+ this.populateCaches({
347
+ anonymousId: profile.id,
348
+ profile,
349
+ experiences,
350
+ changes
351
+ });
294
352
  logger.debug('Profile from api: ', profile);
295
353
  logger.debug('Experiences from api: ', experiences);
296
354
  this.instance.dispatch({
297
355
  type: PROFILE_CHANGE,
298
356
  profile,
299
- experiences
357
+ experiences,
358
+ changes,
359
+ error: undefined
300
360
  });
301
361
  await delay(20);
302
362
  return {
@@ -304,22 +364,26 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
304
364
  };
305
365
  } catch (error) {
306
366
  logger.debug('An error occurred during flushing the events: ', error);
307
- const fallbackProfile = this.instance.storage.getItem(PROFILE_FALLBACK_CACHE);
308
- const fallbackExperiences = this.instance.storage.getItem(EXPERIENCES_FALLBACK_CACHE) || [];
367
+ const fallbackProfile = this.getFallbackProfile();
368
+ const fallbackExperiences = this.getFallbackExperiences();
369
+ const fallbackChanges = this.getFallbackChanges();
309
370
  if (fallbackProfile) {
310
371
  logger.debug('Found a fallback profile - will use this.');
311
372
  this.instance.dispatch({
312
373
  type: PROFILE_CHANGE,
313
374
  profile: fallbackProfile,
314
- experiences: fallbackExperiences
375
+ experiences: fallbackExperiences,
376
+ changes: fallbackChanges,
377
+ error: undefined
315
378
  });
316
379
  } else {
317
380
  logger.debug('No fallback profile found - setting profile to null.');
318
381
  this.instance.dispatch({
319
382
  type: PROFILE_CHANGE,
320
383
  profile: null,
384
+ changes: fallbackChanges,
321
385
  experiences: fallbackExperiences,
322
- error
386
+ error: error
323
387
  });
324
388
  }
325
389
  return {
@@ -400,19 +464,17 @@ const isInterestedInHiddenPage = arg => {
400
464
  };
401
465
 
402
466
  const decodeExperienceVariantsMap = encodedExperienceVariantsMap => {
403
- return encodedExperienceVariantsMap.split(',').map(experienceIdWithVariant => {
404
- const [experienceId, _variantIndex] = experienceIdWithVariant.split('=');
405
- const variantIndex = parseInt(_variantIndex);
467
+ const experientVariantsAssignments = encodedExperienceVariantsMap.split(',');
468
+ const experienceVariantsMap = {};
469
+ for (const experienceVariantAssignment of experientVariantsAssignments) {
470
+ const [experienceId, variantIndexString] = experienceVariantAssignment.split('=');
471
+ const variantIndex = parseInt(variantIndexString);
406
472
  if (!experienceId || !variantIndex) {
407
- return null;
473
+ continue;
408
474
  }
409
- return {
410
- experienceId,
411
- variantIndex
412
- };
413
- }).filter(x => !!x).reduce((acc, curr) => Object.assign({}, acc, {
414
- [curr.experienceId]: curr.variantIndex
415
- }), {});
475
+ experienceVariantsMap[experienceId] = variantIndex;
476
+ }
477
+ return experienceVariantsMap;
416
478
  };
417
479
 
418
480
  class OnChangeEmitter {
@@ -474,6 +536,28 @@ const createPassThroughMiddleware = () => {
474
536
  };
475
537
  };
476
538
  };
539
+ function createExperienceSelectionMiddleware({
540
+ plugins,
541
+ experiences,
542
+ baseline,
543
+ profile
544
+ }) {
545
+ if (profile === null) {
546
+ return createPassThroughMiddleware();
547
+ }
548
+ const pluginsWithMiddleware = selectPluginsHavingExperienceSelectionMiddleware(plugins);
549
+ const middlewareFunctions = [];
550
+ for (const plugin of pluginsWithMiddleware) {
551
+ const middleware = plugin.getExperienceSelectionMiddleware({
552
+ experiences,
553
+ baseline
554
+ });
555
+ if (middleware !== undefined) {
556
+ middlewareFunctions.push(middleware);
557
+ }
558
+ }
559
+ return pipe(...middlewareFunctions);
560
+ }
477
561
  const makeExperienceSelectMiddleware = ({
478
562
  plugins,
479
563
  onChange,
@@ -483,18 +567,12 @@ const makeExperienceSelectMiddleware = ({
483
567
  }) => {
484
568
  let removeChangeListeners = [];
485
569
  const pluginsHavingChangeEmitters = selectPluginsHavingOnChangeEmitter(plugins);
486
- const prepareMiddleware = () => {
487
- if (profile === null) {
488
- return createPassThroughMiddleware();
489
- }
490
- const pluginsWithMiddleware = selectPluginsHavingExperienceSelectionMiddleware(plugins);
491
- const middlewareFunctions = pluginsWithMiddleware.map(plugin => plugin.getExperienceSelectionMiddleware({
492
- experiences,
493
- baseline
494
- })).filter(result => typeof result !== 'undefined');
495
- return pipe(...middlewareFunctions);
496
- };
497
- const middleware = prepareMiddleware();
570
+ const middleware = createExperienceSelectionMiddleware({
571
+ plugins,
572
+ experiences,
573
+ baseline,
574
+ profile
575
+ });
498
576
  const addListeners = () => {
499
577
  removeChangeListeners = pluginsHavingChangeEmitters.map(plugin => {
500
578
  const listener = () => {
@@ -503,8 +581,13 @@ const makeExperienceSelectMiddleware = ({
503
581
  return plugin.onChangeEmitter.addListener(listener);
504
582
  });
505
583
  };
584
+
585
+ // WARNING: This specific implementation using forEach is required.
586
+ // DO NOT replace with for...of or other loop constructs as they will break functionality.
587
+ // The exact reason is uncertain but appears related to the transplier.
588
+ // TODO: Come back and find out why this is the case, maybe a version bump is in order.
506
589
  const removeListeners = () => {
507
- removeChangeListeners.forEach(listener => listener());
590
+ removeChangeListeners.forEach(removeListener => removeListener());
508
591
  };
509
592
  return {
510
593
  addListeners,
@@ -518,44 +601,36 @@ class EventBuilder {
518
601
  this.buildRequestContext = void 0;
519
602
  this.buildRequestContext = buildRequestContext || buildClientNinetailedRequestContext;
520
603
  }
521
- page(properties, data) {
522
- return buildPageEvent(Object.assign({
604
+ buildEventBase(data) {
605
+ return Object.assign({
523
606
  messageId: (data == null ? void 0 : data.messageId) || v4()
524
607
  }, data, {
525
608
  timestamp: Date.now(),
526
- properties: properties || {},
527
609
  ctx: this.buildRequestContext()
610
+ });
611
+ }
612
+ page(properties, data) {
613
+ return buildPageEvent(Object.assign({}, this.buildEventBase(data), {
614
+ properties: properties || {}
528
615
  }));
529
616
  }
530
617
  track(event, properties, data) {
531
- return buildTrackEvent(Object.assign({
532
- messageId: (data == null ? void 0 : data.messageId) || v4(),
533
- timestamp: Date.now()
534
- }, data, {
618
+ return buildTrackEvent(Object.assign({}, this.buildEventBase(data), {
535
619
  event,
536
- properties: properties || {},
537
- ctx: this.buildRequestContext()
620
+ properties: properties || {}
538
621
  }));
539
622
  }
540
623
  identify(userId, traits, data) {
541
- return buildIdentifyEvent(Object.assign({
542
- messageId: (data == null ? void 0 : data.messageId) || v4(),
543
- timestamp: Date.now()
544
- }, data, {
624
+ return buildIdentifyEvent(Object.assign({}, this.buildEventBase(data), {
545
625
  traits: traits || {},
546
- userId: userId || '',
547
- ctx: this.buildRequestContext()
626
+ userId: userId || ''
548
627
  }));
549
628
  }
550
629
  component(componentId, experienceId, variantIndex, data) {
551
- return buildComponentViewEvent(Object.assign({
552
- messageId: (data == null ? void 0 : data.messageId) || v4(),
553
- timestamp: Date.now()
554
- }, data, {
630
+ return buildComponentViewEvent(Object.assign({}, this.buildEventBase(data), {
555
631
  componentId,
556
632
  experienceId: experienceId || '',
557
- variantIndex: variantIndex || 0,
558
- ctx: this.buildRequestContext()
633
+ variantIndex: variantIndex || 0
559
634
  }));
560
635
  }
561
636
  }
@@ -628,11 +703,7 @@ class Ninetailed {
628
703
  await _this.instance.page(data, _this.buildOptions(options));
629
704
  return _this.ninetailedCorePlugin.flush();
630
705
  } catch (error) {
631
- logger.error(error);
632
- if (error instanceof RangeError) {
633
- throw new Error(`[Validation Error] "page" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
634
- }
635
- throw error;
706
+ return _this.handleMethodError(error, 'page');
636
707
  }
637
708
  };
638
709
  this.track = async function (event, properties, options) {
@@ -645,11 +716,7 @@ class Ninetailed {
645
716
  await _this.instance.track(event.toString(), result.data, _this.buildOptions(options));
646
717
  return _this.ninetailedCorePlugin.flush();
647
718
  } catch (error) {
648
- logger.error(error);
649
- if (error instanceof RangeError) {
650
- throw new Error(`[Validation Error] "track" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
651
- }
652
- throw error;
719
+ _this.handleMethodError(error, 'track');
653
720
  }
654
721
  };
655
722
  this.identify = async function (uid, traits, options) {
@@ -662,11 +729,7 @@ class Ninetailed {
662
729
  await _this.instance.identify(uid && uid.toString() !== '' ? uid.toString() : EMPTY_MERGE_ID, result.data, _this.buildOptions(options));
663
730
  return _this.ninetailedCorePlugin.flush();
664
731
  } catch (error) {
665
- logger.error(error);
666
- if (error instanceof RangeError) {
667
- throw new Error(`[Validation Error] "identify" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
668
- }
669
- throw error;
732
+ _this.handleMethodError(error, 'identify');
670
733
  }
671
734
  };
672
735
  this.batch = async function (events) {
@@ -695,11 +758,7 @@ class Ninetailed {
695
758
  await Promise.all(promises);
696
759
  return _this.ninetailedCorePlugin.flush();
697
760
  } catch (error) {
698
- logger.error(error);
699
- if (error instanceof RangeError) {
700
- throw new Error(`[Validation Error] "batch" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
701
- }
702
- throw error;
761
+ _this.handleMethodError(error, 'batch');
703
762
  }
704
763
  };
705
764
  this.trackStickyComponentView = async function ({
@@ -717,11 +776,7 @@ class Ninetailed {
717
776
  });
718
777
  return _this.ninetailedCorePlugin.flush();
719
778
  } catch (error) {
720
- logger.error(error);
721
- if (error instanceof RangeError) {
722
- throw new Error(`[Validation Error] "trackStickyComponentView" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
723
- }
724
- throw error;
779
+ _this.handleMethodError(error, 'trackStickyComponentView');
725
780
  }
726
781
  };
727
782
  /**
@@ -744,33 +799,13 @@ class Ninetailed {
744
799
  const {
745
800
  element
746
801
  } = payload,
747
- remaingPayload = _objectWithoutPropertiesLoose(payload, _excluded2);
802
+ remainingPayload = _objectWithoutPropertiesLoose(payload, _excluded2);
748
803
  if (!(element instanceof Element)) {
749
- const isObject = typeof element === 'object' && element !== null;
750
- const constructorName = isObject ? element.constructor.name : '';
751
- const isConstructorNameNotObject = constructorName && constructorName !== 'Object';
752
- logger.warn(`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${isConstructorNameNotObject ? ` of type ${constructorName}` : ''}. This call will be ignored.`);
753
- } else {
754
- const existingPayloads = this.observedElements.get(element);
755
- const delays = this.pluginsWithCustomComponentViewThreshold.map(plugin => plugin.getComponentViewTrackingThreshold());
756
- const uniqueDelays = Array.from(new Set([...delays, options == null ? void 0 : options.delay]));
757
- if (!existingPayloads) {
758
- this.observedElements.set(element, [remaingPayload]);
759
- } else {
760
- const isPayloadAlreadyObserved = existingPayloads.some(payload => {
761
- return JSON.stringify(payload) === JSON.stringify(remaingPayload);
762
- });
763
- if (isPayloadAlreadyObserved) {
764
- return;
765
- }
766
- this.observedElements.set(element, [...existingPayloads, remaingPayload]);
767
- }
768
- uniqueDelays.forEach(delay => {
769
- this.elementSeenObserver.observe(element, {
770
- delay
771
- });
772
- });
804
+ this.logInvalidElement(element);
805
+ return;
773
806
  }
807
+ this.storeElementPayload(element, remainingPayload);
808
+ this.setupElementObservation(element, options == null ? void 0 : options.delay);
774
809
  };
775
810
  this.unobserveElement = element => {
776
811
  this.observedElements.delete(element);
@@ -817,6 +852,7 @@ class Ninetailed {
817
852
  cb(Object.assign({}, this._profileState, {
818
853
  status: 'success',
819
854
  profile: payload.profile,
855
+ changes: payload.changes,
820
856
  experiences: payload.experiences,
821
857
  error: null
822
858
  }));
@@ -985,14 +1021,26 @@ class Ninetailed {
985
1021
  removeMiddlewareChangeListeners();
986
1022
  };
987
1023
  };
988
- this.onIsInitialized = onIsInitialized => {
989
- if (typeof onIsInitialized === 'function') {
1024
+ /**
1025
+ * Registers a callback to be notified when changes occur in the profile state.
1026
+ *
1027
+ * @param cb - Callback function that receives the changes state
1028
+ * @returns Function to unsubscribe from changes updates
1029
+ */
1030
+ this.onChangesChange = cb => {
1031
+ this.notifyChangesCallback(cb, this._profileState);
1032
+ return this.onProfileChange(profileState => {
1033
+ this.notifyChangesCallback(cb, profileState);
1034
+ });
1035
+ };
1036
+ this.onIsInitialized = onIsInitializedCallback => {
1037
+ if (typeof onIsInitializedCallback === 'function') {
990
1038
  if (this.isInitialized) {
991
- onIsInitialized();
1039
+ onIsInitializedCallback();
992
1040
  } else {
993
1041
  const detachOnReadyListener = this.instance.on('ready', () => {
994
1042
  this.isInitialized = true;
995
- onIsInitialized();
1043
+ onIsInitializedCallback();
996
1044
  detachOnReadyListener();
997
1045
  });
998
1046
  }
@@ -1072,6 +1120,7 @@ class Ninetailed {
1072
1120
  status: 'loading',
1073
1121
  profile: null,
1074
1122
  experiences: null,
1123
+ changes: null,
1075
1124
  error: null,
1076
1125
  from: 'api'
1077
1126
  };
@@ -1119,6 +1168,72 @@ class Ninetailed {
1119
1168
  get pluginsWithCustomComponentViewThreshold() {
1120
1169
  return [this.ninetailedCorePlugin, ...this.plugins].filter(plugin => hasComponentViewTrackingThreshold(plugin));
1121
1170
  }
1171
+ logInvalidElement(element) {
1172
+ const isObject = typeof element === 'object' && element !== null;
1173
+ const constructorName = isObject ? element.constructor.name : '';
1174
+ const isConstructorNameNotObject = constructorName && constructorName !== 'Object';
1175
+ logger.warn(`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${isConstructorNameNotObject ? ` of type ${constructorName}` : ''}. This call will be ignored.`);
1176
+ }
1177
+ storeElementPayload(element, payload) {
1178
+ const existingPayloads = this.observedElements.get(element) || [];
1179
+
1180
+ // Check if the payload is already being observed for this element
1181
+ const isPayloadAlreadyObserved = existingPayloads.some(existingPayload => JSON.stringify(existingPayload) === JSON.stringify(payload));
1182
+ if (isPayloadAlreadyObserved) {
1183
+ return;
1184
+ }
1185
+
1186
+ // Store the new or updated payloads
1187
+ this.observedElements.set(element, [...existingPayloads, payload]);
1188
+ }
1189
+ setupElementObservation(element, delay) {
1190
+ // Get all relevant delays from plugins and the custom delay
1191
+ const pluginDelays = this.pluginsWithCustomComponentViewThreshold.map(plugin => plugin.getComponentViewTrackingThreshold());
1192
+
1193
+ // Ensure we only observe each delay once
1194
+ const uniqueDelays = Array.from(new Set([...pluginDelays, delay]));
1195
+
1196
+ // Set up observation for each delay
1197
+ uniqueDelays.forEach(delay => {
1198
+ this.elementSeenObserver.observe(element, {
1199
+ delay
1200
+ });
1201
+ });
1202
+ }
1203
+ /**
1204
+ * Helper method to extract changes state from profile state and notify callback
1205
+ * @private
1206
+ */
1207
+ notifyChangesCallback(cb, profileState) {
1208
+ if (profileState.status === 'loading') {
1209
+ cb({
1210
+ status: 'loading',
1211
+ changes: null,
1212
+ error: null
1213
+ });
1214
+ } else if (profileState.status === 'error') {
1215
+ cb({
1216
+ status: 'error',
1217
+ changes: null,
1218
+ error: profileState.error
1219
+ });
1220
+ } else {
1221
+ cb({
1222
+ status: 'success',
1223
+ changes: profileState.changes,
1224
+ error: null
1225
+ });
1226
+ }
1227
+ }
1228
+
1229
+ // Always throws, never returns
1230
+ handleMethodError(error, method) {
1231
+ logger.error(error);
1232
+ if (error instanceof RangeError) {
1233
+ throw new Error(`[Validation Error] "${method}" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
1234
+ }
1235
+ throw error;
1236
+ }
1122
1237
  get profileState() {
1123
1238
  return this._profileState;
1124
1239
  }
@@ -1228,4 +1343,4 @@ const selectVariant = (baseline, variants, {
1228
1343
  };
1229
1344
  };
1230
1345
 
1231
- export { ANONYMOUS_ID, COMPONENT, COMPONENT_START, CONSENT, DEBUG_FLAG, EMPTY_MERGE_ID, EXPERIENCES_FALLBACK_CACHE, EventBuilder, HAS_SEEN_STICKY_COMPONENT, LEGACY_ANONYMOUS_ID, Ninetailed, NinetailedCorePlugin, OnChangeEmitter, PAGE_HIDDEN, PLUGIN_NAME, PROFILE_CHANGE, PROFILE_FALLBACK_CACHE, PROFILE_RESET, SET_ENABLED_FEATURES, buildClientNinetailedRequestContext, decodeExperienceVariantsMap, makeExperienceSelectMiddleware, selectPluginsHavingExperienceSelectionMiddleware, selectPluginsHavingOnChangeEmitter, selectVariant };
1346
+ export { ANONYMOUS_ID, CHANGES_FALLBACK_CACHE, COMPONENT, COMPONENT_START, CONSENT, DEBUG_FLAG, EMPTY_MERGE_ID, EXPERIENCES_FALLBACK_CACHE, EventBuilder, HAS_SEEN_STICKY_COMPONENT, LEGACY_ANONYMOUS_ID, Ninetailed, NinetailedCorePlugin, OnChangeEmitter, PAGE_HIDDEN, PLUGIN_NAME, PROFILE_CHANGE, PROFILE_FALLBACK_CACHE, PROFILE_RESET, SET_ENABLED_FEATURES, buildClientNinetailedRequestContext, decodeExperienceVariantsMap, makeExperienceSelectMiddleware, selectPluginsHavingExperienceSelectionMiddleware, selectPluginsHavingOnChangeEmitter, selectVariant };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninetailed/experience.js",
3
- "version": "7.11.0",
3
+ "version": "7.12.0-beta.0",
4
4
  "description": "Ninetailed SDK for javascript",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,8 +9,8 @@
9
9
  "directory": "packages/sdks/javascript"
10
10
  },
11
11
  "dependencies": {
12
- "@ninetailed/experience.js-plugin-analytics": "7.11.0",
13
- "@ninetailed/experience.js-shared": "7.11.0",
12
+ "@ninetailed/experience.js-plugin-analytics": "7.12.0-beta.0",
13
+ "@ninetailed/experience.js-shared": "7.12.0-beta.0",
14
14
  "analytics": "0.8.1",
15
15
  "uuid": "9.0.0"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  /// <reference types="analytics" />
2
2
  import { Locale, Traits, OnLogHandler, OnErrorHandler, Logger, PageviewProperties, Properties, NinetailedApiClient, NinetailedApiClientOptions, NinetailedRequestContext, Reference, Event } from '@ninetailed/experience.js-shared';
3
3
  import { OnInitProfileId } from './NinetailedCorePlugin';
4
- import { EventFunctionOptions, NinetailedInstance, OnIsInitializedCallback, OnProfileChangeCallback, ProfileState, TrackHasSeenComponent, TrackComponentView } from './types';
4
+ import { EventFunctionOptions, NinetailedInstance, OnIsInitializedCallback, OnProfileChangeCallback, ProfileState, TrackHasSeenComponent, TrackComponentView, OnChangesChangeCallback } from './types';
5
5
  import { ObserveOptions } from './ElementSeenObserver';
6
6
  import { OnSelectVariantArgs, OnSelectVariantCallback } from './types/OnSelectVariant';
7
7
  import { ElementSeenPayload, NinetailedPlugin } from '@ninetailed/experience.js-plugin-analytics';
@@ -71,13 +71,29 @@ export declare class Ninetailed implements NinetailedInstance {
71
71
  trackComponentView: TrackComponentView;
72
72
  private get pluginsWithCustomComponentViewThreshold();
73
73
  observeElement: (payload: ElementSeenPayload, options?: ObserveOptions) => void;
74
+ private logInvalidElement;
75
+ private storeElementPayload;
76
+ private setupElementObservation;
74
77
  unobserveElement: (element: Element) => void;
75
78
  private onElementSeen;
76
79
  reset: () => Promise<void>;
77
80
  debug: (enabled: boolean) => Promise<void>;
78
81
  onProfileChange: (cb: OnProfileChangeCallback) => import("analytics").DetachListeners;
79
82
  onSelectVariant: <Baseline extends Reference, Variant extends Reference>({ baseline, experiences }: OnSelectVariantArgs<Baseline, Variant>, cb: OnSelectVariantCallback<Baseline, Variant>) => () => void;
80
- onIsInitialized: (onIsInitialized: OnIsInitializedCallback) => void;
83
+ /**
84
+ * Registers a callback to be notified when changes occur in the profile state.
85
+ *
86
+ * @param cb - Callback function that receives the changes state
87
+ * @returns Function to unsubscribe from changes updates
88
+ */
89
+ onChangesChange: (cb: OnChangesChangeCallback) => import("analytics").DetachListeners;
90
+ /**
91
+ * Helper method to extract changes state from profile state and notify callback
92
+ * @private
93
+ */
94
+ private notifyChangesCallback;
95
+ private handleMethodError;
96
+ onIsInitialized: (onIsInitializedCallback: OnIsInitializedCallback) => void;
81
97
  private waitUntilInitialized;
82
98
  get profileState(): ProfileState;
83
99
  private buildOptions;
@@ -1,9 +1,10 @@
1
- import { AnalyticsInstance } from 'analytics';
1
+ import { AnalyticsInstance, DetachListeners } from 'analytics';
2
2
  import { Locale, NinetailedApiClient, NinetailedRequestContext } from '@ninetailed/experience.js-shared';
3
3
  import { NinetailedAnalyticsPlugin, SanitizedElementSeenPayload } from '@ninetailed/experience.js-plugin-analytics';
4
4
  import { SET_ENABLED_FEATURES } from './constants';
5
5
  import { NinetailedInstance, FlushResult } from '../types';
6
6
  import { HAS_SEEN_STICKY_COMPONENT } from '../constants';
7
+ import { DispatchAction, ActionPayloadMap } from './actions';
7
8
  export type OnInitProfileId = (profileId?: string) => Promise<string | undefined> | string | undefined;
8
9
  type AnalyticsPluginNinetailedConfig = {
9
10
  apiClient: NinetailedApiClient;
@@ -16,14 +17,15 @@ type AnalyticsPluginNinetailedConfig = {
16
17
  export declare const PLUGIN_NAME = "ninetailed:core";
17
18
  type EventFn = {
18
19
  payload: any;
19
- instance: InternalAnalyticsInstance;
20
+ instance: EventHandlerAnalyticsInstance;
20
21
  };
21
22
  type AbortableFnParams = {
22
23
  abort: () => void;
23
24
  payload: unknown;
24
25
  };
25
- type InternalAnalyticsInstance = AnalyticsInstance & {
26
- dispatch: (action: any) => void;
26
+ export type EventHandlerAnalyticsInstance = Omit<AnalyticsInstance, 'on' | 'dispatch'> & {
27
+ dispatch: (action: DispatchAction) => Promise<void>;
28
+ on<T extends DispatchAction['type']>(action: T, handler: (data: ActionPayloadMap[T]) => void): DetachListeners;
27
29
  };
28
30
  export interface NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
29
31
  flush: (args: void) => Promise<FlushResult>;
@@ -40,7 +42,7 @@ export declare class NinetailedCorePlugin extends NinetailedAnalyticsPlugin impl
40
42
  private readonly ninetailed;
41
43
  constructor({ apiClient, locale, ninetailed, onInitProfileId, buildClientContext, }: AnalyticsPluginNinetailedConfig);
42
44
  initialize({ instance, }: {
43
- instance: InternalAnalyticsInstance;
45
+ instance: EventHandlerAnalyticsInstance;
44
46
  }): Promise<void>;
45
47
  [SET_ENABLED_FEATURES]: ({ payload }: EventFn) => Promise<void>;
46
48
  pageStart(params: AbortableFnParams): unknown;
@@ -65,6 +67,20 @@ export declare class NinetailedCorePlugin extends NinetailedAnalyticsPlugin impl
65
67
  private static abortNonClientEvents;
66
68
  private get instance();
67
69
  flush: (args: void) => Promise<FlushResult>;
70
+ private getAnonymousId;
71
+ private setAnonymousId;
72
+ private clearAnonymousId;
73
+ private setFallbackProfile;
74
+ private getFallbackProfile;
75
+ private clearFallbackProfile;
76
+ private setFallbackExperiences;
77
+ private getFallbackExperiences;
78
+ private clearFallbackExperiences;
79
+ private setFallbackChanges;
80
+ private getFallbackChanges;
81
+ private clearFallbackChanges;
82
+ private clearCaches;
83
+ private populateCaches;
68
84
  private _flush;
69
85
  }
70
86
  export {};