@openreplay/tracker 18.0.14-beta.0 → 18.0.14

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/dist/lib/entry.js CHANGED
@@ -1823,6 +1823,30 @@ class CanvasRecorder {
1823
1823
  this.MAX_QUEUE_SIZE = 50; // ~500 images max (50 batches × 10 images)
1824
1824
  this.pendingBatches = [];
1825
1825
  this.isProcessingQueue = false;
1826
+ /**
1827
+ * Reacts to a runtime sanitization change on a canvas: stop capturing if it
1828
+ * just became masked, start if it just became visible. (Already-sent frames
1829
+ * can't be retracted — escalation only stops future capture.)
1830
+ */
1831
+ this.resanitizeCanvas = (node, id) => {
1832
+ if (!hasTag(node, 'canvas')) {
1833
+ return;
1834
+ }
1835
+ const isIgnored = this.app.sanitizer.isObscured(id) || this.app.sanitizer.isHidden(id);
1836
+ if (isIgnored) {
1837
+ if (this.snapshots[id] || this.observers.has(id)) {
1838
+ const observer = this.observers.get(id);
1839
+ if (observer) {
1840
+ observer.disconnect();
1841
+ this.observers.delete(id);
1842
+ }
1843
+ this.cleanupCanvas(id);
1844
+ }
1845
+ }
1846
+ else if (!this.snapshots[id] && !this.observers.has(id)) {
1847
+ this.captureCanvas(node);
1848
+ }
1849
+ };
1826
1850
  this.restartTracking = () => {
1827
1851
  this.clear();
1828
1852
  this.app.nodes.scanTree(this.captureCanvas);
@@ -1926,6 +1950,7 @@ class CanvasRecorder {
1926
1950
  setTimeout(() => {
1927
1951
  this.app.nodes.scanTree(this.captureCanvas);
1928
1952
  this.app.nodes.attachNodeCallback(this.captureCanvas);
1953
+ this.app.attachResanitizeCallback(this.resanitizeCanvas);
1929
1954
  }, 125);
1930
1955
  }
1931
1956
  sendSnaps(images, canvasId, createdAt) {
@@ -2810,6 +2835,120 @@ function ConstructedStyleSheets (app) {
2810
2835
  });
2811
2836
  }
2812
2837
 
2838
+ var SanitizeLevel;
2839
+ (function (SanitizeLevel) {
2840
+ SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
2841
+ SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
2842
+ SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
2843
+ })(SanitizeLevel || (SanitizeLevel = {}));
2844
+ const stringWiper = (input) => input
2845
+ .trim()
2846
+ .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
2847
+ class Sanitizer {
2848
+ constructor(params) {
2849
+ // Node id -> level. Plain (0) is never stored; a missing entry means Plain.
2850
+ // A map (not the old grow-only Sets) so levels can be raised and lowered.
2851
+ this.levels = new Map();
2852
+ this.app = params.app;
2853
+ const defaultOptions = {
2854
+ obscureTextEmails: true,
2855
+ obscureTextNumbers: false,
2856
+ privateMode: false,
2857
+ domSanitizer: undefined,
2858
+ };
2859
+ this.privateMode = params.options?.privateMode ?? false;
2860
+ this.options = Object.assign(defaultOptions, params.options);
2861
+ }
2862
+ // Pure recomputation of a node's level from the live DOM + parent level.
2863
+ // Reading current state on every call is what lets resanitize() pick up
2864
+ // runtime attribute/domSanitizer changes.
2865
+ computeLevel(node, parentLevel) {
2866
+ if (this.options.privateMode) {
2867
+ if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
2868
+ return SanitizeLevel.Obscured;
2869
+ }
2870
+ if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode, 'unmask')) {
2871
+ return SanitizeLevel.Obscured;
2872
+ }
2873
+ }
2874
+ let level = SanitizeLevel.Plain;
2875
+ if (parentLevel >= SanitizeLevel.Obscured ||
2876
+ (isElementNode(node) &&
2877
+ (hasOpenreplayAttribute(node, 'masked') || hasOpenreplayAttribute(node, 'obscured')))) {
2878
+ level = SanitizeLevel.Obscured;
2879
+ }
2880
+ if (parentLevel === SanitizeLevel.Hidden ||
2881
+ (isElementNode(node) &&
2882
+ (hasOpenreplayAttribute(node, 'htmlmasked') || hasOpenreplayAttribute(node, 'hidden')))) {
2883
+ level = SanitizeLevel.Hidden;
2884
+ }
2885
+ if (this.options.domSanitizer !== undefined && isElementNode(node)) {
2886
+ const sanitizeLevel = this.options.domSanitizer(node);
2887
+ if (sanitizeLevel === SanitizeLevel.Obscured && level < SanitizeLevel.Obscured) {
2888
+ level = SanitizeLevel.Obscured;
2889
+ }
2890
+ if (sanitizeLevel === SanitizeLevel.Hidden) {
2891
+ level = SanitizeLevel.Hidden;
2892
+ }
2893
+ }
2894
+ return level;
2895
+ }
2896
+ getLevel(id) {
2897
+ return this.levels.get(id) ?? SanitizeLevel.Plain;
2898
+ }
2899
+ // Sets a node's level (either direction) and returns the previous one.
2900
+ setLevel(id, level) {
2901
+ const prev = this.getLevel(id);
2902
+ if (level === SanitizeLevel.Plain) {
2903
+ this.levels.delete(id);
2904
+ }
2905
+ else {
2906
+ this.levels.set(id, level);
2907
+ }
2908
+ return prev;
2909
+ }
2910
+ handleNode(id, parentID, node) {
2911
+ const level = this.computeLevel(node, this.getLevel(parentID));
2912
+ // Escalate-only: commits never lower a level, only resanitize/setLevel do.
2913
+ if (level > this.getLevel(id)) {
2914
+ this.setLevel(id, level);
2915
+ }
2916
+ }
2917
+ sanitize(id, data) {
2918
+ if (this.getLevel(id) >= SanitizeLevel.Obscured) {
2919
+ // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
2920
+ return stringWiper(data);
2921
+ }
2922
+ if (this.options.obscureTextNumbers) {
2923
+ data = data.replace(/\d/g, '0');
2924
+ }
2925
+ if (this.options.obscureTextEmails) {
2926
+ data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
2927
+ const [name, domain] = email.split('@');
2928
+ const [domainName, host] = domain.split('.');
2929
+ return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
2930
+ });
2931
+ }
2932
+ return data;
2933
+ }
2934
+ isObscured(id) {
2935
+ return this.getLevel(id) >= SanitizeLevel.Obscured;
2936
+ }
2937
+ isHidden(id) {
2938
+ return this.getLevel(id) === SanitizeLevel.Hidden;
2939
+ }
2940
+ getInnerTextSecure(el) {
2941
+ const id = this.app.nodes.getID(el);
2942
+ if (!id) {
2943
+ return '';
2944
+ }
2945
+ return this.sanitize(id, el.innerText);
2946
+ }
2947
+ clear() {
2948
+ this.levels.clear();
2949
+ }
2950
+ }
2951
+
2813
2952
  const iconCache = {};
2814
2953
  const svgUrlCache = {};
2815
2954
  async function parseUseEl(useElement, mode, domParser) {
@@ -3399,6 +3538,100 @@ class Observer {
3399
3538
  beforeCommit(this.app.nodes.getID(node));
3400
3539
  this.commitNodes(true);
3401
3540
  }
3541
+ /**
3542
+ * Re-evaluates sanitization for every tracked node in `root`'s subtree against
3543
+ * the current DOM and re-emits whatever changed. Pass the highest node you
3544
+ * changed (or the document root) so inherited levels propagate correctly.
3545
+ */
3546
+ resanitizeSubtree(root) {
3547
+ if (!isObservable(root)) {
3548
+ return;
3549
+ }
3550
+ const parent = root.parentNode;
3551
+ const parentId = parent !== null ? this.app.nodes.getID(parent) : undefined;
3552
+ const parentLevel = parentId !== undefined ? this.app.sanitizer.getLevel(parentId) : SanitizeLevel.Plain;
3553
+ this.resanitizeNode(root, parentLevel);
3554
+ }
3555
+ resanitizeNode(node, parentLevel) {
3556
+ if (isIgnored(node)) {
3557
+ return;
3558
+ }
3559
+ const id = this.app.nodes.getID(node);
3560
+ if (id === undefined) {
3561
+ // Untracked (new, or under a hidden ancestor): the live observer handles it.
3562
+ return;
3563
+ }
3564
+ const newLevel = this.app.sanitizer.computeLevel(node, parentLevel);
3565
+ const prevLevel = this.app.sanitizer.getLevel(id);
3566
+ const wasHidden = prevLevel === SanitizeLevel.Hidden;
3567
+ const willHidden = newLevel === SanitizeLevel.Hidden;
3568
+ // Crossing the hidden boundary changes the rendered structure (placeholder vs
3569
+ // real subtree), so rebuild rather than re-emit.
3570
+ if (wasHidden !== willHidden) {
3571
+ this.recreateSubtree(node);
3572
+ return;
3573
+ }
3574
+ if (willHidden) {
3575
+ return;
3576
+ }
3577
+ // Plain <-> Obscured: same structure, only leaf content changes.
3578
+ if (prevLevel !== newLevel) {
3579
+ this.app.sanitizer.setLevel(id, newLevel);
3580
+ this.reemitNode(id, node);
3581
+ }
3582
+ for (let child = node.firstChild; child !== null; child = child.nextSibling) {
3583
+ this.resanitizeNode(child, newLevel);
3584
+ }
3585
+ }
3586
+ // Destroys the node player-side and re-emits its subtree from scratch (new ids)
3587
+ // so it materializes at the freshly-computed level.
3588
+ recreateSubtree(node) {
3589
+ const id = this.app.nodes.getID(node);
3590
+ if (id === undefined) {
3591
+ return;
3592
+ }
3593
+ this.app.send(RemoveNode(id));
3594
+ this.clearSubtreeRegistration(node);
3595
+ this.bindTree(node);
3596
+ this.commitNodes();
3597
+ }
3598
+ clearSubtreeRegistration(node) {
3599
+ const clearOne = (n) => {
3600
+ const oldId = this.app.nodes.getID(n);
3601
+ if (oldId !== undefined) {
3602
+ this.app.sanitizer.setLevel(oldId, SanitizeLevel.Plain);
3603
+ }
3604
+ this.app.nodes.unregisterNode(n);
3605
+ };
3606
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
3607
+ acceptNode: (n) => isIgnored(n) || this.app.nodes.getID(n) === undefined
3608
+ ? NodeFilter.FILTER_REJECT
3609
+ : NodeFilter.FILTER_ACCEPT,
3610
+ },
3611
+ // @ts-ignore
3612
+ false);
3613
+ // Collect first, then clear: unregistering mutates the ids the walker reads.
3614
+ const subtree = [];
3615
+ while (walker.nextNode()) {
3616
+ subtree.push(walker.currentNode);
3617
+ }
3618
+ clearOne(node);
3619
+ subtree.forEach(clearOne);
3620
+ }
3621
+ reemitNode(id, node) {
3622
+ if (isTextNode(node)) {
3623
+ const parent = node.parentNode;
3624
+ if (parent !== null && isElementNode(parent)) {
3625
+ // re-runs sanitize() at the level we just set
3626
+ this.sendNodeData(id, parent, node.data);
3627
+ }
3628
+ return;
3629
+ }
3630
+ if (isElementNode(node)) {
3631
+ // inputs/images/canvas re-emit their own payload via registered callbacks
3632
+ this.app.callResanitizeCallbacks(node, id);
3633
+ }
3634
+ }
3402
3635
  disconnect() {
3403
3636
  // THEORY S3: a disconnect may discard MutationRecords still queued by the
3404
3637
  // browser. takeRecords() drains them — they would be discarded by
@@ -3758,94 +3991,6 @@ class TopObserver extends Observer {
3758
3991
  }
3759
3992
  }
3760
3993
 
3761
- var SanitizeLevel;
3762
- (function (SanitizeLevel) {
3763
- SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
3764
- SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
3765
- SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
3766
- })(SanitizeLevel || (SanitizeLevel = {}));
3767
- const stringWiper = (input) => input
3768
- .trim()
3769
- .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
3770
- class Sanitizer {
3771
- constructor(params) {
3772
- this.obscured = new Set();
3773
- this.hidden = new Set();
3774
- this.app = params.app;
3775
- const defaultOptions = {
3776
- obscureTextEmails: true,
3777
- obscureTextNumbers: false,
3778
- privateMode: false,
3779
- domSanitizer: undefined,
3780
- };
3781
- this.privateMode = params.options?.privateMode ?? false;
3782
- this.options = Object.assign(defaultOptions, params.options);
3783
- }
3784
- handleNode(id, parentID, node) {
3785
- if (this.options.privateMode) {
3786
- if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
3787
- return this.obscured.add(id);
3788
- }
3789
- if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode, 'unmask')) {
3790
- return this.obscured.add(id);
3791
- }
3792
- }
3793
- if (this.obscured.has(parentID) ||
3794
- (isElementNode(node) &&
3795
- (hasOpenreplayAttribute(node, 'masked') || hasOpenreplayAttribute(node, 'obscured')))) {
3796
- this.obscured.add(id);
3797
- }
3798
- if (this.hidden.has(parentID) ||
3799
- (isElementNode(node) &&
3800
- (hasOpenreplayAttribute(node, 'htmlmasked') || hasOpenreplayAttribute(node, 'hidden')))) {
3801
- this.hidden.add(id);
3802
- }
3803
- if (this.options.domSanitizer !== undefined && isElementNode(node)) {
3804
- const sanitizeLevel = this.options.domSanitizer(node);
3805
- if (sanitizeLevel === SanitizeLevel.Obscured) {
3806
- this.obscured.add(id);
3807
- }
3808
- if (sanitizeLevel === SanitizeLevel.Hidden) {
3809
- this.hidden.add(id);
3810
- }
3811
- }
3812
- }
3813
- sanitize(id, data) {
3814
- if (this.obscured.has(id)) {
3815
- // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
3816
- return stringWiper(data);
3817
- }
3818
- if (this.options.obscureTextNumbers) {
3819
- data = data.replace(/\d/g, '0');
3820
- }
3821
- if (this.options.obscureTextEmails) {
3822
- data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
3823
- const [name, domain] = email.split('@');
3824
- const [domainName, host] = domain.split('.');
3825
- return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
3826
- });
3827
- }
3828
- return data;
3829
- }
3830
- isObscured(id) {
3831
- return this.obscured.has(id);
3832
- }
3833
- isHidden(id) {
3834
- return this.hidden.has(id);
3835
- }
3836
- getInnerTextSecure(el) {
3837
- const id = this.app.nodes.getID(el);
3838
- if (!id) {
3839
- return '';
3840
- }
3841
- return this.sanitize(id, el.innerText);
3842
- }
3843
- clear() {
3844
- this.obscured.clear();
3845
- this.hidden.clear();
3846
- }
3847
- }
3848
-
3849
3994
  const tokenSeparator = '_$_';
3850
3995
  class Session {
3851
3996
  constructor(params) {
@@ -4097,6 +4242,8 @@ class App {
4097
4242
  constructor(projectKey, sessionToken, options, signalError, insideIframe) {
4098
4243
  this.signalError = signalError;
4099
4244
  this.insideIframe = insideIframe;
4245
+ // Registered by input/img/canvas to re-emit a node when its level changes.
4246
+ this.resanitizeCallbacks = [];
4100
4247
  this.messages = [];
4101
4248
  /**
4102
4249
  * we need 2 buffers, so we don't lose anything
@@ -4108,7 +4255,7 @@ class App {
4108
4255
  this.stopCallbacks = [];
4109
4256
  this.commitCallbacks = [];
4110
4257
  this.activityState = ActivityState.NotActive;
4111
- this.version = '18.0.14-beta.0'; // TODO: version compatability check inside each plugin.
4258
+ this.version = '18.0.14'; // TODO: version compatability check inside each plugin.
4112
4259
  this.socketMode = false;
4113
4260
  this.compressionThreshold = 24 * 1000;
4114
4261
  this.bc = null;
@@ -4639,6 +4786,26 @@ class App {
4639
4786
  this.restartCanvasTracking = () => {
4640
4787
  this.canvasRecorder?.restartTracking();
4641
4788
  };
4789
+ this.attachResanitizeCallback = (cb) => {
4790
+ this.resanitizeCallbacks.push(cb);
4791
+ };
4792
+ this.callResanitizeCallbacks = (node, id) => {
4793
+ this.resanitizeCallbacks.forEach((cb) => cb(node, id));
4794
+ };
4795
+ this.resanitize = (el) => {
4796
+ const root = el ?? (IN_BROWSER ? document.documentElement : undefined);
4797
+ if (!root) {
4798
+ return;
4799
+ }
4800
+ this.observer.resanitizeSubtree(root);
4801
+ };
4802
+ this.checkSanitization = (el) => {
4803
+ const id = this.nodes.getID(el);
4804
+ if (id === undefined) {
4805
+ return undefined;
4806
+ }
4807
+ return this.sanitizer.getLevel(id);
4808
+ };
4642
4809
  this.flushBuffer = async (buffer) => {
4643
4810
  return new Promise((res, reject) => {
4644
4811
  if (buffer.length === 0) {
@@ -6331,6 +6498,12 @@ function Img (app) {
6331
6498
  sendImgAttrs(node);
6332
6499
  observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] });
6333
6500
  });
6501
+ // On a runtime level change, re-evaluate placeholder vs real src for this image.
6502
+ app.attachResanitizeCallback((node) => {
6503
+ if (hasTag(node, 'img')) {
6504
+ sendImgAttrs(node);
6505
+ }
6506
+ });
6334
6507
  }
6335
6508
 
6336
6509
  const INPUT_TYPES = [
@@ -6517,6 +6690,13 @@ function Input (app, opts) {
6517
6690
  }
6518
6691
  app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime));
6519
6692
  }
6693
+ // Re-emit a field's value when its sanitization level changes at runtime.
6694
+ // getInputValue() reads the current level, so re-sending applies the new mask.
6695
+ app.attachResanitizeCallback((node, id) => {
6696
+ if (isTextFieldElement(node) || hasTag(node, 'select')) {
6697
+ sendInputValue(id, node);
6698
+ }
6699
+ });
6520
6700
  app.nodes.attachNodeCallback(app.safe((node) => {
6521
6701
  const id = app.nodes.getID(node);
6522
6702
  if (id === undefined) {
@@ -9407,7 +9587,7 @@ class ConstantProperties {
9407
9587
  user_id: this.user_id,
9408
9588
  distinct_id: this.deviceId,
9409
9589
  sdk_edition: 'web',
9410
- sdk_version: '18.0.14-beta.0',
9590
+ sdk_version: '18.0.14',
9411
9591
  timezone: getUTCOffsetString(),
9412
9592
  search_engine: this.searchEngine,
9413
9593
  };
@@ -10109,7 +10289,7 @@ class API {
10109
10289
  this.signalStartIssue = (reason, missingApi) => {
10110
10290
  const doNotTrack = this.checkDoNotTrack();
10111
10291
  console.log("Tracker couldn't start due to:", JSON.stringify({
10112
- trackerVersion: '18.0.14-beta.0',
10292
+ trackerVersion: '18.0.14',
10113
10293
  projectKey: this.options.projectKey,
10114
10294
  doNotTrack,
10115
10295
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -10121,6 +10301,31 @@ class API {
10121
10301
  }
10122
10302
  this.app.restartCanvasTracking();
10123
10303
  };
10304
+ /**
10305
+ * Re-evaluates sanitization against the current DOM and re-emits whatever
10306
+ * changed, updating already-recorded nodes mid-session. Call after toggling
10307
+ * `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
10308
+ * keys on (class/id/etc).
10309
+ *
10310
+ * @param el - the highest node you changed; omit to re-scan the whole document;
10311
+ * scanning the entire doc is O(dom size)
10312
+ * */
10313
+ this.resanitize = (el) => {
10314
+ if (this.app === null) {
10315
+ return;
10316
+ }
10317
+ this.app.resanitize(el);
10318
+ };
10319
+ /**
10320
+ * Returns the sanitization level the tracker currently has for a node
10321
+ * (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
10322
+ * */
10323
+ this.checkSanitization = (el) => {
10324
+ if (this.app === null) {
10325
+ return undefined;
10326
+ }
10327
+ return this.app.checkSanitization(el);
10328
+ };
10124
10329
  this.getSessionURL = (options) => {
10125
10330
  if (this.app === null) {
10126
10331
  return undefined;
@@ -10134,7 +10339,10 @@ class API {
10134
10339
  }
10135
10340
  };
10136
10341
  this.identify = this.setUserID;
10137
- this.track = this.analytics?.track;
10342
+ // Delegates at call time: `this.analytics` is assigned in the constructor body,
10343
+ // which runs AFTER field initializers, so binding it here directly would always
10344
+ // capture `undefined`.
10345
+ this.track = (eventName, properties, options) => this.analytics?.track(eventName, properties, options);
10138
10346
  this.userID = (id) => {
10139
10347
  deprecationWarn("'userID' method", "'setUserID' method", '/');
10140
10348
  this.setUserID(id);
@@ -10517,12 +10725,20 @@ class TrackerSingleton {
10517
10725
  constructor() {
10518
10726
  this.instance = null;
10519
10727
  this.isConfigured = false;
10728
+ this.setUserID = (id) => {
10729
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
10730
+ return;
10731
+ }
10732
+ this.instance.setUserID(id);
10733
+ };
10520
10734
  this.identify = this.setUserID;
10521
10735
  this.track = (eventName, properties, options) => {
10522
10736
  if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
10523
10737
  return;
10524
10738
  }
10525
- this.instance.track?.(eventName, properties);
10739
+ // Route through analytics directly: Tracker.track is bound to analytics?.track
10740
+ // at field-init time (before analytics exists), so it is always undefined.
10741
+ this.instance.analytics?.track(eventName, properties, options);
10526
10742
  };
10527
10743
  }
10528
10744
  /**
@@ -10552,11 +10768,16 @@ class TrackerSingleton {
10552
10768
  if (!IN_BROWSER) {
10553
10769
  return Promise.resolve({ success: false, reason: 'Not in browser environment' });
10554
10770
  }
10555
- if (!this.ensureConfigured()) {
10771
+ if (!this.ensureConfigured() || !this.instance) {
10556
10772
  return Promise.resolve({ success: false, reason: 'Tracker not configured' });
10557
10773
  }
10558
- return (this.instance?.start(startOpts) ||
10559
- Promise.resolve({ success: false, reason: 'Tracker not initialized' }));
10774
+ // Tracker.start() rejects (instead of resolving {success:false}) when the
10775
+ // underlying app failed to initialise (non-https, missing api, doNotTrack,
10776
+ // already initialised...). Normalize so callers always get {success, reason}.
10777
+ return this.instance.start(startOpts).catch((reason) => ({
10778
+ success: false,
10779
+ reason: typeof reason === 'string' ? reason : String(reason),
10780
+ }));
10560
10781
  }
10561
10782
  /**
10562
10783
  * Stop the session and return sessionHash
@@ -10568,21 +10789,9 @@ class TrackerSingleton {
10568
10789
  }
10569
10790
  return this.instance.stop();
10570
10791
  }
10571
- setUserID(id) {
10572
- if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
10573
- return;
10574
- }
10575
- this.instance.setUserID(id);
10576
- }
10577
10792
  get analytics() {
10578
- if (this.instance?.analytics) {
10579
- return this.instance.analytics;
10580
- }
10581
- else {
10582
- return null;
10583
- }
10793
+ return this.instance?.analytics ?? null;
10584
10794
  }
10585
- ;
10586
10795
  /**
10587
10796
  * Set metadata for the current session
10588
10797
  *
@@ -10757,6 +10966,51 @@ class TrackerSingleton {
10757
10966
  }
10758
10967
  return this.instance.getTabId();
10759
10968
  }
10969
+ /**
10970
+ * Re-evaluates sanitization against the current DOM and re-emits whatever
10971
+ * changed, updating already-recorded nodes mid-session. Call after toggling
10972
+ * `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
10973
+ * keys on (class/id/etc).
10974
+ *
10975
+ * @param el - the highest node you changed; omit to re-scan the whole document.
10976
+ * */
10977
+ resanitize(el) {
10978
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
10979
+ return;
10980
+ }
10981
+ return this.instance.resanitize(el);
10982
+ }
10983
+ /**
10984
+ * Returns the sanitization level the tracker currently has for a node
10985
+ * (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
10986
+ * */
10987
+ checkSanitization(el) {
10988
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
10989
+ return undefined;
10990
+ }
10991
+ return this.instance.checkSanitization(el);
10992
+ }
10993
+ incident(options) {
10994
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
10995
+ return;
10996
+ }
10997
+ this.instance.incident(options);
10998
+ }
10999
+ /**
11000
+ * Use custom token for analytics events without session recording
11001
+ * */
11002
+ setAnalyticsToken(token) {
11003
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
11004
+ return;
11005
+ }
11006
+ this.instance.setAnalyticsToken(token);
11007
+ }
11008
+ getAnalyticsToken() {
11009
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
11010
+ return undefined;
11011
+ }
11012
+ return this.instance.getAnalyticsToken();
11013
+ }
10760
11014
  }
10761
11015
  const tracker = new TrackerSingleton();
10762
11016