@amplitude/session-replay-browser 1.46.0 → 1.47.0-sr-trc-debug-log.1

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