@openreplay/tracker 18.0.14-beta.2 → 18.0.15
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/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
|
|
@@ -3748,94 +3981,6 @@ class TopObserver extends Observer {
|
|
|
3748
3981
|
}
|
|
3749
3982
|
}
|
|
3750
3983
|
|
|
3751
|
-
var SanitizeLevel;
|
|
3752
|
-
(function (SanitizeLevel) {
|
|
3753
|
-
SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
|
|
3754
|
-
SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
|
|
3755
|
-
SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
|
|
3756
|
-
})(SanitizeLevel || (SanitizeLevel = {}));
|
|
3757
|
-
const stringWiper = (input) => input
|
|
3758
|
-
.trim()
|
|
3759
|
-
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
|
|
3760
|
-
class Sanitizer {
|
|
3761
|
-
constructor(params) {
|
|
3762
|
-
this.obscured = new Set();
|
|
3763
|
-
this.hidden = new Set();
|
|
3764
|
-
this.app = params.app;
|
|
3765
|
-
const defaultOptions = {
|
|
3766
|
-
obscureTextEmails: true,
|
|
3767
|
-
obscureTextNumbers: false,
|
|
3768
|
-
privateMode: false,
|
|
3769
|
-
domSanitizer: undefined,
|
|
3770
|
-
};
|
|
3771
|
-
this.privateMode = params.options?.privateMode ?? false;
|
|
3772
|
-
this.options = Object.assign(defaultOptions, params.options);
|
|
3773
|
-
}
|
|
3774
|
-
handleNode(id, parentID, node) {
|
|
3775
|
-
if (this.options.privateMode) {
|
|
3776
|
-
if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
|
|
3777
|
-
return this.obscured.add(id);
|
|
3778
|
-
}
|
|
3779
|
-
if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode, 'unmask')) {
|
|
3780
|
-
return this.obscured.add(id);
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
if (this.obscured.has(parentID) ||
|
|
3784
|
-
(isElementNode(node) &&
|
|
3785
|
-
(hasOpenreplayAttribute(node, 'masked') || hasOpenreplayAttribute(node, 'obscured')))) {
|
|
3786
|
-
this.obscured.add(id);
|
|
3787
|
-
}
|
|
3788
|
-
if (this.hidden.has(parentID) ||
|
|
3789
|
-
(isElementNode(node) &&
|
|
3790
|
-
(hasOpenreplayAttribute(node, 'htmlmasked') || hasOpenreplayAttribute(node, 'hidden')))) {
|
|
3791
|
-
this.hidden.add(id);
|
|
3792
|
-
}
|
|
3793
|
-
if (this.options.domSanitizer !== undefined && isElementNode(node)) {
|
|
3794
|
-
const sanitizeLevel = this.options.domSanitizer(node);
|
|
3795
|
-
if (sanitizeLevel === SanitizeLevel.Obscured) {
|
|
3796
|
-
this.obscured.add(id);
|
|
3797
|
-
}
|
|
3798
|
-
if (sanitizeLevel === SanitizeLevel.Hidden) {
|
|
3799
|
-
this.hidden.add(id);
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
sanitize(id, data) {
|
|
3804
|
-
if (this.obscured.has(id)) {
|
|
3805
|
-
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
|
|
3806
|
-
return stringWiper(data);
|
|
3807
|
-
}
|
|
3808
|
-
if (this.options.obscureTextNumbers) {
|
|
3809
|
-
data = data.replace(/\d/g, '0');
|
|
3810
|
-
}
|
|
3811
|
-
if (this.options.obscureTextEmails) {
|
|
3812
|
-
data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
|
|
3813
|
-
const [name, domain] = email.split('@');
|
|
3814
|
-
const [domainName, host] = domain.split('.');
|
|
3815
|
-
return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
|
|
3816
|
-
});
|
|
3817
|
-
}
|
|
3818
|
-
return data;
|
|
3819
|
-
}
|
|
3820
|
-
isObscured(id) {
|
|
3821
|
-
return this.obscured.has(id);
|
|
3822
|
-
}
|
|
3823
|
-
isHidden(id) {
|
|
3824
|
-
return this.hidden.has(id);
|
|
3825
|
-
}
|
|
3826
|
-
getInnerTextSecure(el) {
|
|
3827
|
-
const id = this.app.nodes.getID(el);
|
|
3828
|
-
if (!id) {
|
|
3829
|
-
return '';
|
|
3830
|
-
}
|
|
3831
|
-
return this.sanitize(id, el.innerText);
|
|
3832
|
-
}
|
|
3833
|
-
clear() {
|
|
3834
|
-
this.obscured.clear();
|
|
3835
|
-
this.hidden.clear();
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
|
|
3839
3984
|
const tokenSeparator = '_$_';
|
|
3840
3985
|
class Session {
|
|
3841
3986
|
constructor(params) {
|
|
@@ -4087,6 +4232,8 @@ class App {
|
|
|
4087
4232
|
constructor(projectKey, sessionToken, options, signalError, insideIframe) {
|
|
4088
4233
|
this.signalError = signalError;
|
|
4089
4234
|
this.insideIframe = insideIframe;
|
|
4235
|
+
// Registered by input/img/canvas to re-emit a node when its level changes.
|
|
4236
|
+
this.resanitizeCallbacks = [];
|
|
4090
4237
|
this.messages = [];
|
|
4091
4238
|
/**
|
|
4092
4239
|
* we need 2 buffers, so we don't lose anything
|
|
@@ -4098,7 +4245,7 @@ class App {
|
|
|
4098
4245
|
this.stopCallbacks = [];
|
|
4099
4246
|
this.commitCallbacks = [];
|
|
4100
4247
|
this.activityState = ActivityState.NotActive;
|
|
4101
|
-
this.version = '18.0.
|
|
4248
|
+
this.version = '18.0.15'; // TODO: version compatability check inside each plugin.
|
|
4102
4249
|
this.socketMode = false;
|
|
4103
4250
|
this.compressionThreshold = 24 * 1000;
|
|
4104
4251
|
this.bc = null;
|
|
@@ -4629,6 +4776,26 @@ class App {
|
|
|
4629
4776
|
this.restartCanvasTracking = () => {
|
|
4630
4777
|
this.canvasRecorder?.restartTracking();
|
|
4631
4778
|
};
|
|
4779
|
+
this.attachResanitizeCallback = (cb) => {
|
|
4780
|
+
this.resanitizeCallbacks.push(cb);
|
|
4781
|
+
};
|
|
4782
|
+
this.callResanitizeCallbacks = (node, id) => {
|
|
4783
|
+
this.resanitizeCallbacks.forEach((cb) => cb(node, id));
|
|
4784
|
+
};
|
|
4785
|
+
this.resanitize = (el) => {
|
|
4786
|
+
const root = el ?? (IN_BROWSER ? document.documentElement : undefined);
|
|
4787
|
+
if (!root) {
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
this.observer.resanitizeSubtree(root);
|
|
4791
|
+
};
|
|
4792
|
+
this.checkSanitization = (el) => {
|
|
4793
|
+
const id = this.nodes.getID(el);
|
|
4794
|
+
if (id === undefined) {
|
|
4795
|
+
return undefined;
|
|
4796
|
+
}
|
|
4797
|
+
return this.sanitizer.getLevel(id);
|
|
4798
|
+
};
|
|
4632
4799
|
this.flushBuffer = async (buffer) => {
|
|
4633
4800
|
return new Promise((res, reject) => {
|
|
4634
4801
|
if (buffer.length === 0) {
|
|
@@ -6324,6 +6491,12 @@ function Img (app) {
|
|
|
6324
6491
|
sendImgAttrs(node);
|
|
6325
6492
|
observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] });
|
|
6326
6493
|
});
|
|
6494
|
+
// On a runtime level change, re-evaluate placeholder vs real src for this image.
|
|
6495
|
+
app.attachResanitizeCallback((node) => {
|
|
6496
|
+
if (hasTag(node, 'img')) {
|
|
6497
|
+
sendImgAttrs(node);
|
|
6498
|
+
}
|
|
6499
|
+
});
|
|
6327
6500
|
}
|
|
6328
6501
|
|
|
6329
6502
|
const INPUT_TYPES = [
|
|
@@ -6510,6 +6683,13 @@ function Input (app, opts) {
|
|
|
6510
6683
|
}
|
|
6511
6684
|
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime));
|
|
6512
6685
|
}
|
|
6686
|
+
// Re-emit a field's value when its sanitization level changes at runtime.
|
|
6687
|
+
// getInputValue() reads the current level, so re-sending applies the new mask.
|
|
6688
|
+
app.attachResanitizeCallback((node, id) => {
|
|
6689
|
+
if (isTextFieldElement(node) || hasTag(node, 'select')) {
|
|
6690
|
+
sendInputValue(id, node);
|
|
6691
|
+
}
|
|
6692
|
+
});
|
|
6513
6693
|
app.nodes.attachNodeCallback(app.safe((node) => {
|
|
6514
6694
|
const id = app.nodes.getID(node);
|
|
6515
6695
|
if (id === undefined) {
|
|
@@ -9400,7 +9580,7 @@ class ConstantProperties {
|
|
|
9400
9580
|
user_id: this.user_id,
|
|
9401
9581
|
distinct_id: this.deviceId,
|
|
9402
9582
|
sdk_edition: 'web',
|
|
9403
|
-
sdk_version: '18.0.
|
|
9583
|
+
sdk_version: '18.0.15',
|
|
9404
9584
|
timezone: getUTCOffsetString(),
|
|
9405
9585
|
search_engine: this.searchEngine,
|
|
9406
9586
|
};
|
|
@@ -10102,7 +10282,7 @@ class API {
|
|
|
10102
10282
|
this.signalStartIssue = (reason, missingApi) => {
|
|
10103
10283
|
const doNotTrack = this.checkDoNotTrack();
|
|
10104
10284
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
10105
|
-
trackerVersion: '18.0.
|
|
10285
|
+
trackerVersion: '18.0.15',
|
|
10106
10286
|
projectKey: this.options.projectKey,
|
|
10107
10287
|
doNotTrack,
|
|
10108
10288
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|
|
@@ -10114,6 +10294,31 @@ class API {
|
|
|
10114
10294
|
}
|
|
10115
10295
|
this.app.restartCanvasTracking();
|
|
10116
10296
|
};
|
|
10297
|
+
/**
|
|
10298
|
+
* Re-evaluates sanitization against the current DOM and re-emits whatever
|
|
10299
|
+
* changed, updating already-recorded nodes mid-session. Call after toggling
|
|
10300
|
+
* `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
|
|
10301
|
+
* keys on (class/id/etc).
|
|
10302
|
+
*
|
|
10303
|
+
* @param el - the highest node you changed; omit to re-scan the whole document;
|
|
10304
|
+
* scanning the entire doc is O(dom size)
|
|
10305
|
+
* */
|
|
10306
|
+
this.resanitize = (el) => {
|
|
10307
|
+
if (this.app === null) {
|
|
10308
|
+
return;
|
|
10309
|
+
}
|
|
10310
|
+
this.app.resanitize(el);
|
|
10311
|
+
};
|
|
10312
|
+
/**
|
|
10313
|
+
* Returns the sanitization level the tracker currently has for a node
|
|
10314
|
+
* (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
|
|
10315
|
+
* */
|
|
10316
|
+
this.checkSanitization = (el) => {
|
|
10317
|
+
if (this.app === null) {
|
|
10318
|
+
return undefined;
|
|
10319
|
+
}
|
|
10320
|
+
return this.app.checkSanitization(el);
|
|
10321
|
+
};
|
|
10117
10322
|
this.getSessionURL = (options) => {
|
|
10118
10323
|
if (this.app === null) {
|
|
10119
10324
|
return undefined;
|
|
@@ -10127,7 +10332,10 @@ class API {
|
|
|
10127
10332
|
}
|
|
10128
10333
|
};
|
|
10129
10334
|
this.identify = this.setUserID;
|
|
10130
|
-
|
|
10335
|
+
// Delegates at call time: `this.analytics` is assigned in the constructor body,
|
|
10336
|
+
// which runs AFTER field initializers, so binding it here directly would always
|
|
10337
|
+
// capture `undefined`.
|
|
10338
|
+
this.track = (eventName, properties, options) => this.analytics?.track(eventName, properties, options);
|
|
10131
10339
|
this.userID = (id) => {
|
|
10132
10340
|
deprecationWarn("'userID' method", "'setUserID' method", '/');
|
|
10133
10341
|
this.setUserID(id);
|
|
@@ -10510,12 +10718,20 @@ class TrackerSingleton {
|
|
|
10510
10718
|
constructor() {
|
|
10511
10719
|
this.instance = null;
|
|
10512
10720
|
this.isConfigured = false;
|
|
10721
|
+
this.setUserID = (id) => {
|
|
10722
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10723
|
+
return;
|
|
10724
|
+
}
|
|
10725
|
+
this.instance.setUserID(id);
|
|
10726
|
+
};
|
|
10513
10727
|
this.identify = this.setUserID;
|
|
10514
10728
|
this.track = (eventName, properties, options) => {
|
|
10515
10729
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10516
10730
|
return;
|
|
10517
10731
|
}
|
|
10518
|
-
|
|
10732
|
+
// Route through analytics directly: Tracker.track is bound to analytics?.track
|
|
10733
|
+
// at field-init time (before analytics exists), so it is always undefined.
|
|
10734
|
+
this.instance.analytics?.track(eventName, properties, options);
|
|
10519
10735
|
};
|
|
10520
10736
|
}
|
|
10521
10737
|
/**
|
|
@@ -10545,11 +10761,16 @@ class TrackerSingleton {
|
|
|
10545
10761
|
if (!IN_BROWSER) {
|
|
10546
10762
|
return Promise.resolve({ success: false, reason: 'Not in browser environment' });
|
|
10547
10763
|
}
|
|
10548
|
-
if (!this.ensureConfigured()) {
|
|
10764
|
+
if (!this.ensureConfigured() || !this.instance) {
|
|
10549
10765
|
return Promise.resolve({ success: false, reason: 'Tracker not configured' });
|
|
10550
10766
|
}
|
|
10551
|
-
|
|
10552
|
-
|
|
10767
|
+
// Tracker.start() rejects (instead of resolving {success:false}) when the
|
|
10768
|
+
// underlying app failed to initialise (non-https, missing api, doNotTrack,
|
|
10769
|
+
// already initialised...). Normalize so callers always get {success, reason}.
|
|
10770
|
+
return this.instance.start(startOpts).catch((reason) => ({
|
|
10771
|
+
success: false,
|
|
10772
|
+
reason: typeof reason === 'string' ? reason : String(reason),
|
|
10773
|
+
}));
|
|
10553
10774
|
}
|
|
10554
10775
|
/**
|
|
10555
10776
|
* Stop the session and return sessionHash
|
|
@@ -10561,21 +10782,9 @@ class TrackerSingleton {
|
|
|
10561
10782
|
}
|
|
10562
10783
|
return this.instance.stop();
|
|
10563
10784
|
}
|
|
10564
|
-
setUserID(id) {
|
|
10565
|
-
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10566
|
-
return;
|
|
10567
|
-
}
|
|
10568
|
-
this.instance.setUserID(id);
|
|
10569
|
-
}
|
|
10570
10785
|
get analytics() {
|
|
10571
|
-
|
|
10572
|
-
return this.instance.analytics;
|
|
10573
|
-
}
|
|
10574
|
-
else {
|
|
10575
|
-
return null;
|
|
10576
|
-
}
|
|
10786
|
+
return this.instance?.analytics ?? null;
|
|
10577
10787
|
}
|
|
10578
|
-
;
|
|
10579
10788
|
/**
|
|
10580
10789
|
* Set metadata for the current session
|
|
10581
10790
|
*
|
|
@@ -10750,6 +10959,51 @@ class TrackerSingleton {
|
|
|
10750
10959
|
}
|
|
10751
10960
|
return this.instance.getTabId();
|
|
10752
10961
|
}
|
|
10962
|
+
/**
|
|
10963
|
+
* Re-evaluates sanitization against the current DOM and re-emits whatever
|
|
10964
|
+
* changed, updating already-recorded nodes mid-session. Call after toggling
|
|
10965
|
+
* `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
|
|
10966
|
+
* keys on (class/id/etc).
|
|
10967
|
+
*
|
|
10968
|
+
* @param el - the highest node you changed; omit to re-scan the whole document.
|
|
10969
|
+
* */
|
|
10970
|
+
resanitize(el) {
|
|
10971
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10972
|
+
return;
|
|
10973
|
+
}
|
|
10974
|
+
return this.instance.resanitize(el);
|
|
10975
|
+
}
|
|
10976
|
+
/**
|
|
10977
|
+
* Returns the sanitization level the tracker currently has for a node
|
|
10978
|
+
* (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
|
|
10979
|
+
* */
|
|
10980
|
+
checkSanitization(el) {
|
|
10981
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10982
|
+
return undefined;
|
|
10983
|
+
}
|
|
10984
|
+
return this.instance.checkSanitization(el);
|
|
10985
|
+
}
|
|
10986
|
+
incident(options) {
|
|
10987
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10988
|
+
return;
|
|
10989
|
+
}
|
|
10990
|
+
this.instance.incident(options);
|
|
10991
|
+
}
|
|
10992
|
+
/**
|
|
10993
|
+
* Use custom token for analytics events without session recording
|
|
10994
|
+
* */
|
|
10995
|
+
setAnalyticsToken(token) {
|
|
10996
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10997
|
+
return;
|
|
10998
|
+
}
|
|
10999
|
+
this.instance.setAnalyticsToken(token);
|
|
11000
|
+
}
|
|
11001
|
+
getAnalyticsToken() {
|
|
11002
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
11003
|
+
return undefined;
|
|
11004
|
+
}
|
|
11005
|
+
return this.instance.getAnalyticsToken();
|
|
11006
|
+
}
|
|
10753
11007
|
}
|
|
10754
11008
|
const tracker = new TrackerSingleton();
|
|
10755
11009
|
|