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