debugbundle-python 0.1.0__py3-none-any.whl
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/__init__.py +165 -0
- debugbundle/config.py +192 -0
- debugbundle/core.py +811 -0
- debugbundle/integrations/__init__.py +16 -0
- debugbundle/integrations/common.py +77 -0
- debugbundle/integrations/django.py +53 -0
- debugbundle/integrations/fastapi.py +94 -0
- debugbundle/integrations/flask.py +67 -0
- debugbundle/integrations/relay_django.py +50 -0
- debugbundle/integrations/relay_fastapi.py +46 -0
- debugbundle/integrations/relay_flask.py +46 -0
- debugbundle/logger_integrations.py +154 -0
- debugbundle/py.typed +0 -0
- debugbundle/redaction.py +29 -0
- debugbundle/relay.py +261 -0
- debugbundle/suppression.py +124 -0
- debugbundle/transport.py +59 -0
- debugbundle/trigger_token.py +143 -0
- debugbundle_python-0.1.0.dist-info/METADATA +66 -0
- debugbundle_python-0.1.0.dist-info/RECORD +23 -0
- debugbundle_python-0.1.0.dist-info/WHEEL +5 -0
- debugbundle_python-0.1.0.dist-info/licenses/LICENSE +17 -0
- debugbundle_python-0.1.0.dist-info/top_level.txt +1 -0
debugbundle/__init__.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""DebugBundle Python SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from contextvars import Token
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .config import RemoteProbeDirective
|
|
11
|
+
from .core import ConfigFetchResponse, DebugBundleSdk
|
|
12
|
+
from .relay import BrowserRelayAcceptedBatch, BrowserRelayHandler, BrowserRelayResponse
|
|
13
|
+
|
|
14
|
+
_sdk = DebugBundleSdk()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def init(
|
|
18
|
+
project_token: str,
|
|
19
|
+
environment: str | None = None,
|
|
20
|
+
service: str | None = None,
|
|
21
|
+
enabled: bool = True,
|
|
22
|
+
redact_fields: list[str] | None = None,
|
|
23
|
+
sample_rate: float = 1.0,
|
|
24
|
+
batch_size: int = 25,
|
|
25
|
+
flush_interval: float = 5.0,
|
|
26
|
+
endpoint: str = "https://api.debugbundle.com/v1/events",
|
|
27
|
+
log_level: str = "warning",
|
|
28
|
+
max_probe_labels: int = 50,
|
|
29
|
+
max_probe_entries_per_label: int = 10,
|
|
30
|
+
probe_flush_on_error: bool = True,
|
|
31
|
+
fetch_impl: Callable[[str, dict[str, object]], ConfigFetchResponse] | None = None,
|
|
32
|
+
on_diagnostic: Callable[[dict[str, object]], None] | None = None,
|
|
33
|
+
probes_poll_interval: int = 60_000,
|
|
34
|
+
) -> None:
|
|
35
|
+
_sdk.init(
|
|
36
|
+
project_token=project_token,
|
|
37
|
+
environment=environment,
|
|
38
|
+
service=service,
|
|
39
|
+
enabled=enabled,
|
|
40
|
+
redact_fields=redact_fields,
|
|
41
|
+
sample_rate=sample_rate,
|
|
42
|
+
batch_size=batch_size,
|
|
43
|
+
flush_interval=flush_interval,
|
|
44
|
+
endpoint=endpoint,
|
|
45
|
+
log_level=log_level,
|
|
46
|
+
max_probe_labels=max_probe_labels,
|
|
47
|
+
max_probe_entries_per_label=max_probe_entries_per_label,
|
|
48
|
+
probe_flush_on_error=probe_flush_on_error,
|
|
49
|
+
fetch_impl=fetch_impl,
|
|
50
|
+
on_diagnostic=on_diagnostic,
|
|
51
|
+
probes_poll_interval=probes_poll_interval,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def capture_exception(error: BaseException, context: dict[str, object] | None = None) -> None:
|
|
56
|
+
_sdk.capture_exception(error, context=context)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def capture_error(error: BaseException, context: dict[str, object] | None = None) -> None:
|
|
60
|
+
_sdk.capture_error(error, context=context)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def capture_log(message: str, level: str = "warning", context: dict[str, object] | None = None) -> None:
|
|
64
|
+
_sdk.capture_log(message, level=level, context=context)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def capture_request(
|
|
68
|
+
request: Mapping[str, object],
|
|
69
|
+
response: Mapping[str, object] | None = None,
|
|
70
|
+
context: Mapping[str, object] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
_sdk.capture_request(request, response=response, context=context)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def capture_message(message: str, level: str | None = None, context: dict[str, object] | None = None) -> None:
|
|
76
|
+
_sdk.capture_message(message, level=level, context=context)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_context(key: str, value: object) -> None:
|
|
80
|
+
_sdk.set_context(key, value)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def flush() -> None:
|
|
84
|
+
_sdk.flush()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_status() -> str:
|
|
88
|
+
return _sdk.status
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_last_event_at() -> float | None:
|
|
92
|
+
return _sdk.last_event_at
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def probe(label: str, data: object | Callable[[], object], opts: Mapping[str, object] | None = None) -> None:
|
|
96
|
+
_sdk.probe(label, data, opts=opts)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def capture_exceptions() -> None:
|
|
100
|
+
_sdk.capture_exceptions()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def capture_logging(logger: logging.Logger | None = None) -> None:
|
|
104
|
+
_sdk.capture_logging(logger=logger)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def capture_async(loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
108
|
+
_sdk.capture_async(loop=loop)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def begin_request(request: dict) -> object:
|
|
112
|
+
return _sdk.begin_request(request)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def end_request(token: Token[list[RemoteProbeDirective] | None]) -> None:
|
|
116
|
+
_sdk.end_request(token)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_OPTIONAL_EXPORTS = {
|
|
120
|
+
"DebugBundleDjangoMiddleware": (".integrations.django", "DebugBundleDjangoMiddleware"),
|
|
121
|
+
"DebugBundleFastAPIMiddleware": (".integrations.fastapi", "DebugBundleFastAPIMiddleware"),
|
|
122
|
+
"create_django_relay_view": (".integrations.relay_django", "create_django_relay_view"),
|
|
123
|
+
"create_fastapi_relay_handler": (".integrations.relay_fastapi", "create_fastapi_relay_handler"),
|
|
124
|
+
"create_flask_relay_handler": (".integrations.relay_flask", "create_flask_relay_handler"),
|
|
125
|
+
"instrument_fastapi": (".integrations.fastapi", "instrument_fastapi"),
|
|
126
|
+
"instrument_flask": (".integrations.flask", "instrument_flask"),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def __getattr__(name: str) -> Any:
|
|
131
|
+
if name not in _OPTIONAL_EXPORTS:
|
|
132
|
+
raise AttributeError(f"module 'debugbundle' has no attribute {name!r}")
|
|
133
|
+
|
|
134
|
+
module_name, attribute_name = _OPTIONAL_EXPORTS[name]
|
|
135
|
+
module = importlib.import_module(module_name, __name__)
|
|
136
|
+
return getattr(module, attribute_name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"BrowserRelayAcceptedBatch",
|
|
141
|
+
"BrowserRelayHandler",
|
|
142
|
+
"BrowserRelayResponse",
|
|
143
|
+
"DebugBundleSdk",
|
|
144
|
+
"DebugBundleDjangoMiddleware",
|
|
145
|
+
"DebugBundleFastAPIMiddleware",
|
|
146
|
+
"begin_request",
|
|
147
|
+
"capture_async",
|
|
148
|
+
"capture_error",
|
|
149
|
+
"capture_exception",
|
|
150
|
+
"capture_exceptions",
|
|
151
|
+
"capture_log",
|
|
152
|
+
"capture_logging",
|
|
153
|
+
"capture_message",
|
|
154
|
+
"capture_request",
|
|
155
|
+
"create_django_relay_view",
|
|
156
|
+
"create_fastapi_relay_handler",
|
|
157
|
+
"create_flask_relay_handler",
|
|
158
|
+
"end_request",
|
|
159
|
+
"flush",
|
|
160
|
+
"init",
|
|
161
|
+
"instrument_fastapi",
|
|
162
|
+
"instrument_flask",
|
|
163
|
+
"probe",
|
|
164
|
+
"set_context",
|
|
165
|
+
]
|
debugbundle/config.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
DEFAULT_PROBES_POLL_INTERVAL_MS = 60_000
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CapturePolicy:
|
|
11
|
+
preset: str
|
|
12
|
+
capture_logs: str
|
|
13
|
+
capture_request_events: str
|
|
14
|
+
capture_breadcrumbs: str
|
|
15
|
+
capture_probe_events: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RemoteProbeDirective:
|
|
20
|
+
id: str
|
|
21
|
+
label_pattern: str
|
|
22
|
+
service: str
|
|
23
|
+
environment: str
|
|
24
|
+
expires_at: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RemoteConfigSnapshot:
|
|
29
|
+
probes_enabled: bool
|
|
30
|
+
remote_probes_enabled: bool
|
|
31
|
+
directives: list[RemoteProbeDirective]
|
|
32
|
+
poll_interval_ms: int
|
|
33
|
+
capture_policy: CapturePolicy
|
|
34
|
+
trigger_token_key: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
BALANCED_CAPTURE_POLICY = CapturePolicy(
|
|
38
|
+
preset="balanced",
|
|
39
|
+
capture_logs="warning",
|
|
40
|
+
capture_request_events="all",
|
|
41
|
+
capture_breadcrumbs="local_only",
|
|
42
|
+
capture_probe_events="standalone_when_activated",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
MINIMAL_CAPTURE_POLICY = CapturePolicy(
|
|
46
|
+
preset="minimal",
|
|
47
|
+
capture_logs="warning",
|
|
48
|
+
capture_request_events="off",
|
|
49
|
+
capture_breadcrumbs="local_only",
|
|
50
|
+
capture_probe_events="buffer_only",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_remote_config(payload: object, fallback_poll_interval_ms: int, now_ms: int) -> RemoteConfigSnapshot | None:
|
|
55
|
+
if not isinstance(payload, dict):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
probes_enabled = payload.get("probes_enabled") is True
|
|
59
|
+
remote_probes_enabled = payload.get("remote_probes_enabled") is True
|
|
60
|
+
poll_interval_candidate = payload.get("poll_interval_ms")
|
|
61
|
+
poll_interval_ms = (
|
|
62
|
+
int(poll_interval_candidate)
|
|
63
|
+
if isinstance(poll_interval_candidate, (int, float)) and int(poll_interval_candidate) > 0
|
|
64
|
+
else fallback_poll_interval_ms
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
directives: list[RemoteProbeDirective] = []
|
|
68
|
+
active_probes = payload.get("active_probes")
|
|
69
|
+
if isinstance(active_probes, list):
|
|
70
|
+
for directive in active_probes:
|
|
71
|
+
parsed = _parse_directive(directive)
|
|
72
|
+
if parsed is not None and _expires_at_ms(parsed.expires_at) > now_ms:
|
|
73
|
+
directives.append(parsed)
|
|
74
|
+
|
|
75
|
+
capture_policy = _parse_capture_policy(payload.get("capture_policy"))
|
|
76
|
+
if capture_policy is None:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return RemoteConfigSnapshot(
|
|
80
|
+
probes_enabled=probes_enabled,
|
|
81
|
+
remote_probes_enabled=remote_probes_enabled,
|
|
82
|
+
directives=directives,
|
|
83
|
+
poll_interval_ms=poll_interval_ms if remote_probes_enabled else DEFAULT_PROBES_POLL_INTERVAL_MS,
|
|
84
|
+
capture_policy=capture_policy,
|
|
85
|
+
trigger_token_key=_as_non_empty_string(payload.get("trigger_token_key")),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def find_matching_remote_probe_directives(
|
|
90
|
+
directives: list[RemoteProbeDirective],
|
|
91
|
+
label: str,
|
|
92
|
+
service: str,
|
|
93
|
+
environment: str,
|
|
94
|
+
now_ms: int,
|
|
95
|
+
) -> list[RemoteProbeDirective]:
|
|
96
|
+
matches: list[RemoteProbeDirective] = []
|
|
97
|
+
for directive in directives:
|
|
98
|
+
if _expires_at_ms(directive.expires_at) <= now_ms:
|
|
99
|
+
continue
|
|
100
|
+
if directive.service != "*" and directive.service != service:
|
|
101
|
+
continue
|
|
102
|
+
if directive.environment != "*" and directive.environment != environment:
|
|
103
|
+
continue
|
|
104
|
+
if _matches_label_pattern(directive.label_pattern, label):
|
|
105
|
+
matches.append(directive)
|
|
106
|
+
return matches
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
110
|
+
if payload is None:
|
|
111
|
+
return BALANCED_CAPTURE_POLICY
|
|
112
|
+
if not isinstance(payload, dict):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
preset = _as_non_empty_string(payload.get("preset")) or BALANCED_CAPTURE_POLICY.preset
|
|
116
|
+
capture_logs = _as_non_empty_string(payload.get("capture_logs"))
|
|
117
|
+
capture_request_events = _as_non_empty_string(payload.get("capture_request_events"))
|
|
118
|
+
capture_breadcrumbs = _as_non_empty_string(payload.get("capture_breadcrumbs"))
|
|
119
|
+
capture_probe_events = _as_non_empty_string(payload.get("capture_probe_events"))
|
|
120
|
+
|
|
121
|
+
if capture_logs not in {"off", "error", "warning", "info"}:
|
|
122
|
+
return None
|
|
123
|
+
if capture_request_events not in {"off", "failures_only", "filtered", "all"}:
|
|
124
|
+
return None
|
|
125
|
+
if capture_breadcrumbs not in {"local_only", "exception_only", "standalone"}:
|
|
126
|
+
return None
|
|
127
|
+
if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
return CapturePolicy(
|
|
131
|
+
preset=preset,
|
|
132
|
+
capture_logs=capture_logs,
|
|
133
|
+
capture_request_events=capture_request_events,
|
|
134
|
+
capture_breadcrumbs=capture_breadcrumbs,
|
|
135
|
+
capture_probe_events=capture_probe_events,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_directive(payload: object) -> RemoteProbeDirective | None:
|
|
140
|
+
if not isinstance(payload, dict):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
directive_id = _as_non_empty_string(payload.get("id"))
|
|
144
|
+
label_pattern = _as_non_empty_string(payload.get("label_pattern"))
|
|
145
|
+
service = _as_non_empty_string(payload.get("service"))
|
|
146
|
+
environment = _as_non_empty_string(payload.get("environment"))
|
|
147
|
+
expires_at = _as_non_empty_string(payload.get("expires_at"))
|
|
148
|
+
|
|
149
|
+
if directive_id is None:
|
|
150
|
+
return None
|
|
151
|
+
if label_pattern is None:
|
|
152
|
+
return None
|
|
153
|
+
if service is None:
|
|
154
|
+
return None
|
|
155
|
+
if environment is None:
|
|
156
|
+
return None
|
|
157
|
+
if expires_at is None:
|
|
158
|
+
return None
|
|
159
|
+
if _expires_at_ms(expires_at) == 0:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
return RemoteProbeDirective(
|
|
163
|
+
id=directive_id,
|
|
164
|
+
label_pattern=label_pattern,
|
|
165
|
+
service=service,
|
|
166
|
+
environment=environment,
|
|
167
|
+
expires_at=expires_at,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _matches_label_pattern(pattern: str, label: str) -> bool:
|
|
172
|
+
if pattern == "*":
|
|
173
|
+
return True
|
|
174
|
+
if pattern.endswith(".*"):
|
|
175
|
+
prefix = pattern[:-2]
|
|
176
|
+
return label == prefix or label.startswith(f"{prefix}.")
|
|
177
|
+
return pattern == label
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _expires_at_ms(value: str) -> int:
|
|
181
|
+
from datetime import datetime
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if value.endswith("Z"):
|
|
185
|
+
value = value[:-1] + "+00:00"
|
|
186
|
+
return int(datetime.fromisoformat(value).timestamp() * 1000)
|
|
187
|
+
except ValueError:
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _as_non_empty_string(value: Any) -> str | None:
|
|
192
|
+
return value if isinstance(value, str) and len(value) > 0 else None
|