Skip to content

Commit 5147ab9

Browse files
feat(breadcrumbs): add _meta information for truncation of breadcrumbs (#4007)
- Implements annotations for breadcrumbs - Adds an `int` field to `Scope` to track the number of truncated breadcrumbs - When scopes are merged, the number of breadcrumbs that were removed are added - If breadcrumbs were truncated, add the original number of breadcrumbs to `_meta` - Closes getsentry/projects#593 --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent f1a8db0 commit 5147ab9

File tree

5 files changed

+69
-17
lines changed

5 files changed

+69
-17
lines changed

sentry_sdk/_types.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ def __eq__(self, other):
3030

3131
return self.value == other.value and self.metadata == other.metadata
3232

33+
def __str__(self):
34+
# type: (AnnotatedValue) -> str
35+
return str({"value": str(self.value), "metadata": str(self.metadata)})
36+
37+
def __len__(self):
38+
# type: (AnnotatedValue) -> int
39+
if self.value is not None:
40+
return len(self.value)
41+
else:
42+
return 0
43+
3344
@classmethod
3445
def removed_because_raw_data(cls):
3546
# type: () -> AnnotatedValue
@@ -152,8 +163,8 @@ class SDKInfo(TypedDict):
152163
Event = TypedDict(
153164
"Event",
154165
{
155-
"breadcrumbs": dict[
156-
Literal["values"], list[dict[str, Any]]
166+
"breadcrumbs": Annotated[
167+
dict[Literal["values"], list[dict[str, Any]]]
157168
], # TODO: We can expand on this type
158169
"check_in_id": str,
159170
"contexts": dict[str, dict[str, object]],

sentry_sdk/client.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ def _prepare_event(
498498
# type: (...) -> Optional[Event]
499499

500500
previous_total_spans = None # type: Optional[int]
501+
previous_total_breadcrumbs = None # type: Optional[int]
501502

502503
if event.get("timestamp") is None:
503504
event["timestamp"] = datetime.now(timezone.utc)
@@ -534,6 +535,16 @@ def _prepare_event(
534535
dropped_spans = event.pop("_dropped_spans", 0) + spans_delta # type: int
535536
if dropped_spans > 0:
536537
previous_total_spans = spans_before + dropped_spans
538+
if scope._n_breadcrumbs_truncated > 0:
539+
breadcrumbs = event.get("breadcrumbs", {})
540+
values = (
541+
breadcrumbs.get("values", [])
542+
if not isinstance(breadcrumbs, AnnotatedValue)
543+
else []
544+
)
545+
previous_total_breadcrumbs = (
546+
len(values) + scope._n_breadcrumbs_truncated
547+
)
537548

538549
if (
539550
self.options["attach_stacktrace"]
@@ -586,7 +597,10 @@ def _prepare_event(
586597
event["spans"] = AnnotatedValue(
587598
event.get("spans", []), {"len": previous_total_spans}
588599
)
589-
600+
if previous_total_breadcrumbs is not None:
601+
event["breadcrumbs"] = AnnotatedValue(
602+
event.get("breadcrumbs", []), {"len": previous_total_breadcrumbs}
603+
)
590604
# Postprocess the event here so that annotated types do
591605
# generally not surface in before_send
592606
if event is not None:

sentry_sdk/scope.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from functools import wraps
1010
from itertools import chain
1111

12+
from sentry_sdk._types import AnnotatedValue
1213
from sentry_sdk.attachments import Attachment
1314
from sentry_sdk.consts import DEFAULT_MAX_BREADCRUMBS, FALSE_VALUES, INSTRUMENTER
1415
from sentry_sdk.feature_flags import FlagBuffer, DEFAULT_FLAG_CAPACITY
@@ -186,6 +187,7 @@ class Scope:
186187
"_contexts",
187188
"_extras",
188189
"_breadcrumbs",
190+
"_n_breadcrumbs_truncated",
189191
"_event_processors",
190192
"_error_processors",
191193
"_should_capture",
@@ -210,6 +212,7 @@ def __init__(self, ty=None, client=None):
210212

211213
self._name = None # type: Optional[str]
212214
self._propagation_context = None # type: Optional[PropagationContext]
215+
self._n_breadcrumbs_truncated = 0 # type: int
213216

214217
self.client = NonRecordingClient() # type: sentry_sdk.client.BaseClient
215218

@@ -243,6 +246,7 @@ def __copy__(self):
243246
rv._extras = dict(self._extras)
244247

245248
rv._breadcrumbs = copy(self._breadcrumbs)
249+
rv._n_breadcrumbs_truncated = copy(self._n_breadcrumbs_truncated)
246250
rv._event_processors = list(self._event_processors)
247251
rv._error_processors = list(self._error_processors)
248252
rv._propagation_context = self._propagation_context
@@ -916,6 +920,7 @@ def clear_breadcrumbs(self):
916920
# type: () -> None
917921
"""Clears breadcrumb buffer."""
918922
self._breadcrumbs = deque() # type: Deque[Breadcrumb]
923+
self._n_breadcrumbs_truncated = 0
919924

920925
def add_attachment(
921926
self,
@@ -983,6 +988,7 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs):
983988

984989
while len(self._breadcrumbs) > max_breadcrumbs:
985990
self._breadcrumbs.popleft()
991+
self._n_breadcrumbs_truncated += 1
986992

987993
def start_transaction(
988994
self,
@@ -1366,17 +1372,23 @@ def _apply_level_to_event(self, event, hint, options):
13661372

13671373
def _apply_breadcrumbs_to_event(self, event, hint, options):
13681374
# type: (Event, Hint, Optional[Dict[str, Any]]) -> None
1369-
event.setdefault("breadcrumbs", {}).setdefault("values", []).extend(
1370-
self._breadcrumbs
1371-
)
1375+
event.setdefault("breadcrumbs", {})
1376+
1377+
# This check is just for mypy -
1378+
if not isinstance(event["breadcrumbs"], AnnotatedValue):
1379+
event["breadcrumbs"].setdefault("values", [])
1380+
event["breadcrumbs"]["values"].extend(self._breadcrumbs)
13721381

13731382
# Attempt to sort timestamps
13741383
try:
1375-
for crumb in event["breadcrumbs"]["values"]:
1376-
if isinstance(crumb["timestamp"], str):
1377-
crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"])
1384+
if not isinstance(event["breadcrumbs"], AnnotatedValue):
1385+
for crumb in event["breadcrumbs"]["values"]:
1386+
if isinstance(crumb["timestamp"], str):
1387+
crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"])
13781388

1379-
event["breadcrumbs"]["values"].sort(key=lambda crumb: crumb["timestamp"])
1389+
event["breadcrumbs"]["values"].sort(
1390+
key=lambda crumb: crumb["timestamp"]
1391+
)
13801392
except Exception as err:
13811393
logger.debug("Error when sorting breadcrumbs", exc_info=err)
13821394
pass
@@ -1564,6 +1576,10 @@ def update_from_scope(self, scope):
15641576
self._extras.update(scope._extras)
15651577
if scope._breadcrumbs:
15661578
self._breadcrumbs.extend(scope._breadcrumbs)
1579+
if scope._n_breadcrumbs_truncated:
1580+
self._n_breadcrumbs_truncated = (
1581+
self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated
1582+
)
15671583
if scope._span:
15681584
self._span = scope._span
15691585
if scope._attachments:

sentry_sdk/scrubber.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ def scrub_breadcrumbs(self, event):
144144
# type: (Event) -> None
145145
with capture_internal_exceptions():
146146
if "breadcrumbs" in event:
147-
if "values" in event["breadcrumbs"]:
147+
if (
148+
not isinstance(event["breadcrumbs"], AnnotatedValue)
149+
and "values" in event["breadcrumbs"]
150+
):
148151
for value in event["breadcrumbs"]["values"]:
149152
if "data" in value:
150153
self.scrub_dict(value["data"])

tests/test_scrubber.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -119,25 +119,33 @@ def test_stack_var_scrubbing(sentry_init, capture_events):
119119

120120

121121
def test_breadcrumb_extra_scrubbing(sentry_init, capture_events):
122-
sentry_init()
122+
sentry_init(max_breadcrumbs=2)
123123
events = capture_events()
124-
125-
logger.info("bread", extra=dict(foo=42, password="secret"))
124+
logger.info("breadcrumb 1", extra=dict(foo=1, password="secret"))
125+
logger.info("breadcrumb 2", extra=dict(bar=2, auth="secret"))
126+
logger.info("breadcrumb 3", extra=dict(foobar=3, password="secret"))
126127
logger.critical("whoops", extra=dict(bar=69, auth="secret"))
127128

128129
(event,) = events
129130

130131
assert event["extra"]["bar"] == 69
131132
assert event["extra"]["auth"] == "[Filtered]"
132-
133133
assert event["breadcrumbs"]["values"][0]["data"] == {
134-
"foo": 42,
134+
"bar": 2,
135+
"auth": "[Filtered]",
136+
}
137+
assert event["breadcrumbs"]["values"][1]["data"] == {
138+
"foobar": 3,
135139
"password": "[Filtered]",
136140
}
137141

138142
assert event["_meta"]["extra"]["auth"] == {"": {"rem": [["!config", "s"]]}}
139143
assert event["_meta"]["breadcrumbs"] == {
140-
"values": {"0": {"data": {"password": {"": {"rem": [["!config", "s"]]}}}}}
144+
"": {"len": 3},
145+
"values": {
146+
"0": {"data": {"auth": {"": {"rem": [["!config", "s"]]}}}},
147+
"1": {"data": {"password": {"": {"rem": [["!config", "s"]]}}}},
148+
},
141149
}
142150

143151

0 commit comments

Comments
 (0)