watchdock-errors 0.2.2__tar.gz → 0.2.4__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.
Files changed (25) hide show
  1. {watchdock_errors-0.2.2/src/watchdock_errors.egg-info → watchdock_errors-0.2.4}/PKG-INFO +23 -1
  2. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/README.md +22 -0
  3. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/__init__.py +26 -5
  4. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/event.py +13 -1
  5. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/utils.py +27 -0
  6. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4/src/watchdock_errors.egg-info}/PKG-INFO +23 -1
  7. watchdock_errors-0.2.4/src/watchdock_errors.egg-info/scm_version.json +8 -0
  8. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/tests/test_event.py +69 -0
  9. watchdock_errors-0.2.2/src/watchdock_errors.egg-info/scm_version.json +0 -8
  10. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/.github/workflows/ci.yml +0 -0
  11. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/.github/workflows/release.yml +0 -0
  12. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/.gitignore +0 -0
  13. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/pyproject.toml +0 -0
  14. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/setup.cfg +0 -0
  15. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/client.py +0 -0
  16. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/config.py +0 -0
  17. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/__init__.py +0 -0
  18. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/django.py +0 -0
  19. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/fastapi.py +0 -0
  20. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/SOURCES.txt +0 -0
  21. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/dependency_links.txt +0 -0
  22. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/requires.txt +0 -0
  23. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/scm_file_list.json +0 -0
  24. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/top_level.txt +0 -0
  25. {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdock-errors
3
- Version: 0.2.2
3
+ Version: 0.2.4
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(exc: BaseException | None = None, request_context: dict | None = None) -> None:
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(message: str, request_context: dict | None = None) -> None:
109
- """Capture an arbitrary message string and send it to Watchdock."""
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(None, _config, message=message, request_context=request_context)
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
 
@@ -13,7 +13,9 @@ def build_event(
13
13
  exc: BaseException | None,
14
14
  config: SDKConfig,
15
15
  message: str | None = None,
16
+ level: str | None = None,
16
17
  request_context: dict | None = None,
18
+ trace_id: str | None = None,
17
19
  ) -> dict | None:
18
20
  """
19
21
  Build a Watchdock error event payload.
@@ -22,6 +24,9 @@ def build_event(
22
24
  """
23
25
  now = datetime.datetime.now(datetime.timezone.utc).isoformat()
24
26
 
27
+ if level is None:
28
+ level = "error" if exc is not None else "info"
29
+
25
30
  if exc is not None:
26
31
  exception_data = {
27
32
  "type": type(exc).__name__,
@@ -41,6 +46,7 @@ def build_event(
41
46
  "project_key": config.api_key,
42
47
  "timestamp": now,
43
48
  "environment": config.environment,
49
+ "level": level,
44
50
  "title": title,
45
51
  "sdk": {
46
52
  "name": config.sdk_name,
@@ -56,6 +62,12 @@ def build_event(
56
62
  if config.server_name:
57
63
  event["server"]["server_name"] = config.server_name
58
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
+
59
71
  if request_context:
60
72
  if config.send_pii:
61
73
  event["request"] = request_context.get("request", {})
@@ -31,3 +31,30 @@ def get_server_info() -> dict:
31
31
  "hostname": socket.gethostname(),
32
32
  "python_version": platform.python_version(),
33
33
  }
34
+
35
+
36
+ def extract_trace_id(headers: dict | None) -> str | None:
37
+ """
38
+ Pull a correlation ID off incoming request headers so this event can be
39
+ linked back to the nginx access/error log line for the same request.
40
+
41
+ Prefers ``X-Request-Id`` (nginx's built-in ``$request_id``, zero extra
42
+ modules required) and falls back to the trace-id segment of a W3C
43
+ ``traceparent`` header if present.
44
+ """
45
+ if not headers:
46
+ return None
47
+
48
+ normalized = {str(k).lower(): v for k, v in headers.items()}
49
+
50
+ request_id = normalized.get("x-request-id")
51
+ if request_id:
52
+ return request_id
53
+
54
+ traceparent = normalized.get("traceparent")
55
+ if traceparent:
56
+ parts = traceparent.split("-")
57
+ if len(parts) >= 2 and parts[1]:
58
+ return parts[1]
59
+
60
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdock-errors
3
- Version: 0.2.2
3
+ Version: 0.2.4
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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.4",
3
+ "distance": 0,
4
+ "node": "g8207b34ddbb2d7b5c9fe4f4df0960161f5b11709",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-11"
8
+ }
@@ -30,6 +30,24 @@ def test_build_event_from_message(config):
30
30
  assert event is not None
31
31
  assert event["exception"]["message"] == "Stripe webhook failed"
32
32
  assert event["exception"]["type"] == "Message"
33
+ assert event["level"] == "info"
34
+
35
+
36
+ def test_build_event_level_override(config):
37
+ event = build_event(None, config, message="disk almost full", level="warning")
38
+
39
+ assert event is not None
40
+ assert event["level"] == "warning"
41
+
42
+
43
+ def test_build_event_exception_default_level(config):
44
+ try:
45
+ raise ValueError("bad")
46
+ except ValueError as exc:
47
+ event = build_event(exc, config)
48
+
49
+ assert event is not None
50
+ assert event["level"] == "error"
33
51
 
34
52
 
35
53
  def test_before_send_can_drop_event(config):
@@ -76,3 +94,54 @@ def test_pii_headers_scrubbed_by_default(config):
76
94
 
77
95
  assert "Authorization" not in event["request"]["headers"]
78
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"
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.2.2",
3
- "distance": 0,
4
- "node": "gefdacc6610345ad37dfcfd533d1a97715e23d6bd",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-06-29"
8
- }