@agent-native/core 0.84.67 → 0.85.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 (149) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
  4. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
  5. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
  6. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
  7. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
  8. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
  9. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
  10. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
  11. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
  12. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
  13. package/corpus/core/docs/content/tracking.mdx +2 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/action-continuation-guidance.ts +1 -1
  16. package/corpus/core/src/agent/production-agent.ts +222 -38
  17. package/corpus/core/src/agent/run-manager.ts +79 -0
  18. package/corpus/core/src/agent/run-store.ts +122 -8
  19. package/corpus/core/src/agent/types.ts +13 -0
  20. package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
  21. package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
  22. package/corpus/core/src/client/analytics.ts +2 -0
  23. package/corpus/core/src/client/error-format.ts +4 -0
  24. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
  25. package/corpus/core/src/client/session-replay.ts +774 -2
  26. package/corpus/core/src/client/sse-event-processor.ts +65 -9
  27. package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
  28. package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
  29. package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
  30. package/corpus/core/src/server/core-routes-plugin.ts +91 -4
  31. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  32. package/corpus/core/src/server/self-dispatch.ts +29 -0
  33. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  34. package/corpus/templates/analytics/AGENTS.md +13 -0
  35. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  36. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  37. package/corpus/templates/analytics/app/global.css +55 -0
  38. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  39. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  40. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  43. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  46. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  47. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  48. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  53. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  54. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  55. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  56. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  57. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  58. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  59. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  60. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  61. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  62. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  63. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  64. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  65. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  66. package/corpus/templates/plan/server/plans.ts +26 -9
  67. package/dist/agent/action-continuation-guidance.js +1 -1
  68. package/dist/agent/action-continuation-guidance.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +9 -1
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +168 -34
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +29 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +73 -0
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +41 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +105 -7
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +13 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  85. package/dist/client/RunStuckBanner.js +13 -0
  86. package/dist/client/RunStuckBanner.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +410 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +1 -1
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js.map +1 -1
  93. package/dist/client/error-format.d.ts.map +1 -1
  94. package/dist/client/error-format.js +2 -0
  95. package/dist/client/error-format.js.map +1 -1
  96. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  97. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  98. package/dist/client/session-replay.d.ts +36 -0
  99. package/dist/client/session-replay.d.ts.map +1 -1
  100. package/dist/client/session-replay.js +579 -1
  101. package/dist/client/session-replay.js.map +1 -1
  102. package/dist/client/sse-event-processor.d.ts +20 -1
  103. package/dist/client/sse-event-processor.d.ts.map +1 -1
  104. package/dist/client/sse-event-processor.js +42 -11
  105. package/dist/client/sse-event-processor.js.map +1 -1
  106. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  107. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  108. package/dist/client/use-run-stuck-detection.js +18 -4
  109. package/dist/client/use-run-stuck-detection.js.map +1 -1
  110. package/dist/collab/awareness.d.ts +2 -2
  111. package/dist/collab/awareness.d.ts.map +1 -1
  112. package/dist/collab/routes.d.ts +2 -2
  113. package/dist/db/runtime-diagnostics.d.ts +52 -0
  114. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  115. package/dist/db/runtime-diagnostics.js +250 -0
  116. package/dist/db/runtime-diagnostics.js.map +1 -0
  117. package/dist/notifications/routes.d.ts +1 -1
  118. package/dist/observability/routes.d.ts +3 -3
  119. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  120. package/dist/resources/handlers.d.ts +3 -3
  121. package/dist/secrets/routes.d.ts +3 -3
  122. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  123. package/dist/server/agent-chat-plugin.js +101 -2
  124. package/dist/server/agent-chat-plugin.js.map +1 -1
  125. package/dist/server/core-routes-plugin.d.ts +17 -0
  126. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  127. package/dist/server/core-routes-plugin.js +65 -5
  128. package/dist/server/core-routes-plugin.js.map +1 -1
  129. package/dist/server/http-response-telemetry.d.ts +4 -0
  130. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  131. package/dist/server/http-response-telemetry.js +174 -0
  132. package/dist/server/http-response-telemetry.js.map +1 -0
  133. package/dist/server/self-dispatch.d.ts +17 -0
  134. package/dist/server/self-dispatch.d.ts.map +1 -1
  135. package/dist/server/self-dispatch.js +11 -0
  136. package/dist/server/self-dispatch.js.map +1 -1
  137. package/dist/server/transcribe-voice.d.ts +1 -1
  138. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  139. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  140. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  141. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  142. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  143. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  144. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  145. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  146. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  147. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  148. package/docs/content/tracking.mdx +2 -0
  149. package/package.json +1 -1
@@ -41,6 +41,39 @@ const DEFAULT_MAX_EVENTS_PER_BATCH = 50;
41
41
  const DEFAULT_MAX_BATCH_BYTES = 256 * 1024;
42
42
  const MAX_KEEPALIVE_REPLAY_UPLOAD_BYTES = 60 * 1024;
43
43
  const RRWEB_FULL_SNAPSHOT_EVENT_TYPE = 2;
44
+ /** rrweb custom-event tag for captured console/window-error entries. */
45
+ export const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
46
+ /** rrweb custom-event tag for captured fetch/XHR request summaries. */
47
+ export const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
48
+ const DEFAULT_MAX_CONSOLE_EVENTS = 1000;
49
+ const DEFAULT_MAX_NETWORK_EVENTS = 2000;
50
+ const MAX_CONSOLE_MESSAGE_LENGTH = 500;
51
+ const MAX_CONSOLE_ARGS = 10;
52
+ const MAX_CONSOLE_STACK_LENGTH = 2000;
53
+ const MAX_CONSOLE_SERIALIZE_DEPTH = 4;
54
+ const MAX_CONSOLE_SERIALIZE_ENTRIES = 20;
55
+ /**
56
+ * Re-entrancy guard: true while the recorder itself is emitting custom events
57
+ * or flushing (synchronously) so the console/fetch/XHR wrappers never capture
58
+ * the recorder's own work and feed it back into the replay stream.
59
+ */
60
+ let replayCaptureInternal = false;
61
+ // Credential-looking token redaction for captured console/network text,
62
+ // adapted from the clips template's browser-diagnostics redaction helper.
63
+ const CAPTURE_SECRET_KEY_FRAGMENT = "(?:authorization|cookie|set[-_]?cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|session|credential)";
64
+ const CAPTURE_AUTHORIZATION_SCHEME_RE = /\b(authorization)\b(\s*[:=]\s*)(?:bearer|basic)\s+[a-z0-9._~+/-]+=*/gi;
65
+ const CAPTURE_BEARER_RE = /\b(bearer|basic)\s+[a-z0-9._~+/-]+=*/gi;
66
+ const CAPTURE_DOUBLE_QUOTED_SECRET_RE = new RegExp(`(["']?)([A-Za-z0-9_$.-]*${CAPTURE_SECRET_KEY_FRAGMENT}[A-Za-z0-9_$.-]*)\\1(\\s*[:=]\\s*)"(?:[^"\\\\]|\\\\.)*"`, "gi");
67
+ const CAPTURE_SINGLE_QUOTED_SECRET_RE = new RegExp(`(["']?)([A-Za-z0-9_$.-]*${CAPTURE_SECRET_KEY_FRAGMENT}[A-Za-z0-9_$.-]*)\\1(\\s*[:=]\\s*)'(?:[^'\\\\]|\\\\.)*'`, "gi");
68
+ const CAPTURE_UNQUOTED_SECRET_RE = new RegExp(`(["']?)([A-Za-z0-9_$.-]*${CAPTURE_SECRET_KEY_FRAGMENT}[A-Za-z0-9_$.-]*)\\1(\\s*[:=]\\s*)([^"',\\s;}\\]]+)`, "gi");
69
+ function redactCaptureText(value) {
70
+ return value
71
+ .replace(CAPTURE_AUTHORIZATION_SCHEME_RE, "$1$2<redacted>")
72
+ .replace(CAPTURE_BEARER_RE, "$1 <redacted>")
73
+ .replace(CAPTURE_DOUBLE_QUOTED_SECRET_RE, '$1$2$1$3"<redacted>"')
74
+ .replace(CAPTURE_SINGLE_QUOTED_SECRET_RE, "$1$2$1$3'<redacted>'")
75
+ .replace(CAPTURE_UNQUOTED_SECRET_RE, "$1$2$1$3<redacted>");
76
+ }
44
77
  const URL_LIKE_KEYS = new Set([
45
78
  "url",
46
79
  "uri",
@@ -69,6 +102,8 @@ function getState() {
69
102
  stopRecorder: null,
70
103
  restoreUrlMonitor: null,
71
104
  removeLifecycleListeners: null,
105
+ addCustomEvent: null,
106
+ restoreCaptures: null,
72
107
  options: null,
73
108
  lastAuthenticatedProperties: null,
74
109
  };
@@ -319,9 +354,25 @@ function normalizeOptions(options) {
319
354
  collectFonts: options.collectFonts ?? false,
320
355
  inlineImages: options.inlineImages ?? false,
321
356
  eventSampling: options.eventSampling ?? DEFAULT_EVENT_SAMPLING,
357
+ console: normalizeCaptureToggle(options.console, DEFAULT_MAX_CONSOLE_EVENTS),
358
+ network: normalizeCaptureToggle(options.network, DEFAULT_MAX_NETWORK_EVENTS),
322
359
  extraProperties: options.extraProperties,
323
360
  };
324
361
  }
362
+ /**
363
+ * Console/network capture default to ON whenever session replay records;
364
+ * `false` disables a category, an object form overrides its caps.
365
+ */
366
+ function normalizeCaptureToggle(value, defaultMaxEvents) {
367
+ if (value === false)
368
+ return null;
369
+ const overrides = typeof value === "object" && value !== null ? value : {};
370
+ const maxEvents = typeof overrides.maxEvents === "number" &&
371
+ Number.isFinite(overrides.maxEvents)
372
+ ? Math.max(1, Math.floor(overrides.maxEvents))
373
+ : defaultMaxEvents;
374
+ return { maxEvents };
375
+ }
325
376
  function scrubStringValue(key, value) {
326
377
  const lowerKey = key.toLowerCase();
327
378
  const isUrlKey = URL_LIKE_KEYS.has(key) || URL_LIKE_KEYS.has(lowerKey);
@@ -689,7 +740,16 @@ export async function flushSessionReplay(reason = "manual") {
689
740
  if (reservedSequence)
690
741
  rollbackReplaySequenceReservation(state, payload);
691
742
  restoreReplayEvents(state, events);
692
- console.warn("[session-replay] upload failed", error);
743
+ // Guard the recorder's own warning so console capture never records it
744
+ // (a captured warning would enqueue an event and retrigger a flush).
745
+ const previousInternal = replayCaptureInternal;
746
+ replayCaptureInternal = true;
747
+ try {
748
+ console.warn("[session-replay] upload failed", error);
749
+ }
750
+ finally {
751
+ replayCaptureInternal = previousInternal;
752
+ }
693
753
  }
694
754
  finally {
695
755
  state.flushing = false;
@@ -746,6 +806,500 @@ function installLifecycleListeners(state) {
746
806
  state.removeLifecycleListeners = null;
747
807
  };
748
808
  }
809
+ function truncateCaptureText(value, maxLength) {
810
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
811
+ }
812
+ /** Circular-safe, depth/length-limited plain value for JSON.stringify. */
813
+ function toCaptureSerializable(value, depth, seen) {
814
+ if (value === null || value === undefined)
815
+ return value;
816
+ if (typeof value === "string")
817
+ return value;
818
+ if (typeof value === "number" || typeof value === "boolean")
819
+ return value;
820
+ if (typeof value === "bigint")
821
+ return `${value}n`;
822
+ if (typeof value === "function")
823
+ return "[function]";
824
+ if (typeof value === "symbol")
825
+ return String(value);
826
+ if (value instanceof Error)
827
+ return `${value.name}: ${value.message}`;
828
+ if (typeof value !== "object")
829
+ return String(value);
830
+ if (seen.has(value))
831
+ return "[circular]";
832
+ if (depth >= MAX_CONSOLE_SERIALIZE_DEPTH) {
833
+ return Array.isArray(value) ? "[array]" : "[object]";
834
+ }
835
+ seen.add(value);
836
+ if (Array.isArray(value)) {
837
+ const items = value
838
+ .slice(0, MAX_CONSOLE_SERIALIZE_ENTRIES)
839
+ .map((item) => toCaptureSerializable(item, depth + 1, seen));
840
+ if (value.length > MAX_CONSOLE_SERIALIZE_ENTRIES)
841
+ items.push("[truncated]");
842
+ return items;
843
+ }
844
+ const out = {};
845
+ let count = 0;
846
+ for (const [key, child] of Object.entries(value)) {
847
+ if (count >= MAX_CONSOLE_SERIALIZE_ENTRIES) {
848
+ out["[truncated]"] = true;
849
+ break;
850
+ }
851
+ out[key] = toCaptureSerializable(child, depth + 1, seen);
852
+ count += 1;
853
+ }
854
+ return out;
855
+ }
856
+ function serializeConsoleArg(value) {
857
+ try {
858
+ if (typeof value === "string")
859
+ return value;
860
+ if (value instanceof Error)
861
+ return `${value.name}: ${value.message}`;
862
+ if (value === null ||
863
+ value === undefined ||
864
+ typeof value === "number" ||
865
+ typeof value === "boolean" ||
866
+ typeof value === "bigint") {
867
+ return String(value);
868
+ }
869
+ return (JSON.stringify(toCaptureSerializable(value, 0, new WeakSet())) ??
870
+ String(value));
871
+ }
872
+ catch {
873
+ try {
874
+ return Object.prototype.toString.call(value);
875
+ }
876
+ catch {
877
+ return "[unserializable]";
878
+ }
879
+ }
880
+ }
881
+ /**
882
+ * Emit an rrweb custom event while recording. Sets the re-entrancy guard so
883
+ * synchronous work triggered by the emit (enqueue -> flush -> fetch of the
884
+ * ingest endpoint) is never re-captured by the interceptors below.
885
+ */
886
+ function emitReplayCustomEvent(state, tag, payload) {
887
+ const addCustomEvent = state.addCustomEvent;
888
+ if (!state.active || !addCustomEvent)
889
+ return;
890
+ const previous = replayCaptureInternal;
891
+ replayCaptureInternal = true;
892
+ try {
893
+ addCustomEvent(tag, payload);
894
+ }
895
+ catch {
896
+ // recorder already stopped -- drop the event
897
+ }
898
+ finally {
899
+ replayCaptureInternal = previous;
900
+ }
901
+ }
902
+ function captureCurrentUrl() {
903
+ try {
904
+ return scrubUrl(window.location.href);
905
+ }
906
+ catch {
907
+ return undefined;
908
+ }
909
+ }
910
+ const CAPTURE_CONSOLE_LEVELS = [
911
+ "log",
912
+ "info",
913
+ "warn",
914
+ "error",
915
+ "debug",
916
+ ];
917
+ function installConsoleCapture(state, captureOptions) {
918
+ let emitted = 0;
919
+ let stopped = false;
920
+ let pending = null;
921
+ const emitPayload = (payload) => {
922
+ if (stopped)
923
+ return;
924
+ if (emitted >= captureOptions.maxEvents) {
925
+ stopped = true;
926
+ emitReplayCustomEvent(state, SESSION_REPLAY_CONSOLE_EVENT_TAG, {
927
+ level: "warn",
928
+ source: "console",
929
+ message: "session replay console capture truncated",
930
+ truncated: true,
931
+ });
932
+ return;
933
+ }
934
+ emitted += 1;
935
+ emitReplayCustomEvent(state, SESSION_REPLAY_CONSOLE_EVENT_TAG, payload);
936
+ };
937
+ /**
938
+ * The first occurrence of a message is emitted immediately; consecutive
939
+ * identical messages accumulate here and are flushed as one event whose
940
+ * `repeat` is the number of collapsed duplicates.
941
+ */
942
+ const flushPending = () => {
943
+ const entry = pending;
944
+ pending = null;
945
+ if (!entry || entry.repeat <= 0)
946
+ return;
947
+ emitPayload({ ...entry.payload, repeat: entry.repeat });
948
+ };
949
+ const capture = (level, source, args, stackOverride) => {
950
+ if (stopped || replayCaptureInternal)
951
+ return;
952
+ try {
953
+ const message = truncateCaptureText(redactCaptureText(args.length ? serializeConsoleArg(args[0]) : ""), MAX_CONSOLE_MESSAGE_LENGTH);
954
+ const extraArgs = args
955
+ .slice(1, 1 + MAX_CONSOLE_ARGS)
956
+ .map((arg) => truncateCaptureText(redactCaptureText(serializeConsoleArg(arg)), MAX_CONSOLE_MESSAGE_LENGTH));
957
+ const errorArg = args.find((arg) => arg instanceof Error);
958
+ const rawStack = stackOverride ?? errorArg?.stack;
959
+ const stack = typeof rawStack === "string" && rawStack
960
+ ? truncateCaptureText(redactCaptureText(rawStack), MAX_CONSOLE_STACK_LENGTH)
961
+ : undefined;
962
+ const url = captureCurrentUrl();
963
+ const payload = {
964
+ level,
965
+ source,
966
+ message,
967
+ ...(extraArgs.length ? { args: extraArgs } : {}),
968
+ ...(stack ? { stack } : {}),
969
+ ...(url ? { url } : {}),
970
+ };
971
+ const key = `${level}${source}${message}`;
972
+ if (pending && pending.key === key) {
973
+ pending.repeat += 1;
974
+ return;
975
+ }
976
+ flushPending();
977
+ emitPayload(payload);
978
+ pending = { key, payload, repeat: 0 };
979
+ }
980
+ catch {
981
+ // capture must never break the host page
982
+ }
983
+ };
984
+ const originals = {};
985
+ const wrappers = {};
986
+ for (const level of CAPTURE_CONSOLE_LEVELS) {
987
+ const original = console[level];
988
+ if (typeof original !== "function")
989
+ continue;
990
+ originals[level] = original;
991
+ const wrapper = (...args) => {
992
+ original.apply(console, args);
993
+ try {
994
+ capture(level, "console", args);
995
+ }
996
+ catch {
997
+ // never throw from the wrapper
998
+ }
999
+ };
1000
+ wrappers[level] = wrapper;
1001
+ console[level] = wrapper;
1002
+ }
1003
+ const onWindowError = (event) => {
1004
+ try {
1005
+ const error = event?.error;
1006
+ capture("error", "window-error", [error instanceof Error ? error : (event?.message ?? "Error")], error instanceof Error ? error.stack : undefined);
1007
+ }
1008
+ catch {
1009
+ // never throw from the listener
1010
+ }
1011
+ };
1012
+ const onUnhandledRejection = (event) => {
1013
+ try {
1014
+ const reason = event?.reason;
1015
+ capture("error", "unhandledrejection", [reason instanceof Error ? reason : reason], reason instanceof Error ? reason.stack : undefined);
1016
+ }
1017
+ catch {
1018
+ // never throw from the listener
1019
+ }
1020
+ };
1021
+ window.addEventListener("error", onWindowError);
1022
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
1023
+ return () => {
1024
+ try {
1025
+ flushPending();
1026
+ }
1027
+ catch {
1028
+ // best-effort duplicate flush
1029
+ }
1030
+ stopped = true;
1031
+ for (const level of CAPTURE_CONSOLE_LEVELS) {
1032
+ const original = originals[level];
1033
+ // Restore only what we installed: if another library patched on top of
1034
+ // our wrapper, leave the current function in place.
1035
+ if (original && console[level] === wrappers[level]) {
1036
+ console[level] = original;
1037
+ }
1038
+ }
1039
+ window.removeEventListener("error", onWindowError);
1040
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
1041
+ };
1042
+ }
1043
+ function captureRequestUrl(input) {
1044
+ if (typeof input === "string")
1045
+ return input;
1046
+ if (typeof URL !== "undefined" && input instanceof URL) {
1047
+ return input.toString();
1048
+ }
1049
+ if (input && typeof input === "object" && "url" in input) {
1050
+ const url = input.url;
1051
+ if (typeof url === "string")
1052
+ return url;
1053
+ }
1054
+ return "";
1055
+ }
1056
+ function captureRequestMethod(input, init) {
1057
+ const initMethod = init?.method;
1058
+ if (typeof initMethod === "string" && initMethod) {
1059
+ return initMethod.toUpperCase();
1060
+ }
1061
+ if (input && typeof input === "object" && "method" in input) {
1062
+ const method = input.method;
1063
+ if (typeof method === "string" && method)
1064
+ return method.toUpperCase();
1065
+ }
1066
+ return "GET";
1067
+ }
1068
+ /**
1069
+ * Requests the recorder must never record: its own ingest endpoint and the
1070
+ * core analytics tracking endpoint (either would create a flush -> event ->
1071
+ * flush feedback loop), plus non-network schemes.
1072
+ */
1073
+ function isCaptureExcludedUrl(rawUrl, ingestEndpoint) {
1074
+ const trimmed = rawUrl.trim();
1075
+ if (!trimmed)
1076
+ return true;
1077
+ const lower = trimmed.toLowerCase();
1078
+ if (lower.startsWith("data:") ||
1079
+ lower.startsWith("blob:") ||
1080
+ lower.startsWith("about:")) {
1081
+ return true;
1082
+ }
1083
+ try {
1084
+ const resolved = new URL(trimmed, window.location.href);
1085
+ const ingest = new URL(ingestEndpoint, window.location.href);
1086
+ if (resolved.origin === ingest.origin &&
1087
+ resolved.pathname === ingest.pathname) {
1088
+ return true;
1089
+ }
1090
+ if (resolved.pathname.endsWith("/api/analytics/replay"))
1091
+ return true;
1092
+ if (resolved.pathname.endsWith("/api/analytics/track"))
1093
+ return true;
1094
+ }
1095
+ catch {
1096
+ // Unresolvable URL -- skip capture rather than risk recording junk.
1097
+ return true;
1098
+ }
1099
+ return false;
1100
+ }
1101
+ function installNetworkCapture(state, captureOptions) {
1102
+ const options = state.options;
1103
+ if (!options)
1104
+ return () => { };
1105
+ const ingestEndpoint = options.endpoint;
1106
+ let emitted = 0;
1107
+ let stopped = false;
1108
+ const emitPayload = (payload) => {
1109
+ if (stopped)
1110
+ return;
1111
+ if (emitted >= captureOptions.maxEvents) {
1112
+ stopped = true;
1113
+ emitReplayCustomEvent(state, SESSION_REPLAY_NETWORK_EVENT_TAG, {
1114
+ message: "session replay network capture truncated",
1115
+ truncated: true,
1116
+ });
1117
+ return;
1118
+ }
1119
+ emitted += 1;
1120
+ emitReplayCustomEvent(state, SESSION_REPLAY_NETWORK_EVENT_TAG, payload);
1121
+ };
1122
+ const recordRequest = (api, method, rawUrl, status, ok, durationMs, error) => {
1123
+ if (stopped)
1124
+ return;
1125
+ try {
1126
+ if (isCaptureExcludedUrl(rawUrl, ingestEndpoint))
1127
+ return;
1128
+ const absolute = new URL(rawUrl, window.location.href).toString();
1129
+ const url = scrubUrl(absolute) ?? absolute;
1130
+ emitPayload({
1131
+ api,
1132
+ method: method.toUpperCase(),
1133
+ url,
1134
+ status,
1135
+ ok,
1136
+ durationMs: Math.max(0, Math.round(durationMs)),
1137
+ ...(error
1138
+ ? {
1139
+ error: truncateCaptureText(redactCaptureText(error), MAX_CONSOLE_MESSAGE_LENGTH),
1140
+ }
1141
+ : {}),
1142
+ });
1143
+ }
1144
+ catch {
1145
+ // capture must never break the host page
1146
+ }
1147
+ };
1148
+ const restores = [];
1149
+ if (typeof window.fetch === "function") {
1150
+ const originalFetch = window.fetch;
1151
+ const wrappedFetch = function (input, init) {
1152
+ const self = this ?? window;
1153
+ if (stopped || replayCaptureInternal) {
1154
+ return originalFetch.call(self, input, init);
1155
+ }
1156
+ let method = "GET";
1157
+ let url = "";
1158
+ let skip = true;
1159
+ try {
1160
+ method = captureRequestMethod(input, init);
1161
+ url = captureRequestUrl(input);
1162
+ skip = isCaptureExcludedUrl(url, ingestEndpoint);
1163
+ }
1164
+ catch {
1165
+ skip = true;
1166
+ }
1167
+ if (skip) {
1168
+ return originalFetch.call(self, input, init);
1169
+ }
1170
+ const startedAt = performance.now();
1171
+ // Call the original exactly as the page did; never read bodies, never
1172
+ // replace the response, propagate rejections untouched.
1173
+ const result = originalFetch.call(self, input, init);
1174
+ if (!result || typeof result.then !== "function") {
1175
+ return result;
1176
+ }
1177
+ return result.then((response) => {
1178
+ try {
1179
+ recordRequest("fetch", method, url, response.status, response.ok, performance.now() - startedAt);
1180
+ }
1181
+ catch {
1182
+ // never affect the caller
1183
+ }
1184
+ return response;
1185
+ }, (error) => {
1186
+ try {
1187
+ recordRequest("fetch", method, url, 0, false, performance.now() - startedAt, error instanceof Error ? error.message : String(error));
1188
+ }
1189
+ catch {
1190
+ // never affect the caller
1191
+ }
1192
+ throw error;
1193
+ });
1194
+ };
1195
+ window.fetch = wrappedFetch;
1196
+ restores.push(() => {
1197
+ // Restore only what we installed (another lib may have patched on top).
1198
+ if (window.fetch === wrappedFetch) {
1199
+ window.fetch = originalFetch;
1200
+ }
1201
+ });
1202
+ }
1203
+ if (typeof XMLHttpRequest !== "undefined" && XMLHttpRequest.prototype) {
1204
+ const originalOpen = XMLHttpRequest.prototype.open;
1205
+ const originalSend = XMLHttpRequest.prototype.send;
1206
+ const xhrInfo = new WeakMap();
1207
+ const wrappedOpen = function (method, url, async, username, password) {
1208
+ try {
1209
+ xhrInfo.set(this, { method: String(method), url: String(url) });
1210
+ }
1211
+ catch {
1212
+ // never throw from the wrapper
1213
+ }
1214
+ return originalOpen.call(this, method, url, async ?? true, username, password);
1215
+ };
1216
+ const wrappedSend = function (body) {
1217
+ try {
1218
+ const info = xhrInfo.get(this);
1219
+ if (info && !stopped && !replayCaptureInternal) {
1220
+ const startedAt = performance.now();
1221
+ let errorMessage;
1222
+ const markError = (message) => () => {
1223
+ errorMessage = message;
1224
+ };
1225
+ this.addEventListener("error", markError("XMLHttpRequest failed"), {
1226
+ once: true,
1227
+ });
1228
+ this.addEventListener("abort", markError("XMLHttpRequest aborted"), {
1229
+ once: true,
1230
+ });
1231
+ this.addEventListener("timeout", markError("XMLHttpRequest timed out"), { once: true });
1232
+ this.addEventListener("loadend", () => {
1233
+ const status = typeof this.status === "number" ? this.status : 0;
1234
+ recordRequest("xhr", info.method, info.url, errorMessage ? 0 : status, !errorMessage && status >= 200 && status < 300, performance.now() - startedAt, errorMessage);
1235
+ xhrInfo.delete(this);
1236
+ }, { once: true });
1237
+ }
1238
+ }
1239
+ catch {
1240
+ // never throw from the wrapper
1241
+ }
1242
+ return originalSend.call(this, body ?? null);
1243
+ };
1244
+ XMLHttpRequest.prototype.open =
1245
+ wrappedOpen;
1246
+ XMLHttpRequest.prototype.send =
1247
+ wrappedSend;
1248
+ restores.push(() => {
1249
+ if (XMLHttpRequest.prototype.open ===
1250
+ wrappedOpen) {
1251
+ XMLHttpRequest.prototype.open = originalOpen;
1252
+ }
1253
+ if (XMLHttpRequest.prototype.send ===
1254
+ wrappedSend) {
1255
+ XMLHttpRequest.prototype.send = originalSend;
1256
+ }
1257
+ });
1258
+ }
1259
+ return () => {
1260
+ stopped = true;
1261
+ for (const restore of restores) {
1262
+ try {
1263
+ restore();
1264
+ }
1265
+ catch {
1266
+ // best-effort restore
1267
+ }
1268
+ }
1269
+ };
1270
+ }
1271
+ function installCaptureInterceptors(state) {
1272
+ const options = state.options;
1273
+ if (!options || state.restoreCaptures || !state.addCustomEvent)
1274
+ return;
1275
+ if (!options.console && !options.network)
1276
+ return;
1277
+ const restores = [];
1278
+ try {
1279
+ if (options.console) {
1280
+ restores.push(installConsoleCapture(state, options.console));
1281
+ }
1282
+ if (options.network) {
1283
+ restores.push(installNetworkCapture(state, options.network));
1284
+ }
1285
+ }
1286
+ catch {
1287
+ // keep whatever installed cleanly; restores below still uninstall it
1288
+ }
1289
+ if (restores.length === 0)
1290
+ return;
1291
+ state.restoreCaptures = () => {
1292
+ state.restoreCaptures = null;
1293
+ for (const restore of restores) {
1294
+ try {
1295
+ restore();
1296
+ }
1297
+ catch {
1298
+ // best-effort restore
1299
+ }
1300
+ }
1301
+ };
1302
+ }
749
1303
  export async function startSessionReplay(options = {}) {
750
1304
  if (options.enabled === false)
751
1305
  return { started: false, reason: "disabled" };
@@ -873,6 +1427,11 @@ async function startSessionReplayRecorder(state, normalized, sessionId, sampled,
873
1427
  state.maxDurationTimer = window.setTimeout(() => stopSessionReplay("max-duration"), normalized.maxDurationMs);
874
1428
  installUrlMonitor(state);
875
1429
  installLifecycleListeners(state);
1430
+ state.addCustomEvent =
1431
+ typeof rrweb.record.addCustomEvent === "function"
1432
+ ? rrweb.record.addCustomEvent
1433
+ : null;
1434
+ installCaptureInterceptors(state);
876
1435
  return {
877
1436
  started: true,
878
1437
  replayId: state.replayId,
@@ -881,6 +1440,14 @@ async function startSessionReplayRecorder(state, normalized, sessionId, sampled,
881
1440
  };
882
1441
  }
883
1442
  catch {
1443
+ try {
1444
+ state.restoreCaptures?.();
1445
+ }
1446
+ catch {
1447
+ // best-effort interceptor teardown
1448
+ }
1449
+ state.restoreCaptures = null;
1450
+ state.addCustomEvent = null;
884
1451
  state.active = false;
885
1452
  state.options = null;
886
1453
  state.replayId = null;
@@ -893,7 +1460,18 @@ export async function stopSessionReplay(reason = "manual") {
893
1460
  const state = getState();
894
1461
  if (!state.active)
895
1462
  return;
1463
+ // Restore console/fetch/XHR before tearing down the recorder: the restore
1464
+ // flushes any pending collapsed console duplicate, which must still be able
1465
+ // to emit through rrweb while it is recording.
1466
+ try {
1467
+ state.restoreCaptures?.();
1468
+ }
1469
+ catch {
1470
+ // best-effort interceptor teardown
1471
+ }
1472
+ state.restoreCaptures = null;
896
1473
  state.active = false;
1474
+ state.addCustomEvent = null;
897
1475
  try {
898
1476
  state.stopRecorder?.();
899
1477
  }