@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/cjs/entry.js
CHANGED
|
@@ -1827,6 +1827,30 @@ class CanvasRecorder {
|
|
|
1827
1827
|
this.MAX_QUEUE_SIZE = 50; // ~500 images max (50 batches × 10 images)
|
|
1828
1828
|
this.pendingBatches = [];
|
|
1829
1829
|
this.isProcessingQueue = false;
|
|
1830
|
+
/**
|
|
1831
|
+
* Reacts to a runtime sanitization change on a canvas: stop capturing if it
|
|
1832
|
+
* just became masked, start if it just became visible. (Already-sent frames
|
|
1833
|
+
* can't be retracted — escalation only stops future capture.)
|
|
1834
|
+
*/
|
|
1835
|
+
this.resanitizeCanvas = (node, id) => {
|
|
1836
|
+
if (!hasTag(node, 'canvas')) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
const isIgnored = this.app.sanitizer.isObscured(id) || this.app.sanitizer.isHidden(id);
|
|
1840
|
+
if (isIgnored) {
|
|
1841
|
+
if (this.snapshots[id] || this.observers.has(id)) {
|
|
1842
|
+
const observer = this.observers.get(id);
|
|
1843
|
+
if (observer) {
|
|
1844
|
+
observer.disconnect();
|
|
1845
|
+
this.observers.delete(id);
|
|
1846
|
+
}
|
|
1847
|
+
this.cleanupCanvas(id);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
else if (!this.snapshots[id] && !this.observers.has(id)) {
|
|
1851
|
+
this.captureCanvas(node);
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1830
1854
|
this.restartTracking = () => {
|
|
1831
1855
|
this.clear();
|
|
1832
1856
|
this.app.nodes.scanTree(this.captureCanvas);
|
|
@@ -1930,6 +1954,7 @@ class CanvasRecorder {
|
|
|
1930
1954
|
setTimeout(() => {
|
|
1931
1955
|
this.app.nodes.scanTree(this.captureCanvas);
|
|
1932
1956
|
this.app.nodes.attachNodeCallback(this.captureCanvas);
|
|
1957
|
+
this.app.attachResanitizeCallback(this.resanitizeCanvas);
|
|
1933
1958
|
}, 125);
|
|
1934
1959
|
}
|
|
1935
1960
|
sendSnaps(images, canvasId, createdAt) {
|
|
@@ -2814,6 +2839,120 @@ function ConstructedStyleSheets (app) {
|
|
|
2814
2839
|
});
|
|
2815
2840
|
}
|
|
2816
2841
|
|
|
2842
|
+
exports.SanitizeLevel = void 0;
|
|
2843
|
+
(function (SanitizeLevel) {
|
|
2844
|
+
SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
|
|
2845
|
+
SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
|
|
2846
|
+
SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
|
|
2847
|
+
})(exports.SanitizeLevel || (exports.SanitizeLevel = {}));
|
|
2848
|
+
const stringWiper = (input) => input
|
|
2849
|
+
.trim()
|
|
2850
|
+
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
|
|
2851
|
+
class Sanitizer {
|
|
2852
|
+
constructor(params) {
|
|
2853
|
+
// Node id -> level. Plain (0) is never stored; a missing entry means Plain.
|
|
2854
|
+
// A map (not the old grow-only Sets) so levels can be raised and lowered.
|
|
2855
|
+
this.levels = new Map();
|
|
2856
|
+
this.app = params.app;
|
|
2857
|
+
const defaultOptions = {
|
|
2858
|
+
obscureTextEmails: true,
|
|
2859
|
+
obscureTextNumbers: false,
|
|
2860
|
+
privateMode: false,
|
|
2861
|
+
domSanitizer: undefined,
|
|
2862
|
+
};
|
|
2863
|
+
this.privateMode = params.options?.privateMode ?? false;
|
|
2864
|
+
this.options = Object.assign(defaultOptions, params.options);
|
|
2865
|
+
}
|
|
2866
|
+
// Pure recomputation of a node's level from the live DOM + parent level.
|
|
2867
|
+
// Reading current state on every call is what lets resanitize() pick up
|
|
2868
|
+
// runtime attribute/domSanitizer changes.
|
|
2869
|
+
computeLevel(node, parentLevel) {
|
|
2870
|
+
if (this.options.privateMode) {
|
|
2871
|
+
if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
|
|
2872
|
+
return exports.SanitizeLevel.Obscured;
|
|
2873
|
+
}
|
|
2874
|
+
if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode, 'unmask')) {
|
|
2875
|
+
return exports.SanitizeLevel.Obscured;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
let level = exports.SanitizeLevel.Plain;
|
|
2879
|
+
if (parentLevel >= exports.SanitizeLevel.Obscured ||
|
|
2880
|
+
(isElementNode(node) &&
|
|
2881
|
+
(hasOpenreplayAttribute(node, 'masked') || hasOpenreplayAttribute(node, 'obscured')))) {
|
|
2882
|
+
level = exports.SanitizeLevel.Obscured;
|
|
2883
|
+
}
|
|
2884
|
+
if (parentLevel === exports.SanitizeLevel.Hidden ||
|
|
2885
|
+
(isElementNode(node) &&
|
|
2886
|
+
(hasOpenreplayAttribute(node, 'htmlmasked') || hasOpenreplayAttribute(node, 'hidden')))) {
|
|
2887
|
+
level = exports.SanitizeLevel.Hidden;
|
|
2888
|
+
}
|
|
2889
|
+
if (this.options.domSanitizer !== undefined && isElementNode(node)) {
|
|
2890
|
+
const sanitizeLevel = this.options.domSanitizer(node);
|
|
2891
|
+
if (sanitizeLevel === exports.SanitizeLevel.Obscured && level < exports.SanitizeLevel.Obscured) {
|
|
2892
|
+
level = exports.SanitizeLevel.Obscured;
|
|
2893
|
+
}
|
|
2894
|
+
if (sanitizeLevel === exports.SanitizeLevel.Hidden) {
|
|
2895
|
+
level = exports.SanitizeLevel.Hidden;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return level;
|
|
2899
|
+
}
|
|
2900
|
+
getLevel(id) {
|
|
2901
|
+
return this.levels.get(id) ?? exports.SanitizeLevel.Plain;
|
|
2902
|
+
}
|
|
2903
|
+
// Sets a node's level (either direction) and returns the previous one.
|
|
2904
|
+
setLevel(id, level) {
|
|
2905
|
+
const prev = this.getLevel(id);
|
|
2906
|
+
if (level === exports.SanitizeLevel.Plain) {
|
|
2907
|
+
this.levels.delete(id);
|
|
2908
|
+
}
|
|
2909
|
+
else {
|
|
2910
|
+
this.levels.set(id, level);
|
|
2911
|
+
}
|
|
2912
|
+
return prev;
|
|
2913
|
+
}
|
|
2914
|
+
handleNode(id, parentID, node) {
|
|
2915
|
+
const level = this.computeLevel(node, this.getLevel(parentID));
|
|
2916
|
+
// Escalate-only: commits never lower a level, only resanitize/setLevel do.
|
|
2917
|
+
if (level > this.getLevel(id)) {
|
|
2918
|
+
this.setLevel(id, level);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
sanitize(id, data) {
|
|
2922
|
+
if (this.getLevel(id) >= exports.SanitizeLevel.Obscured) {
|
|
2923
|
+
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
|
|
2924
|
+
return stringWiper(data);
|
|
2925
|
+
}
|
|
2926
|
+
if (this.options.obscureTextNumbers) {
|
|
2927
|
+
data = data.replace(/\d/g, '0');
|
|
2928
|
+
}
|
|
2929
|
+
if (this.options.obscureTextEmails) {
|
|
2930
|
+
data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
|
|
2931
|
+
const [name, domain] = email.split('@');
|
|
2932
|
+
const [domainName, host] = domain.split('.');
|
|
2933
|
+
return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
return data;
|
|
2937
|
+
}
|
|
2938
|
+
isObscured(id) {
|
|
2939
|
+
return this.getLevel(id) >= exports.SanitizeLevel.Obscured;
|
|
2940
|
+
}
|
|
2941
|
+
isHidden(id) {
|
|
2942
|
+
return this.getLevel(id) === exports.SanitizeLevel.Hidden;
|
|
2943
|
+
}
|
|
2944
|
+
getInnerTextSecure(el) {
|
|
2945
|
+
const id = this.app.nodes.getID(el);
|
|
2946
|
+
if (!id) {
|
|
2947
|
+
return '';
|
|
2948
|
+
}
|
|
2949
|
+
return this.sanitize(id, el.innerText);
|
|
2950
|
+
}
|
|
2951
|
+
clear() {
|
|
2952
|
+
this.levels.clear();
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2817
2956
|
const iconCache = {};
|
|
2818
2957
|
const svgUrlCache = {};
|
|
2819
2958
|
async function parseUseEl(useElement, mode, domParser) {
|
|
@@ -3403,6 +3542,100 @@ class Observer {
|
|
|
3403
3542
|
beforeCommit(this.app.nodes.getID(node));
|
|
3404
3543
|
this.commitNodes(true);
|
|
3405
3544
|
}
|
|
3545
|
+
/**
|
|
3546
|
+
* Re-evaluates sanitization for every tracked node in `root`'s subtree against
|
|
3547
|
+
* the current DOM and re-emits whatever changed. Pass the highest node you
|
|
3548
|
+
* changed (or the document root) so inherited levels propagate correctly.
|
|
3549
|
+
*/
|
|
3550
|
+
resanitizeSubtree(root) {
|
|
3551
|
+
if (!isObservable(root)) {
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3554
|
+
const parent = root.parentNode;
|
|
3555
|
+
const parentId = parent !== null ? this.app.nodes.getID(parent) : undefined;
|
|
3556
|
+
const parentLevel = parentId !== undefined ? this.app.sanitizer.getLevel(parentId) : exports.SanitizeLevel.Plain;
|
|
3557
|
+
this.resanitizeNode(root, parentLevel);
|
|
3558
|
+
}
|
|
3559
|
+
resanitizeNode(node, parentLevel) {
|
|
3560
|
+
if (isIgnored(node)) {
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const id = this.app.nodes.getID(node);
|
|
3564
|
+
if (id === undefined) {
|
|
3565
|
+
// Untracked (new, or under a hidden ancestor): the live observer handles it.
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
const newLevel = this.app.sanitizer.computeLevel(node, parentLevel);
|
|
3569
|
+
const prevLevel = this.app.sanitizer.getLevel(id);
|
|
3570
|
+
const wasHidden = prevLevel === exports.SanitizeLevel.Hidden;
|
|
3571
|
+
const willHidden = newLevel === exports.SanitizeLevel.Hidden;
|
|
3572
|
+
// Crossing the hidden boundary changes the rendered structure (placeholder vs
|
|
3573
|
+
// real subtree), so rebuild rather than re-emit.
|
|
3574
|
+
if (wasHidden !== willHidden) {
|
|
3575
|
+
this.recreateSubtree(node);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
if (willHidden) {
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
// Plain <-> Obscured: same structure, only leaf content changes.
|
|
3582
|
+
if (prevLevel !== newLevel) {
|
|
3583
|
+
this.app.sanitizer.setLevel(id, newLevel);
|
|
3584
|
+
this.reemitNode(id, node);
|
|
3585
|
+
}
|
|
3586
|
+
for (let child = node.firstChild; child !== null; child = child.nextSibling) {
|
|
3587
|
+
this.resanitizeNode(child, newLevel);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
// Destroys the node player-side and re-emits its subtree from scratch (new ids)
|
|
3591
|
+
// so it materializes at the freshly-computed level.
|
|
3592
|
+
recreateSubtree(node) {
|
|
3593
|
+
const id = this.app.nodes.getID(node);
|
|
3594
|
+
if (id === undefined) {
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
this.app.send(RemoveNode(id));
|
|
3598
|
+
this.clearSubtreeRegistration(node);
|
|
3599
|
+
this.bindTree(node);
|
|
3600
|
+
this.commitNodes();
|
|
3601
|
+
}
|
|
3602
|
+
clearSubtreeRegistration(node) {
|
|
3603
|
+
const clearOne = (n) => {
|
|
3604
|
+
const oldId = this.app.nodes.getID(n);
|
|
3605
|
+
if (oldId !== undefined) {
|
|
3606
|
+
this.app.sanitizer.setLevel(oldId, exports.SanitizeLevel.Plain);
|
|
3607
|
+
}
|
|
3608
|
+
this.app.nodes.unregisterNode(n);
|
|
3609
|
+
};
|
|
3610
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
3611
|
+
acceptNode: (n) => isIgnored(n) || this.app.nodes.getID(n) === undefined
|
|
3612
|
+
? NodeFilter.FILTER_REJECT
|
|
3613
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
3614
|
+
},
|
|
3615
|
+
// @ts-ignore
|
|
3616
|
+
false);
|
|
3617
|
+
// Collect first, then clear: unregistering mutates the ids the walker reads.
|
|
3618
|
+
const subtree = [];
|
|
3619
|
+
while (walker.nextNode()) {
|
|
3620
|
+
subtree.push(walker.currentNode);
|
|
3621
|
+
}
|
|
3622
|
+
clearOne(node);
|
|
3623
|
+
subtree.forEach(clearOne);
|
|
3624
|
+
}
|
|
3625
|
+
reemitNode(id, node) {
|
|
3626
|
+
if (isTextNode(node)) {
|
|
3627
|
+
const parent = node.parentNode;
|
|
3628
|
+
if (parent !== null && isElementNode(parent)) {
|
|
3629
|
+
// re-runs sanitize() at the level we just set
|
|
3630
|
+
this.sendNodeData(id, parent, node.data);
|
|
3631
|
+
}
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
if (isElementNode(node)) {
|
|
3635
|
+
// inputs/images/canvas re-emit their own payload via registered callbacks
|
|
3636
|
+
this.app.callResanitizeCallbacks(node, id);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3406
3639
|
disconnect() {
|
|
3407
3640
|
// THEORY S3: a disconnect may discard MutationRecords still queued by the
|
|
3408
3641
|
// browser. takeRecords() drains them — they would be discarded by
|
|
@@ -3762,94 +3995,6 @@ class TopObserver extends Observer {
|
|
|
3762
3995
|
}
|
|
3763
3996
|
}
|
|
3764
3997
|
|
|
3765
|
-
exports.SanitizeLevel = void 0;
|
|
3766
|
-
(function (SanitizeLevel) {
|
|
3767
|
-
SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
|
|
3768
|
-
SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
|
|
3769
|
-
SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
|
|
3770
|
-
})(exports.SanitizeLevel || (exports.SanitizeLevel = {}));
|
|
3771
|
-
const stringWiper = (input) => input
|
|
3772
|
-
.trim()
|
|
3773
|
-
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
|
|
3774
|
-
class Sanitizer {
|
|
3775
|
-
constructor(params) {
|
|
3776
|
-
this.obscured = new Set();
|
|
3777
|
-
this.hidden = new Set();
|
|
3778
|
-
this.app = params.app;
|
|
3779
|
-
const defaultOptions = {
|
|
3780
|
-
obscureTextEmails: true,
|
|
3781
|
-
obscureTextNumbers: false,
|
|
3782
|
-
privateMode: false,
|
|
3783
|
-
domSanitizer: undefined,
|
|
3784
|
-
};
|
|
3785
|
-
this.privateMode = params.options?.privateMode ?? false;
|
|
3786
|
-
this.options = Object.assign(defaultOptions, params.options);
|
|
3787
|
-
}
|
|
3788
|
-
handleNode(id, parentID, node) {
|
|
3789
|
-
if (this.options.privateMode) {
|
|
3790
|
-
if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
|
|
3791
|
-
return this.obscured.add(id);
|
|
3792
|
-
}
|
|
3793
|
-
if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode, 'unmask')) {
|
|
3794
|
-
return this.obscured.add(id);
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
if (this.obscured.has(parentID) ||
|
|
3798
|
-
(isElementNode(node) &&
|
|
3799
|
-
(hasOpenreplayAttribute(node, 'masked') || hasOpenreplayAttribute(node, 'obscured')))) {
|
|
3800
|
-
this.obscured.add(id);
|
|
3801
|
-
}
|
|
3802
|
-
if (this.hidden.has(parentID) ||
|
|
3803
|
-
(isElementNode(node) &&
|
|
3804
|
-
(hasOpenreplayAttribute(node, 'htmlmasked') || hasOpenreplayAttribute(node, 'hidden')))) {
|
|
3805
|
-
this.hidden.add(id);
|
|
3806
|
-
}
|
|
3807
|
-
if (this.options.domSanitizer !== undefined && isElementNode(node)) {
|
|
3808
|
-
const sanitizeLevel = this.options.domSanitizer(node);
|
|
3809
|
-
if (sanitizeLevel === exports.SanitizeLevel.Obscured) {
|
|
3810
|
-
this.obscured.add(id);
|
|
3811
|
-
}
|
|
3812
|
-
if (sanitizeLevel === exports.SanitizeLevel.Hidden) {
|
|
3813
|
-
this.hidden.add(id);
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
|
-
sanitize(id, data) {
|
|
3818
|
-
if (this.obscured.has(id)) {
|
|
3819
|
-
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
|
|
3820
|
-
return stringWiper(data);
|
|
3821
|
-
}
|
|
3822
|
-
if (this.options.obscureTextNumbers) {
|
|
3823
|
-
data = data.replace(/\d/g, '0');
|
|
3824
|
-
}
|
|
3825
|
-
if (this.options.obscureTextEmails) {
|
|
3826
|
-
data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
|
|
3827
|
-
const [name, domain] = email.split('@');
|
|
3828
|
-
const [domainName, host] = domain.split('.');
|
|
3829
|
-
return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
|
|
3830
|
-
});
|
|
3831
|
-
}
|
|
3832
|
-
return data;
|
|
3833
|
-
}
|
|
3834
|
-
isObscured(id) {
|
|
3835
|
-
return this.obscured.has(id);
|
|
3836
|
-
}
|
|
3837
|
-
isHidden(id) {
|
|
3838
|
-
return this.hidden.has(id);
|
|
3839
|
-
}
|
|
3840
|
-
getInnerTextSecure(el) {
|
|
3841
|
-
const id = this.app.nodes.getID(el);
|
|
3842
|
-
if (!id) {
|
|
3843
|
-
return '';
|
|
3844
|
-
}
|
|
3845
|
-
return this.sanitize(id, el.innerText);
|
|
3846
|
-
}
|
|
3847
|
-
clear() {
|
|
3848
|
-
this.obscured.clear();
|
|
3849
|
-
this.hidden.clear();
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
|
|
3853
3998
|
const tokenSeparator = '_$_';
|
|
3854
3999
|
class Session {
|
|
3855
4000
|
constructor(params) {
|
|
@@ -4101,6 +4246,8 @@ class App {
|
|
|
4101
4246
|
constructor(projectKey, sessionToken, options, signalError, insideIframe) {
|
|
4102
4247
|
this.signalError = signalError;
|
|
4103
4248
|
this.insideIframe = insideIframe;
|
|
4249
|
+
// Registered by input/img/canvas to re-emit a node when its level changes.
|
|
4250
|
+
this.resanitizeCallbacks = [];
|
|
4104
4251
|
this.messages = [];
|
|
4105
4252
|
/**
|
|
4106
4253
|
* we need 2 buffers, so we don't lose anything
|
|
@@ -4112,7 +4259,7 @@ class App {
|
|
|
4112
4259
|
this.stopCallbacks = [];
|
|
4113
4260
|
this.commitCallbacks = [];
|
|
4114
4261
|
this.activityState = ActivityState.NotActive;
|
|
4115
|
-
this.version = '18.0.14
|
|
4262
|
+
this.version = '18.0.14'; // TODO: version compatability check inside each plugin.
|
|
4116
4263
|
this.socketMode = false;
|
|
4117
4264
|
this.compressionThreshold = 24 * 1000;
|
|
4118
4265
|
this.bc = null;
|
|
@@ -4643,6 +4790,26 @@ class App {
|
|
|
4643
4790
|
this.restartCanvasTracking = () => {
|
|
4644
4791
|
this.canvasRecorder?.restartTracking();
|
|
4645
4792
|
};
|
|
4793
|
+
this.attachResanitizeCallback = (cb) => {
|
|
4794
|
+
this.resanitizeCallbacks.push(cb);
|
|
4795
|
+
};
|
|
4796
|
+
this.callResanitizeCallbacks = (node, id) => {
|
|
4797
|
+
this.resanitizeCallbacks.forEach((cb) => cb(node, id));
|
|
4798
|
+
};
|
|
4799
|
+
this.resanitize = (el) => {
|
|
4800
|
+
const root = el ?? (IN_BROWSER ? document.documentElement : undefined);
|
|
4801
|
+
if (!root) {
|
|
4802
|
+
return;
|
|
4803
|
+
}
|
|
4804
|
+
this.observer.resanitizeSubtree(root);
|
|
4805
|
+
};
|
|
4806
|
+
this.checkSanitization = (el) => {
|
|
4807
|
+
const id = this.nodes.getID(el);
|
|
4808
|
+
if (id === undefined) {
|
|
4809
|
+
return undefined;
|
|
4810
|
+
}
|
|
4811
|
+
return this.sanitizer.getLevel(id);
|
|
4812
|
+
};
|
|
4646
4813
|
this.flushBuffer = async (buffer) => {
|
|
4647
4814
|
return new Promise((res, reject) => {
|
|
4648
4815
|
if (buffer.length === 0) {
|
|
@@ -6335,6 +6502,12 @@ function Img (app) {
|
|
|
6335
6502
|
sendImgAttrs(node);
|
|
6336
6503
|
observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] });
|
|
6337
6504
|
});
|
|
6505
|
+
// On a runtime level change, re-evaluate placeholder vs real src for this image.
|
|
6506
|
+
app.attachResanitizeCallback((node) => {
|
|
6507
|
+
if (hasTag(node, 'img')) {
|
|
6508
|
+
sendImgAttrs(node);
|
|
6509
|
+
}
|
|
6510
|
+
});
|
|
6338
6511
|
}
|
|
6339
6512
|
|
|
6340
6513
|
const INPUT_TYPES = [
|
|
@@ -6521,6 +6694,13 @@ function Input (app, opts) {
|
|
|
6521
6694
|
}
|
|
6522
6695
|
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime));
|
|
6523
6696
|
}
|
|
6697
|
+
// Re-emit a field's value when its sanitization level changes at runtime.
|
|
6698
|
+
// getInputValue() reads the current level, so re-sending applies the new mask.
|
|
6699
|
+
app.attachResanitizeCallback((node, id) => {
|
|
6700
|
+
if (isTextFieldElement(node) || hasTag(node, 'select')) {
|
|
6701
|
+
sendInputValue(id, node);
|
|
6702
|
+
}
|
|
6703
|
+
});
|
|
6524
6704
|
app.nodes.attachNodeCallback(app.safe((node) => {
|
|
6525
6705
|
const id = app.nodes.getID(node);
|
|
6526
6706
|
if (id === undefined) {
|
|
@@ -9411,7 +9591,7 @@ class ConstantProperties {
|
|
|
9411
9591
|
user_id: this.user_id,
|
|
9412
9592
|
distinct_id: this.deviceId,
|
|
9413
9593
|
sdk_edition: 'web',
|
|
9414
|
-
sdk_version: '18.0.14
|
|
9594
|
+
sdk_version: '18.0.14',
|
|
9415
9595
|
timezone: getUTCOffsetString(),
|
|
9416
9596
|
search_engine: this.searchEngine,
|
|
9417
9597
|
};
|
|
@@ -10113,7 +10293,7 @@ class API {
|
|
|
10113
10293
|
this.signalStartIssue = (reason, missingApi) => {
|
|
10114
10294
|
const doNotTrack = this.checkDoNotTrack();
|
|
10115
10295
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
10116
|
-
trackerVersion: '18.0.14
|
|
10296
|
+
trackerVersion: '18.0.14',
|
|
10117
10297
|
projectKey: this.options.projectKey,
|
|
10118
10298
|
doNotTrack,
|
|
10119
10299
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|
|
@@ -10125,6 +10305,31 @@ class API {
|
|
|
10125
10305
|
}
|
|
10126
10306
|
this.app.restartCanvasTracking();
|
|
10127
10307
|
};
|
|
10308
|
+
/**
|
|
10309
|
+
* Re-evaluates sanitization against the current DOM and re-emits whatever
|
|
10310
|
+
* changed, updating already-recorded nodes mid-session. Call after toggling
|
|
10311
|
+
* `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
|
|
10312
|
+
* keys on (class/id/etc).
|
|
10313
|
+
*
|
|
10314
|
+
* @param el - the highest node you changed; omit to re-scan the whole document;
|
|
10315
|
+
* scanning the entire doc is O(dom size)
|
|
10316
|
+
* */
|
|
10317
|
+
this.resanitize = (el) => {
|
|
10318
|
+
if (this.app === null) {
|
|
10319
|
+
return;
|
|
10320
|
+
}
|
|
10321
|
+
this.app.resanitize(el);
|
|
10322
|
+
};
|
|
10323
|
+
/**
|
|
10324
|
+
* Returns the sanitization level the tracker currently has for a node
|
|
10325
|
+
* (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
|
|
10326
|
+
* */
|
|
10327
|
+
this.checkSanitization = (el) => {
|
|
10328
|
+
if (this.app === null) {
|
|
10329
|
+
return undefined;
|
|
10330
|
+
}
|
|
10331
|
+
return this.app.checkSanitization(el);
|
|
10332
|
+
};
|
|
10128
10333
|
this.getSessionURL = (options) => {
|
|
10129
10334
|
if (this.app === null) {
|
|
10130
10335
|
return undefined;
|
|
@@ -10138,7 +10343,10 @@ class API {
|
|
|
10138
10343
|
}
|
|
10139
10344
|
};
|
|
10140
10345
|
this.identify = this.setUserID;
|
|
10141
|
-
|
|
10346
|
+
// Delegates at call time: `this.analytics` is assigned in the constructor body,
|
|
10347
|
+
// which runs AFTER field initializers, so binding it here directly would always
|
|
10348
|
+
// capture `undefined`.
|
|
10349
|
+
this.track = (eventName, properties, options) => this.analytics?.track(eventName, properties, options);
|
|
10142
10350
|
this.userID = (id) => {
|
|
10143
10351
|
deprecationWarn("'userID' method", "'setUserID' method", '/');
|
|
10144
10352
|
this.setUserID(id);
|
|
@@ -10521,12 +10729,20 @@ class TrackerSingleton {
|
|
|
10521
10729
|
constructor() {
|
|
10522
10730
|
this.instance = null;
|
|
10523
10731
|
this.isConfigured = false;
|
|
10732
|
+
this.setUserID = (id) => {
|
|
10733
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10734
|
+
return;
|
|
10735
|
+
}
|
|
10736
|
+
this.instance.setUserID(id);
|
|
10737
|
+
};
|
|
10524
10738
|
this.identify = this.setUserID;
|
|
10525
10739
|
this.track = (eventName, properties, options) => {
|
|
10526
10740
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10527
10741
|
return;
|
|
10528
10742
|
}
|
|
10529
|
-
|
|
10743
|
+
// Route through analytics directly: Tracker.track is bound to analytics?.track
|
|
10744
|
+
// at field-init time (before analytics exists), so it is always undefined.
|
|
10745
|
+
this.instance.analytics?.track(eventName, properties, options);
|
|
10530
10746
|
};
|
|
10531
10747
|
}
|
|
10532
10748
|
/**
|
|
@@ -10556,11 +10772,16 @@ class TrackerSingleton {
|
|
|
10556
10772
|
if (!IN_BROWSER) {
|
|
10557
10773
|
return Promise.resolve({ success: false, reason: 'Not in browser environment' });
|
|
10558
10774
|
}
|
|
10559
|
-
if (!this.ensureConfigured()) {
|
|
10775
|
+
if (!this.ensureConfigured() || !this.instance) {
|
|
10560
10776
|
return Promise.resolve({ success: false, reason: 'Tracker not configured' });
|
|
10561
10777
|
}
|
|
10562
|
-
|
|
10563
|
-
|
|
10778
|
+
// Tracker.start() rejects (instead of resolving {success:false}) when the
|
|
10779
|
+
// underlying app failed to initialise (non-https, missing api, doNotTrack,
|
|
10780
|
+
// already initialised...). Normalize so callers always get {success, reason}.
|
|
10781
|
+
return this.instance.start(startOpts).catch((reason) => ({
|
|
10782
|
+
success: false,
|
|
10783
|
+
reason: typeof reason === 'string' ? reason : String(reason),
|
|
10784
|
+
}));
|
|
10564
10785
|
}
|
|
10565
10786
|
/**
|
|
10566
10787
|
* Stop the session and return sessionHash
|
|
@@ -10572,21 +10793,9 @@ class TrackerSingleton {
|
|
|
10572
10793
|
}
|
|
10573
10794
|
return this.instance.stop();
|
|
10574
10795
|
}
|
|
10575
|
-
setUserID(id) {
|
|
10576
|
-
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10577
|
-
return;
|
|
10578
|
-
}
|
|
10579
|
-
this.instance.setUserID(id);
|
|
10580
|
-
}
|
|
10581
10796
|
get analytics() {
|
|
10582
|
-
|
|
10583
|
-
return this.instance.analytics;
|
|
10584
|
-
}
|
|
10585
|
-
else {
|
|
10586
|
-
return null;
|
|
10587
|
-
}
|
|
10797
|
+
return this.instance?.analytics ?? null;
|
|
10588
10798
|
}
|
|
10589
|
-
;
|
|
10590
10799
|
/**
|
|
10591
10800
|
* Set metadata for the current session
|
|
10592
10801
|
*
|
|
@@ -10761,6 +10970,51 @@ class TrackerSingleton {
|
|
|
10761
10970
|
}
|
|
10762
10971
|
return this.instance.getTabId();
|
|
10763
10972
|
}
|
|
10973
|
+
/**
|
|
10974
|
+
* Re-evaluates sanitization against the current DOM and re-emits whatever
|
|
10975
|
+
* changed, updating already-recorded nodes mid-session. Call after toggling
|
|
10976
|
+
* `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
|
|
10977
|
+
* keys on (class/id/etc).
|
|
10978
|
+
*
|
|
10979
|
+
* @param el - the highest node you changed; omit to re-scan the whole document.
|
|
10980
|
+
* */
|
|
10981
|
+
resanitize(el) {
|
|
10982
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10983
|
+
return;
|
|
10984
|
+
}
|
|
10985
|
+
return this.instance.resanitize(el);
|
|
10986
|
+
}
|
|
10987
|
+
/**
|
|
10988
|
+
* Returns the sanitization level the tracker currently has for a node
|
|
10989
|
+
* (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
|
|
10990
|
+
* */
|
|
10991
|
+
checkSanitization(el) {
|
|
10992
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10993
|
+
return undefined;
|
|
10994
|
+
}
|
|
10995
|
+
return this.instance.checkSanitization(el);
|
|
10996
|
+
}
|
|
10997
|
+
incident(options) {
|
|
10998
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
10999
|
+
return;
|
|
11000
|
+
}
|
|
11001
|
+
this.instance.incident(options);
|
|
11002
|
+
}
|
|
11003
|
+
/**
|
|
11004
|
+
* Use custom token for analytics events without session recording
|
|
11005
|
+
* */
|
|
11006
|
+
setAnalyticsToken(token) {
|
|
11007
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
11008
|
+
return;
|
|
11009
|
+
}
|
|
11010
|
+
this.instance.setAnalyticsToken(token);
|
|
11011
|
+
}
|
|
11012
|
+
getAnalyticsToken() {
|
|
11013
|
+
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
|
11014
|
+
return undefined;
|
|
11015
|
+
}
|
|
11016
|
+
return this.instance.getAnalyticsToken();
|
|
11017
|
+
}
|
|
10764
11018
|
}
|
|
10765
11019
|
const tracker = new TrackerSingleton();
|
|
10766
11020
|
|