bidar 0.1.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.
- bidar-0.1.0/LICENSE +17 -0
- bidar-0.1.0/PKG-INFO +156 -0
- bidar-0.1.0/README.md +135 -0
- bidar-0.1.0/bidar/__init__.py +23 -0
- bidar-0.1.0/bidar/client.py +241 -0
- bidar-0.1.0/bidar/config.py +33 -0
- bidar-0.1.0/bidar/middleware.py +80 -0
- bidar-0.1.0/bidar/transport.py +33 -0
- bidar-0.1.0/bidar/utils/__init__.py +0 -0
- bidar-0.1.0/bidar/utils/sanitizers.py +28 -0
- bidar-0.1.0/bidar.egg-info/PKG-INFO +156 -0
- bidar-0.1.0/bidar.egg-info/SOURCES.txt +18 -0
- bidar-0.1.0/bidar.egg-info/dependency_links.txt +1 -0
- bidar-0.1.0/bidar.egg-info/requires.txt +8 -0
- bidar-0.1.0/bidar.egg-info/top_level.txt +1 -0
- bidar-0.1.0/pyproject.toml +26 -0
- bidar-0.1.0/setup.cfg +4 -0
- bidar-0.1.0/tests/test_client.py +58 -0
- bidar-0.1.0/tests/test_config.py +13 -0
- bidar-0.1.0/tests/test_sanitizers.py +13 -0
bidar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CyberHuginn
|
|
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.
|
bidar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bidar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Background error reporting SDK for Python and Django
|
|
5
|
+
Author-email: CyberHuginn <contact@cyberhuginn.com>
|
|
6
|
+
Maintainer-email: CyberHuginn <contact@cyberhuginn.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://cyberhuginn.com
|
|
9
|
+
Project-URL: Organization, https://fardadnoavar.ir
|
|
10
|
+
Keywords: bidar,error-reporting,django,monitoring,sentry
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Provides-Extra: django
|
|
16
|
+
Requires-Dist: Django>=3.2; extra == "django"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-mock>=3; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# bidar-sdk-python
|
|
23
|
+
|
|
24
|
+
A lightweight error and event reporting SDK for Python applications, with optional Django integration. Reports are submitted in the background so application request handling does not wait for the remote API.
|
|
25
|
+
|
|
26
|
+
**Author:** CyberHuginn
|
|
27
|
+
**Organization:** Fardad Noavar
|
|
28
|
+
**Website:** [cyberhuginn.com](https://cyberhuginn.com)
|
|
29
|
+
**Organization website:** [fardadnoavar.ir](https://fardadnoavar.ir)
|
|
30
|
+
**Contact:** contact@cyberhuginn.com
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install bidar-sdk-python
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For Django projects:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "bidar-sdk-python[django]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import bidar
|
|
48
|
+
|
|
49
|
+
bidar.init(
|
|
50
|
+
url="https://api.bidarhq.ir/api/v1/errors/",
|
|
51
|
+
access_key="YOUR_ACCESS_KEY",
|
|
52
|
+
project_name="billing-api",
|
|
53
|
+
environment="Production",
|
|
54
|
+
log_level="error",
|
|
55
|
+
timeout=3.0,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
run_job()
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
bidar.capture_exception(exc)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Reports are queued on a background worker. `capture_exception`, `capture_message`, and `capture_event` return immediately with a boolean indicating whether the event was accepted for processing.
|
|
65
|
+
|
|
66
|
+
## Django integration
|
|
67
|
+
|
|
68
|
+
Add the middleware to `settings.py`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
MIDDLEWARE = [
|
|
72
|
+
# ...
|
|
73
|
+
"bidar.middleware.BidarMiddleware",
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Configure the SDK:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
BIDAR_SDK = {
|
|
81
|
+
"URL": "https://api.bidarhq.ir/api/v1/errors/",
|
|
82
|
+
"ACCESS_KEY": "YOUR_ACCESS_KEY",
|
|
83
|
+
"PROJECT_NAME": "my-django-app",
|
|
84
|
+
"ENVIRONMENT": "Production",
|
|
85
|
+
"LOG_LEVEL": "error",
|
|
86
|
+
"TIMEOUT": 3.0,
|
|
87
|
+
"ENABLED": True,
|
|
88
|
+
"MIDDLEWARE_ENABLED": True,
|
|
89
|
+
"CAPTURE_REQUEST_BODY": False,
|
|
90
|
+
"DEDUPLICATION_WINDOW": 30,
|
|
91
|
+
"MAX_WORKERS": 2,
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The middleware reports unhandled exceptions and re-raises the original exception. Set `MIDDLEWARE_ENABLED` to `False` to disable automatic middleware reporting without removing the middleware from `MIDDLEWARE`.
|
|
96
|
+
|
|
97
|
+
The SDK is initialized lazily from `BIDAR_SDK` when the middleware first runs. You can also call `bidar_sdk.init(...)` explicitly during application startup.
|
|
98
|
+
|
|
99
|
+
## Manual reporting
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import bidar
|
|
103
|
+
|
|
104
|
+
bidar.capture_exception(error)
|
|
105
|
+
|
|
106
|
+
bidar.capture_message(
|
|
107
|
+
"Payment provider is unavailable",
|
|
108
|
+
level="warning",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
bidar.capture_event(
|
|
112
|
+
{"event": "subscription.renewal_failed", "customer_id": 42},
|
|
113
|
+
level="error",
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
| Option | Default | Description |
|
|
120
|
+
|---|---:|---|
|
|
121
|
+
| `url` | Required | HTTP endpoint that receives JSON POST reports |
|
|
122
|
+
| `access_key` | Required | API key sent in `X-Bidar-Access-Key` |
|
|
123
|
+
| `log_level` | `error` | One of `info`, `warning`, or `error` |
|
|
124
|
+
| `project_name` | `None` | Project identifier attached to every event |
|
|
125
|
+
| `environment` | `Production` | Deployment environment |
|
|
126
|
+
| `timeout` | `3.0` | Outbound HTTP timeout in seconds |
|
|
127
|
+
| `enabled` | `True` | Enables or disables all reporting |
|
|
128
|
+
| `middleware_enabled` | `True` | Enables or disables Django middleware reporting |
|
|
129
|
+
| `capture_request_body` | `False` | Opt-in request body capture |
|
|
130
|
+
| `deduplication_window` | `30` | Suppresses identical events within this many seconds; set to `0` to disable |
|
|
131
|
+
| `max_workers` | `2` | Number of background HTTP workers |
|
|
132
|
+
|
|
133
|
+
Log levels are threshold-based: `info` accepts all levels, `warning` accepts warnings and errors, and `error` accepts errors only.
|
|
134
|
+
|
|
135
|
+
## Reliability and privacy
|
|
136
|
+
|
|
137
|
+
- Network requests run in a background thread pool.
|
|
138
|
+
- HTTP failures, serialization errors, and worker failures are contained and do not propagate into application code.
|
|
139
|
+
- `flush()` waits for submitted reports; `shutdown()` flushes and closes the client.
|
|
140
|
+
- Duplicate reports with the same event fingerprint are suppressed during the configured deduplication window.
|
|
141
|
+
- Authorization, cookies, passwords, tokens, secrets, and API-key-like fields are redacted.
|
|
142
|
+
- Request bodies are not captured by default. Enable `CAPTURE_REQUEST_BODY` only after reviewing the data your application sends.
|
|
143
|
+
- The SDK does not intentionally collect local variables or environment variables.
|
|
144
|
+
|
|
145
|
+
## Lifecycle
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
bidar_sdk.flush()
|
|
149
|
+
bidar_sdk.shutdown()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Call `flush()` during controlled shutdown if pending reports should be given a chance to finish. `shutdown()` prevents further submissions until the SDK is initialized again.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
bidar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# bidar-sdk-python
|
|
2
|
+
|
|
3
|
+
A lightweight error and event reporting SDK for Python applications, with optional Django integration. Reports are submitted in the background so application request handling does not wait for the remote API.
|
|
4
|
+
|
|
5
|
+
**Author:** CyberHuginn
|
|
6
|
+
**Organization:** Fardad Noavar
|
|
7
|
+
**Website:** [cyberhuginn.com](https://cyberhuginn.com)
|
|
8
|
+
**Organization website:** [fardadnoavar.ir](https://fardadnoavar.ir)
|
|
9
|
+
**Contact:** contact@cyberhuginn.com
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install bidar-sdk-python
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For Django projects:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install "bidar-sdk-python[django]"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import bidar
|
|
27
|
+
|
|
28
|
+
bidar.init(
|
|
29
|
+
url="https://api.bidarhq.ir/api/v1/errors/",
|
|
30
|
+
access_key="YOUR_ACCESS_KEY",
|
|
31
|
+
project_name="billing-api",
|
|
32
|
+
environment="Production",
|
|
33
|
+
log_level="error",
|
|
34
|
+
timeout=3.0,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
run_job()
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
bidar.capture_exception(exc)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Reports are queued on a background worker. `capture_exception`, `capture_message`, and `capture_event` return immediately with a boolean indicating whether the event was accepted for processing.
|
|
44
|
+
|
|
45
|
+
## Django integration
|
|
46
|
+
|
|
47
|
+
Add the middleware to `settings.py`:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
MIDDLEWARE = [
|
|
51
|
+
# ...
|
|
52
|
+
"bidar.middleware.BidarMiddleware",
|
|
53
|
+
]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Configure the SDK:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
BIDAR_SDK = {
|
|
60
|
+
"URL": "https://api.bidarhq.ir/api/v1/errors/",
|
|
61
|
+
"ACCESS_KEY": "YOUR_ACCESS_KEY",
|
|
62
|
+
"PROJECT_NAME": "my-django-app",
|
|
63
|
+
"ENVIRONMENT": "Production",
|
|
64
|
+
"LOG_LEVEL": "error",
|
|
65
|
+
"TIMEOUT": 3.0,
|
|
66
|
+
"ENABLED": True,
|
|
67
|
+
"MIDDLEWARE_ENABLED": True,
|
|
68
|
+
"CAPTURE_REQUEST_BODY": False,
|
|
69
|
+
"DEDUPLICATION_WINDOW": 30,
|
|
70
|
+
"MAX_WORKERS": 2,
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The middleware reports unhandled exceptions and re-raises the original exception. Set `MIDDLEWARE_ENABLED` to `False` to disable automatic middleware reporting without removing the middleware from `MIDDLEWARE`.
|
|
75
|
+
|
|
76
|
+
The SDK is initialized lazily from `BIDAR_SDK` when the middleware first runs. You can also call `bidar_sdk.init(...)` explicitly during application startup.
|
|
77
|
+
|
|
78
|
+
## Manual reporting
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import bidar
|
|
82
|
+
|
|
83
|
+
bidar.capture_exception(error)
|
|
84
|
+
|
|
85
|
+
bidar.capture_message(
|
|
86
|
+
"Payment provider is unavailable",
|
|
87
|
+
level="warning",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
bidar.capture_event(
|
|
91
|
+
{"event": "subscription.renewal_failed", "customer_id": 42},
|
|
92
|
+
level="error",
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
| Option | Default | Description |
|
|
99
|
+
|---|---:|---|
|
|
100
|
+
| `url` | Required | HTTP endpoint that receives JSON POST reports |
|
|
101
|
+
| `access_key` | Required | API key sent in `X-Bidar-Access-Key` |
|
|
102
|
+
| `log_level` | `error` | One of `info`, `warning`, or `error` |
|
|
103
|
+
| `project_name` | `None` | Project identifier attached to every event |
|
|
104
|
+
| `environment` | `Production` | Deployment environment |
|
|
105
|
+
| `timeout` | `3.0` | Outbound HTTP timeout in seconds |
|
|
106
|
+
| `enabled` | `True` | Enables or disables all reporting |
|
|
107
|
+
| `middleware_enabled` | `True` | Enables or disables Django middleware reporting |
|
|
108
|
+
| `capture_request_body` | `False` | Opt-in request body capture |
|
|
109
|
+
| `deduplication_window` | `30` | Suppresses identical events within this many seconds; set to `0` to disable |
|
|
110
|
+
| `max_workers` | `2` | Number of background HTTP workers |
|
|
111
|
+
|
|
112
|
+
Log levels are threshold-based: `info` accepts all levels, `warning` accepts warnings and errors, and `error` accepts errors only.
|
|
113
|
+
|
|
114
|
+
## Reliability and privacy
|
|
115
|
+
|
|
116
|
+
- Network requests run in a background thread pool.
|
|
117
|
+
- HTTP failures, serialization errors, and worker failures are contained and do not propagate into application code.
|
|
118
|
+
- `flush()` waits for submitted reports; `shutdown()` flushes and closes the client.
|
|
119
|
+
- Duplicate reports with the same event fingerprint are suppressed during the configured deduplication window.
|
|
120
|
+
- Authorization, cookies, passwords, tokens, secrets, and API-key-like fields are redacted.
|
|
121
|
+
- Request bodies are not captured by default. Enable `CAPTURE_REQUEST_BODY` only after reviewing the data your application sends.
|
|
122
|
+
- The SDK does not intentionally collect local variables or environment variables.
|
|
123
|
+
|
|
124
|
+
## Lifecycle
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
bidar_sdk.flush()
|
|
128
|
+
bidar_sdk.shutdown()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Call `flush()` during controlled shutdown if pending reports should be given a chance to finish. `shutdown()` prevents further submissions until the SDK is initialized again.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
BidarClient,
|
|
3
|
+
capture_event,
|
|
4
|
+
capture_exception,
|
|
5
|
+
capture_message,
|
|
6
|
+
flush,
|
|
7
|
+
get_client,
|
|
8
|
+
init,
|
|
9
|
+
shutdown,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BidarClient",
|
|
14
|
+
"init",
|
|
15
|
+
"get_client",
|
|
16
|
+
"capture_exception",
|
|
17
|
+
"capture_message",
|
|
18
|
+
"capture_event",
|
|
19
|
+
"flush",
|
|
20
|
+
"shutdown",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import traceback
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, wait
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from time import monotonic
|
|
11
|
+
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .transport import Transport
|
|
14
|
+
from .utils.sanitizers import redact_mapping
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("bidar")
|
|
17
|
+
_LEVELS = {"info": 10, "warning": 20, "error": 30}
|
|
18
|
+
_client = None
|
|
19
|
+
_client_lock = threading.RLock()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BidarClient:
|
|
23
|
+
def __init__(self, config):
|
|
24
|
+
self.config = config
|
|
25
|
+
self.transport = Transport(config)
|
|
26
|
+
self.executor = ThreadPoolExecutor(
|
|
27
|
+
max_workers=config.max_workers,
|
|
28
|
+
thread_name_prefix="bidar-sdk",
|
|
29
|
+
)
|
|
30
|
+
self._futures = set()
|
|
31
|
+
self._lock = threading.RLock()
|
|
32
|
+
self._recent_events = {}
|
|
33
|
+
self._closed = False
|
|
34
|
+
|
|
35
|
+
def _enabled(self, level):
|
|
36
|
+
level = level.lower()
|
|
37
|
+
if level not in _LEVELS:
|
|
38
|
+
raise ValueError("level must be info, warning, or error")
|
|
39
|
+
return self.config.enabled and _LEVELS[level] >= _LEVELS[self.config.log_level]
|
|
40
|
+
|
|
41
|
+
def _fingerprint(self, payload, level):
|
|
42
|
+
stable = json.dumps(
|
|
43
|
+
{"level": level, "payload": payload},
|
|
44
|
+
sort_keys=True,
|
|
45
|
+
default=str,
|
|
46
|
+
separators=(",", ":"),
|
|
47
|
+
)
|
|
48
|
+
return hashlib.sha256(stable.encode("utf-8")).hexdigest()
|
|
49
|
+
|
|
50
|
+
def _is_duplicate(self, fingerprint):
|
|
51
|
+
window = self.config.deduplication_window
|
|
52
|
+
if window <= 0:
|
|
53
|
+
return False
|
|
54
|
+
now = monotonic()
|
|
55
|
+
with self._lock:
|
|
56
|
+
expired = [
|
|
57
|
+
key for key, timestamp in self._recent_events.items()
|
|
58
|
+
if now - timestamp >= window
|
|
59
|
+
]
|
|
60
|
+
for key in expired:
|
|
61
|
+
self._recent_events.pop(key, None)
|
|
62
|
+
previous = self._recent_events.get(fingerprint)
|
|
63
|
+
if previous is not None and now - previous < window:
|
|
64
|
+
return True
|
|
65
|
+
self._recent_events[fingerprint] = now
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def _submit(self, payload):
|
|
69
|
+
try:
|
|
70
|
+
if self._closed:
|
|
71
|
+
return False
|
|
72
|
+
future = self.executor.submit(self.transport.send, payload)
|
|
73
|
+
with self._lock:
|
|
74
|
+
self._futures.add(future)
|
|
75
|
+
future.add_done_callback(self._discard_future)
|
|
76
|
+
return True
|
|
77
|
+
except Exception:
|
|
78
|
+
logger.exception("Failed to queue an event for Bidar")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def _discard_future(self, future):
|
|
82
|
+
with self._lock:
|
|
83
|
+
self._futures.discard(future)
|
|
84
|
+
try:
|
|
85
|
+
future.result()
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.exception("Background event submission failed")
|
|
88
|
+
|
|
89
|
+
def capture_event(self, payload, level="info"):
|
|
90
|
+
try:
|
|
91
|
+
level = level.lower()
|
|
92
|
+
if not self._enabled(level):
|
|
93
|
+
return False
|
|
94
|
+
safe_payload = redact_mapping(payload if isinstance(payload, dict) else {"data": payload})
|
|
95
|
+
fingerprint = self._fingerprint(safe_payload, level)
|
|
96
|
+
if self._is_duplicate(fingerprint):
|
|
97
|
+
return False
|
|
98
|
+
event = {
|
|
99
|
+
"level": level,
|
|
100
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
101
|
+
"project_name": self.config.project_name,
|
|
102
|
+
"environment": self.config.environment,
|
|
103
|
+
"runtime": {
|
|
104
|
+
"language": "python",
|
|
105
|
+
"python_version": platform.python_version(),
|
|
106
|
+
"implementation": sys.implementation.name,
|
|
107
|
+
},
|
|
108
|
+
**safe_payload,
|
|
109
|
+
}
|
|
110
|
+
return self._submit(event)
|
|
111
|
+
except Exception:
|
|
112
|
+
logger.exception("Failed to prepare a Bidar event")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def capture_exception(self, error, *, request=None, extra=None):
|
|
116
|
+
try:
|
|
117
|
+
payload = {
|
|
118
|
+
"exception_type": type(error).__name__,
|
|
119
|
+
"exception_message": str(error),
|
|
120
|
+
"traceback": "".join(
|
|
121
|
+
traceback.format_exception(type(error), error, error.__traceback__)
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
if request is not None:
|
|
125
|
+
payload["request"] = request
|
|
126
|
+
if extra:
|
|
127
|
+
payload["extra"] = redact_mapping(extra)
|
|
128
|
+
return self.capture_event(payload, level="error")
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.exception("Failed to capture an exception")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def capture_message(self, message, level="info", *, extra=None):
|
|
134
|
+
try:
|
|
135
|
+
payload = {"message": str(message)}
|
|
136
|
+
if extra:
|
|
137
|
+
payload["extra"] = redact_mapping(extra)
|
|
138
|
+
return self.capture_event(payload, level=level)
|
|
139
|
+
except Exception:
|
|
140
|
+
logger.exception("Failed to capture a message")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def flush(self):
|
|
144
|
+
try:
|
|
145
|
+
with self._lock:
|
|
146
|
+
pending = list(self._futures)
|
|
147
|
+
if pending:
|
|
148
|
+
wait(pending, timeout=self.config.timeout + 1)
|
|
149
|
+
except Exception:
|
|
150
|
+
logger.exception("Failed while flushing Bidar events")
|
|
151
|
+
|
|
152
|
+
def shutdown(self):
|
|
153
|
+
if self._closed:
|
|
154
|
+
return
|
|
155
|
+
self.flush()
|
|
156
|
+
self._closed = True
|
|
157
|
+
try:
|
|
158
|
+
self.executor.shutdown(wait=False, cancel_futures=False)
|
|
159
|
+
except TypeError:
|
|
160
|
+
self.executor.shutdown(wait=False)
|
|
161
|
+
except Exception:
|
|
162
|
+
logger.exception("Failed to shut down the Bidar executor")
|
|
163
|
+
self.transport.close()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def init(
|
|
167
|
+
*,
|
|
168
|
+
url,
|
|
169
|
+
access_key,
|
|
170
|
+
log_level="error",
|
|
171
|
+
project_name=None,
|
|
172
|
+
environment="Production",
|
|
173
|
+
timeout=3.0,
|
|
174
|
+
enabled=True,
|
|
175
|
+
middleware_enabled=True,
|
|
176
|
+
capture_request_body=False,
|
|
177
|
+
deduplication_window=30.0,
|
|
178
|
+
max_workers=2,
|
|
179
|
+
):
|
|
180
|
+
global _client
|
|
181
|
+
config = Config(
|
|
182
|
+
url=url,
|
|
183
|
+
access_key=access_key,
|
|
184
|
+
log_level=log_level,
|
|
185
|
+
project_name=project_name,
|
|
186
|
+
environment=environment,
|
|
187
|
+
timeout=timeout,
|
|
188
|
+
enabled=enabled,
|
|
189
|
+
middleware_enabled=middleware_enabled,
|
|
190
|
+
capture_request_body=capture_request_body,
|
|
191
|
+
deduplication_window=deduplication_window,
|
|
192
|
+
max_workers=max_workers,
|
|
193
|
+
)
|
|
194
|
+
new_client = BidarClient(config)
|
|
195
|
+
with _client_lock:
|
|
196
|
+
old_client = _client
|
|
197
|
+
_client = new_client
|
|
198
|
+
if old_client is not None:
|
|
199
|
+
old_client.shutdown()
|
|
200
|
+
return new_client
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_client():
|
|
204
|
+
with _client_lock:
|
|
205
|
+
return _client
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _get_client():
|
|
209
|
+
client = get_client()
|
|
210
|
+
if client is None:
|
|
211
|
+
logger.warning("Bidar SDK is not initialized; call bidar.init() first")
|
|
212
|
+
return client
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def capture_exception(error, **kwargs):
|
|
216
|
+
client = _get_client()
|
|
217
|
+
return client.capture_exception(error, **kwargs) if client else False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def capture_message(message, level="info", **kwargs):
|
|
221
|
+
client = _get_client()
|
|
222
|
+
return client.capture_message(message, level=level, **kwargs) if client else False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def capture_event(payload, level="info"):
|
|
226
|
+
client = _get_client()
|
|
227
|
+
return client.capture_event(payload, level=level) if client else False
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def flush():
|
|
231
|
+
client = _get_client()
|
|
232
|
+
return client.flush() if client else None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def shutdown():
|
|
236
|
+
global _client
|
|
237
|
+
with _client_lock:
|
|
238
|
+
client = _client
|
|
239
|
+
_client = None
|
|
240
|
+
if client is not None:
|
|
241
|
+
client.shutdown()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
VALID_LEVELS = {"info", "warning", "error"}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Config:
|
|
8
|
+
url: str
|
|
9
|
+
access_key: str
|
|
10
|
+
log_level: str = "error"
|
|
11
|
+
project_name: str = None
|
|
12
|
+
environment: str = "Production"
|
|
13
|
+
timeout: float = 3.0
|
|
14
|
+
enabled: bool = True
|
|
15
|
+
middleware_enabled: bool = True
|
|
16
|
+
capture_request_body: bool = False
|
|
17
|
+
deduplication_window: float = 30.0
|
|
18
|
+
max_workers: int = 2
|
|
19
|
+
|
|
20
|
+
def __post_init__(self):
|
|
21
|
+
self.log_level = self.log_level.lower()
|
|
22
|
+
if self.log_level not in VALID_LEVELS:
|
|
23
|
+
raise ValueError("log_level must be one of: info, warning, error")
|
|
24
|
+
if not self.url:
|
|
25
|
+
raise ValueError("url is required")
|
|
26
|
+
if not self.access_key:
|
|
27
|
+
raise ValueError("access_key is required")
|
|
28
|
+
if self.timeout <= 0:
|
|
29
|
+
raise ValueError("timeout must be greater than zero")
|
|
30
|
+
if self.deduplication_window < 0:
|
|
31
|
+
raise ValueError("deduplication_window cannot be negative")
|
|
32
|
+
if self.max_workers < 1:
|
|
33
|
+
raise ValueError("max_workers must be at least one")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .client import capture_exception, get_client, init
|
|
5
|
+
from .utils.sanitizers import redact_headers, redact_mapping
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("bidar")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _initialize_from_django_settings():
|
|
11
|
+
if get_client() is not None:
|
|
12
|
+
return get_client()
|
|
13
|
+
try:
|
|
14
|
+
from django.conf import settings
|
|
15
|
+
|
|
16
|
+
options = getattr(settings, "BIDAR_SDK", {})
|
|
17
|
+
if not options or not options.get("URL") or not options.get("ACCESS_KEY"):
|
|
18
|
+
return None
|
|
19
|
+
return init(
|
|
20
|
+
url=options["URL"],
|
|
21
|
+
access_key=options["ACCESS_KEY"],
|
|
22
|
+
log_level=options.get("LOG_LEVEL", "error"),
|
|
23
|
+
project_name=options.get("PROJECT_NAME"),
|
|
24
|
+
environment=options.get("ENVIRONMENT", "Production"),
|
|
25
|
+
timeout=options.get("TIMEOUT", 3.0),
|
|
26
|
+
enabled=options.get("ENABLED", True),
|
|
27
|
+
middleware_enabled=options.get("MIDDLEWARE_ENABLED", True),
|
|
28
|
+
capture_request_body=options.get("CAPTURE_REQUEST_BODY", False),
|
|
29
|
+
deduplication_window=options.get("DEDUPLICATION_WINDOW", 30.0),
|
|
30
|
+
max_workers=options.get("MAX_WORKERS", 2),
|
|
31
|
+
)
|
|
32
|
+
except Exception:
|
|
33
|
+
logger.exception("Could not initialize Bidar SDK from Django settings")
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BidarMiddleware:
|
|
38
|
+
def __init__(self, get_response):
|
|
39
|
+
self.get_response = get_response
|
|
40
|
+
|
|
41
|
+
def __call__(self, request):
|
|
42
|
+
try:
|
|
43
|
+
return self.get_response(request)
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
self._report(request, exc)
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
def _report(self, request, error):
|
|
49
|
+
try:
|
|
50
|
+
client = _initialize_from_django_settings()
|
|
51
|
+
if client is None:
|
|
52
|
+
return
|
|
53
|
+
if not client.config.enabled or not client.config.middleware_enabled:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
request_data = {
|
|
57
|
+
"method": request.method,
|
|
58
|
+
"path": request.path,
|
|
59
|
+
"query_params": redact_mapping(request.GET.dict()),
|
|
60
|
+
"headers": redact_headers(request.headers),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if client.config.capture_request_body:
|
|
64
|
+
try:
|
|
65
|
+
raw_body = request.body
|
|
66
|
+
try:
|
|
67
|
+
parsed_body = json.loads(raw_body.decode("utf-8"))
|
|
68
|
+
request_data["input"] = redact_mapping(parsed_body)
|
|
69
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
70
|
+
request_data["input"] = raw_body.decode("utf-8", errors="replace")[:10000]
|
|
71
|
+
except Exception:
|
|
72
|
+
request_data["input"] = "[UNAVAILABLE]"
|
|
73
|
+
|
|
74
|
+
user = getattr(request, "user", None)
|
|
75
|
+
if user is not None and getattr(user, "is_authenticated", False):
|
|
76
|
+
request_data["user_id"] = str(user.pk)
|
|
77
|
+
|
|
78
|
+
capture_exception(error, request=request_data)
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.exception("Bidar middleware failed while reporting an exception")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger("bidar")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Transport:
|
|
8
|
+
def __init__(self, config):
|
|
9
|
+
self.config = config
|
|
10
|
+
self.session = requests.Session()
|
|
11
|
+
|
|
12
|
+
def send(self, payload):
|
|
13
|
+
try:
|
|
14
|
+
response = self.session.post(
|
|
15
|
+
self.config.url,
|
|
16
|
+
json=payload,
|
|
17
|
+
headers={
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"X-Bidar-Access-Key": self.config.access_key,
|
|
20
|
+
},
|
|
21
|
+
timeout=self.config.timeout,
|
|
22
|
+
)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
return True
|
|
25
|
+
except Exception:
|
|
26
|
+
logger.exception("Failed to submit an event to Bidar")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def close(self):
|
|
30
|
+
try:
|
|
31
|
+
self.session.close()
|
|
32
|
+
except Exception:
|
|
33
|
+
logger.exception("Failed to close the Bidar HTTP session")
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
SENSITIVE_PARTS = (
|
|
2
|
+
"authorization", "cookie", "password", "passwd", "token",
|
|
3
|
+
"secret", "api_key", "access_key", "refresh",
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def redact_mapping(data):
|
|
8
|
+
if not isinstance(data, dict):
|
|
9
|
+
return data
|
|
10
|
+
cleaned = {}
|
|
11
|
+
for key, value in data.items():
|
|
12
|
+
key_text = str(key).lower().replace("-", "_")
|
|
13
|
+
if any(part in key_text for part in SENSITIVE_PARTS):
|
|
14
|
+
cleaned[key] = "[REDACTED]"
|
|
15
|
+
elif isinstance(value, dict):
|
|
16
|
+
cleaned[key] = redact_mapping(value)
|
|
17
|
+
elif isinstance(value, list):
|
|
18
|
+
cleaned[key] = [
|
|
19
|
+
redact_mapping(item) if isinstance(item, dict) else item
|
|
20
|
+
for item in value
|
|
21
|
+
]
|
|
22
|
+
else:
|
|
23
|
+
cleaned[key] = value
|
|
24
|
+
return cleaned
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def redact_headers(headers):
|
|
28
|
+
return redact_mapping(dict(headers))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bidar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Background error reporting SDK for Python and Django
|
|
5
|
+
Author-email: CyberHuginn <contact@cyberhuginn.com>
|
|
6
|
+
Maintainer-email: CyberHuginn <contact@cyberhuginn.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://cyberhuginn.com
|
|
9
|
+
Project-URL: Organization, https://fardadnoavar.ir
|
|
10
|
+
Keywords: bidar,error-reporting,django,monitoring,sentry
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Provides-Extra: django
|
|
16
|
+
Requires-Dist: Django>=3.2; extra == "django"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-mock>=3; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# bidar-sdk-python
|
|
23
|
+
|
|
24
|
+
A lightweight error and event reporting SDK for Python applications, with optional Django integration. Reports are submitted in the background so application request handling does not wait for the remote API.
|
|
25
|
+
|
|
26
|
+
**Author:** CyberHuginn
|
|
27
|
+
**Organization:** Fardad Noavar
|
|
28
|
+
**Website:** [cyberhuginn.com](https://cyberhuginn.com)
|
|
29
|
+
**Organization website:** [fardadnoavar.ir](https://fardadnoavar.ir)
|
|
30
|
+
**Contact:** contact@cyberhuginn.com
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install bidar-sdk-python
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For Django projects:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "bidar-sdk-python[django]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import bidar
|
|
48
|
+
|
|
49
|
+
bidar.init(
|
|
50
|
+
url="https://api.bidarhq.ir/api/v1/errors/",
|
|
51
|
+
access_key="YOUR_ACCESS_KEY",
|
|
52
|
+
project_name="billing-api",
|
|
53
|
+
environment="Production",
|
|
54
|
+
log_level="error",
|
|
55
|
+
timeout=3.0,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
run_job()
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
bidar.capture_exception(exc)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Reports are queued on a background worker. `capture_exception`, `capture_message`, and `capture_event` return immediately with a boolean indicating whether the event was accepted for processing.
|
|
65
|
+
|
|
66
|
+
## Django integration
|
|
67
|
+
|
|
68
|
+
Add the middleware to `settings.py`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
MIDDLEWARE = [
|
|
72
|
+
# ...
|
|
73
|
+
"bidar.middleware.BidarMiddleware",
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Configure the SDK:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
BIDAR_SDK = {
|
|
81
|
+
"URL": "https://api.bidarhq.ir/api/v1/errors/",
|
|
82
|
+
"ACCESS_KEY": "YOUR_ACCESS_KEY",
|
|
83
|
+
"PROJECT_NAME": "my-django-app",
|
|
84
|
+
"ENVIRONMENT": "Production",
|
|
85
|
+
"LOG_LEVEL": "error",
|
|
86
|
+
"TIMEOUT": 3.0,
|
|
87
|
+
"ENABLED": True,
|
|
88
|
+
"MIDDLEWARE_ENABLED": True,
|
|
89
|
+
"CAPTURE_REQUEST_BODY": False,
|
|
90
|
+
"DEDUPLICATION_WINDOW": 30,
|
|
91
|
+
"MAX_WORKERS": 2,
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The middleware reports unhandled exceptions and re-raises the original exception. Set `MIDDLEWARE_ENABLED` to `False` to disable automatic middleware reporting without removing the middleware from `MIDDLEWARE`.
|
|
96
|
+
|
|
97
|
+
The SDK is initialized lazily from `BIDAR_SDK` when the middleware first runs. You can also call `bidar_sdk.init(...)` explicitly during application startup.
|
|
98
|
+
|
|
99
|
+
## Manual reporting
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import bidar
|
|
103
|
+
|
|
104
|
+
bidar.capture_exception(error)
|
|
105
|
+
|
|
106
|
+
bidar.capture_message(
|
|
107
|
+
"Payment provider is unavailable",
|
|
108
|
+
level="warning",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
bidar.capture_event(
|
|
112
|
+
{"event": "subscription.renewal_failed", "customer_id": 42},
|
|
113
|
+
level="error",
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
| Option | Default | Description |
|
|
120
|
+
|---|---:|---|
|
|
121
|
+
| `url` | Required | HTTP endpoint that receives JSON POST reports |
|
|
122
|
+
| `access_key` | Required | API key sent in `X-Bidar-Access-Key` |
|
|
123
|
+
| `log_level` | `error` | One of `info`, `warning`, or `error` |
|
|
124
|
+
| `project_name` | `None` | Project identifier attached to every event |
|
|
125
|
+
| `environment` | `Production` | Deployment environment |
|
|
126
|
+
| `timeout` | `3.0` | Outbound HTTP timeout in seconds |
|
|
127
|
+
| `enabled` | `True` | Enables or disables all reporting |
|
|
128
|
+
| `middleware_enabled` | `True` | Enables or disables Django middleware reporting |
|
|
129
|
+
| `capture_request_body` | `False` | Opt-in request body capture |
|
|
130
|
+
| `deduplication_window` | `30` | Suppresses identical events within this many seconds; set to `0` to disable |
|
|
131
|
+
| `max_workers` | `2` | Number of background HTTP workers |
|
|
132
|
+
|
|
133
|
+
Log levels are threshold-based: `info` accepts all levels, `warning` accepts warnings and errors, and `error` accepts errors only.
|
|
134
|
+
|
|
135
|
+
## Reliability and privacy
|
|
136
|
+
|
|
137
|
+
- Network requests run in a background thread pool.
|
|
138
|
+
- HTTP failures, serialization errors, and worker failures are contained and do not propagate into application code.
|
|
139
|
+
- `flush()` waits for submitted reports; `shutdown()` flushes and closes the client.
|
|
140
|
+
- Duplicate reports with the same event fingerprint are suppressed during the configured deduplication window.
|
|
141
|
+
- Authorization, cookies, passwords, tokens, secrets, and API-key-like fields are redacted.
|
|
142
|
+
- Request bodies are not captured by default. Enable `CAPTURE_REQUEST_BODY` only after reviewing the data your application sends.
|
|
143
|
+
- The SDK does not intentionally collect local variables or environment variables.
|
|
144
|
+
|
|
145
|
+
## Lifecycle
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
bidar_sdk.flush()
|
|
149
|
+
bidar_sdk.shutdown()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Call `flush()` during controlled shutdown if pending reports should be given a chance to finish. `shutdown()` prevents further submissions until the SDK is initialized again.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
bidar/__init__.py
|
|
5
|
+
bidar/client.py
|
|
6
|
+
bidar/config.py
|
|
7
|
+
bidar/middleware.py
|
|
8
|
+
bidar/transport.py
|
|
9
|
+
bidar.egg-info/PKG-INFO
|
|
10
|
+
bidar.egg-info/SOURCES.txt
|
|
11
|
+
bidar.egg-info/dependency_links.txt
|
|
12
|
+
bidar.egg-info/requires.txt
|
|
13
|
+
bidar.egg-info/top_level.txt
|
|
14
|
+
bidar/utils/__init__.py
|
|
15
|
+
bidar/utils/sanitizers.py
|
|
16
|
+
tests/test_client.py
|
|
17
|
+
tests/test_config.py
|
|
18
|
+
tests/test_sanitizers.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bidar
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bidar"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Background error reporting SDK for Python and Django"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "CyberHuginn", email = "contact@cyberhuginn.com" }]
|
|
13
|
+
maintainers = [{ name = "CyberHuginn", email = "contact@cyberhuginn.com" }]
|
|
14
|
+
keywords = ["bidar", "error-reporting", "django", "monitoring", "sentry"]
|
|
15
|
+
dependencies = ["requests>=2.28"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://cyberhuginn.com"
|
|
19
|
+
Organization = "https://fardadnoavar.ir"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
django = ["Django>=3.2"]
|
|
23
|
+
dev = ["pytest>=7", "pytest-mock>=3"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["bidar*"]
|
bidar-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from bidar.client import BidarClient
|
|
2
|
+
from bidar.config import Config
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FakeTransport:
|
|
6
|
+
def __init__(self, config):
|
|
7
|
+
self.config = config
|
|
8
|
+
self.sent = []
|
|
9
|
+
|
|
10
|
+
def send(self, payload):
|
|
11
|
+
self.sent.append(payload)
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
def close(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_client():
|
|
19
|
+
client = BidarClient(Config(
|
|
20
|
+
url="https://example.com",
|
|
21
|
+
access_key="test-key",
|
|
22
|
+
project_name="test-project",
|
|
23
|
+
environment="Development",
|
|
24
|
+
deduplication_window=10,
|
|
25
|
+
))
|
|
26
|
+
client.transport.close()
|
|
27
|
+
client.transport = FakeTransport(client.config)
|
|
28
|
+
return client
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_event_contains_project_and_environment():
|
|
32
|
+
client = make_client()
|
|
33
|
+
assert client.capture_message("hello", level="info")
|
|
34
|
+
client.flush()
|
|
35
|
+
assert client.transport.sent[0]["project_name"] == "test-project"
|
|
36
|
+
assert client.transport.sent[0]["environment"] == "Development"
|
|
37
|
+
client.shutdown()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_duplicate_events_are_suppressed():
|
|
41
|
+
client = make_client()
|
|
42
|
+
assert client.capture_message("same event", level="error")
|
|
43
|
+
assert client.capture_message("same event", level="error") is False
|
|
44
|
+
client.flush()
|
|
45
|
+
assert len(client.transport.sent) == 1
|
|
46
|
+
client.shutdown()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_transport_failure_does_not_escape_capture():
|
|
50
|
+
client = make_client()
|
|
51
|
+
|
|
52
|
+
def broken_send(payload):
|
|
53
|
+
raise RuntimeError("network failure")
|
|
54
|
+
|
|
55
|
+
client.transport.send = broken_send
|
|
56
|
+
assert client.capture_message("failure", level="error")
|
|
57
|
+
client.flush()
|
|
58
|
+
client.shutdown()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from bidar.config import Config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_config_rejects_invalid_log_level():
|
|
7
|
+
with pytest.raises(ValueError):
|
|
8
|
+
Config(url="https://example.com", access_key="key", log_level="debug")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_config_accepts_supported_levels():
|
|
12
|
+
for level in ("info", "warning", "error"):
|
|
13
|
+
assert Config(url="https://example.com", access_key="key", log_level=level)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from bidar.utils.sanitizers import redact_mapping
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_redacts_sensitive_fields_recursively():
|
|
5
|
+
result = redact_mapping({
|
|
6
|
+
"password": "secret",
|
|
7
|
+
"nested": {"access_key": "abc", "name": "Alice"},
|
|
8
|
+
"items": [{"refresh_token": "xyz"}],
|
|
9
|
+
})
|
|
10
|
+
assert result["password"] == "[REDACTED]"
|
|
11
|
+
assert result["nested"]["access_key"] == "[REDACTED]"
|
|
12
|
+
assert result["nested"]["name"] == "Alice"
|
|
13
|
+
assert result["items"][0]["refresh_token"] == "[REDACTED]"
|