Skip to content

Commit 97c72db

Browse files
feat: config options to customize privacy classes and selectors (#241)
1 parent 28ab2ab commit 97c72db

File tree

7 files changed

+253
-4
lines changed

7 files changed

+253
-4
lines changed

e2e/react-router/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from 'react-router-dom'
1111
import Root from './routes/root'
1212
import Welcome from './routes/welcome'
13+
import PrivacyDemo from './routes/privacy-demo'
1314

1415
function rootAction() {
1516
const contact = { name: 'hello' }
@@ -55,6 +56,7 @@ const router = createBrowserRouter(
5556
</Route>
5657
</Route>
5758
<Route path={'/welcome'} element={<Welcome />} />
59+
<Route path={'/privacy'} element={<PrivacyDemo />} />
5860
</>,
5961
),
6062
)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useEffect } from 'react'
2+
import { initialize as init } from 'launchdarkly-js-client-sdk'
3+
import SessionReplay from '@launchdarkly/session-replay'
4+
5+
export default function PrivacyDemo() {
6+
useEffect(() => {
7+
init(
8+
'548f6741c1efad40031b18ae',
9+
{ key: 'unknown' },
10+
{
11+
plugins: [
12+
new SessionReplay({
13+
privacySetting: 'none',
14+
serviceName: 'privacy-test',
15+
backendUrl: 'http://localhost:8082/public',
16+
debug: { clientInteractions: true, domRecording: true },
17+
18+
maskTextClass: 'ld-mask-text',
19+
maskTextSelector: '[data-masking="true"]',
20+
ignoreClass: 'ld-ignore',
21+
ignoreSelector: '[data-ignore="true"]',
22+
blockClass: 'ld-block',
23+
blockSelector: '[data-block="true"]',
24+
}),
25+
],
26+
},
27+
)
28+
}, [])
29+
30+
return (
31+
<div style={{ display: 'grid', gap: 16 }}>
32+
<h2>Session Replay Privacy Demo</h2>
33+
<p>
34+
This page showcases rrweb privacy options exposed via
35+
@launchdarkly/session-replay.
36+
</p>
37+
<section>
38+
<h3>Masked text by class</h3>
39+
<p>
40+
<span>Visible text</span>
41+
</p>
42+
<p className="ld-mask-text">
43+
<span>Secret text</span>
44+
</p>
45+
</section>
46+
47+
<section>
48+
<h3>Masked text by selector</h3>
49+
<div data-masking="true">
50+
This subtree should be masked by selector
51+
<div>
52+
<div>Deep child content</div>
53+
</div>
54+
</div>
55+
</section>
56+
57+
<section>
58+
<h3>Ignored inputs by class</h3>
59+
<label>
60+
Not ignored input:
61+
<input type="text" placeholder="Type here (recorded)" />
62+
</label>
63+
<label>
64+
Ignored input (class):
65+
<input
66+
className="ld-ignore"
67+
type="text"
68+
placeholder="Type here (ignored)"
69+
/>
70+
</label>
71+
</section>
72+
73+
<section>
74+
<h3>Ignored inputs by selector</h3>
75+
<label>
76+
Ignored input (selector):
77+
<input
78+
data-ignore="true"
79+
type="text"
80+
placeholder="Type here (ignored)"
81+
/>
82+
</label>
83+
</section>
84+
85+
<section>
86+
<h3>Blocked subtree by class</h3>
87+
<div
88+
className="ld-block"
89+
style={{ border: '1px solid #ccc', padding: 8 }}
90+
>
91+
<p>Contents should be blocked (not recorded)</p>
92+
<img src="/vite.svg" alt="example" width={48} height={48} />
93+
</div>
94+
</section>
95+
96+
<section>
97+
<h3>Blocked subtree by selector</h3>
98+
<div
99+
data-block="true"
100+
style={{ border: '1px solid #ccc', padding: 8 }}
101+
>
102+
<p>Contents should be blocked (not recorded)</p>
103+
<img src="/vite.svg" alt="example" width={48} height={48} />
104+
</div>
105+
</section>
106+
</div>
107+
)
108+
}

e2e/react-router/src/routes/root.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export default function Root() {
3939
return (
4040
<div id="sidebar">
4141
<h1>Hello, world</h1>
42+
<nav style={{ display: 'flex', gap: 12 }}>
43+
<a href="/welcome">Welcome</a>
44+
<a href="/privacy">Privacy Demo</a>
45+
</nav>
4246
<p>{flags}</p>
4347
<a href={session} target={'_blank'}>
4448
{session}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import { RecordSDK } from '../sdk/record'
3+
4+
const recordSpy = vi.fn((...args: any[]) => {
5+
return vi.fn()
6+
})
7+
vi.mock('rrweb', () => ({
8+
addCustomEvent: vi.fn(),
9+
record: (...args: any[]) => recordSpy(...args),
10+
}))
11+
12+
// Mock GraphQL generated SDK used inside RecordSDK.initializeSession
13+
vi.mock('../client/graph/generated/operations', () => ({
14+
getSdk: () => ({
15+
initializeSession: vi.fn().mockResolvedValue({
16+
initializeSession: {
17+
secure_id: 'test-session',
18+
project_id: '1',
19+
},
20+
}),
21+
}),
22+
}))
23+
24+
// Provide a minimal worker mock used by RecordSDK
25+
vi.mock('../client/workers/highlight-client-worker?worker&inline', () => ({
26+
default: class MockWorker {
27+
onmessage: any
28+
postMessage() {}
29+
},
30+
}))
31+
32+
describe('RecordSDK -> rrweb.record config wiring', () => {
33+
const originalWindow = global.window
34+
35+
beforeEach(() => {
36+
vi.useFakeTimers()
37+
recordSpy.mockClear()
38+
})
39+
40+
afterEach(() => {
41+
vi.useRealTimers()
42+
global.window = originalWindow
43+
})
44+
45+
it('passes default ignore/block/mask options to rrweb.record', async () => {
46+
const sdk = new RecordSDK({
47+
organizationID: '1',
48+
sessionSecureID: 'seed',
49+
})
50+
51+
await sdk.start()
52+
53+
expect(recordSpy).toHaveBeenCalledTimes(1)
54+
const arg = (recordSpy.mock.calls as any[])[0][0]
55+
// Defaults
56+
expect(arg.ignoreClass).toBe('highlight-ignore')
57+
expect(arg.blockClass).toBe('highlight-block')
58+
expect(arg.ignoreSelector).toBeUndefined()
59+
expect(arg.blockSelector).toBeUndefined()
60+
expect(arg.maskTextClass).toBeUndefined()
61+
expect(arg.maskTextSelector).toBeUndefined()
62+
})
63+
64+
it('respects overridden ignore/block/mask options', async () => {
65+
const sdk = new RecordSDK({
66+
organizationID: '1',
67+
sessionSecureID: 'seed',
68+
ignoreClass: 'custom-ignore',
69+
ignoreSelector: '.ignore-me',
70+
blockClass: 'custom-block',
71+
blockSelector: '.block-me',
72+
maskTextClass: 'mask-this',
73+
maskTextSelector: '.mask-me',
74+
})
75+
76+
await sdk.start()
77+
78+
expect(recordSpy).toHaveBeenCalledTimes(1)
79+
const arg = (recordSpy.mock.calls as any[])[0][0]
80+
expect(arg.ignoreClass).toBe('custom-ignore')
81+
expect(arg.ignoreSelector).toBe('.ignore-me')
82+
expect(arg.blockClass).toBe('custom-block')
83+
expect(arg.blockSelector).toBe('.block-me')
84+
expect(arg.maskTextClass).toBe('mask-this')
85+
expect(arg.maskTextSelector).toBe('.mask-me')
86+
})
87+
})

sdk/highlight-run/src/client/index.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ export type HighlightClassOptions = {
143143
reportConsoleErrors?: boolean
144144
consoleMethodsToRecord?: ConsoleMethods[]
145145
privacySetting?: PrivacySettingOption
146+
maskTextClass?: string | RegExp
147+
maskTextSelector?: string
148+
blockClass?: string | RegExp
149+
blockSelector?: string
150+
ignoreClass?: string
151+
ignoreSelector?: string
146152
enableSegmentIntegration?: boolean
147153
enableCanvasRecording?: boolean
148154
enablePerformanceRecording?: boolean
@@ -823,13 +829,17 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`,
823829
)
824830

825831
this._recordStop = record({
826-
ignoreClass: 'highlight-ignore',
827-
blockClass: 'highlight-block',
832+
ignoreClass: this.options.ignoreClass ?? 'highlight-ignore',
833+
ignoreSelector: this.options.ignoreSelector,
834+
blockClass: this.options.blockClass ?? 'highlight-block',
835+
blockSelector: this.options.blockSelector,
828836
emit,
829837
recordCrossOriginIframes: this.options.recordCrossOriginIframe,
830838
privacySetting: this.privacySetting,
831839
maskAllInputs,
832840
maskInputOptions: maskInputOptions,
841+
maskTextClass: this.options.maskTextClass,
842+
maskTextSelector: this.options.maskTextSelector,
833843
recordCanvas: this.enableCanvasRecording,
834844
sampling: {
835845
canvas: {

sdk/highlight-run/src/client/types/record.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,40 @@ export type RecordOptions = CommonOptions & {
3737
*/
3838
privacySetting?: PrivacySettingOption
3939

40+
/**
41+
* Customize which elements' text should be masked by specifying a CSS class name or RegExp.
42+
* Default class is 'highlight-mask'.
43+
*/
44+
maskTextClass?: string | RegExp
45+
46+
/**
47+
* Customize which elements' text should be masked via a CSS selector that will match the element
48+
* and its descendants.
49+
*/
50+
maskTextSelector?: string
51+
52+
/**
53+
* Customize which elements should be blocked (not recorded) by specifying a class name or RegExp.
54+
* Default is 'highlight-block'.
55+
*/
56+
blockClass?: string | RegExp
57+
58+
/**
59+
* Customize which elements should be blocked via a CSS selector.
60+
*/
61+
blockSelector?: string
62+
63+
/**
64+
* Customize which elements and their descendants should be ignored from DOM events by class.
65+
* Default is 'highlight-ignore'.
66+
*/
67+
ignoreClass?: string
68+
69+
/**
70+
* Customize which elements should be ignored from DOM events via a CSS selector.
71+
*/
72+
ignoreSelector?: string
73+
4074
/**
4175
* Specifies whether to record canvas elements or not.
4276
* @default false

sdk/highlight-run/src/sdk/record.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,13 +596,17 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`,
596596
)
597597

598598
this._recordStop = record({
599-
ignoreClass: 'highlight-ignore',
600-
blockClass: 'highlight-block',
599+
ignoreClass: this.options.ignoreClass ?? 'highlight-ignore',
600+
ignoreSelector: this.options.ignoreSelector,
601+
blockClass: this.options.blockClass ?? 'highlight-block',
602+
blockSelector: this.options.blockSelector,
601603
emit,
602604
recordCrossOriginIframes: this.options.recordCrossOriginIframe,
603605
privacySetting: this.privacySetting,
604606
maskAllInputs,
605607
maskInputOptions: maskInputOptions,
608+
maskTextClass: this.options.maskTextClass,
609+
maskTextSelector: this.options.maskTextSelector,
606610
recordCanvas: this.enableCanvasRecording,
607611
sampling: {
608612
canvas: {

0 commit comments

Comments
 (0)