watchdock-errors 0.4.0__tar.gz → 0.5.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.
Files changed (27) hide show
  1. {watchdock_errors-0.4.0/src/watchdock_errors.egg-info → watchdock_errors-0.5.0}/PKG-INFO +2 -2
  2. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/README.md +1 -1
  3. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/event.py +21 -5
  4. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/integrations/fastapi.py +9 -1
  5. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/utils.py +55 -0
  6. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0/src/watchdock_errors.egg-info}/PKG-INFO +2 -2
  7. watchdock_errors-0.5.0/src/watchdock_errors.egg-info/scm_version.json +8 -0
  8. watchdock_errors-0.4.0/src/watchdock_errors.egg-info/scm_version.json +0 -8
  9. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/.github/workflows/ci.yml +0 -0
  10. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/.github/workflows/release.yml +0 -0
  11. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/.gitignore +0 -0
  12. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/pyproject.toml +0 -0
  13. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/setup.cfg +0 -0
  14. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/__init__.py +0 -0
  15. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/client.py +0 -0
  16. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/config.py +0 -0
  17. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/integrations/__init__.py +0 -0
  18. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/integrations/celery.py +0 -0
  19. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors/integrations/django.py +0 -0
  20. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors.egg-info/SOURCES.txt +0 -0
  21. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors.egg-info/dependency_links.txt +0 -0
  22. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors.egg-info/requires.txt +0 -0
  23. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors.egg-info/scm_file_list.json +0 -0
  24. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/src/watchdock_errors.egg-info/top_level.txt +0 -0
  25. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/tests/test_client.py +0 -0
  26. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/tests/test_event.py +0 -0
  27. {watchdock_errors-0.4.0 → watchdock_errors-0.5.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdock-errors
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Application error tracking SDK for the Watchdock platform
5
5
  License: MIT
6
6
  Keywords: error-tracking,observability,watchdock
@@ -138,7 +138,7 @@ When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the p
138
138
 
139
139
  ## PII scrubbing
140
140
 
141
- 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.
141
+ By default, `Authorization`, `Cookie`, `Set-Cookie`, and `X-Api-Key` headers are stripped, the request body is not sent, and sensitive-looking query-param values (names containing `token`, `secret`, `password`, `auth`, `key`, `session`, `credential`, `otp`, `pin`, `ssn`, etc.) are redacted in both `query_params` and the captured `url` itself. Set `send_pii=True` to disable scrubbing.
142
142
 
143
143
  Use the `before_send` hook for custom scrubbing:
144
144
 
@@ -116,7 +116,7 @@ When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the p
116
116
 
117
117
  ## PII scrubbing
118
118
 
119
- 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.
119
+ By default, `Authorization`, `Cookie`, `Set-Cookie`, and `X-Api-Key` headers are stripped, the request body is not sent, and sensitive-looking query-param values (names containing `token`, `secret`, `password`, `auth`, `key`, `session`, `credential`, `otp`, `pin`, `ssn`, etc.) are redacted in both `query_params` and the captured `url` itself. Set `send_pii=True` to disable scrubbing.
120
120
 
121
121
  Use the `before_send` hook for custom scrubbing:
122
122
 
@@ -4,10 +4,17 @@ import datetime
4
4
  import logging
5
5
 
6
6
  from .config import SDKConfig
7
- from .utils import extract_stacktrace, extract_trace_id, get_server_info
7
+ from .utils import extract_stacktrace, extract_trace_id, get_server_info, sanitize_url_and_query_params
8
8
 
9
9
  logger = logging.getLogger("watchdock_errors")
10
10
 
11
+ #: Lowercased for comparison -- header casing isn't consistent across integrations (e.g.
12
+ #: Django's request.headers is Title-Case, but raw ASGI scope headers used by the FastAPI
13
+ #: integration are always lowercase per spec), so matching by exact Title-Case key silently
14
+ #: let Authorization/Cookie/X-Api-Key through unscrubbed for any integration that doesn't
15
+ #: happen to produce Django's casing.
16
+ _SENSITIVE_HEADER_NAMES = {"authorization", "cookie", "set-cookie", "x-api-key"}
17
+
11
18
 
12
19
  def build_event(
13
20
  exc: BaseException | None,
@@ -75,10 +82,19 @@ def build_event(
75
82
  else:
76
83
  req = dict(request_context.get("request", {}))
77
84
  req.pop("body", None)
78
- headers = dict(req.get("headers", {}))
79
- for sensitive in ("Authorization", "Cookie", "X-Api-Key"):
80
- headers.pop(sensitive, None)
81
- req["headers"] = headers
85
+ req["headers"] = {
86
+ key: value
87
+ for key, value in req.get("headers", {}).items()
88
+ if key.lower() not in _SENSITIVE_HEADER_NAMES
89
+ }
90
+ if "url" in req or "query_params" in req:
91
+ sanitized_url, sanitized_params = sanitize_url_and_query_params(
92
+ req.get("url", ""), req.get("query_params")
93
+ )
94
+ if "url" in req:
95
+ req["url"] = sanitized_url
96
+ if "query_params" in req:
97
+ req["query_params"] = sanitized_params
82
98
  event["request"] = req
83
99
 
84
100
  if config.before_send is not None:
@@ -10,6 +10,7 @@ Usage:
10
10
  from __future__ import annotations
11
11
 
12
12
  from typing import TYPE_CHECKING
13
+ from urllib.parse import parse_qs
13
14
 
14
15
  import watchdock_errors
15
16
 
@@ -37,15 +38,22 @@ class WatchdockASGIMiddleware:
37
38
 
38
39
 
39
40
  def _build_request_context(scope: dict) -> dict:
41
+ # ASGI header names are always lowercase bytes per spec (unlike Django's Title-Case
42
+ # request.headers) -- header scrubbing is case-insensitive, so this doesn't need to
43
+ # match Django's casing, just be a dict of decoded strings.
40
44
  headers = {k.decode(): v.decode() for k, v in scope.get("headers", [])}
41
45
  query_string = scope.get("query_string", b"").decode()
46
+ # dict, not the raw query string -- matches the shape every other integration produces
47
+ # (and what sanitize_url_and_query_params/the frontend expect). Single-value params
48
+ # come through as a plain string rather than a 1-item list, matching Django's shape.
49
+ query_params = {key: values[0] if len(values) == 1 else values for key, values in parse_qs(query_string).items()}
42
50
 
43
51
  return {
44
52
  "request": {
45
53
  "method": scope.get("method", ""),
46
54
  "url": _build_url(scope),
47
55
  "headers": headers,
48
- "query_params": query_string,
56
+ "query_params": query_params,
49
57
  }
50
58
  }
51
59
 
@@ -65,6 +65,61 @@ def get_server_info() -> dict:
65
65
  }
66
66
 
67
67
 
68
+ #: Substring match (case-insensitive) against query-param names. Deliberately broad —
69
+ #: over-redacting an innocuous param (e.g. "sort_key") is a much smaller cost than
70
+ #: leaking a reset token, session id, or api key sitting in a URL.
71
+ _SENSITIVE_QUERY_PARAM_SUBSTRINGS = (
72
+ "token",
73
+ "secret",
74
+ "password",
75
+ "passwd",
76
+ "pwd",
77
+ "auth",
78
+ "key",
79
+ "session",
80
+ "credential",
81
+ "otp",
82
+ "pin",
83
+ "ssn",
84
+ )
85
+
86
+
87
+ def _is_sensitive_param(name: str) -> bool:
88
+ lowered = name.lower()
89
+ return any(substring in lowered for substring in _SENSITIVE_QUERY_PARAM_SUBSTRINGS)
90
+
91
+
92
+ def sanitize_url_and_query_params(url: str, query_params: dict | None) -> tuple[str, dict]:
93
+ """Redact sensitive query-param values from both ``query_params`` and ``url`` itself.
94
+
95
+ Headers and the request body get their own scrubbing, but a URL is often the most
96
+ PII-dense thing captured by default -- password-reset tokens, magic-link codes,
97
+ ``?api_key=...``, session ids -- and previously passed through untouched even with
98
+ PII collection off. Rebuilds the url's query string from the redacted params rather
99
+ than just deleting it, so the two never disagree and the path (still useful for
100
+ grouping/debugging) is preserved.
101
+ """
102
+ from urllib.parse import urlencode, urlsplit, urlunsplit
103
+
104
+ sanitized_params: dict = {}
105
+ for key, value in (query_params or {}).items():
106
+ if _is_sensitive_param(key):
107
+ sanitized_params[key] = ["[REDACTED]"] * len(value) if isinstance(value, list) else "[REDACTED]"
108
+ else:
109
+ sanitized_params[key] = value
110
+
111
+ if not url:
112
+ return url, sanitized_params
113
+
114
+ parts = urlsplit(url)
115
+ if parts.query:
116
+ # doseq=True so list-valued params (e.g. Django QueryDict-style) re-encode correctly.
117
+ new_query = urlencode(sanitized_params, doseq=True)
118
+ url = urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment))
119
+
120
+ return url, sanitized_params
121
+
122
+
68
123
  def extract_trace_id(headers: dict | None) -> str | None:
69
124
  """
70
125
  Pull a correlation ID off incoming request headers so this event can be
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdock-errors
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Application error tracking SDK for the Watchdock platform
5
5
  License: MIT
6
6
  Keywords: error-tracking,observability,watchdock
@@ -138,7 +138,7 @@ When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the p
138
138
 
139
139
  ## PII scrubbing
140
140
 
141
- 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.
141
+ By default, `Authorization`, `Cookie`, `Set-Cookie`, and `X-Api-Key` headers are stripped, the request body is not sent, and sensitive-looking query-param values (names containing `token`, `secret`, `password`, `auth`, `key`, `session`, `credential`, `otp`, `pin`, `ssn`, etc.) are redacted in both `query_params` and the captured `url` itself. Set `send_pii=True` to disable scrubbing.
142
142
 
143
143
  Use the `before_send` hook for custom scrubbing:
144
144
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.5.0",
3
+ "distance": 0,
4
+ "node": "gb60ccd75b33be3b29188767aa21e15ab18047fb6",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-27"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.4.0",
3
- "distance": 0,
4
- "node": "gcbc7ed27dbd4e8399c6f64c46a993f99ab8f34ce",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-13"
8
- }