@absmartly/sdk-plugins 1.1.2 → 1.2.1

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 (98) hide show
  1. package/dist/absmartly-dom-changes-core.min.js +1 -1
  2. package/dist/absmartly-dom-changes-lite.min.js +1 -1
  3. package/dist/absmartly-overrides-full.min.js +1 -1
  4. package/dist/absmartly-overrides-lite.min.js +1 -1
  5. package/dist/absmartly-sdk-plugins.min.js +1 -1
  6. package/es/core/DOMChangesPluginLite.js +192 -220
  7. package/es/core/DOMChangesPluginLite.js.map +1 -1
  8. package/es/core/DOMManipulatorLite.js +66 -9
  9. package/es/core/DOMManipulatorLite.js.map +1 -1
  10. package/es/core/ExposureTracker.js +136 -11
  11. package/es/core/ExposureTracker.js.map +1 -1
  12. package/es/entries/dom-with-overrides-lite.js +3 -0
  13. package/es/entries/dom-with-overrides-lite.js.map +1 -1
  14. package/es/index.js +9 -2
  15. package/es/index.js.map +1 -1
  16. package/es/overrides/OverridesPlugin.js +2 -2
  17. package/es/overrides/OverridesPlugin.js.map +1 -1
  18. package/es/overrides/OverridesPluginLite.js +29 -1
  19. package/es/overrides/OverridesPluginLite.js.map +1 -1
  20. package/es/overrides/overridesUtils.js +27 -18
  21. package/es/overrides/overridesUtils.js.map +1 -1
  22. package/es/parsers/VariantExtractor.js +14 -7
  23. package/es/parsers/VariantExtractor.js.map +1 -1
  24. package/es/url-redirect/URLRedirectExtractor.js +185 -0
  25. package/es/url-redirect/URLRedirectExtractor.js.map +1 -0
  26. package/es/url-redirect/URLRedirectMatcher.js +115 -0
  27. package/es/url-redirect/URLRedirectMatcher.js.map +1 -0
  28. package/es/url-redirect/URLRedirectPlugin.js +350 -0
  29. package/es/url-redirect/URLRedirectPlugin.js.map +1 -0
  30. package/es/url-redirect/index.js +5 -0
  31. package/es/url-redirect/index.js.map +1 -0
  32. package/es/url-redirect/types.js +2 -0
  33. package/es/url-redirect/types.js.map +1 -0
  34. package/es/utils/persistence.js +278 -0
  35. package/es/utils/persistence.js.map +1 -0
  36. package/es/utils/plugin-registry.js +47 -0
  37. package/es/utils/plugin-registry.js.map +1 -0
  38. package/lib/core/DOMChangesPluginLite.d.ts +10 -7
  39. package/lib/core/DOMChangesPluginLite.js +192 -220
  40. package/lib/core/DOMChangesPluginLite.js.map +1 -1
  41. package/lib/core/DOMManipulatorLite.js +66 -9
  42. package/lib/core/DOMManipulatorLite.js.map +1 -1
  43. package/lib/core/ExposureTracker.d.ts +6 -0
  44. package/lib/core/ExposureTracker.js +136 -11
  45. package/lib/core/ExposureTracker.js.map +1 -1
  46. package/lib/entries/dom-with-overrides-lite.d.ts +2 -0
  47. package/lib/entries/dom-with-overrides-lite.js +3 -1
  48. package/lib/entries/dom-with-overrides-lite.js.map +1 -1
  49. package/lib/index.d.ts +8 -2
  50. package/lib/index.js +17 -1
  51. package/lib/index.js.map +1 -1
  52. package/lib/overrides/OverridesPlugin.js +2 -2
  53. package/lib/overrides/OverridesPlugin.js.map +1 -1
  54. package/lib/overrides/OverridesPluginLite.d.ts +8 -0
  55. package/lib/overrides/OverridesPluginLite.js +29 -1
  56. package/lib/overrides/OverridesPluginLite.js.map +1 -1
  57. package/lib/overrides/overridesUtils.d.ts +10 -5
  58. package/lib/overrides/overridesUtils.js +28 -18
  59. package/lib/overrides/overridesUtils.js.map +1 -1
  60. package/lib/parsers/VariantExtractor.js +14 -7
  61. package/lib/parsers/VariantExtractor.js.map +1 -1
  62. package/lib/types/index.d.ts +1 -0
  63. package/lib/url-redirect/URLRedirectExtractor.d.ts +16 -0
  64. package/lib/url-redirect/URLRedirectExtractor.js +188 -0
  65. package/lib/url-redirect/URLRedirectExtractor.js.map +1 -0
  66. package/lib/url-redirect/URLRedirectMatcher.d.ts +8 -0
  67. package/lib/url-redirect/URLRedirectMatcher.js +118 -0
  68. package/lib/url-redirect/URLRedirectMatcher.js.map +1 -0
  69. package/lib/url-redirect/URLRedirectPlugin.d.ts +21 -0
  70. package/lib/url-redirect/URLRedirectPlugin.js +353 -0
  71. package/lib/url-redirect/URLRedirectPlugin.js.map +1 -0
  72. package/lib/url-redirect/index.d.ts +4 -0
  73. package/lib/url-redirect/index.js +25 -0
  74. package/lib/url-redirect/index.js.map +1 -0
  75. package/lib/url-redirect/types.d.ts +29 -0
  76. package/lib/url-redirect/types.js +3 -0
  77. package/lib/url-redirect/types.js.map +1 -0
  78. package/lib/utils/persistence.d.ts +23 -0
  79. package/lib/utils/persistence.js +281 -0
  80. package/lib/utils/persistence.js.map +1 -0
  81. package/lib/utils/plugin-registry.d.ts +40 -0
  82. package/lib/utils/plugin-registry.js +53 -0
  83. package/lib/utils/plugin-registry.js.map +1 -0
  84. package/package.json +59 -3
  85. package/types/core/DOMChangesPluginLite.d.ts +10 -7
  86. package/types/core/ExposureTracker.d.ts +6 -0
  87. package/types/entries/dom-with-overrides-lite.d.ts +2 -0
  88. package/types/index.d.ts +8 -2
  89. package/types/overrides/OverridesPluginLite.d.ts +8 -0
  90. package/types/overrides/overridesUtils.d.ts +10 -5
  91. package/types/types/index.d.ts +1 -0
  92. package/types/url-redirect/URLRedirectExtractor.d.ts +16 -0
  93. package/types/url-redirect/URLRedirectMatcher.d.ts +8 -0
  94. package/types/url-redirect/URLRedirectPlugin.d.ts +21 -0
  95. package/types/url-redirect/index.d.ts +4 -0
  96. package/types/url-redirect/types.d.ts +29 -0
  97. package/types/utils/persistence.d.ts +23 -0
  98. package/types/utils/plugin-registry.d.ts +40 -0
@@ -79,6 +79,8 @@ import { ExposureTracker } from './ExposureTracker';
79
79
  import { HTMLInjector } from './HTMLInjector';
80
80
  import { logDebug, logExperimentSummary, logPerformance, DEBUG } from '../utils/debug';
81
81
  import { URLMatcher } from '../utils/URLMatcher';
82
+ import { DOMPersistenceManager } from '../utils/persistence';
83
+ import { registerPlugin, unregisterPlugin } from '../utils/plugin-registry';
82
84
  var DOMChangesPluginLite = /** @class */ (function () {
83
85
  function DOMChangesPluginLite(config) {
84
86
  var _this = this;
@@ -88,11 +90,7 @@ var DOMChangesPluginLite = /** @class */ (function () {
88
90
  this.eventListeners = new Map();
89
91
  this.styleManagers = new Map();
90
92
  this.initialized = false;
91
- this.watchedElements = new WeakMap();
92
- this.persistenceObserver = null;
93
- this.reapplyingElements = new Set();
94
- this.reapplyLogThrottle = new Map();
95
- this.appliedChanges = new Map();
93
+ this.persistenceManager = null;
96
94
  this.antiFlickerTimeout = null;
97
95
  this.antiFlickerStyleId = 'absmartly-antiflicker';
98
96
  this.config = {
@@ -117,6 +115,17 @@ var DOMChangesPluginLite = /** @class */ (function () {
117
115
  this.variantExtractor = new VariantExtractor(this.config.context, this.config.variableName, this.config.debug);
118
116
  this.exposureTracker = new ExposureTracker(this.config.context, this.config.debug);
119
117
  this.htmlInjector = new HTMLInjector(this.config.debug);
118
+ this.persistenceManager = new DOMPersistenceManager({
119
+ debug: this.config.debug,
120
+ onReapply: function (change, experimentName) {
121
+ logDebug('[DOMChangesPluginLite] Re-applying change due to mutation', {
122
+ experimentName: experimentName,
123
+ selector: change.selector,
124
+ type: change.type,
125
+ });
126
+ _this.domManipulator.applyChange(change, experimentName);
127
+ },
128
+ });
120
129
  // Auto-initialize when context is ready
121
130
  this.readyPromise = this.config.context
122
131
  .ready()
@@ -160,9 +169,14 @@ var DOMChangesPluginLite = /** @class */ (function () {
160
169
  case 1:
161
170
  _a.trys.push([1, 4, , 5]);
162
171
  if (this.config.spa) {
172
+ // SPA mode: observe body for React/Vue hydration recovery
173
+ // and listen for URL changes to re-apply changes on navigation
163
174
  this.setupMutationObserver();
164
175
  this.setupURLChangeListener();
165
176
  }
177
+ else {
178
+ logDebug('[DOMChangesPluginLite] SPA mode disabled - skipping body observer');
179
+ }
166
180
  if (!this.config.autoApply) return [3 /*break*/, 3];
167
181
  // Apply HTML injections and DOM changes in parallel to minimize flickering
168
182
  return [4 /*yield*/, this.applyInjectionsAndChanges()];
@@ -173,6 +187,7 @@ var DOMChangesPluginLite = /** @class */ (function () {
173
187
  case 3:
174
188
  this.initialized = true;
175
189
  this.registerWithContext();
190
+ this.registerGlobally();
176
191
  this.emit('initialized');
177
192
  duration = performance.now() - startTime;
178
193
  logPerformance('Plugin initialization', duration);
@@ -198,10 +213,12 @@ var DOMChangesPluginLite = /** @class */ (function () {
198
213
  if (mutation.type === 'childList') {
199
214
  // Check if any added nodes match our applied change selectors
200
215
  mutation.addedNodes.forEach(function (node) {
216
+ var _a;
201
217
  if (node.nodeType === Node.ELEMENT_NODE) {
202
218
  var element_1 = node;
203
219
  // Check all experiments with applied changes (not just style changes)
204
- _this.appliedChanges.forEach(function (changes, experimentName) {
220
+ var appliedChanges = ((_a = _this.persistenceManager) === null || _a === void 0 ? void 0 : _a.getAppliedChanges()) || new Map();
221
+ appliedChanges.forEach(function (changes, experimentName) {
205
222
  changes.forEach(function (change) {
206
223
  try {
207
224
  // Check if this new element or any of its descendants match the selector
@@ -231,11 +248,30 @@ var DOMChangesPluginLite = /** @class */ (function () {
231
248
  }
232
249
  });
233
250
  });
234
- observer.observe(document.body, {
235
- childList: true,
236
- subtree: true,
237
- });
238
- this.mutationObserver = observer;
251
+ // Wait for document.body to exist before observing
252
+ // This prevents "parameter 1 is not of type 'Node'" errors when SDK loads in <head>
253
+ var startObserving = function () {
254
+ observer.observe(document.body, {
255
+ childList: true,
256
+ subtree: true,
257
+ });
258
+ _this.mutationObserver = observer;
259
+ logDebug('[DOMChangesPluginLite] MutationObserver started on document.body');
260
+ };
261
+ if (document.body) {
262
+ startObserving();
263
+ }
264
+ else {
265
+ // Use MutationObserver on documentElement to detect when body is added
266
+ var bodyObserver = new MutationObserver(function (_mutations, obs) {
267
+ if (document.body) {
268
+ obs.disconnect();
269
+ startObserving();
270
+ }
271
+ });
272
+ bodyObserver.observe(document.documentElement, { childList: true });
273
+ logDebug('[DOMChangesPluginLite] Waiting for document.body...');
274
+ }
239
275
  };
240
276
  /**
241
277
  * Set up URL change listener for SPA mode
@@ -302,7 +338,9 @@ var DOMChangesPluginLite = /** @class */ (function () {
302
338
  // The stylesheets will be reused or cleared on next applyChanges
303
339
  this.styleManagers.clear();
304
340
  // Clear applied changes
305
- this.appliedChanges.clear();
341
+ if (this.persistenceManager) {
342
+ this.persistenceManager.clearAll();
343
+ }
306
344
  // Clear HTML injections
307
345
  this.htmlInjector.destroy();
308
346
  // Clear variant extractor cache to force re-extraction
@@ -419,22 +457,46 @@ var DOMChangesPluginLite = /** @class */ (function () {
419
457
  allExperiments = this.getAllExperimentsData();
420
458
  totalApplied = 0;
421
459
  experimentStats = new Map();
422
- logDebug('[ABsmartly] Experiments to process:', Array.from(allExperiments.keys()));
423
- logDebug('[ABsmartly] Total experiments with changes:', allExperiments.size);
460
+ logDebug('[APPLY-CHANGES] Experiments found with DOM changes:', {
461
+ experimentNames: Array.from(allExperiments.keys()),
462
+ totalCount: allExperiments.size,
463
+ currentURL: currentURL,
464
+ });
465
+ if (allExperiments.size === 0) {
466
+ logDebug('[APPLY-CHANGES] ⚠️ No experiments found with DOM changes - nothing to process');
467
+ }
424
468
  try {
425
469
  for (allExperiments_1 = __values(allExperiments), allExperiments_1_1 = allExperiments_1.next(); !allExperiments_1_1.done; allExperiments_1_1 = allExperiments_1.next()) {
426
470
  _a = __read(allExperiments_1_1.value, 2), expName = _a[0], experimentData = _a[1];
471
+ logDebug("[APPLY-CHANGES] [".concat(expName, "] Starting to process experiment"), {
472
+ experimentName: expName,
473
+ currentURL: currentURL,
474
+ });
427
475
  // Skip if filtering by specific experiment
428
476
  if (experimentName && expName !== experimentName) {
477
+ logDebug("[APPLY-CHANGES] [".concat(expName, "] \u23ED\uFE0F Skipping - filtering for different experiment"), {
478
+ filteringFor: experimentName,
479
+ });
429
480
  continue;
430
481
  }
431
482
  currentVariant = this.config.context.peek(expName);
483
+ if (currentVariant === undefined || currentVariant === null) {
484
+ logDebug("[APPLY-CHANGES] [".concat(expName, "] \u23ED\uFE0F Skipping - no variant assigned (peek returned ").concat(currentVariant, ")"), {
485
+ experimentName: expName,
486
+ });
487
+ continue;
488
+ }
489
+ logDebug("[APPLY-CHANGES] [".concat(expName, "] User assigned to variant ").concat(currentVariant));
432
490
  variantData = experimentData.variantData, urlFilter = experimentData.urlFilter, globalDefaults = experimentData.globalDefaults;
433
491
  anyVariantMatchesURL = this.variantExtractor.anyVariantMatchesURL(expName, currentURL);
434
492
  if (!anyVariantMatchesURL) {
435
- logDebug("[ABsmartly] Skipping experiment '".concat(expName, "' - no variant matches URL: ").concat(currentURL));
493
+ logDebug("[APPLY-CHANGES] [".concat(expName, "] \u23ED\uFE0F Skipping - no variant matches URL: ").concat(currentURL), {
494
+ experimentName: expName,
495
+ currentURL: currentURL,
496
+ });
436
497
  continue;
437
498
  }
499
+ logDebug("[APPLY-CHANGES] [".concat(expName, "] \u2713 At least one variant matches current URL"));
438
500
  shouldApplyVisualChanges = this.shouldApplyVisualChanges(variantData, urlFilter, currentURL);
439
501
  changes = this.extractChangesFromData(variantData, globalDefaults);
440
502
  allVariantChanges = this.extractAllVariantChanges(expName);
@@ -448,7 +510,9 @@ var DOMChangesPluginLite = /** @class */ (function () {
448
510
  }
449
511
  stats = { total: (changes === null || changes === void 0 ? void 0 : changes.length) || 0, success: 0, pending: 0 };
450
512
  if (this.config.debug) {
451
- logDebug("[ABsmartly] Processing experiment '".concat(expName, "' (variant ").concat(currentVariant, "):"), {
513
+ logDebug("[ABsmartly] Processing experiment '".concat(expName, "' - User is in variant ").concat(currentVariant, ":"), {
514
+ experimentName: expName,
515
+ userVariant: currentVariant,
452
516
  urlMatches: shouldApplyVisualChanges,
453
517
  changeCount: (changes === null || changes === void 0 ? void 0 : changes.length) || 0,
454
518
  userVariantHasChanges: ((changes === null || changes === void 0 ? void 0 : changes.length) || 0) > 0,
@@ -459,11 +523,28 @@ var DOMChangesPluginLite = /** @class */ (function () {
459
523
  }); })) || [],
460
524
  });
461
525
  }
526
+ // Log if user is in control variant (no changes)
527
+ if (!changes || changes.length === 0) {
528
+ if (this.config.debug) {
529
+ logDebug("[ABsmartly] User is in control variant ".concat(currentVariant, " with no changes, but will still track for SRM prevention"), {
530
+ experimentName: expName,
531
+ currentVariant: currentVariant,
532
+ });
533
+ }
534
+ }
462
535
  // Apply visual changes only if URL matches for user's variant AND user has changes
463
536
  if (shouldApplyVisualChanges && changes && changes.length > 0) {
464
537
  try {
465
538
  for (changes_1 = (e_2 = void 0, __values(changes)), changes_1_1 = changes_1.next(); !changes_1_1.done; changes_1_1 = changes_1.next()) {
466
539
  change = changes_1_1.value;
540
+ // Skip delete changes with viewport triggers - they must be applied AFTER exposure
541
+ // Otherwise the element is removed before it can be observed
542
+ if (change.type === 'delete' && change.trigger_on_view) {
543
+ if (this.config.debug) {
544
+ logDebug("[ABsmartly] Deferring delete change until after viewport trigger for '".concat(expName, "'"), { selector: change.selector });
545
+ }
546
+ continue;
547
+ }
467
548
  success = this.domManipulator.applyChange(change, expName);
468
549
  if (success) {
469
550
  totalApplied++;
@@ -581,14 +662,41 @@ var DOMChangesPluginLite = /** @class */ (function () {
581
662
  finally { if (e_3) throw e_3.error; }
582
663
  }
583
664
  if (this.config.debug) {
584
- logDebug("[ABsmartly] Final trigger decision for '".concat(expName, "': hasImmediate=").concat(hasAnyImmediateTriggerInAnyVariant, ", hasViewport=").concat(hasAnyViewportTriggerInAnyVariant));
665
+ logDebug("[TRIGGER-DETECTION] [".concat(expName, "] Final trigger decision for '").concat(expName, "':"), {
666
+ experimentName: expName,
667
+ userVariant: currentVariant,
668
+ hasImmediateTrigger: hasAnyImmediateTriggerInAnyVariant,
669
+ hasViewportTrigger: hasAnyViewportTriggerInAnyVariant,
670
+ willRegisterExperiment: hasAnyViewportTriggerInAnyVariant || hasAnyImmediateTriggerInAnyVariant,
671
+ userChangesCount: (changes === null || changes === void 0 ? void 0 : changes.length) || 0,
672
+ allVariantsChangesCount: allVariantChanges.map(function (vc) { return vc.length; }),
673
+ });
585
674
  }
586
675
  // CRITICAL: Always register experiment for tracking if ANY variant has ANY trigger type
587
676
  // This prevents SRM even when user's variant doesn't match URL filter
588
677
  // Pass the URL-filtered trigger flags to ExposureTracker
589
678
  if (hasAnyViewportTriggerInAnyVariant || hasAnyImmediateTriggerInAnyVariant) {
679
+ logDebug("[TRIGGER-DETECTION] [".concat(expName, "] \u2713 REGISTERING EXPERIMENT - Calling exposureTracker.registerExperiment()"), {
680
+ experimentName: expName,
681
+ userVariant: currentVariant,
682
+ hasImmediateTrigger: hasAnyImmediateTriggerInAnyVariant,
683
+ hasViewportTrigger: hasAnyViewportTriggerInAnyVariant,
684
+ changesForUser: (changes === null || changes === void 0 ? void 0 : changes.length) || 0,
685
+ allVariantCounts: allVariantChanges.map(function (vc, idx) { return ({
686
+ variant: idx,
687
+ count: vc.length,
688
+ }); }),
689
+ });
590
690
  this.exposureTracker.registerExperiment(expName, currentVariant || 0, changes || [], allVariantChanges, hasAnyImmediateTriggerInAnyVariant, hasAnyViewportTriggerInAnyVariant);
591
691
  }
692
+ else {
693
+ logDebug("[TRIGGER-DETECTION] [".concat(expName, "] \u26A0\uFE0F No triggers detected - experiment will NOT be registered!"), {
694
+ experimentName: expName,
695
+ userVariant: currentVariant,
696
+ hasAnyChangesInAnyVariant: hasAnyChangesInAnyVariant,
697
+ allVariantsChangesCount: allVariantChanges.map(function (vc) { return vc.length; }),
698
+ });
699
+ }
592
700
  // Note: We do NOT call treatment() here anymore to avoid duplicate calls.
593
701
  // The ExposureTracker.registerExperiment() will call triggerExposure() internally
594
702
  // when hasImmediateTrigger is true (see ExposureTracker line 169-174).
@@ -644,18 +752,43 @@ var DOMChangesPluginLite = /** @class */ (function () {
644
752
  var e_5, _a;
645
753
  var experiments = new Map();
646
754
  var allVariants = this.variantExtractor.extractAllChanges();
755
+ logDebug('[GET-EXPERIMENTS] Extracting experiment data from context', {
756
+ experimentsWithChanges: Array.from(allVariants.keys()),
757
+ totalCount: allVariants.size,
758
+ });
647
759
  try {
648
760
  for (var allVariants_1 = __values(allVariants), allVariants_1_1 = allVariants_1.next(); !allVariants_1_1.done; allVariants_1_1 = allVariants_1.next()) {
649
761
  var _b = __read(allVariants_1_1.value, 1), expName = _b[0];
650
762
  var currentVariant = this.config.context.peek(expName);
763
+ logDebug("[GET-EXPERIMENTS] [".concat(expName, "] Checking experiment"), {
764
+ experimentName: expName,
765
+ currentVariant: currentVariant,
766
+ variantIsValid: currentVariant !== undefined && currentVariant !== null,
767
+ });
651
768
  if (currentVariant === undefined || currentVariant === null) {
769
+ logDebug("[GET-EXPERIMENTS] [".concat(expName, "] \u23ED\uFE0F Skipping - context.peek() returned ").concat(currentVariant));
652
770
  continue;
653
771
  }
654
772
  var variantsData = this.variantExtractor.getAllVariantsData(expName);
655
773
  var variantData = variantsData.get(currentVariant);
774
+ if (this.config.debug) {
775
+ logDebug("[GET-EXPERIMENTS] [".concat(expName, "] Variant data lookup:"), {
776
+ experimentName: expName,
777
+ userVariant: currentVariant,
778
+ variantDataExists: !!variantData,
779
+ allVariantsInMap: Array.from(variantsData.keys()),
780
+ userVariantInMap: variantsData.has(currentVariant),
781
+ });
782
+ }
656
783
  if (!variantData) {
657
- continue;
784
+ logDebug("[GET-EXPERIMENTS] [".concat(expName, "] \u26A0\uFE0F User's variant ").concat(currentVariant, " has no DOM changes (control variant)"), {
785
+ availableVariants: Array.from(variantsData.keys()),
786
+ note: 'Will still track for SRM prevention',
787
+ });
788
+ // Don't skip! Include experiments even if user's variant has no changes
789
+ // This is critical for SRM prevention - we need to track ALL experiments
658
790
  }
791
+ logDebug("[GET-EXPERIMENTS] [".concat(expName, "] \u2713 Including experiment (variant ").concat(currentVariant, ")"));
659
792
  // Extract URL filter and global defaults if using wrapped format
660
793
  var urlFilter = null;
661
794
  var globalDefaults = {};
@@ -688,12 +821,15 @@ var DOMChangesPluginLite = /** @class */ (function () {
688
821
  * Extract changes from DOMChangesData and apply global defaults
689
822
  */
690
823
  DOMChangesPluginLite.prototype.extractChangesFromData = function (data, globalDefaults) {
824
+ if (!data) {
825
+ return null;
826
+ }
691
827
  var changes = null;
692
828
  // Extract changes array
693
829
  if (Array.isArray(data)) {
694
830
  changes = data;
695
831
  }
696
- else if (data && typeof data === 'object' && 'changes' in data) {
832
+ else if (typeof data === 'object' && 'changes' in data) {
697
833
  changes = data.changes;
698
834
  }
699
835
  if (!changes || changes.length === 0) {
@@ -750,6 +886,10 @@ var DOMChangesPluginLite = /** @class */ (function () {
750
886
  * Determine if visual changes should be applied based on URL filter
751
887
  */
752
888
  DOMChangesPluginLite.prototype.shouldApplyVisualChanges = function (variantData, urlFilter, url) {
889
+ // No data for this variant - no changes to apply
890
+ if (!variantData) {
891
+ return false;
892
+ }
753
893
  // Legacy array format has no URL filter - always apply
754
894
  if (Array.isArray(variantData)) {
755
895
  return true;
@@ -883,9 +1023,9 @@ var DOMChangesPluginLite = /** @class */ (function () {
883
1023
  this.mutationObserver.disconnect();
884
1024
  this.mutationObserver = null;
885
1025
  }
886
- if (this.persistenceObserver) {
887
- this.persistenceObserver.disconnect();
888
- this.persistenceObserver = null;
1026
+ if (this.persistenceManager) {
1027
+ this.persistenceManager.destroy();
1028
+ this.persistenceManager = null;
889
1029
  }
890
1030
  // Clean up anti-flicker timeout and style
891
1031
  if (this.antiFlickerTimeout !== null) {
@@ -898,11 +1038,8 @@ var DOMChangesPluginLite = /** @class */ (function () {
898
1038
  }
899
1039
  this.eventListeners.clear();
900
1040
  this.exposedExperiments.clear();
901
- this.watchedElements = new WeakMap();
902
- this.appliedChanges.clear();
903
- this.reapplyingElements.clear();
904
- this.reapplyLogThrottle.clear();
905
1041
  this.unregisterFromContext();
1042
+ this.unregisterGlobally();
906
1043
  this.initialized = false;
907
1044
  if (this.config.debug) {
908
1045
  logDebug('[ABsmartly] DOM Changes Plugin Lite destroyed');
@@ -941,6 +1078,31 @@ var DOMChangesPluginLite = /** @class */ (function () {
941
1078
  }
942
1079
  }
943
1080
  };
1081
+ /**
1082
+ * Register plugin in global registry for detection
1083
+ */
1084
+ DOMChangesPluginLite.prototype.registerGlobally = function () {
1085
+ registerPlugin('dom', {
1086
+ name: 'DOMChangesPluginLite',
1087
+ version: DOMChangesPluginLite.VERSION,
1088
+ initialized: true,
1089
+ timestamp: Date.now(),
1090
+ capabilities: ['spa', 'visibility', 'auto-apply'],
1091
+ instance: this,
1092
+ });
1093
+ if (this.config.debug) {
1094
+ logDebug('[ABsmartly] DOMChangesPluginLite registered in global window.__ABSMARTLY_PLUGINS__');
1095
+ }
1096
+ };
1097
+ /**
1098
+ * Unregister plugin from global registry
1099
+ */
1100
+ DOMChangesPluginLite.prototype.unregisterGlobally = function () {
1101
+ unregisterPlugin('dom');
1102
+ if (this.config.debug) {
1103
+ logDebug('[ABsmartly] DOMChangesPluginLite unregistered from global registry');
1104
+ }
1105
+ };
944
1106
  /**
945
1107
  * Hide content to prevent flicker before experiments are applied
946
1108
  */
@@ -1020,204 +1182,14 @@ var DOMChangesPluginLite = /** @class */ (function () {
1020
1182
  }
1021
1183
  };
1022
1184
  DOMChangesPluginLite.prototype.watchElement = function (element, experimentName, change) {
1023
- var experiments = this.watchedElements.get(element);
1024
- if (!experiments) {
1025
- experiments = new Set();
1026
- this.watchedElements.set(element, experiments);
1027
- }
1028
- experiments.add(experimentName);
1029
- // Store the applied change for this experiment (ALL types: style, class, attribute, html, text, etc.)
1030
- if (!this.appliedChanges.has(experimentName)) {
1031
- this.appliedChanges.set(experimentName, []);
1032
- }
1033
- var changes = this.appliedChanges.get(experimentName);
1034
- var isNewWatch = !changes.includes(change);
1035
- if (isNewWatch) {
1036
- changes.push(change);
1037
- // Only log when actually adding a new watch, not on every reapply
1038
- if (this.config.debug) {
1039
- var currentStyles_1 = {};
1040
- if (change.value && typeof change.value === 'object') {
1041
- Object.keys(change.value).forEach(function (prop) {
1042
- var cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
1043
- currentStyles_1[cssProp] = element.style.getPropertyValue(cssProp);
1044
- });
1045
- }
1046
- logDebug('[WATCH-ELEMENT] Started watching element for persistence/hydration recovery', {
1047
- experimentName: experimentName,
1048
- selector: change.selector,
1049
- element: element.tagName,
1050
- changeType: change.type,
1051
- value: change.value,
1052
- currentStyles: change.type === 'style' ? currentStyles_1 : undefined,
1053
- timestamp: Date.now(),
1054
- });
1055
- }
1056
- }
1057
- if (!this.persistenceObserver) {
1058
- this.setupPersistenceObserver();
1185
+ if (this.persistenceManager) {
1186
+ this.persistenceManager.watchElement(element, experimentName, change);
1059
1187
  }
1060
1188
  };
1061
1189
  DOMChangesPluginLite.prototype.unwatchElement = function (element, experimentName) {
1062
- var experiments = this.watchedElements.get(element);
1063
- if (experiments) {
1064
- experiments.delete(experimentName);
1065
- if (experiments.size === 0) {
1066
- this.watchedElements.delete(element);
1067
- }
1068
- }
1069
- };
1070
- DOMChangesPluginLite.prototype.setupPersistenceObserver = function () {
1071
- var _this = this;
1072
- if (this.persistenceObserver)
1073
- return;
1074
- if (this.config.debug) {
1075
- logDebug('[PERSISTENCE-OBSERVER] Setting up persistence observer', {
1076
- timestamp: Date.now(),
1077
- watchedElementsCount: this.watchedElements ? 'has WeakMap' : 'no WeakMap',
1078
- });
1079
- }
1080
- this.persistenceObserver = new MutationObserver(function (mutations) {
1081
- if (_this.config.debug) {
1082
- logDebug('[MUTATION-DETECTED] Persistence observer detected mutations', {
1083
- mutationCount: mutations.length,
1084
- timestamp: Date.now(),
1085
- mutations: mutations.map(function (m) {
1086
- var _a;
1087
- return ({
1088
- target: m.target.tagName,
1089
- oldValue: (_a = m.oldValue) === null || _a === void 0 ? void 0 : _a.substring(0, 100),
1090
- });
1091
- }),
1092
- });
1093
- }
1094
- mutations.forEach(function (mutation) {
1095
- var element = mutation.target;
1096
- if (_this.reapplyingElements.has(element)) {
1097
- if (_this.config.debug) {
1098
- logDebug('[MUTATION-SKIP] Skipping mutation - currently reapplying', {
1099
- element: element.tagName,
1100
- });
1101
- }
1102
- return;
1103
- }
1104
- var experiments = _this.watchedElements.get(element);
1105
- if (experiments) {
1106
- // Throttle mutation detection logs to once per 5 seconds per element
1107
- if (_this.config.debug) {
1108
- var elementKey = "mutation:".concat(element.tagName, ":").concat(element.getAttribute('name') || element.className);
1109
- var now = Date.now();
1110
- var lastLogged = _this.reapplyLogThrottle.get(elementKey) || 0;
1111
- if (now - lastLogged > 5000) {
1112
- logDebug('[MUTATION-ON-WATCHED] Style mutation detected on watched element', {
1113
- element: element.tagName,
1114
- selector: element.getAttribute('name') || element.className,
1115
- oldValue: mutation.oldValue,
1116
- newValue: element.getAttribute('style'),
1117
- experiments: Array.from(experiments),
1118
- });
1119
- _this.reapplyLogThrottle.set(elementKey, now);
1120
- }
1121
- }
1122
- experiments.forEach(function (experimentName) {
1123
- var appliedChanges = _this.appliedChanges.get(experimentName);
1124
- if (appliedChanges) {
1125
- appliedChanges.forEach(function (change) {
1126
- var e_7, _a;
1127
- // Skip the .matches() check - we already know this element should have this change
1128
- // because we added it to watchedElements when we applied it successfully.
1129
- // The .matches() check fails for complex selectors with parent relationships (e.g., "div > p")
1130
- if (change.type === 'style') {
1131
- var needsReapply = _this.checkStyleOverwritten(element, change.value);
1132
- if (needsReapply) {
1133
- _this.reapplyingElements.add(element);
1134
- var logKey = "".concat(experimentName, "-").concat(change.selector);
1135
- var now = Date.now();
1136
- var lastLogged = _this.reapplyLogThrottle.get(logKey) || 0;
1137
- if (_this.config.debug && now - lastLogged > 5000) {
1138
- logDebug('[REAPPLY-TRIGGERED] Reapplying style after mutation (React/framework conflict detected)', {
1139
- experimentName: experimentName,
1140
- selector: change.selector,
1141
- element: element.tagName,
1142
- timestamp: now,
1143
- note: 'This happens when the page framework (React/Vue/etc) fights with DOM changes',
1144
- });
1145
- _this.reapplyLogThrottle.set(logKey, now);
1146
- // Cleanup old throttle entries to prevent memory leak
1147
- if (_this.reapplyLogThrottle.size > 100) {
1148
- var oldestAllowed = now - 60000; // 1 minute
1149
- try {
1150
- for (var _b = __values(_this.reapplyLogThrottle.entries()), _c = _b.next(); !_c.done; _c = _b.next()) {
1151
- var _d = __read(_c.value, 2), key = _d[0], time = _d[1];
1152
- if (time < oldestAllowed) {
1153
- _this.reapplyLogThrottle.delete(key);
1154
- }
1155
- }
1156
- }
1157
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
1158
- finally {
1159
- try {
1160
- if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
1161
- }
1162
- finally { if (e_7) throw e_7.error; }
1163
- }
1164
- }
1165
- }
1166
- _this.domManipulator.applyChange(change, experimentName);
1167
- setTimeout(function () {
1168
- _this.reapplyingElements.delete(element);
1169
- }, 0);
1170
- }
1171
- else if (_this.config.debug) {
1172
- logDebug('[MUTATION-NO-REAPPLY] Style mutation detected but no reapply needed', {
1173
- experimentName: experimentName,
1174
- selector: change.selector,
1175
- });
1176
- }
1177
- }
1178
- });
1179
- }
1180
- });
1181
- }
1182
- });
1183
- });
1184
- this.persistenceObserver.observe(document.body, {
1185
- attributes: true,
1186
- attributeFilter: ['style'],
1187
- subtree: true,
1188
- attributeOldValue: true,
1189
- });
1190
- if (this.config.debug) {
1191
- logDebug('[PERSISTENCE-OBSERVER] Setup complete - now observing style mutations', {
1192
- target: 'document.body',
1193
- attributeFilter: ['style'],
1194
- subtree: true,
1195
- attributeOldValue: true,
1196
- timestamp: Date.now(),
1197
- });
1198
- }
1199
- };
1200
- DOMChangesPluginLite.prototype.checkStyleOverwritten = function (element, expectedStyles) {
1201
- var e_8, _a;
1202
- try {
1203
- for (var _b = __values(Object.entries(expectedStyles)), _c = _b.next(); !_c.done; _c = _b.next()) {
1204
- var _d = __read(_c.value, 2), prop = _d[0], value = _d[1];
1205
- var cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
1206
- var currentValue = element.style.getPropertyValue(cssProp);
1207
- var currentPriority = element.style.getPropertyPriority(cssProp);
1208
- if (currentValue !== value || (value.includes('!important') && !currentPriority)) {
1209
- return true;
1210
- }
1211
- }
1212
- }
1213
- catch (e_8_1) { e_8 = { error: e_8_1 }; }
1214
- finally {
1215
- try {
1216
- if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
1217
- }
1218
- finally { if (e_8) throw e_8.error; }
1190
+ if (this.persistenceManager) {
1191
+ this.persistenceManager.unwatchElement(element, experimentName);
1219
1192
  }
1220
- return false;
1221
1193
  };
1222
1194
  DOMChangesPluginLite.VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : '1.1.2';
1223
1195
  return DOMChangesPluginLite;