Skip to content

Commit f5ac26d

Browse files
committed
wip
1 parent de1e48f commit f5ac26d

File tree

9 files changed

+319
-33
lines changed

9 files changed

+319
-33
lines changed

packages/integration-tests/utils/helpers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/type
33

44
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
55

6-
export const envelopeRequestParser = (request: Request | null): Event => {
6+
export const envelopeParser = (request: Request | null): unknown[] => {
77
// https://develop.sentry.dev/sdk/envelopes/
88
const envelope = request?.postData() || '';
99

@@ -14,7 +14,11 @@ export const envelopeRequestParser = (request: Request | null): Event => {
1414
} catch (error) {
1515
return line;
1616
}
17-
})[2];
17+
});
18+
};
19+
20+
export const envelopeRequestParser = (request: Request | null): Event => {
21+
return envelopeParser(request)[2] as Event;
1822
};
1923

2024
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {

packages/replay/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,11 @@ The following options can be configured as options to the integration, in `new R
194194
| maskAllInputs | boolean | `true` | Mask values of `<input>` elements. Passes input values through `maskInputFn` before sending to server. |
195195
| blockAllMedia | boolean | `true` | Block _all_ media elements (`img, svg, video, object, picture, embed, map, audio`)
196196
| maskTextFn | (text: string) => string | `(text) => '*'.repeat(text.length)` | Function to customize how text content is masked before sending to server. By default, masks text with `*`. |
197-
| block | Array<string\|RegExp> | `.sentry-block, [data-sentry-block]` | Redact any elements that match the DOM selectors. See [privacy](#blocking) section for an example. |
198-
| unblock | Array<string\|RegExp> | `.sentry-unblock, [data-sentry-unblock]`| Do not redact any elements that match the DOM selectors. Useful when using `blockAllMedia`. See [privacy](#blocking) section for an example. |
199-
| mask | Array<string\|RegExp> | `.sentry-mask, [data-sentry-mask]` | Mask all elements that match the given DOM selectors. See [privacy](#masking) section for an example. |
200-
| unmask | Array<string\|RegExp> | `.sentry-unmask, [data-sentry-unmask]` | Unmask all elements that match the given DOM selectors. Useful when using `maskAllText`. See [privacy](#masking) section for an example. |
201-
| ignore | Array<string\|RegExp> | `.sentry-ignore, [data-sentry-ignore]` | Ignores all events on the matching input fields. See [privacy](#ignoring) section for an example. |
197+
| block | Array<string> | `.sentry-block, [data-sentry-block]` | Redact any elements that match the DOM selectors. See [privacy](#blocking) section for an example. |
198+
| unblock | Array<string> | `.sentry-unblock, [data-sentry-unblock]`| Do not redact any elements that match the DOM selectors. Useful when using `blockAllMedia`. See [privacy](#blocking) section for an example. |
199+
| mask | Array<string> | `.sentry-mask, [data-sentry-mask]` | Mask all elements that match the given DOM selectors. See [privacy](#masking) section for an example. |
200+
| unmask | Array<string> | `.sentry-unmask, [data-sentry-unmask]` | Unmask all elements that match the given DOM selectors. Useful when using `maskAllText`. See [privacy](#masking) section for an example. |
201+
| ignore | Array<string> | `.sentry-ignore, [data-sentry-ignore]` | Ignores all events on the matching input fields. See [privacy](#ignoring) section for an example. |
202202

203203
#### Deprecated options
204204
In order to streamline our privacy options, the following have been deprecated in favor for the respective options above.

packages/replay/src/integration.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { BrowserClientReplayOptions, Integration } from '@sentry/types';
44
import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY, MASK_ALL_TEXT_SELECTOR } from './constants';
55
import { ReplayContainer } from './replay';
66
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types';
7+
import { getPrivacyOptions } from './util/getPrivacyOptions';
78
import { isBrowser } from './util/isBrowser';
89

910
const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio';
@@ -38,27 +39,57 @@ export class Replay implements Integration {
3839
flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY,
3940
stickySession = true,
4041
useCompression = true,
42+
_experiments = {},
4143
sessionSampleRate,
4244
errorSampleRate,
4345
maskAllText,
44-
maskTextSelector,
4546
maskAllInputs = true,
4647
blockAllMedia = true,
47-
_experiments = {},
48-
blockClass = 'sentry-block',
49-
ignoreClass = 'sentry-ignore',
50-
maskTextClass = 'sentry-mask',
51-
blockSelector = '[data-sentry-block]',
52-
..._recordingOptions
48+
49+
mask = [],
50+
unmask = [],
51+
block = [],
52+
unblock = [],
53+
ignore = [],
54+
maskFn,
55+
56+
// eslint-disable-next-line deprecation/deprecation
57+
blockClass,
58+
// eslint-disable-next-line deprecation/deprecation
59+
blockSelector,
60+
// eslint-disable-next-line deprecation/deprecation
61+
maskTextClass,
62+
// eslint-disable-next-line deprecation/deprecation
63+
maskTextSelector,
64+
// eslint-disable-next-line deprecation/deprecation
65+
ignoreClass,
5366
}: ReplayConfiguration = {}) {
5467
this._recordingOptions = {
5568
maskAllInputs,
56-
blockClass,
57-
ignoreClass,
58-
maskTextClass,
59-
maskTextSelector,
60-
blockSelector,
61-
..._recordingOptions,
69+
maskTextFn: maskFn,
70+
maskInputFn: maskFn,
71+
72+
...getPrivacyOptions({
73+
mask,
74+
unmask,
75+
block,
76+
unblock,
77+
ignore,
78+
blockClass,
79+
blockSelector,
80+
maskTextClass,
81+
maskTextSelector,
82+
ignoreClass,
83+
}),
84+
85+
// Our defaults
86+
slimDOMOptions: 'all',
87+
inlineStylesheet: true,
88+
// Disable inline images as it will increase segment/replay size
89+
inlineImages: false,
90+
// collect fonts, but be aware that `sentry.io` needs to be an allowed
91+
// origin for playback
92+
collectFonts: true,
6293
};
6394

6495
this._options = {

packages/replay/src/types.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,70 @@ export interface ReplayPluginOptions extends SessionOptions {
113113
}>;
114114
}
115115

116+
export interface ReplayIntegrationPrivacyOptions {
117+
/**
118+
* Mask text content for elements that match the CSS selectors in the list.
119+
*/
120+
mask?: string[];
121+
122+
/**
123+
* Unmask text content for elements that match the CSS selectors in the list.
124+
*/
125+
unmask?: string[];
126+
127+
/**
128+
* Block elements that match the CSS selectors in the list. Blocking replaces
129+
* the element with an empty placeholder with the same dimensions.
130+
*/
131+
block?: string[];
132+
133+
/**
134+
* Unblock elements that match the CSS selectors in the list. This is useful when using `blockAllMedia`.
135+
*/
136+
unblock?: string[];
137+
138+
/**
139+
* Ignore input events for elements that match the CSS selectors in the list.
140+
*/
141+
ignore?: string[];
142+
143+
/**
144+
* A callback function to customize how your text is masked.
145+
*/
146+
maskFn?: Pick<RecordingOptions, 'maskTextFn'>;
147+
}
148+
116149
// These are optional for ReplayPluginOptions because the plugin sets default values
117150
type OptionalReplayPluginOptions = Partial<ReplayPluginOptions>;
118151

119-
export interface ReplayConfiguration extends OptionalReplayPluginOptions, RecordingOptions {}
152+
export interface DeprecatedPrivacyOptions {
153+
/**
154+
* @deprecated Use `block` which accepts an array of CSS selectors
155+
*/
156+
blockSelector?: RecordingOptions['blockSelector'];
157+
/**
158+
* @deprecated Use `block` which accepts an array of CSS selectors
159+
*/
160+
blockClass?: RecordingOptions['blockClass'];
161+
/**
162+
* @deprecated Use `mask` which accepts an array of CSS selectors
163+
*/
164+
maskTextClass?: RecordingOptions['maskTextClass'];
165+
/**
166+
* @deprecated Use `mask` which accepts an array of CSS selectors
167+
*/
168+
maskTextSelector?: RecordingOptions['maskTextSelector'];
169+
/**
170+
* @deprecated Use `ignore` which accepts an array of CSS selectors
171+
*/
172+
ignoreClass?: RecordingOptions['ignoreClass'];
173+
}
174+
175+
export interface ReplayConfiguration
176+
extends ReplayIntegrationPrivacyOptions,
177+
OptionalReplayPluginOptions,
178+
DeprecatedPrivacyOptions,
179+
Pick<RecordingOptions, 'maskAllInputs'> {}
120180

121181
interface CommonEventContext {
122182
/**

packages/replay/src/types/rrweb.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ export type recordOptions = {
3434
blockClass?: blockClass;
3535
ignoreClass?: string;
3636
maskTextClass?: maskTextClass;
37+
maskTextSelector?: string;
3738
blockSelector?: string;
3839
} & Record<string, unknown>;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { DeprecatedPrivacyOptions, ReplayIntegrationPrivacyOptions } from '../types';
2+
3+
type GetPrivacyOptions = Required<Omit<ReplayIntegrationPrivacyOptions, 'maskFn'>> & DeprecatedPrivacyOptions;
4+
interface GetPrivacyReturn {
5+
maskTextSelector: string;
6+
unmaskTextSelector: string;
7+
maskInputSelector: string;
8+
unmaskInputSelector: string;
9+
blockSelector: string;
10+
unblockSelector: string;
11+
ignoreSelector: string;
12+
13+
blockClass?: RegExp;
14+
maskTextClass?: RegExp;
15+
}
16+
17+
function getOption(
18+
selectors: string[],
19+
defaultSelectors: string[],
20+
deprecatedClassOption?: string | RegExp,
21+
deprecatedSelectorOption?: string,
22+
): string {
23+
const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : [];
24+
25+
const allSelectors = [
26+
...selectors,
27+
// @deprecated
28+
...deprecatedSelectors,
29+
30+
// sentry defaults
31+
...defaultSelectors,
32+
];
33+
34+
// @deprecated
35+
if (typeof deprecatedClassOption !== 'undefined') {
36+
// NOTE: No support for RegExp
37+
if (typeof deprecatedClassOption === 'string') {
38+
allSelectors.push(`.${deprecatedClassOption}`);
39+
}
40+
41+
// eslint-disable-next-line no-console
42+
console.warn(
43+
'[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.',
44+
);
45+
}
46+
47+
return allSelectors.join(',');
48+
}
49+
50+
/**
51+
* Returns privacy related configuration for use in rrweb
52+
*/
53+
export function getPrivacyOptions({
54+
mask,
55+
unmask,
56+
block,
57+
unblock,
58+
ignore,
59+
60+
// eslint-disable-next-line deprecation/deprecation
61+
blockClass,
62+
// eslint-disable-next-line deprecation/deprecation
63+
blockSelector,
64+
// eslint-disable-next-line deprecation/deprecation
65+
maskTextClass,
66+
// eslint-disable-next-line deprecation/deprecation
67+
maskTextSelector,
68+
// eslint-disable-next-line deprecation/deprecation
69+
ignoreClass,
70+
}: GetPrivacyOptions): GetPrivacyReturn {
71+
const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector);
72+
const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']);
73+
74+
const options: GetPrivacyReturn = {
75+
// We are making the decision to make text and input selectors the same
76+
maskTextSelector: maskSelector,
77+
unmaskTextSelector: unmaskSelector,
78+
maskInputSelector: maskSelector,
79+
unmaskInputSelector: unmaskSelector,
80+
81+
blockSelector: getOption(block, ['.sentry-block', '[data-sentry-block]'], blockClass, blockSelector),
82+
unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']),
83+
ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]'], ignoreClass),
84+
};
85+
86+
87+
if (blockClass instanceof RegExp) {
88+
options.blockClass = blockClass;
89+
}
90+
91+
if (maskTextClass instanceof RegExp) {
92+
options.maskTextClass = maskTextClass;
93+
}
94+
95+
return options
96+
}

packages/replay/test/integration/integrationSettings.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ describe('Integration | integrationSettings', () => {
1010
it('sets the correct configuration when `blockAllMedia` is disabled', async () => {
1111
const { replay } = await mockSdk({ replayOptions: { blockAllMedia: false } });
1212

13-
expect(replay['_recordingOptions'].blockSelector).toBe('[data-sentry-block]');
13+
expect(replay['_recordingOptions'].blockSelector).toBe('.sentry-block,[data-sentry-block]');
1414
});
1515

1616
it('sets the correct configuration when `blockSelector` is empty and `blockAllMedia` is enabled', async () => {
1717
const { replay } = await mockSdk({ replayOptions: { blockSelector: '' } });
1818

1919
expect(replay['_recordingOptions'].blockSelector).toMatchInlineSnapshot(
20-
'"img,image,svg,path,rect,area,video,object,picture,embed,map,audio"',
20+
'",.sentry-block,[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio"',
2121
);
2222
});
2323

@@ -27,7 +27,7 @@ describe('Integration | integrationSettings', () => {
2727
});
2828

2929
expect(replay['_recordingOptions'].blockSelector).toMatchInlineSnapshot(
30-
'"[data-test-blockSelector],img,image,svg,path,rect,area,video,object,picture,embed,map,audio"',
30+
'"[data-test-blockSelector],.sentry-block,[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio"',
3131
);
3232
});
3333
});
@@ -181,13 +181,13 @@ describe('Integration | integrationSettings', () => {
181181
it('works with false', async () => {
182182
const { replay } = await mockSdk({ replayOptions: { maskAllText: false } });
183183

184-
expect(replay['_recordingOptions'].maskTextSelector).toBe(undefined);
184+
expect(replay['_recordingOptions'].maskTextSelector).toBe('.sentry-mask,[data-sentry-mask]');
185185
});
186186

187187
it('maskTextSelector takes precedence over maskAllText when not specifiying maskAllText:true', async () => {
188188
const { replay } = await mockSdk({ replayOptions: { maskTextSelector: '[custom]' } });
189189

190-
expect(replay['_recordingOptions'].maskTextSelector).toBe('[custom]');
190+
expect(replay['_recordingOptions'].maskTextSelector).toBe('[custom],.sentry-mask,[data-sentry-mask]');
191191
});
192192

193193
it('maskAllText takes precedence over maskTextSelector when specifiying maskAllText:true', async () => {

packages/replay/test/integration/rrweb.test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { MASK_ALL_TEXT_SELECTOR } from '../../src/constants';
21
import { resetSdkMock } from '../mocks/resetSdkMock';
32
import { useFakeTimers } from '../utils/use-fake-timers';
43

@@ -12,19 +11,27 @@ describe('Integration | rrweb', () => {
1211
it('calls rrweb.record with custom options', async () => {
1312
const { mockRecord } = await resetSdkMock({
1413
replayOptions: {
15-
ignoreClass: 'sentry-test-ignore',
14+
ignore: ['.sentry-test-ignore'],
1615
stickySession: false,
1716
},
1817
});
1918
expect(mockRecord.mock.calls[0][0]).toMatchInlineSnapshot(`
2019
Object {
21-
"blockClass": "sentry-block",
22-
"blockSelector": "[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio",
20+
"blockSelector": ".sentry-block,[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio",
21+
"collectFonts": true,
2322
"emit": [Function],
24-
"ignoreClass": "sentry-test-ignore",
23+
"ignoreSelector": ".sentry-test-ignore,.sentry-ignore,[data-sentry-ignore]",
24+
"inlineImages": false,
25+
"inlineStylesheet": true,
2526
"maskAllInputs": true,
26-
"maskTextClass": "sentry-mask",
27-
"maskTextSelector": "${MASK_ALL_TEXT_SELECTOR}",
27+
"maskInputFn": undefined,
28+
"maskInputSelector": ".sentry-mask,[data-sentry-mask]",
29+
"maskTextFn": undefined,
30+
"maskTextSelector": "body *:not(style), body *:not(script)",
31+
"slimDOMOptions": "all",
32+
"unblockSelector": ".sentry-unblock,[data-sentry-unblock]",
33+
"unmaskInputSelector": ".sentry-unmask,[data-sentry-unmask]",
34+
"unmaskTextSelector": ".sentry-unmask,[data-sentry-unmask]",
2835
}
2936
`);
3037
});

0 commit comments

Comments
 (0)