1
1
import uuid
2
2
import zlib
3
- from datetime import datetime , timezone
3
+ from datetime import UTC , datetime , timezone
4
4
from unittest .mock import patch
5
5
6
6
import requests
11
11
from sentry import nodestore
12
12
from sentry .eventstore .models import Event
13
13
from sentry .replays .endpoints .project_replay_summarize_breadcrumbs import (
14
- ErrorEvent ,
14
+ GroupEvent ,
15
15
as_log_message ,
16
16
get_request_data ,
17
17
)
18
18
from sentry .replays .lib .storage import FilestoreBlob , RecordingSegmentStorageMeta
19
19
from sentry .replays .testutils import mock_replay
20
20
from sentry .testutils .cases import TransactionTestCase
21
+ from sentry .testutils .pytest .fixtures import django_db_all
21
22
from sentry .testutils .skips import requires_snuba
22
23
from sentry .utils import json
23
24
@@ -56,6 +57,42 @@ def save_recording_segment(
56
57
)
57
58
FilestoreBlob ().set (metadata , zlib .compress (data ) if compressed else data )
58
59
60
+ def mock_create_feedback_occurrence (self , project_id : int , replay_id : str | None = None ):
61
+ dt = datetime .now (UTC )
62
+
63
+ event = {
64
+ "project_id" : project_id ,
65
+ "event_id" : "56b08cf7852c42cbb95e4a6998c66ad6" ,
66
+ "timestamp" : dt .timestamp (),
67
+ "received" : dt .isoformat (),
68
+ "first_seen" : dt .isoformat (),
69
+ "user" : {
70
+ "ip_address" : "72.164.175.154" ,
71
+ "email" : "josh.ferge@sentry.io" ,
72
+ "id" : 880461 ,
73
+ "isStaff" : False ,
74
+ "name" : "Josh Ferge" ,
75
+ },
76
+ "contexts" : {
77
+ "feedback" : {
78
+ "contact_email" : "josh.ferge@sentry.io" ,
79
+ "name" : "Josh Ferge" ,
80
+ "message" : "Great website!" ,
81
+ "replay_id" : replay_id ,
82
+ "url" : "https://sentry.sentry.io/feedback/?statsPeriod=14d" ,
83
+ },
84
+ },
85
+ }
86
+
87
+ self .store_event (
88
+ data = {
89
+ "event_id" : event ["event_id" ],
90
+ "timestamp" : event ["timestamp" ],
91
+ "contexts" : event ["contexts" ],
92
+ },
93
+ project_id = self .project .id ,
94
+ )
95
+
59
96
@patch ("sentry.replays.endpoints.project_replay_summarize_breadcrumbs.make_seer_request" )
60
97
def test_get (self , make_seer_request ):
61
98
return_value = json .dumps ({"hello" : "world" }).encode ()
@@ -215,7 +252,7 @@ def test_get_with_error(self, make_seer_request):
215
252
assert response .content == return_value
216
253
217
254
@patch ("sentry.replays.endpoints.project_replay_summarize_breadcrumbs.make_seer_request" )
218
- def test_get_with_error_context_disabled (self , make_seer_request ):
255
+ def test_get_with_error_context_disabled_and_enabled (self , make_seer_request ):
219
256
"""Test handling of breadcrumbs with error context disabled"""
220
257
return_value = json .dumps ({"error" : "An error happened" }).encode ()
221
258
make_seer_request .return_value = return_value
@@ -261,6 +298,7 @@ def test_get_with_error_context_disabled(self, make_seer_request):
261
298
]
262
299
self .save_recording_segment (0 , json .dumps (data ).encode ())
263
300
301
+ # with error context disabled
264
302
with self .feature (
265
303
{
266
304
"organizations:session-replay" : True ,
@@ -280,8 +318,88 @@ def test_get_with_error_context_disabled(self, make_seer_request):
280
318
assert response .get ("Content-Type" ) == "application/json"
281
319
assert response .content == return_value
282
320
321
+ # with error context enabled
322
+ with self .feature (
323
+ {
324
+ "organizations:session-replay" : True ,
325
+ "organizations:replay-ai-summaries" : True ,
326
+ "organizations:gen-ai-features" : True ,
327
+ }
328
+ ):
329
+ response = self .client .get (self .url , {"enable_error_context" : "true" })
330
+
331
+ call_args = json .loads (make_seer_request .call_args [0 ][0 ])
332
+ assert "logs" in call_args
333
+ assert any ("ZeroDivisionError" in log for log in call_args ["logs" ])
334
+ assert any ("division by zero" in log for log in call_args ["logs" ])
335
+
336
+ assert response .status_code == 200
337
+ assert response .get ("Content-Type" ) == "application/json"
338
+ assert response .content == return_value
339
+
340
+ @patch ("sentry.replays.endpoints.project_replay_summarize_breadcrumbs.make_seer_request" )
341
+ def test_get_with_feedback (self , make_seer_request ):
342
+ """Test handling of breadcrumbs with user feedback"""
343
+ return_value = json .dumps ({"feedback" : "Feedback was submitted" }).encode ()
344
+ make_seer_request .return_value = return_value
345
+
346
+ self .mock_create_feedback_occurrence (self .project .id , replay_id = self .replay_id )
347
+
348
+ now = datetime .now (timezone .utc )
349
+
350
+ self .store_replays (
351
+ mock_replay (
352
+ now ,
353
+ self .project .id ,
354
+ self .replay_id ,
355
+ )
356
+ )
357
+
358
+ data = [
359
+ {
360
+ "type" : 5 ,
361
+ "timestamp" : float (now .timestamp ()),
362
+ "data" : {
363
+ "tag" : "breadcrumb" ,
364
+ "payload" : {"category" : "console" , "message" : "hello" },
365
+ },
366
+ },
367
+ {
368
+ "type" : 5 ,
369
+ "timestamp" : float (now .timestamp ()),
370
+ "data" : {
371
+ "tag" : "breadcrumb" ,
372
+ "payload" : {
373
+ "category" : "sentry.feedback" ,
374
+ "data" : {"feedback_id" : "56b08cf7852c42cbb95e4a6998c66ad6" },
375
+ },
376
+ },
377
+ },
378
+ ]
379
+ self .save_recording_segment (0 , json .dumps (data ).encode ())
380
+
381
+ with self .feature (
382
+ {
383
+ "organizations:session-replay" : True ,
384
+ "organizations:replay-ai-summaries" : True ,
385
+ "organizations:gen-ai-features" : True ,
386
+ }
387
+ ):
388
+ response = self .client .get (self .url )
389
+
390
+ make_seer_request .assert_called_once ()
391
+ call_args = json .loads (make_seer_request .call_args [0 ][0 ])
392
+ assert "logs" in call_args
393
+ assert any ("Great website!" in log for log in call_args ["logs" ])
394
+ assert any ("User submitted feedback" in log for log in call_args ["logs" ])
395
+
396
+ assert response .status_code == 200
397
+ assert response .get ("Content-Type" ) == "application/json"
398
+ assert response .content == return_value
399
+
283
400
284
- def test_get_request_data ():
401
+ @django_db_all
402
+ def test_get_request_data (default_project ):
285
403
def _faker ():
286
404
yield 0 , memoryview (
287
405
json .dumps (
@@ -307,14 +425,14 @@ def _faker():
307
425
)
308
426
309
427
error_events = [
310
- ErrorEvent (
428
+ GroupEvent (
311
429
category = "error" ,
312
430
id = "123" ,
313
431
title = "ZeroDivisionError" ,
314
432
timestamp = 3.0 ,
315
433
message = "division by zero" ,
316
434
),
317
- ErrorEvent (
435
+ GroupEvent (
318
436
category = "error" ,
319
437
id = "234" ,
320
438
title = "BadError" ,
@@ -323,7 +441,7 @@ def _faker():
323
441
),
324
442
]
325
443
326
- result = get_request_data (_faker (), error_events = error_events )
444
+ result = get_request_data (_faker (), error_events = error_events , project_id = default_project . id )
327
445
assert result == [
328
446
"User experienced an error: 'BadError: something else bad' at 1.0" ,
329
447
"Logged: hello at 1.5" ,
0 commit comments