@openreplay/tracker 18.0.11 → 18.0.13-beta.0
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 +441 -54
- package/dist/cjs/entry.js.map +1 -1
- package/dist/cjs/index.js +441 -54
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/main/app/index.d.ts +61 -0
- package/dist/cjs/main/app/nodes/index.d.ts +1 -0
- package/dist/cjs/main/app/observer/observer.d.ts +6 -0
- package/dist/lib/entry.js +441 -54
- package/dist/lib/entry.js.map +1 -1
- package/dist/lib/index.js +441 -54
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/main/app/index.d.ts +61 -0
- package/dist/lib/main/app/nodes/index.d.ts +1 -0
- package/dist/lib/main/app/observer/observer.d.ts +6 -0
- package/dist/types/main/app/index.d.ts +61 -0
- package/dist/types/main/app/nodes/index.d.ts +1 -0
- package/dist/types/main/app/observer/observer.d.ts +6 -0
- package/package.json +1 -1
package/dist/lib/entry.js
CHANGED
|
@@ -2340,8 +2340,9 @@ class Nodes {
|
|
|
2340
2340
|
this.nextNodeId = this.createFrameId(level, frameOrder);
|
|
2341
2341
|
}
|
|
2342
2342
|
registerNode(node) {
|
|
2343
|
-
|
|
2344
|
-
const isNew =
|
|
2343
|
+
const existing = node[this.node_id];
|
|
2344
|
+
const isNew = existing === undefined || this.nodes.get(existing) !== node;
|
|
2345
|
+
let id = existing;
|
|
2345
2346
|
if (isNew) {
|
|
2346
2347
|
id = this.nextNodeId;
|
|
2347
2348
|
this.totalNodeAmount++;
|
|
@@ -2370,6 +2371,12 @@ class Nodes {
|
|
|
2370
2371
|
return undefined;
|
|
2371
2372
|
return node[this.node_id];
|
|
2372
2373
|
}
|
|
2374
|
+
isBound(node) {
|
|
2375
|
+
if (!node)
|
|
2376
|
+
return false;
|
|
2377
|
+
const id = node[this.node_id];
|
|
2378
|
+
return id !== undefined && this.nodes.get(id) === node;
|
|
2379
|
+
}
|
|
2373
2380
|
getNode(id) {
|
|
2374
2381
|
return this.nodes.get(id);
|
|
2375
2382
|
}
|
|
@@ -2711,9 +2718,15 @@ function ConstructedStyleSheets (app) {
|
|
|
2711
2718
|
app.send(AdoptedSSAddOwner(sheetID, nodeID));
|
|
2712
2719
|
}
|
|
2713
2720
|
if (init) {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2721
|
+
try {
|
|
2722
|
+
const rules = s.cssRules;
|
|
2723
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2724
|
+
app.send(AdoptedSSInsertRuleURLBased(sheetID, rules[i].cssText, i, app.getBaseHref()));
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
catch (e) {
|
|
2728
|
+
app.debug.log('Couldnt access adopted stylesheet', e);
|
|
2729
|
+
// Skip inaccessible (cross-origin) stylesheet
|
|
2717
2730
|
}
|
|
2718
2731
|
}
|
|
2719
2732
|
nowOwning.push(sheetID);
|
|
@@ -2962,46 +2975,58 @@ class Observer {
|
|
|
2962
2975
|
this.inlinerOptions = options.inlinerOptions;
|
|
2963
2976
|
this.observer = createMutationObserver(this.app.safe((mutations) => {
|
|
2964
2977
|
for (const mutation of mutations) {
|
|
2965
|
-
//
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2978
|
+
// THEORY S1: app.safe() wraps this whole callback in a try/catch that
|
|
2979
|
+
// SILENTLY swallows — so a throw while processing one mutation aborts
|
|
2980
|
+
// the entire batch (the single commitNodes() below never runs) and
|
|
2981
|
+
// every pending node in it is lost. Isolating + logging per mutation
|
|
2982
|
+
// both surfaces it into the session and prevents one bad node from
|
|
2983
|
+
// dropping the rest of the batch.
|
|
2984
|
+
try {
|
|
2985
|
+
// mutations order is sequential
|
|
2986
|
+
const target = mutation.target;
|
|
2987
|
+
const type = mutation.type;
|
|
2988
|
+
if (!isObservable(target)) {
|
|
2989
|
+
continue;
|
|
2990
|
+
}
|
|
2991
|
+
if (type === 'childList') {
|
|
2992
|
+
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
2993
|
+
// Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
|
|
2994
|
+
if (isObservable(mutation.removedNodes[i])) {
|
|
2995
|
+
this.bindNode(mutation.removedNodes[i]);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
2999
|
+
this.bindTree(mutation.addedNodes[i]);
|
|
2976
3000
|
}
|
|
3001
|
+
continue;
|
|
2977
3002
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
3003
|
+
const id = this.app.nodes.getID(target);
|
|
3004
|
+
if (id === undefined) {
|
|
3005
|
+
continue;
|
|
2980
3006
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3007
|
+
if (!this.recents.has(id)) {
|
|
3008
|
+
this.recents.set(id, RecentsType.Changed); // TODO only when altered
|
|
3009
|
+
}
|
|
3010
|
+
if (type === 'attributes') {
|
|
3011
|
+
const name = mutation.attributeName;
|
|
3012
|
+
if (name === null) {
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
let attr = this.attributesMap.get(id);
|
|
3016
|
+
if (attr === undefined) {
|
|
3017
|
+
this.attributesMap.set(id, (attr = new Set()));
|
|
3018
|
+
}
|
|
3019
|
+
attr.add(name);
|
|
2993
3020
|
continue;
|
|
2994
3021
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3022
|
+
if (type === 'characterData') {
|
|
3023
|
+
this.textSet.add(id);
|
|
3024
|
+
continue;
|
|
2998
3025
|
}
|
|
2999
|
-
attr.add(name);
|
|
3000
|
-
continue;
|
|
3001
3026
|
}
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3027
|
+
catch (mutationErr) {
|
|
3028
|
+
const t = mutation.target;
|
|
3029
|
+
this.orDebug(`mutation processing threw type=${mutation.type} target=<${t.tagName ?? t.nodeName ?? 'unknown'}>: ${mutationErr?.message ?? String(mutationErr)}`);
|
|
3005
3030
|
}
|
|
3006
3031
|
}
|
|
3007
3032
|
this.commitNodes();
|
|
@@ -3167,10 +3192,11 @@ class Observer {
|
|
|
3167
3192
|
this.bindNode(node);
|
|
3168
3193
|
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
3169
3194
|
acceptNode: (node) => {
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3195
|
+
// Use the Map-aware isBound() rather than the raw __openreplay_id
|
|
3196
|
+
// property: a node carrying a stale id from a previous observe cycle
|
|
3197
|
+
// must be re-accepted and re-bound here, otherwise the snapshot walk
|
|
3198
|
+
// silently skips it (and its subtree, e.g. a <style>'s CSS text).
|
|
3199
|
+
return isIgnored(node) || this.app.nodes.isBound(node)
|
|
3174
3200
|
? NodeFilter.FILTER_REJECT
|
|
3175
3201
|
: NodeFilter.FILTER_ACCEPT;
|
|
3176
3202
|
},
|
|
@@ -3224,15 +3250,25 @@ class Observer {
|
|
|
3224
3250
|
if (parent === null) {
|
|
3225
3251
|
// Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
|
|
3226
3252
|
// That shouldn't affect the visual rendering ( should it? maybe when transition applied? )
|
|
3253
|
+
// THEORY: an element silently dropped at commit time (no message emitted).
|
|
3254
|
+
if (isElementNode(node)) {
|
|
3255
|
+
this.orDebug(`commit drop <${node.tagName}> reason=no-parent`);
|
|
3256
|
+
}
|
|
3227
3257
|
this.unbindTree(node);
|
|
3228
3258
|
return false;
|
|
3229
3259
|
}
|
|
3230
3260
|
parentID = this.app.nodes.getID(parent);
|
|
3231
3261
|
if (parentID === undefined) {
|
|
3262
|
+
if (isElementNode(node)) {
|
|
3263
|
+
this.orDebug(`commit drop <${node.tagName}> reason=parent-unbound parent=<${parent.tagName ?? parent.nodeName}>`);
|
|
3264
|
+
}
|
|
3232
3265
|
this.unbindTree(node);
|
|
3233
3266
|
return false;
|
|
3234
3267
|
}
|
|
3235
3268
|
if (!this.commitNode(parentID)) {
|
|
3269
|
+
if (isElementNode(node)) {
|
|
3270
|
+
this.orDebug(`commit drop <${node.tagName}> reason=parent-commit-failed`);
|
|
3271
|
+
}
|
|
3236
3272
|
this.unbindTree(node);
|
|
3237
3273
|
return false;
|
|
3238
3274
|
}
|
|
@@ -3376,7 +3412,45 @@ class Observer {
|
|
|
3376
3412
|
beforeCommit(this.app.nodes.getID(node));
|
|
3377
3413
|
this.commitNodes(true);
|
|
3378
3414
|
}
|
|
3415
|
+
/**
|
|
3416
|
+
* [OPENREPLAYDEBUG] Emits a diagnostic line INTO the recorded session's
|
|
3417
|
+
* console (searchable in replay by "[OPENREPLAYDEBUG]"), NOT the local
|
|
3418
|
+
* devtools console — used to test capture-loss theories against real traffic.
|
|
3419
|
+
*/
|
|
3420
|
+
orDebug(message) {
|
|
3421
|
+
try {
|
|
3422
|
+
this.app.send(ConsoleLog('warn', `[OPENREPLAYDEBUG] ${message}`));
|
|
3423
|
+
}
|
|
3424
|
+
catch (_) {
|
|
3425
|
+
/* diagnostics must never break recording */
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3379
3428
|
disconnect() {
|
|
3429
|
+
// THEORY S3: a disconnect may discard MutationRecords still queued by the
|
|
3430
|
+
// browser. takeRecords() drains them — they would be discarded by
|
|
3431
|
+
// disconnect() below anyway, so this changes nothing functionally, it only
|
|
3432
|
+
// lets us SEE whether a disconnect strands un-processed DOM additions
|
|
3433
|
+
// (e.g. a freshly appended <head> <style>). NOTE: if this disconnect is part
|
|
3434
|
+
// of stop(), the surrounding buffer may be cleared and this log lost; it is
|
|
3435
|
+
// most reliable for mid-session re-observes (cold-start cycle / iframe).
|
|
3436
|
+
const pending = this.observer.takeRecords();
|
|
3437
|
+
if (pending.length) {
|
|
3438
|
+
let addedEls = 0;
|
|
3439
|
+
const tags = [];
|
|
3440
|
+
for (const m of pending) {
|
|
3441
|
+
for (let i = 0; i < m.addedNodes.length; i++) {
|
|
3442
|
+
const n = m.addedNodes[i];
|
|
3443
|
+
if (n.tagName) {
|
|
3444
|
+
addedEls++;
|
|
3445
|
+
if (tags.length < 10)
|
|
3446
|
+
tags.push(n.tagName);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
if (addedEls > 0) {
|
|
3451
|
+
this.orDebug(`disconnect stranded ${pending.length} pending record(s), ${addedEls} added element(s): ${tags.join(',')}`);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3380
3454
|
this.observer.disconnect();
|
|
3381
3455
|
this.clear();
|
|
3382
3456
|
this.throttledSetNodeData.clear();
|
|
@@ -4024,12 +4098,16 @@ const proto = {
|
|
|
4024
4098
|
parentAlive: 'signal that parent is live',
|
|
4025
4099
|
killIframe: 'stop tracker inside frame',
|
|
4026
4100
|
startIframe: 'start tracker inside frame',
|
|
4101
|
+
// child -> parent: once-per-minute encoded debug snapshot from inside an iframe
|
|
4102
|
+
iframeDebug: 'iframe debug snapshot',
|
|
4027
4103
|
// checking updates
|
|
4028
4104
|
polling: 'hello-how-are-you-im-under-the-water-please-help-me',
|
|
4029
4105
|
// happens if tab is old and has outdated token but
|
|
4030
4106
|
// not communicating with backend to update it (for whatever reason)
|
|
4031
4107
|
reset: 'reset-your-session-please',
|
|
4032
4108
|
};
|
|
4109
|
+
/** reverse map proto value -> short readable key, for the crossdomain debug log */
|
|
4110
|
+
const protoLabel = Object.fromEntries(Object.entries(proto).map(([k, v]) => [v, k]));
|
|
4033
4111
|
class App {
|
|
4034
4112
|
get tagMatcher() {
|
|
4035
4113
|
return this.tagWatcher.matcher;
|
|
@@ -4048,7 +4126,7 @@ class App {
|
|
|
4048
4126
|
this.stopCallbacks = [];
|
|
4049
4127
|
this.commitCallbacks = [];
|
|
4050
4128
|
this.activityState = ActivityState.NotActive;
|
|
4051
|
-
this.version = '18.0.
|
|
4129
|
+
this.version = '18.0.13-beta.0'; // TODO: version compatability check inside each plugin.
|
|
4052
4130
|
this.socketMode = false;
|
|
4053
4131
|
this.compressionThreshold = 24 * 1000;
|
|
4054
4132
|
this.bc = null;
|
|
@@ -4065,10 +4143,64 @@ class App {
|
|
|
4065
4143
|
this.checkStatus = () => {
|
|
4066
4144
|
return this.parentActive;
|
|
4067
4145
|
};
|
|
4146
|
+
/** child-side crossdomain debug state (only meaningful when insideIframe) */
|
|
4147
|
+
this.lastTokenReceived = null;
|
|
4148
|
+
this.lastParentMsgAt = 0;
|
|
4149
|
+
this.lastSentToParentAt = 0;
|
|
4150
|
+
this.iframeDebugInterval = null;
|
|
4151
|
+
/**
|
|
4152
|
+
* Child-side counterpart of emitCrossdomainDebug: once per minute an iframe posts an
|
|
4153
|
+
* encoded snapshot of its own tracking state up to the parent, which records it as a
|
|
4154
|
+
* console log. Posted directly (not via this.send) so it is reported even when the
|
|
4155
|
+
* child is NOT active — an inactive/orphaned child is exactly what we want to catch.
|
|
4156
|
+
*/
|
|
4157
|
+
this.emitIframeDebug = () => {
|
|
4158
|
+
if (!this.insideIframe || !this.options.crossdomain?.enabled)
|
|
4159
|
+
return;
|
|
4160
|
+
const now = Date.now();
|
|
4161
|
+
const rel = (t) => (t ? now - t : null);
|
|
4162
|
+
const payload = {
|
|
4163
|
+
ctx: this.contextId,
|
|
4164
|
+
active: this.active(),
|
|
4165
|
+
state: ActivityState[this.activityState],
|
|
4166
|
+
parentActive: this.parentActive,
|
|
4167
|
+
rootId: this.rootId,
|
|
4168
|
+
frameOrder: this.frameOderNumber,
|
|
4169
|
+
// when and what token we last received from the parent (token truncated)
|
|
4170
|
+
token: this.lastTokenReceived
|
|
4171
|
+
? { tok: this.lastTokenReceived.tok, agoMs: now - this.lastTokenReceived.at }
|
|
4172
|
+
: null,
|
|
4173
|
+
// last two-way communication with the parent
|
|
4174
|
+
lastParentMsgAgoMs: rel(this.lastParentMsgAt),
|
|
4175
|
+
lastSentToParentAgoMs: rel(this.lastSentToParentAt),
|
|
4176
|
+
};
|
|
4177
|
+
const json = JSON.stringify(payload);
|
|
4178
|
+
let encoded;
|
|
4179
|
+
try {
|
|
4180
|
+
encoded = btoa(json);
|
|
4181
|
+
}
|
|
4182
|
+
catch {
|
|
4183
|
+
encoded = json;
|
|
4184
|
+
}
|
|
4185
|
+
try {
|
|
4186
|
+
window.parent.postMessage({ line: proto.iframeDebug, context: this.contextId, debug: encoded }, this.options.crossdomain?.parentDomain ?? '*');
|
|
4187
|
+
this.markSentToParent();
|
|
4188
|
+
}
|
|
4189
|
+
catch (e) {
|
|
4190
|
+
this.debug.error('iframe debug post failed', e);
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4068
4193
|
this.parentCrossDomainFrameListener = (event) => {
|
|
4069
4194
|
const { data } = event;
|
|
4070
4195
|
if (!data || event.source === window)
|
|
4071
4196
|
return;
|
|
4197
|
+
// Debug: remember the last time the parent talked to us.
|
|
4198
|
+
if (data.line === proto.startIframe ||
|
|
4199
|
+
data.line === proto.parentAlive ||
|
|
4200
|
+
data.line === proto.iframeId ||
|
|
4201
|
+
data.line === proto.killIframe) {
|
|
4202
|
+
this.lastParentMsgAt = Date.now();
|
|
4203
|
+
}
|
|
4072
4204
|
if (data.line === proto.startIframe) {
|
|
4073
4205
|
// Avoid corrupting an in-flight start; let it complete.
|
|
4074
4206
|
if (this.activityState === ActivityState.Starting)
|
|
@@ -4079,6 +4211,7 @@ class App {
|
|
|
4079
4211
|
}
|
|
4080
4212
|
if (data.token) {
|
|
4081
4213
|
this.session.setSessionToken(data.token, this.projectKey);
|
|
4214
|
+
this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
|
|
4082
4215
|
}
|
|
4083
4216
|
if (data.id !== undefined) {
|
|
4084
4217
|
this.rootId = data.id;
|
|
@@ -4097,6 +4230,7 @@ class App {
|
|
|
4097
4230
|
this.parentActive = true;
|
|
4098
4231
|
this.rootId = data.id;
|
|
4099
4232
|
this.session.setSessionToken(data.token, this.projectKey);
|
|
4233
|
+
this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
|
|
4100
4234
|
this.frameOderNumber = data.frameOrderNumber;
|
|
4101
4235
|
this.frameLevel = data.frameLevel;
|
|
4102
4236
|
this.debug.log('starting iframe tracking', data);
|
|
@@ -4109,14 +4243,110 @@ class App {
|
|
|
4109
4243
|
}
|
|
4110
4244
|
};
|
|
4111
4245
|
this.trackedFrames = [];
|
|
4246
|
+
/** every context that has been enrolled at least once, to tell an orphan (re-adopt) apart
|
|
4247
|
+
* from a brand-new child still mid-enrollment (leave alone). */
|
|
4248
|
+
this.everTrackedFrames = new Set();
|
|
4112
4249
|
this.frameLastSeen = new Map();
|
|
4250
|
+
/** crossdomain debug diagnostics, reported once per minute as an encoded console log */
|
|
4251
|
+
this.frameOrigin = new Map();
|
|
4252
|
+
this.frameAnyLastSeen = new Map();
|
|
4253
|
+
this.frameBatchLastSeen = new Map();
|
|
4254
|
+
this.frameLastSent = new Map();
|
|
4255
|
+
this.xdomainDebugInterval = null;
|
|
4256
|
+
/** last time we re-adopted a given orphaned context, to avoid restart spam */
|
|
4257
|
+
this.reAdoptCooldown = new Map();
|
|
4258
|
+
this.RE_ADOPT_COOLDOWN_MS = 2000;
|
|
4259
|
+
/**
|
|
4260
|
+
* Stable, collision-free frame-order allocation. Node ids are partitioned by
|
|
4261
|
+
* (frameLevel, frameOrder) via pack() — every (level, order) owns its own id block, so
|
|
4262
|
+
* two simultaneously-live frames sharing an order at the same level corrupt each other's
|
|
4263
|
+
* node trees and one stops rendering. The previous `trackedFrames.findIndex+1` derived
|
|
4264
|
+
* order from a mutable array index, and pruneStaleFrames()'s .filter() shifts those
|
|
4265
|
+
* indices, so a newly enrolled frame could be handed an order still in use by a live
|
|
4266
|
+
* (but pruned) frame. We instead assign each context a persistent order, unique among all
|
|
4267
|
+
* non-recycled contexts at its level, freed only when the context is GC'd (truly gone).
|
|
4268
|
+
*/
|
|
4269
|
+
this.frameAlloc = new Map();
|
|
4270
|
+
this.usedOrdersByLevel = new Map();
|
|
4113
4271
|
this.FRAME_STALE_MS = 1500;
|
|
4272
|
+
/**
|
|
4273
|
+
* Once per minute: emit an encoded console log from the parent tracker describing every
|
|
4274
|
+
* tracked child iframe and the freshness of our two-way communication with it. Lets us
|
|
4275
|
+
* see in replay which crossdomain iframe went silent and on which leg of the handshake.
|
|
4276
|
+
*/
|
|
4277
|
+
/** drop debug entries for contexts we have neither heard from nor messaged in this long */
|
|
4278
|
+
this.XDOMAIN_DEBUG_RETENTION_MS = 10 * 60000;
|
|
4279
|
+
this.emitCrossdomainDebug = () => {
|
|
4280
|
+
if (this.insideIframe || !this.options.crossdomain?.enabled || !this.active())
|
|
4281
|
+
return;
|
|
4282
|
+
const now = Date.now();
|
|
4283
|
+
const rel = (t) => (t === undefined ? null : now - t);
|
|
4284
|
+
// Report the union of currently-tracked frames and every context we have any debug
|
|
4285
|
+
// record for: a frame that broke and stopped polling gets pruned from trackedFrames,
|
|
4286
|
+
// but it is exactly the one we want to surface (with a large lastAnyMsgAgoMs).
|
|
4287
|
+
const tracked = new Set(this.trackedFrames);
|
|
4288
|
+
const contexts = new Set([
|
|
4289
|
+
...this.trackedFrames,
|
|
4290
|
+
...this.frameAnyLastSeen.keys(),
|
|
4291
|
+
...this.frameLastSent.keys(),
|
|
4292
|
+
]);
|
|
4293
|
+
const frames = Array.from(contexts).map((ctx, i) => {
|
|
4294
|
+
const sent = this.frameLastSent.get(ctx);
|
|
4295
|
+
const alloc = this.frameAlloc.get(ctx);
|
|
4296
|
+
return {
|
|
4297
|
+
// the actual allocated (level, order) node-id partition, else an enumeration index
|
|
4298
|
+
n: alloc ? alloc.order : i + 1,
|
|
4299
|
+
level: alloc ? alloc.level : null,
|
|
4300
|
+
// identify by domain if we have it, otherwise the context id, otherwise the number
|
|
4301
|
+
id: this.frameOrigin.get(ctx) || ctx || `#${i + 1}`,
|
|
4302
|
+
tracked: tracked.has(ctx),
|
|
4303
|
+
lastAnyMsgAgoMs: rel(this.frameAnyLastSeen.get(ctx)),
|
|
4304
|
+
lastBatchAgoMs: rel(this.frameBatchLastSeen.get(ctx)),
|
|
4305
|
+
lastSent: sent ? { line: sent.line, agoMs: now - sent.t } : null,
|
|
4306
|
+
};
|
|
4307
|
+
});
|
|
4308
|
+
// GC: forget contexts that have been silent and un-messaged past the retention window.
|
|
4309
|
+
const cutoff = now - this.XDOMAIN_DEBUG_RETENTION_MS;
|
|
4310
|
+
for (const ctx of contexts) {
|
|
4311
|
+
if (tracked.has(ctx))
|
|
4312
|
+
continue;
|
|
4313
|
+
const seen = this.frameAnyLastSeen.get(ctx) ?? 0;
|
|
4314
|
+
const sentT = this.frameLastSent.get(ctx)?.t ?? 0;
|
|
4315
|
+
if (Math.max(seen, sentT) < cutoff) {
|
|
4316
|
+
this.frameOrigin.delete(ctx);
|
|
4317
|
+
this.frameAnyLastSeen.delete(ctx);
|
|
4318
|
+
this.frameBatchLastSeen.delete(ctx);
|
|
4319
|
+
this.frameLastSent.delete(ctx);
|
|
4320
|
+
this.reAdoptCooldown.delete(ctx);
|
|
4321
|
+
this.everTrackedFrames.delete(ctx);
|
|
4322
|
+
this.freeFrameOrder(ctx);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
const payload = { t: now, count: frames.length, frames };
|
|
4326
|
+
const json = JSON.stringify(payload);
|
|
4327
|
+
let encoded;
|
|
4328
|
+
try {
|
|
4329
|
+
// payload is ASCII (base36 contexts, URL origins, numbers), so plain base64 is safe
|
|
4330
|
+
encoded = btoa(json);
|
|
4331
|
+
}
|
|
4332
|
+
catch {
|
|
4333
|
+
encoded = json;
|
|
4334
|
+
}
|
|
4335
|
+
this.send(ConsoleLog('info', `[OR_XDOMAIN_DEBUG] ${encoded}`));
|
|
4336
|
+
};
|
|
4114
4337
|
this.crossDomainIframeListener = (event) => {
|
|
4115
4338
|
if (event.source === window)
|
|
4116
4339
|
return;
|
|
4117
4340
|
const { data } = event;
|
|
4118
4341
|
if (!data)
|
|
4119
4342
|
return;
|
|
4343
|
+
// Debug: remember when we last heard *anything* from this context, and its domain.
|
|
4344
|
+
if (data.context) {
|
|
4345
|
+
this.frameAnyLastSeen.set(data.context, Date.now());
|
|
4346
|
+
if (event.origin && !this.frameOrigin.has(data.context)) {
|
|
4347
|
+
this.frameOrigin.set(data.context, event.origin);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4120
4350
|
// Record liveness regardless of our own active state so the queue can prune
|
|
4121
4351
|
// stale contexts reliably once we resume dispatching commands after a cycle.
|
|
4122
4352
|
if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
|
|
@@ -4124,9 +4354,15 @@ class App {
|
|
|
4124
4354
|
}
|
|
4125
4355
|
if (!this.active())
|
|
4126
4356
|
return;
|
|
4357
|
+
if (data.line === proto.iframeDebug) {
|
|
4358
|
+
// A child posted its once-per-minute snapshot; surface it in our recorded console.
|
|
4359
|
+
this.send(ConsoleLog('info', `[OR_XDOMAIN_IFRAME_DEBUG] ${data.debug}`));
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4127
4362
|
if (data.line === proto.iframeSignal) {
|
|
4128
4363
|
// @ts-ignore
|
|
4129
4364
|
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
|
|
4365
|
+
this.recordSentToFrame(data.context, proto.parentAlive);
|
|
4130
4366
|
const signalId = async () => {
|
|
4131
4367
|
if (event.source === null) {
|
|
4132
4368
|
return console.error('Couldnt connect to event.source for child iframe tracking');
|
|
@@ -4143,22 +4379,25 @@ class App {
|
|
|
4143
4379
|
else {
|
|
4144
4380
|
this.trackedFrames.push(data.context);
|
|
4145
4381
|
}
|
|
4382
|
+
this.everTrackedFrames.add(data.context);
|
|
4146
4383
|
await this.waitStarted();
|
|
4147
4384
|
const token = this.session.getSessionToken(this.projectKey);
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4385
|
+
// Persistent, collision-free order (NOT the shifting array index). A restart of the
|
|
4386
|
+
// same context keeps its order/id-block for continuity; distinct live frames at the
|
|
4387
|
+
// same level never share one.
|
|
4388
|
+
const frameLevel = this.frameLevel + 1;
|
|
4389
|
+
const order = this.allocateFrameOrder(data.context, frameLevel);
|
|
4152
4390
|
const iframeData = {
|
|
4153
4391
|
line: proto.iframeId,
|
|
4154
4392
|
id,
|
|
4155
4393
|
token,
|
|
4156
4394
|
frameOrderNumber: order,
|
|
4157
|
-
frameLevel
|
|
4395
|
+
frameLevel,
|
|
4158
4396
|
};
|
|
4159
4397
|
this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
|
|
4160
4398
|
// @ts-ignore
|
|
4161
4399
|
event.source?.postMessage(iframeData, '*');
|
|
4400
|
+
this.recordSentToFrame(data.context, proto.iframeId);
|
|
4162
4401
|
}
|
|
4163
4402
|
catch (e) {
|
|
4164
4403
|
console.error(e);
|
|
@@ -4171,6 +4410,9 @@ class App {
|
|
|
4171
4410
|
* plus we rewrite some of the messages to be relative to the main context/window
|
|
4172
4411
|
* */
|
|
4173
4412
|
if (data.line === proto.iframeBatch) {
|
|
4413
|
+
if (data.context) {
|
|
4414
|
+
this.frameBatchLastSeen.set(data.context, Date.now());
|
|
4415
|
+
}
|
|
4174
4416
|
const msgBatch = data.messages;
|
|
4175
4417
|
const mappedMessages = [];
|
|
4176
4418
|
msgBatch.forEach((msg) => {
|
|
@@ -4218,6 +4460,16 @@ class App {
|
|
|
4218
4460
|
this.messages.push(...mappedMessages);
|
|
4219
4461
|
}
|
|
4220
4462
|
if (data.line === proto.polling) {
|
|
4463
|
+
// Self-heal: a live child that was enrolled before but fell out of trackedFrames
|
|
4464
|
+
// (pruned during a stop/start gap) keeps polling yet never re-signals. Re-adopt it
|
|
4465
|
+
// so it restarts and re-enrolls. We require everTrackedFrames so a brand-new child
|
|
4466
|
+
// still mid-enrollment (iframeSignal/checkNodeId in flight) is left alone.
|
|
4467
|
+
if (data.context &&
|
|
4468
|
+
this.everTrackedFrames.has(data.context) &&
|
|
4469
|
+
!this.trackedFrames.includes(data.context)) {
|
|
4470
|
+
this.reAdoptOrphanFrame(event, data.context);
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4221
4473
|
if (!this.pollingQueue.order.length) {
|
|
4222
4474
|
return;
|
|
4223
4475
|
}
|
|
@@ -4254,6 +4506,7 @@ class App {
|
|
|
4254
4506
|
}
|
|
4255
4507
|
// @ts-ignore
|
|
4256
4508
|
event.source?.postMessage(message, '*');
|
|
4509
|
+
this.recordSentToFrame(data.context, nextCommand);
|
|
4257
4510
|
if (this.pollingQueue[nextCommand].length === 0) {
|
|
4258
4511
|
delete this.pollingQueue[nextCommand];
|
|
4259
4512
|
this.pollingQueue.order.shift();
|
|
@@ -4291,6 +4544,7 @@ class App {
|
|
|
4291
4544
|
source: thisTab,
|
|
4292
4545
|
context: this.contextId,
|
|
4293
4546
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
4547
|
+
this.markSentToParent();
|
|
4294
4548
|
/**
|
|
4295
4549
|
* since we need to wait uncertain amount of time
|
|
4296
4550
|
* and I don't want to have recursion going on,
|
|
@@ -4311,6 +4565,7 @@ class App {
|
|
|
4311
4565
|
source: thisTab,
|
|
4312
4566
|
context: this.contextId,
|
|
4313
4567
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
4568
|
+
this.markSentToParent();
|
|
4314
4569
|
this.debug.info('Trying to signal to parent, attempt:', retries + 1);
|
|
4315
4570
|
retries++;
|
|
4316
4571
|
};
|
|
@@ -4528,7 +4783,13 @@ class App {
|
|
|
4528
4783
|
line: proto.polling,
|
|
4529
4784
|
context: this.contextId,
|
|
4530
4785
|
}, options.crossdomain?.parentDomain ?? '*');
|
|
4786
|
+
this.markSentToParent();
|
|
4531
4787
|
}, 250);
|
|
4788
|
+
// Child-only: once per minute, post an encoded snapshot of our own tracking state
|
|
4789
|
+
// (active?, token received, last comms) up to the parent so it lands in the replay.
|
|
4790
|
+
if (this.iframeDebugInterval)
|
|
4791
|
+
clearInterval(this.iframeDebugInterval);
|
|
4792
|
+
this.iframeDebugInterval = setInterval(this.emitIframeDebug, 60000);
|
|
4532
4793
|
}
|
|
4533
4794
|
else {
|
|
4534
4795
|
this.initWorker();
|
|
@@ -4537,6 +4798,13 @@ class App {
|
|
|
4537
4798
|
* so they can act as if it was just a same-domain iframe
|
|
4538
4799
|
* */
|
|
4539
4800
|
window.addEventListener('message', this.crossDomainIframeListener);
|
|
4801
|
+
// Parent-only: once per minute, log an encoded snapshot of every tracked child
|
|
4802
|
+
// iframe and the freshness of our two-way comms, to debug iframes that go silent.
|
|
4803
|
+
if (this.options.crossdomain?.enabled) {
|
|
4804
|
+
if (this.xdomainDebugInterval)
|
|
4805
|
+
clearInterval(this.xdomainDebugInterval);
|
|
4806
|
+
this.xdomainDebugInterval = setInterval(this.emitCrossdomainDebug, 60000);
|
|
4807
|
+
}
|
|
4540
4808
|
}
|
|
4541
4809
|
if (this.bc !== null) {
|
|
4542
4810
|
this.bc.postMessage({
|
|
@@ -4586,6 +4854,62 @@ class App {
|
|
|
4586
4854
|
};
|
|
4587
4855
|
}
|
|
4588
4856
|
}
|
|
4857
|
+
/** stamp every outbound post to the parent window, for the child debug snapshot */
|
|
4858
|
+
markSentToParent() {
|
|
4859
|
+
this.lastSentToParentAt = Date.now();
|
|
4860
|
+
}
|
|
4861
|
+
allocateFrameOrder(ctx, level) {
|
|
4862
|
+
const existing = this.frameAlloc.get(ctx);
|
|
4863
|
+
if (existing !== undefined)
|
|
4864
|
+
return existing.order;
|
|
4865
|
+
let used = this.usedOrdersByLevel.get(level);
|
|
4866
|
+
if (!used) {
|
|
4867
|
+
used = new Set();
|
|
4868
|
+
this.usedOrdersByLevel.set(level, used);
|
|
4869
|
+
}
|
|
4870
|
+
let order = -1;
|
|
4871
|
+
for (let n = 1; n <= MASK_ORDER; n++) {
|
|
4872
|
+
if (!used.has(n)) {
|
|
4873
|
+
order = n;
|
|
4874
|
+
break;
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
if (order === -1) {
|
|
4878
|
+
// Overflow (>127 live frames at one level): evict the least-recently-seen context at
|
|
4879
|
+
// this level that is not currently tracked, and reuse its slot rather than failing.
|
|
4880
|
+
let lru = null;
|
|
4881
|
+
let lruSeen = Infinity;
|
|
4882
|
+
const trackedSet = new Set(this.trackedFrames);
|
|
4883
|
+
this.frameAlloc.forEach((alloc, c) => {
|
|
4884
|
+
if (alloc.level !== level || trackedSet.has(c))
|
|
4885
|
+
return;
|
|
4886
|
+
const seen = this.frameAnyLastSeen.get(c) ?? 0;
|
|
4887
|
+
if (seen < lruSeen) {
|
|
4888
|
+
lruSeen = seen;
|
|
4889
|
+
lru = c;
|
|
4890
|
+
}
|
|
4891
|
+
});
|
|
4892
|
+
if (lru !== null) {
|
|
4893
|
+
order = this.frameAlloc.get(lru).order;
|
|
4894
|
+
this.frameAlloc.delete(lru);
|
|
4895
|
+
this.debug.error('OR: frame order space exhausted, evicting', lru, 'for', ctx);
|
|
4896
|
+
}
|
|
4897
|
+
else {
|
|
4898
|
+
order = MASK_ORDER;
|
|
4899
|
+
this.debug.error('OR: frame order overflow, reusing max order for', ctx);
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
used.add(order);
|
|
4903
|
+
this.frameAlloc.set(ctx, { order, level });
|
|
4904
|
+
return order;
|
|
4905
|
+
}
|
|
4906
|
+
freeFrameOrder(ctx) {
|
|
4907
|
+
const alloc = this.frameAlloc.get(ctx);
|
|
4908
|
+
if (!alloc)
|
|
4909
|
+
return;
|
|
4910
|
+
this.frameAlloc.delete(ctx);
|
|
4911
|
+
this.usedOrdersByLevel.get(alloc.level)?.delete(alloc.order);
|
|
4912
|
+
}
|
|
4589
4913
|
pruneStaleFrames() {
|
|
4590
4914
|
const staleAfter = Date.now() - this.FRAME_STALE_MS;
|
|
4591
4915
|
this.trackedFrames = this.trackedFrames.filter((ctx) => {
|
|
@@ -4596,6 +4920,45 @@ class App {
|
|
|
4596
4920
|
return false;
|
|
4597
4921
|
});
|
|
4598
4922
|
}
|
|
4923
|
+
/** records the last command/signal we posted to a given child iframe context (debug) */
|
|
4924
|
+
recordSentToFrame(ctx, line) {
|
|
4925
|
+
if (!ctx)
|
|
4926
|
+
return;
|
|
4927
|
+
this.frameLastSent.set(ctx, { line: protoLabel[line] ?? line, t: Date.now() });
|
|
4928
|
+
}
|
|
4929
|
+
/**
|
|
4930
|
+
* Self-heal for the "kill-then-prune orphan" race: a live child can fall out of
|
|
4931
|
+
* `trackedFrames` (its 250ms poll was delayed past FRAME_STALE_MS during the parent's
|
|
4932
|
+
* stop/start NotActive gap, so pruneStaleFrames evicted it). It keeps polling but the
|
|
4933
|
+
* only re-enrollment path is an `iframeSignal`, which a stopped/active-but-orphaned
|
|
4934
|
+
* child never re-emits — so it would record nothing forever. When we (the parent) are
|
|
4935
|
+
* active and see a poll from an un-tracked context, push a `startIframe` so the child
|
|
4936
|
+
* restarts, re-runs the full handshake and re-observes with a fresh rootId. Cooldowned
|
|
4937
|
+
* so we don't spam restarts during the child's start window.
|
|
4938
|
+
*/
|
|
4939
|
+
reAdoptOrphanFrame(event, ctx) {
|
|
4940
|
+
const now = Date.now();
|
|
4941
|
+
const last = this.reAdoptCooldown.get(ctx) ?? 0;
|
|
4942
|
+
if (now - last < this.RE_ADOPT_COOLDOWN_MS)
|
|
4943
|
+
return;
|
|
4944
|
+
this.reAdoptCooldown.set(ctx, now);
|
|
4945
|
+
const message = {
|
|
4946
|
+
line: proto.startIframe,
|
|
4947
|
+
token: this.session.getSessionToken(this.projectKey),
|
|
4948
|
+
};
|
|
4949
|
+
const targetFrame = this.pageFrames.find((f) => f.contentWindow === event.source) ||
|
|
4950
|
+
Array.from(document.querySelectorAll('iframe')).find((f) => f.contentWindow === event.source);
|
|
4951
|
+
if (targetFrame) {
|
|
4952
|
+
const nodeId = targetFrame[this.options.node_id];
|
|
4953
|
+
if (nodeId !== undefined) {
|
|
4954
|
+
message.id = nodeId;
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
// @ts-ignore
|
|
4958
|
+
event.source?.postMessage(message, '*');
|
|
4959
|
+
this.recordSentToFrame(ctx, proto.startIframe);
|
|
4960
|
+
this.debug.log('Re-adopting orphaned crossdomain iframe', ctx);
|
|
4961
|
+
}
|
|
4599
4962
|
allowAppStart() {
|
|
4600
4963
|
this.canStart = true;
|
|
4601
4964
|
if (this.startTimeout) {
|
|
@@ -4687,6 +5050,16 @@ class App {
|
|
|
4687
5050
|
}
|
|
4688
5051
|
else if (data === 'not_init') {
|
|
4689
5052
|
this.debug.warn('OR WebWorker: writer not initialised. Restarting tracker');
|
|
5053
|
+
// [OPENREPLAYDEBUG] THEORY: a message batch reached the worker before the
|
|
5054
|
+
// writer was initialised (start handshake not finished) and was DISCARDED.
|
|
5055
|
+
// Surface it into the session (searchable in replay) to test whether
|
|
5056
|
+
// startup batches — which can carry the initial DOM snapshot — are lost.
|
|
5057
|
+
try {
|
|
5058
|
+
this.send(ConsoleLog('warn', '[OPENREPLAYDEBUG] worker not_init: a pre-start message batch was discarded'));
|
|
5059
|
+
}
|
|
5060
|
+
catch (_) {
|
|
5061
|
+
/* diagnostics must never break recording */
|
|
5062
|
+
}
|
|
4690
5063
|
}
|
|
4691
5064
|
else if (data.type === 'failure') {
|
|
4692
5065
|
this.stop(false);
|
|
@@ -4761,7 +5134,9 @@ class App {
|
|
|
4761
5134
|
window.parent.postMessage({
|
|
4762
5135
|
line: proto.iframeBatch,
|
|
4763
5136
|
messages: this.messages,
|
|
5137
|
+
context: this.contextId,
|
|
4764
5138
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
5139
|
+
this.markSentToParent();
|
|
4765
5140
|
this.commitCallbacks.forEach((cb) => cb(this.messages));
|
|
4766
5141
|
this.messages.length = 0;
|
|
4767
5142
|
return;
|
|
@@ -7017,15 +7392,28 @@ function CSSRules (app, opts) {
|
|
|
7017
7392
|
if (!nodeID)
|
|
7018
7393
|
return;
|
|
7019
7394
|
const sheet = node.sheet;
|
|
7395
|
+
// Accessing cssRules on a cross-origin stylesheet (e.g. injected by a
|
|
7396
|
+
// browser extension) throws a SecurityError. Probe it before registering
|
|
7397
|
+
// the sheet so an inaccessible sheet is skipped instead of aborting start.
|
|
7398
|
+
let rules;
|
|
7399
|
+
try {
|
|
7400
|
+
rules = sheet.cssRules;
|
|
7401
|
+
}
|
|
7402
|
+
catch (e) {
|
|
7403
|
+
// Skip inaccessible (cross-origin) stylesheet
|
|
7404
|
+
app.debug.log('Couldnt access stylesheet during initial scan', e);
|
|
7405
|
+
return;
|
|
7406
|
+
}
|
|
7020
7407
|
const sheetID = nextID();
|
|
7021
7408
|
styleSheetIDMap.set(sheet, sheetID);
|
|
7022
7409
|
app.send(AdoptedSSAddOwner(sheetID, nodeID));
|
|
7023
|
-
for (let i = 0; i <
|
|
7410
|
+
for (let i = 0; i < rules.length; i++) {
|
|
7024
7411
|
try {
|
|
7025
|
-
sendInsertDeleteRule(sheet, i,
|
|
7412
|
+
sendInsertDeleteRule(sheet, i, rules[i].cssText);
|
|
7026
7413
|
}
|
|
7027
7414
|
catch (e) {
|
|
7028
7415
|
// Skip inaccessible rules
|
|
7416
|
+
app.debug.log('Couldnt access stylesheet rule during initial scan', e);
|
|
7029
7417
|
}
|
|
7030
7418
|
}
|
|
7031
7419
|
});
|
|
@@ -7454,7 +7842,7 @@ class NetworkMessage {
|
|
|
7454
7842
|
return null;
|
|
7455
7843
|
const gqlHeader = "application/graphql-response";
|
|
7456
7844
|
const isGraphql = messageInfo.url.includes("/graphql")
|
|
7457
|
-
|| Object.values(messageInfo.request.headers).some(v => v
|
|
7845
|
+
|| Object.values(messageInfo.request.headers).some(v => v.includes(gqlHeader));
|
|
7458
7846
|
if (isGraphql && messageInfo.response.body && typeof messageInfo.response.body === 'string') {
|
|
7459
7847
|
const isError = messageInfo.response.body.includes("errors");
|
|
7460
7848
|
messageInfo.status = isError ? 400 : 200;
|
|
@@ -7558,7 +7946,6 @@ const genStringBody = (body) => {
|
|
|
7558
7946
|
}
|
|
7559
7947
|
else if (body instanceof Blob ||
|
|
7560
7948
|
body instanceof ReadableStream ||
|
|
7561
|
-
ArrayBuffer.isView(body) ||
|
|
7562
7949
|
body instanceof ArrayBuffer) {
|
|
7563
7950
|
result = 'byte data';
|
|
7564
7951
|
}
|
|
@@ -9047,7 +9434,7 @@ class ConstantProperties {
|
|
|
9047
9434
|
user_id: this.user_id,
|
|
9048
9435
|
distinct_id: this.deviceId,
|
|
9049
9436
|
sdk_edition: 'web',
|
|
9050
|
-
sdk_version: '18.0.
|
|
9437
|
+
sdk_version: '18.0.13-beta.0',
|
|
9051
9438
|
timezone: getUTCOffsetString(),
|
|
9052
9439
|
search_engine: this.searchEngine,
|
|
9053
9440
|
};
|
|
@@ -9749,7 +10136,7 @@ class API {
|
|
|
9749
10136
|
this.signalStartIssue = (reason, missingApi) => {
|
|
9750
10137
|
const doNotTrack = this.checkDoNotTrack();
|
|
9751
10138
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
9752
|
-
trackerVersion: '18.0.
|
|
10139
|
+
trackerVersion: '18.0.13-beta.0',
|
|
9753
10140
|
projectKey: this.options.projectKey,
|
|
9754
10141
|
doNotTrack,
|
|
9755
10142
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|