From e26e3523b0074078d6ca0d2ddd0c76253657bd7e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 15 Jul 2019 13:12:33 -0400 Subject: [PATCH 1/4] feat(apm) Expose transaction start & end timestamp On transaction events expose both the start and end timestamps. This will allow us to correctly display the transaction span length. Refs SEN-799 --- src/sentry/api/serializers/models/event.py | 5 + src/sentry/data/samples/transaction.json | 160 +++++++++++++++++++++ src/sentry/utils/samples.py | 2 +- tests/sentry/api/serializers/test_event.py | 15 +- 4 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/sentry/data/samples/transaction.json diff --git a/src/sentry/api/serializers/models/event.py b/src/sentry/api/serializers/models/event.py index b2ba6945ad6a17..96622ec033d865 100644 --- a/src/sentry/api/serializers/models/event.py +++ b/src/sentry/api/serializers/models/event.py @@ -285,6 +285,11 @@ def serialize(self, obj, attrs, user): 'tags': tags_meta, }, } + # transaction events have start and end-times that are + # timestamp floats. + if obj.get_event_type() == 'transaction': + d['startTimestamp'] = obj.data.get('start_timestamp') + d['timestamp'] = obj.data.get('timestamp') return d diff --git a/src/sentry/data/samples/transaction.json b/src/sentry/data/samples/transaction.json new file mode 100644 index 00000000000000..9e48ad9fc458a4 --- /dev/null +++ b/src/sentry/data/samples/transaction.json @@ -0,0 +1,160 @@ +{ + "platform": "python", + "message": "", + "tags": [ + [ + "application", + "countries" + ], + [ + "browser", + "Python Requests 2.22" + ], + [ + "browser.name", + "Python Requests" + ], + [ + "environment", + "dev" + ], + [ + "release", + "v0.1" + ], + [ + "user", + "ip:127.0.0.1" + ], + [ + "trace", + "a7d67cf796774551a95be6543cacd459" + ], + [ + "trace.ctx", + "a7d67cf796774551a95be6543cacd459-babaae0d4b7512d9" + ], + [ + "trace.span", + "babaae0d4b7512d9" + ], + [ + "transaction", + "/country_by_code/" + ], + [ + "url", + "http://countries:8010/country_by_code/" + ] + ], + "breadcrumbs": { + "values": [ + { + "category": "query", + "timestamp":1562681591.0, + "message": "SELECT \"countries\".\"id\", \"countries\".\"name\", \"countries\".\"continent\", \"countries\".\"region\", \"countries\".\"surface_area\", \"coun...'CAN'", + "type": "default", + "level": "info" + } + ] + }, + "contexts": { + "runtime": { + "version": "3.7.3", + "type": "runtime", + "name": "CPython", + "build": "3.7.3 (default, Jun 27 2019, 22:53:21) \n[GCC 8.3.0]" + }, + "trace": { + "parent_span_id": "8988cec7cc0779c1", + "type": "trace", + "trace_id": "a7d67cf796774551a95be6543cacd459", + "span_id": "babaae0d4b7512d9" + }, + "browser": { + "version": "2.22", + "type": "browser", + "name": "Python Requests" + } + }, + "culprit": "/country_by_code/", + "environment": "dev", + "extra": {}, + "logger": "", + "metadata": { + "location": "/country_by_code/", + "title": "/country_by_code/" + }, + "request": { + "url": "http://countries:8010/country_by_code/", + "headers": [ + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "gzip, deflate" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Content-Length", + "" + ], + [ + "Content-Type", + "text/plain" + ], + [ + "Host", + "countries:8010" + ], + [ + "Sentry-Trace", + "a7d67cf796774551a95be6543cacd459-8988cec7cc0779c1-1" + ], + [ + "User-Agent", + "python-requests/2.22.0" + ] + ], + "env": { + "SERVER_PORT": "8010", + "SERVER_NAME": "a90286977562" + }, + "query_string": [ + [ + "code", + "CAN" + ] + ], + "method": "GET", + "inferred_content_type": "text/plain" + }, + "spans": [ + { + "start_timestamp": 1562681591.0, + "same_process_as_parent":true, + "description": "Django: SELECT \"countries\".\"id\", \"countries\".\"name\", \"countries\".\"continent\", \"countries\".\"region\", \"countries\".\"surface_area\", \"coun...'CAN'", + "tags": { + "error":false + }, + "timestamp":1562681591.0, + "parent_span_id": "babaae0d4b7512d9", + "trace_id": "a7d67cf796774551a95be6543cacd459", + "op": "db", + "data": {}, + "span_id": "b048b4c8fdc5168c" + } + ], + "start_timestamp": 1562681591.0, + "timestamp": 1562681591.0, + "transaction": "/country_by_code/", + "type": "transaction", + "user": { + "ip_address": "127.0.0.1" + } +} diff --git a/src/sentry/utils/samples.py b/src/sentry/utils/samples.py index 76539ae15cc6ab..9c2fae808a513a 100644 --- a/src/sentry/utils/samples.py +++ b/src/sentry/utils/samples.py @@ -132,7 +132,7 @@ def load_data(platform, default=None, sample_name=None): return data = CanonicalKeyDict(data) - if platform in ('csp', 'hkpk', 'expectct', 'expectstaple'): + if platform in ('transaction', 'csp', 'hkpk', 'expectct', 'expectstaple'): return data data['platform'] = platform diff --git a/tests/sentry/api/serializers/test_event.py b/tests/sentry/api/serializers/test_event.py index 183af77bb92270..b621a0bc2af427 100644 --- a/tests/sentry/api/serializers/test_event.py +++ b/tests/sentry/api/serializers/test_event.py @@ -11,6 +11,7 @@ ) from sentry.models import EventError from sentry.testutils import TestCase +from sentry.utils.samples import load_data class EventSerializerTest(TestCase): @@ -181,6 +182,18 @@ def test_none_interfaces(self): assert result['sdk'] is None assert result['contexts'] == {} + def test_transaction_event(self): + event_data = load_data('transaction') + event = self.store_event( + data=event_data, + project_id=self.project.id + ) + result = serialize(event) + assert result['timestamp'] == event.data.get('timestamp') + assert isinstance(result['timestamp'], float) + assert result['startTimestamp'] == event.data.get('start_timestamp') + assert isinstance(result['startTimestamp'], float) + class SharedEventSerializerTest(TestCase): def test_simple(self): @@ -199,7 +212,7 @@ def test_simple(self): assert entry['type'] != 'breadcrumbs' -class SnubaEventSerializerTest(TestCase): +class SimpleEventSerializerTest(TestCase): def test_user(self): """ Use the SimpleEventSerializer to serialize an event From 0c40d7217d5180886e8efa898f998d0a55474cb7 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 24 Jul 2019 13:11:29 -0400 Subject: [PATCH 2/4] Specialize transaction events differently Move logic to serialize transaction only events into a separate method. This also allows error/default only logic to be moved as well. I've elected to not emit `dateCreated` on transaction events as it is redundant given the start/end timestamps. --- src/sentry/api/serializers/models/event.py | 34 ++++++++++++++++------ tests/sentry/api/serializers/test_event.py | 12 ++++++-- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/sentry/api/serializers/models/event.py b/src/sentry/api/serializers/models/event.py index 96622ec033d865..c77a381ede30e2 100644 --- a/src/sentry/api/serializers/models/event.py +++ b/src/sentry/api/serializers/models/event.py @@ -257,10 +257,8 @@ def serialize(self, obj, attrs, user): 'message': message, 'title': obj.title, 'location': obj.location, - 'culprit': obj.culprit, 'user': attrs['user'], 'contexts': attrs['contexts'], - 'crashFile': attrs['crash_file'], 'sdk': attrs['sdk'], # TODO(dcramer): move into contexts['extra'] 'context': context, @@ -269,11 +267,8 @@ def serialize(self, obj, attrs, user): 'metadata': obj.get_event_metadata(), 'tags': tags, 'platform': obj.platform, - 'dateCreated': obj.datetime, 'dateReceived': received, 'errors': errors, - 'fingerprints': obj.get_hashes(), - 'groupingConfig': obj.get_grouping_config(), '_meta': { 'entries': attrs['_meta']['entries'], 'message': message_meta, @@ -285,13 +280,34 @@ def serialize(self, obj, attrs, user): 'tags': tags_meta, }, } - # transaction events have start and end-times that are - # timestamp floats. + # Serialize attributes that are specific to different types of events. if obj.get_event_type() == 'transaction': - d['startTimestamp'] = obj.data.get('start_timestamp') - d['timestamp'] = obj.data.get('timestamp') + d.update(self.serialize_transaction_attrs(attrs, obj)) + else: + d.update(self.serialize_error_attrs(attrs, obj)) return d + def serialize_transaction_attrs(self, attrs, obj): + """ + Add attributes that are only present on transaction events. + """ + return { + 'startTimestamp': obj.data.get('start_timestamp'), + 'endTimestamp': obj.data.get('timestamp'), + } + + def serialize_error_attrs(self, attrs, obj): + """ + Add attributes that are present on error and default event types + """ + return { + 'crashFile': attrs['crash_file'], + 'culprit': obj.culprit, + 'dateCreated': obj.datetime, + 'fingerprints': obj.get_hashes(), + 'groupingConfig': obj.get_grouping_config(), + } + class DetailedEventSerializer(EventSerializer): """ diff --git a/tests/sentry/api/serializers/test_event.py b/tests/sentry/api/serializers/test_event.py index b621a0bc2af427..7d19e60faaa60e 100644 --- a/tests/sentry/api/serializers/test_event.py +++ b/tests/sentry/api/serializers/test_event.py @@ -37,6 +37,8 @@ def test_eventerror(self): assert 'data' in result['errors'][0] assert result['errors'][0]['type'] == EventError.INVALID_DATA assert result['errors'][0]['data'] == {'name': u'ΓΌ'} + assert 'startTimestamp' not in result + assert 'timestamp' not in result def test_hidden_eventerror(self): event = self.create_event( @@ -181,6 +183,7 @@ def test_none_interfaces(self): assert result['user'] is None assert result['sdk'] is None assert result['contexts'] == {} + assert 'startTimestamp' not in result def test_transaction_event(self): event_data = load_data('transaction') @@ -189,10 +192,13 @@ def test_transaction_event(self): project_id=self.project.id ) result = serialize(event) - assert result['timestamp'] == event.data.get('timestamp') - assert isinstance(result['timestamp'], float) - assert result['startTimestamp'] == event.data.get('start_timestamp') + assert isinstance(result['endTimestamp'], float) + assert result['endTimestamp'] == event.data.get('timestamp') assert isinstance(result['startTimestamp'], float) + assert result['startTimestamp'] == event.data.get('start_timestamp') + assert 'dateCreated' not in result + assert 'crashFile' not in result + assert 'fingerprints' not in result class SharedEventSerializerTest(TestCase): From bc6b3888379887efa7a2fcabc62b918b58ea4210 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 24 Jul 2019 14:56:34 -0400 Subject: [PATCH 3/4] Use dateCreated string or endTimestamp for transaction events. --- .../app/views/organizationEventsV2/eventModalContent.jsx | 5 ++++- .../app/views/organizationEventsV2/modalLineGraph.jsx | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/eventModalContent.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/eventModalContent.jsx index 37a52aaee9b019..e536ac74bc1cec 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/eventModalContent.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/eventModalContent.jsx @@ -95,7 +95,10 @@ const EventMetadata = props => { ID {event.eventID} JSON () diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx index 51c72dd63bbf6c..612ca4b59df561 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx @@ -27,7 +27,9 @@ import {MODAL_QUERY_KEYS, PIN_ICON} from './data'; */ const getCurrentEventMarker = currentEvent => { const title = t('Current Event'); - const eventTime = +new Date(currentEvent.dateCreated); + const eventTime = +new Date( + currentEvent.dateCreated || currentEvent.endTimestamp * 1000 + ); return { type: 'line', @@ -42,7 +44,7 @@ const getCurrentEventMarker = currentEvent => { }, }, tooltip: { - formatter: ({data}) => { + formatter: () => { return `
${getFormattedDate(eventTime, 'MMM D, YYYY LT')}
`; }, }, From 1344a2deb8b7715d251bde9f2dfa4433ffca1302 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 24 Jul 2019 14:57:56 -0400 Subject: [PATCH 4/4] Make event type serialization methods private --- src/sentry/api/serializers/models/event.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sentry/api/serializers/models/event.py b/src/sentry/api/serializers/models/event.py index c77a381ede30e2..6e906a4527a621 100644 --- a/src/sentry/api/serializers/models/event.py +++ b/src/sentry/api/serializers/models/event.py @@ -282,12 +282,12 @@ def serialize(self, obj, attrs, user): } # Serialize attributes that are specific to different types of events. if obj.get_event_type() == 'transaction': - d.update(self.serialize_transaction_attrs(attrs, obj)) + d.update(self.__serialize_transaction_attrs(attrs, obj)) else: - d.update(self.serialize_error_attrs(attrs, obj)) + d.update(self.__serialize_error_attrs(attrs, obj)) return d - def serialize_transaction_attrs(self, attrs, obj): + def __serialize_transaction_attrs(self, attrs, obj): """ Add attributes that are only present on transaction events. """ @@ -296,7 +296,7 @@ def serialize_transaction_attrs(self, attrs, obj): 'endTimestamp': obj.data.get('timestamp'), } - def serialize_error_attrs(self, attrs, obj): + def __serialize_error_attrs(self, attrs, obj): """ Add attributes that are present on error and default event types """