Skip to content

Commit da700e7

Browse files
Allow users to set replaceGroups on regex PII rules (#103885)
Relay already has the ability to configure what part of a pattern match to scrub, but so far it has not been exposed to users. Add a checkbox to the UI to allow scrubbing the first match group instead of the entire expression. --------- Co-authored-by: Priscila Oliveira <[email protected]>
1 parent f7ac1bc commit da700e7

File tree

7 files changed

+87
-7
lines changed

7 files changed

+87
-7
lines changed

static/app/views/settings/components/dataScrubbing/convertRelayPiiConfig.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export function convertRelayPiiConfig(relayPiiConfig?: string | null): Rule[] {
5858
source,
5959
placeholder: redaction?.text,
6060
pattern: resolvedRule.pattern,
61+
replaceCaptured:
62+
resolvedRule.replaceGroups?.length === 1 &&
63+
resolvedRule.replaceGroups?.at(0) === 1,
6164
});
6265
} else {
6366
convertedRules.push({
@@ -75,6 +78,9 @@ export function convertRelayPiiConfig(relayPiiConfig?: string | null): Rule[] {
7578
type: RuleType.PATTERN,
7679
source,
7780
pattern: resolvedRule.pattern,
81+
replaceCaptured:
82+
resolvedRule.replaceGroups?.length === 1 &&
83+
resolvedRule.replaceGroups?.at(0) === 1,
7884
});
7985
} else {
8086
convertedRules.push({id, method, type, source});

static/app/views/settings/components/dataScrubbing/modals/edit.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ describe('Edit Modal', () => {
189189
method: 'mask',
190190
pattern: '',
191191
placeholder: '',
192+
replaceCaptured: false,
192193
type: 'anything',
193194
source: valueSuggestions[2]!.value,
194195
},

static/app/views/settings/components/dataScrubbing/modals/form/index.tsx

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import {css} from '@emotion/react';
33
import styled from '@emotion/styled';
44
import sortBy from 'lodash/sortBy';
55

6+
import {Flex} from '@sentry/scraps/layout';
7+
import {Tooltip} from '@sentry/scraps/tooltip';
8+
69
import {Alert} from 'sentry/components/core/alert';
710
import {Button} from 'sentry/components/core/button';
11+
import {Checkbox} from 'sentry/components/core/checkbox';
812
import {Input} from 'sentry/components/core/input';
913
import FieldGroup from 'sentry/components/forms/fieldGroup';
1014
import RadioField from 'sentry/components/forms/fields/radioField';
@@ -14,6 +18,7 @@ import {space} from 'sentry/styles/space';
1418
import type {Organization} from 'sentry/types/organization';
1519
import type {Project} from 'sentry/types/project';
1620
import withOrganization from 'sentry/utils/withOrganization';
21+
import {hasCaptureGroups} from 'sentry/views/settings/components/dataScrubbing/modals/utils';
1722
import {
1823
AllowedDataScrubbingDatasets,
1924
MethodType,
@@ -46,7 +51,7 @@ type Props<V extends Values, K extends keyof V> = {
4651
errors: Partial<V>;
4752
eventId: EventId;
4853
onAttributeError: (message: string) => void;
49-
onChange: (field: K, value: string) => void;
54+
onChange: (field: K, value: V[K]) => void;
5055
onChangeDataset: (dataset: AllowedDataScrubbingDatasets) => void;
5156
onUpdateEventId: (eventId: string) => void;
5257
onValidate: (field: K) => () => void;
@@ -60,6 +65,35 @@ type State = {
6065
displayEventId: boolean;
6166
};
6267

68+
function ReplaceCapturedCheckbox({
69+
values,
70+
onChange,
71+
}: {
72+
onChange: (field: 'replaceCaptured', value: boolean) => void;
73+
values: Values;
74+
}) {
75+
const disabled = !hasCaptureGroups(values.pattern);
76+
return (
77+
<Tooltip
78+
title={disabled ? t('This rule does not contain capture groups') : undefined}
79+
disabled={!disabled}
80+
>
81+
<Flex gap="xs" align="center">
82+
<Checkbox
83+
id="replace-captured"
84+
name="replaceCaptured"
85+
checked={values.replaceCaptured}
86+
disabled={disabled}
87+
onChange={e => onChange('replaceCaptured', e.target.checked)}
88+
/>
89+
<ReplaceCapturedLabel htmlFor="replace-captured" disabled={disabled}>
90+
{t('Only replace first capture match')}
91+
</ReplaceCapturedLabel>
92+
</Flex>
93+
</Tooltip>
94+
);
95+
}
96+
6397
class Form extends Component<Props<Values, KeysOfUnion<Values>>, State> {
6498
state: State = {
6599
displayEventId: !!this.props.eventId?.value,
@@ -68,7 +102,7 @@ class Form extends Component<Props<Values, KeysOfUnion<Values>>, State> {
68102
handleChange =
69103
<K extends keyof Values>(field: K) =>
70104
(event: React.ChangeEvent<HTMLInputElement>) => {
71-
this.props.onChange(field, event.target.value);
105+
this.props.onChange(field, event.target.value as Values[K]);
72106
};
73107

74108
handleToggleEventId = () => {
@@ -217,6 +251,7 @@ class Form extends Component<Props<Values, KeysOfUnion<Values>>, State> {
217251
onBlur={onValidate('pattern')}
218252
id="regex-matches"
219253
/>
254+
<ReplaceCapturedCheckbox values={values} onChange={onChange} />
220255
</FieldGroup>
221256
)}
222257
</FieldContainer>
@@ -384,6 +419,7 @@ class Form extends Component<Props<Values, KeysOfUnion<Values>>, State> {
384419
onBlur={onValidate('pattern')}
385420
id="regex-matches"
386421
/>
422+
<ReplaceCapturedCheckbox values={values} onChange={onChange} />
387423
</FieldGroup>
388424
)}
389425
</FieldContainer>
@@ -426,7 +462,7 @@ const FieldContainer = styled('div')<{hasTwoColumns: boolean}>`
426462
@media (min-width: ${p => p.theme.breakpoints.sm}) {
427463
gap: ${space(2)};
428464
${p => p.hasTwoColumns && `grid-template-columns: 1fr 1fr;`}
429-
margin-bottom: ${p => (p.hasTwoColumns ? 0 : space(2))};
465+
margin-bottom: ${p => p.theme.space.xl};
430466
}
431467
`;
432468

@@ -446,6 +482,7 @@ const SourceGroup = styled('div')<{isExpanded?: boolean}>`
446482

447483
const RegularExpression = styled(Input)`
448484
font-family: ${p => p.theme.text.familyMono};
485+
margin-bottom: ${p => p.theme.space.md};
449486
`;
450487

451488
const DatasetRadioField = styled(RadioField)`
@@ -474,3 +511,14 @@ const Toggle = styled(Button)`
474511
align-items: center;
475512
}
476513
`;
514+
515+
const ReplaceCapturedLabel = styled('label')<{disabled: boolean}>`
516+
font-weight: normal;
517+
margin-bottom: 0;
518+
line-height: 1rem;
519+
${p =>
520+
p.disabled &&
521+
css`
522+
color: ${p.theme.disabled};
523+
`}
524+
`;

static/app/views/settings/components/dataScrubbing/modals/modalManager.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
import Form from './form';
3030
import handleError, {ErrorType} from './handleError';
3131
import Modal from './modal';
32-
import {useSourceGroupData} from './utils';
32+
import {hasCaptureGroups, useSourceGroupData} from './utils';
3333

3434
type FormProps = React.ComponentProps<typeof Form>;
3535
type Values = FormProps['values'];
@@ -118,6 +118,7 @@ class ModalManager extends Component<ModalManagerWithLocalStorageProps, State> {
118118
source: initialState?.source ?? '',
119119
placeholder: initialState?.placeholder ?? '',
120120
pattern: initialState?.pattern ?? '',
121+
replaceCaptured: initialState?.replaceCaptured ?? false,
121122
};
122123
}
123124

@@ -244,8 +245,13 @@ class ModalManager extends Component<ModalManagerWithLocalStorageProps, State> {
244245
[field]: value,
245246
};
246247

247-
if (values.type !== RuleType.PATTERN && values.pattern) {
248+
if (values.type !== RuleType.PATTERN) {
248249
values.pattern = '';
250+
values.replaceCaptured = false;
251+
}
252+
253+
if (values.type === RuleType.PATTERN && !hasCaptureGroups(values.pattern)) {
254+
values.replaceCaptured = false;
249255
}
250256

251257
if (values.method !== MethodType.REPLACE && values.placeholder) {
@@ -291,7 +297,12 @@ class ModalManager extends Component<ModalManagerWithLocalStorageProps, State> {
291297
handleValidate =
292298
<K extends keyof Values>(field: K) =>
293299
() => {
294-
const isFieldValueEmpty = !this.state.values[field].trim();
300+
const value = this.state.values[field];
301+
if (typeof value !== 'string') {
302+
return;
303+
}
304+
305+
const isFieldValueEmpty = !value.trim();
295306

296307
const fieldErrorAlreadyExist = this.state.errors[field];
297308

static/app/views/settings/components/dataScrubbing/modals/utils.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,8 @@ export function useSourceGroupData() {
4242
saveToSourceGroupData,
4343
};
4444
}
45+
46+
export function hasCaptureGroups(pattern: string) {
47+
const m = pattern.match(/\(.*\)/);
48+
return m !== null && m.length > 0;
49+
}

static/app/views/settings/components/dataScrubbing/submitRules.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function getSubmitFormatRule(rule: Rule): PiiConfig {
1212
method: rule.method,
1313
text: rule?.placeholder,
1414
},
15+
replaceGroups: rule.replaceCaptured ? [1] : undefined,
1516
};
1617
}
1718

@@ -22,6 +23,7 @@ function getSubmitFormatRule(rule: Rule): PiiConfig {
2223
redaction: {
2324
method: rule.method,
2425
},
26+
replaceGroups: rule.replaceCaptured ? [1] : undefined,
2527
};
2628
}
2729

static/app/views/settings/components/dataScrubbing/types.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export type RuleDefault = RuleBase & {
7575

7676
type RulePattern = RuleBase & {
7777
pattern: string;
78+
replaceCaptured: boolean;
7879
type: RuleType.PATTERN;
7980
} & Pick<RuleDefault, 'method'>;
8081

@@ -94,7 +95,12 @@ export type EventId = {
9495
value: string;
9596
};
9697

97-
export type EditableRule = Omit<Record<KeysOfUnion<Rule>, string>, 'id'>;
98+
export type EditableRule = Omit<
99+
{
100+
[K in KeysOfUnion<Rule>]: K extends 'replaceCaptured' ? boolean : string;
101+
},
102+
'id'
103+
>;
98104

99105
export type AttributeResults = Record<
100106
AllowedDataScrubbingDatasets,
@@ -122,6 +128,7 @@ type PiiConfigPattern = {
122128
method: RulePattern['method'];
123129
};
124130
type: RulePattern['type'];
131+
replaceGroups?: number[];
125132
};
126133

127134
type PiiConfigReplaceAndPattern = Omit<PiiConfigPattern, 'redaction'> &

0 commit comments

Comments
 (0)