Privacy Controls

Last updated:

|Edit this page

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.

TypeScript
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.

TypeScript
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 globally
maskAllInputs: true,
maskInputFn: (text, element) => {
if (element?.attributes['type']?.value === 'search') {
// If this is a search input, don't mask it
return text
}
// Otherwise, mask it with asterisks
return '*'.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).

TypeScript
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.

TypeScript
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.
}
})

IMPORTANT: The masking of an element applies to its children meaning :not selectors do not work. The ability to selectively mask elements is detailed below

You can further control the text that gets masked. For example, by only masking text that looks like an email

TypeScript
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 advanced
const emailRegex = /(\S+)@(\S+\.\S+)/g
return 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!

HTML
<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: "*"
}

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 advanced
const emailRegex = /(\S+)@(\S+\.\S+)/g
return 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,
maskTextSelector: "*",
maskTextFn: (text, element) => {
if (element?.dataset['record'] === 'true') {
return text
}
return '*'.repeat(text.length)
},
}

Network capture

Session replay also allows you to capture network requests and responses. Headers and bodies can include sensitive information. We scrub some headers automatically, but if your network requests and responses include sensitive information you can provide a function to scrub them. Read more in our network capture docs

Questions?

  • Gavin
    10 days ago

    Does Masking - Nomral (mask inputs but not text/images) work on <div contenteditable='true' class='text-area'>

    We have some input elements that are now div's - it doesn't look like masking works in this case.

    Our masking setting is 'Normal (mask inputs but not text/images)'

    • Paul(he/him)
      10 days ago

      hey,

      no... the default masking only works on inputs so wouldn't affect a contenteditable div at all (it would be seen as a text element)

      you most likely need to use one of the custom text masking options in the SDK itself

      https://github.com/PostHog/posthog-js/blob/main/src/types.ts#L963-L980

      for example you could pass a maskTextFn and check if the element was contenteditable

    • Gavin
      Author10 days ago

      Ok, thanks Paul!

  • Neil
    a month ago

    User-level privacy controls

    Say I have a core team, and an extended team. I want the core team to be able to see all content unmasked, but I want the extended team to only see masked content. Is there a way to do this?

    It looks like the current settings change what is recorded, not what is shown. Ideally this feature would exist at the org level (so I can invite extended team members with the right permission), but it would also be ok if I could set it on the sharing URL when I share an embedded replay.

  • David
    6 months ago

    maskAllTextInputs too broad

    Is it possible to break out text from text inputs? Would like to capture application text but not user input.

  • Nick
    6 months ago

    Why use accessibilityLabel?

    Isn't this an issue for users with screen readers? The screen reader will tell them the "ph-no-capture" instead of something more useful like the actually name of the element.

    • Peter
      3 months ago

      Hi, Max AI suggest that for iOS either of the following can be used for custom masking: accessibilityLabel="ph-no-capture" accessibilityIdentifier="ph-no-capture"

      Is it possible to use accessibilityIdentifier="ph-no-capture" in React-Native.
      We cannot allow our screen readers to be polluted with accessibilityLabel="ph-no-capture" .

      Thanks Peter

  • Alex
    9 months ago

    Unmask by URL

    Hi,

    I have a single webapp an unauthed marketing section and an authed console. I would like to apply full masking to the console but unmask all text for the marketing sections. Is it possible to apply URL based filtering to the maskTextFn? Something like:

    maskTextFn: (text, element) => {
    if (url.contains('/app/') {
    // Full masking on the console
    return '*'.repeat(text.trim().length)
    }
    // No masking, on the marketing site
    return text
    },
  • Anton
    a year ago

    Masking texts

    Hey there,

    We've been experimenting with unmasking specific texts in our project using the PostHog npm package (version 1.118.0). Following the documentation's example, we've added the data-record="true" attribute to elements we want to keep unmasked, and initialized PostHog with the maskTextSelector: ":not([data-record='true'])" configuration.

    However, it seems like all texts on the screen are still being masked. We've tried various approaches to diagnose the issue, and it appears that the :not selector isn't functioning as expected. Interestingly, when we removed the :not part, it did indeed only mask the elements with the specified attribute.

    We've even experimented with different types of elements, including <p data-record="true">I will be visible!</p> as suggested in the documentation, but unfortunately, we're still encountering the same behavior.

    Any insights or suggestions on how to resolve this issue would be greatly appreciated.

    Thanks in advance!

    • David
      a year agoSolution

      Hi Anton,

      This issue has now been resolved. You will need to upgrade to the latest version of the PostHog SDK (v1.120.3 as of today) and follow the docs to selectively mask elements.

      Note: a negative maskTextSelector is not enough on its own to selectively show elements, you will need to use the maskTextFn too.

  • Jura
    a year ago

    What's the best practice legal-wise?

    From the legal standpoint, what's the recommended approach to configuring session capture privacy?

    Say, if I'm configuring screen capture for a collaborative document editing service like Google Docs, my gut feeling would be to obscure everything that's within the document canvas, except for controls such as context menus. However, I'm not sure if my gut feeling aligns well with the law in Europe or the US.

    Is it more like I'm free to screen capture whatever I want as long as it's explicitly stated in my privacy policy and I make sure to get content? Or is there a legal risk even when the privacy policy is explicit and consent has been received?

    • Andy
      a year agoSolution

      We can't provide legal advice, but I'd make two points:

      1. It's better to be safe than sorry. If there's no reason to capture this data, it's best not to.

      2. We recently added canvas capture to session replay, but it doesn't currently support privacy masking. See the changelog update for more.

  • Upendra
    a year ago

    Saving session-recording in my storage

    After the session recording is done, can i save it in my S3 Bucket/Some other storage location instead of sending it to posthog ?

    • Paul(he/him)
      a year ago

      Hey,

      There's no way to use session recording & PostHog Cloud without sending data to PostHog.

      You can self-host PostHog if you have to be sure data isn't sent to systems you don't control.

      Out of interest what does this solve for you?

  • Neima
    2 years ago

    Google Tag Manager

    Hi,

    Is this possible to do with the posthog tag? Or would I have to install the posthog library to get this level of customization?

    • Marcus
      2 years agoSolution

      That is possible with the PostHog snippet, you'll just need to modify the init function with those settings.

Was this page useful?

Next article

Sharing or embedding replays

Replays are typically viewed in Session Replay part of PostHog. However, you may want to share specific recordings with people outside of your PostHog organization. You can share them directly using a URL, or embed them in a webpage using an iframe: Above is an embedded recording of posthog.com… on posthog.com – very meta. How to share session replays Method one: Sharing via the PostHog app When viewing any recording, press Share . You can then configure the recording for external sharing…

Read next article