|
8 | 8 | from werkzeug.test import Client |
9 | 9 | from werkzeug.wrappers import Response |
10 | 10 |
|
| 11 | +import sentry_sdk |
11 | 12 | from sentry_sdk import capture_message |
12 | 13 | from sentry_sdk.integrations.bottle import BottleIntegration |
13 | 14 | from sentry_sdk.integrations.logging import LoggingIntegration |
@@ -462,23 +463,37 @@ def here(): |
462 | 463 | assert not events |
463 | 464 |
|
464 | 465 |
|
| 466 | +@pytest.mark.parametrize("span_streaming", [True, False]) |
465 | 467 | def test_span_origin( |
466 | 468 | sentry_init, |
467 | 469 | get_client, |
468 | 470 | capture_events, |
| 471 | + capture_items, |
| 472 | + span_streaming, |
469 | 473 | ): |
470 | 474 | sentry_init( |
471 | 475 | integrations=[BottleIntegration()], |
472 | 476 | traces_sample_rate=1.0, |
| 477 | + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, |
473 | 478 | ) |
474 | | - events = capture_events() |
| 479 | + |
| 480 | + if span_streaming: |
| 481 | + items = capture_items("span") |
| 482 | + else: |
| 483 | + events = capture_events() |
475 | 484 |
|
476 | 485 | client = get_client() |
477 | 486 | client.get("/message") |
478 | 487 |
|
479 | | - (_, event) = events |
480 | | - |
481 | | - assert event["contexts"]["trace"]["origin"] == "auto.http.bottle" |
| 488 | + if span_streaming: |
| 489 | + sentry_sdk.flush() |
| 490 | + spans = [item.payload for item in items] |
| 491 | + segment = spans[-1] |
| 492 | + assert segment["is_segment"] is True |
| 493 | + assert segment["attributes"]["sentry.origin"] == "auto.http.bottle" |
| 494 | + else: |
| 495 | + (_, event) = events |
| 496 | + assert event["contexts"]["trace"]["origin"] == "auto.http.bottle" |
482 | 497 |
|
483 | 498 |
|
484 | 499 | @pytest.mark.parametrize("raise_error", [True, False]) |
@@ -556,3 +571,239 @@ def handle(): |
556 | 571 |
|
557 | 572 | (event,) = events |
558 | 573 | assert event["exception"]["values"][0]["type"] == "ZeroDivisionError" |
| 574 | + |
| 575 | + |
| 576 | +def test_span_streaming_basic(sentry_init, capture_items): |
| 577 | + sentry_init( |
| 578 | + integrations=[BottleIntegration()], |
| 579 | + traces_sample_rate=1.0, |
| 580 | + _experiments={"trace_lifecycle": "stream"}, |
| 581 | + ) |
| 582 | + items = capture_items("span") |
| 583 | + |
| 584 | + app = Bottle() |
| 585 | + |
| 586 | + @app.route("/message") |
| 587 | + def hi(): |
| 588 | + return "ok" |
| 589 | + |
| 590 | + client = Client(app) |
| 591 | + client.get("/message") |
| 592 | + |
| 593 | + sentry_sdk.flush() |
| 594 | + |
| 595 | + spans = [item.payload for item in items] |
| 596 | + assert len(spans) == 1 |
| 597 | + |
| 598 | + segment = spans[0] |
| 599 | + |
| 600 | + # Segment span (root, created by WSGI middleware) |
| 601 | + assert segment["is_segment"] is True |
| 602 | + assert "parent_span_id" not in segment |
| 603 | + assert segment["status"] == "ok" |
| 604 | + assert segment["attributes"]["sentry.op"] == "http.server" |
| 605 | + assert segment["attributes"]["sentry.origin"] == "auto.http.bottle" |
| 606 | + assert segment["attributes"]["http.request.method"] == "GET" |
| 607 | + assert segment["attributes"]["http.response.status_code"] == 200 |
| 608 | + assert segment["name"].endswith("hi") |
| 609 | + |
| 610 | + |
| 611 | +@pytest.mark.parametrize( |
| 612 | + "url,transaction_style,expected_name,expected_source", |
| 613 | + [ |
| 614 | + ("/message", "endpoint", "hi", "component"), |
| 615 | + ("/message", "url", "/message", "route"), |
| 616 | + ("/message/123456", "url", "/message/<message_id>", "route"), |
| 617 | + ("/message-named-route", "endpoint", "hi", "component"), |
| 618 | + ], |
| 619 | +) |
| 620 | +def test_span_streaming_transaction_style( |
| 621 | + sentry_init, |
| 622 | + capture_items, |
| 623 | + url, |
| 624 | + transaction_style, |
| 625 | + expected_name, |
| 626 | + expected_source, |
| 627 | +): |
| 628 | + sentry_init( |
| 629 | + integrations=[BottleIntegration(transaction_style=transaction_style)], |
| 630 | + traces_sample_rate=1.0, |
| 631 | + _experiments={"trace_lifecycle": "stream"}, |
| 632 | + ) |
| 633 | + items = capture_items("span") |
| 634 | + |
| 635 | + app = Bottle() |
| 636 | + |
| 637 | + @app.route("/message") |
| 638 | + def hi(): |
| 639 | + return "ok" |
| 640 | + |
| 641 | + @app.route("/message/<message_id>") |
| 642 | + def hi_with_id(message_id): |
| 643 | + return "ok" |
| 644 | + |
| 645 | + @app.route("/message-named-route", name="hi") |
| 646 | + def named_hi(): |
| 647 | + return "ok" |
| 648 | + |
| 649 | + client = Client(app) |
| 650 | + client.get(url) |
| 651 | + |
| 652 | + sentry_sdk.flush() |
| 653 | + |
| 654 | + spans = [item.payload for item in items] |
| 655 | + assert len(spans) == 1 |
| 656 | + |
| 657 | + segment = spans[0] |
| 658 | + |
| 659 | + assert segment["is_segment"] is True |
| 660 | + |
| 661 | + assert segment["name"].endswith(expected_name) |
| 662 | + assert segment["attributes"]["sentry.span.source"] == expected_source |
| 663 | + |
| 664 | + |
| 665 | +def test_span_streaming_with_error(sentry_init, capture_items): |
| 666 | + sentry_init( |
| 667 | + integrations=[BottleIntegration()], |
| 668 | + traces_sample_rate=1.0, |
| 669 | + _experiments={"trace_lifecycle": "stream"}, |
| 670 | + ) |
| 671 | + items = capture_items("event", "span") |
| 672 | + |
| 673 | + app = Bottle() |
| 674 | + |
| 675 | + @app.route("/error") |
| 676 | + def error(): |
| 677 | + 1 / 0 |
| 678 | + |
| 679 | + client = Client(app) |
| 680 | + try: |
| 681 | + client.get("/error") |
| 682 | + except ZeroDivisionError: |
| 683 | + pass |
| 684 | + |
| 685 | + sentry_sdk.flush() |
| 686 | + |
| 687 | + events = [item.payload for item in items if item.type == "event"] |
| 688 | + spans = [item.payload for item in items if item.type == "span"] |
| 689 | + assert len(events) == 1 |
| 690 | + assert len(spans) == 1 |
| 691 | + |
| 692 | + error_event = events[0] |
| 693 | + segment = spans[0] |
| 694 | + |
| 695 | + # Confirm the same trace is shared |
| 696 | + assert segment["trace_id"] == error_event["contexts"]["trace"]["trace_id"] |
| 697 | + |
| 698 | + # Span hierarchy |
| 699 | + assert segment["is_segment"] is True |
| 700 | + assert "parent_span_id" not in segment |
| 701 | + |
| 702 | + # Error event span_id points to the segment span (where the exception was raised) |
| 703 | + assert error_event["contexts"]["trace"]["span_id"] == segment["span_id"] |
| 704 | + |
| 705 | + # Span status |
| 706 | + assert segment["status"] == "error" |
| 707 | + |
| 708 | + # Bottle mechanism on the error event |
| 709 | + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "bottle" |
| 710 | + assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False |
| 711 | + |
| 712 | + |
| 713 | +@pytest.mark.parametrize( |
| 714 | + "status_code,expected_span_status", |
| 715 | + [ |
| 716 | + (200, "ok"), |
| 717 | + (404, "error"), |
| 718 | + (500, "error"), |
| 719 | + ], |
| 720 | +) |
| 721 | +def test_span_streaming_http_error_status( |
| 722 | + sentry_init, |
| 723 | + capture_items, |
| 724 | + status_code, |
| 725 | + expected_span_status, |
| 726 | +): |
| 727 | + sentry_init( |
| 728 | + integrations=[BottleIntegration()], |
| 729 | + traces_sample_rate=1.0, |
| 730 | + _experiments={"trace_lifecycle": "stream"}, |
| 731 | + ) |
| 732 | + items = capture_items("span") |
| 733 | + |
| 734 | + app = Bottle() |
| 735 | + |
| 736 | + @app.route("/") |
| 737 | + def handle(): |
| 738 | + return HTTPResponse(status=status_code, body="response") |
| 739 | + |
| 740 | + client = Client(app) |
| 741 | + client.get("/") |
| 742 | + |
| 743 | + sentry_sdk.flush() |
| 744 | + |
| 745 | + spans = [item.payload for item in items] |
| 746 | + assert len(spans) == 1 |
| 747 | + |
| 748 | + segment = spans[0] |
| 749 | + |
| 750 | + assert segment["is_segment"] is True |
| 751 | + |
| 752 | + assert segment["status"] == expected_span_status |
| 753 | + assert segment["attributes"]["http.response.status_code"] == status_code |
| 754 | + |
| 755 | + |
| 756 | +@pytest.mark.parametrize("raise_error", [True, False]) |
| 757 | +@pytest.mark.parametrize( |
| 758 | + ("integration_kwargs", "status_code", "should_capture"), |
| 759 | + ( |
| 760 | + ({}, 500, True), |
| 761 | + ({}, 400, False), |
| 762 | + ({"failed_request_status_codes": set()}, 500, False), |
| 763 | + ({"failed_request_status_codes": {404, *range(500, 600)}}, 404, True), |
| 764 | + ({"failed_request_status_codes": {404, *range(500, 600)}}, 400, False), |
| 765 | + ), |
| 766 | +) |
| 767 | +def test_span_streaming_failed_request_status_codes( |
| 768 | + sentry_init, |
| 769 | + capture_items, |
| 770 | + integration_kwargs, |
| 771 | + status_code, |
| 772 | + should_capture, |
| 773 | + raise_error, |
| 774 | +): |
| 775 | + sentry_init( |
| 776 | + integrations=[BottleIntegration(**integration_kwargs)], |
| 777 | + traces_sample_rate=1.0, |
| 778 | + _experiments={"trace_lifecycle": "stream"}, |
| 779 | + ) |
| 780 | + items = capture_items("event", "span") |
| 781 | + |
| 782 | + app = Bottle() |
| 783 | + |
| 784 | + @app.route("/") |
| 785 | + def handle(): |
| 786 | + response = HTTPResponse(status=status_code) |
| 787 | + if raise_error: |
| 788 | + raise response |
| 789 | + return response |
| 790 | + |
| 791 | + client = Client(app, Response) |
| 792 | + client.get("/") |
| 793 | + |
| 794 | + sentry_sdk.flush() |
| 795 | + |
| 796 | + events = [item.payload for item in items if item.type == "event"] |
| 797 | + spans = [item.payload for item in items if item.type == "span"] |
| 798 | + assert len(spans) == 1 |
| 799 | + |
| 800 | + segment = spans[0] |
| 801 | + |
| 802 | + assert segment["is_segment"] is True |
| 803 | + |
| 804 | + if should_capture: |
| 805 | + assert len(events) == 1 |
| 806 | + assert events[0]["exception"]["values"][0]["type"] == "HTTPResponse" |
| 807 | + assert events[0]["exception"]["values"][0]["mechanism"]["handled"] is True |
| 808 | + else: |
| 809 | + assert len(events) == 0 |
0 commit comments