watchdock-errors 0.2.3__tar.gz → 0.3.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.
- {watchdock_errors-0.2.3/src/watchdock_errors.egg-info → watchdock_errors-0.3.0}/PKG-INFO +23 -1
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/README.md +22 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/__init__.py +26 -5
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/event.py +8 -1
- watchdock_errors-0.3.0/src/watchdock_errors/utils.py +92 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0/src/watchdock_errors.egg-info}/PKG-INFO +23 -1
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/SOURCES.txt +2 -1
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/scm_file_list.json +9 -8
- watchdock_errors-0.3.0/src/watchdock_errors.egg-info/scm_version.json +8 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/tests/test_event.py +51 -0
- watchdock_errors-0.3.0/tests/test_utils.py +44 -0
- watchdock_errors-0.2.3/src/watchdock_errors/utils.py +0 -33
- watchdock_errors-0.2.3/src/watchdock_errors.egg-info/scm_version.json +0 -8
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/.github/workflows/ci.yml +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/.github/workflows/release.yml +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/.gitignore +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/pyproject.toml +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/setup.cfg +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/client.py +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/config.py +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/integrations/__init__.py +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/integrations/django.py +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/integrations/fastapi.py +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/dependency_links.txt +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/requires.txt +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/top_level.txt +0 -0
- {watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: watchdock-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Application error tracking SDK for the Watchdock platform
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: error-tracking,observability,watchdock
|
|
@@ -88,8 +88,30 @@ watchdock_errors.capture_exception(exc)
|
|
|
88
88
|
|
|
89
89
|
# Capture a message
|
|
90
90
|
watchdock_errors.capture_message("Stripe webhook signature invalid")
|
|
91
|
+
|
|
92
|
+
# Capture a message with a custom level
|
|
93
|
+
watchdock_errors.capture_message("Queue depth high", level="warning")
|
|
91
94
|
```
|
|
92
95
|
|
|
96
|
+
### Event levels
|
|
97
|
+
|
|
98
|
+
Every event carries a `level`. Exceptions default to `"error"`; messages default to `"info"` unless you pass `level` explicitly to `capture_message`.
|
|
99
|
+
|
|
100
|
+
## Correlating with nginx requests
|
|
101
|
+
|
|
102
|
+
If your app is behind nginx and you've added `$request_id` to your access log format (see the [nginx log collection docs](https://watchdock.cc/docs/nginx-log-collection)) and forwarded it to your app via `proxy_set_header X-Request-Id $request_id;`, this SDK automatically reads that header off every captured request and attaches it as `trace_id` — no code changes needed. This lets WatchDock link a failed request in your nginx access logs directly to the exception it produced.
|
|
103
|
+
|
|
104
|
+
You can also pass `trace_id` explicitly, which takes priority over the auto-extracted value:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
watchdock_errors.capture_exception(exc, trace_id=my_trace_id)
|
|
108
|
+
watchdock_errors.capture_message("Queue depth high", level="warning", trace_id=my_trace_id)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## SDK initialization
|
|
112
|
+
|
|
113
|
+
When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the platform (with the SDK version and environment) to register that it started up. This never blocks application startup and any failure is silently ignored.
|
|
114
|
+
|
|
93
115
|
## PII scrubbing
|
|
94
116
|
|
|
95
117
|
By default, `Authorization`, `Cookie`, and `X-Api-Key` headers are stripped and the request body is not sent. Set `send_pii=True` to disable scrubbing.
|
|
@@ -68,8 +68,30 @@ watchdock_errors.capture_exception(exc)
|
|
|
68
68
|
|
|
69
69
|
# Capture a message
|
|
70
70
|
watchdock_errors.capture_message("Stripe webhook signature invalid")
|
|
71
|
+
|
|
72
|
+
# Capture a message with a custom level
|
|
73
|
+
watchdock_errors.capture_message("Queue depth high", level="warning")
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
### Event levels
|
|
77
|
+
|
|
78
|
+
Every event carries a `level`. Exceptions default to `"error"`; messages default to `"info"` unless you pass `level` explicitly to `capture_message`.
|
|
79
|
+
|
|
80
|
+
## Correlating with nginx requests
|
|
81
|
+
|
|
82
|
+
If your app is behind nginx and you've added `$request_id` to your access log format (see the [nginx log collection docs](https://watchdock.cc/docs/nginx-log-collection)) and forwarded it to your app via `proxy_set_header X-Request-Id $request_id;`, this SDK automatically reads that header off every captured request and attaches it as `trace_id` — no code changes needed. This lets WatchDock link a failed request in your nginx access logs directly to the exception it produced.
|
|
83
|
+
|
|
84
|
+
You can also pass `trace_id` explicitly, which takes priority over the auto-extracted value:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
watchdock_errors.capture_exception(exc, trace_id=my_trace_id)
|
|
88
|
+
watchdock_errors.capture_message("Queue depth high", level="warning", trace_id=my_trace_id)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## SDK initialization
|
|
92
|
+
|
|
93
|
+
When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the platform (with the SDK version and environment) to register that it started up. This never blocks application startup and any failure is silently ignored.
|
|
94
|
+
|
|
73
95
|
## PII scrubbing
|
|
74
96
|
|
|
75
97
|
By default, `Authorization`, `Cookie`, and `X-Api-Key` headers are stripped and the request body is not sent. Set `send_pii=True` to disable scrubbing.
|
|
@@ -77,13 +77,21 @@ def init(
|
|
|
77
77
|
_send_init_ping(_config)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def capture_exception(
|
|
80
|
+
def capture_exception(
|
|
81
|
+
exc: BaseException | None = None,
|
|
82
|
+
request_context: dict | None = None,
|
|
83
|
+
trace_id: str | None = None,
|
|
84
|
+
) -> None:
|
|
81
85
|
"""
|
|
82
86
|
Capture an exception and send it to Watchdock.
|
|
83
87
|
|
|
84
88
|
If ``exc`` is None the current exception from ``sys.exc_info()`` is used.
|
|
85
89
|
Safe to call outside of an except block — does nothing if there is no
|
|
86
90
|
active exception and ``exc`` is not provided.
|
|
91
|
+
|
|
92
|
+
``trace_id`` correlates this event with the originating nginx request
|
|
93
|
+
(e.g. ``X-Request-Id``) and takes priority over any value auto-extracted
|
|
94
|
+
from ``request_context``'s headers if both are present.
|
|
87
95
|
"""
|
|
88
96
|
if _client is None or _config is None:
|
|
89
97
|
return
|
|
@@ -97,7 +105,7 @@ def capture_exception(exc: BaseException | None = None, request_context: dict |
|
|
|
97
105
|
|
|
98
106
|
from .event import build_event
|
|
99
107
|
|
|
100
|
-
event = build_event(exc, _config, request_context=request_context)
|
|
108
|
+
event = build_event(exc, _config, request_context=request_context, trace_id=trace_id)
|
|
101
109
|
if event is not None:
|
|
102
110
|
logger.info("watchdock_errors: capturing exception — %s: %s", type(exc).__name__, exc)
|
|
103
111
|
_client.capture(event)
|
|
@@ -105,14 +113,27 @@ def capture_exception(exc: BaseException | None = None, request_context: dict |
|
|
|
105
113
|
logger.debug("watchdock_errors: event dropped by before_send hook")
|
|
106
114
|
|
|
107
115
|
|
|
108
|
-
def capture_message(
|
|
109
|
-
|
|
116
|
+
def capture_message(
|
|
117
|
+
message: str,
|
|
118
|
+
level: str = "info",
|
|
119
|
+
request_context: dict | None = None,
|
|
120
|
+
trace_id: str | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Capture an arbitrary message string and send it to Watchdock.
|
|
124
|
+
|
|
125
|
+
``trace_id`` correlates this event with the originating nginx request
|
|
126
|
+
(e.g. ``X-Request-Id``) and takes priority over any value auto-extracted
|
|
127
|
+
from ``request_context``'s headers if both are present.
|
|
128
|
+
"""
|
|
110
129
|
if _client is None or _config is None:
|
|
111
130
|
return
|
|
112
131
|
|
|
113
132
|
from .event import build_event
|
|
114
133
|
|
|
115
|
-
event = build_event(
|
|
134
|
+
event = build_event(
|
|
135
|
+
None, _config, message=message, level=level, request_context=request_context, trace_id=trace_id
|
|
136
|
+
)
|
|
116
137
|
if event is not None:
|
|
117
138
|
logger.info("watchdock_errors: capturing message — %s", message)
|
|
118
139
|
_client.capture(event)
|
|
@@ -4,7 +4,7 @@ import datetime
|
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
6
|
from .config import SDKConfig
|
|
7
|
-
from .utils import extract_stacktrace, get_server_info
|
|
7
|
+
from .utils import extract_stacktrace, extract_trace_id, get_server_info
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger("watchdock_errors")
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ def build_event(
|
|
|
15
15
|
message: str | None = None,
|
|
16
16
|
level: str | None = None,
|
|
17
17
|
request_context: dict | None = None,
|
|
18
|
+
trace_id: str | None = None,
|
|
18
19
|
) -> dict | None:
|
|
19
20
|
"""
|
|
20
21
|
Build a Watchdock error event payload.
|
|
@@ -61,6 +62,12 @@ def build_event(
|
|
|
61
62
|
if config.server_name:
|
|
62
63
|
event["server"]["server_name"] = config.server_name
|
|
63
64
|
|
|
65
|
+
resolved_trace_id = trace_id or (
|
|
66
|
+
extract_trace_id(request_context.get("request", {}).get("headers", {})) if request_context else None
|
|
67
|
+
)
|
|
68
|
+
if resolved_trace_id:
|
|
69
|
+
event["trace_id"] = resolved_trace_id
|
|
70
|
+
|
|
64
71
|
if request_context:
|
|
65
72
|
if config.send_pii:
|
|
66
73
|
event["request"] = request_context.get("request", {})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import linecache
|
|
4
|
+
import platform
|
|
5
|
+
import socket
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
#: Number of source lines captured on each side of the failing line. Matches the
|
|
10
|
+
#: window the backend uses when it falls back to enriching frames itself, so a
|
|
11
|
+
#: frame looks the same whether the context came from here or from the server.
|
|
12
|
+
CONTEXT_LINES = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _source_context(filename: str, lineno: int) -> tuple[list[str], list[str]]:
|
|
16
|
+
"""Read the source lines immediately before and after ``lineno``.
|
|
17
|
+
|
|
18
|
+
Returns ``(pre_context, post_context)``. We do this in the SDK, on the box
|
|
19
|
+
where the code actually runs, because the source is here — the backend can
|
|
20
|
+
only guess at it from its own filesystem, which is the wrong machine for any
|
|
21
|
+
real app. Out-of-range lines come back as empty strings so the two lists are
|
|
22
|
+
always the same length regardless of where in the file the error landed.
|
|
23
|
+
"""
|
|
24
|
+
if not filename or not lineno:
|
|
25
|
+
return [], []
|
|
26
|
+
|
|
27
|
+
pre = [
|
|
28
|
+
linecache.getline(filename, i).rstrip("\n")
|
|
29
|
+
for i in range(max(1, lineno - CONTEXT_LINES), lineno)
|
|
30
|
+
]
|
|
31
|
+
post = [
|
|
32
|
+
linecache.getline(filename, i).rstrip("\n")
|
|
33
|
+
for i in range(lineno + 1, lineno + CONTEXT_LINES + 1)
|
|
34
|
+
]
|
|
35
|
+
return pre, post
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_stacktrace(exc: BaseException) -> list[dict]:
|
|
39
|
+
"""Extract structured stack frames from an exception."""
|
|
40
|
+
tb = exc.__traceback__
|
|
41
|
+
if tb is None:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
frames = []
|
|
45
|
+
for frame_summary in traceback.extract_tb(tb):
|
|
46
|
+
context_line = linecache.getline(frame_summary.filename, frame_summary.lineno).strip()
|
|
47
|
+
pre_context, post_context = _source_context(frame_summary.filename, frame_summary.lineno)
|
|
48
|
+
frames.append(
|
|
49
|
+
{
|
|
50
|
+
"filename": frame_summary.filename,
|
|
51
|
+
"function": frame_summary.name,
|
|
52
|
+
"lineno": frame_summary.lineno,
|
|
53
|
+
"context_line": context_line,
|
|
54
|
+
"pre_context": pre_context,
|
|
55
|
+
"post_context": post_context,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
return frames
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_server_info() -> dict:
|
|
62
|
+
return {
|
|
63
|
+
"hostname": socket.gethostname(),
|
|
64
|
+
"python_version": platform.python_version(),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_trace_id(headers: dict | None) -> str | None:
|
|
69
|
+
"""
|
|
70
|
+
Pull a correlation ID off incoming request headers so this event can be
|
|
71
|
+
linked back to the nginx access/error log line for the same request.
|
|
72
|
+
|
|
73
|
+
Prefers ``X-Request-Id`` (nginx's built-in ``$request_id``, zero extra
|
|
74
|
+
modules required) and falls back to the trace-id segment of a W3C
|
|
75
|
+
``traceparent`` header if present.
|
|
76
|
+
"""
|
|
77
|
+
if not headers:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
normalized = {str(k).lower(): v for k, v in headers.items()}
|
|
81
|
+
|
|
82
|
+
request_id = normalized.get("x-request-id")
|
|
83
|
+
if request_id:
|
|
84
|
+
return request_id
|
|
85
|
+
|
|
86
|
+
traceparent = normalized.get("traceparent")
|
|
87
|
+
if traceparent:
|
|
88
|
+
parts = traceparent.split("-")
|
|
89
|
+
if len(parts) >= 2 and parts[1]:
|
|
90
|
+
return parts[1]
|
|
91
|
+
|
|
92
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: watchdock-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Application error tracking SDK for the Watchdock platform
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: error-tracking,observability,watchdock
|
|
@@ -88,8 +88,30 @@ watchdock_errors.capture_exception(exc)
|
|
|
88
88
|
|
|
89
89
|
# Capture a message
|
|
90
90
|
watchdock_errors.capture_message("Stripe webhook signature invalid")
|
|
91
|
+
|
|
92
|
+
# Capture a message with a custom level
|
|
93
|
+
watchdock_errors.capture_message("Queue depth high", level="warning")
|
|
91
94
|
```
|
|
92
95
|
|
|
96
|
+
### Event levels
|
|
97
|
+
|
|
98
|
+
Every event carries a `level`. Exceptions default to `"error"`; messages default to `"info"` unless you pass `level` explicitly to `capture_message`.
|
|
99
|
+
|
|
100
|
+
## Correlating with nginx requests
|
|
101
|
+
|
|
102
|
+
If your app is behind nginx and you've added `$request_id` to your access log format (see the [nginx log collection docs](https://watchdock.cc/docs/nginx-log-collection)) and forwarded it to your app via `proxy_set_header X-Request-Id $request_id;`, this SDK automatically reads that header off every captured request and attaches it as `trace_id` — no code changes needed. This lets WatchDock link a failed request in your nginx access logs directly to the exception it produced.
|
|
103
|
+
|
|
104
|
+
You can also pass `trace_id` explicitly, which takes priority over the auto-extracted value:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
watchdock_errors.capture_exception(exc, trace_id=my_trace_id)
|
|
108
|
+
watchdock_errors.capture_message("Queue depth high", level="warning", trace_id=my_trace_id)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## SDK initialization
|
|
112
|
+
|
|
113
|
+
When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the platform (with the SDK version and environment) to register that it started up. This never blocks application startup and any failure is silently ignored.
|
|
114
|
+
|
|
93
115
|
## PII scrubbing
|
|
94
116
|
|
|
95
117
|
By default, `Authorization`, `Cookie`, and `X-Api-Key` headers are stripped and the request body is not sent. Set `send_pii=True` to disable scrubbing.
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/scm_file_list.json
RENAMED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
+
".gitignore",
|
|
3
4
|
"README.md",
|
|
4
5
|
"pyproject.toml",
|
|
5
|
-
".
|
|
6
|
-
"src/watchdock_errors/
|
|
6
|
+
"src/watchdock_errors/event.py",
|
|
7
|
+
"src/watchdock_errors/client.py",
|
|
7
8
|
"src/watchdock_errors/config.py",
|
|
8
9
|
"src/watchdock_errors/utils.py",
|
|
9
|
-
"src/watchdock_errors/
|
|
10
|
-
"src/watchdock_errors/event.py",
|
|
11
|
-
"src/watchdock_errors/integrations/__init__.py",
|
|
10
|
+
"src/watchdock_errors/__init__.py",
|
|
12
11
|
"src/watchdock_errors/integrations/fastapi.py",
|
|
13
12
|
"src/watchdock_errors/integrations/django.py",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"src/watchdock_errors/integrations/__init__.py",
|
|
14
|
+
".github/workflows/ci.yml",
|
|
16
15
|
".github/workflows/release.yml",
|
|
17
|
-
"
|
|
16
|
+
"tests/test_event.py",
|
|
17
|
+
"tests/test_client.py",
|
|
18
|
+
"tests/test_utils.py"
|
|
18
19
|
]
|
|
19
20
|
}
|
|
@@ -94,3 +94,54 @@ def test_pii_headers_scrubbed_by_default(config):
|
|
|
94
94
|
|
|
95
95
|
assert "Authorization" not in event["request"]["headers"]
|
|
96
96
|
assert event["request"]["headers"]["Content-Type"] == "application/json"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_trace_id_extracted_from_x_request_id_header(config):
|
|
100
|
+
req_ctx = {
|
|
101
|
+
"request": {
|
|
102
|
+
"method": "GET",
|
|
103
|
+
"url": "/checkout/",
|
|
104
|
+
"headers": {"X-Request-Id": "abc123"},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
raise ValueError("x")
|
|
110
|
+
except ValueError as exc:
|
|
111
|
+
event = build_event(exc, config, request_context=req_ctx)
|
|
112
|
+
|
|
113
|
+
assert event["trace_id"] == "abc123"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_trace_id_falls_back_to_traceparent_header(config):
|
|
117
|
+
req_ctx = {
|
|
118
|
+
"request": {
|
|
119
|
+
"headers": {"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
event = build_event(None, config, message="hi", request_context=req_ctx)
|
|
124
|
+
|
|
125
|
+
assert event["trace_id"] == "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_trace_id_absent_when_no_matching_header(config):
|
|
129
|
+
req_ctx = {"request": {"headers": {"Content-Type": "application/json"}}}
|
|
130
|
+
|
|
131
|
+
event = build_event(None, config, message="hi", request_context=req_ctx)
|
|
132
|
+
|
|
133
|
+
assert "trace_id" not in event
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_explicit_trace_id_wins_over_header_extraction(config):
|
|
137
|
+
req_ctx = {"request": {"headers": {"X-Request-Id": "from-header"}}}
|
|
138
|
+
|
|
139
|
+
event = build_event(None, config, message="hi", request_context=req_ctx, trace_id="explicit-id")
|
|
140
|
+
|
|
141
|
+
assert event["trace_id"] == "explicit-id"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_explicit_trace_id_works_without_request_context(config):
|
|
145
|
+
event = build_event(None, config, message="hi", trace_id="explicit-id")
|
|
146
|
+
|
|
147
|
+
assert event["trace_id"] == "explicit-id"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from watchdock_errors.utils import extract_stacktrace
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _boom():
|
|
5
|
+
# This line and the two on either side are what the context capture should
|
|
6
|
+
# pick up. Keep the surrounding lines stable so the assertions below hold.
|
|
7
|
+
x = 1
|
|
8
|
+
raise ValueError("boom") # <-- failing line
|
|
9
|
+
return x
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_extract_stacktrace_captures_source_context():
|
|
13
|
+
try:
|
|
14
|
+
_boom()
|
|
15
|
+
except ValueError as exc:
|
|
16
|
+
frames = extract_stacktrace(exc)
|
|
17
|
+
|
|
18
|
+
assert frames, "expected at least one frame"
|
|
19
|
+
|
|
20
|
+
# The frame for _boom is the one whose failing line raised.
|
|
21
|
+
boom_frame = next(f for f in frames if f["function"] == "_boom")
|
|
22
|
+
|
|
23
|
+
assert boom_frame["context_line"] == 'raise ValueError("boom") # <-- failing line'
|
|
24
|
+
# Two lines before and two after, in order, with indentation preserved.
|
|
25
|
+
assert len(boom_frame["pre_context"]) == 2
|
|
26
|
+
assert len(boom_frame["post_context"]) == 2
|
|
27
|
+
# The line immediately before the raise is `x = 1` (indentation kept).
|
|
28
|
+
assert boom_frame["pre_context"][1] == " x = 1"
|
|
29
|
+
# The line immediately after the raise is the return.
|
|
30
|
+
assert boom_frame["post_context"][0] == " return x"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_frames_always_have_context_keys():
|
|
34
|
+
try:
|
|
35
|
+
raise KeyError("missing")
|
|
36
|
+
except KeyError as exc:
|
|
37
|
+
frames = extract_stacktrace(exc)
|
|
38
|
+
|
|
39
|
+
for frame in frames:
|
|
40
|
+
assert "context_line" in frame
|
|
41
|
+
assert "pre_context" in frame
|
|
42
|
+
assert "post_context" in frame
|
|
43
|
+
assert isinstance(frame["pre_context"], list)
|
|
44
|
+
assert isinstance(frame["post_context"], list)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import linecache
|
|
4
|
-
import platform
|
|
5
|
-
import socket
|
|
6
|
-
import traceback
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def extract_stacktrace(exc: BaseException) -> list[dict]:
|
|
10
|
-
"""Extract structured stack frames from an exception."""
|
|
11
|
-
tb = exc.__traceback__
|
|
12
|
-
if tb is None:
|
|
13
|
-
return []
|
|
14
|
-
|
|
15
|
-
frames = []
|
|
16
|
-
for frame_summary in traceback.extract_tb(tb):
|
|
17
|
-
context_line = linecache.getline(frame_summary.filename, frame_summary.lineno).strip()
|
|
18
|
-
frames.append(
|
|
19
|
-
{
|
|
20
|
-
"filename": frame_summary.filename,
|
|
21
|
-
"function": frame_summary.name,
|
|
22
|
-
"lineno": frame_summary.lineno,
|
|
23
|
-
"context_line": context_line,
|
|
24
|
-
}
|
|
25
|
-
)
|
|
26
|
-
return frames
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_server_info() -> dict:
|
|
30
|
-
return {
|
|
31
|
-
"hostname": socket.gethostname(),
|
|
32
|
-
"python_version": platform.python_version(),
|
|
33
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/integrations/__init__.py
RENAMED
|
File without changes
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/integrations/django.py
RENAMED
|
File without changes
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors/integrations/fastapi.py
RENAMED
|
File without changes
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/requires.txt
RENAMED
|
File without changes
|
{watchdock_errors-0.2.3 → watchdock_errors-0.3.0}/src/watchdock_errors.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|