@agent-native/core 0.98.2 → 0.98.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +2 -2
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
- package/corpus/core/src/client/settings/SettingsPanel.tsx +31 -211
- package/corpus/core/src/client/settings/agent-settings-search.ts +219 -0
- package/corpus/core/src/client/settings/index.ts +4 -0
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
- package/corpus/core/src/demo/config.ts +6 -6
- package/corpus/core/src/demo/redact.ts +18 -76
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -4
- package/corpus/templates/analytics/AGENTS.md +6 -6
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +429 -244
- package/corpus/templates/analytics/app/components/layout/command-palette-search.ts +93 -0
- package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +2 -1
- package/corpus/templates/analytics/app/lib/demo-chart-trend.ts +46 -33
- package/corpus/templates/analytics/app/pages/Settings.tsx +8 -45
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +205 -70
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +32 -16
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +126 -0
- package/corpus/templates/analytics/changelog/2026-07-12-command-menu-loading-placeholders-now-stay-below-available-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-command-menu-search-now-prioritizes-the-best-match.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-demo-charts-preserve-source-movement.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-replay-storage-settings-stay-readable-at-narrow-widths.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-replays-preserve-recorded-styling-and-keep-malformed.md +6 -0
- package/dist/agent/production-agent.js +2 -2
- package/dist/agent/production-agent.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/settings/DemoModeSection.d.ts +3 -3
- package/dist/client/settings/DemoModeSection.js +4 -4
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +41 -191
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/agent-settings-search.d.ts +16 -0
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -0
- package/dist/client/settings/agent-settings-search.js +178 -0
- package/dist/client/settings/agent-settings-search.js.map +1 -0
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -0
- package/dist/client/settings/index.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +2 -2
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/config.js +6 -6
- package/dist/demo/config.js.map +1 -1
- package/dist/demo/redact.d.ts +7 -5
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +19 -54
- package/dist/demo/redact.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
|
@@ -26,3 +26,96 @@ export function commandPaletteKeywords(
|
|
|
26
26
|
|
|
27
27
|
return Array.from(variants);
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
function normalizeSearchText(value: string): string {
|
|
31
|
+
return value
|
|
32
|
+
.normalize("NFKD")
|
|
33
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fuzzySubsequenceScore(candidate: string, query: string): number {
|
|
40
|
+
const compactCandidate = candidate.replace(/\s/g, "");
|
|
41
|
+
const compactQuery = query.replace(/\s/g, "");
|
|
42
|
+
let queryIndex = 0;
|
|
43
|
+
|
|
44
|
+
for (const character of compactCandidate) {
|
|
45
|
+
if (character === compactQuery[queryIndex]) queryIndex += 1;
|
|
46
|
+
if (queryIndex === compactQuery.length) {
|
|
47
|
+
return 0.2 * (compactQuery.length / compactCandidate.length);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function commandPaletteFilter(
|
|
55
|
+
value: string,
|
|
56
|
+
search: string,
|
|
57
|
+
keywords: string[] = [],
|
|
58
|
+
): number {
|
|
59
|
+
const query = normalizeSearchText(search);
|
|
60
|
+
if (!query) return 1;
|
|
61
|
+
|
|
62
|
+
const queryWords = query.split(" ");
|
|
63
|
+
let bestScore = 0;
|
|
64
|
+
|
|
65
|
+
for (const rawCandidate of [...keywords, value]) {
|
|
66
|
+
const candidate = normalizeSearchText(rawCandidate);
|
|
67
|
+
if (!candidate) continue;
|
|
68
|
+
|
|
69
|
+
const words = candidate.split(" ");
|
|
70
|
+
let score = 0;
|
|
71
|
+
|
|
72
|
+
if (candidate === query) score = 1;
|
|
73
|
+
else if (candidate.startsWith(query)) score = 0.96;
|
|
74
|
+
else if (words.includes(query)) score = 0.94;
|
|
75
|
+
else if (words.some((word) => word.startsWith(query))) score = 0.9;
|
|
76
|
+
else if (candidate.includes(query)) score = 0.8;
|
|
77
|
+
else if (
|
|
78
|
+
queryWords.every((queryWord) =>
|
|
79
|
+
words.some((word) => word.startsWith(queryWord)),
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
score = 0.72;
|
|
83
|
+
} else {
|
|
84
|
+
score = fuzzySubsequenceScore(candidate, query);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
bestScore = Math.max(bestScore, score);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return bestScore;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function rankCommandPaletteEntries<T>(
|
|
94
|
+
entries: T[],
|
|
95
|
+
search: string,
|
|
96
|
+
getSearchData: (entry: T) => { value: string; keywords?: string[] },
|
|
97
|
+
): Array<{ entry: T; score: number }> {
|
|
98
|
+
return entries
|
|
99
|
+
.map((entry, index) => {
|
|
100
|
+
const { value, keywords } = getSearchData(entry);
|
|
101
|
+
return {
|
|
102
|
+
entry,
|
|
103
|
+
index,
|
|
104
|
+
score: commandPaletteFilter(value, search, keywords),
|
|
105
|
+
};
|
|
106
|
+
})
|
|
107
|
+
.filter(({ score }) => score > 0)
|
|
108
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
109
|
+
.map(({ entry, score }) => ({ entry, score }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function uniqueCommandItems<T extends { id: string; name: string }>(
|
|
113
|
+
items: T[],
|
|
114
|
+
): T[] {
|
|
115
|
+
const seenIds = new Set<string>();
|
|
116
|
+
return items.filter((item) => {
|
|
117
|
+
if (seenIds.has(item.id)) return false;
|
|
118
|
+
seenIds.add(item.id);
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -46,11 +46,12 @@ export async function fetchReplayStorageStatus(): Promise<ReplayStorageStatus> {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function useReplayStorageStatus() {
|
|
49
|
+
export function useReplayStorageStatus(options?: { enabled?: boolean }) {
|
|
50
50
|
return useQuery({
|
|
51
51
|
queryKey: REPLAY_STORAGE_STATUS_KEY,
|
|
52
52
|
queryFn: fetchReplayStorageStatus,
|
|
53
53
|
staleTime: 60_000,
|
|
54
|
+
enabled: options?.enabled,
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -52,49 +52,56 @@ function normalizedVolatility(
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function normalizedTrend(
|
|
55
|
-
|
|
55
|
+
series: number[],
|
|
56
|
+
minimum: number,
|
|
57
|
+
range: number,
|
|
56
58
|
volatilityScore: number,
|
|
57
59
|
random: () => number,
|
|
58
60
|
): number[] {
|
|
61
|
+
const length = series.length;
|
|
59
62
|
if (length <= 1) return [0];
|
|
60
63
|
if (length === 2) return [0, 1];
|
|
61
64
|
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const normalized = series.map((value) => (value - minimum) / range);
|
|
66
|
+
const randomization = 0.012 + volatilityScore * 0.08;
|
|
67
|
+
const shape = [normalized[0]];
|
|
68
|
+
|
|
69
|
+
// Preserve the source's actual step pattern (including when and how sharply
|
|
70
|
+
// it spikes), with just enough seeded variation that otherwise-similar demo
|
|
71
|
+
// series do not become identical. Smooth sources receive almost no jitter;
|
|
72
|
+
// volatile sources can vary a little more without moving their events.
|
|
73
|
+
for (let index = 1; index < length; index += 1) {
|
|
74
|
+
const sourceStep = normalized[index] - normalized[index - 1];
|
|
75
|
+
const factor = 1 + (random() * 2 - 1) * randomization;
|
|
76
|
+
shape.push(shape[index - 1] + sourceStep * factor);
|
|
77
|
+
}
|
|
68
78
|
|
|
79
|
+
// Add only the linear drift required for the first point to be the global
|
|
80
|
+
// minimum and the last to be the global maximum. A linear term has zero
|
|
81
|
+
// second difference, so the source's local acceleration, spikes, and dips
|
|
82
|
+
// survive instead of being replaced by a synthetic random walk.
|
|
83
|
+
let requiredDrift = 0;
|
|
84
|
+
for (let index = 1; index < length; index += 1) {
|
|
85
|
+
const progress = index / (length - 1);
|
|
86
|
+
requiredDrift = Math.max(
|
|
87
|
+
requiredDrift,
|
|
88
|
+
(shape[0] - shape[index]) / progress,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (let index = 0; index < length - 1; index += 1) {
|
|
69
92
|
const progress = index / (length - 1);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Pullback count and depth follow the source's normalized volatility. A
|
|
76
|
-
// smooth source remains a gentle rise; a jagged source gets several seeded
|
|
77
|
-
// reversals. Spacing the reversals across the series prevents them from
|
|
78
|
-
// collapsing into one noisy patch.
|
|
79
|
-
if (length >= 5 && volatilityScore >= 0.12) {
|
|
80
|
-
const maximumPullbacks = Math.min(3, Math.floor((length - 2) / 2));
|
|
81
|
-
const pullbackCount = Math.max(
|
|
82
|
-
1,
|
|
83
|
-
Math.round(volatilityScore * maximumPullbacks),
|
|
93
|
+
requiredDrift = Math.max(
|
|
94
|
+
requiredDrift,
|
|
95
|
+
(shape[index] - shape[length - 1]) / (1 - progress),
|
|
84
96
|
);
|
|
85
|
-
for (let pullback = 0; pullback < pullbackCount; pullback += 1) {
|
|
86
|
-
const segmentCenter =
|
|
87
|
-
((pullback + 1) * (length - 3)) / (pullbackCount + 1) + 1;
|
|
88
|
-
const jitter = (random() - 0.5) * Math.max(1, length / 10);
|
|
89
|
-
const dipIndex = Math.round(clamp(segmentCenter + jitter, 2, length - 2));
|
|
90
|
-
const center = dipIndex / (length - 1);
|
|
91
|
-
const amplitude = 0.025 + volatilityScore * (0.075 + random() * 0.12);
|
|
92
|
-
values[dipIndex - 1] = clamp(center + amplitude, 0.075, 0.925);
|
|
93
|
-
values[dipIndex] = clamp(center - amplitude, 0.05, 0.9);
|
|
94
|
-
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
const drift = requiredDrift + 0.012 + random() * 0.018;
|
|
100
|
+
const candidate = shape.map(
|
|
101
|
+
(value, index) => value + drift * (index / (length - 1)),
|
|
102
|
+
);
|
|
103
|
+
const candidateRange = candidate[length - 1] - candidate[0];
|
|
104
|
+
return candidate.map((value) => (value - candidate[0]) / candidateRange);
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/**
|
|
@@ -133,7 +140,13 @@ export function createDemoChartTrendRows(
|
|
|
133
140
|
minimum,
|
|
134
141
|
range,
|
|
135
142
|
);
|
|
136
|
-
const trend = normalizedTrend(
|
|
143
|
+
const trend = normalizedTrend(
|
|
144
|
+
points.map((point) => point.numeric),
|
|
145
|
+
minimum,
|
|
146
|
+
range,
|
|
147
|
+
volatilityScore,
|
|
148
|
+
random,
|
|
149
|
+
);
|
|
137
150
|
const useStringValues = points.every(
|
|
138
151
|
(point) => typeof point.original === "string",
|
|
139
152
|
);
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
useAgentSettingsTabs,
|
|
6
6
|
useSession,
|
|
7
7
|
useT,
|
|
8
|
-
type SettingsSearchEntry,
|
|
9
8
|
type SettingsTabItem,
|
|
10
9
|
} from "@agent-native/core/client";
|
|
11
10
|
import { TeamPage } from "@agent-native/core/client/org";
|
|
@@ -27,6 +26,7 @@ import changelog from "../../CHANGELOG.md?raw";
|
|
|
27
26
|
import { useReplayStorageStatus } from "../hooks/use-replay-storage-status";
|
|
28
27
|
import { ReplayStorageHint } from "./sessions/SessionsPage";
|
|
29
28
|
import { AlertRulesSettingsCard } from "./settings/AlertRulesSettingsCard";
|
|
29
|
+
import { buildAnalyticsGeneralSettingsSearchEntries } from "./settings/settings-search";
|
|
30
30
|
|
|
31
31
|
export default function Settings() {
|
|
32
32
|
// Settings is also reachable directly from the full-page agent surface.
|
|
@@ -56,49 +56,12 @@ export default function Settings() {
|
|
|
56
56
|
[agentSettingsTabs, t],
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
const generalSearchEntries = useMemo
|
|
60
|
-
() =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
hash: "account",
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: "analytics-credentials",
|
|
69
|
-
label: t("settings.credentials"),
|
|
70
|
-
keywords: "data sources api keys manage credentials",
|
|
71
|
-
hash: "credentials",
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: "analytics-dashboard-templates",
|
|
75
|
-
label: t("settings.dashboardTemplates"),
|
|
76
|
-
keywords: "templates catalog dashboards",
|
|
77
|
-
hash: "dashboard-templates",
|
|
78
|
-
},
|
|
79
|
-
...(replayStorageStatus.data?.configured
|
|
80
|
-
? [
|
|
81
|
-
{
|
|
82
|
-
id: "analytics-replay-storage",
|
|
83
|
-
label: t("sessions.storageSetupTitle"),
|
|
84
|
-
keywords: "session replay recording storage s3 bucket builder",
|
|
85
|
-
hash: "replay-storage",
|
|
86
|
-
},
|
|
87
|
-
]
|
|
88
|
-
: []),
|
|
89
|
-
{
|
|
90
|
-
id: "analytics-language",
|
|
91
|
-
label: t("settings.languageTitle"),
|
|
92
|
-
keywords: "language locale translation i18n",
|
|
93
|
-
hash: "language",
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "analytics-about",
|
|
97
|
-
label: t("settings.about"),
|
|
98
|
-
keywords: "about version info usage",
|
|
99
|
-
hash: "about",
|
|
100
|
-
},
|
|
101
|
-
],
|
|
59
|
+
const generalSearchEntries = useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
buildAnalyticsGeneralSettingsSearchEntries(
|
|
62
|
+
t,
|
|
63
|
+
!!replayStorageStatus.data?.configured,
|
|
64
|
+
),
|
|
102
65
|
[replayStorageStatus.data?.configured, t],
|
|
103
66
|
);
|
|
104
67
|
|
|
@@ -184,7 +147,7 @@ export default function Settings() {
|
|
|
184
147
|
</CardDescription>
|
|
185
148
|
</CardHeader>
|
|
186
149
|
<CardContent>
|
|
187
|
-
<ReplayStorageHint />
|
|
150
|
+
<ReplayStorageHint embedded />
|
|
188
151
|
</CardContent>
|
|
189
152
|
</Card>
|
|
190
153
|
) : null}
|
|
@@ -160,8 +160,17 @@ type ReplayViewportDimensions = {
|
|
|
160
160
|
height: number;
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
type ReplayViewportChange = ReplayViewportDimensions & {
|
|
164
|
+
offsetMs: number;
|
|
165
|
+
};
|
|
166
|
+
|
|
163
167
|
const DEFAULT_PLAYER_WIDTH = 1024;
|
|
164
168
|
const DEFAULT_PLAYER_HEIGHT = 640;
|
|
169
|
+
const MALFORMED_REPLAY_MIN_WIDTH = 4000;
|
|
170
|
+
const MALFORMED_REPLAY_MIN_ASPECT_RATIO = 4;
|
|
171
|
+
const MIN_REPLAY_DISPLAY_DIMENSION = 240;
|
|
172
|
+
const RECOVERED_REPLAY_DISPLAY_ASPECT_RATIO =
|
|
173
|
+
DEFAULT_PLAYER_WIDTH / DEFAULT_PLAYER_HEIGHT;
|
|
165
174
|
const DEFAULT_SPEED = 2;
|
|
166
175
|
const SPEED_OPTIONS = [0.5, 1, 2, 4, 8];
|
|
167
176
|
const SKIP_STEP_MS = 5000;
|
|
@@ -487,8 +496,17 @@ function ReplayPlayer({
|
|
|
487
496
|
height: number;
|
|
488
497
|
} | null>(null);
|
|
489
498
|
const [fitScale, setFitScale] = useState(1);
|
|
490
|
-
const initialDims = useMemo(
|
|
499
|
+
const initialDims = useMemo(
|
|
500
|
+
() => replayInitialDisplayDimensions(events),
|
|
501
|
+
[events],
|
|
502
|
+
);
|
|
503
|
+
const viewportTimeline = useMemo(
|
|
504
|
+
() => buildReplayViewportTimeline(events),
|
|
505
|
+
[events],
|
|
506
|
+
);
|
|
491
507
|
const eventsRef = useLiveRef(events);
|
|
508
|
+
const viewportTimelineRef = useLiveRef(viewportTimeline);
|
|
509
|
+
const streamedDimsRef = useLiveRef(streamedDims);
|
|
492
510
|
// Stable identity for the loaded event set so progressive chunk publishes
|
|
493
511
|
// that only grow the array do not tear down a healthy Replayer mid-playback.
|
|
494
512
|
const eventsIdentity = useMemo(
|
|
@@ -501,13 +519,16 @@ function ReplayPlayer({
|
|
|
501
519
|
const scrubbingRef = useRef(false);
|
|
502
520
|
const scrubResumePlayingRef = useRef(false);
|
|
503
521
|
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
522
|
+
// IMPORTANT — DO NOT REMOVE THIS LEGACY VIEWPORT CORRECTION.
|
|
523
|
+
// Some stored sessions contain impossible 4,000–7,500px-wide Meta/resize
|
|
524
|
+
// values even though the browser was a normal desktop viewport. Rendering
|
|
525
|
+
// those raw values collapses the replay into a tiny horizontal ribbon. Keep
|
|
526
|
+
// normal recordings exact—including 32:9 and mobile portrait. Recover only
|
|
527
|
+
// widths >=4,000px with aspect >4:1 to the standard 16:10 viewport. The same
|
|
528
|
+
// dimensions are applied to rrweb's iframe so CSS breakpoints/camera agree.
|
|
529
|
+
const displayDims = clampReplayDisplayDimensions(streamedDims ?? initialDims);
|
|
530
|
+
const playerWidth = displayDims?.width ?? DEFAULT_PLAYER_WIDTH;
|
|
531
|
+
const playerHeight = displayDims?.height ?? DEFAULT_PLAYER_HEIGHT;
|
|
511
532
|
const skipRanges = useMemo(() => buildIdleSkipRanges(events), [events]);
|
|
512
533
|
const skipRangesRef = useLiveRef(skipRanges);
|
|
513
534
|
const skipInactiveRef = useLiveRef(skipInactive);
|
|
@@ -626,9 +647,35 @@ function ReplayPlayer({
|
|
|
626
647
|
console.warn("[session-replay] seek failed", seekError);
|
|
627
648
|
return;
|
|
628
649
|
}
|
|
650
|
+
const seekDims = replayViewportDimensionsAtTime(
|
|
651
|
+
viewportTimelineRef.current,
|
|
652
|
+
clamped,
|
|
653
|
+
);
|
|
654
|
+
if (seekDims) {
|
|
655
|
+
// rrweb 2.1 does not reliably re-emit Resize when seeking backwards.
|
|
656
|
+
// IMPORTANT: correct both layers together. Correcting only the outer
|
|
657
|
+
// stage or only rrweb's iframe recreates the ultra-wide/clipped bug.
|
|
658
|
+
const correctedSeekDims = clampReplayDisplayDimensions(seekDims);
|
|
659
|
+
const currentDims = streamedDimsRef.current;
|
|
660
|
+
if (
|
|
661
|
+
correctedSeekDims &&
|
|
662
|
+
(currentDims?.width !== correctedSeekDims.width ||
|
|
663
|
+
currentDims?.height !== correctedSeekDims.height)
|
|
664
|
+
) {
|
|
665
|
+
replayer.handleResize?.(correctedSeekDims);
|
|
666
|
+
setStreamedDims(correctedSeekDims);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
629
669
|
updateTime(clamped);
|
|
630
670
|
},
|
|
631
|
-
[
|
|
671
|
+
[
|
|
672
|
+
playingRef,
|
|
673
|
+
status,
|
|
674
|
+
streamedDimsRef,
|
|
675
|
+
totalTime,
|
|
676
|
+
updateTime,
|
|
677
|
+
viewportTimelineRef,
|
|
678
|
+
],
|
|
632
679
|
);
|
|
633
680
|
|
|
634
681
|
const beginScrub = useCallback(
|
|
@@ -693,11 +740,13 @@ function ReplayPlayer({
|
|
|
693
740
|
|
|
694
741
|
stageRootRef.current.innerHTML = "";
|
|
695
742
|
// Match builder-internal: pass events through untouched and let rrweb own
|
|
696
|
-
// iframe sizing via Meta / ViewportResize.
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
// the stage
|
|
700
|
-
|
|
743
|
+
// normal iframe sizing via Meta / ViewportResize. Never rewrite Meta or
|
|
744
|
+
// force arbitrary iframe dimensions (that mismatches the FullSnapshot DOM
|
|
745
|
+
// and blanks the stage). The one exception below mirrors the narrowly
|
|
746
|
+
// detected legacy malformed viewport onto both the iframe and outer stage.
|
|
747
|
+
const rawInitialDims = replayInitialViewportDimensions(replayEvents);
|
|
748
|
+
const correctedInitialDims = replayInitialDisplayDimensions(replayEvents);
|
|
749
|
+
setStreamedDims(correctedInitialDims);
|
|
701
750
|
localReplayer = new Replayer(replayEvents as any[], {
|
|
702
751
|
root: stageRootRef.current,
|
|
703
752
|
speed: speedRef.current,
|
|
@@ -708,6 +757,21 @@ function ReplayPlayer({
|
|
|
708
757
|
mouseTail: false,
|
|
709
758
|
insertStyleRules: SUPPRESS_OVERLAYS_CSS,
|
|
710
759
|
});
|
|
760
|
+
// IMPORTANT: rrweb constructs its iframe from the raw initial Meta event.
|
|
761
|
+
// When that legacy Meta width is malformed, correcting only streamedDims
|
|
762
|
+
// fixes the outer stage but leaves CSS breakpoints and clicks ultra-wide.
|
|
763
|
+
// Apply the same correction to rrweb immediately, before first playback.
|
|
764
|
+
if (
|
|
765
|
+
correctedInitialDims &&
|
|
766
|
+
(rawInitialDims?.width !== correctedInitialDims.width ||
|
|
767
|
+
rawInitialDims?.height !== correctedInitialDims.height)
|
|
768
|
+
) {
|
|
769
|
+
localReplayer.handleResize?.(correctedInitialDims);
|
|
770
|
+
}
|
|
771
|
+
// rrweb already sandboxes the replay document without script execution.
|
|
772
|
+
// Do not mutate recorded URLs/CSS; suppress viewer-page referrer leakage
|
|
773
|
+
// at the iframe boundary while retaining historical visual resources.
|
|
774
|
+
localReplayer.iframe?.setAttribute?.("referrerpolicy", "no-referrer");
|
|
711
775
|
replayerRef.current = localReplayer;
|
|
712
776
|
const meta = localReplayer.getMetaData?.();
|
|
713
777
|
const total = Number(meta?.totalTime ?? replayDuration(replayEvents));
|
|
@@ -732,10 +796,29 @@ function ReplayPlayer({
|
|
|
732
796
|
dims.width > 0 &&
|
|
733
797
|
dims.height > 0
|
|
734
798
|
) {
|
|
735
|
-
|
|
799
|
+
const rawDims = {
|
|
736
800
|
width: Math.round(dims.width),
|
|
737
801
|
height: Math.round(dims.height),
|
|
738
|
-
}
|
|
802
|
+
};
|
|
803
|
+
const correctedDims = clampReplayDisplayDimensions(rawDims);
|
|
804
|
+
if (!correctedDims) return;
|
|
805
|
+
// rrweb handles the raw resize before notifying us. Override only
|
|
806
|
+
// impossible legacy geometry so its iframe and our stage stay equal.
|
|
807
|
+
// DO NOT clamp just one layer; that breaks responsive CSS and clicks.
|
|
808
|
+
const currentDims = streamedDimsRef.current;
|
|
809
|
+
if (
|
|
810
|
+
currentDims?.width === correctedDims.width &&
|
|
811
|
+
currentDims?.height === correctedDims.height
|
|
812
|
+
) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (
|
|
816
|
+
correctedDims.width !== rawDims.width ||
|
|
817
|
+
correctedDims.height !== rawDims.height
|
|
818
|
+
) {
|
|
819
|
+
localReplayer.handleResize?.(correctedDims);
|
|
820
|
+
}
|
|
821
|
+
setStreamedDims(correctedDims);
|
|
739
822
|
}
|
|
740
823
|
});
|
|
741
824
|
updateTime(startAt);
|
|
@@ -1855,65 +1938,19 @@ function useReplayEvents(
|
|
|
1855
1938
|
);
|
|
1856
1939
|
}
|
|
1857
1940
|
|
|
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
|
-
]);
|
|
1870
|
-
|
|
1871
|
-
function stripReplayCssUrls(value: string): string {
|
|
1872
|
-
return value
|
|
1873
|
-
.replace(/@import\s+(?:url\()?[^;)]+(?:\))?\s*;?/gi, "")
|
|
1874
|
-
.replace(/url\(\s*[^)]*\)/gi, "none");
|
|
1875
|
-
}
|
|
1876
|
-
|
|
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";
|
|
1889
|
-
}
|
|
1890
|
-
if (
|
|
1891
|
-
normalizedKey === "style" ||
|
|
1892
|
-
normalizedKey === "_csstext" ||
|
|
1893
|
-
normalizedKey === "csstext"
|
|
1894
|
-
) {
|
|
1895
|
-
return stripReplayCssUrls(value);
|
|
1896
|
-
}
|
|
1897
|
-
return value;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
return Object.fromEntries(
|
|
1901
|
-
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
1902
|
-
entryKey,
|
|
1903
|
-
sanitizeReplayValue(entryValue, entryKey),
|
|
1904
|
-
]),
|
|
1905
|
-
);
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
1941
|
/**
|
|
1909
|
-
* rrweb
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1942
|
+
* Stock rrweb consumes the recorded event objects directly. Playback-time URL
|
|
1943
|
+
* or CSS rewriting breaks snapshots, responsive rules, fonts, and navigation
|
|
1944
|
+
* metadata, so filtering invalid container entries is the only normalization.
|
|
1945
|
+
*
|
|
1946
|
+
* IMPORTANT — DO NOT add URL/CSS sanitization here. A previous security review
|
|
1947
|
+
* changed href/src/_cssText to about:blank and immediately regressed every
|
|
1948
|
+
* historical recording that depended on captured styles. Network/privacy
|
|
1949
|
+
* controls belong at capture or the sandbox boundary, never in rrweb events.
|
|
1912
1950
|
*/
|
|
1913
1951
|
export function normalizeReplayEvents(events: unknown[]): AnyReplayEvent[] {
|
|
1914
1952
|
return events
|
|
1915
1953
|
.filter((event): event is AnyReplayEvent => isRecord(event))
|
|
1916
|
-
.map((event) => sanitizeReplayValue(event) as AnyReplayEvent)
|
|
1917
1954
|
.sort((a, b) => Number(a.timestamp ?? 0) - Number(b.timestamp ?? 0));
|
|
1918
1955
|
}
|
|
1919
1956
|
|
|
@@ -2236,6 +2273,81 @@ export function replayViewportDimensions(
|
|
|
2236
2273
|
return best;
|
|
2237
2274
|
}
|
|
2238
2275
|
|
|
2276
|
+
export function replayInitialViewportDimensions(
|
|
2277
|
+
events: AnyReplayEvent[],
|
|
2278
|
+
): ReplayViewportDimensions | null {
|
|
2279
|
+
// rrweb itself initializes from the first Meta event. A resize can appear
|
|
2280
|
+
// earlier in chunk order after reconnect/flush boundaries, but it must not
|
|
2281
|
+
// replace the snapshot's native starting viewport.
|
|
2282
|
+
for (const event of events) {
|
|
2283
|
+
if (event.type !== RRWEB_EVENT_TYPE.Meta) continue;
|
|
2284
|
+
const dims = dimensionsFromReplayEvent(event);
|
|
2285
|
+
if (dims) return dims;
|
|
2286
|
+
}
|
|
2287
|
+
for (const event of events) {
|
|
2288
|
+
const dims = dimensionsFromReplayEvent(event);
|
|
2289
|
+
if (dims) return dims;
|
|
2290
|
+
}
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
export function replayInitialDisplayDimensions(
|
|
2295
|
+
events: AnyReplayEvent[],
|
|
2296
|
+
): ReplayViewportDimensions | null {
|
|
2297
|
+
// Keep the initial outer stage and rrweb iframe on one corrected camera.
|
|
2298
|
+
// DO NOT use the raw initial Meta dimensions for only one of those layers.
|
|
2299
|
+
return clampReplayDisplayDimensions(replayInitialViewportDimensions(events));
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
export function buildReplayViewportTimeline(
|
|
2303
|
+
events: AnyReplayEvent[],
|
|
2304
|
+
): ReplayViewportChange[] {
|
|
2305
|
+
const initial = replayInitialViewportDimensions(events);
|
|
2306
|
+
if (!initial) return [];
|
|
2307
|
+
const startedAt = replayStartedAt(events);
|
|
2308
|
+
const firstMetaTimestamp = events.reduce((best, event) => {
|
|
2309
|
+
if (event.type !== RRWEB_EVENT_TYPE.Meta) return best;
|
|
2310
|
+
const timestamp = Number(event.timestamp ?? 0);
|
|
2311
|
+
return Number.isFinite(timestamp) && timestamp > 0
|
|
2312
|
+
? Math.min(best, timestamp)
|
|
2313
|
+
: best;
|
|
2314
|
+
}, Number.POSITIVE_INFINITY);
|
|
2315
|
+
const changes: ReplayViewportChange[] = [{ ...initial, offsetMs: 0 }];
|
|
2316
|
+
for (const event of events) {
|
|
2317
|
+
const timestamp = Number(event.timestamp ?? 0);
|
|
2318
|
+
if (!Number.isFinite(timestamp) || timestamp <= firstMetaTimestamp)
|
|
2319
|
+
continue;
|
|
2320
|
+
const dims = dimensionsFromReplayEvent(event);
|
|
2321
|
+
if (!dims) continue;
|
|
2322
|
+
const previous = changes[changes.length - 1];
|
|
2323
|
+
if (previous?.width === dims.width && previous.height === dims.height) {
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
changes.push({
|
|
2327
|
+
...dims,
|
|
2328
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
return changes;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
export function replayViewportDimensionsAtTime(
|
|
2335
|
+
changes: ReplayViewportChange[],
|
|
2336
|
+
elapsedMs: number,
|
|
2337
|
+
): ReplayViewportDimensions | null {
|
|
2338
|
+
if (changes.length === 0) return null;
|
|
2339
|
+
const target = Math.max(0, elapsedMs);
|
|
2340
|
+
let low = 0;
|
|
2341
|
+
let high = changes.length - 1;
|
|
2342
|
+
while (low < high) {
|
|
2343
|
+
const middle = Math.ceil((low + high) / 2);
|
|
2344
|
+
if ((changes[middle]?.offsetMs ?? 0) <= target) low = middle;
|
|
2345
|
+
else high = middle - 1;
|
|
2346
|
+
}
|
|
2347
|
+
const match = changes[low];
|
|
2348
|
+
return match ? { width: match.width, height: match.height } : null;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2239
2351
|
function dimensionsFromReplayEvent(
|
|
2240
2352
|
event: AnyReplayEvent,
|
|
2241
2353
|
): ReplayViewportDimensions | null {
|
|
@@ -2272,6 +2384,29 @@ export function normalizeReplayDimensions(
|
|
|
2272
2384
|
};
|
|
2273
2385
|
}
|
|
2274
2386
|
|
|
2387
|
+
export function clampReplayDisplayDimensions(
|
|
2388
|
+
dims: ReplayViewportDimensions | null,
|
|
2389
|
+
): ReplayViewportDimensions | null {
|
|
2390
|
+
if (!dims) return null;
|
|
2391
|
+
if (
|
|
2392
|
+
dims.width < MIN_REPLAY_DISPLAY_DIMENSION ||
|
|
2393
|
+
dims.height < MIN_REPLAY_DISPLAY_DIMENSION
|
|
2394
|
+
) {
|
|
2395
|
+
return { width: DEFAULT_PLAYER_WIDTH, height: DEFAULT_PLAYER_HEIGHT };
|
|
2396
|
+
}
|
|
2397
|
+
const aspect = dims.width / dims.height;
|
|
2398
|
+
if (
|
|
2399
|
+
dims.width >= MALFORMED_REPLAY_MIN_WIDTH &&
|
|
2400
|
+
aspect > MALFORMED_REPLAY_MIN_ASPECT_RATIO
|
|
2401
|
+
) {
|
|
2402
|
+
return {
|
|
2403
|
+
width: Math.round(dims.height * RECOVERED_REPLAY_DISPLAY_ASPECT_RATIO),
|
|
2404
|
+
height: dims.height,
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
return dims;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2275
2410
|
export function filterReplayMarkers(
|
|
2276
2411
|
markers: ReplayMarker[],
|
|
2277
2412
|
query: string,
|