debugbundle-python 2.0.1__tar.gz → 2.1.1__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-2.0.1 → debugbundle_python-2.1.1}/PKG-INFO +117 -1
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/README.md +116 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/pyproject.toml +4 -1
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/__init__.py +16 -0
- debugbundle_python-2.1.1/src/debugbundle/before_send.py +62 -0
- debugbundle_python-2.1.1/src/debugbundle/canonical_event.py +202 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/capture_hooks.py +14 -6
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/config.py +6 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/core.py +76 -318
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/delivery_hooks.py +14 -0
- debugbundle_python-2.1.1/src/debugbundle/diagnostic_budget.py +191 -0
- debugbundle_python-2.1.1/src/debugbundle/event-envelope.schema.json +4904 -0
- debugbundle_python-2.1.1/src/debugbundle/evidence_delivery.py +115 -0
- debugbundle_python-2.1.1/src/debugbundle/evidence_http.py +45 -0
- debugbundle_python-2.1.1/src/debugbundle/evidence_json.py +68 -0
- debugbundle_python-2.1.1/src/debugbundle/evidence_wire.py +409 -0
- debugbundle_python-2.1.1/src/debugbundle/exception_capture.py +98 -0
- debugbundle_python-2.1.1/src/debugbundle/exception_evidence.py +199 -0
- debugbundle_python-2.1.1/src/debugbundle/exception_identity.py +47 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/exception_snapshot.py +1 -1
- debugbundle_python-2.1.1/src/debugbundle/flush_delivery.py +203 -0
- debugbundle_python-2.1.1/src/debugbundle/ingestion_wire.py +75 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_django.py +4 -1
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_fastapi.py +10 -2
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_flask.py +4 -1
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/logger_integrations.py +16 -0
- debugbundle_python-2.1.1/src/debugbundle/operation_context.py +121 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/process_support.py +7 -0
- debugbundle_python-2.1.1/src/debugbundle/queue_admission.py +79 -0
- debugbundle_python-2.1.1/src/debugbundle/recovery_capture.py +43 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/relay.py +93 -36
- debugbundle_python-2.1.1/src/debugbundle/relay_delivery.py +229 -0
- debugbundle_python-2.1.1/src/debugbundle/remote_config_client.py +81 -0
- debugbundle_python-2.1.1/src/debugbundle/scope_ownership.py +99 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/structlog_integration.py +21 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/transport.py +20 -14
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/PKG-INFO +117 -1
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/SOURCES.txt +21 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_before_send.py +96 -5
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_contracts.py +3 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_delivery_hooks.py +26 -5
- debugbundle_python-2.1.1/tests/test_evidence_capture.py +272 -0
- debugbundle_python-2.1.1/tests/test_evidence_wire.py +198 -0
- debugbundle_python-2.1.1/tests/test_exception_evidence.py +125 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_exception_snapshot.py +16 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_framework_integrations.py +48 -9
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_http_integration.py +2 -4
- debugbundle_python-2.1.1/tests/test_logging_exception.py +110 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_relay.py +9 -1
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_relay_delivery.py +16 -1
- debugbundle_python-2.1.1/tests/test_relay_evidence.py +215 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_remote_config.py +16 -7
- debugbundle_python-2.1.1/tests/test_transport_reliability.py +286 -0
- debugbundle_python-2.0.1/src/debugbundle/before_send.py +0 -218
- debugbundle_python-2.0.1/src/debugbundle/queue_admission.py +0 -39
- debugbundle_python-2.0.1/src/debugbundle/relay_delivery.py +0 -138
- debugbundle_python-2.0.1/tests/test_transport_reliability.py +0 -144
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/LICENSE +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/setup.cfg +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/acknowledgement.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/event_support.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/flush_aggregates.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/probe_capture.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/request_capture_policy.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_hooks.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_privacy_conformance.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_repository_metadata.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_sdk.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_smoke_script.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_structlog_filtering.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_suppression.py +0 -0
- {debugbundle_python-2.0.1 → debugbundle_python-2.1.1}/tests/test_trigger_token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -41,8 +41,18 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
# debugbundle-python
|
|
43
43
|
|
|
44
|
+
Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
|
|
45
|
+
|
|
44
46
|
Python SDK for DebugBundle.
|
|
45
47
|
|
|
48
|
+
After `make install-docker`, `make evidence-performance` compares identical
|
|
49
|
+
exception/recovery calls with evidence disabled and enabled, including a full
|
|
50
|
+
warning queue, with networking disabled. It reports capture timing separately from
|
|
51
|
+
Python `tracemalloc` peak/live allocation measurements, verifies native causes and
|
|
52
|
+
original-error priority, and checks existing retention ceilings. Setup and cleanup
|
|
53
|
+
are excluded; traced allocations are Python-managed memory, not cumulative churn
|
|
54
|
+
or total process RSS.
|
|
55
|
+
|
|
46
56
|

|
|
47
57
|

|
|
48
58
|

|
|
@@ -51,6 +61,93 @@ Use this package to capture Python backend exceptions, request metadata, structu
|
|
|
51
61
|
|
|
52
62
|
Requires Python 3.10 or newer.
|
|
53
63
|
|
|
64
|
+
## Evidence in 2.1.0
|
|
65
|
+
|
|
66
|
+
Version 2.1.0 adds `capture_exception_evidence=True` and
|
|
67
|
+
`capture_recovery_observations=False` installation options. Collection requires
|
|
68
|
+
verified delivery/reader support, discovered on existing config/ACK traffic with
|
|
69
|
+
`debugbundle_evidence=1`. No capture call performs discovery or adds polling.
|
|
70
|
+
`evidence_capabilities` is an optional descriptor pin asserting the **entire**
|
|
71
|
+
path, including local readers, relays and wrappers; later discovery can revoke it.
|
|
72
|
+
Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
|
|
73
|
+
|
|
74
|
+
Native cause, implicit context and Python 3.11+ exception-group evidence excludes
|
|
75
|
+
locals, source excerpts, arbitrary renderers and application properties. Python
|
|
76
|
+
3.10 has no native exception groups. Graphs retain at most eight nodes, depth
|
|
77
|
+
four, five children and 32 frames per node; all optional exception additions fit
|
|
78
|
+
32 KiB. Uncaught native capture bypasses `before_send` and uses two nodes, eight
|
|
79
|
+
frames and a 4-KiB total including execution/reference metadata. The original
|
|
80
|
+
sanitised stack remains separate.
|
|
81
|
+
|
|
82
|
+
`with sdk.operation_context({"kind": "job", "name": "invoice.calculate",
|
|
83
|
+
"limit_ms": 5000}):` supplies a fresh operation scope and monotonic elapsed time
|
|
84
|
+
to synchronous or asynchronous work inside the block. Kinds are `request`,
|
|
85
|
+
`command`, `job` and `unknown`. The limit is a known configured limit, not time
|
|
86
|
+
remaining. Framework requests use their existing begin/end lifecycle. Completed
|
|
87
|
+
owners revoke copied async context; reinitialisation/fork discard prior ownership.
|
|
88
|
+
Application results and exceptions pass through the context manager unchanged.
|
|
89
|
+
|
|
90
|
+
The existing background sender remains best-effort at immediate process exit.
|
|
91
|
+
The uncaught hook collects bounded native evidence without waiting for transport;
|
|
92
|
+
applications needing a delivery attempt use explicit `flush()` in a controlled
|
|
93
|
+
shutdown window. This release does not add default crash-file persistence or
|
|
94
|
+
synchronous capture transport.
|
|
95
|
+
|
|
96
|
+
Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
|
|
97
|
+
`evidence_scope` can specify a fresh page or operation UUID; an active operation
|
|
98
|
+
provides the default. `capture_recovery({"failure_ref": failure_ref,
|
|
99
|
+
"attempt_id": attempt_id, "state": "attempt_completed"})` records an explicit
|
|
100
|
+
application observation. Optional fields are `evidence_scope`, technical
|
|
101
|
+
`operation`, `status_code`, `duration_ms` and primitive `correlation` metadata.
|
|
102
|
+
Page scopes require a nonblank session ID, including the valid string `"0"`.
|
|
103
|
+
Only explicit `recovery_observed` claims semantic recovery; HTTP 200 and
|
|
104
|
+
`attempt_completed` leave it unknown. The other states are `attempt_started`,
|
|
105
|
+
`attempt_failed` and `failure_persisted`.
|
|
106
|
+
|
|
107
|
+
Standalone recovery requires its local opt-in, `recovery_v1` support and server
|
|
108
|
+
`capture_diagnostic_context`; minimal policy disables it. Privacy, sampling,
|
|
109
|
+
resulting hook-event policy and queue limits still apply. Fifty failure windows,
|
|
110
|
+
five attempts and twenty observations per failure share 64 KiB of tracker and
|
|
111
|
+
pending-payload ownership, retained until ACK/drop even offline. Canonical hooks
|
|
112
|
+
may change any allowed identity/type field; ownership follows the replacement.
|
|
113
|
+
The complete closed envelope is checked against the vendored accepting Draft 7
|
|
114
|
+
schema, followed by evidence semantics and mandatory privacy. Only native JSON
|
|
115
|
+
containers/scalars are copied; custom serializers and container subclasses are
|
|
116
|
+
never invoked by hook validation. Invalid or throwing hooks keep the protected
|
|
117
|
+
original. Exhausted validation or an unfit valid replacement is withheld, so an
|
|
118
|
+
application redaction cannot be undone by a budget fallback.
|
|
119
|
+
|
|
120
|
+
Native HTTP requests select an ordered prefix by the actual compact UTF-8 JSON
|
|
121
|
+
serializer and remain within the receiving 256-KiB limit. Unsent finalized tails
|
|
122
|
+
stay charged and continue through the existing sender without repeating their
|
|
123
|
+
hooks. Optional SDK evidence is reduced before withholding an unfit primary;
|
|
124
|
+
explicit custom/local transports retain their established budgets. Browser
|
|
125
|
+
relays measure after server identity/token expansion, split downstream requests
|
|
126
|
+
and keep incomplete spools. A legacy custom success remains accepted but cannot
|
|
127
|
+
mark a durable spool delivered; that requires complete canonical acknowledgements.
|
|
128
|
+
Relay chunks share a five-second monotonic forwarding deadline, and spool markers
|
|
129
|
+
are created exclusively with private permissions without following symlinks.
|
|
130
|
+
|
|
131
|
+
Native HTTP ACK reads stop at 256 KiB and close the response. Requests ask for
|
|
132
|
+
identity encoding; unexpected compressed ACKs are withheld. The five-second
|
|
133
|
+
deadline is checked between streamed reads; HTTPX's synchronous per-read/connect/
|
|
134
|
+
write timeouts remain advisory and an in-progress native read can overrun that
|
|
135
|
+
deadline. Explicit custom transports/config fetchers retain their own timeout,
|
|
136
|
+
allocation and cancellation responsibilities. No additional timer thread or
|
|
137
|
+
inline capture transport is introduced.
|
|
138
|
+
|
|
139
|
+
Browser relays and Django/Flask/FastAPI relay factories accept the same optional
|
|
140
|
+
`evidence_capabilities` pin. They preserve closed DTOs, protected application
|
|
141
|
+
context and server-owned credentials, and advertise only verified downstream
|
|
142
|
+
support. Opted-in replies keep string `errors` and add indexed `evidence_errors`;
|
|
143
|
+
partial batches return 202, while legacy partial responses retain 400. A durable
|
|
144
|
+
spool accepts locally but is marked delivered only after a valid complete ACK.
|
|
145
|
+
Malformed ACKs cannot grant proof. Only negotiated `invalid_event` rejection of
|
|
146
|
+
an original exception can retry its stripped legacy form once; standalone
|
|
147
|
+
context, policy and authentication rejections do not use this fallback.
|
|
148
|
+
Framework relay adapters read at most the configured body limit plus one byte
|
|
149
|
+
before parsing, and preserve the negotiation query/header through the handler.
|
|
150
|
+
|
|
54
151
|
Version 2.0.0 protects capture callers with bounded, priority-aware background delivery. Review the [2.0 migration guide](MIGRATION-2.0.md) before upgrading from 1.x.
|
|
55
152
|
|
|
56
153
|
## Automatic capture and application filtering
|
|
@@ -363,3 +460,22 @@ Apache-2.0. See `LICENSE`.
|
|
|
363
460
|
The built-in HTTP transport requires the canonical ingestion acknowledgement (`accepted`, `rejected`, and `errors`). Empty responses, unrelated JSON, malformed counts, or invalid rejection indices retain the full batch with backoff. Valid acknowledgements remove accepted and terminally rejected events and retry only the indexed retryable rejections; an all-rejected batch does not advance `lastEventAt`.
|
|
364
461
|
|
|
365
462
|
File transports and explicitly supplied custom/legacy transports retain their documented bodyless success fallback. A custom transport that returns acknowledgement fields must return the complete canonical shape. Retry hints (delay seconds or HTTP dates) are bounded to five minutes and measured from response receipt, including partial/protocol acknowledgements and retryable server failures. Without a server hint, existing retry timing is preserved. Failures remain contained within the SDK's existing delivery path.
|
|
463
|
+
|
|
464
|
+
## Probe checkpoint guidance
|
|
465
|
+
|
|
466
|
+
Application instrumentation supplies probe labels and bounded data. Deploy a new
|
|
467
|
+
checkpoint before activating it. Remote activation affects future matching calls
|
|
468
|
+
when policy permits; it does not inject code or recover unretained measurements.
|
|
469
|
+
Built-in runtime/error/device collectors keep their documented SDK settings.
|
|
470
|
+
|
|
471
|
+
Use fixed labels and allowlisted technical states: `media.preview.checkpoint`
|
|
472
|
+
(stage/input kind/attempt), `auth.session.checkpoint` (observed session presence
|
|
473
|
+
and result), `job.dependency.checkpoint` (named dependency/attempt/result), or
|
|
474
|
+
`cache.preview.checkpoint` (hit/miss/coarse age bucket). Omit customer identifiers,
|
|
475
|
+
tokens/cookies, cache keys, URLs and raw payloads. HTTP 200 or attempt completion
|
|
476
|
+
alone does not establish semantic recovery.
|
|
477
|
+
|
|
478
|
+
Empty probe data means no matching retained observation for the selected sample
|
|
479
|
+
window. It does not confirm activation delivery to every SDK or execution of a
|
|
480
|
+
label. Check the deployed SDK/wrapper version, instrumentation, scope, expiry,
|
|
481
|
+
policy, sampling and delivery receipts. See the [probe recipes](https://debugbundle.com/docs/probes#bounded-checkpoint-recipes).
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# debugbundle-python
|
|
2
2
|
|
|
3
|
+
Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
|
|
4
|
+
|
|
3
5
|
Python SDK for DebugBundle.
|
|
4
6
|
|
|
7
|
+
After `make install-docker`, `make evidence-performance` compares identical
|
|
8
|
+
exception/recovery calls with evidence disabled and enabled, including a full
|
|
9
|
+
warning queue, with networking disabled. It reports capture timing separately from
|
|
10
|
+
Python `tracemalloc` peak/live allocation measurements, verifies native causes and
|
|
11
|
+
original-error priority, and checks existing retention ceilings. Setup and cleanup
|
|
12
|
+
are excluded; traced allocations are Python-managed memory, not cumulative churn
|
|
13
|
+
or total process RSS.
|
|
14
|
+
|
|
5
15
|

|
|
6
16
|

|
|
7
17
|

|
|
@@ -10,6 +20,93 @@ Use this package to capture Python backend exceptions, request metadata, structu
|
|
|
10
20
|
|
|
11
21
|
Requires Python 3.10 or newer.
|
|
12
22
|
|
|
23
|
+
## Evidence in 2.1.0
|
|
24
|
+
|
|
25
|
+
Version 2.1.0 adds `capture_exception_evidence=True` and
|
|
26
|
+
`capture_recovery_observations=False` installation options. Collection requires
|
|
27
|
+
verified delivery/reader support, discovered on existing config/ACK traffic with
|
|
28
|
+
`debugbundle_evidence=1`. No capture call performs discovery or adds polling.
|
|
29
|
+
`evidence_capabilities` is an optional descriptor pin asserting the **entire**
|
|
30
|
+
path, including local readers, relays and wrappers; later discovery can revoke it.
|
|
31
|
+
Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
|
|
32
|
+
|
|
33
|
+
Native cause, implicit context and Python 3.11+ exception-group evidence excludes
|
|
34
|
+
locals, source excerpts, arbitrary renderers and application properties. Python
|
|
35
|
+
3.10 has no native exception groups. Graphs retain at most eight nodes, depth
|
|
36
|
+
four, five children and 32 frames per node; all optional exception additions fit
|
|
37
|
+
32 KiB. Uncaught native capture bypasses `before_send` and uses two nodes, eight
|
|
38
|
+
frames and a 4-KiB total including execution/reference metadata. The original
|
|
39
|
+
sanitised stack remains separate.
|
|
40
|
+
|
|
41
|
+
`with sdk.operation_context({"kind": "job", "name": "invoice.calculate",
|
|
42
|
+
"limit_ms": 5000}):` supplies a fresh operation scope and monotonic elapsed time
|
|
43
|
+
to synchronous or asynchronous work inside the block. Kinds are `request`,
|
|
44
|
+
`command`, `job` and `unknown`. The limit is a known configured limit, not time
|
|
45
|
+
remaining. Framework requests use their existing begin/end lifecycle. Completed
|
|
46
|
+
owners revoke copied async context; reinitialisation/fork discard prior ownership.
|
|
47
|
+
Application results and exceptions pass through the context manager unchanged.
|
|
48
|
+
|
|
49
|
+
The existing background sender remains best-effort at immediate process exit.
|
|
50
|
+
The uncaught hook collects bounded native evidence without waiting for transport;
|
|
51
|
+
applications needing a delivery attempt use explicit `flush()` in a controlled
|
|
52
|
+
shutdown window. This release does not add default crash-file persistence or
|
|
53
|
+
synchronous capture transport.
|
|
54
|
+
|
|
55
|
+
Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
|
|
56
|
+
`evidence_scope` can specify a fresh page or operation UUID; an active operation
|
|
57
|
+
provides the default. `capture_recovery({"failure_ref": failure_ref,
|
|
58
|
+
"attempt_id": attempt_id, "state": "attempt_completed"})` records an explicit
|
|
59
|
+
application observation. Optional fields are `evidence_scope`, technical
|
|
60
|
+
`operation`, `status_code`, `duration_ms` and primitive `correlation` metadata.
|
|
61
|
+
Page scopes require a nonblank session ID, including the valid string `"0"`.
|
|
62
|
+
Only explicit `recovery_observed` claims semantic recovery; HTTP 200 and
|
|
63
|
+
`attempt_completed` leave it unknown. The other states are `attempt_started`,
|
|
64
|
+
`attempt_failed` and `failure_persisted`.
|
|
65
|
+
|
|
66
|
+
Standalone recovery requires its local opt-in, `recovery_v1` support and server
|
|
67
|
+
`capture_diagnostic_context`; minimal policy disables it. Privacy, sampling,
|
|
68
|
+
resulting hook-event policy and queue limits still apply. Fifty failure windows,
|
|
69
|
+
five attempts and twenty observations per failure share 64 KiB of tracker and
|
|
70
|
+
pending-payload ownership, retained until ACK/drop even offline. Canonical hooks
|
|
71
|
+
may change any allowed identity/type field; ownership follows the replacement.
|
|
72
|
+
The complete closed envelope is checked against the vendored accepting Draft 7
|
|
73
|
+
schema, followed by evidence semantics and mandatory privacy. Only native JSON
|
|
74
|
+
containers/scalars are copied; custom serializers and container subclasses are
|
|
75
|
+
never invoked by hook validation. Invalid or throwing hooks keep the protected
|
|
76
|
+
original. Exhausted validation or an unfit valid replacement is withheld, so an
|
|
77
|
+
application redaction cannot be undone by a budget fallback.
|
|
78
|
+
|
|
79
|
+
Native HTTP requests select an ordered prefix by the actual compact UTF-8 JSON
|
|
80
|
+
serializer and remain within the receiving 256-KiB limit. Unsent finalized tails
|
|
81
|
+
stay charged and continue through the existing sender without repeating their
|
|
82
|
+
hooks. Optional SDK evidence is reduced before withholding an unfit primary;
|
|
83
|
+
explicit custom/local transports retain their established budgets. Browser
|
|
84
|
+
relays measure after server identity/token expansion, split downstream requests
|
|
85
|
+
and keep incomplete spools. A legacy custom success remains accepted but cannot
|
|
86
|
+
mark a durable spool delivered; that requires complete canonical acknowledgements.
|
|
87
|
+
Relay chunks share a five-second monotonic forwarding deadline, and spool markers
|
|
88
|
+
are created exclusively with private permissions without following symlinks.
|
|
89
|
+
|
|
90
|
+
Native HTTP ACK reads stop at 256 KiB and close the response. Requests ask for
|
|
91
|
+
identity encoding; unexpected compressed ACKs are withheld. The five-second
|
|
92
|
+
deadline is checked between streamed reads; HTTPX's synchronous per-read/connect/
|
|
93
|
+
write timeouts remain advisory and an in-progress native read can overrun that
|
|
94
|
+
deadline. Explicit custom transports/config fetchers retain their own timeout,
|
|
95
|
+
allocation and cancellation responsibilities. No additional timer thread or
|
|
96
|
+
inline capture transport is introduced.
|
|
97
|
+
|
|
98
|
+
Browser relays and Django/Flask/FastAPI relay factories accept the same optional
|
|
99
|
+
`evidence_capabilities` pin. They preserve closed DTOs, protected application
|
|
100
|
+
context and server-owned credentials, and advertise only verified downstream
|
|
101
|
+
support. Opted-in replies keep string `errors` and add indexed `evidence_errors`;
|
|
102
|
+
partial batches return 202, while legacy partial responses retain 400. A durable
|
|
103
|
+
spool accepts locally but is marked delivered only after a valid complete ACK.
|
|
104
|
+
Malformed ACKs cannot grant proof. Only negotiated `invalid_event` rejection of
|
|
105
|
+
an original exception can retry its stripped legacy form once; standalone
|
|
106
|
+
context, policy and authentication rejections do not use this fallback.
|
|
107
|
+
Framework relay adapters read at most the configured body limit plus one byte
|
|
108
|
+
before parsing, and preserve the negotiation query/header through the handler.
|
|
109
|
+
|
|
13
110
|
Version 2.0.0 protects capture callers with bounded, priority-aware background delivery. Review the [2.0 migration guide](MIGRATION-2.0.md) before upgrading from 1.x.
|
|
14
111
|
|
|
15
112
|
## Automatic capture and application filtering
|
|
@@ -322,3 +419,22 @@ Apache-2.0. See `LICENSE`.
|
|
|
322
419
|
The built-in HTTP transport requires the canonical ingestion acknowledgement (`accepted`, `rejected`, and `errors`). Empty responses, unrelated JSON, malformed counts, or invalid rejection indices retain the full batch with backoff. Valid acknowledgements remove accepted and terminally rejected events and retry only the indexed retryable rejections; an all-rejected batch does not advance `lastEventAt`.
|
|
323
420
|
|
|
324
421
|
File transports and explicitly supplied custom/legacy transports retain their documented bodyless success fallback. A custom transport that returns acknowledgement fields must return the complete canonical shape. Retry hints (delay seconds or HTTP dates) are bounded to five minutes and measured from response receipt, including partial/protocol acknowledgements and retryable server failures. Without a server hint, existing retry timing is preserved. Failures remain contained within the SDK's existing delivery path.
|
|
422
|
+
|
|
423
|
+
## Probe checkpoint guidance
|
|
424
|
+
|
|
425
|
+
Application instrumentation supplies probe labels and bounded data. Deploy a new
|
|
426
|
+
checkpoint before activating it. Remote activation affects future matching calls
|
|
427
|
+
when policy permits; it does not inject code or recover unretained measurements.
|
|
428
|
+
Built-in runtime/error/device collectors keep their documented SDK settings.
|
|
429
|
+
|
|
430
|
+
Use fixed labels and allowlisted technical states: `media.preview.checkpoint`
|
|
431
|
+
(stage/input kind/attempt), `auth.session.checkpoint` (observed session presence
|
|
432
|
+
and result), `job.dependency.checkpoint` (named dependency/attempt/result), or
|
|
433
|
+
`cache.preview.checkpoint` (hit/miss/coarse age bucket). Omit customer identifiers,
|
|
434
|
+
tokens/cookies, cache keys, URLs and raw payloads. HTTP 200 or attempt completion
|
|
435
|
+
alone does not establish semantic recovery.
|
|
436
|
+
|
|
437
|
+
Empty probe data means no matching retained observation for the selected sample
|
|
438
|
+
window. It does not confirm activation delivery to every SDK or execution of a
|
|
439
|
+
label. Check the deployed SDK/wrapper version, instrumentation, scope, expiry,
|
|
440
|
+
policy, sampling and delivery receipts. See the [probe recipes](https://debugbundle.com/docs/probes#bounded-checkpoint-recipes).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "debugbundle-python"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.1"
|
|
8
8
|
description = "DebugBundle SDK for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -54,6 +54,9 @@ Issues = "https://github.com/debugbundle/debugbundle-python/issues"
|
|
|
54
54
|
package-dir = {"" = "src"}
|
|
55
55
|
include-package-data = true
|
|
56
56
|
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
debugbundle = ["event-envelope.schema.json", "py.typed"]
|
|
59
|
+
|
|
57
60
|
[tool.setuptools.packages.find]
|
|
58
61
|
where = ["src"]
|
|
59
62
|
|
|
@@ -33,6 +33,9 @@ def init(
|
|
|
33
33
|
on_diagnostic: Callable[[dict[str, object]], None] | None = None,
|
|
34
34
|
before_send: BeforeSendHook | None = None,
|
|
35
35
|
probes_poll_interval: int = 60_000,
|
|
36
|
+
capture_exception_evidence: bool = True,
|
|
37
|
+
evidence_capabilities: object = None,
|
|
38
|
+
capture_recovery_observations: bool = False,
|
|
36
39
|
) -> None:
|
|
37
40
|
_sdk.init(
|
|
38
41
|
project_token=project_token,
|
|
@@ -52,6 +55,9 @@ def init(
|
|
|
52
55
|
on_diagnostic=on_diagnostic,
|
|
53
56
|
before_send=before_send,
|
|
54
57
|
probes_poll_interval=probes_poll_interval,
|
|
58
|
+
capture_exception_evidence=capture_exception_evidence,
|
|
59
|
+
evidence_capabilities=evidence_capabilities,
|
|
60
|
+
capture_recovery_observations=capture_recovery_observations,
|
|
55
61
|
)
|
|
56
62
|
|
|
57
63
|
|
|
@@ -63,6 +69,14 @@ def capture_error(error: BaseException, context: dict[str, object] | None = None
|
|
|
63
69
|
_sdk.capture_error(error, context=context)
|
|
64
70
|
|
|
65
71
|
|
|
72
|
+
def operation_context(options: object) -> Any:
|
|
73
|
+
return _sdk.operation_context(options)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def capture_recovery(observation: object) -> None:
|
|
77
|
+
_sdk.capture_recovery(observation)
|
|
78
|
+
|
|
79
|
+
|
|
66
80
|
def capture_log(message: str, level: str = "warning", context: dict[str, object] | None = None) -> None:
|
|
67
81
|
_sdk.capture_log(message, level=level, context=context)
|
|
68
82
|
|
|
@@ -155,6 +169,7 @@ __all__ = [
|
|
|
155
169
|
"capture_logging",
|
|
156
170
|
"capture_message",
|
|
157
171
|
"capture_request",
|
|
172
|
+
"capture_recovery",
|
|
158
173
|
"create_django_relay_view",
|
|
159
174
|
"create_fastapi_relay_handler",
|
|
160
175
|
"create_flask_relay_handler",
|
|
@@ -164,5 +179,6 @@ __all__ = [
|
|
|
164
179
|
"instrument_fastapi",
|
|
165
180
|
"instrument_flask",
|
|
166
181
|
"probe",
|
|
182
|
+
"operation_context",
|
|
167
183
|
"set_context",
|
|
168
184
|
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Canonical, owned hook results with explicit invalid and exhausted outcomes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from .canonical_event import EXHAUSTED, INVALID, ValidationBudgetExceeded, own_json, valid_owned_event
|
|
10
|
+
from .evidence_wire import validate_evidence
|
|
11
|
+
|
|
12
|
+
BeforeSendHook = Callable[[dict[str, object]], dict[str, object] | None]
|
|
13
|
+
_MOBILE = ("@debugbundle/sdk-android", "@debugbundle/sdk-swift", "@debugbundle/sdk-react-native")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalized(event: object) -> object:
|
|
17
|
+
if type(event) is not dict:
|
|
18
|
+
return INVALID
|
|
19
|
+
owned = own_json(event)
|
|
20
|
+
if owned is INVALID or owned is EXHAUSTED:
|
|
21
|
+
return owned
|
|
22
|
+
try:
|
|
23
|
+
if not valid_owned_event(owned) or not validate_evidence(owned):
|
|
24
|
+
return INVALID
|
|
25
|
+
# The accepting Zod contract refines this condition outside JSON Schema.
|
|
26
|
+
if owned["event_type"] == "frontend_exception":
|
|
27
|
+
field = "device" if owned["sdk_name"] in _MOBILE else "browser"
|
|
28
|
+
if field not in owned["payload"] or type(owned["payload"][field]) is not dict:
|
|
29
|
+
return INVALID
|
|
30
|
+
return owned
|
|
31
|
+
except ValidationBudgetExceeded:
|
|
32
|
+
return EXHAUSTED
|
|
33
|
+
except Exception:
|
|
34
|
+
return INVALID
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def apply_before_send(
|
|
38
|
+
event: dict[str, object],
|
|
39
|
+
hook: BeforeSendHook | None,
|
|
40
|
+
emit_diagnostic: Callable[[str, str], None],
|
|
41
|
+
) -> dict[str, object] | None:
|
|
42
|
+
if hook is None:
|
|
43
|
+
return event
|
|
44
|
+
try:
|
|
45
|
+
result = hook(copy.deepcopy(event))
|
|
46
|
+
except Exception:
|
|
47
|
+
emit_diagnostic("before_send_failed", "Python before_send hook failed")
|
|
48
|
+
return event
|
|
49
|
+
if result is None:
|
|
50
|
+
return None
|
|
51
|
+
checked = _normalized(result)
|
|
52
|
+
if checked is EXHAUSTED:
|
|
53
|
+
return None
|
|
54
|
+
if checked is INVALID:
|
|
55
|
+
emit_diagnostic("before_send_invalid_event", "Python before_send returned an invalid event")
|
|
56
|
+
return event
|
|
57
|
+
return cast(dict[str, object], checked)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_valid_event(event: object) -> bool:
|
|
61
|
+
result = _normalized(event)
|
|
62
|
+
return result is not INVALID and result is not EXHAUSTED
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Owned JSON and a closed interpreter for the vendored accepting Draft 7 schema.
|
|
2
|
+
|
|
3
|
+
Only native containers/scalars are inspected. No application serializers, remote
|
|
4
|
+
references or optional runtime dependencies participate in hook validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import re
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .evidence_wire import _timestamp, _uuid
|
|
17
|
+
|
|
18
|
+
MAX_OWNED_BYTES = 8 * 1024 * 1024
|
|
19
|
+
INVALID = object()
|
|
20
|
+
EXHAUSTED = object()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationBudgetExceeded(Exception):
|
|
24
|
+
"""Withhold hook output rather than restore content it may have removed."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def own_json(value: object) -> Any:
|
|
28
|
+
remaining = MAX_OWNED_BYTES
|
|
29
|
+
nodes = 4096
|
|
30
|
+
|
|
31
|
+
def copy(item: Any, depth: int) -> Any:
|
|
32
|
+
nonlocal remaining, nodes
|
|
33
|
+
nodes -= 1
|
|
34
|
+
if nodes < 0 or depth > 16 or remaining < 0:
|
|
35
|
+
raise ValidationBudgetExceeded
|
|
36
|
+
kind = type(item)
|
|
37
|
+
if item is None or kind is bool:
|
|
38
|
+
remaining -= 5
|
|
39
|
+
return item
|
|
40
|
+
if kind is str:
|
|
41
|
+
if len(item) > remaining:
|
|
42
|
+
raise ValidationBudgetExceeded
|
|
43
|
+
remaining -= len(item.encode("utf-8")) + 2
|
|
44
|
+
if remaining < 0:
|
|
45
|
+
raise ValidationBudgetExceeded
|
|
46
|
+
return item
|
|
47
|
+
if kind is int or kind is float:
|
|
48
|
+
# The accepting JavaScript number domain is finite IEEE-754.
|
|
49
|
+
if not -1.7976931348623157e308 <= item <= 1.7976931348623157e308 or not math.isfinite(item):
|
|
50
|
+
raise ValueError("non-finite number")
|
|
51
|
+
remaining -= 24
|
|
52
|
+
return item
|
|
53
|
+
if kind is list:
|
|
54
|
+
if len(item) > nodes:
|
|
55
|
+
raise ValidationBudgetExceeded
|
|
56
|
+
remaining -= 2 + len(item)
|
|
57
|
+
return [copy(child, depth + 1) for child in item]
|
|
58
|
+
if kind is dict:
|
|
59
|
+
if len(item) * 2 > nodes:
|
|
60
|
+
raise ValidationBudgetExceeded
|
|
61
|
+
remaining -= 2 + 2 * len(item)
|
|
62
|
+
result = {}
|
|
63
|
+
for key, child in item.items():
|
|
64
|
+
if type(key) is not str:
|
|
65
|
+
raise ValueError("non-string key")
|
|
66
|
+
result[copy(key, depth + 1)] = copy(child, depth + 1)
|
|
67
|
+
return result
|
|
68
|
+
raise ValueError("non-native JSON value")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
return copy(value, 0)
|
|
72
|
+
except ValidationBudgetExceeded:
|
|
73
|
+
return EXHAUSTED
|
|
74
|
+
except (ValueError, UnicodeError, OverflowError):
|
|
75
|
+
return INVALID
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CanonicalEventSchema:
|
|
79
|
+
KEYWORDS = frozenset(
|
|
80
|
+
"$schema $id $comment $ref type anyOf const enum properties required additionalProperties "
|
|
81
|
+
"items minimum maximum exclusiveMinimum minLength maxLength minItems maxItems format pattern".split()
|
|
82
|
+
)
|
|
83
|
+
TYPES = frozenset("null boolean number integer string array object".split())
|
|
84
|
+
|
|
85
|
+
def __init__(self, schema: dict[str, Any]) -> None:
|
|
86
|
+
self.root = schema
|
|
87
|
+
self.references: dict[str, Any] = {}
|
|
88
|
+
self.patterns: dict[str, re.Pattern[str]] = {}
|
|
89
|
+
self._compile(schema)
|
|
90
|
+
|
|
91
|
+
def _resolve(self, reference: str) -> Any:
|
|
92
|
+
if not reference.startswith("#/"):
|
|
93
|
+
raise ValueError("external schema ref")
|
|
94
|
+
if reference not in self.references:
|
|
95
|
+
node: Any = self.root
|
|
96
|
+
for part in reference[2:].split("/"):
|
|
97
|
+
key = part.replace("~1", "/").replace("~0", "~")
|
|
98
|
+
node = node[int(key)] if type(node) is list else node[key]
|
|
99
|
+
self.references[reference] = node
|
|
100
|
+
return self.references[reference]
|
|
101
|
+
|
|
102
|
+
def _compile(self, node: Any) -> None:
|
|
103
|
+
if type(node) is bool:
|
|
104
|
+
return
|
|
105
|
+
if not set(node).issubset(self.KEYWORDS):
|
|
106
|
+
raise ValueError("unsupported schema keyword")
|
|
107
|
+
kinds = node.get("type", [])
|
|
108
|
+
if not set([kinds] if type(kinds) is str else kinds).issubset(self.TYPES):
|
|
109
|
+
raise ValueError("unsupported schema type")
|
|
110
|
+
if node.get("format") not in (None, "uuid", "date-time"):
|
|
111
|
+
raise ValueError("unsupported schema format")
|
|
112
|
+
if "pattern" in node:
|
|
113
|
+
self.patterns[node["pattern"]] = re.compile(node["pattern"])
|
|
114
|
+
if "$ref" in node:
|
|
115
|
+
self._resolve(node["$ref"])
|
|
116
|
+
for child in node.get("properties", {}).values():
|
|
117
|
+
self._compile(child)
|
|
118
|
+
for child in node.get("anyOf", []):
|
|
119
|
+
self._compile(child)
|
|
120
|
+
for key in ("items", "additionalProperties"):
|
|
121
|
+
if key in node:
|
|
122
|
+
self._compile(node[key])
|
|
123
|
+
|
|
124
|
+
def accepts(self, value: Any) -> bool:
|
|
125
|
+
return self._check(self.root, value, [100_000], 0)
|
|
126
|
+
|
|
127
|
+
def _check(self, node: Any, value: Any, budget: list[int], depth: int) -> bool:
|
|
128
|
+
budget[0] -= 1
|
|
129
|
+
if depth > 64 or budget[0] < 0:
|
|
130
|
+
raise ValidationBudgetExceeded
|
|
131
|
+
if type(node) is bool:
|
|
132
|
+
return node
|
|
133
|
+
if "$ref" in node:
|
|
134
|
+
return self._check(self._resolve(node["$ref"]), value, budget, depth + 1)
|
|
135
|
+
if "anyOf" in node and not any(self._check(child, value, budget, depth + 1) for child in node["anyOf"]):
|
|
136
|
+
return False
|
|
137
|
+
if "type" in node:
|
|
138
|
+
kinds = node["type"] if type(node["type"]) is list else [node["type"]]
|
|
139
|
+
if not any(self._type(value, kind) for kind in kinds):
|
|
140
|
+
return False
|
|
141
|
+
if "const" in node and not self._equal(value, node["const"]):
|
|
142
|
+
return False
|
|
143
|
+
if "enum" in node and not any(self._equal(value, choice) for choice in node["enum"]):
|
|
144
|
+
return False
|
|
145
|
+
if type(value) is dict:
|
|
146
|
+
properties = node.get("properties", {})
|
|
147
|
+
return all(key in value for key in node.get("required", [])) and all(
|
|
148
|
+
self._check(properties.get(key, node.get("additionalProperties", True)), child, budget, depth + 1)
|
|
149
|
+
for key, child in value.items()
|
|
150
|
+
)
|
|
151
|
+
if type(value) is list:
|
|
152
|
+
return (
|
|
153
|
+
len(value) >= node.get("minItems", 0)
|
|
154
|
+
and len(value) <= node.get("maxItems", len(value))
|
|
155
|
+
and (
|
|
156
|
+
"items" not in node or all(self._check(node["items"], child, budget, depth + 1) for child in value)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
if type(value) is str:
|
|
160
|
+
length = len(value.encode("utf-16-le")) // 2 # Zod counts JavaScript UTF-16 code units.
|
|
161
|
+
return (
|
|
162
|
+
length >= node.get("minLength", 0)
|
|
163
|
+
and length <= node.get("maxLength", length)
|
|
164
|
+
and ("pattern" not in node or bool(self.patterns[node["pattern"]].search(value)))
|
|
165
|
+
and (node.get("format") != "uuid" or _uuid(value))
|
|
166
|
+
and (node.get("format") != "date-time" or _timestamp(value))
|
|
167
|
+
)
|
|
168
|
+
if type(value) is int or type(value) is float:
|
|
169
|
+
return (
|
|
170
|
+
("minimum" not in node or value >= node["minimum"])
|
|
171
|
+
and ("maximum" not in node or value <= node["maximum"])
|
|
172
|
+
and ("exclusiveMinimum" not in node or value > node["exclusiveMinimum"])
|
|
173
|
+
)
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _equal(value: Any, choice: Any) -> bool:
|
|
178
|
+
return (
|
|
179
|
+
type(value) is type(choice)
|
|
180
|
+
and value == choice
|
|
181
|
+
or (type(value) in (int, float) and type(choice) in (int, float) and value == choice)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _type(value: Any, kind: str) -> bool:
|
|
186
|
+
if kind == "number" or kind == "integer":
|
|
187
|
+
return type(value) in (int, float) and (kind == "number" or value == math.floor(value))
|
|
188
|
+
return type(value) is {"null": type(None), "boolean": bool, "string": str, "array": list, "object": dict}[kind]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@lru_cache(maxsize=1)
|
|
192
|
+
def _engine() -> CanonicalEventSchema:
|
|
193
|
+
return CanonicalEventSchema(json.loads(Path(__file__).with_name("event-envelope.schema.json").read_text()))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def valid_owned_event(value: dict[str, Any]) -> bool:
|
|
197
|
+
try:
|
|
198
|
+
return _engine().accepts(value)
|
|
199
|
+
except ValidationBudgetExceeded:
|
|
200
|
+
raise
|
|
201
|
+
except Exception:
|
|
202
|
+
return False
|
|
@@ -30,14 +30,19 @@ class DebugBundleLogHandler(logging.Handler):
|
|
|
30
30
|
def emit(self, record: logging.LogRecord) -> None:
|
|
31
31
|
if not self._sdk._log_level_eligible(record.levelname.lower()):
|
|
32
32
|
return
|
|
33
|
+
context = {"logger_name": record.name, "pathname": record.pathname, "lineno": record.lineno}
|
|
34
|
+
if (
|
|
35
|
+
record.levelno >= logging.ERROR
|
|
36
|
+
and type(record.exc_info) is tuple
|
|
37
|
+
and len(record.exc_info) == 3
|
|
38
|
+
and isinstance(record.exc_info[1], BaseException)
|
|
39
|
+
):
|
|
40
|
+
self._sdk.capture_exception(record.exc_info[1], {**context, "log_message": safe_log_message(record)})
|
|
41
|
+
return
|
|
33
42
|
self._sdk.capture_log(
|
|
34
43
|
safe_log_message(record),
|
|
35
44
|
level=record.levelname.lower(),
|
|
36
|
-
context=
|
|
37
|
-
"logger_name": record.name,
|
|
38
|
-
"pathname": record.pathname,
|
|
39
|
-
"lineno": record.lineno,
|
|
40
|
-
},
|
|
45
|
+
context=context,
|
|
41
46
|
)
|
|
42
47
|
|
|
43
48
|
|
|
@@ -55,7 +60,10 @@ def safe_log_message(record: logging.LogRecord) -> str:
|
|
|
55
60
|
if type(args) is not tuple or len(args) > 8:
|
|
56
61
|
return template + " [log arguments withheld]"
|
|
57
62
|
safe_args = [
|
|
58
|
-
value[:1024]
|
|
63
|
+
value[:1024]
|
|
64
|
+
if type(value) is str
|
|
65
|
+
else str(value)
|
|
66
|
+
if type(value) in (int, float, bool)
|
|
59
67
|
else "[unsupported argument]"
|
|
60
68
|
for value in args
|
|
61
69
|
]
|