diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 5c19ccd64c558..43201b1bfbc6c 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -164,6 +164,7 @@ export const FEATURE_FLAGS = { SURVEY_NPS_RESULTS: 'survey-nps-results', // owner: @liyiy // owner: #team-monitoring SESSION_RECORDING_ALLOW_V1_SNAPSHOTS: 'session-recording-allow-v1-snapshots', + SESSION_REPLAY_CORS_PROXY: 'session-replay-cors-proxy', // owner: #team-monitoring HOGQL_INSIGHTS: 'hogql-insights', // owner: @mariusandra WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline } as const diff --git a/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap b/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000000..ecae917dffa9b --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/rrweb/__snapshots__/index.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CorsPlugin should replace font urls in links 1`] = `"https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/fonts/my-font.woff2?t=1234"`; + +exports[`CorsPlugin should replace font urls in links 2`] = `"https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/fonts/my-font.ttf"`; + +exports[`CorsPlugin should replace font urls in stylesheets 1`] = `"@font-face { font-display: fallback; font-family: "Roboto Condensed"; font-weight: 400; font-style: normal; src: url("https://replay.ph-proxy.com/proxy?url=https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff2?11012022") format("woff2"), url("https://replay.ph-proxy.com/proxy?url=https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff?11012022")"`; + +exports[`CorsPlugin should replace font urls in stylesheets 2`] = `"url("https://replay.ph-proxy.com/proxy?url=https://app.posthog.com/fonts/my-font.woff2")"`; diff --git a/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts b/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts new file mode 100644 index 0000000000000..da2684cf38e5a --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/rrweb/index.test.ts @@ -0,0 +1,24 @@ +import { CorsPlugin } from '.' + +describe('CorsPlugin', () => { + it.each([ + `@font-face { font-display: fallback; font-family: "Roboto Condensed"; font-weight: 400; font-style: normal; src: url("https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff2?11012022") format("woff2"), url("https://posthog.com/assets/fonts/roboto/roboto_condensed_reg-webfont.woff?11012022")`, + `url("https://app.posthog.com/fonts/my-font.woff2")`, + ])('should replace font urls in stylesheets', (content: string) => { + expect(CorsPlugin._replaceFontCssUrls(content)).toMatchSnapshot() + }) + + it.each(['https://app.posthog.com/fonts/my-font.woff2?t=1234', 'https://app.posthog.com/fonts/my-font.ttf'])( + 'should replace font urls in links', + (content: string) => { + expect(CorsPlugin._replaceFontUrl(content)).toMatchSnapshot() + } + ) + + it.each(['https://app.posthog.com/my-image.jpeg'])( + 'should not replace non-font urls in links', + (content: string) => { + expect(CorsPlugin._replaceFontUrl(content)).toEqual(content) + } + ) +}) diff --git a/frontend/src/scenes/session-recordings/player/rrweb/index.ts b/frontend/src/scenes/session-recordings/player/rrweb/index.ts new file mode 100644 index 0000000000000..f2032d070d4a0 --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/rrweb/index.ts @@ -0,0 +1,38 @@ +import { ReplayPlugin, playerConfig } from 'rrweb/typings/types' + +const PROXY_URL = 'https://replay.ph-proxy.com' as const + +export const CorsPlugin: ReplayPlugin & { + _replaceFontCssUrls: (value: string) => string + _replaceFontUrl: (value: string) => string +} = { + _replaceFontCssUrls: (value: string): string => { + return value.replace( + /url\("(https:\/\/\S*(?:.eot|.woff2|.ttf|.woff)\S*)"\)/gi, + `url("${PROXY_URL}/proxy?url=$1")` + ) + }, + + _replaceFontUrl: (value: string): string => { + return value.replace(/^(https:\/\/\S*(?:.eot|.woff2|.ttf|.woff)\S*)$/i, `${PROXY_URL}/proxy?url=$1`) + }, + + onBuild: (node) => { + if (node.nodeName === 'STYLE') { + const styleElement = node as HTMLStyleElement + styleElement.innerText = CorsPlugin._replaceFontCssUrls(styleElement.innerText) + } + + if (node.nodeName === 'LINK') { + const linkElement = node as HTMLLinkElement + linkElement.href = CorsPlugin._replaceFontUrl(linkElement.href) + } + }, +} + +export const COMMON_REPLAYER_CONFIG: Partial = { + triggerFocus: false, + insertStyleRules: [ + `.ph-no-capture { background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNOCAwSDE2TDAgMTZWOEw4IDBaIiBmaWxsPSIjMkQyRDJEIi8+CjxwYXRoIGQ9Ik0xNiA4VjE2SDhMMTYgOFoiIGZpbGw9IiMyRDJEMkQiLz4KPC9zdmc+Cg=="); }`, + ], +} diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index e7668af245c1b..2ad9a9dfae4f5 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -31,7 +31,12 @@ import { SessionRecordingPlayerExplorerProps } from './view-explorer/SessionReco import { createExportedSessionRecording } from '../file-playback/sessionRecordingFilePlaybackLogic' import { RefObject } from 'react' import posthog from 'posthog-js' +import { COMMON_REPLAYER_CONFIG, CorsPlugin } from './rrweb' import { now } from 'lib/dayjs' +import { ReplayPlugin } from 'rrweb/typings/types' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' export const PLAYBACK_SPEEDS = [0.5, 1, 2, 3, 4, 8, 16] export const ONE_FRAME_MS = 100 // We don't really have frames but this feels granular enough @@ -102,6 +107,10 @@ export const sessionRecordingPlayerLogic = kea( ['speed', 'skipInactivitySetting'], userLogic, ['hasAvailableFeature'], + preflightLogic, + ['preflight'], + featureFlagLogic, + ['featureFlags'], ], actions: [ sessionRecordingDataLogic(props), @@ -471,16 +480,24 @@ export const sessionRecordingPlayerLogic = kea( return } + const plugins: ReplayPlugin[] = [] + + // We don't want non-cloud products to talk to our proxy as it likely won't work, but we _do_ want local testing to work + if ( + values.featureFlags[FEATURE_FLAGS.SESSION_REPLAY_CORS_PROXY] && + (values.preflight?.cloud || window.location.hostname === 'localhost') + ) { + plugins.push(CorsPlugin) + } + const replayer = new Replayer(values.sessionPlayerData.snapshotsByWindowId[windowId], { root: values.rootFrame, - triggerFocus: false, - insertStyleRules: [ - `.ph-no-capture { background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNOCAwSDE2TDAgMTZWOEw4IDBaIiBmaWxsPSIjMkQyRDJEIi8+CjxwYXRoIGQ9Ik0xNiA4VjE2SDhMMTYgOFoiIGZpbGw9IiMyRDJEMkQiLz4KPC9zdmc+Cg=="); }`, - ], + ...COMMON_REPLAYER_CONFIG, // these two settings are attempts to improve performance of running two Replayers at once // the main player and a preview player mouseTail: props.mode !== SessionRecordingPlayerMode.Preview, useVirtualDom: false, + plugins, }) actions.setPlayer({ replayer, windowId }) diff --git a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png index 8b8203a70dcf5..3b185216c6362 100644 Binary files a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png and b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Hidden-Mobile-1-chromium-linux.png differ