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.
@@ -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