tracelify-sdk 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.
- tracelify_sdk-0.1.0/LICENSE +21 -0
- tracelify_sdk-0.1.0/PKG-INFO +105 -0
- tracelify_sdk-0.1.0/README.md +86 -0
- tracelify_sdk-0.1.0/pyproject.toml +35 -0
- tracelify_sdk-0.1.0/setup.cfg +4 -0
- tracelify_sdk-0.1.0/tests/test_client.py +68 -0
- tracelify_sdk-0.1.0/tracelify/__init__.py +6 -0
- tracelify_sdk-0.1.0/tracelify/client.py +103 -0
- tracelify_sdk-0.1.0/tracelify/config.py +60 -0
- tracelify_sdk-0.1.0/tracelify/exceptions.py +17 -0
- tracelify_sdk-0.1.0/tracelify/handlers.py +30 -0
- tracelify_sdk-0.1.0/tracelify/hub.py +13 -0
- tracelify_sdk-0.1.0/tracelify/models/__init__.py +3 -0
- tracelify_sdk-0.1.0/tracelify/models/event.py +40 -0
- tracelify_sdk-0.1.0/tracelify/queue.py +5 -0
- tracelify_sdk-0.1.0/tracelify/scope.py +47 -0
- tracelify_sdk-0.1.0/tracelify/transport.py +68 -0
- tracelify_sdk-0.1.0/tracelify/utils.py +25 -0
- tracelify_sdk-0.1.0/tracelify/version.py +1 -0
- tracelify_sdk-0.1.0/tracelify/worker.py +38 -0
- tracelify_sdk-0.1.0/tracelify_sdk.egg-info/PKG-INFO +105 -0
- tracelify_sdk-0.1.0/tracelify_sdk.egg-info/SOURCES.txt +23 -0
- tracelify_sdk-0.1.0/tracelify_sdk.egg-info/dependency_links.txt +1 -0
- tracelify_sdk-0.1.0/tracelify_sdk.egg-info/requires.txt +1 -0
- tracelify_sdk-0.1.0/tracelify_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tracelify Team
|
|
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.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tracelify-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Tracelify - Error Tracking and Performance Monitoring.
|
|
5
|
+
Author-email: Tracelify Team <support@tracelify.io>
|
|
6
|
+
Project-URL: Homepage, https://github.com/tracelify-io/tracelify-python
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/tracelify-io/tracelify-python/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.25.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Tracelify Python SDK
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/tracelify-sdk/)
|
|
23
|
+
[](https://pypi.org/project/tracelify-sdk/)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for Tracelify - Effortless Error Tracking and Performance Monitoring for your Python applications.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ⚡ Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install tracelify-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Initialize
|
|
39
|
+
|
|
40
|
+
Initialize the client with your DSN early in your application's entry point:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from tracelify import Client
|
|
44
|
+
|
|
45
|
+
# Initialize the client (automatically captures unhandled exceptions)
|
|
46
|
+
client = Client("https://your-key@tracelify.io/project-id")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Capture Exceptions Manually
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
try:
|
|
53
|
+
1 / 0
|
|
54
|
+
except Exception as e:
|
|
55
|
+
client.capture_exception(e)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 4. Enrich context
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# Set user context
|
|
62
|
+
client.set_user({"id": "123", "email": "user@example.com"})
|
|
63
|
+
|
|
64
|
+
# Set custom tags
|
|
65
|
+
client.set_tag("environment", "production")
|
|
66
|
+
|
|
67
|
+
# Add breadcrumbs for debugging
|
|
68
|
+
client.add_breadcrumb("User clicked 'Submit' button")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🛠️ Components
|
|
74
|
+
|
|
75
|
+
- **Client**: Main interface to the SDK.
|
|
76
|
+
- **Config**: Configuration and DSN parsing.
|
|
77
|
+
- **Scope**: Manages user context, tags, and breadcrumbs.
|
|
78
|
+
- **Transport**: Sends events asynchronously to the Tracelify server with automatic retries and exponential backoff.
|
|
79
|
+
- **Handlers**: Automatic global exception capture for unhandled errors.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ⚙️ Configuration
|
|
84
|
+
|
|
85
|
+
The Tracelify SDK is designed to work with minimal configuration. Simply provide a valid DSN:
|
|
86
|
+
|
|
87
|
+
| DSN Part | Description |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `protocol` | http or https |
|
|
90
|
+
| `key` | Your public authentication key |
|
|
91
|
+
| `host` | Tracelify server hostname |
|
|
92
|
+
| `port` | Server port (optional) |
|
|
93
|
+
| `project_id`| Your project ID |
|
|
94
|
+
|
|
95
|
+
Example DSN: `https://abcd1234@api.tracelify.io:8080/1`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🤝 Contributing
|
|
100
|
+
|
|
101
|
+
We welcome contributions! Please check out our [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md).
|
|
102
|
+
|
|
103
|
+
## 📄 License
|
|
104
|
+
|
|
105
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Tracelify Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/tracelify-sdk/)
|
|
4
|
+
[](https://pypi.org/project/tracelify-sdk/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Official Python SDK for Tracelify - Effortless Error Tracking and Performance Monitoring for your Python applications.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ⚡ Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install tracelify-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Initialize
|
|
20
|
+
|
|
21
|
+
Initialize the client with your DSN early in your application's entry point:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from tracelify import Client
|
|
25
|
+
|
|
26
|
+
# Initialize the client (automatically captures unhandled exceptions)
|
|
27
|
+
client = Client("https://your-key@tracelify.io/project-id")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. Capture Exceptions Manually
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
try:
|
|
34
|
+
1 / 0
|
|
35
|
+
except Exception as e:
|
|
36
|
+
client.capture_exception(e)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 4. Enrich context
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# Set user context
|
|
43
|
+
client.set_user({"id": "123", "email": "user@example.com"})
|
|
44
|
+
|
|
45
|
+
# Set custom tags
|
|
46
|
+
client.set_tag("environment", "production")
|
|
47
|
+
|
|
48
|
+
# Add breadcrumbs for debugging
|
|
49
|
+
client.add_breadcrumb("User clicked 'Submit' button")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🛠️ Components
|
|
55
|
+
|
|
56
|
+
- **Client**: Main interface to the SDK.
|
|
57
|
+
- **Config**: Configuration and DSN parsing.
|
|
58
|
+
- **Scope**: Manages user context, tags, and breadcrumbs.
|
|
59
|
+
- **Transport**: Sends events asynchronously to the Tracelify server with automatic retries and exponential backoff.
|
|
60
|
+
- **Handlers**: Automatic global exception capture for unhandled errors.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ⚙️ Configuration
|
|
65
|
+
|
|
66
|
+
The Tracelify SDK is designed to work with minimal configuration. Simply provide a valid DSN:
|
|
67
|
+
|
|
68
|
+
| DSN Part | Description |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `protocol` | http or https |
|
|
71
|
+
| `key` | Your public authentication key |
|
|
72
|
+
| `host` | Tracelify server hostname |
|
|
73
|
+
| `port` | Server port (optional) |
|
|
74
|
+
| `project_id`| Your project ID |
|
|
75
|
+
|
|
76
|
+
Example DSN: `https://abcd1234@api.tracelify.io:8080/1`
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🤝 Contributing
|
|
81
|
+
|
|
82
|
+
We welcome contributions! Please check out our [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md).
|
|
83
|
+
|
|
84
|
+
## 📄 License
|
|
85
|
+
|
|
86
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tracelify-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Tracelify Team", email = "support@tracelify.io" },
|
|
10
|
+
]
|
|
11
|
+
description = "Official Python SDK for Tracelify - Error Tracking and Performance Monitoring."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"requests>=2.25.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
"Homepage" = "https://github.com/tracelify-io/tracelify-python"
|
|
28
|
+
"Bug Tracker" = "https://github.com/tracelify-io/tracelify-python/issues"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
include = ["tracelify*"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
python_files = ["test_*.py"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from tracelify import Client, Config, ConfigError
|
|
3
|
+
|
|
4
|
+
def test_config_parsing_valid():
|
|
5
|
+
"""Test valid DSN parsing."""
|
|
6
|
+
dsn = "http://public_key@localhost:8000/1"
|
|
7
|
+
config = Config(dsn)
|
|
8
|
+
|
|
9
|
+
assert config.protocol == "http"
|
|
10
|
+
assert config.public_key == "public_key"
|
|
11
|
+
assert config.host == "localhost"
|
|
12
|
+
assert config.port == 8000
|
|
13
|
+
assert config.project_id == "1"
|
|
14
|
+
assert config.get_endpoint() == "http://localhost:8000/api/1/events"
|
|
15
|
+
|
|
16
|
+
def test_config_parsing_invalid():
|
|
17
|
+
"""Test invalid DSN parsing raises ConfigError."""
|
|
18
|
+
# Missing public key
|
|
19
|
+
with pytest.raises(ConfigError):
|
|
20
|
+
Config("http://localhost:8000/1")
|
|
21
|
+
|
|
22
|
+
# Missing host
|
|
23
|
+
with pytest.raises(ConfigError):
|
|
24
|
+
Config("http://key@/1")
|
|
25
|
+
|
|
26
|
+
def test_client_init_success(monkeypatch):
|
|
27
|
+
"""Test successful client initialization."""
|
|
28
|
+
# Mocking background worker start
|
|
29
|
+
monkeypatch.setattr("tracelify.client.start_worker", lambda config: None)
|
|
30
|
+
|
|
31
|
+
dsn = "http://key@localhost:8000/1"
|
|
32
|
+
client = Client(dsn)
|
|
33
|
+
|
|
34
|
+
assert client.config.host == "localhost"
|
|
35
|
+
assert client.scope is not None
|
|
36
|
+
|
|
37
|
+
def test_scope_context():
|
|
38
|
+
"""Test that scope correctly accumulates context."""
|
|
39
|
+
from tracelify.scope import Scope
|
|
40
|
+
scope = Scope()
|
|
41
|
+
|
|
42
|
+
scope.set_user({"id": "1"})
|
|
43
|
+
scope.set_tag("env", "prod")
|
|
44
|
+
scope.add_breadcrumb("test message")
|
|
45
|
+
|
|
46
|
+
context = scope.get()
|
|
47
|
+
assert context["user"] == {"id": "1"}
|
|
48
|
+
assert context["tags"] == {"env": "prod"}
|
|
49
|
+
assert len(context["breadcrumbs"]) == 1
|
|
50
|
+
assert context["breadcrumbs"][0]["message"] == "test message"
|
|
51
|
+
|
|
52
|
+
def test_event_model():
|
|
53
|
+
"""Test that the event model dictionary conversion is correct."""
|
|
54
|
+
from tracelify.models import Event
|
|
55
|
+
from tracelify.utils import get_timestamp
|
|
56
|
+
|
|
57
|
+
ts = get_timestamp()
|
|
58
|
+
event = Event(
|
|
59
|
+
project_id="123",
|
|
60
|
+
timestamp=ts,
|
|
61
|
+
error={"type": "ValueError", "message": "error msg", "stacktrace": "..."}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
d = event.to_dict()
|
|
65
|
+
assert d["project_id"] == "123"
|
|
66
|
+
assert d["timestamp"] == ts
|
|
67
|
+
assert d["error"]["type"] == "ValueError"
|
|
68
|
+
assert d["client"]["sdk"] == "tracelify-python"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .client import Client
|
|
2
|
+
from .config import Config
|
|
3
|
+
from .exceptions import TracelifyError, ConfigError, TransportError, APIError
|
|
4
|
+
from .version import __version__
|
|
5
|
+
|
|
6
|
+
__all__ = ["Client", "Config", "TracelifyError", "ConfigError", "TransportError", "APIError", "__version__"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, Optional, List
|
|
4
|
+
from .config import Config
|
|
5
|
+
from .hub import set_client
|
|
6
|
+
from .scope import Scope
|
|
7
|
+
from .queue import event_queue
|
|
8
|
+
from .worker import start_worker
|
|
9
|
+
from .handlers import setup_global_handler
|
|
10
|
+
from .utils import get_runtime_context, get_timestamp
|
|
11
|
+
from .models import Event
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class Client:
|
|
16
|
+
"""
|
|
17
|
+
The main client for the Tracelify SDK.
|
|
18
|
+
|
|
19
|
+
This class orchestrates exception capture, context management, and background event sending.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
config (Config): Configuration for the SDK.
|
|
23
|
+
scope (Scope): The current scope for user and tag context.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, dsn: str):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the Tracelify client.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
dsn: DSN of the project.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
self.config = Config(dsn)
|
|
35
|
+
self.scope = Scope()
|
|
36
|
+
|
|
37
|
+
# Global registration
|
|
38
|
+
set_client(self)
|
|
39
|
+
|
|
40
|
+
# Start background worker for asynchronous events
|
|
41
|
+
start_worker(self.config)
|
|
42
|
+
|
|
43
|
+
# Automatically capture unhandled exceptions
|
|
44
|
+
setup_global_handler()
|
|
45
|
+
|
|
46
|
+
logger.info("Tracelify SDK client initialized successfully.")
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"Failed to initialize Tracelify SDK client: {e}")
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def capture_exception(self, error: Exception, stacktrace: Optional[str] = None):
|
|
53
|
+
"""
|
|
54
|
+
Manually capture an exception and send it to Tracelify.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
error: The exception to capture.
|
|
58
|
+
stacktrace: Optional manual stacktrace string.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
if stacktrace is None:
|
|
62
|
+
tb = getattr(error, "__traceback__", None)
|
|
63
|
+
if tb is not None:
|
|
64
|
+
stacktrace = "".join(traceback.format_exception(type(error), error, tb))
|
|
65
|
+
else:
|
|
66
|
+
stacktrace = "".join(traceback.format_stack())
|
|
67
|
+
|
|
68
|
+
if not stacktrace:
|
|
69
|
+
stacktrace = "No stacktrace available"
|
|
70
|
+
|
|
71
|
+
# Prepare the event data
|
|
72
|
+
event_data = Event(
|
|
73
|
+
project_id=self.config.project_id,
|
|
74
|
+
timestamp=get_timestamp(),
|
|
75
|
+
error={
|
|
76
|
+
"type": type(error).__name__,
|
|
77
|
+
"message": str(error),
|
|
78
|
+
"stacktrace": stacktrace
|
|
79
|
+
},
|
|
80
|
+
context=get_runtime_context(),
|
|
81
|
+
**self.scope.get()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Send the event to the background queue
|
|
85
|
+
event_queue.put(event_data.to_dict())
|
|
86
|
+
logger.debug(f"Exception captured and queued: {type(error).__name__}")
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Error while capturing exception: {e}")
|
|
90
|
+
|
|
91
|
+
# -------- CONTEXT METHODS --------
|
|
92
|
+
|
|
93
|
+
def set_user(self, user: Dict[str, Any]):
|
|
94
|
+
"""Set the user for the current scope."""
|
|
95
|
+
self.scope.set_user(user)
|
|
96
|
+
|
|
97
|
+
def set_tag(self, key: str, value: str):
|
|
98
|
+
"""Set a custom tag for context."""
|
|
99
|
+
self.scope.set_tag(key, value)
|
|
100
|
+
|
|
101
|
+
def add_breadcrumb(self, message: str, level: str = "info"):
|
|
102
|
+
"""Add a breadcrumb to the current scope."""
|
|
103
|
+
self.scope.add_breadcrumb(message, level)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from urllib.parse import urlparse
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .exceptions import ConfigError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
"""
|
|
8
|
+
Configuration for the Tracelify client.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
dsn (str): The Data Source Name for the project.
|
|
12
|
+
protocol (str): The protocol (http/https).
|
|
13
|
+
host (str): The host of the server.
|
|
14
|
+
port (int): The port of the server.
|
|
15
|
+
public_key (str): The public key for authentication.
|
|
16
|
+
project_id (str): The project ID to which events will be sent.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, dsn: str):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the config from a DSN string.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
dsn: DSN of the project (e.g., http://key@host:port/1)
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ConfigError: If the DSN is invalid.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
parsed = urlparse(dsn)
|
|
31
|
+
self.dsn = dsn
|
|
32
|
+
self.protocol = parsed.scheme or "https"
|
|
33
|
+
self.host = parsed.hostname
|
|
34
|
+
self.port = parsed.port or (443 if self.protocol == "https" else 80)
|
|
35
|
+
self.public_key = parsed.username
|
|
36
|
+
|
|
37
|
+
if not self.host:
|
|
38
|
+
raise ConfigError("Invalid DSN: Missing host")
|
|
39
|
+
if not self.public_key:
|
|
40
|
+
raise ConfigError("Invalid DSN: Missing public key (username)")
|
|
41
|
+
|
|
42
|
+
path_parts = parsed.path.strip("/").split("/")
|
|
43
|
+
if not path_parts or not path_parts[0]:
|
|
44
|
+
self.project_id = "1"
|
|
45
|
+
else:
|
|
46
|
+
self.project_id = path_parts[0]
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
if isinstance(e, ConfigError):
|
|
50
|
+
raise
|
|
51
|
+
raise ConfigError(f"Failed to parse DSN: {e}") from e
|
|
52
|
+
|
|
53
|
+
def get_endpoint(self) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Get the endpoint URL for sending events.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The complete URL for the events endpoint.
|
|
59
|
+
"""
|
|
60
|
+
return f"{self.protocol}://{self.host}:{self.port}/api/{self.project_id}/events"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class TracelifyError(Exception):
|
|
2
|
+
"""Base exception for all Tracelify SDK errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class ConfigError(TracelifyError):
|
|
6
|
+
"""Raised when there is a configuration error."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class TransportError(TracelifyError):
|
|
10
|
+
"""Raised when there is an issue with the underlying HTTP transport."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class APIError(TracelifyError):
|
|
14
|
+
"""Raised when the API returns an error response."""
|
|
15
|
+
def __init__(self, message, status_code=None):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, Any
|
|
5
|
+
from .hub import get_client
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
def setup_global_handler():
|
|
10
|
+
"""Register the SDK with sys.excepthook to automatically capture unhandled exceptions."""
|
|
11
|
+
|
|
12
|
+
def handle_exception(exc_type, exc_value, exc_traceback):
|
|
13
|
+
"""Global exception handler callback."""
|
|
14
|
+
# Don't capture KeyboardInterrupt or system exit
|
|
15
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
16
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
client = get_client()
|
|
20
|
+
if client:
|
|
21
|
+
try:
|
|
22
|
+
# Capture the exception
|
|
23
|
+
client.capture_exception(exc_value)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error(f"Error while capturing exception in global handler: {e}")
|
|
26
|
+
|
|
27
|
+
# Still call the original excepthook to avoid hiding the error from the user
|
|
28
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
29
|
+
|
|
30
|
+
sys.excepthook = handle_exception
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
# Global reference to the current client
|
|
4
|
+
_current_client = None
|
|
5
|
+
|
|
6
|
+
def set_client(client: Any) -> None:
|
|
7
|
+
"""Set the globally available client."""
|
|
8
|
+
global _current_client
|
|
9
|
+
_current_client = client
|
|
10
|
+
|
|
11
|
+
def get_client() -> Optional[Any]:
|
|
12
|
+
"""Get the currently active client."""
|
|
13
|
+
return _current_client
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
from ..utils import get_timestamp
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class User:
|
|
7
|
+
id: Optional[str] = None
|
|
8
|
+
email: Optional[str] = None
|
|
9
|
+
username: Optional[str] = None
|
|
10
|
+
ip_address: Optional[str] = None
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Breadcrumb:
|
|
14
|
+
message: str
|
|
15
|
+
timestamp: str = field(default_factory=get_timestamp)
|
|
16
|
+
level: str = "info"
|
|
17
|
+
category: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Event:
|
|
21
|
+
project_id: str
|
|
22
|
+
timestamp: str
|
|
23
|
+
error: Dict[str, Any]
|
|
24
|
+
client: Dict[str, str] = field(default_factory=lambda: {"sdk": "tracelify-python"})
|
|
25
|
+
user: Optional[Dict[str, Any]] = None
|
|
26
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
27
|
+
breadcrumbs: List[Dict[str, Any]] = field(default_factory=list)
|
|
28
|
+
context: Dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"project_id": self.project_id,
|
|
33
|
+
"timestamp": self.timestamp,
|
|
34
|
+
"error": self.error,
|
|
35
|
+
"client": self.client,
|
|
36
|
+
"user": self.user,
|
|
37
|
+
"tags": self.tags,
|
|
38
|
+
"breadcrumbs": self.breadcrumbs,
|
|
39
|
+
"context": self.context,
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
|
2
|
+
from .models.event import Breadcrumb
|
|
3
|
+
|
|
4
|
+
class Scope:
|
|
5
|
+
"""
|
|
6
|
+
Manages the current context (user, tags, breadcrumbs) for events.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
"""Initialize an empty scope."""
|
|
11
|
+
self.user: Dict[str, Any] = {}
|
|
12
|
+
self.tags: Dict[str, str] = {}
|
|
13
|
+
self.breadcrumbs: List[Dict[str, Any]] = []
|
|
14
|
+
|
|
15
|
+
def set_user(self, user: Dict[str, Any]) -> None:
|
|
16
|
+
"""Set user information for the current context."""
|
|
17
|
+
self.user = user
|
|
18
|
+
|
|
19
|
+
def set_tag(self, key: str, value: str) -> None:
|
|
20
|
+
"""Set a tag for the current context."""
|
|
21
|
+
self.tags[key] = value
|
|
22
|
+
|
|
23
|
+
def add_breadcrumb(self, message: str, level: str = "info") -> None:
|
|
24
|
+
"""Add a breadcrumb to the current context."""
|
|
25
|
+
breadcrumb = Breadcrumb(message=message, level=level)
|
|
26
|
+
self.breadcrumbs.append({
|
|
27
|
+
"message": breadcrumb.message,
|
|
28
|
+
"timestamp": breadcrumb.timestamp,
|
|
29
|
+
"level": breadcrumb.level
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
# Keep only the last 100 breadcrumbs for memory safety
|
|
33
|
+
if len(self.breadcrumbs) > 100:
|
|
34
|
+
self.breadcrumbs.pop(0)
|
|
35
|
+
|
|
36
|
+
def get(self) -> Dict[str, Any]:
|
|
37
|
+
"""
|
|
38
|
+
Get the current scope as a dictionary.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A dictionary containing user, tags, and breadcrumbs.
|
|
42
|
+
"""
|
|
43
|
+
return {
|
|
44
|
+
"user": self.user,
|
|
45
|
+
"tags": self.tags,
|
|
46
|
+
"breadcrumbs": self.breadcrumbs[-20:] # Only include last 20 in the final event
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import logging
|
|
3
|
+
from requests.adapters import HTTPAdapter
|
|
4
|
+
from urllib3.util.retry import Retry
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
from .config import Config
|
|
7
|
+
from .exceptions import TransportError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Use a session with retries for efficiency and resilience
|
|
12
|
+
_session = None
|
|
13
|
+
|
|
14
|
+
def get_session():
|
|
15
|
+
"""Get or create a requests session with retries."""
|
|
16
|
+
global _session
|
|
17
|
+
if _session is None:
|
|
18
|
+
_session = requests.Session()
|
|
19
|
+
|
|
20
|
+
# Configure retry logic: 3 retries, exponential backoff, handle status codes 429, 500, 502, 503, 504
|
|
21
|
+
retries = Retry(
|
|
22
|
+
total=3,
|
|
23
|
+
backoff_factor=0.3,
|
|
24
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
25
|
+
allowed_methods=["POST"]
|
|
26
|
+
)
|
|
27
|
+
_session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
28
|
+
_session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
29
|
+
|
|
30
|
+
return _session
|
|
31
|
+
|
|
32
|
+
def send_event(config: Config, data: Dict[str, Any]):
|
|
33
|
+
"""
|
|
34
|
+
Send an event to the API using the provided configuration.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: The SDK configuration.
|
|
38
|
+
data: The event data to send.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
TransportError: If the event fails to send.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
session = get_session()
|
|
45
|
+
response = session.post(
|
|
46
|
+
config.get_endpoint(),
|
|
47
|
+
json=data,
|
|
48
|
+
headers={
|
|
49
|
+
"X-Tracelify-Key": config.public_key,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
"User-Agent": "tracelify-python-sdk/0.1.0"
|
|
52
|
+
},
|
|
53
|
+
timeout=5
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Raise an exception for 4xx or 5xx responses (not handled by Retry)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
|
|
59
|
+
logger.debug(f"Event sent successfully: {response.status_code}")
|
|
60
|
+
|
|
61
|
+
except requests.exceptions.RequestException as e:
|
|
62
|
+
logger.error(f"Failed to send event to Tracelify: {e}")
|
|
63
|
+
# We don't raise here to avoid crashing the user's application
|
|
64
|
+
# unless it was a configuration error. We just log the failure.
|
|
65
|
+
pass
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Unexpected error in transport: {e}")
|
|
68
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
def get_runtime_context() -> Dict[str, Any]:
|
|
6
|
+
"""
|
|
7
|
+
Get information about the system and runtime.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
A dictionary containing OS and Python runtime information.
|
|
11
|
+
"""
|
|
12
|
+
return {
|
|
13
|
+
"os": platform.system(),
|
|
14
|
+
"runtime": "python",
|
|
15
|
+
"python_version": platform.python_version()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def get_timestamp() -> str:
|
|
19
|
+
"""
|
|
20
|
+
Get current UTC timestamp in ISO 8601 format.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
ISO format timestamp (UTC).
|
|
24
|
+
"""
|
|
25
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import logging
|
|
3
|
+
from .queue import event_queue
|
|
4
|
+
from .transport import send_event
|
|
5
|
+
from .config import Config
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
def start_worker(config: Config) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Start a background thread to send events from the queue to the API.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
config: SDK configuration for the transport.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def worker():
|
|
18
|
+
"""Background worker loop."""
|
|
19
|
+
while True:
|
|
20
|
+
try:
|
|
21
|
+
# Wait for an event to be added to the queue
|
|
22
|
+
event = event_queue.get()
|
|
23
|
+
if event is None: # Shutdown signal
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
# Send the event via transport
|
|
27
|
+
send_event(config, event)
|
|
28
|
+
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.error(f"Error in background worker: {e}")
|
|
31
|
+
finally:
|
|
32
|
+
# Always mark the task as done
|
|
33
|
+
event_queue.task_done()
|
|
34
|
+
|
|
35
|
+
# Create a daemon thread so it doesn't block process exit
|
|
36
|
+
thread = threading.Thread(target=worker, daemon=True, name="tracelify-worker")
|
|
37
|
+
thread.start()
|
|
38
|
+
logger.debug("Background worker thread started.")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tracelify-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Tracelify - Error Tracking and Performance Monitoring.
|
|
5
|
+
Author-email: Tracelify Team <support@tracelify.io>
|
|
6
|
+
Project-URL: Homepage, https://github.com/tracelify-io/tracelify-python
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/tracelify-io/tracelify-python/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.25.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Tracelify Python SDK
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/tracelify-sdk/)
|
|
23
|
+
[](https://pypi.org/project/tracelify-sdk/)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for Tracelify - Effortless Error Tracking and Performance Monitoring for your Python applications.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ⚡ Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install tracelify-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Initialize
|
|
39
|
+
|
|
40
|
+
Initialize the client with your DSN early in your application's entry point:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from tracelify import Client
|
|
44
|
+
|
|
45
|
+
# Initialize the client (automatically captures unhandled exceptions)
|
|
46
|
+
client = Client("https://your-key@tracelify.io/project-id")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Capture Exceptions Manually
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
try:
|
|
53
|
+
1 / 0
|
|
54
|
+
except Exception as e:
|
|
55
|
+
client.capture_exception(e)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 4. Enrich context
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# Set user context
|
|
62
|
+
client.set_user({"id": "123", "email": "user@example.com"})
|
|
63
|
+
|
|
64
|
+
# Set custom tags
|
|
65
|
+
client.set_tag("environment", "production")
|
|
66
|
+
|
|
67
|
+
# Add breadcrumbs for debugging
|
|
68
|
+
client.add_breadcrumb("User clicked 'Submit' button")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🛠️ Components
|
|
74
|
+
|
|
75
|
+
- **Client**: Main interface to the SDK.
|
|
76
|
+
- **Config**: Configuration and DSN parsing.
|
|
77
|
+
- **Scope**: Manages user context, tags, and breadcrumbs.
|
|
78
|
+
- **Transport**: Sends events asynchronously to the Tracelify server with automatic retries and exponential backoff.
|
|
79
|
+
- **Handlers**: Automatic global exception capture for unhandled errors.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ⚙️ Configuration
|
|
84
|
+
|
|
85
|
+
The Tracelify SDK is designed to work with minimal configuration. Simply provide a valid DSN:
|
|
86
|
+
|
|
87
|
+
| DSN Part | Description |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `protocol` | http or https |
|
|
90
|
+
| `key` | Your public authentication key |
|
|
91
|
+
| `host` | Tracelify server hostname |
|
|
92
|
+
| `port` | Server port (optional) |
|
|
93
|
+
| `project_id`| Your project ID |
|
|
94
|
+
|
|
95
|
+
Example DSN: `https://abcd1234@api.tracelify.io:8080/1`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🤝 Contributing
|
|
100
|
+
|
|
101
|
+
We welcome contributions! Please check out our [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md).
|
|
102
|
+
|
|
103
|
+
## 📄 License
|
|
104
|
+
|
|
105
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_client.py
|
|
5
|
+
tracelify/__init__.py
|
|
6
|
+
tracelify/client.py
|
|
7
|
+
tracelify/config.py
|
|
8
|
+
tracelify/exceptions.py
|
|
9
|
+
tracelify/handlers.py
|
|
10
|
+
tracelify/hub.py
|
|
11
|
+
tracelify/queue.py
|
|
12
|
+
tracelify/scope.py
|
|
13
|
+
tracelify/transport.py
|
|
14
|
+
tracelify/utils.py
|
|
15
|
+
tracelify/version.py
|
|
16
|
+
tracelify/worker.py
|
|
17
|
+
tracelify/models/__init__.py
|
|
18
|
+
tracelify/models/event.py
|
|
19
|
+
tracelify_sdk.egg-info/PKG-INFO
|
|
20
|
+
tracelify_sdk.egg-info/SOURCES.txt
|
|
21
|
+
tracelify_sdk.egg-info/dependency_links.txt
|
|
22
|
+
tracelify_sdk.egg-info/requires.txt
|
|
23
|
+
tracelify_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tracelify
|