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

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 (43) hide show
  1. package/lib/cjs/diagnostics.d.ts +7 -0
  2. package/lib/cjs/diagnostics.d.ts.map +1 -1
  3. package/lib/cjs/diagnostics.js +14 -1
  4. package/lib/cjs/diagnostics.js.map +1 -1
  5. package/lib/cjs/plugins/url-tracking-plugin.d.ts +13 -0
  6. package/lib/cjs/plugins/url-tracking-plugin.d.ts.map +1 -1
  7. package/lib/cjs/plugins/url-tracking-plugin.js +6 -2
  8. package/lib/cjs/plugins/url-tracking-plugin.js.map +1 -1
  9. package/lib/cjs/session-replay.d.ts +8 -0
  10. package/lib/cjs/session-replay.d.ts.map +1 -1
  11. package/lib/cjs/session-replay.js +132 -24
  12. package/lib/cjs/session-replay.js.map +1 -1
  13. package/lib/cjs/targeting/targeting-manager.d.ts.map +1 -1
  14. package/lib/cjs/targeting/targeting-manager.js +26 -17
  15. package/lib/cjs/targeting/targeting-manager.js.map +1 -1
  16. package/lib/cjs/version.d.ts +1 -1
  17. package/lib/cjs/version.js +1 -1
  18. package/lib/cjs/version.js.map +1 -1
  19. package/lib/esm/diagnostics.d.ts +7 -0
  20. package/lib/esm/diagnostics.d.ts.map +1 -1
  21. package/lib/esm/diagnostics.js +14 -1
  22. package/lib/esm/diagnostics.js.map +1 -1
  23. package/lib/esm/plugins/url-tracking-plugin.d.ts +13 -0
  24. package/lib/esm/plugins/url-tracking-plugin.d.ts.map +1 -1
  25. package/lib/esm/plugins/url-tracking-plugin.js +6 -2
  26. package/lib/esm/plugins/url-tracking-plugin.js.map +1 -1
  27. package/lib/esm/session-replay.d.ts +8 -0
  28. package/lib/esm/session-replay.d.ts.map +1 -1
  29. package/lib/esm/session-replay.js +132 -24
  30. package/lib/esm/session-replay.js.map +1 -1
  31. package/lib/esm/targeting/targeting-manager.d.ts.map +1 -1
  32. package/lib/esm/targeting/targeting-manager.js +26 -17
  33. package/lib/esm/targeting/targeting-manager.js.map +1 -1
  34. package/lib/esm/version.d.ts +1 -1
  35. package/lib/esm/version.js +1 -1
  36. package/lib/esm/version.js.map +1 -1
  37. package/lib/scripts/index-min.js +1 -1
  38. package/lib/scripts/index-min.js.gz +0 -0
  39. package/lib/scripts/index-min.js.map +1 -1
  40. package/lib/scripts/session-replay-browser-min.js +1 -1
  41. package/lib/scripts/session-replay-browser-min.js.gz +0 -0
  42. package/lib/scripts/session-replay-browser-min.js.map +1 -1
  43. package/package.json +2 -2
@@ -52,6 +52,9 @@ var SessionReplay = /** @class */ (function () {
52
52
  this.recordEventsPendingShouldLogMetadata = null;
53
53
  /** Cleanup for URL change listener used to re-evaluate targeting on SPA route changes */
54
54
  this.urlChangeCleanup = null;
55
+ // Ensures the url_poll.first_tick diagnostic event is emitted at most once per listener setup
56
+ // (the per-tick counter still increments every tick; only the rich event is one-shot).
57
+ this.urlPollFirstTickRecorded = false;
55
58
  this.crossOriginIframeCoordinator = null;
56
59
  this.crossOriginParentSignalCleanup = null;
57
60
  /** Monotonic counter to ignore stale URL-change targeting results */
@@ -169,6 +172,17 @@ var SessionReplay = /** @class */ (function () {
169
172
  }
170
173
  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
174
  pageForTargeting = (_g = targetingParams.page) !== null && _g !== void 0 ? _g : (pageUrl !== '' ? { url: pageUrl } : undefined);
175
+ // Record the targeting trigger event
176
+ this.recordDiagnosticEvent(SrDiagnostic.targetingTrigger, {
177
+ sessionId: this.identifiers.sessionId,
178
+ deviceId: this.getDeviceId(),
179
+ targetingConfig: this.config.targetingConfig,
180
+ targetingParams: {
181
+ userProperties: targetingParams.userProperties,
182
+ event: eventForTargeting,
183
+ page: pageForTargeting,
184
+ },
185
+ });
172
186
  evalStart = Date.now();
173
187
  return [4 /*yield*/, evaluateTargetingAndStore({
174
188
  sessionId: this.identifiers.sessionId,
@@ -326,12 +340,17 @@ var SessionReplay = /** @class */ (function () {
326
340
  */
327
341
  SessionReplay.prototype.setupUrlChangeListener = function () {
328
342
  var _this = this;
329
- var _a, _b;
343
+ var _a, _b, _c, _d, _e, _f;
330
344
  // If init() runs multiple times, remove the previous URL-change subscription first
331
345
  // so we don't leak callbacks and trigger duplicate targeting evaluations.
332
346
  (_a = this.urlChangeCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
333
347
  var globalScope = getGlobalScope();
334
348
  if (!(globalScope === null || globalScope === void 0 ? void 0 : globalScope.location)) {
349
+ // No window/location (SSR, worker, or pre-render) — the listener can't attach, so TRC will
350
+ // never re-evaluate on navigation. Surface it rather than failing silently.
351
+ this.incrementDiagnostic(SrDiagnostic.urlListenerSkipped);
352
+ this.recordDiagnosticEvent(SrDiagnostic.urlListenerSkipped, { reason: 'no_global_scope' });
353
+ this.loggerProvider.debug('URL-change listener not attached: no global scope/location.');
335
354
  return;
336
355
  }
337
356
  var hasTargeting = !!((_b = this.config) === null || _b === void 0 ? void 0 : _b.targetingConfig);
@@ -356,7 +375,39 @@ var SessionReplay = /** @class */ (function () {
356
375
  _this.loggerProvider.debug("Queued URL-change targeting re-evaluation #".concat(evaluationId, " for ").concat(href, "."));
357
376
  }
358
377
  };
359
- var unsubscribe = subscribeToUrlChanges(globalScope, onUrlChange);
378
+ // Pass the polling options so targeting re-evaluation also respects `enableUrlChangePolling`.
379
+ // Without this, the targeting listener only sees history.pushState/replaceState + popstate +
380
+ // hashchange — so SPA navigations that bypass the history API never re-evaluate TRC and
381
+ // recording never starts on the new URL (enableUrlChangePolling previously only affected the
382
+ // rrweb URL-tracking plugin, which runs only once recording is already active).
383
+ var enablePolling = (_d = (_c = this.config) === null || _c === void 0 ? void 0 : _c.enableUrlChangePolling) !== null && _d !== void 0 ? _d : false;
384
+ this.urlPollFirstTickRecorded = false;
385
+ var unsubscribe = subscribeToUrlChanges(globalScope, onUrlChange, {
386
+ enablePolling: enablePolling,
387
+ pollingInterval: (_e = this.config) === null || _e === void 0 ? void 0 : _e.urlChangePollingInterval,
388
+ // Mirror each poll tick to the console (Debug level) so we can confirm polling is firing.
389
+ log: this.loggerProvider.debug.bind(this.loggerProvider),
390
+ // Prove in DataDog that polling actually FIRES (not just that it was scheduled). Per-tick is a
391
+ // cheap aggregated counter; the rich event is emitted once (with href) to avoid flooding the
392
+ // diagnostics endpoint with one capture POST per second.
393
+ onPoll: function (href, changed) {
394
+ _this.incrementDiagnostic(SrDiagnostic.urlPollTick);
395
+ if (!_this.urlPollFirstTickRecorded) {
396
+ _this.urlPollFirstTickRecorded = true;
397
+ // pollingInterval is already on url_listener.attached, so it's omitted here.
398
+ _this.recordDiagnosticEvent(SrDiagnostic.urlPollFirstTick, { href: href, changed: changed });
399
+ }
400
+ },
401
+ });
402
+ // Confirm the listener is actually live, and under what settings — if recording never starts on
403
+ // navigation, this tells us whether the listener existed and whether polling (the fallback for
404
+ // SPAs that bypass the History API) was on.
405
+ this.recordDiagnosticEvent(SrDiagnostic.urlListenerAttached, {
406
+ hasTargeting: hasTargeting,
407
+ enablePolling: enablePolling,
408
+ pollingInterval: (_f = this.config) === null || _f === void 0 ? void 0 : _f.urlChangePollingInterval,
409
+ });
410
+ this.loggerProvider.debug("URL-change listener attached (polling: ".concat(String(enablePolling), ")."));
360
411
  this.urlChangeCleanup = function () {
361
412
  unsubscribe();
362
413
  _this.urlChangeCleanup = null;
@@ -385,14 +436,34 @@ var SessionReplay = /** @class */ (function () {
385
436
  var currentUrl = (_b = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.location) === null || _b === void 0 ? void 0 : _b.href;
386
437
  return currentUrl != null ? { url: currentUrl } : undefined;
387
438
  };
439
+ /**
440
+ * Best-effort navigation type from the Navigation Timing API: 'reload' | 'navigate' |
441
+ * 'back_forward' | 'prerender'. Surfaced in the init diagnostic so a page refresh ('reload')
442
+ * is distinguishable from a fresh load ('navigate') — neither changes the session id, so this
443
+ * is the only way to tell them apart. Returns undefined when the API is unavailable.
444
+ */
445
+ SessionReplay.prototype.getNavigationType = function () {
446
+ try {
447
+ var globalScope = getGlobalScope();
448
+ var performance_1 = globalScope && globalScope.performance;
449
+ if (!performance_1 || typeof performance_1.getEntriesByType !== 'function') {
450
+ return undefined;
451
+ }
452
+ var navEntries = performance_1.getEntriesByType('navigation');
453
+ return navEntries.length > 0 ? navEntries[0].type : undefined;
454
+ }
455
+ catch (_a) {
456
+ return undefined;
457
+ }
458
+ };
388
459
  SessionReplay.prototype._init = function (apiKey, options) {
389
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
460
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
390
461
  return __awaiter(this, void 0, void 0, function () {
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;
462
+ var now, _u, _v, joinedConfig, localConfig, remoteConfig, scrollWatcher, managers, storeType, compressionWorkerScript, trackDestinationWorkerScript, globalScope, _w, compressionScript, trackDestinationScript, rrwebEventManager, error_1, typedError, payloadBatcher, interactionEventManager, error_2, typedError, onFullSnapshotProcessed, pending_2, pending_1, pending_1_1, _x, event_1, sessionId, messenger, needsUrlTracking;
463
+ var e_2, _y;
393
464
  var _this = this;
394
- return __generator(this, function (_w) {
395
- switch (_w.label) {
465
+ return __generator(this, function (_z) {
466
+ switch (_z.label) {
396
467
  case 0:
397
468
  // Re-init should always tear down any previous URL-change subscription, even when the
398
469
  // next config has no targeting config and we don't subscribe again.
@@ -408,13 +479,13 @@ var SessionReplay = /** @class */ (function () {
408
479
  options.sessionId !== undefined
409
480
  ? (_b = getOrInitReplayStartTime(apiKey, options.sessionId, now, this.loggerProvider)) !== null && _b !== void 0 ? _b : now
410
481
  : now;
411
- _r = this;
482
+ _u = this;
412
483
  return [4 /*yield*/, createSessionReplayJoinedConfigGenerator(apiKey, options)];
413
484
  case 1:
414
- _r.joinedConfigGenerator = _w.sent();
485
+ _u.joinedConfigGenerator = _z.sent();
415
486
  return [4 /*yield*/, this.joinedConfigGenerator.generateJoinedConfig()];
416
487
  case 2:
417
- _s = _w.sent(), joinedConfig = _s.joinedConfig, localConfig = _s.localConfig, remoteConfig = _s.remoteConfig;
488
+ _v = _z.sent(), joinedConfig = _v.joinedConfig, localConfig = _v.localConfig, remoteConfig = _v.remoteConfig;
418
489
  this.config = joinedConfig;
419
490
  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);
420
491
  this.pageLeaveFns = [];
@@ -438,12 +509,12 @@ var SessionReplay = /** @class */ (function () {
438
509
  if (!(this.config.useWebWorker && globalScope && globalScope.Worker)) return [3 /*break*/, 4];
439
510
  return [4 /*yield*/, import('./worker')];
440
511
  case 3:
441
- _t = _w.sent(), compressionScript = _t.compressionScript, trackDestinationScript = _t.trackDestinationScript;
512
+ _w = _z.sent(), compressionScript = _w.compressionScript, trackDestinationScript = _w.trackDestinationScript;
442
513
  compressionWorkerScript = compressionScript;
443
514
  trackDestinationWorkerScript = trackDestinationScript;
444
- _w.label = 4;
515
+ _z.label = 4;
445
516
  case 4:
446
- _w.trys.push([4, 6, , 7]);
517
+ _z.trys.push([4, 6, , 7]);
447
518
  return [4 /*yield*/, createEventsManager({
448
519
  config: this.config,
449
520
  type: 'replay',
@@ -455,21 +526,21 @@ var SessionReplay = /** @class */ (function () {
455
526
  shouldSend: function () { return !_this.isBelowMinSessionDuration(); },
456
527
  })];
457
528
  case 5:
458
- rrwebEventManager = _w.sent();
529
+ rrwebEventManager = _z.sent();
459
530
  this.rrwebEventManager = rrwebEventManager;
460
531
  managers.push({ name: 'replay', manager: rrwebEventManager });
461
532
  return [3 /*break*/, 7];
462
533
  case 6:
463
- error_1 = _w.sent();
534
+ error_1 = _z.sent();
464
535
  typedError = error_1;
465
536
  this.loggerProvider.warn("Error occurred while creating replay events manager: ".concat(typedError.toString()));
466
537
  return [3 /*break*/, 7];
467
538
  case 7:
468
539
  if (!((_j = this.config.interactionConfig) === null || _j === void 0 ? void 0 : _j.enabled)) return [3 /*break*/, 11];
469
540
  payloadBatcher = this.config.interactionConfig.batch ? clickBatcher : clickNonBatcher;
470
- _w.label = 8;
541
+ _z.label = 8;
471
542
  case 8:
472
- _w.trys.push([8, 10, , 11]);
543
+ _z.trys.push([8, 10, , 11]);
473
544
  return [4 /*yield*/, createEventsManager({
474
545
  config: this.config,
475
546
  type: 'interaction',
@@ -481,11 +552,11 @@ var SessionReplay = /** @class */ (function () {
481
552
  trackDestinationWorkerScript: trackDestinationWorkerScript,
482
553
  })];
483
554
  case 9:
484
- interactionEventManager = _w.sent();
555
+ interactionEventManager = _z.sent();
485
556
  managers.push({ name: 'interaction', manager: interactionEventManager });
486
557
  return [3 /*break*/, 11];
487
558
  case 10:
488
- error_2 = _w.sent();
559
+ error_2 = _z.sent();
489
560
  typedError = error_2;
490
561
  this.loggerProvider.warn("Error occurred while creating interaction events manager: ".concat(typedError.toString()));
491
562
  return [3 /*break*/, 11];
@@ -503,14 +574,14 @@ var SessionReplay = /** @class */ (function () {
503
574
  pending_2 = this.pendingEmitEvents.splice(0);
504
575
  try {
505
576
  for (pending_1 = __values(pending_2), pending_1_1 = pending_1.next(); !pending_1_1.done; pending_1_1 = pending_1.next()) {
506
- _u = pending_1_1.value, event_1 = _u.event, sessionId = _u.sessionId;
577
+ _x = pending_1_1.value, event_1 = _x.event, sessionId = _x.sessionId;
507
578
  this.eventCompressor.enqueueEvent(event_1, sessionId);
508
579
  }
509
580
  }
510
581
  catch (e_2_1) { e_2 = { error: e_2_1 }; }
511
582
  finally {
512
583
  try {
513
- if (pending_1_1 && !pending_1_1.done && (_v = pending_1.return)) _v.call(pending_1);
584
+ if (pending_1_1 && !pending_1_1.done && (_y = pending_1.return)) _y.call(pending_1);
514
585
  }
515
586
  finally { if (e_2) throw e_2.error; }
516
587
  }
@@ -547,7 +618,7 @@ var SessionReplay = /** @class */ (function () {
547
618
  ], false);
548
619
  return [4 /*yield*/, this.initializeNetworkObservers()];
549
620
  case 12:
550
- _w.sent();
621
+ _z.sent();
551
622
  // Enable background capture when this page is opened by the Amplitude app
552
623
  // (window.opener exists). Uses the shared messenger singleton so that if
553
624
  // autocapture is also loaded, both share a single messenger and script load.
@@ -569,11 +640,27 @@ var SessionReplay = /** @class */ (function () {
569
640
  sampleRate: this.config.sampleRate,
570
641
  optOut: this.shouldOptOut(),
571
642
  currentUrl: (_m = this.getCurrentPageForTargeting()) === null || _m === void 0 ? void 0 : _m.url,
643
+ // 'reload' tells a page refresh apart from a fresh load ('navigate') or back/forward
644
+ // ('back_forward'). New tabs and refreshes keep the same session id, so this is the only
645
+ // way to distinguish a refresh from a brand-new init within a session.
646
+ navigationType: this.getNavigationType(),
572
647
  });
573
648
  return [4 /*yield*/, this.evaluateTargetingAndCapture({ userProperties: options.userProperties, page: this.getCurrentPageForTargeting() }, true)];
574
649
  case 13:
575
- _w.sent();
650
+ _z.sent();
576
651
  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;
652
+ // Record whether we even attempt to wire up the URL-change listener, and the inputs behind that
653
+ // decision. If `needsUrlTracking` is false the listener is never attached, so SPA navigations are
654
+ // invisible to TRC — this is the first thing to check when "TRC is on but never re-evaluates".
655
+ this.recordDiagnosticEvent(SrDiagnostic.urlListenerSetup, {
656
+ needsUrlTracking: !!needsUrlTracking,
657
+ hasTargetingConfig: !!this.config.targetingConfig,
658
+ urlMaskLevels: (_t = (_s = (_r = this.config.privacyConfig) === null || _r === void 0 ? void 0 : _r.urlMaskLevels) === null || _s === void 0 ? void 0 : _s.length) !== null && _t !== void 0 ? _t : 0,
659
+ // config always defaults this to a boolean (see local-config), so no `?? false` needed here.
660
+ enableUrlChangePolling: this.config.enableUrlChangePolling,
661
+ urlChangePollingInterval: this.config.urlChangePollingInterval,
662
+ });
663
+ this.loggerProvider.debug("URL-change listener needed: ".concat(String(!!needsUrlTracking), "."));
577
664
  if (needsUrlTracking) {
578
665
  this.setupUrlChangeListener();
579
666
  }
@@ -613,6 +700,18 @@ var SessionReplay = /** @class */ (function () {
613
700
  this.sendEvents(previousSessionId);
614
701
  }
615
702
  isSessionChange = previousSessionId !== sessionId;
703
+ // A real session transition (not the first set): recording stops/restarts and targeting is
704
+ // re-evaluated here, so session churn (timeout, multi-tab custom ids, explicit setSessionId)
705
+ // is a prime suspect for "sometimes records, sometimes not". recordDiagnosticEvent ships to
706
+ // DataDog AND mirrors to loggerProvider.debug. New tabs / refreshes keep the same id and won't
707
+ // hit this — use the init diagnostic's navigationType for those.
708
+ if (isSessionChange && previousSessionId !== undefined) {
709
+ this.recordDiagnosticEvent(SrDiagnostic.sessionChanged, {
710
+ from: previousSessionId,
711
+ to: sessionId,
712
+ deviceIdChanged: deviceId !== undefined && deviceId !== currentDeviceId,
713
+ });
714
+ }
616
715
  // Drop any beacon-buffered events from the previous session BEFORE installing the
617
716
  // new identifiers / start time. Otherwise the page-leave beacon path could attribute
618
717
  // old-session events to the new session id, and the gate (using the new start time)
@@ -777,6 +876,9 @@ var SessionReplay = /** @class */ (function () {
777
876
  SessionReplay.prototype.incrementDiagnostic = function (counter) {
778
877
  var _a, _b;
779
878
  try {
879
+ // Mirror to the console (Debug level) so the full set of diagnostics is visible in the
880
+ // browser, not only in DataDog — helps debugging when a customer can't share a repro env.
881
+ this.loggerProvider.debug("[SR diagnostics] ".concat(counter));
780
882
  (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.diagnosticsClient) === null || _b === void 0 ? void 0 : _b.increment(counter);
781
883
  }
782
884
  catch (_c) {
@@ -790,6 +892,8 @@ var SessionReplay = /** @class */ (function () {
790
892
  SessionReplay.prototype.recordDiagnosticHistogram = function (name, value) {
791
893
  var _a, _b;
792
894
  try {
895
+ // Mirror to the console (Debug level) — see incrementDiagnostic.
896
+ this.loggerProvider.debug("[SR diagnostics] ".concat(name, "=").concat(value));
793
897
  // The only caller runs inside evaluateTargetingAndCapture's `this.config` guard, so the
794
898
  // `config == null` arm of this optional chain is unreachable here (kept as defensive parity
795
899
  // with recordDiagnosticEvent). The diagnosticsClient-absent arm IS exercised by no-client tests.
@@ -814,7 +918,11 @@ var SessionReplay = /** @class */ (function () {
814
918
  // Always stamp sessionId, deviceId and srId so every diagnostics event (→ DataDog Logs) can
815
919
  // be correlated to a single session/device — and to the actual replay, since the Session
816
920
  // 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));
921
+ var enriched = __assign({ sessionId: sessionId, deviceId: deviceId, srId: deviceId != null && sessionId != null ? "".concat(deviceId, "/").concat(sessionId) : undefined }, properties);
922
+ // Mirror to the console (Debug level) so the full event + props are visible in the browser,
923
+ // not only in DataDog — see incrementDiagnostic.
924
+ this.loggerProvider.debug("[SR diagnostics] ".concat(name, " ").concat(JSON.stringify(enriched)));
925
+ (_c = (_b = this.config) === null || _b === void 0 ? void 0 : _b.diagnosticsClient) === null || _c === void 0 ? void 0 : _c.recordEvent(name, enriched);
818
926
  // Flush immediately so the event ships now rather than on the client's ~5-min timer (and so
819
927
  // short sessions that never reach the timer aren't lost). NOTE: this sends one capture POST
820
928
  // per event — higher request volume; revisit/gate before production.