Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"@prisma/client": "^7.6.0",
"@prisma/extension-read-replicas": "^0.5.0",
"@react-spring/web": "^10.0.3",
"@rrweb/rrweb-plugin-console-record": "2.0.0-alpha.20",
"@rrweb/rrweb-plugin-console-replay": "2.0.0-alpha.20",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.96.0",
"@umami/react-zen": "^0.245.0",
Expand Down Expand Up @@ -106,8 +108,8 @@
"react-window": "^1.8.6",
"redis": "^4.5.1",
"request-ip": "^3.3.0",
"rrweb": "2.0.0-alpha.4",
"rrweb-player": "1.0.0-alpha.4",
"rrweb": "2.0.0-alpha.20",
"rrweb-player": "2.0.0-alpha.20",
Comment on lines +111 to +112
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 rrweb-player major version jump may break existing replay playback

rrweb-player was bumped from 1.0.0-alpha.4 to 2.0.0-alpha.20 — a major-version change across 16 alpha releases. The lock file confirms the new package no longer directly lists rrweb as a dependency; it now pulls in @rrweb/packer and @rrweb/replay instead, indicating significant internal restructuring. Bundling this breaking upgrade with the console-log feature makes it harder to bisect regressions if existing replays stop playing correctly.

"semver": "^7.7.4",
"serialize-error": "^12.0.0",
"thenby": "^1.3.4",
Expand Down
94 changes: 71 additions & 23 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/intl/messages/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@
"pixel": "Pixel",
"pixels": "Pixels",
"play": "Play",
"record-console": "Record console logs",
"poor": "Poor",
"powered-by": "Powered by {name}",
"preferences": "Preferences",
Expand Down
1 change: 1 addition & 0 deletions public/intl/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
"remove": "Remove",
"remove-member": "Remove member",
"play": "Play",
"record-console": "Record console logs",
"replay": "Replay",
"replay-id": "Replay ID",
"replay-code": "Session replay code",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,33 @@ export function ReplayPlayer({ events }: { events: any[] }) {
useEffect(() => {
if (!containerRef.current || !events?.length) return;

import('rrweb-player').then(mod => {
const RRWebPlayer = mod.default;
Promise.all([import('rrweb-player'), import('@rrweb/rrweb-plugin-console-replay')]).then(
([mod, consoleMod]) => {
const RRWebPlayer = mod.default;
const { getReplayConsolePlugin } = consoleMod;

if (containerRef.current) {
containerRef.current.innerHTML = '';
}
if (containerRef.current) {
containerRef.current.innerHTML = '';
}

playerRef.current = new RRWebPlayer({
target: containerRef.current,
props: {
events: events,
width: playerWidth,
height: playerHeight,
autoPlay: false,
showController: true,
speedOption: [1, 2, 4, 8],
useVirtualDom: false,
showWarning: false,
},
});
playerRef.current = new RRWebPlayer({
target: containerRef.current,
props: {
events: events,
width: playerWidth,
height: playerHeight,
autoPlay: false,
showController: true,
speedOption: [1, 2, 4, 8],
useVirtualDom: false,
showWarning: false,
plugins: [getReplayConsolePlugin()],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Console replay panel always shown regardless of recording config

getReplayConsolePlugin() is unconditionally injected into every player instance, even for sessions recorded when recordConsole was false (the default). Those sessions contain no console events, so the player will render an empty console panel — contradicting the explicit opt-out. The ReplayPlayer component should receive the recordConsole flag (or derive it from the events list) and conditionally include the plugin.

},
});

setLoaded(true);
});
setLoaded(true);
},
);

return () => {
if (playerRef.current) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface ReplayConfig {
maskLevel?: string;
maxDuration?: number;
blockSelector?: string;
recordConsole?: boolean;
}

export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
Expand All @@ -37,13 +38,15 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
const [maskLevel, setMaskLevel] = useState(config.maskLevel ?? 'moderate');
const [maxDuration, setMaxDuration] = useState(String(config.maxDuration ?? 300000));
const [blockSelector, setBlockSelector] = useState(config.blockSelector ?? '');
const [recordConsole, setRecordConsole] = useState(config.recordConsole ?? false);

const recorderUrl = cloudMode
? `${process.env.cloudUrl}/${RECORDER_NAME}`
: `${window?.location?.origin || ''}${process.env.basePath || ''}/${RECORDER_NAME}`;

let recorderAttrs = `data-website-id="${websiteId}" data-sample-rate="${sampleRate}" data-mask-level="${maskLevel}" data-max-duration="${parseInt(maxDuration, 10) || 300000}"`;
if (blockSelector) recorderAttrs += ` data-block-selector="${blockSelector}"`;
if (recordConsole) recorderAttrs += ` data-record-console="true"`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 When recordConsole is false (the default), the data-record-console attribute is correctly omitted. Consider a strict equality guard to future-proof against accidental inclusion of a falsy string value.

Suggested change
if (recordConsole) recorderAttrs += ` data-record-console="true"`;
if (recordConsole === true) recorderAttrs += ` data-record-console="true"`;

const recorderCode = `<script defer src="${recorderUrl}" ${recorderAttrs}></script>`;

const handleToggle = async (value: boolean) => {
Expand Down Expand Up @@ -77,6 +80,7 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
maskLevel,
maxDuration: parseInt(maxDuration, 10) || 300000,
...(blockSelector && { blockSelector }),
recordConsole,
},
},
{
Expand Down Expand Up @@ -151,6 +155,9 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
<Label>{t(labels.blockSelector)}</Label>
<TextField value={blockSelector} onChange={setBlockSelector} />
</Column>
<Switch isSelected={recordConsole} onChange={setRecordConsole}>
{t(labels.recordConsole)}
</Switch>
<Row>
<Button variant="primary" onPress={handleSave} isDisabled={isPending}>
{t(labels.save)}
Expand Down
1 change: 1 addition & 0 deletions src/components/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export const labels: Record<string, string> = {
duration: 'label.duration',
recorded: 'label.recorded',
upgrade: 'label.upgrade',
recordConsole: 'label.record-console',
};

export const messages: Record<string, string> = {
Expand Down
9 changes: 9 additions & 0 deletions src/recorder/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { record } from 'rrweb';
import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';

(window => {
const { document } = window;
Expand All @@ -15,6 +16,7 @@ import { record } from 'rrweb';
const maskLevel = config(`mask-level`) || 'moderate';
const maxDuration = parseInt(config(`max-duration`) || '300000', 10);
const blockSelector = config(`block-selector`) || '';
const recordConsole = config(`record-console`) === 'true';

if (!website) return;

Expand Down Expand Up @@ -114,6 +116,12 @@ import { record } from 'rrweb';

const maskConfig = getMaskConfig(maskLevel);

const plugins = [];

if (recordConsole) {
plugins.push(getRecordConsolePlugin());
}
Comment on lines +121 to +123
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Console logs may capture sensitive data before masking rules apply

getRecordConsolePlugin() intercepts console.* calls wholesale. Secrets, tokens, PII, or debug payloads logged by third-party SDKs will be transmitted to the server and stored in replay events. Unlike DOM masking (configurable via maskLevel), there is no filtering or redaction option. Consider documenting this risk near the toggle, or providing a level option (e.g., warn/error only) to limit exposure.


stopFn = record({
emit(event) {
if (stopped) return;
Expand All @@ -132,6 +140,7 @@ import { record } from 'rrweb';
scheduleFlush();
},
...maskConfig,
plugins,
inlineStylesheet: true,
slimDOMOptions: {
script: true,
Expand Down