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: 6 additions & 0 deletions .changeset/popup-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rrweb/rrweb-plugin-popup-record': minor
'@rrweb/rrweb-plugin-popup-replay': minor
---

Add popup plugin pair that records native `window.alert` / `window.confirm` / `window.prompt` popups (message, prompt default value, and user response) and surfaces them during replay via an `onPopup` callback.
104 changes: 104 additions & 0 deletions docs/recipes/popup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Popup Recorder and Replayer

Native browser popups — `window.alert`, `window.confirm`, and `window.prompt` — are rendered as browser chrome outside the DOM, so rrweb does not capture them by default. This plugin records them (the message, a `prompt`'s default value, and the user's response) and lets you surface them during replay.

### Enable Recording Popups

You can enable using the default options like this:

```js
import { record } from '@rrweb/record';
import { getRecordPopupPlugin } from '@rrweb/rrweb-plugin-popup-record';

record({
emit: function emit(event) {
events.push(event);
},
// to use default record options
plugins: [getRecordPopupPlugin()],
});
```

Each captured popup is emitted as a Plugin event with the following payload:

```ts
type PopupData = {
kind: 'alert' | 'confirm' | 'prompt';
message: string;
defaultValue?: string; // prompt's second argument, when provided
returnValue?: boolean | string | null; // confirm → boolean, prompt → string | null; omitted for alert
};
```

Because native popups are synchronous and blocking, the plugin calls through to the real popup first (so the page behaves exactly as before) and records the user's response once they dismiss it.

You can also customize the behavior like this:

```js
import { record } from '@rrweb/record';
import { getRecordPopupPlugin } from '@rrweb/rrweb-plugin-popup-record';

record({
emit: function emit(event) {
events.push(event);
},
// customized record options
plugins: [
getRecordPopupPlugin({
// only hook confirm and prompt, ignore alert
popupKinds: ['confirm', 'prompt'],
// redact sensitive content before it is recorded
maskPopupData: (data) => {
return { ...data, message: maskTextFn(data.message) };
},
}),
],
});
```

Use `maskPopupData` to redact anything sensitive, **including the user's response**. Return a modified copy to mask the `message`, or drop `returnValue` entirely to avoid recording what the user typed into a `prompt` or answered in a `confirm`:

```js
getRecordPopupPlugin({
// record that a popup happened, but never store the user's response
maskPopupData: ({ returnValue, ...rest }) => rest,
});
```

All options are described below:

| key | default | description |
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| popupKinds | `['alert', 'confirm', 'prompt']` | Which native popups to hook. Override it to record only a subset. |
| maskPopupData | `(data) => data` | Transform the payload before it is emitted, e.g. to mask the `message` or drop the `returnValue` to hide the user's response. |

## Replay Popups

A native popup is a blocking browser dialog, so re-showing a real one during replay would freeze the player. Instead it is up to you to decide how to best replay your popup events using the `onPopup` callback (e.g. render a custom overlay, log a line, or add a timeline marker).

```js
import rrweb from 'rrweb';
import { getReplayPopupPlugin } from '@rrweb/rrweb-plugin-popup-replay';

const replayer = new rrweb.Replayer(events, {
plugins: [
getReplayPopupPlugin({
onPopup: (data) => {
const { kind, message, returnValue } = data;
console.log(`${kind}: ${message} -> ${returnValue}`);
},
}),
],
});
replayer.play();
```

Description of the replay option is as follows:

| key | default | description |
| ------- | ----------- | ------------------------------------------------------------------ |
| onPopup | `undefined` | Called for every recorded popup as it is replayed, with `PopupData`. |

## Technical Implementation

This implementation records native popups by patching [`window.alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [`window.confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), and [`window.prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt). Since these functions are synchronous and blocking, the patched wrapper calls through to the original popup first and captures its return value (the user's response) before emitting the event. On teardown the original functions are restored.
212 changes: 212 additions & 0 deletions packages/plugins/rrweb-plugin-popup-record/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# @rrweb/rrweb-plugin-popup-record

Please refer to the [popup recipe](../../../docs/recipes/popup.md) on how to use this plugin.
See the [guide](../../../guide.md) for more info on rrweb.

## Sponsors

[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.

### Gold Sponsors 🥇

<div dir="auto">

<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/0/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/1/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/2/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/3/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/4/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/5/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/gold-sponsor/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/gold-sponsor/6/avatar.svg?requireActive=false&avatarHeight=225" alt="sponsor"></a>

</div>

### Silver Sponsors 🥈

<div dir="auto">

<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/0/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/1/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/2/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/3/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/4/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/5/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/silver-sponsor/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/silver-sponsor/6/avatar.svg?requireActive=false&avatarHeight=158" alt="sponsor"></a>

</div>

### Bronze Sponsors 🥉

<div dir="auto">

<a href="https://opencollective.com/rrweb/tiers/sponsors/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/0/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/1/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/2/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/3/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/4/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/5/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/6/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/7/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/7/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>
<a href="https://opencollective.com/rrweb/tiers/sponsors/8/website?requireActive=false" target="_blank"><img src="https://opencollective.com/rrweb/tiers/sponsors/8/avatar.svg?requireActive=false&avatarHeight=70" alt="sponsor"></a>

</div>

### Backers

<a href="https://opencollective.com/rrweb#sponsor" rel="nofollow"><img src="https://opencollective.com/rrweb/tiers/backers.svg?avatarHeight=36"></a>

## Core Team Members

<table>
<tr>
<td align="center">
<a href="https://github.com/Yuyz0112">
<img
src="https://avatars.githubusercontent.com/u/13651389?s=100"
width="100px;"
alt=""
/>
<br /><sub><b>Yuyz0112</b></sub>
<br /><br />
</a>
</td>
<td align="center">
<a href="https://github.com/YunFeng0817">
<img
src="https://avatars.githubusercontent.com/u/27533910?s=100"
width="100px;"
alt=""
/>
<br /><sub><b>Yun Feng</b></sub>
<br /><br />
</a>
</td>
<td align="center">
<a href="https://github.com/eoghanmurray">
<img
src="https://avatars.githubusercontent.com/u/156780?s=100"
width="100px;"
alt=""
/>
<br /><sub><b>eoghanmurray</b></sub>
<br /><br />
</a>
</td>
<td align="center">
<a href="https://github.com/Juice10">
<img
src="https://avatars.githubusercontent.com/u/4106?s=100"
width="100px;"
alt=""
/>
<br /><sub><b>Juice10</b></sub>
<br /><sub>open for rrweb consulting</sub>
</a>
</td>
</tr>
</table>

## Who's using rrweb?

<table>
<tr>
<td align="center">
<a href="http://www.smartx.com/" target="_blank">
<img width="195px" src="https://raw.githubusercontent.com/rrweb-io/web/master/static/logos/smartx.png" alt="SmartX">
</a>
</td>
<td align="center">
<a href="https://posthog.com?utm_source=rrweb&utm_medium=sponsorship&utm_campaign=open-source-sponsorship" target="_blank">
<img width="195px" src="https://rrweb.com/posthog.png" alt="PostHog">
</a>
</td>
<td align="center">
<a href="https://statcounter.com/session-replay/" target="_blank">
<img width="195px" src="https://statcounter.com/images/logo-statcounter-arc-blue.svg">
</a>
</td>
<td align="center">
<a href="https://recordonce.com/" target="_blank">
<img width="195px" alt="Smart screen recording for SaaS" src="https://uploads-ssl.webflow.com/5f3d133183156245630d4446/5f3d1940abe8db8612c23521_Record-Once-logo-554x80px.svg">
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://sentry.io" target="_blank">
<img width="195px" src="https://rrweb.com/sentry.png" alt="Sentry">
</a>
</td>
<td align="center">
<a href="https://www.pendo.io" target="_blank">
<img width="195px" src="https://rrweb.com/pendo.png" alt="Pendo">
</a>
</td>
<td align="center">
<a href="https://mixpanel.com" target="_blank">
<img width="195px" src="https://rrweb.com/mixpanel.png" alt="Mixpanel">
</a>
</td>
<td align="center">
<a href="https://www.datadoghq.com" target="_blank">
<img width="195px" src="https://rrweb.com/datadog.png" alt="Datadog">
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://amplitude.com" target="_blank">
<img width="195px" src="https://rrweb.com/amplitude.png" alt="Amplitude">
</a>
</td>
<td align="center">
<a href="https://newrelic.com" target="_blank">
<img width="195px" src="https://rrweb.com/new%20relic.png" alt="New Relic">
</a>
</td>
<td align="center">
<a href="https://cux.io" target="_blank">
<img style="padding: 8px" alt="The first ever UX automation tool" width="195px" src="https://cux.io/cux-logo.svg">
</a>
</td>
<td align="center">
<a href="https://remsupp.com" target="_blank">
<img style="padding: 8px" alt="Remote Access & Co-Browsing" width="195px" src="https://remsupp.com/images/logo.png">
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://highlight.io" target="_blank">
<img style="padding: 8px" alt="The open source, fullstack Monitoring Platform." width="195px" src="https://github.com/highlight/highlight/raw/main/highlight.io/public/images/logo.png">
</a>
</td>
<td align="center">
<a href="https://analyzee.io" target="_blank">
<img style="padding: 8px" alt="Comprehensive data analytics platform that empowers businesses to gain valuable insights and make data-driven decisions." width="195px" src="https://analyzee.io/img/analyzee-main-logo.webp">
</a>
</td>
<td align="center">
<a href="https://requestly.io" target="_blank">
<img style="padding: 8px" alt="Intercept, Modify, Record & Replay HTTP Requests." width="195px" src="https://github.com/requestly/requestly/assets/16779465/652552db-c867-44cb-9bb5-94a2026e04ca">
</a>
</td>
<td align="center">
<a href="https://gleap.io" target="_blank">
<img style="padding: 8px" alt="In-app bug reporting & customer feedback platform." width="195px" src="https://assets-global.website-files.com/6506f3f29c68b1724807619d/6506f56010237164c6306591_GleapLogo.svg">
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://uxwizz.com" target="_blank">
<img style="padding: 8px" alt="Self-hosted website analytics with heatmaps and session recordings." width="195px" src="https://github.com/UXWizz/public-files/raw/main/assets/logo.png">
</a>
</td>
<td align="center">
<a href="https://www.howdygo.com" target="_blank">
<img style="padding: 8px" alt="Interactive product demos for small marketing teams" width="195px" src="https://assets-global.website-files.com/650afb446f1dd5bd410f00cc/650b2cec6188ff54dd9b01e1_Logo.svg">
</a>
</td>
</tr>
</table>
63 changes: 63 additions & 0 deletions packages/plugins/rrweb-plugin-popup-record/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@rrweb/rrweb-plugin-popup-record",
"version": "2.1.0",
"description": "",
"type": "module",
"main": "./dist/rrweb-plugin-popup-record.umd.cjs",
"module": "./dist/rrweb-plugin-popup-record.js",
"unpkg": "./dist/rrweb-plugin-popup-record.umd.cjs",
"jsdelivr": "./umd/rrweb-plugin-popup-record.js",
"typings": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/rrweb-plugin-popup-record.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/rrweb-plugin-popup-record.umd.cjs"
}
}
},
"files": [
"umd",
"dist",
"package.json"
],
"scripts": {
"dev": "vite build --watch",
"test": "vitest run",
"test:watch": "vitest watch",
"build": "yarn turbo run prepublish",
"check-types": "tsc -noEmit",
"prepublish": "tsc -noEmit && vite build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rrweb-io/rrweb.git"
},
"keywords": [
"rrweb"
],
"author": "rrweb Core Team <maintainers@rrweb.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/rrweb-io/rrweb/issues"
},
"homepage": "https://github.com/rrweb-io/rrweb#readme",
"devDependencies": {
"@rrweb/types": "^2.1.0",
"@rrweb/utils": "^2.1.0",
"rrweb": "^2.1.0",
"typescript": "^5.4.5",
"vite": "^6.0.1",
"vite-plugin-dts": "^3.9.1",
"vitest": "^1.4.0"
},
"peerDependencies": {
"rrweb": "^2.1.0",
"@rrweb/types": "^2.1.0",
"@rrweb/utils": "^2.1.0"
}
}
Loading