@amplitude/session-replay-browser 1.48.3 → 1.50.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 (64) hide show
  1. package/README.md +16 -2
  2. package/lib/cjs/config/local-config.d.ts +1 -0
  3. package/lib/cjs/config/local-config.d.ts.map +1 -1
  4. package/lib/cjs/config/local-config.js +1 -0
  5. package/lib/cjs/config/local-config.js.map +1 -1
  6. package/lib/cjs/config/types.d.ts +6 -0
  7. package/lib/cjs/config/types.d.ts.map +1 -1
  8. package/lib/cjs/config/types.js.map +1 -1
  9. package/lib/cjs/index.d.ts +1 -1
  10. package/lib/cjs/index.d.ts.map +1 -1
  11. package/lib/cjs/index.js +2 -2
  12. package/lib/cjs/index.js.map +1 -1
  13. package/lib/cjs/session-replay-factory.d.ts.map +1 -1
  14. package/lib/cjs/session-replay-factory.js +2 -0
  15. package/lib/cjs/session-replay-factory.js.map +1 -1
  16. package/lib/cjs/session-replay.d.ts +22 -0
  17. package/lib/cjs/session-replay.d.ts.map +1 -1
  18. package/lib/cjs/session-replay.js +112 -36
  19. package/lib/cjs/session-replay.js.map +1 -1
  20. package/lib/cjs/typings/session-replay.d.ts +11 -0
  21. package/lib/cjs/typings/session-replay.d.ts.map +1 -1
  22. package/lib/cjs/typings/session-replay.js.map +1 -1
  23. package/lib/cjs/utils/rrweb.d.ts +1 -0
  24. package/lib/cjs/utils/rrweb.d.ts.map +1 -1
  25. package/lib/cjs/utils/rrweb.js.map +1 -1
  26. package/lib/cjs/version.d.ts +1 -1
  27. package/lib/cjs/version.js +1 -1
  28. package/lib/cjs/version.js.map +1 -1
  29. package/lib/esm/config/local-config.d.ts +1 -0
  30. package/lib/esm/config/local-config.d.ts.map +1 -1
  31. package/lib/esm/config/local-config.js +1 -0
  32. package/lib/esm/config/local-config.js.map +1 -1
  33. package/lib/esm/config/types.d.ts +6 -0
  34. package/lib/esm/config/types.d.ts.map +1 -1
  35. package/lib/esm/config/types.js.map +1 -1
  36. package/lib/esm/index.d.ts +1 -1
  37. package/lib/esm/index.d.ts.map +1 -1
  38. package/lib/esm/index.js +1 -1
  39. package/lib/esm/index.js.map +1 -1
  40. package/lib/esm/session-replay-factory.d.ts.map +1 -1
  41. package/lib/esm/session-replay-factory.js +2 -0
  42. package/lib/esm/session-replay-factory.js.map +1 -1
  43. package/lib/esm/session-replay.d.ts +22 -0
  44. package/lib/esm/session-replay.d.ts.map +1 -1
  45. package/lib/esm/session-replay.js +112 -36
  46. package/lib/esm/session-replay.js.map +1 -1
  47. package/lib/esm/typings/session-replay.d.ts +11 -0
  48. package/lib/esm/typings/session-replay.d.ts.map +1 -1
  49. package/lib/esm/typings/session-replay.js.map +1 -1
  50. package/lib/esm/utils/rrweb.d.ts +1 -0
  51. package/lib/esm/utils/rrweb.d.ts.map +1 -1
  52. package/lib/esm/utils/rrweb.js.map +1 -1
  53. package/lib/esm/version.d.ts +1 -1
  54. package/lib/esm/version.js +1 -1
  55. package/lib/esm/version.js.map +1 -1
  56. package/lib/scripts/index-min.js +1 -1
  57. package/lib/scripts/index-min.js.gz +0 -0
  58. package/lib/scripts/index-min.js.map +1 -1
  59. package/lib/scripts/session-replay-browser-esm.js +1 -1
  60. package/lib/scripts/session-replay-browser-esm.js.gz +0 -0
  61. package/lib/scripts/session-replay-browser-min.js +1 -1
  62. package/lib/scripts/session-replay-browser-min.js.gz +0 -0
  63. package/lib/scripts/session-replay-browser-min.js.map +1 -1
  64. package/package.json +3 -3
@@ -41,6 +41,24 @@ var SessionReplay = /** @class */ (function () {
41
41
  // Cache the dynamically imported record function
42
42
  this.recordFunction = null;
43
43
  this.recordEventsInFlight = false;
44
+ /**
45
+ * When false, capture is paused by a customer `stop()` (or `shutdown()`) call.
46
+ * Focus/targeting/session-change paths still invoke `recordEvents()`, but
47
+ * `getShouldRecord()` will refuse to start rrweb until `start()` or a new `init()`.
48
+ */
49
+ this.recordingEnabled = true;
50
+ /**
51
+ * Bumped by `stop()` / `shutdown()` so an in-flight `_recordEvents()` that already
52
+ * passed `getShouldRecord()` cannot start rrweb after capture was paused.
53
+ */
54
+ this.recordingGeneration = 0;
55
+ /**
56
+ * Set by customer `start()` so coordinated child iframes self-start instead of
57
+ * waiting for a parent signal that may never be resent. Kept until recording
58
+ * actually starts so a gated `start()` (targeting, sampling, remote capture)
59
+ * still self-starts when those gates later pass.
60
+ */
61
+ this.startChildRecordingOnSetup = false;
44
62
  this.pendingEmitEvents = [];
45
63
  /** Current page URL, kept in sync with SPA navigations for URL-based masking */
46
64
  this.currentPageUrl = '';
@@ -77,6 +95,9 @@ var SessionReplay = /** @class */ (function () {
77
95
  };
78
96
  this.focusListener = function () {
79
97
  var _a;
98
+ if (!_this.recordingEnabled) {
99
+ return;
100
+ }
80
101
  if (_this.recordCancelCallback && _this.recordFunction) {
81
102
  // Recording is already active. The on-focus full snapshot is tunable: when
82
103
  // `captureFullSnapshotOnFocus` is false we skip it entirely so high focus-churn pages
@@ -332,6 +353,9 @@ var SessionReplay = /** @class */ (function () {
332
353
  // Re-init should always tear down any previous URL-change subscription, even when the
333
354
  // next config has no targeting config and we don't subscribe again.
334
355
  (_a = this.urlChangeCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
356
+ // A new init always allows capture again. `stop()` only pauses the current instance.
357
+ this.recordingEnabled = true;
358
+ this.startChildRecordingOnSetup = false;
335
359
  this.loggerProvider = new SafeLoggerProvider(options.loggerProvider || new Logger());
336
360
  Object.prototype.hasOwnProperty.call(options, 'logLevel') &&
337
361
  this.loggerProvider.enable(options.logLevel);
@@ -693,6 +717,10 @@ var SessionReplay = /** @class */ (function () {
693
717
  this.loggerProvider.warn("Session is not being recorded due to lack of config, please call sessionReplay.init.");
694
718
  return false;
695
719
  }
720
+ if (!this.recordingEnabled) {
721
+ this.loggerProvider.log("Session ".concat(this.identifiers.sessionId, " not being captured because recording was stopped."));
722
+ return false;
723
+ }
696
724
  if (!this.config.captureEnabled) {
697
725
  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."));
698
726
  return false;
@@ -885,15 +913,17 @@ var SessionReplay = /** @class */ (function () {
885
913
  });
886
914
  });
887
915
  };
916
+ SessionReplay.prototype.shouldAbandonRecordStart = function (generation) {
917
+ return !this.recordingEnabled || generation !== this.recordingGeneration;
918
+ };
888
919
  SessionReplay.prototype._recordEvents = function (shouldLogMetadata) {
889
920
  var _a, _b, _c, _d, _e, _f, _g, _h;
890
921
  if (shouldLogMetadata === void 0) { shouldLogMetadata = true; }
891
922
  return __awaiter(this, void 0, void 0, function () {
892
- var config, shouldRecord, sessionId, recordFunction, networkLoggingConfig, trackUrl, ignoredUrls, interactionConfig, loggingConfig, hooks, ugcFilterRules, crossOriginIframesEnabled, coordinateChildren, childMode, _j, _k, _l, error_5;
893
- var _m;
923
+ var config, shouldRecord, sessionId, generation, recordFunction, networkLoggingConfig, trackUrl, ignoredUrls, interactionConfig, loggingConfig, hooks, ugcFilterRules, crossOriginIframesEnabled, coordinateChildren, childMode, plugins, error_5;
894
924
  var _this = this;
895
- return __generator(this, function (_o) {
896
- switch (_o.label) {
925
+ return __generator(this, function (_j) {
926
+ switch (_j.label) {
897
927
  case 0:
898
928
  config = this.config;
899
929
  shouldRecord = this.getShouldRecord();
@@ -902,16 +932,20 @@ var SessionReplay = /** @class */ (function () {
902
932
  return [2 /*return*/];
903
933
  }
904
934
  this.stopRecordingEvents();
935
+ generation = this.recordingGeneration;
905
936
  return [4 /*yield*/, this.getRecordFunction()];
906
937
  case 1:
907
- recordFunction = _o.sent();
938
+ recordFunction = _j.sent();
908
939
  // May be undefined if cannot import rrweb-record
909
- if (!recordFunction) {
940
+ if (!recordFunction || this.shouldAbandonRecordStart(generation)) {
910
941
  return [2 /*return*/];
911
942
  }
912
943
  return [4 /*yield*/, this.initializeNetworkObservers()];
913
944
  case 2:
914
- _o.sent();
945
+ _j.sent();
946
+ if (this.shouldAbandonRecordStart(generation)) {
947
+ return [2 /*return*/];
948
+ }
915
949
  networkLoggingConfig = (_b = config.loggingConfig) === null || _b === void 0 ? void 0 : _b.network;
916
950
  trackUrl = getServerUrl(config.serverZone, config.trackServerUrl);
917
951
  ignoredUrls = [SESSION_REPLAY_SERVER_URL, SESSION_REPLAY_EU_URL, SESSION_REPLAY_STAGING_URL, trackUrl];
@@ -937,9 +971,9 @@ var SessionReplay = /** @class */ (function () {
937
971
  : {};
938
972
  ugcFilterRules = (interactionConfig === null || interactionConfig === void 0 ? void 0 : interactionConfig.enabled) && interactionConfig.ugcFilterRules ? interactionConfig.ugcFilterRules : [];
939
973
  this.loggerProvider.log("Session Replay capture beginning for ".concat(sessionId, "."));
940
- _o.label = 3;
974
+ _j.label = 3;
941
975
  case 3:
942
- _o.trys.push([3, 5, , 6]);
976
+ _j.trys.push([3, 5, , 6]);
943
977
  crossOriginIframesEnabled = !!((_g = config.crossOriginIframes) === null || _g === void 0 ? void 0 : _g.enabled);
944
978
  coordinateChildren = ((_h = config.crossOriginIframes) === null || _h === void 0 ? void 0 : _h.coordinateChildren) !== false;
945
979
  childMode = crossOriginIframesEnabled && isInIframe();
@@ -947,7 +981,12 @@ var SessionReplay = /** @class */ (function () {
947
981
  // Child mode: don't self-start; wait for a start signal from the parent.
948
982
  // (The previous listener, if any, was already removed by stopRecordingEvents above.)
949
983
  this.crossOriginParentSignalCleanup = listenForParentSignals({
950
- onStart: function () { return _this._recordEventsInChildMode(recordFunction, sessionId, config, hooks); },
984
+ onStart: function () {
985
+ if (!_this.recordingEnabled) {
986
+ return;
987
+ }
988
+ _this._recordEventsInChildMode(recordFunction, sessionId, config, hooks);
989
+ },
951
990
  onStop: function () {
952
991
  try {
953
992
  // Only cancel the rrweb recording — do NOT call stopRecordingEvents() here,
@@ -962,34 +1001,43 @@ var SessionReplay = /** @class */ (function () {
962
1001
  }
963
1002
  },
964
1003
  });
1004
+ // Customer start() must not wait for a parent signal: the parent may already
1005
+ // be recording and will not send another start. Init still waits.
1006
+ if (this.startChildRecordingOnSetup) {
1007
+ this.startChildRecordingOnSetup = false;
1008
+ this._recordEventsInChildMode(recordFunction, sessionId, config, hooks);
1009
+ }
965
1010
  return [2 /*return*/];
966
1011
  }
967
- _j = this;
968
- _k = recordFunction;
969
- _l = [__assign({}, this.buildRRWebRecordOptions(config, hooks, function (event) {
970
- if (_this.shouldOptOut()) {
971
- _this.loggerProvider.log("Opting session ".concat(sessionId, " out of recording due to optOut config."));
972
- _this.stopRecordingEvents();
973
- _this.sendEvents();
974
- return;
975
- }
976
- if (event.type === RRWebEventType.Meta) {
977
- event.data.href = getPageUrl(event.data.href, ugcFilterRules);
978
- }
979
- if (_this.eventCompressor) {
980
- // Schedule processing during idle time if the browser supports requestIdleCallback
981
- _this.eventCompressor.enqueueEvent(event, sessionId);
982
- }
983
- else {
984
- // eventCompressor is not yet ready (concurrent call racing _init()).
985
- // Buffer the event so it can be flushed once the compressor is initialized.
986
- _this.pendingEmitEvents.push({ event: event, sessionId: sessionId });
987
- }
988
- }, 'Error while capturing replay: '))];
989
- _m = {};
1012
+ this.startChildRecordingOnSetup = false;
990
1013
  return [4 /*yield*/, this.getRecordingPlugins(loggingConfig)];
991
1014
  case 4:
992
- _j.recordCancelCallback = _k.apply(void 0, [__assign.apply(void 0, _l.concat([(_m.plugins = _o.sent(), _m.recordCrossOriginIframes = crossOriginIframesEnabled, _m)]))]);
1015
+ plugins = _j.sent();
1016
+ if (this.shouldAbandonRecordStart(generation)) {
1017
+ return [2 /*return*/];
1018
+ }
1019
+ this.recordCancelCallback = recordFunction(__assign(__assign({}, this.buildRRWebRecordOptions(config, hooks, function (event) {
1020
+ if (_this.shouldOptOut() || !_this.recordingEnabled) {
1021
+ _this.loggerProvider.log(_this.shouldOptOut()
1022
+ ? "Opting session ".concat(sessionId, " out of recording due to optOut config.")
1023
+ : "Session Replay capture stopping for ".concat(sessionId, "."));
1024
+ _this.stopRecordingEvents();
1025
+ _this.sendEvents();
1026
+ return;
1027
+ }
1028
+ if (event.type === RRWebEventType.Meta) {
1029
+ event.data.href = getPageUrl(event.data.href, ugcFilterRules);
1030
+ }
1031
+ if (_this.eventCompressor) {
1032
+ // Schedule processing during idle time if the browser supports requestIdleCallback
1033
+ _this.eventCompressor.enqueueEvent(event, sessionId);
1034
+ }
1035
+ else {
1036
+ // eventCompressor is not yet ready (concurrent call racing _init()).
1037
+ // Buffer the event so it can be flushed once the compressor is initialized.
1038
+ _this.pendingEmitEvents.push({ event: event, sessionId: sessionId });
1039
+ }
1040
+ }, 'Error while capturing replay: ')), { plugins: plugins, recordCrossOriginIframes: crossOriginIframesEnabled }));
993
1041
  if (crossOriginIframesEnabled && !childMode && coordinateChildren) {
994
1042
  if (!this.crossOriginIframeCoordinator) {
995
1043
  this.crossOriginIframeCoordinator = new CrossOriginIframeCoordinator();
@@ -1002,7 +1050,7 @@ var SessionReplay = /** @class */ (function () {
1002
1050
  }
1003
1051
  return [3 /*break*/, 6];
1004
1052
  case 5:
1005
- error_5 = _o.sent();
1053
+ error_5 = _j.sent();
1006
1054
  this.loggerProvider.warn('Failed to initialize session replay:', error_5);
1007
1055
  return [3 /*break*/, 6];
1008
1056
  case 6: return [2 /*return*/];
@@ -1013,7 +1061,7 @@ var SessionReplay = /** @class */ (function () {
1013
1061
  SessionReplay.prototype.buildRRWebRecordOptions = function (config, hooks, emit, errorLogPrefix) {
1014
1062
  var _this = this;
1015
1063
  var privacyConfig = config.privacyConfig;
1016
- return __assign(__assign({ emit: emit, inlineStylesheet: config.shouldInlineStylesheet, hooks: hooks, maskAllInputs: true, maskTextClass: MASK_TEXT_CLASS, blockClass: BLOCK_CLASS, blockSelector: this.getBlockSelectors(), applyBackgroundColorToBlockedElements: config.applyBackgroundColorToBlockedElements, maskInputFn: maskFn('input', privacyConfig, function () { return _this.currentPageUrl; }), maskTextFn: maskFn('text', privacyConfig, function () { return _this.currentPageUrl; }), maskAttributeFn: maskAttributeFn(privacyConfig, function () { return _this.currentPageUrl; }), maskTextSelector: this.getMaskTextSelectors() }, (config.fullSnapshotIntervalMs !== undefined && { checkoutEveryNms: config.fullSnapshotIntervalMs })), { recordCanvas: false, captureAdoptedStyleSheets: config.captureAdoptedStyleSheets,
1064
+ return __assign(__assign({ emit: emit, inlineStylesheet: config.shouldInlineStylesheet, inlineImages: config.inlineImages, hooks: hooks, maskAllInputs: true, maskTextClass: MASK_TEXT_CLASS, blockClass: BLOCK_CLASS, blockSelector: this.getBlockSelectors(), applyBackgroundColorToBlockedElements: config.applyBackgroundColorToBlockedElements, maskInputFn: maskFn('input', privacyConfig, function () { return _this.currentPageUrl; }), maskTextFn: maskFn('text', privacyConfig, function () { return _this.currentPageUrl; }), maskAttributeFn: maskAttributeFn(privacyConfig, function () { return _this.currentPageUrl; }), maskTextSelector: this.getMaskTextSelectors() }, (config.fullSnapshotIntervalMs !== undefined && { checkoutEveryNms: config.fullSnapshotIntervalMs })), { recordCanvas: false, captureAdoptedStyleSheets: config.captureAdoptedStyleSheets,
1017
1065
  // Strip nodes that are never rendered by the rrweb replay player.
1018
1066
  // None of these affect visual fidelity; omitting them reduces snapshot size.
1019
1067
  slimDOMOptions: {
@@ -1072,6 +1120,31 @@ var SessionReplay = /** @class */ (function () {
1072
1120
  var _a;
1073
1121
  return (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId;
1074
1122
  };
1123
+ SessionReplay.prototype.start = function () {
1124
+ return returnWrapper(this._start());
1125
+ };
1126
+ SessionReplay.prototype._start = function () {
1127
+ return __awaiter(this, void 0, void 0, function () {
1128
+ return __generator(this, function (_a) {
1129
+ switch (_a.label) {
1130
+ case 0:
1131
+ this.recordingEnabled = true;
1132
+ this.startChildRecordingOnSetup = true;
1133
+ return [4 /*yield*/, this.recordEvents()];
1134
+ case 1:
1135
+ _a.sent();
1136
+ return [2 /*return*/];
1137
+ }
1138
+ });
1139
+ });
1140
+ };
1141
+ SessionReplay.prototype.stop = function () {
1142
+ this.recordingEnabled = false;
1143
+ this.startChildRecordingOnSetup = false;
1144
+ this.recordingGeneration++;
1145
+ this.stopRecordingEvents();
1146
+ this.sendEvents();
1147
+ };
1075
1148
  SessionReplay.prototype.flush = function (useRetry) {
1076
1149
  var _a;
1077
1150
  if (useRetry === void 0) { useRetry = false; }
@@ -1088,6 +1161,9 @@ var SessionReplay = /** @class */ (function () {
1088
1161
  };
1089
1162
  SessionReplay.prototype.shutdown = function () {
1090
1163
  var _a, _b;
1164
+ this.recordingEnabled = false;
1165
+ this.startChildRecordingOnSetup = false;
1166
+ this.recordingGeneration++;
1091
1167
  (_a = this.urlChangeCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
1092
1168
  (_b = this.crossOriginParentSignalCleanup) === null || _b === void 0 ? void 0 : _b.call(this);
1093
1169
  this.crossOriginParentSignalCleanup = null;