Repository: getsentry/sentry-python

Issue:
Fix Breadcrumb ordering
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

all

### Steps to Reproduce

Breadcrumbs live on the Scope. 

Before the event is sent to Sentry the list of breadcrumbs of multiple Scopes are concatenated list of breadcrumbs that is sent to Sentry.

We do not order the final breadcrumb list before sending, so all breadcrumbs of Scope A are followed by all breadcrumbs of Scope B. 


### Expected Result

The breadcrumbs should be ordered by timestamp before being sent to Sentry.


Discussion:
codecov[bot]: **Test Failures Detected**: Due to failing tests, we cannot provide coverage reports at this time.

### :x: Failed Test Results: 
Completed 10750 tests with **`1 failed`**, 9323 passed and 1426 skipped.
<details><summary>View the full list of failed tests</summary>

## py3.13-common
- **Class name:** tests.test_utils<br>**Test name:** test_installed_modules<br><br>
  <pre>tests/test_utils.py:538: in test_installed_modules<br>    assert installed_distributions == importlib_distributions<br>E   AssertionError: assert {&apos;sentry-sdk&apos;: &apos;2.10.0&apos;, &apos;rpds-py&apos;: &apos;0.19.0&apos;, &apos;pysocks&apos;: &apos;1.7.1&apos;, &apos;watchdog&apos;: &apos;4.0.1&apos;, &apos;iniconfig&apos;: &apos;2.0.0&apos;, &apos;pluggy&apos;: &apos;1.5.0&apos;, &apos;executing&apos;: &apos;2.0.1&apos;, &apos;idna&apos;: &apos;3.7&apos;, &apos;referencing&apos;: &apos;0.35.1&apos;, &apos;responses&apos;: &apos;0.25.3&apos;, &apos;jsonschema&apos;: &apos;4.23.0&apos;, &apos;py&apos;: &apos;1.11.0&apos;, &apos;colorama&apos;: &apos;0.4.6&apos;, &apos;certifi&apos;: &apos;2024.7.4&apos;, &apos;packaging&apos;: &apos;24.1&apos;, &apos;pytest-asyncio&apos;: &apos;0.23.8&apos;, &apos;werkzeug&apos;: &apos;3.0.3&apos;, &apos;pytest-localserver&apos;: &apos;0.8.1&apos;, &apos;pyrsistent&apos;: &apos;0.20.0&apos;, &apos;pytest-forked&apos;: &apos;1.6.0&apos;, &apos;setuptools&apos;: &apos;71.0.3&apos;, &apos;six&apos;: &apos;1.16.0&apos;, &apos;markupsafe&apos;: &apos;2.1.5&apos;, &apos;pytest&apos;: &apos;8.2.2&apos;, &apos;pytest-cov&apos;: &apos;5.0.0&apos;, &apos;charset-normalizer&apos;: &apos;3.3.2&apos;, &apos;asttokens&apos;: &apos;2.4.1&apos;, &apos;docopt&apos;: &apos;0.6.2&apos;, &apos;coverage&apos;: &apos;7.6.0&apos;, &apos;requests&apos;: &apos;2.32.3&apos;, &apos;pip&apos;: &apos;24.0&apos;, &apos;jsonschema-specifications&apos;: &apos;2023.12.1&apos;, &apos;pyyaml&apos;: &apos;6.0.1&apos;, &apos;urllib3&apos;: &apos;2.2.2&apos;, &apos;pytest-watch&apos;: &apos;4.2.0&apos;, &apos;attrs&apos;: &apos;23.2.0&apos;} == {&apos;sentry-sdk&apos;: &apos;2.10.0&apos;, &apos;rpds-py&apos;: &apos;0.19.0&apos;, &apos;pysocks&apos;: &apos;1.7.1&apos;, &apos;watchdog&apos;: &apos;4.0.1&apos;, &apos;iniconfig&apos;: &apos;2.0.0&apos;, &apos;pluggy&apos;: &apos;1.5.0&apos;, &apos;executing&apos;: &apos;2.0.1&apos;, &apos;idna&apos;: &apos;3.7&apos;, &apos;referencing&apos;: &apos;0.35.1&apos;, &apos;responses&apos;: &apos;0.25.3&apos;, &apos;jsonschema&apos;: &apos;4.23.0&apos;, &apos;py&apos;: &apos;1.11.0&apos;, &apos;colorama&apos;: &apos;0.4.6&apos;, &apos;certifi&apos;: &apos;2024.7.4&apos;, &apos;packaging&apos;: &apos;24.1&apos;, &apos;pytest-asyncio&apos;: &apos;0.23.8&apos;, &apos;werkzeug&apos;: &apos;3.0.3&apos;, &apos;pytest-localserver&apos;: &apos;0.8.1&apos;, &apos;pyrsistent&apos;: &apos;0.20.0&apos;, &apos;pytest-forked&apos;: &apos;1.6.0&apos;, &apos;setuptools&apos;: &apos;71.0.3&apos;, &apos;six&apos;: &apos;1.16.0&apos;, &apos;markupsafe&apos;: &apos;2.1.5&apos;, &apos;pytest&apos;: &apos;8.2.2&apos;, &apos;pytest-cov&apos;: &apos;5.0.0&apos;, &apos;charset-normalizer&apos;: &apos;3.3.2&apos;, &apos;asttokens&apos;: &apos;2.4.1&apos;, &apos;docopt&apos;: &apos;0.6.2&apos;, &apos;coverage&apos;: &apos;7.6.0&apos;, &apos;requests&apos;: &apos;2.32.3&apos;, &apos;pip&apos;: &apos;24.0&apos;, &apos;jsonschema-specifications&apos;: &apos;2023.12.1&apos;, &apos;pyyaml&apos;: &apos;6.0.1&apos;, &apos;urllib3&apos;: &apos;2.2.2&apos;, &apos;pytest-watch&apos;: &apos;4.2.0&apos;, &apos;attrs&apos;: &apos;23.2.0&apos;, &apos;jaraco-context&apos;: &apos;5.3.0&apos;, &apos;jaraco-functools&apos;: &apos;4.0.1&apos;, &apos;typing-extensions&apos;: &apos;4.12.2&apos;, &apos;backports-tarfile&apos;: &apos;1.2.0&apos;, &apos;tomli&apos;: &apos;2.0.1&apos;, &apos;inflect&apos;: &apos;7.3.1&apos;, &apos;jaraco-text&apos;: &apos;3.12.1&apos;, &apos;importlib-resources&apos;: &apos;6.4.0&apos;, &apos;importlib-metadata&apos;: &apos;8.0.0&apos;, &apos;more-itertools&apos;: &apos;10.3.0&apos;, &apos;typeguard&apos;: &apos;4.3.0&apos;, &apos;autocommand&apos;: &apos;2.2.2&apos;, &apos;platformdirs&apos;: &apos;4.2.2&apos;, &apos;ordered-set&apos;: &apos;4.1.0&apos;, &apos;zipp&apos;: &apos;3.19.2&apos;, &apos;wheel&apos;: &apos;0.43.0&apos;}<br>E     <br>E     Common items:<br>E     {&apos;asttokens&apos;: &apos;2.4.1&apos;,<br>E      &apos;attrs&apos;: &apos;23.2.0&apos;,<br>E      &apos;certifi&apos;: &apos;2024.7.4&apos;,<br>E      &apos;charset-normalizer&apos;: &apos;3.3.2&apos;,<br>E      &apos;colorama&apos;: &apos;0.4.6&apos;,<br>E      &apos;coverage&apos;: &apos;7.6.0&apos;,<br>E      &apos;docopt&apos;: &apos;0.6.2&apos;,<br>E      &apos;executing&apos;: &apos;2.0.1&apos;,<br>E      &apos;idna&apos;: &apos;3.7&apos;,<br>E      &apos;iniconfig&apos;: &apos;2.0.0&apos;,<br>E      &apos;jsonschema&apos;: &apos;4.23.0&apos;,<br>E      &apos;jsonschema-specifications&apos;: &apos;2023.12.1&apos;,<br>E      &apos;markupsafe&apos;: &apos;2.1.5&apos;,<br>E      &apos;packaging&apos;: &apos;24.1&apos;,<br>E      &apos;pip&apos;: &apos;24.0&apos;,<br>E      &apos;pluggy&apos;: &apos;1.5.0&apos;,<br>E      &apos;py&apos;: &apos;1.11.0&apos;,<br>E      &apos;pyrsistent&apos;: &apos;0.20.0&apos;,<br>E      &apos;pysocks&apos;: &apos;1.7.1&apos;,<br>E      &apos;pytest&apos;: &apos;8.2.2&apos;,<br>E      &apos;pytest-asyncio&apos;: &apos;0.23.8&apos;,<br>E      &apos;pytest-cov&apos;: &apos;5.0.0&apos;,<br>E      &apos;pytest-forked&apos;: &apos;1.6.0&apos;,<br>E      &apos;pytest-localserver&apos;: &apos;0.8.1&apos;,<br>E      &apos;pytest-watch&apos;: &apos;4.2.0&apos;,<br>E      &apos;pyyaml&apos;: &apos;6.0.1&apos;,<br>E      &apos;referencing&apos;: &apos;0.35.1&apos;,<br>E      &apos;requests&apos;: &apos;2.32.3&apos;,<br>E      &apos;responses&apos;: &apos;0.25.3&apos;,<br>E      &apos;rpds-py&apos;: &apos;0.19.0&apos;,<br>E      &apos;sentry-sdk&apos;: &apos;2.10.0&apos;,<br>E      &apos;setuptools&apos;: &apos;71.0.3&apos;,<br>E      &apos;six&apos;: &apos;1.16.0&apos;,<br>E      &apos;urllib3&apos;: &apos;2.2.2&apos;,<br>E      &apos;watchdog&apos;: &apos;4.0.1&apos;,<br>E      &apos;werkzeug&apos;: &apos;3.0.3&apos;}<br>E     Right contains 16 more items:<br>E     {&apos;autocommand&apos;: &apos;2.2.2&apos;,<br>E      &apos;backports-tarfile&apos;: &apos;1.2.0&apos;,<br>E      &apos;importlib-metadata&apos;: &apos;8.0.0&apos;,<br>E      &apos;importlib-resources&apos;: &apos;6.4.0&apos;,<br>E      &apos;inflect&apos;: &apos;7.3.1&apos;,<br>E      &apos;jaraco-context&apos;: &apos;5.3.0&apos;,<br>E      &apos;jaraco-functools&apos;: &apos;4.0.1&apos;,<br>E      &apos;jaraco-text&apos;: &apos;3.12.1&apos;,<br>E      &apos;more-itertools&apos;: &apos;10.3.0&apos;,<br>E      &apos;ordered-set&apos;: &apos;4.1.0&apos;,<br>E      &apos;platformdirs&apos;: &apos;4.2.2&apos;,<br>E      &apos;tomli&apos;: &apos;2.0.1&apos;,<br>E      &apos;typeguard&apos;: &apos;4.3.0&apos;,<br>E      &apos;typing-extensions&apos;: &apos;4.12.2&apos;,<br>E      &apos;wheel&apos;: &apos;0.43.0&apos;,<br>E      &apos;zipp&apos;: &apos;3.19.2&apos;}<br>E     <br>E     Full diff:<br>E       {<br>E           &apos;asttokens&apos;: &apos;2.4.1&apos;,<br>E           &apos;attrs&apos;: &apos;23.2.0&apos;,<br>E     -     &apos;autocommand&apos;: &apos;2.2.2&apos;,<br>E     -     &apos;backports-tarfile&apos;: &apos;1.2.0&apos;,<br>E           &apos;certifi&apos;: &apos;2024.7.4&apos;,<br>E           &apos;charset-normalizer&apos;: &apos;3.3.2&apos;,<br>E           &apos;colorama&apos;: &apos;0.4.6&apos;,<br>E           &apos;coverage&apos;: &apos;7.6.0&apos;,<br>E           &apos;docopt&apos;: &apos;0.6.2&apos;,<br>E           &apos;executing&apos;: &apos;2.0.1&apos;,<br>E           &apos;idna&apos;: &apos;3.7&apos;,<br>E     -     &apos;importlib-metadata&apos;: &apos;8.0.0&apos;,<br>E     -     &apos;importlib-resources&apos;: &apos;6.4.0&apos;,<br>E     -     &apos;inflect&apos;: &apos;7.3.1&apos;,<br>E           &apos;iniconfig&apos;: &apos;2.0.0&apos;,<br>E     -     &apos;jaraco-context&apos;: &apos;5.3.0&apos;,<br>E     -     &apos;jaraco-functools&apos;: &apos;4.0.1&apos;,<br>E     -     &apos;jaraco-text&apos;: &apos;3.12.1&apos;,<br>E           &apos;jsonschema&apos;: &apos;4.23.0&apos;,<br>E           &apos;jsonschema-specifications&apos;: &apos;2023.12.1&apos;,<br>E           &apos;markupsafe&apos;: &apos;2.1.5&apos;,<br>E     -     &apos;more-itertools&apos;: &apos;10.3.0&apos;,<br>E     -     &apos;ordered-set&apos;: &apos;4.1.0&apos;,<br>E           &apos;packaging&apos;: &apos;24.1&apos;,<br>E           &apos;pip&apos;: &apos;24.0&apos;,<br>E     -     &apos;platformdirs&apos;: &apos;4.2.2&apos;,<br>E           &apos;pluggy&apos;: &apos;1.5.0&apos;,<br>E           &apos;py&apos;: &apos;1.11.0&apos;,<br>E           &apos;pyrsistent&apos;: &apos;0.20.0&apos;,<br>E           &apos;pysocks&apos;: &apos;1.7.1&apos;,<br>E           &apos;pytest&apos;: &apos;8.2.2&apos;,<br>E           &apos;pytest-asyncio&apos;: &apos;0.23.8&apos;,<br>E           &apos;pytest-cov&apos;: &apos;5.0.0&apos;,<br>E           &apos;pytest-forked&apos;: &apos;1.6.0&apos;,<br>E           &apos;pytest-localserver&apos;: &apos;0.8.1&apos;,<br>E           &apos;pytest-watch&apos;: &apos;4.2.0&apos;,<br>E           &apos;pyyaml&apos;: &apos;6.0.1&apos;,<br>E           &apos;referencing&apos;: &apos;0.35.1&apos;,<br>E           &apos;requests&apos;: &apos;2.32.3&apos;,<br>E           &apos;responses&apos;: &apos;0.25.3&apos;,<br>E           &apos;rpds-py&apos;: &apos;0.19.0&apos;,<br>E           &apos;sentry-sdk&apos;: &apos;2.10.0&apos;,<br>E           &apos;setuptools&apos;: &apos;71.0.3&apos;,<br>E           &apos;six&apos;: &apos;1.16.0&apos;,<br>E     -     &apos;tomli&apos;: &apos;2.0.1&apos;,<br>E     -     &apos;typeguard&apos;: &apos;4.3.0&apos;,<br>E     -     &apos;typing-extensions&apos;: &apos;4.12.2&apos;,<br>E           &apos;urllib3&apos;: &apos;2.2.2&apos;,<br>E           &apos;watchdog&apos;: &apos;4.0.1&apos;,<br>E           &apos;werkzeug&apos;: &apos;3.0.3&apos;,<br>E     -     &apos;wheel&apos;: &apos;0.43.0&apos;,<br>E     -     &apos;zipp&apos;: &apos;3.19.2&apos;,<br>E       }</pre>
</details>
antonpirker: I'll check

antonpirker: Now with test @sentrivana 

Patch:
diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py
index e6ad8625..8473f1bc 100644
--- a/sentry_sdk/scope.py
+++ b/sentry_sdk/scope.py
@@ -1298,6 +1298,7 @@ class Scope(object):
         event.setdefault("breadcrumbs", {}).setdefault("values", []).extend(
             self._breadcrumbs
         )
+        event["breadcrumbs"]["values"].sort(key=lambda crumb: crumb["timestamp"])
 
     def _apply_user_to_event(self, event, hint, options):
         # type: (Event, Hint, Optional[Dict[str, Any]]) -> None


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 439215e0..52eb5045 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -1,3 +1,4 @@
+import datetime
 import logging
 import os
 import sys
@@ -391,6 +392,37 @@ def test_breadcrumbs(sentry_init, capture_events):
     assert len(event["breadcrumbs"]["values"]) == 0
 
 
+def test_breadcrumb_ordering(sentry_init, capture_events):
+    sentry_init()
+    events = capture_events()
+
+    timestamps = [
+        datetime.datetime.now() - datetime.timedelta(days=10),
+        datetime.datetime.now() - datetime.timedelta(days=8),
+        datetime.datetime.now() - datetime.timedelta(days=12),
+    ]
+
+    for timestamp in timestamps:
+        add_breadcrumb(
+            message="Authenticated at %s" % timestamp,
+            category="auth",
+            level="info",
+            timestamp=timestamp,
+        )
+
+    capture_exception(ValueError())
+    (event,) = events
+
+    assert len(event["breadcrumbs"]["values"]) == len(timestamps)
+    timestamps_from_event = [
+        datetime.datetime.strptime(
+            x["timestamp"].replace("Z", ""), "%Y-%m-%dT%H:%M:%S.%f"
+        )
+        for x in event["breadcrumbs"]["values"]
+    ]
+    assert timestamps_from_event == sorted(timestamps)
+
+
 def test_attachments(sentry_init, capture_envelopes):
     sentry_init()
     envelopes = capture_envelopes()