From 38b5a8d25b3df45f29b070e5b25ffb2cd7d3ee79 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 10 Jun 2026 20:25:19 -0700 Subject: [PATCH 1/5] Flush replay events on pagehide with a shorter interval --- src/recorder/index.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index b3262c79c..e22ab9b98 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -22,7 +22,7 @@ import { record } from 'rrweb'; const configEndpoint = `${hostBase}__RECORDER_CONFIG_ENDPOINT__`.replace('{websiteId}', website); const REPLAY_FLUSH_EVENT_COUNT = 100; - const REPLAY_FLUSH_INTERVAL = 10000; + const REPLAY_FLUSH_INTERVAL = 2000; const HEATMAP_FLUSH_EVENT_COUNT = 20; const HEATMAP_FLUSH_INTERVAL = 5000; @@ -104,11 +104,6 @@ import { record } from 'rrweb'; ); }; - const scheduleReplayFlush = () => { - if (replayFlushTimer) clearTimeout(replayFlushTimer); - replayFlushTimer = setTimeout(() => flushReplay(), REPLAY_FLUSH_INTERVAL); - }; - const scheduleHeatmapFlush = () => { if (heatmapFlushTimer) clearTimeout(heatmapFlushTimer); heatmapFlushTimer = setTimeout(() => flushHeatmap(), HEATMAP_FLUSH_INTERVAL); @@ -133,7 +128,7 @@ import { record } from 'rrweb'; replayStopped = true; - if (replayFlushTimer) clearTimeout(replayFlushTimer); + if (replayFlushTimer) clearInterval(replayFlushTimer); flushReplay(); if (replayStopFn) { @@ -267,6 +262,8 @@ import { record } from 'rrweb'; const beginReplayCapture = () => { replayStartTime = Date.now(); + replayFlushTimer = setInterval(() => flushReplay(), REPLAY_FLUSH_INTERVAL); + replayStopFn = record({ emit(event) { if (replayStopped) return; @@ -281,8 +278,6 @@ import { record } from 'rrweb'; if (replayBuffer.length >= REPLAY_FLUSH_EVENT_COUNT) { flushReplay(); } - - scheduleReplayFlush(); }, ...getMaskConfig(maskLevel), inlineStylesheet: true, @@ -465,6 +460,10 @@ import { record } from 'rrweb'; } }); + window.addEventListener('pagehide', () => { + flushReplay(true); + }); + window.addEventListener('beforeunload', () => { flushReplay(true); }); From 1118f4b28e458fadc0f919e14e5f77c8f62bd0f4 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 10 Jun 2026 21:41:24 -0700 Subject: [PATCH 2/5] Upgrade rrweb and rrweb-player to 2.0.1 --- package.json | 4 +- pnpm-lock.yaml | 139 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 114 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 9a71fddd3..812040b5a 100644 --- a/package.json +++ b/package.json @@ -112,8 +112,8 @@ "react-window": "^2.2.7", "redis": "^5.12.1", "request-ip": "^3.3.0", - "rrweb": "2.0.0-alpha.4", - "rrweb-player": "1.0.0-alpha.4", + "rrweb": "^2.0.1", + "rrweb-player": "^2.0.1", "semver": "^7.8.0", "serialize-error": "^13.0.1", "thenby": "^1.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 600758496..88f32fbcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,11 +186,11 @@ importers: specifier: ^3.3.0 version: 3.3.0 rrweb: - specifier: 2.0.0-alpha.4 - version: 2.0.0-alpha.4 + specifier: ^2.0.1 + version: 2.0.1 rrweb-player: - specifier: 1.0.0-alpha.4 - version: 1.0.0-alpha.4 + specifier: ^2.0.1 + version: 2.0.1 semver: specifier: ^7.8.0 version: 7.8.0 @@ -331,6 +331,8 @@ importers: specifier: ^4.1.6 version: 4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.0)(typescript@6.0.3))(vite@8.0.11(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.2)) + dist: {} + packages: '@adobe/css-tools@4.4.4': @@ -622,24 +624,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.4.15': resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.15': resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.4.15': resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.4.15': resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} @@ -1620,89 +1626,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1844,24 +1866,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -1934,36 +1960,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -2278,36 +2310,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} @@ -2454,66 +2492,79 @@ packages: resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.4': resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.4': resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.4': resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.4': resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.4': resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.4': resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.4': resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.4': resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.4': resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.4': resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.4': resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.4': resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.4': resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} @@ -2545,8 +2596,17 @@ packages: cpu: [x64] os: [win32] - '@rrweb/types@2.0.0-alpha.20': - resolution: {integrity: sha512-RbnDgKxA/odwB1R4gF7eUUj+rdSrq6ROQJsnMw7MIsGzlbSYvJeZN8YY4XqU0G6sKJvXI6bSzk7w/G94jNwzhw==} + '@rrweb/packer@2.0.1': + resolution: {integrity: sha512-2U8RkVe+ZK3gTtmKjJKjoh6QL1Q6wIlt3sBTJeCPERLr9e0JIimNrZ1+bog1L30CTVP3ULuDf5Ba7RZO+QNcHg==} + + '@rrweb/replay@2.0.1': + resolution: {integrity: sha512-UrZONirb8o4t0urg1aJ8aMSWhNHvJ3a2usuVbPJIm/edyFbwTvvY9sg/rrXccyXOntFhhp73R5nCretODRZcCw==} + + '@rrweb/types@2.0.1': + resolution: {integrity: sha512-x8cNaKFxdvS0/5IfKOj1xG8Htnw1fUwqoPqgP61bOaWqZL6t9UtRPpO9ZlMB2z+FiTxg9zlFFL+Da4ZpFuhoiA==} + + '@rrweb/utils@2.0.1': + resolution: {integrity: sha512-t+b+4rB23/e4EpoGMdPUr6cKtDsFuOb3LxA9IHE8e3A/Xo0nMUfH0bkwqdpUNa/VGDOdiGeG3bhHjuO9zWX54g==} '@schummar/icu-type-parser@1.21.5': resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} @@ -2702,36 +2762,42 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.33': resolution: {integrity: sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-ppc64-gnu@1.15.33': resolution: {integrity: sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@swc/core-linux-s390x-gnu@1.15.33': resolution: {integrity: sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==} engines: {node: '>=10'} cpu: [s390x] os: [linux] + libc: [glibc] '@swc/core-linux-x64-gnu@1.15.33': resolution: {integrity: sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.33': resolution: {integrity: sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.33': resolution: {integrity: sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==} @@ -4270,24 +4336,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -5535,17 +5605,17 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrdom@0.1.7: - resolution: {integrity: sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==} + rrdom@2.0.1: + resolution: {integrity: sha512-z8GdQWFmVfa8GpcGkAqfBXseb89z0ciTPSBkID0qr0i6KHCye1MdQ4VoXFf0Ejp3ELe/c7AObu4zsbSbMT+hiw==} - rrweb-player@1.0.0-alpha.4: - resolution: {integrity: sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==} + rrweb-player@2.0.1: + resolution: {integrity: sha512-v8PVnOy7Q3kXqWriGuzZo4ndGEV06nCMRkH/jSHweJfTLwpLqb2evgMj0S5FwQLsN2X1VM+L2/i7QWGJfgj9zw==} - rrweb-snapshot@2.0.0-alpha.4: - resolution: {integrity: sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==} + rrweb-snapshot@2.0.1: + resolution: {integrity: sha512-H8TA29ct2tpYtr5oaB0N+ksRY9bhEErfAYcKbce4+w8lqEUQChpKZd41HpBJLkp04b5QUfWUg4v6r9Okf8r7vA==} - rrweb@2.0.0-alpha.4: - resolution: {integrity: sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==} + rrweb@2.0.1: + resolution: {integrity: sha512-MQYzOtI7aZft9ffDMT6rmRSAj6hO4W6T7iA4eB9Mtcuiv3M7DbnnxDMrcPW0sQXepgYN46YhfSKe75d/mlUHrQ==} run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -8390,7 +8460,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true - '@rrweb/types@2.0.0-alpha.20': {} + '@rrweb/packer@2.0.1': + dependencies: + '@rrweb/types': 2.0.1 + fflate: 0.4.8 + + '@rrweb/replay@2.0.1': + dependencies: + '@rrweb/types': 2.0.1 + rrweb: 2.0.1 + + '@rrweb/types@2.0.1': {} + + '@rrweb/utils@2.0.1': {} '@schummar/icu-type-parser@1.21.5': {} @@ -11185,7 +11267,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -11566,27 +11648,30 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 - rrdom@0.1.7: + rrdom@2.0.1: dependencies: - rrweb-snapshot: 2.0.0-alpha.4 + rrweb-snapshot: 2.0.1 - rrweb-player@1.0.0-alpha.4: + rrweb-player@2.0.1: dependencies: + '@rrweb/packer': 2.0.1 + '@rrweb/replay': 2.0.1 '@tsconfig/svelte': 1.0.13 - rrweb: 2.0.0-alpha.4 - rrweb-snapshot@2.0.0-alpha.4: {} - - rrweb@2.0.0-alpha.4: + rrweb-snapshot@2.0.1: dependencies: - '@rrweb/types': 2.0.0-alpha.20 + postcss: 8.5.15 + + rrweb@2.0.1: + dependencies: + '@rrweb/types': 2.0.1 + '@rrweb/utils': 2.0.1 '@types/css-font-loading-module': 0.0.7 '@xstate/fsm': 1.6.5 base64-arraybuffer: 1.0.2 - fflate: 0.4.8 mitt: 3.0.1 - rrdom: 0.1.7 - rrweb-snapshot: 2.0.0-alpha.4 + rrdom: 2.0.1 + rrweb-snapshot: 2.0.1 run-parallel@1.2.0: dependencies: From 475d9548de5960262394ddd0db39e54e32d6340b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 10 Jun 2026 23:18:50 -0700 Subject: [PATCH 3/5] Reject oversized replay payloads and chunk recorder flushes --- src/app/api/record/route.ts | 34 ++++++++++++- src/lib/response.ts | 9 ++++ src/recorder/index.js | 95 ++++++++++++++++++++++++++++++------- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/src/app/api/record/route.ts b/src/app/api/record/route.ts index ad304a27c..7fa267b79 100644 --- a/src/app/api/record/route.ts +++ b/src/app/api/record/route.ts @@ -8,7 +8,7 @@ import { parseToken } from '@/lib/jwt'; import { fetchAccount, fetchTeam } from '@/lib/load'; import { getRecorderConfig } from '@/lib/recorder'; import { parseRequest } from '@/lib/request'; -import { badRequest, forbidden, json, serverError } from '@/lib/response'; +import { badRequest, forbidden, json, payloadTooLarge, serverError } from '@/lib/response'; import { getWebsite } from '@/queries/prisma'; import { saveRecording } from '@/queries/sql'; import { saveHeatmapEvents } from '@/queries/sql/heatmap/saveHeatmapEvents'; @@ -18,6 +18,8 @@ interface Cache { visitId: string; } +const MAX_RECORD_REQUEST_BYTES = 1_000_000; + const schema = z.discriminatedUnion('type', [ z.object({ type: z.literal('record'), @@ -73,8 +75,38 @@ function getUrlPath(url: string) { } } +async function getRequestBodySize(request: Request): Promise { + const contentLength = request.headers.get('content-length'); + + if (contentLength) { + const size = Number(contentLength); + + if (Number.isFinite(size)) { + return size; + } + } + + try { + const text = await request.clone().text(); + + return new TextEncoder().encode(text).length; + } catch { + return null; + } +} + export async function POST(request: Request) { try { + const requestBodySize = await getRequestBodySize(request); + + if (requestBodySize && requestBodySize > MAX_RECORD_REQUEST_BYTES) { + return payloadTooLarge({ + reason: 'payload_too_large', + maxBytes: MAX_RECORD_REQUEST_BYTES, + size: requestBodySize, + }); + } + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); if (error) { diff --git a/src/lib/response.ts b/src/lib/response.ts index f1ad5c7bf..587802619 100644 --- a/src/lib/response.ts +++ b/src/lib/response.ts @@ -36,6 +36,15 @@ export function forbidden(error?: Record) { ); } +export function payloadTooLarge(error?: Record) { + return Response.json( + { + error: { message: 'Payload too large', code: 'payload-too-large', status: 413, ...error }, + }, + { status: 413 }, + ); +} + export function notFound(error?: Record) { return Response.json( { error: { message: 'Not found', code: 'not-found', status: 404, ...error } }, diff --git a/src/recorder/index.js b/src/recorder/index.js index e22ab9b98..9f5950ec3 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -23,6 +23,7 @@ import { record } from 'rrweb'; const REPLAY_FLUSH_EVENT_COUNT = 100; const REPLAY_FLUSH_INTERVAL = 2000; + const REPLAY_MAX_PAYLOAD_SIZE = 900000; const HEATMAP_FLUSH_EVENT_COUNT = 20; const HEATMAP_FLUSH_INTERVAL = 5000; @@ -45,12 +46,8 @@ import { record } from 'rrweb'; const getSessionCache = () => window.umami?.getSession?.()?.cache; - const sendPayload = (type, payload, useKeepalive = false) => { - const cache = getSessionCache(); - - if (!cache) return; - - const body = JSON.stringify({ + const getPayloadBody = (type, payload) => + JSON.stringify({ type, payload: { website, @@ -58,7 +55,28 @@ import { record } from 'rrweb'; }, }); - const keepalive = useKeepalive && body.length < 60000; + const getPayloadSize = body => { + try { + return new Blob([body]).size; + } catch { + return body.length; + } + }; + + const getReplayPayloadSize = (events, timestamp) => + getPayloadSize(getPayloadBody('record', { events, timestamp })); + + const isReplayPayloadTooLarge = (events, timestamp) => + getReplayPayloadSize(events, timestamp) > REPLAY_MAX_PAYLOAD_SIZE; + + const sendPayload = (type, payload, useKeepalive = false) => { + const cache = getSessionCache(); + + if (!cache) return; + + const body = getPayloadBody(type, payload); + + const keepalive = useKeepalive && getPayloadSize(body) < 60000; return fetch(endpoint, { keepalive, @@ -72,20 +90,55 @@ import { record } from 'rrweb'; }).catch(() => {}); }; + const sendReplayEvents = (events, timestamp, useKeepalive = false) => { + let chunk = []; + let chunkOffset = 0; + + events.forEach(event => { + const candidate = [...chunk, event]; + + if (isReplayPayloadTooLarge(candidate, timestamp + chunkOffset)) { + if (chunk.length) { + sendPayload( + 'record', + { + events: chunk, + timestamp: timestamp + chunkOffset, + }, + useKeepalive, + ); + chunk = []; + chunkOffset += 1; + } + + if (isReplayPayloadTooLarge([event], timestamp + chunkOffset)) { + chunkOffset += 1; + return; + } + } + + chunk.push(event); + }); + + if (chunk.length) { + sendPayload( + 'record', + { + events: chunk, + timestamp: timestamp + chunkOffset, + }, + useKeepalive, + ); + } + }; + const flushReplay = (useKeepalive = false) => { if (!replayBuffer.length) return; const events = replayBuffer; replayBuffer = []; - sendPayload( - 'record', - { - events, - timestamp: Math.floor(Date.now() / 1000), - }, - useKeepalive, - ); + sendReplayEvents(events, Math.floor(Date.now() / 1000), useKeepalive); }; const flushHeatmap = (useKeepalive = false) => { @@ -273,9 +326,19 @@ import { record } from 'rrweb'; return; } + if ( + replayBuffer.length && + isReplayPayloadTooLarge([...replayBuffer, event], Math.floor(Date.now() / 1000)) + ) { + flushReplay(); + } + replayBuffer.push(event); - if (replayBuffer.length >= REPLAY_FLUSH_EVENT_COUNT) { + if ( + replayBuffer.length >= REPLAY_FLUSH_EVENT_COUNT || + isReplayPayloadTooLarge(replayBuffer, Math.floor(Date.now() / 1000)) + ) { flushReplay(); } }, From fa4318fb87a1f4be8ca3d9f51dbbf41056ed6a40 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 11 Jun 2026 17:55:44 -0700 Subject: [PATCH 4/5] Tighten side nav spacing and remove extra bottom margin --- src/app/(main)/SideNav.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index c44011654..3f2b25390 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -74,7 +74,7 @@ export function SideNav(props: any) { backgroundColor="surface-base" border borderRadius - paddingX="2" + padding="2" flexGrow="1" minHeight="0" margin="2" @@ -137,7 +137,7 @@ export function SideNav(props: any) { )} - + From f69f76b3b1ac39e48ad2d691c45f1f601515105e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 11 Jun 2026 19:01:04 -0700 Subject: [PATCH 5/5] Handle invalid IPs and localhost lookup failures in location detection --- src/lib/detect.test.ts | 57 +++++++++++++++++++++++++++++++++++++++++- src/lib/detect.ts | 10 +++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/lib/detect.test.ts b/src/lib/detect.test.ts index 6aaab223b..804c84029 100644 --- a/src/lib/detect.test.ts +++ b/src/lib/detect.test.ts @@ -1,8 +1,23 @@ -import { expect, test } from 'vitest'; +import { beforeEach, expect, test, vi } from 'vitest'; +import { getLocation, hasBlockedIp } from './detect'; import { getIpAddress } from './ip'; const IP = '127.0.0.1'; +const isLocalhost = vi.mocked(await import('is-localhost-ip')); + +vi.mock('is-localhost-ip', () => ({ + default: vi.fn(), +})); + +beforeEach(() => { + vi.resetAllMocks(); + + delete process.env.CLIENT_IP_HEADER; + delete process.env.IGNORE_IP; + delete process.env.SKIP_LOCATION_HEADERS; +}); + test('getIpAddress: Custom header', () => { process.env.CLIENT_IP_HEADER = 'x-custom-ip-header'; @@ -20,3 +35,43 @@ test('getIpAddress: Standard header', () => { test('getIpAddress: No header', () => { expect(getIpAddress(new Headers())).toEqual(undefined); }); + +test('getLocation: returns null for malformed ip', async () => { + await expect( + getLocation( + 'not-an-ip', + new Headers({ + 'cf-ipcountry': 'US', + 'cf-region-code': 'CA', + 'cf-ipcity': 'Los Angeles', + }), + false, + ), + ).resolves.toEqual(null); +}); + +test('getLocation: treats localhost check errors as non-local', async () => { + isLocalhost.default.mockRejectedValue(new Error('DNS Lookup failed.')); + + await expect( + getLocation( + '8.8.8.8', + new Headers({ + 'cf-ipcountry': 'US', + 'cf-region-code': 'CA', + 'cf-ipcity': 'Los Angeles', + }), + false, + ), + ).resolves.toEqual({ + country: 'US', + region: 'US-CA', + city: 'Los Angeles', + }); +}); + +test('hasBlockedIp: returns false for malformed client ip with cidr block', () => { + process.env.IGNORE_IP = '10.0.0.0/8'; + + expect(hasBlockedIp('not-an-ip')).toBe(false); +}); diff --git a/src/lib/detect.ts b/src/lib/detect.ts index fb67571d9..fb36ba212 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -76,11 +76,19 @@ function decodeHeader(s: string | undefined | null): string | undefined | null { return Buffer.from(s, 'latin1').toString('utf-8'); } +async function isLocalIp(ip: string) { + try { + return await isLocalhost(ip); + } catch { + return false; + } +} + export async function getLocation(ip: string = '', headers: Headers, skipHeaders: boolean) { const cleanIp = stripPort(ip); // Ignore local or invalid ips - if (!cleanIp || !ipaddr.isValid(cleanIp) || (await isLocalhost(cleanIp))) { + if (!cleanIp || !ipaddr.isValid(cleanIp) || (await isLocalIp(cleanIp))) { return null; }