debugbundle-python 0.1.2__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {debugbundle_python-0.1.2/src/debugbundle_python.egg-info → debugbundle_python-0.1.5}/PKG-INFO +1 -1
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/pyproject.toml +1 -1
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/config.py +24 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/core.py +39 -2
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5/src/debugbundle_python.egg-info}/PKG-INFO +1 -1
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_remote_config.py +131 -1
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/LICENSE +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/README.md +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/setup.cfg +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_django.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_fastapi.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_flask.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/logger_integrations.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/relay.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/transport.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_contracts.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_hooks.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_http_integration.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_relay.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_repository_metadata.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_sdk.py +0 -0
- {debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/tests/test_trigger_token.py +0 -0
|
@@ -13,6 +13,7 @@ class CapturePolicy:
|
|
|
13
13
|
capture_request_events: str
|
|
14
14
|
capture_breadcrumbs: str
|
|
15
15
|
capture_probe_events: str
|
|
16
|
+
immediate_client_error_statuses: tuple[int, ...]
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@dataclass(frozen=True)
|
|
@@ -40,6 +41,7 @@ BALANCED_CAPTURE_POLICY = CapturePolicy(
|
|
|
40
41
|
capture_request_events="failures_only",
|
|
41
42
|
capture_breadcrumbs="exception_only",
|
|
42
43
|
capture_probe_events="buffer_only",
|
|
44
|
+
immediate_client_error_statuses=(),
|
|
43
45
|
)
|
|
44
46
|
|
|
45
47
|
MINIMAL_CAPTURE_POLICY = CapturePolicy(
|
|
@@ -48,6 +50,7 @@ MINIMAL_CAPTURE_POLICY = CapturePolicy(
|
|
|
48
50
|
capture_request_events="failures_only",
|
|
49
51
|
capture_breadcrumbs="local_only",
|
|
50
52
|
capture_probe_events="buffer_only",
|
|
53
|
+
immediate_client_error_statuses=(),
|
|
51
54
|
)
|
|
52
55
|
|
|
53
56
|
|
|
@@ -117,6 +120,9 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
|
117
120
|
capture_request_events = _as_non_empty_string(payload.get("capture_request_events"))
|
|
118
121
|
capture_breadcrumbs = _as_non_empty_string(payload.get("capture_breadcrumbs"))
|
|
119
122
|
capture_probe_events = _as_non_empty_string(payload.get("capture_probe_events"))
|
|
123
|
+
immediate_client_error_statuses = _parse_immediate_client_error_statuses(
|
|
124
|
+
payload.get("immediate_client_error_statuses")
|
|
125
|
+
)
|
|
120
126
|
|
|
121
127
|
if capture_logs not in {"off", "error", "warning", "info"}:
|
|
122
128
|
return None
|
|
@@ -126,6 +132,8 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
|
126
132
|
return None
|
|
127
133
|
if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
|
|
128
134
|
return None
|
|
135
|
+
if immediate_client_error_statuses is None:
|
|
136
|
+
return None
|
|
129
137
|
|
|
130
138
|
return CapturePolicy(
|
|
131
139
|
preset=preset,
|
|
@@ -133,9 +141,25 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
|
133
141
|
capture_request_events=capture_request_events,
|
|
134
142
|
capture_breadcrumbs=capture_breadcrumbs,
|
|
135
143
|
capture_probe_events=capture_probe_events,
|
|
144
|
+
immediate_client_error_statuses=immediate_client_error_statuses,
|
|
136
145
|
)
|
|
137
146
|
|
|
138
147
|
|
|
148
|
+
def _parse_immediate_client_error_statuses(value: object) -> tuple[int, ...] | None:
|
|
149
|
+
if value is None:
|
|
150
|
+
return ()
|
|
151
|
+
if not isinstance(value, list) or len(value) > 12:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
statuses: list[int] = []
|
|
155
|
+
for item in value:
|
|
156
|
+
if not isinstance(item, int) or isinstance(item, bool) or item < 400 or item > 499:
|
|
157
|
+
return None
|
|
158
|
+
statuses.append(item)
|
|
159
|
+
|
|
160
|
+
return tuple(sorted(set(statuses)))
|
|
161
|
+
|
|
162
|
+
|
|
139
163
|
def _parse_directive(payload: object) -> RemoteProbeDirective | None:
|
|
140
164
|
if not isinstance(payload, dict):
|
|
141
165
|
return None
|
|
@@ -53,6 +53,11 @@ LEVEL_RANKS = {
|
|
|
53
53
|
"error": 40,
|
|
54
54
|
"critical": 50,
|
|
55
55
|
}
|
|
56
|
+
BALANCED_IMMEDIATE_REQUEST_STATUSES = {408, 423, 424, 425, 429}
|
|
57
|
+
INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES = BALANCED_IMMEDIATE_REQUEST_STATUSES | {409}
|
|
58
|
+
BALANCED_STANDARD_ANOMALY_STATUSES = {401, 403, 404, 409, 422}
|
|
59
|
+
BALANCED_HIGH_VOLUME_ANOMALY_STATUSES = {400, 410}
|
|
60
|
+
INVESTIGATIVE_ANOMALY_STATUSES = BALANCED_STANDARD_ANOMALY_STATUSES | BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
|
|
56
61
|
|
|
57
62
|
|
|
58
63
|
@dataclass
|
|
@@ -604,7 +609,11 @@ class DebugBundleSdk:
|
|
|
604
609
|
candidate = response.get("status_code") or response.get("response_status")
|
|
605
610
|
if isinstance(candidate, int):
|
|
606
611
|
status_code = candidate
|
|
607
|
-
if
|
|
612
|
+
if _is_immediate_request_incident_status(
|
|
613
|
+
status_code,
|
|
614
|
+
self._capture_policy.preset,
|
|
615
|
+
self._capture_policy.immediate_client_error_statuses,
|
|
616
|
+
):
|
|
608
617
|
return True
|
|
609
618
|
if policy == "off":
|
|
610
619
|
return False
|
|
@@ -615,7 +624,7 @@ class DebugBundleSdk:
|
|
|
615
624
|
if status_code is None:
|
|
616
625
|
return policy == "filtered"
|
|
617
626
|
if policy == "failures_only":
|
|
618
|
-
return
|
|
627
|
+
return _is_request_anomaly_candidate_status(status_code, self._capture_policy.preset)
|
|
619
628
|
if policy == "filtered":
|
|
620
629
|
return False
|
|
621
630
|
return True
|
|
@@ -846,6 +855,34 @@ def _iso_now(time_provider: Callable[[], float]) -> str:
|
|
|
846
855
|
return datetime.fromtimestamp(time_provider(), tz=timezone.utc).isoformat().replace("+00:00", "Z")
|
|
847
856
|
|
|
848
857
|
|
|
858
|
+
def _is_immediate_request_incident_status(
|
|
859
|
+
status_code: int | None,
|
|
860
|
+
preset: str,
|
|
861
|
+
immediate_client_error_statuses: tuple[int, ...],
|
|
862
|
+
) -> bool:
|
|
863
|
+
if status_code is None:
|
|
864
|
+
return False
|
|
865
|
+
if status_code >= 500:
|
|
866
|
+
return True
|
|
867
|
+
if status_code in immediate_client_error_statuses:
|
|
868
|
+
return True
|
|
869
|
+
if preset == "investigative":
|
|
870
|
+
return status_code in INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES
|
|
871
|
+
if preset == "balanced":
|
|
872
|
+
return status_code in BALANCED_IMMEDIATE_REQUEST_STATUSES
|
|
873
|
+
return False
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _is_request_anomaly_candidate_status(status_code: int | None, preset: str) -> bool:
|
|
877
|
+
if status_code is None or status_code < 400 or status_code >= 500:
|
|
878
|
+
return False
|
|
879
|
+
if preset == "investigative":
|
|
880
|
+
return status_code in INVESTIGATIVE_ANOMALY_STATUSES
|
|
881
|
+
if preset == "balanced":
|
|
882
|
+
return status_code in BALANCED_STANDARD_ANOMALY_STATUSES or status_code in BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
|
|
883
|
+
return False
|
|
884
|
+
|
|
885
|
+
|
|
849
886
|
def _time_now() -> float:
|
|
850
887
|
return datetime.now(tz=timezone.utc).timestamp()
|
|
851
888
|
|
|
@@ -226,4 +226,134 @@ def test_capture_policy_filters_logs_and_request_events_from_remote_config() ->
|
|
|
226
226
|
events = _events(transport)
|
|
227
227
|
assert [event["event_type"] for event in events] == ["log_event", "request_event"]
|
|
228
228
|
assert events[0]["payload"]["message"] == "error kept"
|
|
229
|
-
assert events[1]["payload"]["response_status"] == 503
|
|
229
|
+
assert events[1]["payload"]["response_status"] == 503
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_balanced_capture_policy_keeps_request_failure_anomaly_candidates() -> None:
|
|
233
|
+
clock = ManualClock()
|
|
234
|
+
fetch = FakeFetch(
|
|
235
|
+
responses=[
|
|
236
|
+
FakeConfigResponse(
|
|
237
|
+
200,
|
|
238
|
+
{
|
|
239
|
+
"probes_enabled": True,
|
|
240
|
+
"remote_probes_enabled": True,
|
|
241
|
+
"active_probes": [],
|
|
242
|
+
"poll_interval_ms": 15000,
|
|
243
|
+
"capture_policy": {
|
|
244
|
+
"preset": "balanced",
|
|
245
|
+
"capture_logs": "warning",
|
|
246
|
+
"capture_request_events": "failures_only",
|
|
247
|
+
"capture_breadcrumbs": "exception_only",
|
|
248
|
+
"capture_probe_events": "buffer_only",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
]
|
|
253
|
+
)
|
|
254
|
+
transport = FakeTransport()
|
|
255
|
+
sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
|
|
256
|
+
sdk.init(
|
|
257
|
+
project_token="dbundle_proj_test",
|
|
258
|
+
service="checkout-api",
|
|
259
|
+
environment="production",
|
|
260
|
+
fetch_impl=fetch,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 429})
|
|
264
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 404})
|
|
265
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 409})
|
|
266
|
+
sdk.flush()
|
|
267
|
+
|
|
268
|
+
events = _events(transport)
|
|
269
|
+
request_statuses = [
|
|
270
|
+
event["payload"]["response_status"]
|
|
271
|
+
for event in events
|
|
272
|
+
if event["event_type"] == "request_event"
|
|
273
|
+
]
|
|
274
|
+
assert request_statuses == [429, 404, 409]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_investigative_capture_policy_promotes_409_even_when_request_capture_is_off() -> None:
|
|
278
|
+
clock = ManualClock()
|
|
279
|
+
fetch = FakeFetch(
|
|
280
|
+
responses=[
|
|
281
|
+
FakeConfigResponse(
|
|
282
|
+
200,
|
|
283
|
+
{
|
|
284
|
+
"probes_enabled": True,
|
|
285
|
+
"remote_probes_enabled": True,
|
|
286
|
+
"active_probes": [],
|
|
287
|
+
"poll_interval_ms": 15000,
|
|
288
|
+
"capture_policy": {
|
|
289
|
+
"preset": "investigative",
|
|
290
|
+
"capture_logs": "info",
|
|
291
|
+
"capture_request_events": "off",
|
|
292
|
+
"capture_breadcrumbs": "standalone",
|
|
293
|
+
"capture_probe_events": "standalone_when_activated",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
transport = FakeTransport()
|
|
300
|
+
sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
|
|
301
|
+
sdk.init(
|
|
302
|
+
project_token="dbundle_proj_test",
|
|
303
|
+
service="checkout-api",
|
|
304
|
+
environment="production",
|
|
305
|
+
fetch_impl=fetch,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 409})
|
|
309
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 404})
|
|
310
|
+
sdk.flush()
|
|
311
|
+
|
|
312
|
+
events = _events(transport)
|
|
313
|
+
assert [event["payload"]["response_status"] for event in events if event["event_type"] == "request_event"] == [409]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_capture_policy_promotes_configured_client_error_statuses_when_request_capture_is_off() -> None:
|
|
317
|
+
clock = ManualClock()
|
|
318
|
+
fetch = FakeFetch(
|
|
319
|
+
responses=[
|
|
320
|
+
FakeConfigResponse(
|
|
321
|
+
200,
|
|
322
|
+
{
|
|
323
|
+
"probes_enabled": True,
|
|
324
|
+
"remote_probes_enabled": True,
|
|
325
|
+
"active_probes": [],
|
|
326
|
+
"poll_interval_ms": 15000,
|
|
327
|
+
"capture_policy": {
|
|
328
|
+
"preset": "minimal",
|
|
329
|
+
"capture_logs": "error",
|
|
330
|
+
"capture_request_events": "off",
|
|
331
|
+
"capture_breadcrumbs": "local_only",
|
|
332
|
+
"capture_probe_events": "buffer_only",
|
|
333
|
+
"immediate_client_error_statuses": [422, 403, 403],
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
]
|
|
338
|
+
)
|
|
339
|
+
transport = FakeTransport()
|
|
340
|
+
sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
|
|
341
|
+
sdk.init(
|
|
342
|
+
project_token="dbundle_proj_test",
|
|
343
|
+
service="checkout-api",
|
|
344
|
+
environment="production",
|
|
345
|
+
fetch_impl=fetch,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 403})
|
|
349
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 404})
|
|
350
|
+
sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 422})
|
|
351
|
+
sdk.flush()
|
|
352
|
+
|
|
353
|
+
events = _events(transport)
|
|
354
|
+
request_statuses = [
|
|
355
|
+
event["payload"]["response_status"]
|
|
356
|
+
for event in events
|
|
357
|
+
if event["event_type"] == "request_event"
|
|
358
|
+
]
|
|
359
|
+
assert request_statuses == [403, 422]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_django.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle/logger_integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{debugbundle_python-0.1.2 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|