@amplitude/session-replay-browser 1.46.0 → 1.47.0-sr-trc-debug-log.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.
Files changed (65) hide show
  1. package/lib/cjs/config/joined-config.d.ts +6 -1
  2. package/lib/cjs/config/joined-config.d.ts.map +1 -1
  3. package/lib/cjs/config/joined-config.js +58 -18
  4. package/lib/cjs/config/joined-config.js.map +1 -1
  5. package/lib/cjs/config/local-config.d.ts +5 -1
  6. package/lib/cjs/config/local-config.d.ts.map +1 -1
  7. package/lib/cjs/config/local-config.js +19 -1
  8. package/lib/cjs/config/local-config.js.map +1 -1
  9. package/lib/cjs/config/types.d.ts +19 -1
  10. package/lib/cjs/config/types.d.ts.map +1 -1
  11. package/lib/cjs/config/types.js.map +1 -1
  12. package/lib/cjs/diagnostics.d.ts +43 -0
  13. package/lib/cjs/diagnostics.d.ts.map +1 -0
  14. package/lib/cjs/diagnostics.js +54 -0
  15. package/lib/cjs/diagnostics.js.map +1 -0
  16. package/lib/cjs/session-replay.d.ts +25 -0
  17. package/lib/cjs/session-replay.d.ts.map +1 -1
  18. package/lib/cjs/session-replay.js +246 -51
  19. package/lib/cjs/session-replay.js.map +1 -1
  20. package/lib/cjs/targeting/targeting-manager.d.ts +4 -1
  21. package/lib/cjs/targeting/targeting-manager.d.ts.map +1 -1
  22. package/lib/cjs/targeting/targeting-manager.js +43 -10
  23. package/lib/cjs/targeting/targeting-manager.js.map +1 -1
  24. package/lib/cjs/version.d.ts +1 -1
  25. package/lib/cjs/version.d.ts.map +1 -1
  26. package/lib/cjs/version.js +1 -1
  27. package/lib/cjs/version.js.map +1 -1
  28. package/lib/esm/config/joined-config.d.ts +6 -1
  29. package/lib/esm/config/joined-config.d.ts.map +1 -1
  30. package/lib/esm/config/joined-config.js +58 -18
  31. package/lib/esm/config/joined-config.js.map +1 -1
  32. package/lib/esm/config/local-config.d.ts +5 -1
  33. package/lib/esm/config/local-config.d.ts.map +1 -1
  34. package/lib/esm/config/local-config.js +20 -2
  35. package/lib/esm/config/local-config.js.map +1 -1
  36. package/lib/esm/config/types.d.ts +19 -1
  37. package/lib/esm/config/types.d.ts.map +1 -1
  38. package/lib/esm/config/types.js.map +1 -1
  39. package/lib/esm/diagnostics.d.ts +43 -0
  40. package/lib/esm/diagnostics.d.ts.map +1 -0
  41. package/lib/esm/diagnostics.js +51 -0
  42. package/lib/esm/diagnostics.js.map +1 -0
  43. package/lib/esm/session-replay.d.ts +25 -0
  44. package/lib/esm/session-replay.d.ts.map +1 -1
  45. package/lib/esm/session-replay.js +246 -51
  46. package/lib/esm/session-replay.js.map +1 -1
  47. package/lib/esm/targeting/targeting-manager.d.ts +4 -1
  48. package/lib/esm/targeting/targeting-manager.d.ts.map +1 -1
  49. package/lib/esm/targeting/targeting-manager.js +43 -10
  50. package/lib/esm/targeting/targeting-manager.js.map +1 -1
  51. package/lib/esm/version.d.ts +1 -1
  52. package/lib/esm/version.d.ts.map +1 -1
  53. package/lib/esm/version.js +1 -1
  54. package/lib/esm/version.js.map +1 -1
  55. package/lib/scripts/index-min.js +1 -1
  56. package/lib/scripts/index-min.js.gz +0 -0
  57. package/lib/scripts/index-min.js.map +1 -1
  58. package/lib/scripts/observers-min.js +1 -1
  59. package/lib/scripts/observers-min.js.gz +0 -0
  60. package/lib/scripts/session-replay-browser-min.js +1 -1
  61. package/lib/scripts/session-replay-browser-min.js.gz +0 -0
  62. package/lib/scripts/session-replay-browser-min.js.map +1 -1
  63. package/lib/scripts/targeting-min.js +1 -1
  64. package/lib/scripts/targeting-min.js.gz +0 -0
  65. package/package.json +2 -2
@@ -17,6 +17,7 @@ var identifiers_1 = require("./identifiers");
17
17
  var logger_1 = require("./logger");
18
18
  var replay_start_time_store_1 = require("./replay-start-time-store");
19
19
  var targeting_manager_1 = require("./targeting/targeting-manager");
20
+ var diagnostics_1 = require("./diagnostics");
20
21
  var sampling_1 = require("./sampling");
21
22
  var version_1 = require("./version");
22
23
  var url_tracking_plugin_1 = require("./plugins/url-tracking-plugin");
@@ -28,6 +29,10 @@ var SessionReplay = /** @class */ (function () {
28
29
  this.recordCancelCallback = null;
29
30
  this.eventCount = 0;
30
31
  this.sessionTargetingMatch = false;
32
+ // Session for which the one-per-session TRC diagnostic event was already emitted. The
33
+ // diagnostics client caps in-memory events, so per-call signals go through counters and only
34
+ // a single rich snapshot event is recorded per session.
35
+ this.trcDiagnosticSessionId = undefined;
31
36
  // Public on purpose. `pageLeaveFns` is iterated by `pageLeaveListener`,
32
37
  // `rrwebEventManager` is dereferenced in `asyncSetSessionId` to drop the beacon buffer
33
38
  // at a session boundary, and `sessionStartTime` drives `isBelowMinSessionDuration()`.
@@ -118,12 +123,23 @@ var SessionReplay = /** @class */ (function () {
118
123
  if (forceRestart === void 0) { forceRestart = false; }
119
124
  if (forceTargetingReevaluation === void 0) { forceTargetingReevaluation = false; }
120
125
  return tslib_1.__awaiter(_this, void 0, void 0, function () {
121
- var targetingConfig, shouldReEvaluate, urlChangeEvaluationId, eventForTargeting, pageUrl, pageForTargeting, targetingMatch;
122
- var _a, _b, _c, _d, _e, _f;
123
- return tslib_1.__generator(this, function (_g) {
124
- switch (_g.label) {
126
+ var targetingConfig, shouldReEvaluate, urlChangeEvaluationId, eventForTargeting, pageUrl, pageForTargeting, evalStart, targetingMatch, evalDurationMs, trigger;
127
+ var _a, _b, _c, _d, _e, _f, _g;
128
+ return tslib_1.__generator(this, function (_h) {
129
+ switch (_h.label) {
125
130
  case 0:
131
+ // What triggered this evaluation (init vs SPA URL change vs analytics event) — shows in
132
+ // DataDog how often re-evaluation actually runs.
133
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.evalTrigger(isInit ? 'init' : forceTargetingReevaluation ? 'urlchange' : 'event'));
126
134
  if (!this.identifiers || !this.identifiers.sessionId || !this.config) {
135
+ // Q4: eval can't run because a prerequisite is missing — record exactly which one.
136
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.evalMissingPrereq);
137
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.evalMissingPrereq, {
138
+ hasIdentifiers: !!this.identifiers,
139
+ hasSessionId: !!((_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId),
140
+ hasConfig: !!this.config,
141
+ hasDeviceId: !!this.getDeviceId(),
142
+ });
127
143
  if (this.identifiers && !this.identifiers.sessionId) {
128
144
  this.loggerProvider.log('Session ID has not been set yet, cannot evaluate targeting for Session Replay.');
129
145
  }
@@ -134,6 +150,7 @@ var SessionReplay = /** @class */ (function () {
134
150
  }
135
151
  // Handle cases where there's no targeting config
136
152
  if (!this.config.targetingConfig) {
153
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.evalNoConfig);
137
154
  if (isInit) {
138
155
  this.loggerProvider.log('Targeting config has not been set yet, cannot evaluate targeting.');
139
156
  }
@@ -153,8 +170,9 @@ var SessionReplay = /** @class */ (function () {
153
170
  Object.values(analytics_core_1.SpecialEventType).includes(eventForTargeting.event_type)) {
154
171
  eventForTargeting = undefined;
155
172
  }
156
- pageUrl = (_e = (_b = (_a = targetingParams.page) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : (_d = (_c = (0, analytics_core_1.getGlobalScope)()) === null || _c === void 0 ? void 0 : _c.location) === null || _d === void 0 ? void 0 : _d.href) !== null && _e !== void 0 ? _e : '';
157
- pageForTargeting = (_f = targetingParams.page) !== null && _f !== void 0 ? _f : (pageUrl !== '' ? { url: pageUrl } : undefined);
173
+ pageUrl = (_f = (_c = (_b = targetingParams.page) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : (_e = (_d = (0, analytics_core_1.getGlobalScope)()) === null || _d === void 0 ? void 0 : _d.location) === null || _e === void 0 ? void 0 : _e.href) !== null && _f !== void 0 ? _f : '';
174
+ pageForTargeting = (_g = targetingParams.page) !== null && _g !== void 0 ? _g : (pageUrl !== '' ? { url: pageUrl } : undefined);
175
+ evalStart = Date.now();
158
176
  return [4 /*yield*/, (0, targeting_manager_1.evaluateTargetingAndStore)({
159
177
  sessionId: this.identifiers.sessionId,
160
178
  targetingConfig: targetingConfig,
@@ -166,38 +184,76 @@ var SessionReplay = /** @class */ (function () {
166
184
  page: pageForTargeting,
167
185
  },
168
186
  urlChange: forceTargetingReevaluation,
187
+ diagnosticsClient: this.config.diagnosticsClient,
188
+ deviceId: this.getDeviceId(),
169
189
  })];
170
190
  case 1:
171
- targetingMatch = _g.sent();
191
+ targetingMatch = _h.sent();
192
+ evalDurationMs = Date.now() - evalStart;
193
+ trigger = isInit ? 'init' : forceTargetingReevaluation ? 'urlchange' : 'event';
194
+ // Evaluation latency — surfaces the slow-network residual gap (SR-4234) in DataDog as
195
+ // sdk.diagnostics.sr.trc.eval.duration_ms.{avg,max,...}.
196
+ this.recordDiagnosticHistogram(diagnostics_1.SrDiagnostic.evalDurationMs, evalDurationMs);
172
197
  if (forceTargetingReevaluation &&
173
198
  urlChangeEvaluationId !== undefined &&
174
199
  urlChangeEvaluationId !== this.latestUrlChangeTargetingEvaluationId) {
200
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.evalStaleDiscarded);
201
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.evalStaleDiscarded, {
202
+ sessionId: this.identifiers.sessionId,
203
+ pageUrl: pageForTargeting === null || pageForTargeting === void 0 ? void 0 : pageForTargeting.url,
204
+ evaluationId: urlChangeEvaluationId,
205
+ latestEvaluationId: this.latestUrlChangeTargetingEvaluationId,
206
+ evalDurationMs: evalDurationMs,
207
+ });
175
208
  this.loggerProvider.debug("Ignoring stale URL-change targeting result #".concat(urlChangeEvaluationId, "; latest is #").concat(this.latestUrlChangeTargetingEvaluationId, "."));
176
209
  return [2 /*return*/];
177
210
  }
211
+ // Per-evaluation outcome (distinct from the per-session sr.gate.* gate counters).
212
+ this.incrementDiagnostic(targetingMatch ? diagnostics_1.SrDiagnostic.evalMatch : diagnostics_1.SrDiagnostic.evalNoMatch);
178
213
  // Keep targeting match monotonic within a session: once true, always true.
179
214
  // This avoids races where an older in-flight evaluation resolves false after
180
215
  // a newer evaluation already resolved true.
181
216
  this.sessionTargetingMatch = this.sessionTargetingMatch || targetingMatch;
217
+ // Q5: record ALL evaluation params (→ DataDog Logs) so a single evaluation can be fully
218
+ // reconstructed by URL, identifiers, inputs, outcome, trigger and latency. userProperties
219
+ // values are intentionally reduced to keys to avoid logging PII.
220
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.evalEvent, {
221
+ // sessionId + deviceId are stamped by recordDiagnosticEvent.
222
+ trigger: trigger,
223
+ matched: targetingMatch,
224
+ sessionTargetingMatch: this.sessionTargetingMatch,
225
+ pageUrl: pageForTargeting === null || pageForTargeting === void 0 ? void 0 : pageForTargeting.url,
226
+ hasDeviceId: !!this.getDeviceId(),
227
+ hasEvent: !!eventForTargeting,
228
+ eventType: eventForTargeting === null || eventForTargeting === void 0 ? void 0 : eventForTargeting.event_type,
229
+ userPropertyKeys: targetingParams.userProperties ? Object.keys(targetingParams.userProperties) : [],
230
+ evalDurationMs: evalDurationMs,
231
+ });
182
232
  this.loggerProvider.debug(JSON.stringify({
183
233
  name: 'targeted replay capture config',
184
234
  sessionTargetingMatch: this.sessionTargetingMatch,
185
235
  event: eventForTargeting,
186
236
  targetingParams: targetingParams,
187
237
  }, null, 2));
188
- _g.label = 2;
238
+ return [3 /*break*/, 3];
189
239
  case 2:
190
- if (!isInit) return [3 /*break*/, 3];
191
- void this.initialize(true);
192
- return [3 /*break*/, 5];
240
+ if (targetingConfig && this.sessionTargetingMatch) {
241
+ // Already matched earlier this session — recording continues without re-evaluation.
242
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.evalSkippedAlreadyMatched);
243
+ }
244
+ _h.label = 3;
193
245
  case 3:
194
- if (!(forceRestart || !this.recordCancelCallback)) return [3 /*break*/, 5];
246
+ if (!isInit) return [3 /*break*/, 4];
247
+ void this.initialize(true);
248
+ return [3 /*break*/, 6];
249
+ case 4:
250
+ if (!(forceRestart || !this.recordCancelCallback)) return [3 /*break*/, 6];
195
251
  this.loggerProvider.log('Recording events for session due to forceRestart or no ongoing recording.');
196
252
  return [4 /*yield*/, this.recordEvents()];
197
- case 4:
198
- _g.sent();
199
- _g.label = 5;
200
- case 5: return [2 /*return*/];
253
+ case 5:
254
+ _h.sent();
255
+ _h.label = 6;
256
+ case 6: return [2 /*return*/];
201
257
  }
202
258
  });
203
259
  });
@@ -284,6 +340,15 @@ var SessionReplay = /** @class */ (function () {
284
340
  var hasTargeting = !!((_b = this.config) === null || _b === void 0 ? void 0 : _b.targetingConfig);
285
341
  var onUrlChange = function (href) {
286
342
  _this.currentPageUrl = href;
343
+ // Team-visible signal that an SPA navigation was detected at all (covers the "is the SDK
344
+ // even seeing route changes in this framework?" question — counter + a log with the href
345
+ // and whether it triggered a targeting re-eval).
346
+ _this.incrementDiagnostic(diagnostics_1.SrDiagnostic.urlChange);
347
+ _this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.urlChangeEvent, {
348
+ href: href,
349
+ hasTargeting: hasTargeting,
350
+ alreadyMatched: _this.sessionTargetingMatch,
351
+ });
287
352
  if (hasTargeting) {
288
353
  var evaluationId = ++_this.latestUrlChangeTargetingEvaluationId;
289
354
  void _this.evaluateTargetingAndCapture({
@@ -324,13 +389,13 @@ var SessionReplay = /** @class */ (function () {
324
389
  return currentUrl != null ? { url: currentUrl } : undefined;
325
390
  };
326
391
  SessionReplay.prototype._init = function (apiKey, options) {
327
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
392
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
328
393
  return tslib_1.__awaiter(this, void 0, void 0, function () {
329
- var now, _q, _r, joinedConfig, localConfig, remoteConfig, scrollWatcher, managers, storeType, compressionWorkerScript, trackDestinationWorkerScript, globalScope, _s, compressionScript, trackDestinationScript, rrwebEventManager, error_1, typedError, payloadBatcher, interactionEventManager, error_2, typedError, onFullSnapshotProcessed, pending_2, pending_1, pending_1_1, _t, event_1, sessionId, messenger, needsUrlTracking;
330
- var e_2, _u;
394
+ var now, _r, _s, joinedConfig, localConfig, remoteConfig, scrollWatcher, managers, storeType, compressionWorkerScript, trackDestinationWorkerScript, globalScope, _t, compressionScript, trackDestinationScript, rrwebEventManager, error_1, typedError, payloadBatcher, interactionEventManager, error_2, typedError, onFullSnapshotProcessed, pending_2, pending_1, pending_1_1, _u, event_1, sessionId, messenger, needsUrlTracking;
395
+ var e_2, _v;
331
396
  var _this = this;
332
- return tslib_1.__generator(this, function (_v) {
333
- switch (_v.label) {
397
+ return tslib_1.__generator(this, function (_w) {
398
+ switch (_w.label) {
334
399
  case 0:
335
400
  // Re-init should always tear down any previous URL-change subscription, even when the
336
401
  // next config has no targeting config and we don't subscribe again.
@@ -346,13 +411,13 @@ var SessionReplay = /** @class */ (function () {
346
411
  options.sessionId !== undefined
347
412
  ? (_b = (0, replay_start_time_store_1.getOrInitReplayStartTime)(apiKey, options.sessionId, now, this.loggerProvider)) !== null && _b !== void 0 ? _b : now
348
413
  : now;
349
- _q = this;
414
+ _r = this;
350
415
  return [4 /*yield*/, (0, joined_config_1.createSessionReplayJoinedConfigGenerator)(apiKey, options)];
351
416
  case 1:
352
- _q.joinedConfigGenerator = _v.sent();
417
+ _r.joinedConfigGenerator = _w.sent();
353
418
  return [4 /*yield*/, this.joinedConfigGenerator.generateJoinedConfig()];
354
419
  case 2:
355
- _r = _v.sent(), joinedConfig = _r.joinedConfig, localConfig = _r.localConfig, remoteConfig = _r.remoteConfig;
420
+ _s = _w.sent(), joinedConfig = _s.joinedConfig, localConfig = _s.localConfig, remoteConfig = _s.remoteConfig;
356
421
  this.config = joinedConfig;
357
422
  this.setMetadata(options.sessionId, joinedConfig, localConfig, remoteConfig, (_c = options.version) === null || _c === void 0 ? void 0 : _c.version, version_1.VERSION, (_d = options.version) === null || _d === void 0 ? void 0 : _d.type);
358
423
  this.pageLeaveFns = [];
@@ -376,12 +441,12 @@ var SessionReplay = /** @class */ (function () {
376
441
  if (!(this.config.useWebWorker && globalScope && globalScope.Worker)) return [3 /*break*/, 4];
377
442
  return [4 /*yield*/, Promise.resolve().then(function () { return tslib_1.__importStar(require('./worker')); })];
378
443
  case 3:
379
- _s = _v.sent(), compressionScript = _s.compressionScript, trackDestinationScript = _s.trackDestinationScript;
444
+ _t = _w.sent(), compressionScript = _t.compressionScript, trackDestinationScript = _t.trackDestinationScript;
380
445
  compressionWorkerScript = compressionScript;
381
446
  trackDestinationWorkerScript = trackDestinationScript;
382
- _v.label = 4;
447
+ _w.label = 4;
383
448
  case 4:
384
- _v.trys.push([4, 6, , 7]);
449
+ _w.trys.push([4, 6, , 7]);
385
450
  return [4 /*yield*/, (0, events_manager_1.createEventsManager)({
386
451
  config: this.config,
387
452
  type: 'replay',
@@ -393,21 +458,21 @@ var SessionReplay = /** @class */ (function () {
393
458
  shouldSend: function () { return !_this.isBelowMinSessionDuration(); },
394
459
  })];
395
460
  case 5:
396
- rrwebEventManager = _v.sent();
461
+ rrwebEventManager = _w.sent();
397
462
  this.rrwebEventManager = rrwebEventManager;
398
463
  managers.push({ name: 'replay', manager: rrwebEventManager });
399
464
  return [3 /*break*/, 7];
400
465
  case 6:
401
- error_1 = _v.sent();
466
+ error_1 = _w.sent();
402
467
  typedError = error_1;
403
468
  this.loggerProvider.warn("Error occurred while creating replay events manager: ".concat(typedError.toString()));
404
469
  return [3 /*break*/, 7];
405
470
  case 7:
406
471
  if (!((_j = this.config.interactionConfig) === null || _j === void 0 ? void 0 : _j.enabled)) return [3 /*break*/, 11];
407
472
  payloadBatcher = this.config.interactionConfig.batch ? click_1.clickBatcher : click_1.clickNonBatcher;
408
- _v.label = 8;
473
+ _w.label = 8;
409
474
  case 8:
410
- _v.trys.push([8, 10, , 11]);
475
+ _w.trys.push([8, 10, , 11]);
411
476
  return [4 /*yield*/, (0, events_manager_1.createEventsManager)({
412
477
  config: this.config,
413
478
  type: 'interaction',
@@ -419,11 +484,11 @@ var SessionReplay = /** @class */ (function () {
419
484
  trackDestinationWorkerScript: trackDestinationWorkerScript,
420
485
  })];
421
486
  case 9:
422
- interactionEventManager = _v.sent();
487
+ interactionEventManager = _w.sent();
423
488
  managers.push({ name: 'interaction', manager: interactionEventManager });
424
489
  return [3 /*break*/, 11];
425
490
  case 10:
426
- error_2 = _v.sent();
491
+ error_2 = _w.sent();
427
492
  typedError = error_2;
428
493
  this.loggerProvider.warn("Error occurred while creating interaction events manager: ".concat(typedError.toString()));
429
494
  return [3 /*break*/, 11];
@@ -441,14 +506,14 @@ var SessionReplay = /** @class */ (function () {
441
506
  pending_2 = this.pendingEmitEvents.splice(0);
442
507
  try {
443
508
  for (pending_1 = tslib_1.__values(pending_2), pending_1_1 = pending_1.next(); !pending_1_1.done; pending_1_1 = pending_1.next()) {
444
- _t = pending_1_1.value, event_1 = _t.event, sessionId = _t.sessionId;
509
+ _u = pending_1_1.value, event_1 = _u.event, sessionId = _u.sessionId;
445
510
  this.eventCompressor.enqueueEvent(event_1, sessionId);
446
511
  }
447
512
  }
448
513
  catch (e_2_1) { e_2 = { error: e_2_1 }; }
449
514
  finally {
450
515
  try {
451
- if (pending_1_1 && !pending_1_1.done && (_u = pending_1.return)) _u.call(pending_1);
516
+ if (pending_1_1 && !pending_1_1.done && (_v = pending_1.return)) _v.call(pending_1);
452
517
  }
453
518
  finally { if (e_2) throw e_2.error; }
454
519
  }
@@ -485,7 +550,7 @@ var SessionReplay = /** @class */ (function () {
485
550
  ], false);
486
551
  return [4 /*yield*/, this.initializeNetworkObservers()];
487
552
  case 12:
488
- _v.sent();
553
+ _w.sent();
489
554
  // Enable background capture when this page is opened by the Amplitude app
490
555
  // (window.opener exists). Uses the shared messenger singleton so that if
491
556
  // autocapture is also loaded, both share a single messenger and script load.
@@ -496,10 +561,22 @@ var SessionReplay = /** @class */ (function () {
496
561
  }
497
562
  this.loggerProvider.log('Installing @amplitude/session-replay-browser.');
498
563
  this.teardownEventListeners(false);
564
+ // Q1 "did init happen?": its presence in DataDog proves init() ran to completion for this
565
+ // session; the props show whether the prerequisites (session/device id, config) were present.
566
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.init);
567
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.init, {
568
+ // sessionId is always stamped by recordDiagnosticEvent, so no separate hasSessionId here.
569
+ hasDeviceId: !!this.getDeviceId(),
570
+ captureEnabled: this.config.captureEnabled,
571
+ hasTargetingConfig: !!this.config.targetingConfig,
572
+ sampleRate: this.config.sampleRate,
573
+ optOut: this.shouldOptOut(),
574
+ currentUrl: (_m = this.getCurrentPageForTargeting()) === null || _m === void 0 ? void 0 : _m.url,
575
+ });
499
576
  return [4 /*yield*/, this.evaluateTargetingAndCapture({ userProperties: options.userProperties, page: this.getCurrentPageForTargeting() }, true)];
500
577
  case 13:
501
- _v.sent();
502
- needsUrlTracking = this.config.targetingConfig || ((_p = (_o = (_m = this.config.privacyConfig) === null || _m === void 0 ? void 0 : _m.urlMaskLevels) === null || _o === void 0 ? void 0 : _o.length) !== null && _p !== void 0 ? _p : 0) > 0;
578
+ _w.sent();
579
+ needsUrlTracking = this.config.targetingConfig || ((_q = (_p = (_o = this.config.privacyConfig) === null || _o === void 0 ? void 0 : _o.urlMaskLevels) === null || _p === void 0 ? void 0 : _p.length) !== null && _q !== void 0 ? _q : 0) > 0;
503
580
  if (needsUrlTracking) {
504
581
  this.setupUrlChangeListener();
505
582
  }
@@ -512,17 +589,29 @@ var SessionReplay = /** @class */ (function () {
512
589
  return (0, analytics_core_1.returnWrapper)(this.asyncSetSessionId(sessionId, deviceId));
513
590
  };
514
591
  SessionReplay.prototype.asyncSetSessionId = function (sessionId, deviceId, options) {
515
- var _a, _b, _c;
592
+ var _a, _b, _c, _d;
516
593
  return tslib_1.__awaiter(this, void 0, void 0, function () {
517
- var previousSessionId, isSessionChange, deviceIdForReplayId, joinedConfig;
518
- return tslib_1.__generator(this, function (_d) {
519
- switch (_d.label) {
594
+ var previousSessionId, currentDeviceId, isSessionChange, deviceIdForReplayId, joinedConfig;
595
+ return tslib_1.__generator(this, function (_e) {
596
+ switch (_e.label) {
520
597
  case 0:
598
+ previousSessionId = (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId;
599
+ currentDeviceId = this.getDeviceId();
600
+ // Standalone SDK callers may poll setSessionId with a stable bucket id (e.g. hour-aligned
601
+ // timestamps) and only need a no-op when the bucket hasn't rolled. Without this guard,
602
+ // the rest of asyncSetSessionId still runs: sendEvents, targeting reset, config refetch,
603
+ // and recordEvents (stop + restart rrweb). Proceed when deviceId changes or
604
+ // non-empty userProperties are passed so targeting can re-evaluate on Identify.
605
+ if (previousSessionId !== undefined &&
606
+ previousSessionId === sessionId &&
607
+ (deviceId === undefined || deviceId === currentDeviceId) &&
608
+ ((options === null || options === void 0 ? void 0 : options.userProperties) === undefined || Object.keys(options.userProperties).length === 0)) {
609
+ return [2 /*return*/];
610
+ }
521
611
  // Invalidate any in-flight URL-change re-evaluations from the previous session.
522
612
  this.latestUrlChangeTargetingEvaluationId++;
523
613
  this.sessionTargetingMatch = false;
524
614
  this.lastShouldRecordDecision = undefined; // Reset targeting decision for new session
525
- previousSessionId = this.identifiers && this.identifiers.sessionId;
526
615
  if (previousSessionId) {
527
616
  this.sendEvents(previousSessionId);
528
617
  }
@@ -533,7 +622,7 @@ var SessionReplay = /** @class */ (function () {
533
622
  // would compute the wrong elapsed duration. Skip on a redundant same-sessionId call —
534
623
  // the buffer belongs to the *continuing* session and should ship via beacon as normal.
535
624
  if (isSessionChange) {
536
- (_a = this.rrwebEventManager) === null || _a === void 0 ? void 0 : _a.dropPendingBeaconEvents();
625
+ (_b = this.rrwebEventManager) === null || _b === void 0 ? void 0 : _b.dropPendingBeaconEvents();
537
626
  }
538
627
  deviceIdForReplayId = deviceId || this.getDeviceId();
539
628
  this.identifiers = new identifiers_1.SessionIdentifiers({
@@ -548,7 +637,7 @@ var SessionReplay = /** @class */ (function () {
548
637
  this.sessionStartTime = Date.now();
549
638
  this.suppressedSendCount = 0;
550
639
  this.hasEmittedGateDecision = false;
551
- if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.apiKey) {
640
+ if ((_c = this.config) === null || _c === void 0 ? void 0 : _c.apiKey) {
552
641
  (0, replay_start_time_store_1.setReplayStartTime)(this.config.apiKey, sessionId, this.sessionStartTime, this.loggerProvider);
553
642
  if (previousSessionId !== undefined) {
554
643
  (0, replay_start_time_store_1.removeReplayStartTime)(this.config.apiKey, previousSessionId, this.loggerProvider);
@@ -558,19 +647,19 @@ var SessionReplay = /** @class */ (function () {
558
647
  if (!(this.joinedConfigGenerator && previousSessionId)) return [3 /*break*/, 2];
559
648
  return [4 /*yield*/, this.joinedConfigGenerator.generateJoinedConfig()];
560
649
  case 1:
561
- joinedConfig = (_d.sent()).joinedConfig;
650
+ joinedConfig = (_e.sent()).joinedConfig;
562
651
  this.config = joinedConfig;
563
- _d.label = 2;
652
+ _e.label = 2;
564
653
  case 2:
565
- if (!((_c = this.config) === null || _c === void 0 ? void 0 : _c.targetingConfig)) return [3 /*break*/, 4];
654
+ if (!((_d = this.config) === null || _d === void 0 ? void 0 : _d.targetingConfig)) return [3 /*break*/, 4];
566
655
  return [4 /*yield*/, this.evaluateTargetingAndCapture({ userProperties: options === null || options === void 0 ? void 0 : options.userProperties, page: this.getCurrentPageForTargeting() }, false, true)];
567
656
  case 3:
568
- _d.sent();
657
+ _e.sent();
569
658
  return [3 /*break*/, 6];
570
659
  case 4: return [4 /*yield*/, this.recordEvents()];
571
660
  case 5:
572
- _d.sent();
573
- _d.label = 6;
661
+ _e.sent();
662
+ _e.label = 6;
574
663
  case 6: return [2 /*return*/];
575
664
  }
576
665
  });
@@ -623,6 +712,9 @@ var SessionReplay = /** @class */ (function () {
623
712
  // Cross-session leak is prevented in asyncSetSessionId, which drops the buffer
624
713
  // at the session transition. The page-leave path also gates independently.
625
714
  this.suppressedSendCount++;
715
+ // gap #1: recording but events held back by min_session_duration — another "recording yet
716
+ // no replay in Amplitude" cause.
717
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.sendSuppressedMinDuration);
626
718
  return;
627
719
  }
628
720
  // On the first send-after-pass for the session, emit a custom rrweb event so the
@@ -679,17 +771,107 @@ var SessionReplay = /** @class */ (function () {
679
771
  }
680
772
  return identityStoreOptOut !== undefined ? identityStoreOptOut : (_b = this.config) === null || _b === void 0 ? void 0 : _b.optOut;
681
773
  };
774
+ /**
775
+ * Increment a diagnostics counter so the team can see the distribution of recording decisions
776
+ * across a customer's sessions (e.g. lots of sr.gate.trc_no_match => rule isn't matching).
777
+ * Ships via the analytics SDK's DiagnosticsClient. No-op when there's no client or diagnostics
778
+ * isn't sampled in; never throws — diagnostics must never interfere with recording.
779
+ */
780
+ SessionReplay.prototype.incrementDiagnostic = function (counter) {
781
+ var _a, _b;
782
+ try {
783
+ (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.diagnosticsClient) === null || _b === void 0 ? void 0 : _b.increment(counter);
784
+ }
785
+ catch (_c) {
786
+ // swallow — diagnostics is best-effort
787
+ }
788
+ };
789
+ /**
790
+ * Record a diagnostics histogram value (min/max/avg/count in DataDog). Same best-effort, never-
791
+ * throws contract as incrementDiagnostic. Use for latencies/sizes, not per-call counts.
792
+ */
793
+ SessionReplay.prototype.recordDiagnosticHistogram = function (name, value) {
794
+ var _a, _b;
795
+ try {
796
+ // The only caller runs inside evaluateTargetingAndCapture's `this.config` guard, so the
797
+ // `config == null` arm of this optional chain is unreachable here (kept as defensive parity
798
+ // with recordDiagnosticEvent). The diagnosticsClient-absent arm IS exercised by no-client tests.
799
+ /* istanbul ignore next */
800
+ (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.diagnosticsClient) === null || _b === void 0 ? void 0 : _b.recordHistogram(name, value);
801
+ }
802
+ catch (_c) {
803
+ // swallow — diagnostics is best-effort
804
+ }
805
+ };
806
+ /**
807
+ * Record a diagnostics EVENT with properties. Events are forwarded to DataDog Logs, so the
808
+ * properties become queryable fields (e.g. @matched, @pageUrl). Use sparingly vs counters: the
809
+ * client caps in-memory events (~10 per save interval), so this is for context-rich signals at
810
+ * meaningful decision points, not per-call tallies. Best-effort, never throws.
811
+ */
812
+ SessionReplay.prototype.recordDiagnosticEvent = function (name, properties) {
813
+ var _a, _b, _c;
814
+ try {
815
+ var sessionId = (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId;
816
+ var deviceId = this.getDeviceId();
817
+ // Always stamp sessionId, deviceId and srId so every diagnostics event (→ DataDog Logs) can
818
+ // be correlated to a single session/device — and to the actual replay, since the Session
819
+ // Replay ID is `${deviceId}/${sessionId}`. Caller props override if they pass their own.
820
+ (_c = (_b = this.config) === null || _b === void 0 ? void 0 : _b.diagnosticsClient) === null || _c === void 0 ? void 0 : _c.recordEvent(name, tslib_1.__assign({ sessionId: sessionId, deviceId: deviceId, srId: deviceId != null && sessionId != null ? "".concat(deviceId, "/").concat(sessionId) : undefined }, properties));
821
+ }
822
+ catch (_d) {
823
+ // swallow — diagnostics is best-effort
824
+ }
825
+ };
826
+ /**
827
+ * Emit a single rich TRC decision snapshot per session to diagnostics — surfaced even when
828
+ * nothing records (the exact case customers can't reproduce locally).
829
+ */
830
+ SessionReplay.prototype.recordTrcDecisionDiagnostic = function (shouldRecord) {
831
+ var _a, _b;
832
+ var config = this.config;
833
+ // Every caller is past getShouldRecord's `!this.identifiers`/`!this.config` guard, so both are
834
+ // defined here; the `?.` arms below are unreachable defensive narrowing and excluded from
835
+ // coverage. The reachable guards (no diagnosticsClient, undefined/duplicate sessionId) are tested.
836
+ /* istanbul ignore next */
837
+ if (!config) {
838
+ return;
839
+ }
840
+ /* istanbul ignore next */
841
+ var sessionId = (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId;
842
+ // Once past this guard, config.diagnosticsClient is truthy, so the fields below read config
843
+ // directly rather than re-asserting with `?.` on every access.
844
+ if (!config.diagnosticsClient || sessionId === undefined || sessionId === this.trcDiagnosticSessionId) {
845
+ return;
846
+ }
847
+ this.trcDiagnosticSessionId = sessionId;
848
+ // Goes through recordDiagnosticEvent so it carries sessionId + deviceId like every other event.
849
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.decision, {
850
+ shouldRecord: shouldRecord,
851
+ captureEnabled: config.captureEnabled,
852
+ hasTargetingConfig: !!config.targetingConfig,
853
+ sessionTargetingMatch: this.sessionTargetingMatch,
854
+ sampleRate: config.sampleRate,
855
+ pageUrl: (_b = this.getCurrentPageForTargeting()) === null || _b === void 0 ? void 0 : _b.url,
856
+ sdkVersion: version_1.VERSION,
857
+ });
858
+ };
682
859
  SessionReplay.prototype.getShouldRecord = function () {
683
860
  if (!this.identifiers || !this.config || !this.identifiers.sessionId) {
684
861
  this.loggerProvider.warn("Session is not being recorded due to lack of config, please call sessionReplay.init.");
862
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateNoIdentifiers);
685
863
  return false;
686
864
  }
687
865
  if (!this.config.captureEnabled) {
688
866
  this.loggerProvider.log("Session ".concat(this.identifiers.sessionId, " not being captured due to capture being disabled for project or because the remote config could not be fetched."));
867
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateCaptureDisabled);
868
+ this.recordTrcDecisionDiagnostic(false);
689
869
  return false;
690
870
  }
691
871
  if (this.shouldOptOut()) {
692
872
  this.loggerProvider.log("Opting session ".concat(this.identifiers.sessionId, " out of recording due to optOut config."));
873
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateOptOut);
874
+ this.recordTrcDecisionDiagnostic(false);
693
875
  return false;
694
876
  }
695
877
  var shouldRecord = false;
@@ -703,12 +885,14 @@ var SessionReplay = /** @class */ (function () {
703
885
  this.loggerProvider.log(message);
704
886
  shouldRecord = false;
705
887
  matched = false;
888
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateTrcNoMatch);
706
889
  }
707
890
  else {
708
891
  message = "Capturing replays for session ".concat(this.identifiers.sessionId, " due to matching targeting conditions.");
709
892
  this.loggerProvider.log(message);
710
893
  shouldRecord = true;
711
894
  matched = true;
895
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateTrcMatch);
712
896
  }
713
897
  }
714
898
  else {
@@ -718,10 +902,12 @@ var SessionReplay = /** @class */ (function () {
718
902
  this.loggerProvider.log(message);
719
903
  shouldRecord = false;
720
904
  matched = false;
905
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateSampleOut);
721
906
  }
722
907
  else {
723
908
  shouldRecord = true;
724
909
  matched = true;
910
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.gateSampleIn);
725
911
  }
726
912
  }
727
913
  // Only send custom rrweb event for targeting decision when the decision changes
@@ -734,6 +920,7 @@ var SessionReplay = /** @class */ (function () {
734
920
  });
735
921
  this.lastShouldRecordDecision = shouldRecord;
736
922
  }
923
+ this.recordTrcDecisionDiagnostic(shouldRecord);
737
924
  return shouldRecord;
738
925
  };
739
926
  SessionReplay.prototype.getBlockSelectors = function () {
@@ -898,6 +1085,9 @@ var SessionReplay = /** @class */ (function () {
898
1085
  recordFunction = _o.sent();
899
1086
  // May be undefined if cannot import rrweb-record
900
1087
  if (!recordFunction) {
1088
+ // gap #1: gate said record, but rrweb couldn't load → no replay despite shouldRecord=true.
1089
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.recordNoRecordFn);
1090
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.recordNoRecordFn, {});
901
1091
  return [2 /*return*/];
902
1092
  }
903
1093
  return [4 /*yield*/, this.initializeNetworkObservers()];
@@ -928,6 +1118,11 @@ var SessionReplay = /** @class */ (function () {
928
1118
  : {};
929
1119
  ugcFilterRules = (interactionConfig === null || interactionConfig === void 0 ? void 0 : interactionConfig.enabled) && interactionConfig.ugcFilterRules ? interactionConfig.ugcFilterRules : [];
930
1120
  this.loggerProvider.log("Session Replay capture beginning for ".concat(sessionId, "."));
1121
+ // gap #1: rrweb is actually starting. Closes the "gate said yes but no replay" blind spot,
1122
+ // and its srId is the id the replay uploads under (compare with analytics events to catch the
1123
+ // device-id mismatch that breaks stitching).
1124
+ this.incrementDiagnostic(diagnostics_1.SrDiagnostic.recordStarted);
1125
+ this.recordDiagnosticEvent(diagnostics_1.SrDiagnostic.recordStarted, {});
931
1126
  _o.label = 3;
932
1127
  case 3:
933
1128
  _o.trys.push([3, 5, , 6]);