@openreplay/tracker 18.0.12-beta.1 → 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/lib/index.js CHANGED
@@ -2340,8 +2340,9 @@ class Nodes {
2340
2340
  this.nextNodeId = this.createFrameId(level, frameOrder);
2341
2341
  }
2342
2342
  registerNode(node) {
2343
- let id = node[this.node_id];
2344
- const isNew = id === undefined;
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
  }
@@ -2968,46 +2975,58 @@ class Observer {
2968
2975
  this.inlinerOptions = options.inlinerOptions;
2969
2976
  this.observer = createMutationObserver(this.app.safe((mutations) => {
2970
2977
  for (const mutation of mutations) {
2971
- // mutations order is sequential
2972
- const target = mutation.target;
2973
- const type = mutation.type;
2974
- if (!isObservable(target)) {
2975
- continue;
2976
- }
2977
- if (type === 'childList') {
2978
- for (let i = 0; i < mutation.removedNodes.length; i++) {
2979
- // Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
2980
- if (isObservable(mutation.removedNodes[i])) {
2981
- this.bindNode(mutation.removedNodes[i]);
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]);
2982
3000
  }
3001
+ continue;
2983
3002
  }
2984
- for (let i = 0; i < mutation.addedNodes.length; i++) {
2985
- this.bindTree(mutation.addedNodes[i]);
3003
+ const id = this.app.nodes.getID(target);
3004
+ if (id === undefined) {
3005
+ continue;
2986
3006
  }
2987
- continue;
2988
- }
2989
- const id = this.app.nodes.getID(target);
2990
- if (id === undefined) {
2991
- continue;
2992
- }
2993
- if (!this.recents.has(id)) {
2994
- this.recents.set(id, RecentsType.Changed); // TODO only when altered
2995
- }
2996
- if (type === 'attributes') {
2997
- const name = mutation.attributeName;
2998
- if (name === null) {
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);
2999
3020
  continue;
3000
3021
  }
3001
- let attr = this.attributesMap.get(id);
3002
- if (attr === undefined) {
3003
- this.attributesMap.set(id, (attr = new Set()));
3022
+ if (type === 'characterData') {
3023
+ this.textSet.add(id);
3024
+ continue;
3004
3025
  }
3005
- attr.add(name);
3006
- continue;
3007
3026
  }
3008
- if (type === 'characterData') {
3009
- this.textSet.add(id);
3010
- continue;
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)}`);
3011
3030
  }
3012
3031
  }
3013
3032
  this.commitNodes();
@@ -3173,10 +3192,11 @@ class Observer {
3173
3192
  this.bindNode(node);
3174
3193
  const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
3175
3194
  acceptNode: (node) => {
3176
- if (this.app.nodes.getID(node) !== undefined) {
3177
- this.app.debug.info('! Node is already bound', node);
3178
- }
3179
- return isIgnored(node) || this.app.nodes.getID(node) !== undefined
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)
3180
3200
  ? NodeFilter.FILTER_REJECT
3181
3201
  : NodeFilter.FILTER_ACCEPT;
3182
3202
  },
@@ -3230,15 +3250,25 @@ class Observer {
3230
3250
  if (parent === null) {
3231
3251
  // Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
3232
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
+ }
3233
3257
  this.unbindTree(node);
3234
3258
  return false;
3235
3259
  }
3236
3260
  parentID = this.app.nodes.getID(parent);
3237
3261
  if (parentID === undefined) {
3262
+ if (isElementNode(node)) {
3263
+ this.orDebug(`commit drop <${node.tagName}> reason=parent-unbound parent=<${parent.tagName ?? parent.nodeName}>`);
3264
+ }
3238
3265
  this.unbindTree(node);
3239
3266
  return false;
3240
3267
  }
3241
3268
  if (!this.commitNode(parentID)) {
3269
+ if (isElementNode(node)) {
3270
+ this.orDebug(`commit drop <${node.tagName}> reason=parent-commit-failed`);
3271
+ }
3242
3272
  this.unbindTree(node);
3243
3273
  return false;
3244
3274
  }
@@ -3382,7 +3412,45 @@ class Observer {
3382
3412
  beforeCommit(this.app.nodes.getID(node));
3383
3413
  this.commitNodes(true);
3384
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
+ }
3385
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
+ }
3386
3454
  this.observer.disconnect();
3387
3455
  this.clear();
3388
3456
  this.throttledSetNodeData.clear();
@@ -4058,7 +4126,7 @@ class App {
4058
4126
  this.stopCallbacks = [];
4059
4127
  this.commitCallbacks = [];
4060
4128
  this.activityState = ActivityState.NotActive;
4061
- this.version = '18.0.12-beta.1'; // TODO: version compatability check inside each plugin.
4129
+ this.version = '18.0.13-beta.0'; // TODO: version compatability check inside each plugin.
4062
4130
  this.socketMode = false;
4063
4131
  this.compressionThreshold = 24 * 1000;
4064
4132
  this.bc = null;
@@ -4982,6 +5050,16 @@ class App {
4982
5050
  }
4983
5051
  else if (data === 'not_init') {
4984
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
+ }
4985
5063
  }
4986
5064
  else if (data.type === 'failure') {
4987
5065
  this.stop(false);
@@ -9356,7 +9434,7 @@ class ConstantProperties {
9356
9434
  user_id: this.user_id,
9357
9435
  distinct_id: this.deviceId,
9358
9436
  sdk_edition: 'web',
9359
- sdk_version: '18.0.12-beta.1',
9437
+ sdk_version: '18.0.13-beta.0',
9360
9438
  timezone: getUTCOffsetString(),
9361
9439
  search_engine: this.searchEngine,
9362
9440
  };
@@ -10058,7 +10136,7 @@ class API {
10058
10136
  this.signalStartIssue = (reason, missingApi) => {
10059
10137
  const doNotTrack = this.checkDoNotTrack();
10060
10138
  console.log("Tracker couldn't start due to:", JSON.stringify({
10061
- trackerVersion: '18.0.12-beta.1',
10139
+ trackerVersion: '18.0.13-beta.0',
10062
10140
  projectKey: this.options.projectKey,
10063
10141
  doNotTrack,
10064
10142
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,