Skip to content

Commit b5a0a68

Browse files
authored
feat(apm) Expose transaction start & end timestamp (#14007)
On transaction events expose both the start and end timestamps. This will allow us to correctly display the transaction span length. 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. Refs SEN-799
1 parent 525c9c7 commit b5a0a68

File tree

6 files changed

+214
-9
lines changed

6 files changed

+214
-9
lines changed

src/sentry/api/serializers/models/event.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,8 @@ def serialize(self, obj, attrs, user):
257257
'message': message,
258258
'title': obj.title,
259259
'location': obj.location,
260-
'culprit': obj.culprit,
261260
'user': attrs['user'],
262261
'contexts': attrs['contexts'],
263-
'crashFile': attrs['crash_file'],
264262
'sdk': attrs['sdk'],
265263
# TODO(dcramer): move into contexts['extra']
266264
'context': context,
@@ -269,11 +267,8 @@ def serialize(self, obj, attrs, user):
269267
'metadata': obj.get_event_metadata(),
270268
'tags': tags,
271269
'platform': obj.platform,
272-
'dateCreated': obj.datetime,
273270
'dateReceived': received,
274271
'errors': errors,
275-
'fingerprints': obj.get_hashes(),
276-
'groupingConfig': obj.get_grouping_config(),
277272
'_meta': {
278273
'entries': attrs['_meta']['entries'],
279274
'message': message_meta,
@@ -285,8 +280,34 @@ def serialize(self, obj, attrs, user):
285280
'tags': tags_meta,
286281
},
287282
}
283+
# Serialize attributes that are specific to different types of events.
284+
if obj.get_event_type() == 'transaction':
285+
d.update(self.__serialize_transaction_attrs(attrs, obj))
286+
else:
287+
d.update(self.__serialize_error_attrs(attrs, obj))
288288
return d
289289

290+
def __serialize_transaction_attrs(self, attrs, obj):
291+
"""
292+
Add attributes that are only present on transaction events.
293+
"""
294+
return {
295+
'startTimestamp': obj.data.get('start_timestamp'),
296+
'endTimestamp': obj.data.get('timestamp'),
297+
}
298+
299+
def __serialize_error_attrs(self, attrs, obj):
300+
"""
301+
Add attributes that are present on error and default event types
302+
"""
303+
return {
304+
'crashFile': attrs['crash_file'],
305+
'culprit': obj.culprit,
306+
'dateCreated': obj.datetime,
307+
'fingerprints': obj.get_hashes(),
308+
'groupingConfig': obj.get_grouping_config(),
309+
}
310+
290311

291312
class DetailedEventSerializer(EventSerializer):
292313
"""
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
{
2+
"platform": "python",
3+
"message": "",
4+
"tags": [
5+
[
6+
"application",
7+
"countries"
8+
],
9+
[
10+
"browser",
11+
"Python Requests 2.22"
12+
],
13+
[
14+
"browser.name",
15+
"Python Requests"
16+
],
17+
[
18+
"environment",
19+
"dev"
20+
],
21+
[
22+
"release",
23+
"v0.1"
24+
],
25+
[
26+
"user",
27+
"ip:127.0.0.1"
28+
],
29+
[
30+
"trace",
31+
"a7d67cf796774551a95be6543cacd459"
32+
],
33+
[
34+
"trace.ctx",
35+
"a7d67cf796774551a95be6543cacd459-babaae0d4b7512d9"
36+
],
37+
[
38+
"trace.span",
39+
"babaae0d4b7512d9"
40+
],
41+
[
42+
"transaction",
43+
"/country_by_code/"
44+
],
45+
[
46+
"url",
47+
"http://countries:8010/country_by_code/"
48+
]
49+
],
50+
"breadcrumbs": {
51+
"values": [
52+
{
53+
"category": "query",
54+
"timestamp":1562681591.0,
55+
"message": "SELECT \"countries\".\"id\", \"countries\".\"name\", \"countries\".\"continent\", \"countries\".\"region\", \"countries\".\"surface_area\", \"coun...'CAN'",
56+
"type": "default",
57+
"level": "info"
58+
}
59+
]
60+
},
61+
"contexts": {
62+
"runtime": {
63+
"version": "3.7.3",
64+
"type": "runtime",
65+
"name": "CPython",
66+
"build": "3.7.3 (default, Jun 27 2019, 22:53:21) \n[GCC 8.3.0]"
67+
},
68+
"trace": {
69+
"parent_span_id": "8988cec7cc0779c1",
70+
"type": "trace",
71+
"trace_id": "a7d67cf796774551a95be6543cacd459",
72+
"span_id": "babaae0d4b7512d9"
73+
},
74+
"browser": {
75+
"version": "2.22",
76+
"type": "browser",
77+
"name": "Python Requests"
78+
}
79+
},
80+
"culprit": "/country_by_code/",
81+
"environment": "dev",
82+
"extra": {},
83+
"logger": "",
84+
"metadata": {
85+
"location": "/country_by_code/",
86+
"title": "/country_by_code/"
87+
},
88+
"request": {
89+
"url": "http://countries:8010/country_by_code/",
90+
"headers": [
91+
[
92+
"Accept",
93+
"*/*"
94+
],
95+
[
96+
"Accept-Encoding",
97+
"gzip, deflate"
98+
],
99+
[
100+
"Connection",
101+
"keep-alive"
102+
],
103+
[
104+
"Content-Length",
105+
""
106+
],
107+
[
108+
"Content-Type",
109+
"text/plain"
110+
],
111+
[
112+
"Host",
113+
"countries:8010"
114+
],
115+
[
116+
"Sentry-Trace",
117+
"a7d67cf796774551a95be6543cacd459-8988cec7cc0779c1-1"
118+
],
119+
[
120+
"User-Agent",
121+
"python-requests/2.22.0"
122+
]
123+
],
124+
"env": {
125+
"SERVER_PORT": "8010",
126+
"SERVER_NAME": "a90286977562"
127+
},
128+
"query_string": [
129+
[
130+
"code",
131+
"CAN"
132+
]
133+
],
134+
"method": "GET",
135+
"inferred_content_type": "text/plain"
136+
},
137+
"spans": [
138+
{
139+
"start_timestamp": 1562681591.0,
140+
"same_process_as_parent":true,
141+
"description": "Django: SELECT \"countries\".\"id\", \"countries\".\"name\", \"countries\".\"continent\", \"countries\".\"region\", \"countries\".\"surface_area\", \"coun...'CAN'",
142+
"tags": {
143+
"error":false
144+
},
145+
"timestamp":1562681591.0,
146+
"parent_span_id": "babaae0d4b7512d9",
147+
"trace_id": "a7d67cf796774551a95be6543cacd459",
148+
"op": "db",
149+
"data": {},
150+
"span_id": "b048b4c8fdc5168c"
151+
}
152+
],
153+
"start_timestamp": 1562681591.0,
154+
"timestamp": 1562681591.0,
155+
"transaction": "/country_by_code/",
156+
"type": "transaction",
157+
"user": {
158+
"ip_address": "127.0.0.1"
159+
}
160+
}

src/sentry/static/sentry/app/views/organizationEventsV2/eventModalContent.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ const EventMetadata = props => {
9595
<MetadataContainer data-test-id="event-id">ID {event.eventID}</MetadataContainer>
9696
<MetadataContainer>
9797
<DateTime
98-
date={getDynamicText({value: event.dateCreated, fixed: 'Dummy timestamp'})}
98+
date={getDynamicText({
99+
value: event.dateCreated || event.endTimestamp * 1000,
100+
fixed: 'Dummy timestamp',
101+
})}
99102
/>
100103
<ExternalLink href={eventJsonUrl} className="json-link">
101104
JSON (<FileSize bytes={event.size} />)

src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import {MODAL_QUERY_KEYS, PIN_ICON} from './data';
2727
*/
2828
const getCurrentEventMarker = currentEvent => {
2929
const title = t('Current Event');
30-
const eventTime = +new Date(currentEvent.dateCreated);
30+
const eventTime = +new Date(
31+
currentEvent.dateCreated || currentEvent.endTimestamp * 1000
32+
);
3133

3234
return {
3335
type: 'line',

src/sentry/utils/samples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def load_data(platform, default=None, sample_name=None):
132132
return
133133

134134
data = CanonicalKeyDict(data)
135-
if platform in ('csp', 'hkpk', 'expectct', 'expectstaple'):
135+
if platform in ('transaction', 'csp', 'hkpk', 'expectct', 'expectstaple'):
136136
return data
137137

138138
data['platform'] = platform

tests/sentry/api/serializers/test_event.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from sentry.models import EventError
1313
from sentry.testutils import TestCase
14+
from sentry.utils.samples import load_data
1415

1516

1617
class EventSerializerTest(TestCase):
@@ -36,6 +37,8 @@ def test_eventerror(self):
3637
assert 'data' in result['errors'][0]
3738
assert result['errors'][0]['type'] == EventError.INVALID_DATA
3839
assert result['errors'][0]['data'] == {'name': u'ü'}
40+
assert 'startTimestamp' not in result
41+
assert 'timestamp' not in result
3942

4043
def test_hidden_eventerror(self):
4144
event = self.create_event(
@@ -180,6 +183,22 @@ def test_none_interfaces(self):
180183
assert result['user'] is None
181184
assert result['sdk'] is None
182185
assert result['contexts'] == {}
186+
assert 'startTimestamp' not in result
187+
188+
def test_transaction_event(self):
189+
event_data = load_data('transaction')
190+
event = self.store_event(
191+
data=event_data,
192+
project_id=self.project.id
193+
)
194+
result = serialize(event)
195+
assert isinstance(result['endTimestamp'], float)
196+
assert result['endTimestamp'] == event.data.get('timestamp')
197+
assert isinstance(result['startTimestamp'], float)
198+
assert result['startTimestamp'] == event.data.get('start_timestamp')
199+
assert 'dateCreated' not in result
200+
assert 'crashFile' not in result
201+
assert 'fingerprints' not in result
183202

184203

185204
class SharedEventSerializerTest(TestCase):
@@ -199,7 +218,7 @@ def test_simple(self):
199218
assert entry['type'] != 'breadcrumbs'
200219

201220

202-
class SnubaEventSerializerTest(TestCase):
221+
class SimpleEventSerializerTest(TestCase):
203222
def test_user(self):
204223
"""
205224
Use the SimpleEventSerializer to serialize an event

0 commit comments

Comments
 (0)