PostHog offers a range of controls to limit what data is captured by session recordings. Our privacy controls run in the browser or mobile app. So, masked data is never sent over the network to PostHog.
Input elements
As any input element is highly likely to contain sensitive text such as email or password, we mask these by default. You can explicitly set this to false to disable the masking. You can then specify inputs types you would like to be masked.
posthog.init('<ph_project_api_key>', {session_recording: {maskAllInputs: false,maskInputOptions: {password: true, // Highly recommended as a minimum!!// color: false,// date: false,// 'datetime-local': false,// email: false,// month: false,// number: false,// range: false,// search: false,// tel: false,// text: false,// time: false,// url: false,// week: false,// textarea: false,// select: false,}})
Mask or un-mask specific inputs
You can control the masking more granularly by using maskInputFn
to customize how the masking behaves. For example, you may want to only redact text that looks like an email address, or comes from inputs that aren't search boxes.
posthog.init('<ph_project_api_key>', {session_recording: {// `maskInputFn` only applies to selected elements, so make sure to set this to true if you want this to work globallymaskAllInputs: true,maskInputFn: (text, element) => {if (element?.attributes['type']?.value === 'search') {// If this is a search input, don't mask itreturn text}// Otherwise, mask it with asterisksreturn '*'.repeat(text.length)},}})
Text elements
General text is not masked by default, but we provide multiple options for masking text:
Mask all text
IMPORTANT: The
text
related config options only apply to non-input text. Inputs are masked differently and have separate methods (as detailed above).
posthog.init('<ph_project_api_key>', {session_recording: {maskTextSelector: "*" // Masks all text elements (not including inputs)}})
Mask or un-mask specific text
You can use a CSS selector to mask specific elements. For example, you may want to mask all elements with the class email
or the ID sensitive
.
posthog.init('<ph_project_api_key>', {session_recording: {maskTextSelector: ".email, #sensitive" // masks all elements with the class "email" or the ID "sensitive". This does not apply to input elements.}})
You can further control the text that gets masked. For example, by only masking text that looks like an email
posthog.init('<ph_project_api_key>', {session_recording: {// `maskTextFn` only applies to selected elements, so make sure to set to "*" if you want this to work globally.// This does not apply to input elements.maskTextSelector: "*",maskTextFn: (text) => {// A simple email regex - you may want to use something more advancedconst emailRegex = /(\S+)@(\S+\.\S+)/greturn text.replace(emailRegex, (match, g1, g2) => {// Replace each email with asterisks - ben@posthog.com becomes ***@***********return '*'.repeat(g1.length) + '@' + '*'.repeat(g2.length)})},}})
Other Elements
If your application displays sensitive user information outside of input or text fields, or if there are areas of your application that you simply don't want to capture, you need to update your codebase to prevent PostHog from capturing this information during session recordings.
To do so, you should add the CSS class name ph-no-capture
to elements which should not be recorded. This will lead to the element being replaced with a block of the same size when you play back the recordings. Make sure everyone who watches recordings in your team is aware of this, so that they don't think your product is broken when something doesn't show up!
<div class="ph-no-capture">I won't be captured at all!</div>
Note that adding ph-no-capture
will also prevent any autocapture events from being captured from that element.
Common example configs
Maximum privacy - mask everything
{maskAllInputs: true,maskTextSelector: "*"}
Limited privacy - try to mask all email / password related things
You can mask content that looks like it contains an email or password.
For passwords, it is important to note that "click to show password" buttons typically turn the input type to text
. This would then reveal the password. Thus instead of checking for type='password'
, you need to check a different field, like id
:
{maskAllInputs: true,maskInputFn: (text, element) => {const maskTypes = ['email', 'password']if (maskTypes.indexOf(element?.attributes['type']?.value) !== -1 ||maskTypes.indexOf(element?.attributes['id']?.value) !== -1) {return '*'.repeat(text.length)}return text},maskTextSelector: '*',maskTextFn(text) {// A simple email regex - you may want to use something more advancedconst emailRegex = /(\S+)@(\S+\.\S+)/greturn text.replace(emailRegex, (match, g1, g2) => {// Replace each email with asterisks - ben@posthog.com becomes ***@***********return '*'.repeat(g1.length) + '@' + '*'.repeat(g2.length)})}}
Selective privacy - only reveal things that are marked as safe
Instead of selectively masking, we can selectively unmask fields that you are sure are safe.
This assumes you are adding a data attribute to every "safe" element like <p data-record="true">I will be visible!</p>
{maskAllInputs: true,maskInputFn: (text, element) => {if (element?.dataset['record'] === 'true') {return text}return '*'.repeat(text.length)},maskTextSelector: ":not([data-record='true'])",}