@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/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
- let id = node[this.node_id];
2348
- const isNew = id === undefined;
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
  }
@@ -2972,46 +2979,58 @@ class Observer {
2972
2979
  this.inlinerOptions = options.inlinerOptions;
2973
2980
  this.observer = createMutationObserver(this.app.safe((mutations) => {
2974
2981
  for (const mutation of mutations) {
2975
- // mutations order is sequential
2976
- const target = mutation.target;
2977
- const type = mutation.type;
2978
- if (!isObservable(target)) {
2979
- continue;
2980
- }
2981
- if (type === 'childList') {
2982
- for (let i = 0; i < mutation.removedNodes.length; i++) {
2983
- // Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
2984
- if (isObservable(mutation.removedNodes[i])) {
2985
- this.bindNode(mutation.removedNodes[i]);
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]);
2986
3004
  }
3005
+ continue;
2987
3006
  }
2988
- for (let i = 0; i < mutation.addedNodes.length; i++) {
2989
- this.bindTree(mutation.addedNodes[i]);
3007
+ const id = this.app.nodes.getID(target);
3008
+ if (id === undefined) {
3009
+ continue;
2990
3010
  }
2991
- continue;
2992
- }
2993
- const id = this.app.nodes.getID(target);
2994
- if (id === undefined) {
2995
- continue;
2996
- }
2997
- if (!this.recents.has(id)) {
2998
- this.recents.set(id, RecentsType.Changed); // TODO only when altered
2999
- }
3000
- if (type === 'attributes') {
3001
- const name = mutation.attributeName;
3002
- if (name === null) {
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);
3003
3024
  continue;
3004
3025
  }
3005
- let attr = this.attributesMap.get(id);
3006
- if (attr === undefined) {
3007
- this.attributesMap.set(id, (attr = new Set()));
3026
+ if (type === 'characterData') {
3027
+ this.textSet.add(id);
3028
+ continue;
3008
3029
  }
3009
- attr.add(name);
3010
- continue;
3011
3030
  }
3012
- if (type === 'characterData') {
3013
- this.textSet.add(id);
3014
- continue;
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)}`);
3015
3034
  }
3016
3035
  }
3017
3036
  this.commitNodes();
@@ -3177,10 +3196,11 @@ class Observer {
3177
3196
  this.bindNode(node);
3178
3197
  const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
3179
3198
  acceptNode: (node) => {
3180
- if (this.app.nodes.getID(node) !== undefined) {
3181
- this.app.debug.info('! Node is already bound', node);
3182
- }
3183
- return isIgnored(node) || this.app.nodes.getID(node) !== undefined
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)
3184
3204
  ? NodeFilter.FILTER_REJECT
3185
3205
  : NodeFilter.FILTER_ACCEPT;
3186
3206
  },
@@ -3234,15 +3254,25 @@ class Observer {
3234
3254
  if (parent === null) {
3235
3255
  // Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
3236
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
+ }
3237
3261
  this.unbindTree(node);
3238
3262
  return false;
3239
3263
  }
3240
3264
  parentID = this.app.nodes.getID(parent);
3241
3265
  if (parentID === undefined) {
3266
+ if (isElementNode(node)) {
3267
+ this.orDebug(`commit drop <${node.tagName}> reason=parent-unbound parent=<${parent.tagName ?? parent.nodeName}>`);
3268
+ }
3242
3269
  this.unbindTree(node);
3243
3270
  return false;
3244
3271
  }
3245
3272
  if (!this.commitNode(parentID)) {
3273
+ if (isElementNode(node)) {
3274
+ this.orDebug(`commit drop <${node.tagName}> reason=parent-commit-failed`);
3275
+ }
3246
3276
  this.unbindTree(node);
3247
3277
  return false;
3248
3278
  }
@@ -3386,7 +3416,45 @@ class Observer {
3386
3416
  beforeCommit(this.app.nodes.getID(node));
3387
3417
  this.commitNodes(true);
3388
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
+ }
3389
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
+ }
3390
3458
  this.observer.disconnect();
3391
3459
  this.clear();
3392
3460
  this.throttledSetNodeData.clear();
@@ -4062,7 +4130,7 @@ class App {
4062
4130
  this.stopCallbacks = [];
4063
4131
  this.commitCallbacks = [];
4064
4132
  this.activityState = ActivityState.NotActive;
4065
- this.version = '18.0.12-beta.1'; // TODO: version compatability check inside each plugin.
4133
+ this.version = '18.0.13-beta.0'; // TODO: version compatability check inside each plugin.
4066
4134
  this.socketMode = false;
4067
4135
  this.compressionThreshold = 24 * 1000;
4068
4136
  this.bc = null;
@@ -4986,6 +5054,16 @@ class App {
4986
5054
  }
4987
5055
  else if (data === 'not_init') {
4988
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
+ }
4989
5067
  }
4990
5068
  else if (data.type === 'failure') {
4991
5069
  this.stop(false);
@@ -9360,7 +9438,7 @@ class ConstantProperties {
9360
9438
  user_id: this.user_id,
9361
9439
  distinct_id: this.deviceId,
9362
9440
  sdk_edition: 'web',
9363
- sdk_version: '18.0.12-beta.1',
9441
+ sdk_version: '18.0.13-beta.0',
9364
9442
  timezone: getUTCOffsetString(),
9365
9443
  search_engine: this.searchEngine,
9366
9444
  };
@@ -10062,7 +10140,7 @@ class API {
10062
10140
  this.signalStartIssue = (reason, missingApi) => {
10063
10141
  const doNotTrack = this.checkDoNotTrack();
10064
10142
  console.log("Tracker couldn't start due to:", JSON.stringify({
10065
- trackerVersion: '18.0.12-beta.1',
10143
+ trackerVersion: '18.0.13-beta.0',
10066
10144
  projectKey: this.options.projectKey,
10067
10145
  doNotTrack,
10068
10146
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,