Skip to content

Commit

Permalink
feat: Annotate empty request bodies (#13606)
Browse files Browse the repository at this point in the history
  • Loading branch information
untitaker authored Jun 12, 2019
1 parent 286145f commit 827aa83
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 37 deletions.
60 changes: 60 additions & 0 deletions src/sentry/data/samples/python-formdata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"_meta": {
"request": {
"data": {
"bam": {
"": {
"len": 0,
"rem": [
[
"!raw",
"x",
0,
0
]
]
}
}
}
}
},
"request": {
"url": "http://127.0.0.1:5000/",
"headers": [
[
"Accept",
"*/*"
],
[
"Content-Length",
"75378"
],
[
"Content-Type",
"multipart/form-data; boundary=------------------------f1861cf25dfa767f"
],
[
"Expect",
"100-continue"
],
[
"Host",
"127.0.0.1:5000"
],
[
"User-Agent",
"curl/7.54.0"
]
],
"env": {
"SERVER_PORT": "5000",
"SERVER_NAME": "127.0.0.1"
},
"data": {
"bam": "",
"foo": "bar"
},
"method": "POST",
"inferred_content_type": "multipart/form-data"
}
}
62 changes: 62 additions & 0 deletions src/sentry/data/samples/python-omittedbody.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"_meta": {
"request": {
"data": {
"": {
"len": 5,
"rem": [
[
"!config",
"x",
0,
5
]
]
}
}
}
},
"request": {
"url": "http://127.0.0.1:5000/",
"headers": [
[
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
],
[
"Accept-Encoding",
"gzip, deflate"
],
[
"Accept-Language",
"en-US,en;q=0.5"
],
[
"Cache-Control",
"max-age=0"
],
[
"Connection",
"keep-alive"
],
[
"Host",
"127.0.0.1:5000"
],
[
"Upgrade-Insecure-Requests",
"1"
],
[
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"
]
],
"data": "",
"method": "POST",
"env": {
"SERVER_PORT": "5000",
"SERVER_NAME": "127.0.0.1"
}
}
}
62 changes: 62 additions & 0 deletions src/sentry/data/samples/python-rawbody.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"_meta": {
"request": {
"data": {
"": {
"len": 5,
"rem": [
[
"!raw",
"x",
0,
5
]
]
}
}
}
},
"request": {
"url": "http://127.0.0.1:5000/",
"headers": [
[
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
],
[
"Accept-Encoding",
"gzip, deflate"
],
[
"Accept-Language",
"en-US,en;q=0.5"
],
[
"Cache-Control",
"max-age=0"
],
[
"Connection",
"keep-alive"
],
[
"Host",
"127.0.0.1:5000"
],
[
"Upgrade-Insecure-Requests",
"1"
],
[
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"
]
],
"data": "",
"method": "POST",
"env": {
"SERVER_PORT": "5000",
"SERVER_NAME": "127.0.0.1"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,43 @@ import ClippedBox from 'app/components/clippedBox';
import ContextData from 'app/components/contextData';
import ErrorBoundary from 'app/components/errorBoundary';
import KeyValueList from 'app/components/events/interfaces/keyValueList';
import AnnotatedText from 'app/components/events/meta/annotatedText';
import MetaData from 'app/components/events/meta/metaData';

class RichHttpContent extends React.Component {
static propTypes = {
data: PropTypes.object.isRequired,
};

getBodySection = data => {
getBodySection = (data, value, meta) => {
// The http interface provides an inferred content type for the data body.
switch (data.inferredContentType) {
case 'application/json':
return <ContextData data={data.data} />;
case 'application/x-www-form-urlencoded':
return (
<KeyValueList data={objectToSortedTupleArray(data.data)} isContextData={true} />
);
default:
return <pre>{JSON.stringify(data.data, null, 2)}</pre>;
if (meta && (!value || value instanceof String)) {
// TODO(markus): Currently annotated nested objects are shown without
// annotations.
return (
<pre>
<AnnotatedText
value={value}
chunks={meta.chunks}
remarks={meta.rem}
errors={meta.err}
/>
</pre>
);
} else if (value) {
switch (data.inferredContentType) {
case 'application/json':
return <ContextData data={value} />;
case 'application/x-www-form-urlencoded':
case 'multipart/form-data':
return (
<KeyValueList data={objectToSortedTupleArray(value)} isContextData={true} />
);
default:
return <pre>{JSON.stringify(value, null, 2)}</pre>;
}
} else {
return null;
}
};

Expand Down Expand Up @@ -55,11 +75,19 @@ class RichHttpContent extends React.Component {
</ClippedBox>
)}

{data.data && (
<ClippedBox title={t('Body')}>
<ErrorBoundary mini>{this.getBodySection(data)}</ErrorBoundary>
</ClippedBox>
)}
<MetaData object={data} prop="data">
{(value, meta) => {
if (value || meta) {
return (
<ClippedBox title={t('Body')}>
{this.getBodySection(data, value, meta)}
</ClippedBox>
);
}

return null;
}}
</MetaData>

{data.cookies && !objectIsEmpty(data.cookies) && (
<ClippedBox title={t('Cookies')} defaultCollapsed>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,31 @@ const ErrorIcon = styled(InlineSvg)`
const REMARKS = {
a: 'Annotated',
x: 'Removed',
s: 'Substituted',
s: 'Replaced',
m: 'Masked',
p: 'Pseudonymized',
e: 'Encrypted',
};

const KNOWN_RULES = {
'!limit': 'size limits',
'!raw': 'raw payload',
'!config': 'SDK configuration',
};

function getTooltipText(remark, rule) {
const remark_title = REMARKS[remark];
const rule_title = KNOWN_RULES[rule] || t('PII rule "%s"', rule);
if (remark_title) {
return t('%s because of %s', remark_title, rule_title);
} else {
return rule_title;
}
}

function renderChunk(chunk) {
if (chunk.type === 'redaction') {
const title = t('%s due to PII rule "%s"', REMARKS[chunk.remark], chunk.rule_id);
const title = getTooltipText(chunk.remark, chunk.rule_id);
return (
<Tooltip title={title}>
<Redaction>{chunk.text}</Redaction>
Expand All @@ -56,22 +72,18 @@ function renderChunk(chunk) {
}

function renderChunks(chunks) {
if (chunks.length === 1) {
return chunks[0].text;
}

const spans = chunks.map((chunk, key) => React.cloneElement(renderChunk(chunk), {key}));

return <Chunks>{spans}</Chunks>;
}

function renderValue(value, chunks, errors, remarks) {
if (chunks.length) {
if (chunks.length > 1) {
return renderChunks(chunks);
}

let element = null;
if (!_.isNull(value)) {
if (value) {
element = <Redaction>{value}</Redaction>;
} else if (errors && errors.length) {
element = <Placeholder>invalid</Placeholder>;
Expand All @@ -80,7 +92,7 @@ function renderValue(value, chunks, errors, remarks) {
}

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

Expand Down
15 changes: 15 additions & 0 deletions tests/acceptance/test_issue_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ def test_python_event(self):
self.visit_issue(event.group.id)
self.browser.snapshot('issue details python')

def test_python_rawbody_event(self):
event = self.create_sample_event(
platform='python-rawbody',
)
self.visit_issue(event.group.id)
self.browser.move_to('.request pre span')
self.browser.snapshot('issue details python raw body')

def test_python_formdata_event(self):
event = self.create_sample_event(
platform='python-formdata',
)
self.visit_issue(event.group.id)
self.browser.snapshot('issue details python formdata')

def test_cocoa_event(self):
event = self.create_sample_event(
platform='cocoa',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ exports[`CrashContent renders with meta data 1`] = `
<Tooltip
containerDisplayMode="inline-block"
position="top"
title="Pseudonymized due to PII rule \\"device_id\\""
title="Pseudonymized because of PII rule \\"device_id\\""
>
<Manager>
<Reference>
Expand Down
Loading

0 comments on commit 827aa83

Please sign in to comment.