diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/template.html b/packages/browser-integration-tests/suites/replay/privacyDefault/template.html index a8279dad4d17..c83b62bf2e24 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/template.html +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/template.html @@ -9,6 +9,7 @@
This should be masked by default
This should be unmasked due to data attribute
+
Title should be masked
diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json index 964209872b06..69f74ba00da8 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json @@ -131,6 +131,21 @@ "textContent": "\n ", "id": 20 }, + { + "type": 2, + "tagName": "input", + "attributes": { + "data-sentry-unmask": "", + "placeholder": "Placeholder can be unmasked" + }, + "childNodes": [], + "id": 21 + }, + { + "type": 3, + "textContent": "\n ", + "id": 22 + }, { "type": 2, "tagName": "div", @@ -141,15 +156,15 @@ { "type": 3, "textContent": "***** ****** ** ******", - "id": 22 + "id": 24 } ], - "id": 21 + "id": 23 }, { "type": 3, "textContent": "\n ", - "id": 23 + "id": 25 }, { "type": 2, @@ -160,12 +175,12 @@ }, "childNodes": [], "isSVG": true, - "id": 24 + "id": 26 }, { "type": 3, "textContent": "\n ", - "id": 25 + "id": 27 }, { "type": 2, @@ -184,7 +199,7 @@ }, "childNodes": [], "isSVG": true, - "id": 27 + "id": 29 }, { "type": 2, @@ -192,7 +207,7 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 28 + "id": 30 }, { "type": 2, @@ -200,16 +215,16 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 29 + "id": 31 } ], "isSVG": true, - "id": 26 + "id": 28 }, { "type": 3, "textContent": "\n ", - "id": 30 + "id": 32 }, { "type": 2, @@ -219,12 +234,12 @@ "rr_height": "[100-150]px" }, "childNodes": [], - "id": 31 + "id": 33 }, { "type": 3, "textContent": "\n ", - "id": 32 + "id": 34 }, { "type": 2, @@ -235,12 +250,12 @@ "src": "file:///none.png" }, "childNodes": [], - "id": 33 + "id": 35 }, { "type": 3, "textContent": "\n ", - "id": 34 + "id": 36 }, { "type": 2, @@ -250,17 +265,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 35 + "id": 37 }, { "type": 3, "textContent": "\n ", - "id": 36 + "id": 38 }, { "type": 3, "textContent": "\n\n", - "id": 37 + "id": 39 } ], "id": 8 diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/template.html b/packages/browser-integration-tests/suites/replay/privacyInput/template.html index 735abb395522..fea3e1e29047 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInput/template.html +++ b/packages/browser-integration-tests/suites/replay/privacyInput/template.html @@ -11,5 +11,8 @@ + + + diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts index 039d5f519a87..7020a5e07b98 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts +++ b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts @@ -4,12 +4,13 @@ import { IncrementalSource } from '@sentry-internal/rrweb'; import { sentryTest } from '../../../utils/fixtures'; import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; -import { +import { getFullRecordingSnapshots , getIncrementalRecordingSnapshots, shouldSkipReplayTest, waitForReplayRequest, } from '../../../utils/replayHelpers'; + function isInputMutation( snap: IncrementalRecordingSnapshot, ): snap is IncrementalRecordingSnapshot & { data: inputData } { @@ -137,7 +138,10 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await reqPromise0; + const fullSnapshot = getFullRecordingSnapshots(await reqPromise0) + const stringifiedSnapshot = JSON.stringify(fullSnapshot); + expect(stringifiedSnapshot.includes('Submit form')).toBe(false); + expect(stringifiedSnapshot.includes('Unmasked button')).toBe(true); const text = 'test'; await page.locator('#textarea').fill(text); diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/template.html b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/template.html index 404bed05a6d0..6a250745553e 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/template.html +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/template.html @@ -9,5 +9,8 @@ + + + diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts index 50cbc7a78b7f..0fcf389c2f76 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -3,7 +3,7 @@ import type { inputData } from '@sentry-internal/rrweb'; import { IncrementalSource } from '@sentry-internal/rrweb'; import { sentryTest } from '../../../utils/fixtures'; -import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; +import { getFullRecordingSnapshots, IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; import { getIncrementalRecordingSnapshots, shouldSkipReplayTest, @@ -56,7 +56,10 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await reqPromise0; + const fullSnapshot = getFullRecordingSnapshots(await reqPromise0) + const stringifiedSnapshot = JSON.stringify(fullSnapshot); + expect(stringifiedSnapshot.includes('Submit form')).toBe(false); + expect(stringifiedSnapshot.includes('Unmasked button')).toBe(true); const text = 'test'; diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index d8d5e792a619..982c4c165ae8 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -50,3 +50,6 @@ export const REPLAY_MAX_EVENT_BUFFER_SIZE = 20_000_000; // ~20MB export const MIN_REPLAY_DURATION = 4_999; /* The max. allowed value that the minReplayDuration can be set to. */ export const MIN_REPLAY_DURATION_LIMIT = 15_000; + +/** Default attributes to be ignored when `maskAllText` is enabled */ +export const DEFAULT_IGNORED_ATTRIBUTES = ['title', 'placeholder']; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index ed70c3fb597b..5e2b6aaf559b 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -101,33 +101,49 @@ export class Replay implements Integration { // eslint-disable-next-line deprecation/deprecation ignoreClass, }: ReplayConfiguration = {}) { + const privacyOptions = getPrivacyOptions({ + mask, + unmask, + block, + unblock, + ignore, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + ignoreClass, + }); + this._recordingOptions = { maskAllInputs, maskAllText, maskInputOptions: { ...(maskInputOptions || {}), password: true }, maskTextFn: maskFn, maskInputFn: maskFn, - maskAttributeFn: (key: string, value: string): string => { - // For now, always mask these attributes - if (maskAttributes.includes(key)) { + maskAttributeFn: (key: string, value: string, el: HTMLElement): string => { + // We only mask attributes if `maskAllText` is true + if (!maskAllText) { + return value; + } + + // unmaskTextSelector takes precendence + if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) { + return value; + } + + if ( + maskAttributes.includes(key) || + // Need to mask `value` attribute for `` if it's a button-like + // type + (key === 'value' && el.tagName === 'INPUT' && ['submit', 'button'].includes(el.getAttribute('type') || '')) + ) { return value.replace(/[\S]/g, '*'); } return value; }, - ...getPrivacyOptions({ - mask, - unmask, - block, - unblock, - ignore, - blockClass, - blockSelector, - maskTextClass, - maskTextSelector, - ignoreClass, - }), + ...privacyOptions, // Our defaults slimDOMOptions: 'all',