@agent-native/core 0.98.0 → 0.98.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 +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +2 -2
- package/corpus/core/src/action-change-marker.ts +8 -1
- package/corpus/core/src/changelog/parse.ts +8 -3
- package/corpus/core/src/client/AgentPanel.tsx +2 -3
- package/corpus/core/src/client/AssistantChat.tsx +83 -39
- package/corpus/core/src/client/analytics.ts +29 -0
- package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +16 -2
- package/corpus/core/src/client/chat-model-groups.ts +28 -0
- package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +442 -85
- package/corpus/core/src/client/composer/TiptapComposer.tsx +46 -29
- package/corpus/core/src/client/composer/VoiceButton.tsx +2 -1
- package/corpus/core/src/client/composer/index.ts +5 -0
- package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +1206 -67
- package/corpus/core/src/client/index.ts +5 -0
- package/corpus/core/src/client/progress/RunsTray.tsx +13 -1
- package/corpus/core/src/client/settings/SecretsSection.tsx +387 -266
- package/corpus/core/src/client/settings/SettingsPanel.tsx +117 -38
- package/corpus/core/src/client/use-action.ts +12 -0
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +8 -1
- package/corpus/core/src/client/use-agent-engine-configured.ts +17 -3
- package/corpus/core/src/client/use-db-sync.ts +84 -34
- package/corpus/core/src/client/use-pausing-interval.ts +14 -4
- package/corpus/core/src/integrations/scope-store.ts +1 -1
- package/corpus/core/src/integrations/usage-budget-store.ts +1 -1
- package/corpus/core/src/localization/default-messages.ts +44 -1
- package/corpus/core/src/mcp-client/workspace-servers.ts +4 -4
- package/corpus/core/src/secrets/crypto.ts +28 -8
- package/corpus/core/src/server/action-change.ts +5 -1
- package/corpus/core/src/server/action-routes.ts +8 -0
- package/corpus/core/src/server/poll.ts +1 -0
- package/corpus/core/src/server/realtime-voice.ts +299 -13
- package/corpus/core/src/styles/agent-native.css +2 -2
- package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
- package/corpus/core/src/vite/client.ts +9 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +10 -5
- package/corpus/templates/analytics/AGENTS.md +9 -13
- package/corpus/templates/analytics/actions/navigate.ts +59 -5
- package/corpus/templates/analytics/actions/view-screen.ts +3 -6
- package/corpus/templates/analytics/app/components/ProviderCorpusJobNotifier.tsx +20 -3
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +2 -1
- package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -6
- package/corpus/templates/analytics/app/global.css +19 -20
- package/corpus/templates/analytics/app/lib/provider-corpus-job-sync.ts +26 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +35 -341
- package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +38 -7
- package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +1 -1
- package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +3 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +333 -272
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +72 -17
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +297 -108
- package/corpus/templates/analytics/app/root.tsx +28 -8
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +2 -1
- package/corpus/templates/analytics/changelog/2026-06-25-referrer-dashboard-panels-show-external-domains.md +2 -1
- package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-08-dashboard-history-and-chat-context.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-11-analytics-no-longer-repeatedly-restarts-background-requests-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-analytics-only-asks-for-replay-storage-when-you-enable-sessi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-charts-now-show-their-loading-skeleton-while-refreshing.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-realtime-voice-now-shows-a-clear-connection-indicator-comple.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-replay-event-counts-stay-hidden-until-recordings-finish-load.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-replay-storage-key-mismatches-show-actionable-setup-guidance.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-replay-timelines-group-continuous-scrolling-and-dev-tools-co.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-filters-now-stay-compact-in-a-settings-menu-at-mediu.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replay-console-errors-now-link-to-matching-issues-or.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replays-load-large-recordings-faster-and-surface-fai.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replays-preserve-recorded-styles-and-viewport-layout.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replays-stay-centered-instead-of-clipping-wide-recor.md +6 -0
- package/corpus/templates/analytics/package.json +1 -1
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -303
- package/corpus/templates/analytics/server/handlers/session-replay.ts +57 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +16 -3
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +12 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +208 -2
- package/corpus/templates/analytics/server/plugins/onboarding.ts +7 -79
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks.get.ts +1 -0
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/assets/app/global.css +4 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +0 -1
- package/corpus/templates/assets/app/routes/library.tsx +0 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/brain/app/global.css +5 -5
- package/corpus/templates/brain/app/root.tsx +0 -3
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/calendar/app/root.tsx +1 -0
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +74 -8
- package/corpus/templates/clips/app/hooks/use-library.ts +12 -25
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +20 -22
- package/corpus/templates/clips/changelog/2026-07-11-generated-titles-no-longer-cause-continuous-background-refreshes.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-11-s3-storage-setup-stays-collapsed-until-you-choose-it-as-the-.md +6 -0
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/content/app/hooks/use-comments.ts +0 -1
- package/corpus/templates/content/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
- package/corpus/templates/design/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -2
- package/corpus/templates/macros/app/root.tsx +0 -3
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/mail/app/hooks/use-compose-state.ts +1 -0
- package/corpus/templates/mail/app/root.tsx +1 -12
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +5 -2
- package/corpus/templates/plan/app/hooks/use-plans.ts +6 -13
- package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -27
- package/corpus/templates/plan/changelog/2026-07-11-plan-refreshes-only-when-content-changes.md +6 -0
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +4 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +1 -0
- package/corpus/templates/slides/app/hooks/use-slide-comments.ts +7 -31
- package/corpus/templates/slides/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
- package/dist/action-change-marker.d.ts +1 -0
- package/dist/action-change-marker.d.ts.map +1 -1
- package/dist/action-change-marker.js +7 -1
- package/dist/action-change-marker.js.map +1 -1
- package/dist/changelog/parse.d.ts +4 -2
- package/dist/changelog/parse.d.ts.map +1 -1
- package/dist/changelog/parse.js +5 -3
- package/dist/changelog/parse.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +7 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +62 -40
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +26 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/blocks/library/wireframe-kit.d.ts +2 -0
- package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
- package/dist/client/blocks/library/wireframe-kit.js +9 -2
- package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
- package/dist/client/chat-model-groups.d.ts.map +1 -1
- package/dist/client/chat-model-groups.js +24 -0
- package/dist/client/chat-model-groups.js.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts +40 -4
- package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.js +172 -42
- package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +4 -0
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +47 -29
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/VoiceButton.d.ts.map +1 -1
- package/dist/client/composer/VoiceButton.js +2 -2
- package/dist/client/composer/VoiceButton.js.map +1 -1
- package/dist/client/composer/index.d.ts +2 -2
- package/dist/client/composer/index.d.ts.map +1 -1
- package/dist/client/composer/index.js +1 -1
- package/dist/client/composer/index.js.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.d.ts +100 -3
- package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.js +952 -51
- package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/progress/RunsTray.d.ts.map +1 -1
- package/dist/client/progress/RunsTray.js +10 -1
- package/dist/client/progress/RunsTray.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts +2 -3
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +46 -22
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +9 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +46 -12
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-action.d.ts +2 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +10 -0
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +2 -0
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +7 -1
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +17 -3
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +8 -2
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +57 -32
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-pausing-interval.d.ts.map +1 -1
- package/dist/client/use-pausing-interval.js +16 -4
- package/dist/client/use-pausing-interval.js.map +1 -1
- package/dist/integrations/scope-store.js +1 -1
- package/dist/integrations/scope-store.js.map +1 -1
- package/dist/integrations/usage-budget-store.js +1 -1
- package/dist/integrations/usage-budget-store.js.map +1 -1
- package/dist/localization/default-messages.d.ts +41 -1
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +41 -1
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp-client/workspace-servers.js +4 -4
- package/dist/mcp-client/workspace-servers.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/crypto.d.ts +7 -6
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +30 -11
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-change.d.ts +1 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +4 -1
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +5 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +1 -0
- package/dist/server/poll.js.map +1 -1
- package/dist/server/realtime-voice.d.ts +7 -0
- package/dist/server/realtime-voice.d.ts.map +1 -1
- package/dist/server/realtime-voice.js +222 -12
- package/dist/server/realtime-voice.js.map +1 -1
- package/dist/styles/agent-native.css +2 -2
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +6 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +2 -2
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
useT,
|
|
9
9
|
} from "@agent-native/core/client";
|
|
10
10
|
import { SESSION_REPLAY_AGENT_ACCESS_PARAM } from "@shared/session-replay-agent-access";
|
|
11
|
+
import {
|
|
12
|
+
isFailedSessionReplayNetworkStatus,
|
|
13
|
+
SESSION_REPLAY_CONSOLE_EVENT_TAG,
|
|
14
|
+
SESSION_REPLAY_NETWORK_EVENT_TAG,
|
|
15
|
+
} from "@shared/session-replay-diagnostics";
|
|
11
16
|
import {
|
|
12
17
|
IconArrowLeft,
|
|
13
18
|
IconCheck,
|
|
@@ -58,6 +63,7 @@ import { getIdToken } from "@/lib/auth";
|
|
|
58
63
|
import { cn } from "@/lib/utils";
|
|
59
64
|
|
|
60
65
|
import { extractReplayDiagnostics } from "./session-replay-devtools";
|
|
66
|
+
import type { ReplayDevToolsDiagnostics } from "./session-replay-devtools";
|
|
61
67
|
import {
|
|
62
68
|
type SessionIssueMatch,
|
|
63
69
|
SessionDevToolsPanel,
|
|
@@ -156,15 +162,16 @@ type ReplayViewportDimensions = {
|
|
|
156
162
|
|
|
157
163
|
const DEFAULT_PLAYER_WIDTH = 1024;
|
|
158
164
|
const DEFAULT_PLAYER_HEIGHT = 640;
|
|
159
|
-
const MIN_REPLAY_DISPLAY_ASPECT_RATIO = 0.45;
|
|
160
|
-
const MAX_REPLAY_DISPLAY_ASPECT_RATIO = 3;
|
|
161
165
|
const DEFAULT_SPEED = 2;
|
|
162
166
|
const SPEED_OPTIONS = [0.5, 1, 2, 4, 8];
|
|
163
167
|
const SKIP_STEP_MS = 5000;
|
|
164
168
|
const MIN_IDLE_SKIP_MS = 8000;
|
|
165
169
|
const IDLE_EDGE_PAD_MS = 1200;
|
|
166
|
-
const
|
|
170
|
+
const REPLAY_CHUNK_BATCH_MAX_CHUNKS = 20;
|
|
171
|
+
const REPLAY_CHUNK_BATCH_MAX_DECLARED_BYTES = 4 * 1024 * 1024;
|
|
172
|
+
const REPLAY_CHUNK_BATCH_FETCH_CONCURRENCY = 3;
|
|
167
173
|
const REPLAY_CHUNK_UNAVAILABLE_MESSAGE = "Session replay chunk is unavailable";
|
|
174
|
+
const SCROLL_MARKER_BURST_MS = 1_000;
|
|
168
175
|
const DEFAULT_DEVTOOLS_HEIGHT = 220;
|
|
169
176
|
const MIN_STAGE_HEIGHT_PX = 240;
|
|
170
177
|
const SCRUBBER_MARKER_LIMIT = 500;
|
|
@@ -174,16 +181,6 @@ const TIMELINE_FOLLOW_PAUSE_MS = 4000;
|
|
|
174
181
|
const SUPPRESS_OVERLAYS_CSS = [
|
|
175
182
|
"[data-radix-popper-content-wrapper], .Toastify, [class*='toast'], [class*='Toast'], [class*='Snackbar'] { display: none !important; }",
|
|
176
183
|
];
|
|
177
|
-
const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
|
|
178
|
-
const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
|
|
179
|
-
|
|
180
|
-
const EMPTY_DIAGNOSTICS: ReturnType<typeof extractReplayDiagnostics> = {
|
|
181
|
-
console: [],
|
|
182
|
-
network: [],
|
|
183
|
-
consoleErrorCount: 0,
|
|
184
|
-
networkFailedCount: 0,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
184
|
type ReplayConsoleDiagnostics = ReturnType<
|
|
188
185
|
typeof extractReplayDiagnostics
|
|
189
186
|
>["console"];
|
|
@@ -237,18 +234,16 @@ const INCREMENTAL_SOURCE = {
|
|
|
237
234
|
} as const;
|
|
238
235
|
|
|
239
236
|
const MOUSE_INTERACTION = {
|
|
237
|
+
MouseUp: 0,
|
|
238
|
+
MouseDown: 1,
|
|
240
239
|
Click: 2,
|
|
240
|
+
ContextMenu: 3,
|
|
241
241
|
DblClick: 4,
|
|
242
242
|
Focus: 5,
|
|
243
|
+
TouchStart: 7,
|
|
244
|
+
TouchEnd: 9,
|
|
243
245
|
} as const;
|
|
244
246
|
|
|
245
|
-
const INTERACTION_SOURCES = new Set<number>([
|
|
246
|
-
INCREMENTAL_SOURCE.MouseInteraction,
|
|
247
|
-
INCREMENTAL_SOURCE.Scroll,
|
|
248
|
-
INCREMENTAL_SOURCE.Input,
|
|
249
|
-
INCREMENTAL_SOURCE.TouchMove,
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
247
|
export default function SessionDetailPage() {
|
|
253
248
|
const t = useT();
|
|
254
249
|
const { recordingId = "" } = useParams();
|
|
@@ -506,9 +501,13 @@ function ReplayPlayer({
|
|
|
506
501
|
const scrubbingRef = useRef(false);
|
|
507
502
|
const scrubResumePlayingRef = useRef(false);
|
|
508
503
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
504
|
+
// Keep the stage in rrweb's coordinate system. Clamping only this outer
|
|
505
|
+
// wrapper while rrweb keeps its raw iframe dimensions clips and stretches
|
|
506
|
+
// wide recordings because the two layers no longer share a viewport.
|
|
507
|
+
const playerWidth =
|
|
508
|
+
streamedDims?.width ?? initialDims?.width ?? DEFAULT_PLAYER_WIDTH;
|
|
509
|
+
const playerHeight =
|
|
510
|
+
streamedDims?.height ?? initialDims?.height ?? DEFAULT_PLAYER_HEIGHT;
|
|
512
511
|
const skipRanges = useMemo(() => buildIdleSkipRanges(events), [events]);
|
|
513
512
|
const skipRangesRef = useLiveRef(skipRanges);
|
|
514
513
|
const skipInactiveRef = useLiveRef(skipInactive);
|
|
@@ -520,13 +519,11 @@ function ReplayPlayer({
|
|
|
520
519
|
() => currentUrlAt(events, currentTime),
|
|
521
520
|
[events, currentTime],
|
|
522
521
|
);
|
|
523
|
-
const diagnostics = useMemo(
|
|
524
|
-
|
|
525
|
-
|
|
522
|
+
const diagnostics = useMemo(() => extractReplayDiagnostics(events), [events]);
|
|
523
|
+
const devToolsIssueCount = replayDevToolsIssueCount(
|
|
524
|
+
diagnostics,
|
|
525
|
+
response.isComplete,
|
|
526
526
|
);
|
|
527
|
-
const devToolsIssueCount = devToolsOpen
|
|
528
|
-
? diagnostics.consoleErrorCount + diagnostics.networkFailedCount
|
|
529
|
-
: response.recording.errorCount;
|
|
530
527
|
|
|
531
528
|
// Resolve captured console errors in this replay to their Sentry-style issue
|
|
532
529
|
// groups (one batched, access-scoped call, server-computed fingerprints) so
|
|
@@ -572,6 +569,7 @@ function ReplayPlayer({
|
|
|
572
569
|
if (!el) return;
|
|
573
570
|
const update = () => {
|
|
574
571
|
const next = Math.min(
|
|
572
|
+
1,
|
|
575
573
|
el.clientWidth / playerWidth,
|
|
576
574
|
el.clientHeight / playerHeight,
|
|
577
575
|
);
|
|
@@ -879,10 +877,13 @@ function ReplayPlayer({
|
|
|
879
877
|
|
|
880
878
|
return (
|
|
881
879
|
<TooltipProvider>
|
|
882
|
-
<div
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
880
|
+
<div
|
|
881
|
+
ref={playerShellRef}
|
|
882
|
+
className="flex min-h-0 min-w-0 flex-1 flex-col"
|
|
883
|
+
>
|
|
884
|
+
<Card className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
885
|
+
<CardContent className="flex min-h-0 min-w-0 flex-1 flex-col p-0">
|
|
886
|
+
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-muted/20 p-2">
|
|
886
887
|
{currentUrl ? (
|
|
887
888
|
<div
|
|
888
889
|
className="flex h-8 shrink-0 items-center rounded-t-md border border-b-0 bg-background px-3 font-mono text-xs text-muted-foreground"
|
|
@@ -1042,8 +1043,10 @@ function ReplayPlayer({
|
|
|
1042
1043
|
"inline-flex h-8 items-center gap-1.5 rounded-md border px-2 text-xs font-medium transition-colors hover:bg-muted",
|
|
1043
1044
|
devToolsOpen &&
|
|
1044
1045
|
"border-primary/40 bg-primary/10 text-primary",
|
|
1046
|
+
!response.isComplete && "cursor-not-allowed opacity-50",
|
|
1045
1047
|
)}
|
|
1046
1048
|
onClick={() => setDevToolsOpen((value) => !value)}
|
|
1049
|
+
disabled={!response.isComplete}
|
|
1047
1050
|
aria-pressed={devToolsOpen}
|
|
1048
1051
|
aria-expanded={devToolsOpen}
|
|
1049
1052
|
>
|
|
@@ -1061,12 +1064,14 @@ function ReplayPlayer({
|
|
|
1061
1064
|
) : null}
|
|
1062
1065
|
</button>
|
|
1063
1066
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1067
|
+
{response.isComplete ? (
|
|
1068
|
+
<span className="ms-auto hidden text-xs text-muted-foreground lg:inline">
|
|
1069
|
+
{t("sessions.replayEventCount", {
|
|
1070
|
+
events: String(response.eventCount),
|
|
1071
|
+
})}
|
|
1072
|
+
{response.truncated ? ` ${t("sessions.truncated")}` : ""}
|
|
1073
|
+
</span>
|
|
1074
|
+
) : null}
|
|
1070
1075
|
</div>
|
|
1071
1076
|
|
|
1072
1077
|
{devToolsOpen ? (
|
|
@@ -1078,6 +1083,7 @@ function ReplayPlayer({
|
|
|
1078
1083
|
onHeightChange={setDevToolsHeight}
|
|
1079
1084
|
onSeek={(ms) => seek(ms, true)}
|
|
1080
1085
|
issueMatches={issueMatches}
|
|
1086
|
+
issueMatching={issueMatchQuery.isFetching}
|
|
1081
1087
|
/>
|
|
1082
1088
|
) : null}
|
|
1083
1089
|
|
|
@@ -1464,7 +1470,9 @@ function useSessionReplayPlayback(recordingId: string) {
|
|
|
1464
1470
|
let loadedCount = 0;
|
|
1465
1471
|
let loadedBytes = 0;
|
|
1466
1472
|
let unavailableChunks = 0;
|
|
1467
|
-
|
|
1473
|
+
const chunkIndexBySeq = new Map(
|
|
1474
|
+
manifest.chunks.map((chunk, index) => [chunk.seq, index]),
|
|
1475
|
+
);
|
|
1468
1476
|
|
|
1469
1477
|
const publish = (force = false) => {
|
|
1470
1478
|
const complete = loadedCount >= manifest.chunks.length;
|
|
@@ -1513,31 +1521,20 @@ function useSessionReplayPlayback(recordingId: string) {
|
|
|
1513
1521
|
|
|
1514
1522
|
publish();
|
|
1515
1523
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
const chunk
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1524
|
+
await fetchReplayChunks(
|
|
1525
|
+
manifest.chunks,
|
|
1526
|
+
{ agentAccessToken },
|
|
1527
|
+
(batch) => {
|
|
1528
|
+
for (const chunk of batch) {
|
|
1529
|
+
const index = chunkIndexBySeq.get(chunk.seq);
|
|
1530
|
+
if (index === undefined || loadedChunks[index]) continue;
|
|
1531
|
+
loadedChunks[index] = chunk;
|
|
1532
|
+
loadedCount += 1;
|
|
1533
|
+
loadedBytes += manifest.chunks[index].byteLength;
|
|
1534
|
+
if (chunk.unavailable) unavailableChunks += 1;
|
|
1535
|
+
}
|
|
1527
1536
|
if (!cancelled) publish();
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
await Promise.all(
|
|
1532
|
-
Array.from(
|
|
1533
|
-
{
|
|
1534
|
-
length: Math.min(
|
|
1535
|
-
REPLAY_CHUNK_FETCH_CONCURRENCY,
|
|
1536
|
-
manifest.chunks.length,
|
|
1537
|
-
),
|
|
1538
|
-
},
|
|
1539
|
-
worker,
|
|
1540
|
-
),
|
|
1537
|
+
},
|
|
1541
1538
|
);
|
|
1542
1539
|
if (!cancelled) publish(true);
|
|
1543
1540
|
} catch (loadError) {
|
|
@@ -1635,27 +1632,118 @@ async function fetchReplayManifest(
|
|
|
1635
1632
|
async function fetchReplayChunks(
|
|
1636
1633
|
chunks: SessionReplayManifestResponse["chunks"],
|
|
1637
1634
|
options: FetchSessionReplayPlaybackOptions,
|
|
1635
|
+
onBatch?: (chunks: ReplayChunkEvents[]) => void,
|
|
1638
1636
|
): Promise<ReplayChunkEvents[]> {
|
|
1639
1637
|
const results = new Array<ReplayChunkEvents>(chunks.length);
|
|
1638
|
+
const indexBySeq = new Map(chunks.map((chunk, index) => [chunk.seq, index]));
|
|
1639
|
+
const batches = partitionReplayChunkBatches(chunks);
|
|
1640
1640
|
let nextIndex = 0;
|
|
1641
1641
|
|
|
1642
1642
|
async function worker() {
|
|
1643
|
-
while (nextIndex <
|
|
1643
|
+
while (nextIndex < batches.length) {
|
|
1644
1644
|
const index = nextIndex;
|
|
1645
1645
|
nextIndex += 1;
|
|
1646
|
-
|
|
1646
|
+
const loaded = await fetchReplayChunkBatch(batches[index], options);
|
|
1647
|
+
for (const chunk of loaded) {
|
|
1648
|
+
const resultIndex = indexBySeq.get(chunk.seq);
|
|
1649
|
+
if (resultIndex !== undefined) results[resultIndex] = chunk;
|
|
1650
|
+
}
|
|
1651
|
+
onBatch?.(loaded);
|
|
1647
1652
|
}
|
|
1648
1653
|
}
|
|
1649
1654
|
|
|
1650
1655
|
await Promise.all(
|
|
1651
1656
|
Array.from(
|
|
1652
|
-
{
|
|
1657
|
+
{
|
|
1658
|
+
length: Math.min(REPLAY_CHUNK_BATCH_FETCH_CONCURRENCY, batches.length),
|
|
1659
|
+
},
|
|
1653
1660
|
worker,
|
|
1654
1661
|
),
|
|
1655
1662
|
);
|
|
1656
1663
|
return results;
|
|
1657
1664
|
}
|
|
1658
1665
|
|
|
1666
|
+
export function partitionReplayChunkBatches(
|
|
1667
|
+
chunks: SessionReplayManifestResponse["chunks"],
|
|
1668
|
+
): Array<SessionReplayManifestResponse["chunks"]> {
|
|
1669
|
+
const batches: Array<SessionReplayManifestResponse["chunks"]> = [];
|
|
1670
|
+
let batch: SessionReplayManifestResponse["chunks"] = [];
|
|
1671
|
+
let declaredBytes = 0;
|
|
1672
|
+
|
|
1673
|
+
for (const chunk of chunks) {
|
|
1674
|
+
const exceedsChunkLimit = batch.length >= REPLAY_CHUNK_BATCH_MAX_CHUNKS;
|
|
1675
|
+
const exceedsByteLimit =
|
|
1676
|
+
batch.length > 0 &&
|
|
1677
|
+
declaredBytes + chunk.byteLength > REPLAY_CHUNK_BATCH_MAX_DECLARED_BYTES;
|
|
1678
|
+
if (exceedsChunkLimit || exceedsByteLimit) {
|
|
1679
|
+
batches.push(batch);
|
|
1680
|
+
batch = [];
|
|
1681
|
+
declaredBytes = 0;
|
|
1682
|
+
}
|
|
1683
|
+
batch.push(chunk);
|
|
1684
|
+
declaredBytes += chunk.byteLength;
|
|
1685
|
+
}
|
|
1686
|
+
if (batch.length) batches.push(batch);
|
|
1687
|
+
return batches;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
async function fetchReplayChunkBatch(
|
|
1691
|
+
chunks: SessionReplayManifestResponse["chunks"],
|
|
1692
|
+
options: FetchSessionReplayPlaybackOptions,
|
|
1693
|
+
): Promise<ReplayChunkEvents[]> {
|
|
1694
|
+
const recordingId = replayRecordingIdFromChunkPath(chunks[0]?.bytesPath);
|
|
1695
|
+
if (!recordingId) {
|
|
1696
|
+
return Promise.all(chunks.map((chunk) => fetchReplayChunk(chunk, options)));
|
|
1697
|
+
}
|
|
1698
|
+
const query = new URLSearchParams({
|
|
1699
|
+
seqs: chunks.map((chunk) => chunk.seq).join(","),
|
|
1700
|
+
});
|
|
1701
|
+
const response = await fetchReplayApi(
|
|
1702
|
+
`/api/session-replay/recordings/${encodeURIComponent(recordingId)}/chunks?${query}`,
|
|
1703
|
+
options.agentAccessToken,
|
|
1704
|
+
);
|
|
1705
|
+
if (!response.ok) {
|
|
1706
|
+
if (response.status === 404 || response.status === 405) {
|
|
1707
|
+
return Promise.all(
|
|
1708
|
+
chunks.map((chunk) => fetchReplayChunk(chunk, options)),
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
throw await replayFetchError(response);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const payload = (await response.json()) as { chunks?: unknown };
|
|
1715
|
+
const returnedChunks = Array.isArray(payload?.chunks)
|
|
1716
|
+
? payload.chunks.filter(isReplayChunkEvents)
|
|
1717
|
+
: [];
|
|
1718
|
+
const returnedBySeq = new Map(
|
|
1719
|
+
returnedChunks.map((chunk) => [chunk.seq, chunk]),
|
|
1720
|
+
);
|
|
1721
|
+
|
|
1722
|
+
return Promise.all(
|
|
1723
|
+
chunks.map(async (manifestChunk) => {
|
|
1724
|
+
const loaded = returnedBySeq.get(manifestChunk.seq);
|
|
1725
|
+
return loaded ?? fetchReplayChunk(manifestChunk, options);
|
|
1726
|
+
}),
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function replayRecordingIdFromChunkPath(path: string | undefined): string {
|
|
1731
|
+
if (!path) return "";
|
|
1732
|
+
const match = path.match(/\/recordings\/([^/]+)\/chunks\//);
|
|
1733
|
+
return match ? decodeURIComponent(match[1]) : "";
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function isReplayChunkEvents(value: unknown): value is ReplayChunkEvents {
|
|
1737
|
+
return (
|
|
1738
|
+
isRecord(value) &&
|
|
1739
|
+
typeof value.seq === "number" &&
|
|
1740
|
+
typeof value.checksum === "string" &&
|
|
1741
|
+
typeof value.byteLength === "number" &&
|
|
1742
|
+
typeof value.eventCount === "number" &&
|
|
1743
|
+
Array.isArray(value.events)
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1659
1747
|
async function fetchReplayChunk(
|
|
1660
1748
|
chunk: SessionReplayManifestResponse["chunks"][number],
|
|
1661
1749
|
options: FetchSessionReplayPlaybackOptions,
|
|
@@ -1758,7 +1846,7 @@ function useReplayEvents(
|
|
|
1758
1846
|
): AnyReplayEvent[] {
|
|
1759
1847
|
return useMemo(
|
|
1760
1848
|
() =>
|
|
1761
|
-
|
|
1849
|
+
normalizeReplayEvents(
|
|
1762
1850
|
response.chunks
|
|
1763
1851
|
.flatMap((chunk) => chunk.events)
|
|
1764
1852
|
.filter((event) => event && typeof event === "object"),
|
|
@@ -1767,187 +1855,66 @@ function useReplayEvents(
|
|
|
1767
1855
|
);
|
|
1768
1856
|
}
|
|
1769
1857
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
} catch {
|
|
1783
|
-
copy = { ...event };
|
|
1784
|
-
}
|
|
1785
|
-
if (copy.type === RRWEB_EVENT_TYPE.FullSnapshot && isRecord(copy.data)) {
|
|
1786
|
-
const node = sanitizeSerializedNode(copy.data.node);
|
|
1787
|
-
if (!node) return null;
|
|
1788
|
-
copy.data.node = node;
|
|
1789
|
-
}
|
|
1790
|
-
if (
|
|
1791
|
-
copy.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
1792
|
-
copy.data?.source === INCREMENTAL_SOURCE.Mutation &&
|
|
1793
|
-
isRecord(copy.data)
|
|
1794
|
-
) {
|
|
1795
|
-
copy.data = sanitizeMutationData(copy.data);
|
|
1796
|
-
}
|
|
1797
|
-
return copy;
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
function sanitizeMutationData(data: AnyRecord): AnyRecord {
|
|
1801
|
-
const next = { ...data };
|
|
1802
|
-
if (Array.isArray(next.adds)) {
|
|
1803
|
-
next.adds = next.adds
|
|
1804
|
-
.map((add) => {
|
|
1805
|
-
if (!isRecord(add)) return add;
|
|
1806
|
-
const node = sanitizeSerializedNode(add.node);
|
|
1807
|
-
if (!node) return null;
|
|
1808
|
-
return { ...add, node };
|
|
1809
|
-
})
|
|
1810
|
-
.filter(Boolean);
|
|
1811
|
-
}
|
|
1812
|
-
if (Array.isArray(next.attributes)) {
|
|
1813
|
-
next.attributes = next.attributes.map((attributeMutation) => {
|
|
1814
|
-
if (
|
|
1815
|
-
!isRecord(attributeMutation) ||
|
|
1816
|
-
!isRecord(attributeMutation.attributes)
|
|
1817
|
-
) {
|
|
1818
|
-
return attributeMutation;
|
|
1819
|
-
}
|
|
1820
|
-
return {
|
|
1821
|
-
...attributeMutation,
|
|
1822
|
-
attributes: sanitizeAttributes(attributeMutation.attributes),
|
|
1823
|
-
};
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
if (Array.isArray(next.texts)) {
|
|
1827
|
-
next.texts = next.texts.map((textMutation) => {
|
|
1828
|
-
if (!isRecord(textMutation)) return textMutation;
|
|
1829
|
-
const copy = { ...textMutation };
|
|
1830
|
-
if (
|
|
1831
|
-
typeof copy.value === "string" &&
|
|
1832
|
-
containsStylesheetNetworkLoad(copy.value)
|
|
1833
|
-
) {
|
|
1834
|
-
copy.value = sanitizeCssText(copy.value);
|
|
1835
|
-
}
|
|
1836
|
-
if (
|
|
1837
|
-
typeof copy.textContent === "string" &&
|
|
1838
|
-
containsStylesheetNetworkLoad(copy.textContent)
|
|
1839
|
-
) {
|
|
1840
|
-
copy.textContent = sanitizeCssText(copy.textContent);
|
|
1841
|
-
}
|
|
1842
|
-
return copy;
|
|
1843
|
-
});
|
|
1844
|
-
}
|
|
1845
|
-
return next;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
function sanitizeSerializedNode(node: unknown): AnyRecord | null {
|
|
1849
|
-
if (!isRecord(node)) return node as AnyRecord;
|
|
1850
|
-
const next: AnyRecord = { ...node };
|
|
1851
|
-
const tagName =
|
|
1852
|
-
typeof next.tagName === "string" ? next.tagName.toLowerCase() : "";
|
|
1853
|
-
if (next.type === 2 && tagName === "script") {
|
|
1854
|
-
return {
|
|
1855
|
-
...next,
|
|
1856
|
-
tagName: "noscript",
|
|
1857
|
-
attributes: {},
|
|
1858
|
-
childNodes: [],
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
if (
|
|
1862
|
-
next.type === 3 &&
|
|
1863
|
-
typeof next.textContent === "string" &&
|
|
1864
|
-
containsStylesheetNetworkLoad(next.textContent)
|
|
1865
|
-
) {
|
|
1866
|
-
next.textContent = sanitizeCssText(next.textContent);
|
|
1867
|
-
}
|
|
1868
|
-
if (
|
|
1869
|
-
next.type === 2 &&
|
|
1870
|
-
tagName === "link" &&
|
|
1871
|
-
isScriptLikeLink(next.attributes)
|
|
1872
|
-
) {
|
|
1873
|
-
return null;
|
|
1874
|
-
}
|
|
1875
|
-
if (isRecord(next.attributes)) {
|
|
1876
|
-
next.attributes = sanitizeAttributes(next.attributes);
|
|
1877
|
-
}
|
|
1878
|
-
if (Array.isArray(next.childNodes)) {
|
|
1879
|
-
next.childNodes = next.childNodes
|
|
1880
|
-
.map((child) => sanitizeSerializedNode(child))
|
|
1881
|
-
.filter(Boolean);
|
|
1882
|
-
}
|
|
1883
|
-
return next;
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
function containsStylesheetNetworkLoad(value: string): boolean {
|
|
1887
|
-
return /@import\b/i.test(value) || /\burl\s*\(/i.test(value);
|
|
1888
|
-
}
|
|
1858
|
+
const REPLAY_NETWORK_ATTRIBUTES = new Set([
|
|
1859
|
+
"src",
|
|
1860
|
+
"srcset",
|
|
1861
|
+
"poster",
|
|
1862
|
+
"background",
|
|
1863
|
+
"action",
|
|
1864
|
+
"formaction",
|
|
1865
|
+
"data",
|
|
1866
|
+
"ping",
|
|
1867
|
+
"cite",
|
|
1868
|
+
"xlink:href",
|
|
1869
|
+
]);
|
|
1889
1870
|
|
|
1890
|
-
function
|
|
1891
|
-
if (!containsStylesheetNetworkLoad(value)) return value;
|
|
1871
|
+
function stripReplayCssUrls(value: string): string {
|
|
1892
1872
|
return value
|
|
1893
|
-
.replace(/@import\s+(?:url\
|
|
1894
|
-
.replace(
|
|
1873
|
+
.replace(/@import\s+(?:url\()?[^;)]+(?:\))?\s*;?/gi, "")
|
|
1874
|
+
.replace(/url\(\s*[^)]*\)/gi, "none");
|
|
1895
1875
|
}
|
|
1896
1876
|
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
if (normalized === "srcdoc") continue;
|
|
1910
|
-
if (isReplayResourceAttribute(normalized)) continue;
|
|
1911
|
-
if (normalized === "style") {
|
|
1912
|
-
const style = sanitizeCssText(String(value));
|
|
1913
|
-
if (style.trim()) next[key] = style;
|
|
1914
|
-
continue;
|
|
1877
|
+
function sanitizeReplayValue(value: unknown, key?: string): unknown {
|
|
1878
|
+
if (Array.isArray(value)) {
|
|
1879
|
+
return value.map((item) => sanitizeReplayValue(item));
|
|
1880
|
+
}
|
|
1881
|
+
if (!isRecord(value)) {
|
|
1882
|
+
if (typeof value !== "string") return value;
|
|
1883
|
+
const normalizedKey = key?.toLowerCase();
|
|
1884
|
+
if (
|
|
1885
|
+
normalizedKey === "href" ||
|
|
1886
|
+
REPLAY_NETWORK_ATTRIBUTES.has(normalizedKey ?? "")
|
|
1887
|
+
) {
|
|
1888
|
+
return "about:blank";
|
|
1915
1889
|
}
|
|
1916
|
-
if (
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1890
|
+
if (
|
|
1891
|
+
normalizedKey === "style" ||
|
|
1892
|
+
normalizedKey === "_csstext" ||
|
|
1893
|
+
normalizedKey === "csstext"
|
|
1894
|
+
) {
|
|
1895
|
+
return stripReplayCssUrls(value);
|
|
1920
1896
|
}
|
|
1921
|
-
|
|
1897
|
+
return value;
|
|
1922
1898
|
}
|
|
1923
|
-
return next;
|
|
1924
|
-
}
|
|
1925
1899
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
name === "xlink:href" ||
|
|
1932
|
-
name === "poster" ||
|
|
1933
|
-
name === "data" ||
|
|
1934
|
-
name === "action" ||
|
|
1935
|
-
name === "formaction" ||
|
|
1936
|
-
name === "background" ||
|
|
1937
|
-
name === "cite"
|
|
1900
|
+
return Object.fromEntries(
|
|
1901
|
+
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
1902
|
+
entryKey,
|
|
1903
|
+
sanitizeReplayValue(entryValue, entryKey),
|
|
1904
|
+
]),
|
|
1938
1905
|
);
|
|
1939
1906
|
}
|
|
1940
1907
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
(
|
|
1949
|
-
(
|
|
1950
|
-
|
|
1908
|
+
/**
|
|
1909
|
+
* rrweb's iframe sandbox prevents scripts but permits passive resource loads.
|
|
1910
|
+
* Preserve the captured DOM structure and inline styling while neutralizing
|
|
1911
|
+
* URLs that could contact a recorded page or third-party origin.
|
|
1912
|
+
*/
|
|
1913
|
+
export function normalizeReplayEvents(events: unknown[]): AnyReplayEvent[] {
|
|
1914
|
+
return events
|
|
1915
|
+
.filter((event): event is AnyReplayEvent => isRecord(event))
|
|
1916
|
+
.map((event) => sanitizeReplayValue(event) as AnyReplayEvent)
|
|
1917
|
+
.sort((a, b) => Number(a.timestamp ?? 0) - Number(b.timestamp ?? 0));
|
|
1951
1918
|
}
|
|
1952
1919
|
|
|
1953
1920
|
export function buildReplayMarkers(events: AnyReplayEvent[]): ReplayMarker[] {
|
|
@@ -2007,6 +1974,23 @@ export function buildReplayMarkers(events: AnyReplayEvent[]): ReplayMarker[] {
|
|
|
2007
1974
|
["Y", event.data?.y],
|
|
2008
1975
|
]),
|
|
2009
1976
|
});
|
|
1977
|
+
} else if (
|
|
1978
|
+
event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
1979
|
+
event.data?.source === INCREMENTAL_SOURCE.Scroll
|
|
1980
|
+
) {
|
|
1981
|
+
markers.push({
|
|
1982
|
+
id: `scroll-${timestamp}-${markers.length}`,
|
|
1983
|
+
timestamp,
|
|
1984
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
1985
|
+
kind: "custom",
|
|
1986
|
+
label: "Scroll",
|
|
1987
|
+
detail: pointerDetail(event.data),
|
|
1988
|
+
fields: markerFields([
|
|
1989
|
+
["Element id", event.data?.id],
|
|
1990
|
+
["X", event.data?.x],
|
|
1991
|
+
["Y", event.data?.y],
|
|
1992
|
+
]),
|
|
1993
|
+
});
|
|
2010
1994
|
} else if (event.type === RRWEB_EVENT_TYPE.Custom) {
|
|
2011
1995
|
const marker = customReplayMarker(
|
|
2012
1996
|
event,
|
|
@@ -2017,7 +2001,53 @@ export function buildReplayMarkers(events: AnyReplayEvent[]): ReplayMarker[] {
|
|
|
2017
2001
|
if (marker) markers.push(marker);
|
|
2018
2002
|
}
|
|
2019
2003
|
}
|
|
2020
|
-
return
|
|
2004
|
+
return collapseScrollMarkerBursts(
|
|
2005
|
+
markers.sort((a, b) => a.offsetMs - b.offsetMs),
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function collapseScrollMarkerBursts(markers: ReplayMarker[]): ReplayMarker[] {
|
|
2010
|
+
const collapsed: ReplayMarker[] = [];
|
|
2011
|
+
const lastScrollByTarget = new Map<
|
|
2012
|
+
string,
|
|
2013
|
+
{ index: number; timestamp: number }
|
|
2014
|
+
>();
|
|
2015
|
+
for (const marker of markers) {
|
|
2016
|
+
if (marker.label !== "Scroll") {
|
|
2017
|
+
collapsed.push(marker);
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
const target =
|
|
2021
|
+
marker.fields?.find((field) => field.label === "Element id")?.value ??
|
|
2022
|
+
"viewport";
|
|
2023
|
+
const previous = lastScrollByTarget.get(target);
|
|
2024
|
+
if (
|
|
2025
|
+
previous &&
|
|
2026
|
+
marker.timestamp - previous.timestamp <= SCROLL_MARKER_BURST_MS
|
|
2027
|
+
) {
|
|
2028
|
+
// Keep one marker per continuous scroll gesture, using its final
|
|
2029
|
+
// position while retaining the first marker's stable id/time.
|
|
2030
|
+
collapsed[previous.index] = {
|
|
2031
|
+
...marker,
|
|
2032
|
+
id: collapsed[previous.index].id,
|
|
2033
|
+
timestamp: collapsed[previous.index].timestamp,
|
|
2034
|
+
offsetMs: collapsed[previous.index].offsetMs,
|
|
2035
|
+
};
|
|
2036
|
+
previous.timestamp = marker.timestamp;
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
const index = collapsed.push(marker) - 1;
|
|
2040
|
+
lastScrollByTarget.set(target, { index, timestamp: marker.timestamp });
|
|
2041
|
+
}
|
|
2042
|
+
return collapsed;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
export function replayDevToolsIssueCount(
|
|
2046
|
+
diagnostics: ReplayDevToolsDiagnostics,
|
|
2047
|
+
isComplete: boolean,
|
|
2048
|
+
): number {
|
|
2049
|
+
if (!isComplete) return 0;
|
|
2050
|
+
return diagnostics.consoleErrorCount + diagnostics.networkFailedCount;
|
|
2021
2051
|
}
|
|
2022
2052
|
|
|
2023
2053
|
function customReplayMarker(
|
|
@@ -2055,7 +2085,33 @@ function customReplayMarker(
|
|
|
2055
2085
|
}
|
|
2056
2086
|
|
|
2057
2087
|
if (tag === SESSION_REPLAY_NETWORK_EVENT_TAG) {
|
|
2058
|
-
|
|
2088
|
+
const status = Number(payload.status ?? 0);
|
|
2089
|
+
if (
|
|
2090
|
+
payload.ok !== false &&
|
|
2091
|
+
(!Number.isFinite(status) || !isFailedSessionReplayNetworkStatus(status))
|
|
2092
|
+
) {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
const method =
|
|
2096
|
+
typeof payload.method === "string" ? payload.method : undefined;
|
|
2097
|
+
const url = typeof payload.url === "string" ? payload.url : undefined;
|
|
2098
|
+
const error = typeof payload.error === "string" ? payload.error : undefined;
|
|
2099
|
+
return {
|
|
2100
|
+
id: `network-${timestamp}-${index}`,
|
|
2101
|
+
timestamp,
|
|
2102
|
+
offsetMs,
|
|
2103
|
+
kind: "custom",
|
|
2104
|
+
label: "Network error", // i18n-ignore: timeline protocol label.
|
|
2105
|
+
detail: [method, url].filter(Boolean).join(" ") || error,
|
|
2106
|
+
severity: "error",
|
|
2107
|
+
fields: markerFields([
|
|
2108
|
+
["Method", method],
|
|
2109
|
+
["URL", url],
|
|
2110
|
+
["Status", Number.isFinite(status) ? status : undefined],
|
|
2111
|
+
["Error", error],
|
|
2112
|
+
["Duration", payload.durationMs],
|
|
2113
|
+
]),
|
|
2114
|
+
};
|
|
2059
2115
|
}
|
|
2060
2116
|
|
|
2061
2117
|
return {
|
|
@@ -2068,7 +2124,7 @@ function customReplayMarker(
|
|
|
2068
2124
|
};
|
|
2069
2125
|
}
|
|
2070
2126
|
|
|
2071
|
-
function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
|
|
2127
|
+
export function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
|
|
2072
2128
|
const startedAt = replayStartedAt(events);
|
|
2073
2129
|
const interactions: number[] = [];
|
|
2074
2130
|
let lastTimestamp = startedAt;
|
|
@@ -2076,13 +2132,7 @@ function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
|
|
|
2076
2132
|
const timestamp = Number(event.timestamp ?? 0);
|
|
2077
2133
|
if (!Number.isFinite(timestamp) || timestamp <= 0) continue;
|
|
2078
2134
|
lastTimestamp = Math.max(lastTimestamp, timestamp);
|
|
2079
|
-
if (event
|
|
2080
|
-
interactions.push(timestamp);
|
|
2081
|
-
} else if (
|
|
2082
|
-
event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
2083
|
-
typeof event.data?.source === "number" &&
|
|
2084
|
-
INTERACTION_SOURCES.has(event.data.source)
|
|
2085
|
-
) {
|
|
2135
|
+
if (isReplayActivityEvent(event)) {
|
|
2086
2136
|
interactions.push(timestamp);
|
|
2087
2137
|
}
|
|
2088
2138
|
}
|
|
@@ -2107,6 +2157,33 @@ function buildIdleSkipRanges(events: AnyReplayEvent[]): SkipRange[] {
|
|
|
2107
2157
|
return ranges;
|
|
2108
2158
|
}
|
|
2109
2159
|
|
|
2160
|
+
function isReplayActivityEvent(event: AnyReplayEvent): boolean {
|
|
2161
|
+
if (event.type === RRWEB_EVENT_TYPE.Meta) return true;
|
|
2162
|
+
if (event.type !== RRWEB_EVENT_TYPE.IncrementalSnapshot) return false;
|
|
2163
|
+
|
|
2164
|
+
const source = event.data?.source;
|
|
2165
|
+
if (
|
|
2166
|
+
source === INCREMENTAL_SOURCE.Input ||
|
|
2167
|
+
source === INCREMENTAL_SOURCE.Scroll ||
|
|
2168
|
+
source === INCREMENTAL_SOURCE.TouchMove
|
|
2169
|
+
) {
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
if (source !== INCREMENTAL_SOURCE.MouseInteraction) return false;
|
|
2173
|
+
|
|
2174
|
+
const interactionType = event.data?.type;
|
|
2175
|
+
return (
|
|
2176
|
+
interactionType === MOUSE_INTERACTION.MouseUp ||
|
|
2177
|
+
interactionType === MOUSE_INTERACTION.MouseDown ||
|
|
2178
|
+
interactionType === MOUSE_INTERACTION.Click ||
|
|
2179
|
+
interactionType === MOUSE_INTERACTION.ContextMenu ||
|
|
2180
|
+
interactionType === MOUSE_INTERACTION.DblClick ||
|
|
2181
|
+
interactionType === MOUSE_INTERACTION.Focus ||
|
|
2182
|
+
interactionType === MOUSE_INTERACTION.TouchStart ||
|
|
2183
|
+
interactionType === MOUSE_INTERACTION.TouchEnd
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2110
2187
|
function pushIdleRange(
|
|
2111
2188
|
ranges: SkipRange[],
|
|
2112
2189
|
fromTs: number,
|
|
@@ -2195,26 +2272,6 @@ export function normalizeReplayDimensions(
|
|
|
2195
2272
|
};
|
|
2196
2273
|
}
|
|
2197
2274
|
|
|
2198
|
-
export function clampReplayDisplayDimensions(
|
|
2199
|
-
dims: ReplayViewportDimensions | null,
|
|
2200
|
-
): ReplayViewportDimensions | null {
|
|
2201
|
-
if (!dims) return null;
|
|
2202
|
-
const aspect = dims.width / dims.height;
|
|
2203
|
-
if (aspect > MAX_REPLAY_DISPLAY_ASPECT_RATIO) {
|
|
2204
|
-
return {
|
|
2205
|
-
width: Math.round(dims.height * MAX_REPLAY_DISPLAY_ASPECT_RATIO),
|
|
2206
|
-
height: dims.height,
|
|
2207
|
-
};
|
|
2208
|
-
}
|
|
2209
|
-
if (aspect < MIN_REPLAY_DISPLAY_ASPECT_RATIO) {
|
|
2210
|
-
return {
|
|
2211
|
-
width: dims.width,
|
|
2212
|
-
height: Math.round(dims.width / MIN_REPLAY_DISPLAY_ASPECT_RATIO),
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
return dims;
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
2275
|
export function filterReplayMarkers(
|
|
2219
2276
|
markers: ReplayMarker[],
|
|
2220
2277
|
query: string,
|
|
@@ -2237,10 +2294,14 @@ export function filterReplayMarkers(
|
|
|
2237
2294
|
}
|
|
2238
2295
|
|
|
2239
2296
|
function replayStartedAt(events: AnyReplayEvent[]): number {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2297
|
+
let startedAt = Number.POSITIVE_INFINITY;
|
|
2298
|
+
for (const event of events) {
|
|
2299
|
+
const timestamp = Number(event.timestamp);
|
|
2300
|
+
if (Number.isFinite(timestamp) && timestamp > 0) {
|
|
2301
|
+
startedAt = Math.min(startedAt, timestamp);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return Number.isFinite(startedAt) ? startedAt : 0;
|
|
2244
2305
|
}
|
|
2245
2306
|
|
|
2246
2307
|
function replayDuration(events: AnyReplayEvent[]): number {
|