@@ -268,12 +268,11 @@ def test_get_trace_tree_for_event(self):
268
268
# Update to patch both Transactions and Events dataset calls
269
269
with patch ("sentry.eventstore.backend.get_events" ) as mock_get_events :
270
270
271
- def side_effect (filter , dataset , ** kwargs ):
271
+ def side_effect (filter , dataset = None , ** kwargs ):
272
272
if dataset == Dataset .Transactions :
273
273
return tx_events
274
- elif dataset == Dataset . Events :
274
+ else :
275
275
return error_events
276
- return []
277
276
278
277
mock_get_events .side_effect = side_effect
279
278
@@ -336,10 +335,6 @@ def side_effect(filter, dataset, **kwargs):
336
335
337
336
# Verify that get_events was called twice - once for transactions and once for errors
338
337
assert mock_get_events .call_count == 2
339
- # Check that the first call used the Transactions dataset
340
- assert mock_get_events .call_args_list [0 ][1 ]["dataset" ] == Dataset .Transactions
341
- # Check that the second call used the Events dataset
342
- assert mock_get_events .call_args_list [1 ][1 ]["dataset" ] == Dataset .Events
343
338
344
339
@patch ("sentry.eventstore.backend.get_events" )
345
340
def test_get_trace_tree_empty_results (self , mock_get_events ):
@@ -431,12 +426,11 @@ def test_get_trace_tree_out_of_order_processing(self, mock_get_events):
431
426
parent_event .trace_id = trace_id
432
427
433
428
# Set up the mock to return different results for different dataset calls
434
- def side_effect (filter , dataset , ** kwargs ):
429
+ def side_effect (filter , dataset = None , ** kwargs ):
435
430
if dataset == Dataset .Transactions :
436
431
return [parent_event ] # Parent is a transaction
437
- elif dataset == Dataset . Events :
432
+ else :
438
433
return [child_event ] # Child is an error
439
- return []
440
434
441
435
mock_get_events .side_effect = side_effect
442
436
@@ -460,6 +454,282 @@ def side_effect(filter, dataset, **kwargs):
460
454
# Verify that get_events was called twice
461
455
assert mock_get_events .call_count == 2
462
456
457
+ @patch ("sentry.eventstore.backend.get_events" )
458
+ def test_get_trace_tree_with_only_errors (self , mock_get_events ):
459
+ """
460
+ Tests that when results contain only error events (no transactions),
461
+ the function still creates a valid trace tree.
462
+
463
+ Expected trace structure with the corrected approach:
464
+ trace (1234567890abcdef1234567890abcdef)
465
+ ├── error1-id (10:00:00Z) "First Error" (has non-matching parent_span_id)
466
+ ├── error2-id (10:00:10Z) "Second Error" (has non-matching parent_span_id)
467
+ │ └── error3-id (10:00:20Z) "Child Error"
468
+ └── error4-id (10:00:30Z) "Orphaned Error" (has non-matching parent_span_id)
469
+
470
+ Note: In real-world scenarios, error events often have parent_span_ids even
471
+ when their parent events aren't captured in our trace data.
472
+ """
473
+ trace_id = "1234567890abcdef1234567890abcdef"
474
+ test_span_id = "abcdef0123456789"
475
+ event_data = load_data ("python" )
476
+ event_data .update ({"contexts" : {"trace" : {"trace_id" : trace_id , "span_id" : test_span_id }}})
477
+ event = self .store_event (data = event_data , project_id = self .project .id )
478
+
479
+ # Create error events with parent-child relationships
480
+ error1_span_id = "error1-span-id"
481
+ error1 = Mock ()
482
+ error1 .event_id = "error1-id"
483
+ error1 .datetime = datetime .fromisoformat ("2023-01-01T10:00:00+00:00" )
484
+ error1 .data = {
485
+ "contexts" : {
486
+ "trace" : {
487
+ "trace_id" : trace_id ,
488
+ "span_id" : error1_span_id ,
489
+ "parent_span_id" : "non-existent-parent-1" , # Parent that doesn't exist in our data
490
+ }
491
+ },
492
+ "title" : "First Error" ,
493
+ }
494
+ error1 .title = "First Error"
495
+ error1 .platform = "python"
496
+ error1 .project_id = self .project .id
497
+ error1 .trace_id = trace_id
498
+
499
+ error2_span_id = "error2-span-id"
500
+ error2 = Mock ()
501
+ error2 .event_id = "error2-id"
502
+ error2 .datetime = datetime .fromisoformat ("2023-01-01T10:00:10+00:00" )
503
+ error2 .data = {
504
+ "contexts" : {
505
+ "trace" : {
506
+ "trace_id" : trace_id ,
507
+ "span_id" : error2_span_id ,
508
+ "parent_span_id" : "non-existent-parent-2" , # Parent that doesn't exist in our data
509
+ }
510
+ },
511
+ "title" : "Second Error" ,
512
+ }
513
+ error2 .title = "Second Error"
514
+ error2 .platform = "python"
515
+ error2 .project_id = self .project .id
516
+ error2 .trace_id = trace_id
517
+
518
+ # This error is a child of error2
519
+ error3 = Mock ()
520
+ error3 .event_id = "error3-id"
521
+ error3 .datetime = datetime .fromisoformat ("2023-01-01T10:00:20+00:00" )
522
+ error3 .data = {
523
+ "contexts" : {
524
+ "trace" : {
525
+ "trace_id" : trace_id ,
526
+ "span_id" : "error3-span-id" ,
527
+ "parent_span_id" : error2_span_id , # Points to error2
528
+ }
529
+ },
530
+ "title" : "Child Error" ,
531
+ }
532
+ error3 .title = "Child Error"
533
+ error3 .platform = "python"
534
+ error3 .project_id = self .project .id
535
+ error3 .trace_id = trace_id
536
+
537
+ # Another "orphaned" error with a parent_span_id that doesn't point to anything
538
+ error4 = Mock ()
539
+ error4 .event_id = "error4-id"
540
+ error4 .datetime = datetime .fromisoformat ("2023-01-01T10:00:30+00:00" )
541
+ error4 .data = {
542
+ "contexts" : {
543
+ "trace" : {
544
+ "trace_id" : trace_id ,
545
+ "span_id" : "error4-span-id" ,
546
+ "parent_span_id" : "non-existent-parent-3" , # Parent that doesn't exist in our data
547
+ }
548
+ },
549
+ "title" : "Orphaned Error" ,
550
+ }
551
+ error4 .title = "Orphaned Error"
552
+ error4 .platform = "python"
553
+ error4 .project_id = self .project .id
554
+ error4 .trace_id = trace_id
555
+
556
+ # Return empty transactions list but populate errors list
557
+ def side_effect (filter , dataset = None , ** kwargs ):
558
+ if dataset == Dataset .Transactions :
559
+ return []
560
+ else :
561
+ return [error1 , error2 , error3 , error4 ]
562
+
563
+ mock_get_events .side_effect = side_effect
564
+
565
+ # Call the function directly
566
+ trace_tree = _get_trace_tree_for_event (event , self .project )
567
+
568
+ # Verify the trace tree structure
569
+ assert trace_tree is not None
570
+ assert trace_tree ["trace_id" ] == trace_id
571
+
572
+ # We should have three root-level errors in the result (error1, error2, error4)
573
+ # In the old logic, this would be empty because all errors have parent_span_ids
574
+ assert len (trace_tree ["events" ]) == 3
575
+
576
+ # Verify all the root events are in chronological order
577
+ events = trace_tree ["events" ]
578
+ assert events [0 ]["event_id" ] == "error1-id"
579
+ assert events [1 ]["event_id" ] == "error2-id"
580
+ assert events [2 ]["event_id" ] == "error4-id"
581
+
582
+ # error3 should be a child of error2
583
+ assert len (events [1 ]["children" ]) == 1
584
+ child = events [1 ]["children" ][0 ]
585
+ assert child ["event_id" ] == "error3-id"
586
+ assert child ["title" ] == "Child Error"
587
+
588
+ # Verify get_events was called twice - once for transactions and once for errors
589
+ assert mock_get_events .call_count == 2
590
+
591
+ @patch ("sentry.eventstore.backend.get_events" )
592
+ def test_get_trace_tree_all_relationship_rules (self , mock_get_events ):
593
+ """
594
+ Tests that all three relationship rules are correctly implemented:
595
+ 1. An event whose span_id is X is a parent of an event whose parent_span_id is X
596
+ 2. A transaction event with a span with span_id X is a parent of an event whose parent_span_id is X
597
+ 3. A transaction event with a span with span_id X is a parent of an event whose span_id is X
598
+
599
+ Expected trace structure:
600
+ trace (1234567890abcdef1234567890abcdef)
601
+ └── root-tx-id (10:00:00Z) "Root Transaction"
602
+ ├── rule1-child-id (10:00:10Z) "Rule 1 Child" (parent_span_id=root-tx-span-id)
603
+ ├── rule2-child-id (10:00:20Z) "Rule 2 Child" (parent_span_id=tx-span-1)
604
+ └── rule3-child-id (10:00:30Z) "Rule 3 Child" (span_id=tx-span-2)
605
+ """
606
+ trace_id = "1234567890abcdef1234567890abcdef"
607
+ test_span_id = "abcdef0123456789"
608
+ event_data = load_data ("python" )
609
+ event_data .update ({"contexts" : {"trace" : {"trace_id" : trace_id , "span_id" : test_span_id }}})
610
+ event = self .store_event (data = event_data , project_id = self .project .id )
611
+
612
+ # Root transaction with two spans
613
+ root_tx_span_id = "root-tx-span-id"
614
+ tx_span_1 = "tx-span-1"
615
+ tx_span_2 = "tx-span-2"
616
+
617
+ root_tx = Mock ()
618
+ root_tx .event_id = "root-tx-id"
619
+ root_tx .datetime = datetime .fromisoformat ("2023-01-01T10:00:00+00:00" )
620
+ root_tx .data = {
621
+ "spans" : [{"span_id" : tx_span_1 }, {"span_id" : tx_span_2 }],
622
+ "contexts" : {
623
+ "trace" : {"trace_id" : trace_id , "span_id" : root_tx_span_id , "op" : "http.server" }
624
+ },
625
+ "title" : "Root Transaction" ,
626
+ }
627
+ root_tx .title = "Root Transaction"
628
+ root_tx .platform = "python"
629
+ root_tx .project_id = self .project .id
630
+ root_tx .trace_id = trace_id
631
+
632
+ # Rule 1: Child whose parent_span_id matches another event's span_id
633
+ rule1_child = Mock ()
634
+ rule1_child .event_id = "rule1-child-id"
635
+ rule1_child .datetime = datetime .fromisoformat ("2023-01-01T10:00:10+00:00" )
636
+ rule1_child .data = {
637
+ "contexts" : {
638
+ "trace" : {
639
+ "trace_id" : trace_id ,
640
+ "span_id" : "rule1-child-span-id" ,
641
+ "parent_span_id" : root_tx_span_id , # Points to root transaction's span_id
642
+ }
643
+ },
644
+ "title" : "Rule 1 Child" ,
645
+ }
646
+ rule1_child .title = "Rule 1 Child"
647
+ rule1_child .platform = "python"
648
+ rule1_child .project_id = self .project .id
649
+ rule1_child .trace_id = trace_id
650
+
651
+ # Rule 2: Child whose parent_span_id matches a span in a transaction
652
+ rule2_child = Mock ()
653
+ rule2_child .event_id = "rule2-child-id"
654
+ rule2_child .datetime = datetime .fromisoformat ("2023-01-01T10:00:20+00:00" )
655
+ rule2_child .data = {
656
+ "contexts" : {
657
+ "trace" : {
658
+ "trace_id" : trace_id ,
659
+ "span_id" : "rule2-child-span-id" ,
660
+ "parent_span_id" : tx_span_1 , # Points to a span in the root transaction
661
+ }
662
+ },
663
+ "title" : "Rule 2 Child" ,
664
+ }
665
+ rule2_child .title = "Rule 2 Child"
666
+ rule2_child .platform = "python"
667
+ rule2_child .project_id = self .project .id
668
+ rule2_child .trace_id = trace_id
669
+
670
+ # Rule 3: Child whose span_id matches a span in a transaction
671
+ rule3_child = Mock ()
672
+ rule3_child .event_id = "rule3-child-id"
673
+ rule3_child .datetime = datetime .fromisoformat ("2023-01-01T10:00:30+00:00" )
674
+ rule3_child .data = {
675
+ "contexts" : {
676
+ "trace" : {
677
+ "trace_id" : trace_id ,
678
+ "span_id" : tx_span_2 , # Same as one of the spans in the root transaction
679
+ }
680
+ },
681
+ "title" : "Rule 3 Child" ,
682
+ }
683
+ rule3_child .title = "Rule 3 Child"
684
+ rule3_child .platform = "python"
685
+ rule3_child .project_id = self .project .id
686
+ rule3_child .trace_id = trace_id
687
+
688
+ # Set up the mock to return our test events
689
+ def side_effect (filter , dataset = None , ** kwargs ):
690
+ if dataset == Dataset .Transactions :
691
+ return [root_tx ]
692
+ else :
693
+ return [rule1_child , rule2_child , rule3_child ]
694
+
695
+ mock_get_events .side_effect = side_effect
696
+
697
+ # Call the function
698
+ trace_tree = _get_trace_tree_for_event (event , self .project )
699
+
700
+ # Verify the trace tree structure
701
+ assert trace_tree is not None
702
+ assert trace_tree ["trace_id" ] == trace_id
703
+ assert len (trace_tree ["events" ]) == 1 # One root node (the transaction)
704
+
705
+ # Verify root transaction
706
+ root = trace_tree ["events" ][0 ]
707
+ assert root ["event_id" ] == "root-tx-id"
708
+ assert root ["title" ] == "http.server - Root Transaction"
709
+ assert root ["is_transaction" ] is True
710
+ assert root ["is_error" ] is False
711
+
712
+ # Root should have all three children according to the rules
713
+ assert len (root ["children" ]) == 3
714
+
715
+ # Children should be in chronological order
716
+ children = root ["children" ]
717
+
718
+ # First child - Rule 1
719
+ assert children [0 ]["event_id" ] == "rule1-child-id"
720
+ assert children [0 ]["title" ] == "Rule 1 Child"
721
+
722
+ # Second child - Rule 2
723
+ assert children [1 ]["event_id" ] == "rule2-child-id"
724
+ assert children [1 ]["title" ] == "Rule 2 Child"
725
+
726
+ # Third child - Rule 3
727
+ assert children [2 ]["event_id" ] == "rule3-child-id"
728
+ assert children [2 ]["title" ] == "Rule 3 Child"
729
+
730
+ # Verify get_events was called twice
731
+ assert mock_get_events .call_count == 2
732
+
463
733
464
734
@requires_snuba
465
735
@pytest .mark .django_db
0 commit comments