Skip to content

Commit 827aa83

Browse files
authored
feat: Annotate empty request bodies (#13606)
1 parent 286145f commit 827aa83

File tree

8 files changed

+279
-37
lines changed

8 files changed

+279
-37
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"_meta": {
3+
"request": {
4+
"data": {
5+
"bam": {
6+
"": {
7+
"len": 0,
8+
"rem": [
9+
[
10+
"!raw",
11+
"x",
12+
0,
13+
0
14+
]
15+
]
16+
}
17+
}
18+
}
19+
}
20+
},
21+
"request": {
22+
"url": "http://127.0.0.1:5000/",
23+
"headers": [
24+
[
25+
"Accept",
26+
"*/*"
27+
],
28+
[
29+
"Content-Length",
30+
"75378"
31+
],
32+
[
33+
"Content-Type",
34+
"multipart/form-data; boundary=------------------------f1861cf25dfa767f"
35+
],
36+
[
37+
"Expect",
38+
"100-continue"
39+
],
40+
[
41+
"Host",
42+
"127.0.0.1:5000"
43+
],
44+
[
45+
"User-Agent",
46+
"curl/7.54.0"
47+
]
48+
],
49+
"env": {
50+
"SERVER_PORT": "5000",
51+
"SERVER_NAME": "127.0.0.1"
52+
},
53+
"data": {
54+
"bam": "",
55+
"foo": "bar"
56+
},
57+
"method": "POST",
58+
"inferred_content_type": "multipart/form-data"
59+
}
60+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"_meta": {
3+
"request": {
4+
"data": {
5+
"": {
6+
"len": 5,
7+
"rem": [
8+
[
9+
"!config",
10+
"x",
11+
0,
12+
5
13+
]
14+
]
15+
}
16+
}
17+
}
18+
},
19+
"request": {
20+
"url": "http://127.0.0.1:5000/",
21+
"headers": [
22+
[
23+
"Accept",
24+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
25+
],
26+
[
27+
"Accept-Encoding",
28+
"gzip, deflate"
29+
],
30+
[
31+
"Accept-Language",
32+
"en-US,en;q=0.5"
33+
],
34+
[
35+
"Cache-Control",
36+
"max-age=0"
37+
],
38+
[
39+
"Connection",
40+
"keep-alive"
41+
],
42+
[
43+
"Host",
44+
"127.0.0.1:5000"
45+
],
46+
[
47+
"Upgrade-Insecure-Requests",
48+
"1"
49+
],
50+
[
51+
"User-Agent",
52+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"
53+
]
54+
],
55+
"data": "",
56+
"method": "POST",
57+
"env": {
58+
"SERVER_PORT": "5000",
59+
"SERVER_NAME": "127.0.0.1"
60+
}
61+
}
62+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"_meta": {
3+
"request": {
4+
"data": {
5+
"": {
6+
"len": 5,
7+
"rem": [
8+
[
9+
"!raw",
10+
"x",
11+
0,
12+
5
13+
]
14+
]
15+
}
16+
}
17+
}
18+
},
19+
"request": {
20+
"url": "http://127.0.0.1:5000/",
21+
"headers": [
22+
[
23+
"Accept",
24+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
25+
],
26+
[
27+
"Accept-Encoding",
28+
"gzip, deflate"
29+
],
30+
[
31+
"Accept-Language",
32+
"en-US,en;q=0.5"
33+
],
34+
[
35+
"Cache-Control",
36+
"max-age=0"
37+
],
38+
[
39+
"Connection",
40+
"keep-alive"
41+
],
42+
[
43+
"Host",
44+
"127.0.0.1:5000"
45+
],
46+
[
47+
"Upgrade-Insecure-Requests",
48+
"1"
49+
],
50+
[
51+
"User-Agent",
52+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"
53+
]
54+
],
55+
"data": "",
56+
"method": "POST",
57+
"env": {
58+
"SERVER_PORT": "5000",
59+
"SERVER_NAME": "127.0.0.1"
60+
}
61+
}
62+
}

src/sentry/static/sentry/app/components/events/interfaces/richHttpContent.jsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,43 @@ import ClippedBox from 'app/components/clippedBox';
88
import ContextData from 'app/components/contextData';
99
import ErrorBoundary from 'app/components/errorBoundary';
1010
import KeyValueList from 'app/components/events/interfaces/keyValueList';
11+
import AnnotatedText from 'app/components/events/meta/annotatedText';
12+
import MetaData from 'app/components/events/meta/metaData';
1113

1214
class RichHttpContent extends React.Component {
1315
static propTypes = {
1416
data: PropTypes.object.isRequired,
1517
};
1618

17-
getBodySection = data => {
19+
getBodySection = (data, value, meta) => {
1820
// The http interface provides an inferred content type for the data body.
19-
switch (data.inferredContentType) {
20-
case 'application/json':
21-
return <ContextData data={data.data} />;
22-
case 'application/x-www-form-urlencoded':
23-
return (
24-
<KeyValueList data={objectToSortedTupleArray(data.data)} isContextData={true} />
25-
);
26-
default:
27-
return <pre>{JSON.stringify(data.data, null, 2)}</pre>;
21+
if (meta && (!value || value instanceof String)) {
22+
// TODO(markus): Currently annotated nested objects are shown without
23+
// annotations.
24+
return (
25+
<pre>
26+
<AnnotatedText
27+
value={value}
28+
chunks={meta.chunks}
29+
remarks={meta.rem}
30+
errors={meta.err}
31+
/>
32+
</pre>
33+
);
34+
} else if (value) {
35+
switch (data.inferredContentType) {
36+
case 'application/json':
37+
return <ContextData data={value} />;
38+
case 'application/x-www-form-urlencoded':
39+
case 'multipart/form-data':
40+
return (
41+
<KeyValueList data={objectToSortedTupleArray(value)} isContextData={true} />
42+
);
43+
default:
44+
return <pre>{JSON.stringify(value, null, 2)}</pre>;
45+
}
46+
} else {
47+
return null;
2848
}
2949
};
3050

@@ -55,11 +75,19 @@ class RichHttpContent extends React.Component {
5575
</ClippedBox>
5676
)}
5777

58-
{data.data && (
59-
<ClippedBox title={t('Body')}>
60-
<ErrorBoundary mini>{this.getBodySection(data)}</ErrorBoundary>
61-
</ClippedBox>
62-
)}
78+
<MetaData object={data} prop="data">
79+
{(value, meta) => {
80+
if (value || meta) {
81+
return (
82+
<ClippedBox title={t('Body')}>
83+
{this.getBodySection(data, value, meta)}
84+
</ClippedBox>
85+
);
86+
}
87+
88+
return null;
89+
}}
90+
</MetaData>
6391

6492
{data.cookies && !objectIsEmpty(data.cookies) && (
6593
<ClippedBox title={t('Cookies')} defaultCollapsed>

src/sentry/static/sentry/app/components/events/meta/annotatedText.jsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,31 @@ const ErrorIcon = styled(InlineSvg)`
3636
const REMARKS = {
3737
a: 'Annotated',
3838
x: 'Removed',
39-
s: 'Substituted',
39+
s: 'Replaced',
4040
m: 'Masked',
4141
p: 'Pseudonymized',
4242
e: 'Encrypted',
4343
};
4444

45+
const KNOWN_RULES = {
46+
'!limit': 'size limits',
47+
'!raw': 'raw payload',
48+
'!config': 'SDK configuration',
49+
};
50+
51+
function getTooltipText(remark, rule) {
52+
const remark_title = REMARKS[remark];
53+
const rule_title = KNOWN_RULES[rule] || t('PII rule "%s"', rule);
54+
if (remark_title) {
55+
return t('%s because of %s', remark_title, rule_title);
56+
} else {
57+
return rule_title;
58+
}
59+
}
60+
4561
function renderChunk(chunk) {
4662
if (chunk.type === 'redaction') {
47-
const title = t('%s due to PII rule "%s"', REMARKS[chunk.remark], chunk.rule_id);
63+
const title = getTooltipText(chunk.remark, chunk.rule_id);
4864
return (
4965
<Tooltip title={title}>
5066
<Redaction>{chunk.text}</Redaction>
@@ -56,22 +72,18 @@ function renderChunk(chunk) {
5672
}
5773

5874
function renderChunks(chunks) {
59-
if (chunks.length === 1) {
60-
return chunks[0].text;
61-
}
62-
6375
const spans = chunks.map((chunk, key) => React.cloneElement(renderChunk(chunk), {key}));
6476

6577
return <Chunks>{spans}</Chunks>;
6678
}
6779

6880
function renderValue(value, chunks, errors, remarks) {
69-
if (chunks.length) {
81+
if (chunks.length > 1) {
7082
return renderChunks(chunks);
7183
}
7284

7385
let element = null;
74-
if (!_.isNull(value)) {
86+
if (value) {
7587
element = <Redaction>{value}</Redaction>;
7688
} else if (errors && errors.length) {
7789
element = <Placeholder>invalid</Placeholder>;
@@ -80,7 +92,7 @@ function renderValue(value, chunks, errors, remarks) {
8092
}
8193

8294
if (remarks && remarks.length) {
83-
const title = t('%s due to PII rule "%s"', REMARKS[remarks[0][1]], remarks[0][0]);
95+
const title = getTooltipText(remarks[0][1], remarks[0][0]);
8496
element = <Tooltip title={title}>{element}</Tooltip>;
8597
}
8698

tests/acceptance/test_issue_details.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ def test_python_event(self):
5454
self.visit_issue(event.group.id)
5555
self.browser.snapshot('issue details python')
5656

57+
def test_python_rawbody_event(self):
58+
event = self.create_sample_event(
59+
platform='python-rawbody',
60+
)
61+
self.visit_issue(event.group.id)
62+
self.browser.move_to('.request pre span')
63+
self.browser.snapshot('issue details python raw body')
64+
65+
def test_python_formdata_event(self):
66+
event = self.create_sample_event(
67+
platform='python-formdata',
68+
)
69+
self.visit_issue(event.group.id)
70+
self.browser.snapshot('issue details python formdata')
71+
5772
def test_cocoa_event(self):
5873
event = self.create_sample_event(
5974
platform='cocoa',

tests/js/spec/components/events/__snapshots__/crashContent.spec.jsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ exports[`CrashContent renders with meta data 1`] = `
127127
<Tooltip
128128
containerDisplayMode="inline-block"
129129
position="top"
130-
title="Pseudonymized due to PII rule \\"device_id\\""
130+
title="Pseudonymized because of PII rule \\"device_id\\""
131131
>
132132
<Manager>
133133
<Reference>

0 commit comments

Comments
 (0)