@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +20 -0
- package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
- package/corpus/core/docs/content/tracking.mdx +2 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/action-continuation-guidance.ts +1 -1
- package/corpus/core/src/agent/production-agent.ts +222 -38
- package/corpus/core/src/agent/run-manager.ts +79 -0
- package/corpus/core/src/agent/run-store.ts +122 -8
- package/corpus/core/src/agent/types.ts +13 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
- package/corpus/core/src/client/analytics.ts +2 -0
- package/corpus/core/src/client/error-format.ts +4 -0
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
- package/corpus/core/src/client/session-replay.ts +774 -2
- package/corpus/core/src/client/sse-event-processor.ts +65 -9
- package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
- package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
- package/corpus/core/src/server/core-routes-plugin.ts +91 -4
- package/corpus/core/src/server/http-response-telemetry.ts +189 -0
- package/corpus/core/src/server/self-dispatch.ts +29 -0
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
- package/corpus/templates/analytics/AGENTS.md +13 -0
- package/corpus/templates/analytics/actions/view-screen.ts +25 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
- package/corpus/templates/analytics/app/global.css +55 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
- package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
- package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
- package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
- package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +3 -0
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
- package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
- package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
- package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
- package/corpus/templates/plan/server/plans.ts +26 -9
- package/dist/agent/action-continuation-guidance.js +1 -1
- package/dist/agent/action-continuation-guidance.js.map +1 -1
- package/dist/agent/production-agent.d.ts +9 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +168 -34
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +29 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +73 -0
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +41 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +105 -7
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +13 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +13 -0
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +410 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +2 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/session-replay.d.ts +36 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +579 -1
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +20 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +42 -11
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +14 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +18 -4
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/runtime-diagnostics.d.ts +52 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -0
- package/dist/db/runtime-diagnostics.js +250 -0
- package/dist/db/runtime-diagnostics.js.map +1 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +101 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +17 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +65 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts +4 -0
- package/dist/server/http-response-telemetry.d.ts.map +1 -0
- package/dist/server/http-response-telemetry.js +174 -0
- package/dist/server/http-response-telemetry.js.map +1 -0
- package/dist/server/self-dispatch.d.ts +17 -0
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +11 -0
- package/dist/server/self-dispatch.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/locales/ar-SA/tracking.mdx +55 -0
- package/docs/content/locales/de-DE/tracking.mdx +55 -0
- package/docs/content/locales/es-ES/tracking.mdx +55 -0
- package/docs/content/locales/fr-FR/tracking.mdx +55 -0
- package/docs/content/locales/hi-IN/tracking.mdx +55 -0
- package/docs/content/locales/ja-JP/tracking.mdx +55 -0
- package/docs/content/locales/ko-KR/tracking.mdx +55 -0
- package/docs/content/locales/pt-BR/tracking.mdx +55 -0
- package/docs/content/locales/zh-CN/tracking.mdx +55 -0
- package/docs/content/locales/zh-TW/tracking.mdx +55 -0
- package/docs/content/tracking.mdx +2 -0
- 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
|
|
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
|
}
|