@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/cjs/entry.js +363 -109
- package/dist/cjs/entry.js.map +1 -1
- package/dist/cjs/index.js +300 -92
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/main/app/canvas.d.ts +6 -0
- package/dist/cjs/main/app/index.d.ts +6 -1
- package/dist/cjs/main/app/observer/observer.d.ts +10 -0
- package/dist/cjs/main/app/sanitizer.d.ts +5 -3
- package/dist/cjs/main/index.d.ts +17 -2
- package/dist/cjs/main/singleton.d.ts +25 -1
- package/dist/lib/entry.js +363 -109
- package/dist/lib/entry.js.map +1 -1
- package/dist/lib/index.js +300 -92
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/main/app/canvas.d.ts +6 -0
- package/dist/lib/main/app/index.d.ts +6 -1
- package/dist/lib/main/app/observer/observer.d.ts +10 -0
- package/dist/lib/main/app/sanitizer.d.ts +5 -3
- package/dist/lib/main/index.d.ts +17 -2
- package/dist/lib/main/singleton.d.ts +25 -1
- package/dist/types/main/app/canvas.d.ts +6 -0
- package/dist/types/main/app/index.d.ts +6 -1
- package/dist/types/main/app/observer/observer.d.ts +10 -0
- package/dist/types/main/app/sanitizer.d.ts +5 -3
- package/dist/types/main/index.d.ts +17 -2
- package/dist/types/main/singleton.d.ts +25 -1
- package/package.json +1 -1
package/dist/lib/index.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
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|