From 827aa83d04c49e445386a369704307301038f44d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 12 Jun 2019 11:27:27 +0200 Subject: [PATCH] feat: Annotate empty request bodies (#13606) --- src/sentry/data/samples/python-formdata.json | 60 ++++++++++++++++++ .../data/samples/python-omittedbody.json | 62 +++++++++++++++++++ src/sentry/data/samples/python-rawbody.json | 62 +++++++++++++++++++ .../events/interfaces/richHttpContent.jsx | 58 ++++++++++++----- .../components/events/meta/annotatedText.jsx | 30 ++++++--- tests/acceptance/test_issue_details.py | 15 +++++ .../__snapshots__/crashContent.spec.jsx.snap | 2 +- .../interfaces/richHttpContent.spec.jsx | 27 ++++---- 8 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 src/sentry/data/samples/python-formdata.json create mode 100644 src/sentry/data/samples/python-omittedbody.json create mode 100644 src/sentry/data/samples/python-rawbody.json diff --git a/src/sentry/data/samples/python-formdata.json b/src/sentry/data/samples/python-formdata.json new file mode 100644 index 00000000000000..9ba951dc06688f --- /dev/null +++ b/src/sentry/data/samples/python-formdata.json @@ -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" + } +} diff --git a/src/sentry/data/samples/python-omittedbody.json b/src/sentry/data/samples/python-omittedbody.json new file mode 100644 index 00000000000000..65edec18d2d73b --- /dev/null +++ b/src/sentry/data/samples/python-omittedbody.json @@ -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" + } + } +} diff --git a/src/sentry/data/samples/python-rawbody.json b/src/sentry/data/samples/python-rawbody.json new file mode 100644 index 00000000000000..a3cbd6166518ca --- /dev/null +++ b/src/sentry/data/samples/python-rawbody.json @@ -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" + } + } +} diff --git a/src/sentry/static/sentry/app/components/events/interfaces/richHttpContent.jsx b/src/sentry/static/sentry/app/components/events/interfaces/richHttpContent.jsx index 9c0711234be65c..6fede61d069884 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/richHttpContent.jsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/richHttpContent.jsx @@ -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 ; - case 'application/x-www-form-urlencoded': - return ( - - ); - default: - return
{JSON.stringify(data.data, null, 2)}
; + if (meta && (!value || value instanceof String)) { + // TODO(markus): Currently annotated nested objects are shown without + // annotations. + return ( +
+          
+        
+ ); + } else if (value) { + switch (data.inferredContentType) { + case 'application/json': + return ; + case 'application/x-www-form-urlencoded': + case 'multipart/form-data': + return ( + + ); + default: + return
{JSON.stringify(value, null, 2)}
; + } + } else { + return null; } }; @@ -55,11 +75,19 @@ class RichHttpContent extends React.Component { )} - {data.data && ( - - {this.getBodySection(data)} - - )} + + {(value, meta) => { + if (value || meta) { + return ( + + {this.getBodySection(data, value, meta)} + + ); + } + + return null; + }} + {data.cookies && !objectIsEmpty(data.cookies) && ( diff --git a/src/sentry/static/sentry/app/components/events/meta/annotatedText.jsx b/src/sentry/static/sentry/app/components/events/meta/annotatedText.jsx index 280f60a225ba6a..f1c9832f7f2b66 100644 --- a/src/sentry/static/sentry/app/components/events/meta/annotatedText.jsx +++ b/src/sentry/static/sentry/app/components/events/meta/annotatedText.jsx @@ -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 ( {chunk.text} @@ -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 {spans}; } 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 = {value}; } else if (errors && errors.length) { element = invalid; @@ -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 = {element}; } diff --git a/tests/acceptance/test_issue_details.py b/tests/acceptance/test_issue_details.py index 3f10e3fefe70f9..19b984d90bbd91 100644 --- a/tests/acceptance/test_issue_details.py +++ b/tests/acceptance/test_issue_details.py @@ -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', diff --git a/tests/js/spec/components/events/__snapshots__/crashContent.spec.jsx.snap b/tests/js/spec/components/events/__snapshots__/crashContent.spec.jsx.snap index 37be76cb7c29a6..e7989d89d212e4 100644 --- a/tests/js/spec/components/events/__snapshots__/crashContent.spec.jsx.snap +++ b/tests/js/spec/components/events/__snapshots__/crashContent.spec.jsx.snap @@ -127,7 +127,7 @@ exports[`CrashContent renders with meta data 1`] = ` diff --git a/tests/js/spec/components/events/interfaces/richHttpContent.spec.jsx b/tests/js/spec/components/events/interfaces/richHttpContent.spec.jsx index eb652c335ba877..0e92728c6cdaaf 100644 --- a/tests/js/spec/components/events/interfaces/richHttpContent.spec.jsx +++ b/tests/js/spec/components/events/interfaces/richHttpContent.spec.jsx @@ -22,19 +22,21 @@ describe('RichHttpContent', function() { describe('getBodySection', function() { it('should return plain-text when given unrecognized inferred Content-Type', function() { - const out = elem.getBodySection({ - inferredContentType: null, // no inferred content type - data: 'helloworld', - }); + const out = elem.getBodySection( + {inferredContentType: null}, // no inferred content type + 'helloworld', + null + ); expect(out.type).toEqual('pre'); }); it('should return a KeyValueList element when inferred Content-Type is x-www-form-urlencoded', function() { - const out = elem.getBodySection({ - inferredContentType: 'application/x-www-form-urlencoded', - data: {foo: ['bar'], bar: ['baz']}, - }); + const out = elem.getBodySection( + {inferredContentType: 'application/x-www-form-urlencoded'}, + {foo: ['bar'], bar: ['baz']}, + null + ); // NOTE: displayName is set manually in this class expect(out.type.displayName).toEqual('KeyValueList'); @@ -42,10 +44,11 @@ describe('RichHttpContent', function() { }); it('should return a ContextData element when inferred Content-Type is application/json', function() { - const out = elem.getBodySection({ - inferredContentType: 'application/json', - data: {foo: 'bar'}, - }); + const out = elem.getBodySection( + {inferredContentType: 'application/json'}, + {foo: 'bar'}, + null + ); // NOTE: displayName is set manually in this class expect(out.type.displayName).toEqual('ContextData');