oluso 1.0.0__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.
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ build-and-test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e ".[dev]"
21
+ - run: ruff check src tests
22
+ - run: mypy src
23
+ - run: pytest -v --cov=oluso --cov-report=term-missing
oluso-1.0.0/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ htmlcov/
9
+ build/
10
+ dist/
11
+ .venv/
12
+ venv/
13
+ .DS_Store
oluso-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Ezeh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
oluso-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: oluso
3
+ Version: 1.0.0
4
+ Summary: AI-powered error monitoring for Python applications: automatic error reporting, breadcrumb tracking, and intelligent error grouping.
5
+ Project-URL: Homepage, https://github.com/olusodotdev/oluso-py
6
+ Project-URL: Repository, https://github.com/olusodotdev/oluso-py
7
+ Project-URL: Issues, https://github.com/olusodotdev/oluso-py/issues
8
+ Author: David Ezeh
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,crash-reporting,debugging,error,error-monitoring,error-tracking,logging,monitoring,reporting
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.9
23
+ Provides-Extra: dev
24
+ Requires-Dist: flask>=2.3; extra == 'dev'
25
+ Requires-Dist: mypy; extra == 'dev'
26
+ Requires-Dist: pytest-cov; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Requires-Dist: ruff; extra == 'dev'
29
+ Provides-Extra: flask
30
+ Requires-Dist: flask>=2.3; extra == 'flask'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # oluso-py
34
+
35
+ AI-powered error monitoring for Python applications: automatic error reporting, breadcrumb tracking, and intelligent error grouping.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install oluso
41
+
42
+ # With a framework integration
43
+ pip install "oluso[flask]"
44
+ ```
45
+
46
+ ## Usage with Flask
47
+
48
+ ```python
49
+ from flask import Flask
50
+ from oluso import Oluso, Options
51
+ from oluso.integrations.flask import init_app
52
+
53
+ app = Flask(__name__)
54
+
55
+ client = Oluso(Options(api_key="your-api-key", environment="production"))
56
+ init_app(app, client)
57
+
58
+ @app.route("/")
59
+ def index():
60
+ raise RuntimeError("something went wrong") # captured and reported automatically
61
+ ```
62
+
63
+ `init_app` wraps `app.wsgi_app`, so it works with any WSGI application, not just Flask. It scopes breadcrumbs to each request, auto-reports unhandled exceptions and 5xx responses, then re-raises so Flask/Werkzeug's own error handling still runs — Oluso only observes and reports, it never changes how your app responds to errors.
64
+
65
+ ## Breadcrumbs & User Context
66
+
67
+ Breadcrumbs and user context are scoped per request using `contextvars` — Python's idiomatic equivalent of the `AsyncLocalStorage`-based scoping the Node SDK uses (and Go's `context.Context`), correctly isolated per thread *and* per asyncio task:
68
+
69
+ ```python
70
+ from oluso import add_breadcrumb, set_user
71
+
72
+ @app.route("/checkout")
73
+ def checkout():
74
+ add_breadcrumb("user started checkout", category="action")
75
+ set_user(UserContext(id="user_456"))
76
+
77
+ try:
78
+ do_checkout()
79
+ except Exception as err:
80
+ client.capture_exception(err, {"cartId": "cart_123"})
81
+ ```
82
+
83
+ For non-request work (a background job, a CLI command) where you still want a scope, open one yourself:
84
+
85
+ ```python
86
+ from oluso import scope, add_breadcrumb
87
+
88
+ with scope():
89
+ add_breadcrumb("job started")
90
+ client.capture_exception(err)
91
+ ```
92
+
93
+ ## Manual Reporting
94
+
95
+ ```python
96
+ client.capture_exception(err, {"customMeta": "extra-info"})
97
+ ```
98
+
99
+ ## Advanced Configuration
100
+
101
+ ```python
102
+ from oluso import Oluso, Options, Severity
103
+
104
+ client = Oluso(Options(
105
+ api_key="your-api-key",
106
+ endpoint="https://api.oluso.dev/api/v1/error/report", # override for self-hosting
107
+ environment="staging",
108
+ default_severity=Severity.MEDIUM,
109
+ max_breadcrumbs=50,
110
+ max_errors_per_minute=100,
111
+ sensitive_keys=["ssn", "internal_id"],
112
+ should_report=lambda err: "expected" not in str(err),
113
+ ))
114
+ ```
115
+
116
+ Call `client.flush(timeout=5)` before your process exits so a capture right before shutdown isn't lost.
117
+
118
+ ## Error Report Structure
119
+
120
+ Reports sent to the API include:
121
+
122
+ - **Metadata**: Title, message, stack trace, severity, tags.
123
+ - **Context**: Request details (URL, method, headers, etc.), server details (hostname, Python version, memory, thread count).
124
+ - **History**: Breadcrumbs leading up to the error.
125
+ - **Identification**: Fingerprint for deduplication and user ID.
126
+
127
+ ## License
128
+
129
+ MIT
oluso-1.0.0/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # oluso-py
2
+
3
+ AI-powered error monitoring for Python applications: automatic error reporting, breadcrumb tracking, and intelligent error grouping.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install oluso
9
+
10
+ # With a framework integration
11
+ pip install "oluso[flask]"
12
+ ```
13
+
14
+ ## Usage with Flask
15
+
16
+ ```python
17
+ from flask import Flask
18
+ from oluso import Oluso, Options
19
+ from oluso.integrations.flask import init_app
20
+
21
+ app = Flask(__name__)
22
+
23
+ client = Oluso(Options(api_key="your-api-key", environment="production"))
24
+ init_app(app, client)
25
+
26
+ @app.route("/")
27
+ def index():
28
+ raise RuntimeError("something went wrong") # captured and reported automatically
29
+ ```
30
+
31
+ `init_app` wraps `app.wsgi_app`, so it works with any WSGI application, not just Flask. It scopes breadcrumbs to each request, auto-reports unhandled exceptions and 5xx responses, then re-raises so Flask/Werkzeug's own error handling still runs — Oluso only observes and reports, it never changes how your app responds to errors.
32
+
33
+ ## Breadcrumbs & User Context
34
+
35
+ Breadcrumbs and user context are scoped per request using `contextvars` — Python's idiomatic equivalent of the `AsyncLocalStorage`-based scoping the Node SDK uses (and Go's `context.Context`), correctly isolated per thread *and* per asyncio task:
36
+
37
+ ```python
38
+ from oluso import add_breadcrumb, set_user
39
+
40
+ @app.route("/checkout")
41
+ def checkout():
42
+ add_breadcrumb("user started checkout", category="action")
43
+ set_user(UserContext(id="user_456"))
44
+
45
+ try:
46
+ do_checkout()
47
+ except Exception as err:
48
+ client.capture_exception(err, {"cartId": "cart_123"})
49
+ ```
50
+
51
+ For non-request work (a background job, a CLI command) where you still want a scope, open one yourself:
52
+
53
+ ```python
54
+ from oluso import scope, add_breadcrumb
55
+
56
+ with scope():
57
+ add_breadcrumb("job started")
58
+ client.capture_exception(err)
59
+ ```
60
+
61
+ ## Manual Reporting
62
+
63
+ ```python
64
+ client.capture_exception(err, {"customMeta": "extra-info"})
65
+ ```
66
+
67
+ ## Advanced Configuration
68
+
69
+ ```python
70
+ from oluso import Oluso, Options, Severity
71
+
72
+ client = Oluso(Options(
73
+ api_key="your-api-key",
74
+ endpoint="https://api.oluso.dev/api/v1/error/report", # override for self-hosting
75
+ environment="staging",
76
+ default_severity=Severity.MEDIUM,
77
+ max_breadcrumbs=50,
78
+ max_errors_per_minute=100,
79
+ sensitive_keys=["ssn", "internal_id"],
80
+ should_report=lambda err: "expected" not in str(err),
81
+ ))
82
+ ```
83
+
84
+ Call `client.flush(timeout=5)` before your process exits so a capture right before shutdown isn't lost.
85
+
86
+ ## Error Report Structure
87
+
88
+ Reports sent to the API include:
89
+
90
+ - **Metadata**: Title, message, stack trace, severity, tags.
91
+ - **Context**: Request details (URL, method, headers, etc.), server details (hostname, Python version, memory, thread count).
92
+ - **History**: Breadcrumbs leading up to the error.
93
+ - **Identification**: Fingerprint for deduplication and user ID.
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,50 @@
1
+ """Minimal example of wiring Oluso into a Flask app.
2
+
3
+ pip install "oluso[flask]"
4
+ OLUSO_API_KEY=your-api-key python examples/flask_app.py
5
+ """
6
+
7
+ import atexit
8
+ import os
9
+
10
+ from flask import Flask
11
+
12
+ from oluso import Oluso, Options, add_breadcrumb
13
+ from oluso.integrations.flask import init_app
14
+
15
+ client = Oluso(Options(api_key=os.environ.get("OLUSO_API_KEY", ""), environment="development"))
16
+ atexit.register(lambda: client.flush(timeout=5))
17
+
18
+ app = Flask(__name__)
19
+ init_app(app, client)
20
+
21
+
22
+ @app.route("/")
23
+ def index():
24
+ add_breadcrumb("handling root request")
25
+ return "ok"
26
+
27
+
28
+ @app.route("/boom")
29
+ def boom():
30
+ # Middleware auto-reports both raised exceptions and 5xx responses, so
31
+ # a deliberately failing view needs no manual capture at all.
32
+ raise RuntimeError("something went wrong")
33
+
34
+
35
+ @app.route("/manual")
36
+ def manual():
37
+ try:
38
+ do_work()
39
+ except Exception as err:
40
+ client.capture_exception(err, {"step": "manual example"})
41
+ return "internal error", 500
42
+ return "ok"
43
+
44
+
45
+ def do_work():
46
+ raise ValueError("work failed")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ app.run(port=8080)
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "oluso"
7
+ version = "1.0.0"
8
+ description = "AI-powered error monitoring for Python applications: automatic error reporting, breadcrumb tracking, and intelligent error grouping."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "David Ezeh" }]
12
+ requires-python = ">=3.9"
13
+ keywords = [
14
+ "error",
15
+ "monitoring",
16
+ "reporting",
17
+ "error-tracking",
18
+ "error-monitoring",
19
+ "ai",
20
+ "crash-reporting",
21
+ "logging",
22
+ "debugging",
23
+ ]
24
+ classifiers = [
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Operating System :: OS Independent",
33
+ "Intended Audience :: Developers",
34
+ "Topic :: Software Development :: Libraries :: Python Modules",
35
+ ]
36
+ # Core has zero required third-party dependencies -- it uses only the
37
+ # standard library (urllib for transport, contextvars for scoping), the same
38
+ # "framework-agnostic core, opt-in everything else" design as the Node/Go
39
+ # SDKs. Framework integrations declare their own deps as extras below.
40
+ dependencies = []
41
+
42
+ [project.optional-dependencies]
43
+ flask = ["flask>=2.3"]
44
+ dev = [
45
+ "pytest>=7.0",
46
+ "pytest-cov",
47
+ "flask>=2.3",
48
+ "mypy",
49
+ "ruff",
50
+ ]
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/olusodotdev/oluso-py"
54
+ Repository = "https://github.com/olusodotdev/oluso-py"
55
+ Issues = "https://github.com/olusodotdev/oluso-py/issues"
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/oluso"]
59
+
60
+ [tool.pytest.ini_options]
61
+ testpaths = ["tests"]
62
+
63
+ [tool.ruff]
64
+ line-length = 100
65
+ target-version = "py39"
66
+
67
+ [tool.mypy]
68
+ # The installed mypy version's oldest supported check target; the package's
69
+ # actual floor is still requires-python = ">=3.9" above -- Python 3.9
70
+ # natively supports the PEP 585 generics (dict[str, Any], etc.) used here.
71
+ python_version = "3.10"
72
+ ignore_missing_imports = true
@@ -0,0 +1,34 @@
1
+ """Oluso: AI-powered error monitoring for Python applications."""
2
+
3
+ from .client import Oluso
4
+ from .context import add_breadcrumb, scope, set_custom_context, set_user
5
+ from .options import Options
6
+ from .types import (
7
+ Breadcrumb,
8
+ BreadcrumbLevel,
9
+ ErrorContext,
10
+ ErrorReport,
11
+ RequestContext,
12
+ ServerContext,
13
+ Severity,
14
+ UserContext,
15
+ )
16
+
17
+ __version__ = "1.0.0"
18
+
19
+ __all__ = [
20
+ "Oluso",
21
+ "Options",
22
+ "scope",
23
+ "add_breadcrumb",
24
+ "set_user",
25
+ "set_custom_context",
26
+ "Breadcrumb",
27
+ "BreadcrumbLevel",
28
+ "ErrorContext",
29
+ "ErrorReport",
30
+ "RequestContext",
31
+ "ServerContext",
32
+ "Severity",
33
+ "UserContext",
34
+ ]
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import queue
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from typing import Any, Dict, Optional, Tuple
9
+
10
+ from .context import _snapshot
11
+ from .fingerprint import generate_fingerprint
12
+ from .options import Options
13
+ from .queue import OfflineQueue
14
+ from .rate_limiter import RateLimiter
15
+ from .sanitizer import Sanitizer
16
+ from .server_context import get_server_context
17
+ from .transport import TransportError, send_error_report
18
+ from .types import ErrorContext, ErrorReport, RequestContext, Severity
19
+
20
+ logger = logging.getLogger("oluso")
21
+
22
+
23
+ class Oluso:
24
+ """Reports errors to Oluso.
25
+
26
+ Sends run on a background daemon thread (tracked so `flush()` can drain
27
+ them before shutdown) rather than blocking the calling thread on network
28
+ I/O, since capture calls typically sit on a request-handling hot path.
29
+ """
30
+
31
+ def __init__(self, options: Options) -> None:
32
+ if not options.api_key:
33
+ raise ValueError("oluso: api_key is required")
34
+ self._options = options
35
+ self._sanitizer = Sanitizer(options.sensitive_keys)
36
+ self._rate_limiter = RateLimiter(options.max_errors_per_minute)
37
+ self._offline_queue = OfflineQueue(options.max_queue_size, options.queue_dir)
38
+
39
+ self._send_queue: "queue.Queue[Dict[str, Any]]" = queue.Queue()
40
+ self._pending = 0
41
+ self._pending_lock = threading.Lock()
42
+ self._all_sent = threading.Condition(self._pending_lock)
43
+ self._worker = threading.Thread(target=self._worker_loop, name="oluso-sender", daemon=True)
44
+ self._worker.start()
45
+
46
+ @property
47
+ def max_breadcrumbs(self) -> int:
48
+ """The configured max breadcrumbs per scope, for framework
49
+ integrations that open a scope themselves.
50
+ """
51
+ return self._options.max_breadcrumbs
52
+
53
+ @property
54
+ def sanitizer(self) -> Sanitizer:
55
+ """The Sanitizer configured for this client (respects
56
+ Options.sensitive_keys), for framework integrations that build a
57
+ RequestContext manually.
58
+ """
59
+ return self._sanitizer
60
+
61
+ def capture_exception(
62
+ self, error: BaseException, custom_context: Optional[Dict[str, Any]] = None
63
+ ) -> None:
64
+ """Report error, attaching any breadcrumbs/user/custom context on the
65
+ current scope (see `oluso.scope`), plus any custom_context given
66
+ here.
67
+ """
68
+ self._capture(error, http_info=None, custom_context=custom_context)
69
+
70
+ def capture_http_error(
71
+ self,
72
+ error: BaseException,
73
+ request_context: RequestContext,
74
+ status_code: int,
75
+ custom_context: Optional[Dict[str, Any]] = None,
76
+ ) -> None:
77
+ """Like capture_exception but also attaches HTTP request context and
78
+ a response status code, for reporting an error with request context
79
+ from a framework integration.
80
+ """
81
+ self._capture(error, http_info=(request_context, status_code), custom_context=custom_context)
82
+
83
+ def _capture(
84
+ self,
85
+ error: BaseException,
86
+ http_info: Optional[Tuple[RequestContext, int]],
87
+ custom_context: Optional[Dict[str, Any]],
88
+ ) -> None:
89
+ if error is None:
90
+ return
91
+ if self._options.should_report and not self._options.should_report(error):
92
+ return
93
+ if not self._rate_limiter.can_send():
94
+ if self._options.log_to_console:
95
+ logger.warning("[Oluso] rate limit exceeded, error not reported")
96
+ return
97
+ if self._options.log_to_console:
98
+ logger.error("[Oluso] %s", error)
99
+
100
+ stack_trace = "".join(
101
+ traceback.format_exception(type(error), error, error.__traceback__)
102
+ )
103
+ err_ctx = self._build_error_context(http_info, custom_context)
104
+
105
+ if self._options.fingerprint:
106
+ fingerprint = self._options.fingerprint(error, err_ctx)
107
+ else:
108
+ fingerprint = generate_fingerprint(error, stack_trace)
109
+
110
+ severity = self._options.default_severity
111
+ if http_info is not None:
112
+ _, status_code = http_info
113
+ if status_code >= 500:
114
+ severity = Severity.CRITICAL
115
+ elif status_code >= 400:
116
+ severity = Severity.HIGH
117
+
118
+ report = ErrorReport(
119
+ title=_error_title(error),
120
+ message=str(error),
121
+ stack_trace=stack_trace,
122
+ environment=self._options.environment,
123
+ severity=severity.value if isinstance(severity, Severity) else severity,
124
+ tags=list(self._options.tags),
125
+ fingerprint=fingerprint,
126
+ context=err_ctx,
127
+ timestamp=int(time.time() * 1000),
128
+ )
129
+
130
+ with self._pending_lock:
131
+ self._pending += 1
132
+ self._send_queue.put(report.to_dict())
133
+
134
+ def _build_error_context(
135
+ self,
136
+ http_info: Optional[Tuple[RequestContext, int]],
137
+ custom_context: Optional[Dict[str, Any]],
138
+ ) -> ErrorContext:
139
+ breadcrumbs, user, custom = _snapshot()
140
+
141
+ if custom_context:
142
+ custom = {**custom, **custom_context}
143
+
144
+ err_ctx = ErrorContext(
145
+ server=get_server_context(),
146
+ user=user,
147
+ custom=custom,
148
+ breadcrumbs=breadcrumbs,
149
+ )
150
+
151
+ if http_info is not None:
152
+ err_ctx.request = http_info[0]
153
+
154
+ return err_ctx
155
+
156
+ def _worker_loop(self) -> None:
157
+ while True:
158
+ report = self._send_queue.get()
159
+ try:
160
+ self._send_report(report)
161
+ finally:
162
+ with self._pending_lock:
163
+ self._pending -= 1
164
+ if self._pending <= 0:
165
+ self._all_sent.notify_all()
166
+ self._send_queue.task_done()
167
+
168
+ def _send_report(self, report: Dict[str, Any]) -> None:
169
+ try:
170
+ send_error_report(
171
+ self._options.endpoint, report, self._options.api_key, self._options.timeout
172
+ )
173
+ except TransportError as e:
174
+ if self._options.log_to_console:
175
+ logger.error("[Oluso] failed to send error report: %s", e)
176
+ if self._options.enable_offline_queue:
177
+ self._offline_queue.enqueue(report)
178
+ return
179
+
180
+ if self._options.enable_offline_queue and not self._offline_queue.is_empty():
181
+ self._offline_queue.process_queue(
182
+ lambda r: send_error_report(
183
+ self._options.endpoint, r, self._options.api_key, self._options.timeout
184
+ )
185
+ )
186
+
187
+ def flush(self, timeout: Optional[float] = None) -> bool:
188
+ """Block until all in-flight and queued reports have been sent, or
189
+ timeout elapses (None waits indefinitely). Returns True if everything
190
+ was flushed before the timeout. Call this before your process exits
191
+ so a capture right before shutdown isn't lost.
192
+ """
193
+ with self._pending_lock:
194
+ deadline = None if timeout is None else time.time() + timeout
195
+ while self._pending > 0:
196
+ remaining = None if deadline is None else max(0.0, deadline - time.time())
197
+ if remaining == 0.0:
198
+ return False
199
+ if not self._all_sent.wait(remaining):
200
+ return False
201
+
202
+ if self._options.enable_offline_queue and not self._offline_queue.is_empty():
203
+ self._offline_queue.process_queue(
204
+ lambda r: send_error_report(
205
+ self._options.endpoint, r, self._options.api_key, self._options.timeout
206
+ )
207
+ )
208
+ return True
209
+
210
+
211
+ def _error_title(error: BaseException) -> str:
212
+ message = str(error)
213
+ first_line = message.splitlines()[0].strip() if message else ""
214
+ if first_line:
215
+ return first_line if len(first_line) <= 100 else first_line[:97] + "..."
216
+ return f"{type(error).__name__} error"