1
1
import logging
2
2
from collections .abc import Generator , Iterator
3
3
from datetime import datetime
4
- from typing import Any , TypedDict
4
+ from typing import Any , Literal , TypedDict
5
5
from urllib .parse import urlparse
6
6
7
7
import sentry_sdk
8
8
9
9
from sentry import nodestore
10
10
from sentry .constants import ObjectStatus
11
11
from sentry .models .project import Project
12
+ from sentry .replays .usecases .ingest .event_parser import EventType
12
13
from sentry .replays .usecases .ingest .event_parser import (
13
- EventType ,
14
- parse_network_content_lengths ,
15
- which ,
14
+ get_timestamp_ms as get_replay_event_timestamp_ms ,
16
15
)
16
+ from sentry .replays .usecases .ingest .event_parser import parse_network_content_lengths , which
17
17
from sentry .search .events .builder .discover import DiscoverQueryBuilder
18
18
from sentry .search .events .types import QueryBuilderConfig , SnubaParams
19
19
from sentry .services .eventstore .models import Event
@@ -59,21 +59,18 @@ def fetch_error_details(project_id: int, error_ids: list[str]) -> list[EventDict
59
59
return []
60
60
61
61
62
- def parse_timestamp (timestamp_value : Any , unit : str ) -> float :
63
- """Parse a timestamp input to a float value.
64
- The argument timestamp value can be string, float, or None.
65
- The returned unit will be the same as the input unit.
62
+ def _parse_snuba_timestamp_to_ms (timestamp : str | float , input_unit : Literal ["s" , "ms" ]) -> float :
66
63
"""
67
- if timestamp_value is not None :
68
- if isinstance ( timestamp_value , str ):
69
- try :
70
- dt = datetime . fromisoformat ( timestamp_value . replace ( "Z" , "+00:00" ))
71
- return dt . timestamp () * 1000 if unit == "ms" else dt . timestamp ( )
72
- except ( ValueError , AttributeError ):
73
- return 0.0
74
- else :
75
- return float ( timestamp_value )
76
- return 0.0
64
+ Parse a numeric or ISO timestamp to float milliseconds. `input_unit` is only used for numeric inputs.
65
+ """
66
+ if isinstance ( timestamp , str ) :
67
+ try :
68
+ dt = datetime . fromisoformat ( timestamp . replace ( "Z" , "+00:00" ) )
69
+ return dt . timestamp () * 1000
70
+ except ( ValueError , AttributeError ):
71
+ return 0.0
72
+
73
+ return float ( timestamp ) * 1000 if input_unit == "s" else float ( timestamp )
77
74
78
75
79
76
@sentry_sdk .trace
@@ -138,9 +135,11 @@ def fetch_trace_connected_errors(
138
135
error_data = query .process_results (result )["data" ]
139
136
140
137
for event in error_data :
141
- timestamp_ms = parse_timestamp (event .get ("timestamp_ms" ), "ms" )
142
- timestamp_s = parse_timestamp (event .get ("timestamp" ), "s" )
143
- timestamp = timestamp_ms or timestamp_s * 1000
138
+ snuba_ts_ms = event .get ("timestamp_ms" , 0.0 )
139
+ snuba_ts_s = event .get ("timestamp" , 0.0 )
140
+ timestamp = _parse_snuba_timestamp_to_ms (
141
+ snuba_ts_ms , "ms"
142
+ ) or _parse_snuba_timestamp_to_ms (snuba_ts_s , "s" )
144
143
145
144
if timestamp :
146
145
error_events .append (
@@ -227,16 +226,18 @@ def generate_summary_logs(
227
226
for _ , segment in segment_data :
228
227
events = json .loads (segment .tobytes ().decode ("utf-8" ))
229
228
for event in events :
229
+ event_type = which (event )
230
+ timestamp = get_replay_event_timestamp_ms (event , event_type )
231
+
230
232
# Check if we need to yield any error messages that occurred before this event
231
- while error_idx < len ( error_events ) and error_events [ error_idx ][
232
- "timestamp"
233
- ] < event . get ( "timestamp" , 0 ):
233
+ while (
234
+ error_idx < len ( error_events ) and error_events [ error_idx ][ "timestamp" ] < timestamp
235
+ ):
234
236
error = error_events [error_idx ]
235
237
yield generate_error_log_message (error )
236
238
error_idx += 1
237
239
238
240
# Yield the current event's log message
239
- event_type = which (event )
240
241
if event_type == EventType .FEEDBACK :
241
242
feedback_id = event ["data" ]["payload" ].get ("data" , {}).get ("feedbackId" )
242
243
feedback = fetch_feedback_details (feedback_id , project_id )
@@ -262,7 +263,7 @@ def as_log_message(event: dict[str, Any]) -> str | None:
262
263
should be forked.
263
264
"""
264
265
event_type = which (event )
265
- timestamp = event . get ( "timestamp" , 0.0 )
266
+ timestamp = get_replay_event_timestamp_ms ( event , event_type )
266
267
267
268
try :
268
269
match event_type :
@@ -276,20 +277,16 @@ def as_log_message(event: dict[str, Any]) -> str | None:
276
277
message = event ["data" ]["payload" ]["message" ]
277
278
return f"User rage clicked on { message } but the triggered action was slow to complete at { timestamp } "
278
279
case EventType .NAVIGATION_SPAN :
279
- timestamp_ms = timestamp * 1000
280
280
to = event ["data" ]["payload" ]["description" ]
281
- return f"User navigated to: { to } at { timestamp_ms } "
281
+ return f"User navigated to: { to } at { timestamp } "
282
282
case EventType .CONSOLE :
283
283
message = event ["data" ]["payload" ]["message" ]
284
284
return f"Logged: { message } at { timestamp } "
285
285
case EventType .UI_BLUR :
286
- # timestamp_ms = timestamp * 1000
287
286
return None
288
287
case EventType .UI_FOCUS :
289
- # timestamp_ms = timestamp * 1000
290
288
return None
291
289
case EventType .RESOURCE_FETCH :
292
- timestamp_ms = timestamp * 1000
293
290
payload = event ["data" ]["payload" ]
294
291
method = payload ["data" ]["method" ]
295
292
status_code = payload ["data" ]["statusCode" ]
@@ -311,14 +308,13 @@ def as_log_message(event: dict[str, Any]) -> str | None:
311
308
return None
312
309
313
310
if response_size is None :
314
- return f'Application initiated request: "{ method } { path } HTTP/2.0" with status code { status_code } ; took { duration } milliseconds at { timestamp_ms } '
311
+ return f'Application initiated request: "{ method } { path } HTTP/2.0" with status code { status_code } ; took { duration } milliseconds at { timestamp } '
315
312
else :
316
- return f'Application initiated request: "{ method } { path } HTTP/2.0" with status code { status_code } and response size { response_size } ; took { duration } milliseconds at { timestamp_ms } '
313
+ return f'Application initiated request: "{ method } { path } HTTP/2.0" with status code { status_code } and response size { response_size } ; took { duration } milliseconds at { timestamp } '
317
314
case EventType .LCP :
318
- timestamp_ms = timestamp * 1000
319
315
duration = event ["data" ]["payload" ]["data" ]["size" ]
320
316
rating = event ["data" ]["payload" ]["data" ]["rating" ]
321
- return f"Application largest contentful paint: { duration } ms and has a { rating } rating at { timestamp_ms } "
317
+ return f"Application largest contentful paint: { duration } ms and has a { rating } rating at { timestamp } "
322
318
case EventType .HYDRATION_ERROR :
323
319
return f"There was a hydration error on the page at { timestamp } "
324
320
case EventType .RESOURCE_XHR :
0 commit comments