devlite 0.1.1__tar.gz → 0.1.2__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 (29) hide show
  1. {devlite-0.1.1 → devlite-0.1.2}/PKG-INFO +1 -1
  2. {devlite-0.1.1 → devlite-0.1.2}/devlite/_client.py +6 -6
  3. {devlite-0.1.1 → devlite-0.1.2}/devlite/_context.py +18 -1
  4. {devlite-0.1.1 → devlite-0.1.2}/devlite/_django.py +6 -1
  5. {devlite-0.1.1 → devlite-0.1.2}/devlite/_errors.py +2 -1
  6. {devlite-0.1.1 → devlite-0.1.2}/devlite/_fastapi.py +6 -1
  7. {devlite-0.1.1 → devlite-0.1.2}/devlite/_flask.py +10 -3
  8. {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/PKG-INFO +1 -1
  9. {devlite-0.1.1 → devlite-0.1.2}/pyproject.toml +1 -1
  10. {devlite-0.1.1 → devlite-0.1.2}/tests/test_context.py +25 -0
  11. {devlite-0.1.1 → devlite-0.1.2}/tests/test_fastapi.py +10 -0
  12. {devlite-0.1.1 → devlite-0.1.2}/LICENSE +0 -0
  13. {devlite-0.1.1 → devlite-0.1.2}/README.md +0 -0
  14. {devlite-0.1.1 → devlite-0.1.2}/devlite/__init__.py +0 -0
  15. {devlite-0.1.1 → devlite-0.1.2}/devlite/_config.py +0 -0
  16. {devlite-0.1.1 → devlite-0.1.2}/devlite/_metrics.py +0 -0
  17. {devlite-0.1.1 → devlite-0.1.2}/devlite/_queue.py +0 -0
  18. {devlite-0.1.1 → devlite-0.1.2}/devlite/_scrub.py +0 -0
  19. {devlite-0.1.1 → devlite-0.1.2}/devlite/_transport.py +0 -0
  20. {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/SOURCES.txt +0 -0
  21. {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/dependency_links.txt +0 -0
  22. {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/requires.txt +0 -0
  23. {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/top_level.txt +0 -0
  24. {devlite-0.1.1 → devlite-0.1.2}/setup.cfg +0 -0
  25. {devlite-0.1.1 → devlite-0.1.2}/tests/test_django.py +0 -0
  26. {devlite-0.1.1 → devlite-0.1.2}/tests/test_errors.py +0 -0
  27. {devlite-0.1.1 → devlite-0.1.2}/tests/test_queue.py +0 -0
  28. {devlite-0.1.1 → devlite-0.1.2}/tests/test_scrub.py +0 -0
  29. {devlite-0.1.1 → devlite-0.1.2}/tests/test_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlite
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: DevLite Python SDK — AI-powered observability with a 2-line integration. Automatic error grouping, source context, user impact tracking, and sensitive-data scrubbing built in.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://devlite.io
@@ -5,7 +5,7 @@ import threading
5
5
  import uuid
6
6
 
7
7
  from ._config import build_config
8
- from ._context import Context, get_user, now_ms, run_with_context
8
+ from ._context import Context, get_trace_id, get_user, new_trace_id, now_ms, run_with_context
9
9
  from ._errors import build_error_event
10
10
  from ._metrics import collect_host_metrics
11
11
  from ._scrub import scrub_deep
@@ -25,7 +25,7 @@ class Span:
25
25
  self.type = "span"
26
26
  self.id = options.get("id") or _new_hex(6)
27
27
  self.name = name
28
- self.trace_id = options.get("trace_id") or _new_hex(8)
28
+ self.trace_id = options.get("trace_id") or get_trace_id() or new_trace_id()
29
29
  self.parent_id = options.get("parent_id")
30
30
  self.tags = options.get("tags") or {}
31
31
  self.start_time = now_ms()
@@ -103,7 +103,7 @@ class Client:
103
103
  self.transport.enqueue(event)
104
104
 
105
105
  def capture_message(self, message, level="info", extra=None):
106
- raw = {"type": "message", "level": level, "message": message, "extra": extra or {}, "timestamp": now_ms()}
106
+ raw = {"type": "message", "level": level, "message": message, "extra": extra or {}, "traceId": get_trace_id(), "timestamp": now_ms()}
107
107
  self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
108
108
 
109
109
  def report_metric(self, name, value, unit=None, tags=None):
@@ -111,11 +111,11 @@ class Client:
111
111
  raise ValueError("[DevLite] report_metric() requires a string `name`.")
112
112
  if not isinstance(value, (int, float)):
113
113
  raise ValueError("[DevLite] report_metric() requires a numeric `value`.")
114
- raw = {"type": "metric", "name": name, "value": value, "unit": unit or None, "tags": tags or {}, "timestamp": now_ms()}
114
+ raw = {"type": "metric", "name": name, "value": value, "unit": unit or None, "tags": tags or {}, "traceId": get_trace_id(), "timestamp": now_ms()}
115
115
  self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
116
116
 
117
117
  def capture_log(self, message, level="info", fields=None):
118
- raw = {"type": "log", "level": level, "message": message, "fields": fields or {}, "timestamp": now_ms()}
118
+ raw = {"type": "log", "level": level, "message": message, "fields": fields or {}, "traceId": get_trace_id(), "timestamp": now_ms()}
119
119
  self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
120
120
 
121
121
  def start_span(self, name, options=None):
@@ -126,7 +126,7 @@ class Client:
126
126
  "type": "span",
127
127
  "id": _new_hex(6),
128
128
  "name": name,
129
- "traceId": trace_id or _new_hex(8),
129
+ "traceId": trace_id or get_trace_id() or new_trace_id(),
130
130
  "parentId": parent_id,
131
131
  "tags": tags or {},
132
132
  "startTime": start_time,
@@ -1,4 +1,4 @@
1
- """Request-scoped context: breadcrumbs, tags, and the current user.
1
+ """Request-scoped context: breadcrumbs, tags, the current user, and traceId.
2
2
 
3
3
  Python equivalent of context.js + userContext.js from the Node SDK. Uses
4
4
  contextvars (the modern replacement for thread-locals) so user context set
@@ -6,6 +6,7 @@ inside one request handler never leaks across concurrent requests.
6
6
  """
7
7
 
8
8
  import contextvars
9
+ import secrets
9
10
  import time
10
11
 
11
12
  MAX_BREADCRUMBS = 20
@@ -13,6 +14,11 @@ MAX_BREADCRUMBS = 20
13
14
  _request_store = contextvars.ContextVar("devlite_request_store", default=None)
14
15
 
15
16
 
17
+ def new_trace_id():
18
+ """A fresh 16-hex-char traceId (8 bytes), matching the Node SDK."""
19
+ return secrets.token_hex(8)
20
+
21
+
16
22
  def _store():
17
23
  store = _request_store.get()
18
24
  if store is None:
@@ -82,6 +88,17 @@ def set_sampled(value):
82
88
  store["sampled"] = value
83
89
 
84
90
 
91
+ def set_trace_id(trace_id):
92
+ """Scope the current request's traceId so captured events link to it."""
93
+ store = _store()
94
+ store["traceId"] = trace_id
95
+
96
+
97
+ def get_trace_id():
98
+ store = _request_store.get()
99
+ return (store or {}).get("traceId") or None
100
+
101
+
85
102
  def is_sampled_out():
86
103
  store = _request_store.get()
87
104
  return bool(store) and store.get("sampled") is False
@@ -69,8 +69,10 @@ class InstrumentDjangoMiddleware:
69
69
  config = client.config
70
70
 
71
71
  reset_context() # fresh request context (also clears inherited/stale stores)
72
- from ._context import set_sampled
72
+ from ._context import new_trace_id, set_sampled, set_trace_id
73
73
 
74
+ trace_id = new_trace_id()
75
+ set_trace_id(trace_id)
74
76
  if self.sample_rate is None:
75
77
  self._configure_sampling(config)
76
78
  set_sampled(self._sampled())
@@ -94,6 +96,7 @@ class InstrumentDjangoMiddleware:
94
96
  "path": path,
95
97
  "statusCode": status,
96
98
  "durationMs": duration_ms,
99
+ "traceId": trace_id,
97
100
  }
98
101
  )
99
102
 
@@ -109,6 +112,7 @@ class InstrumentDjangoMiddleware:
109
112
  "statusCode": status,
110
113
  "durationMs": duration_ms,
111
114
  "headers": headers,
115
+ "traceId": trace_id,
112
116
  "timestamp": start,
113
117
  }
114
118
  )
@@ -119,6 +123,7 @@ class InstrumentDjangoMiddleware:
119
123
  "method": method,
120
124
  "path": path,
121
125
  "durationMs": duration_ms,
126
+ "traceId": trace_id,
122
127
  "timestamp": start,
123
128
  }
124
129
  )
@@ -9,7 +9,7 @@ import os
9
9
  import re
10
10
  import traceback
11
11
 
12
- from ._context import now_ms
12
+ from ._context import get_trace_id, now_ms
13
13
  from ._scrub import scrub_deep
14
14
 
15
15
  _NUMBER_RE = re.compile(r"\d+")
@@ -102,6 +102,7 @@ def build_error_event(exc, config, breadcrumbs=None, user=None, extra=None, fata
102
102
  "sourceContext": source_context,
103
103
  "user": user or None,
104
104
  "breadcrumbs": breadcrumbs or [],
105
+ "traceId": get_trace_id(),
105
106
  "extra": extra,
106
107
  "timestamp": now_ms(),
107
108
  }
@@ -54,8 +54,10 @@ class _DevLiteASGIMiddleware:
54
54
  return await self.app(scope, receive, send)
55
55
 
56
56
  reset_context() # fresh request context (also clears inherited/stale stores)
57
- from ._context import set_sampled
57
+ from ._context import new_trace_id, set_sampled, set_trace_id
58
58
 
59
+ trace_id = new_trace_id()
60
+ set_trace_id(trace_id)
59
61
  set_sampled(self._sampled())
60
62
 
61
63
  start = now_ms()
@@ -90,6 +92,7 @@ class _DevLiteASGIMiddleware:
90
92
  "path": path,
91
93
  "statusCode": status[0],
92
94
  "durationMs": duration_ms,
95
+ "traceId": trace_id,
93
96
  }
94
97
  )
95
98
 
@@ -108,6 +111,7 @@ class _DevLiteASGIMiddleware:
108
111
  "statusCode": status[0],
109
112
  "durationMs": duration_ms,
110
113
  "headers": headers,
114
+ "traceId": trace_id,
111
115
  "timestamp": start,
112
116
  }
113
117
  )
@@ -118,6 +122,7 @@ class _DevLiteASGIMiddleware:
118
122
  "method": method,
119
123
  "path": path,
120
124
  "durationMs": duration_ms,
125
+ "traceId": trace_id,
121
126
  "timestamp": start,
122
127
  }
123
128
  )
@@ -49,11 +49,12 @@ def instrument_flask(app, transport, context, config):
49
49
  @app.before_request
50
50
  def _start():
51
51
  reset_context() # fresh request context
52
+ from ._context import new_trace_id, set_sampled, set_trace_id
53
+
54
+ set_trace_id(new_trace_id())
52
55
  _record_start()
53
56
  counter[0] += 1
54
57
  sampled = sample_rate >= 1.0 or (counter[0] % int(sample_every) == 0)
55
- from ._context import set_sampled
56
-
57
58
  set_sampled(sampled)
58
59
 
59
60
  @app.after_request
@@ -67,7 +68,11 @@ def instrument_flask(app, transport, context, config):
67
68
  method = meta.get("method") or (response.request.method if getattr(response, "request", None) else "GET")
68
69
  status = response.status_code
69
70
 
70
- context.add_breadcrumb({"type": "request", "method": method, "path": path, "statusCode": status, "durationMs": duration_ms})
71
+ from ._context import get_trace_id
72
+
73
+ trace_id = get_trace_id()
74
+
75
+ context.add_breadcrumb({"type": "request", "method": method, "path": path, "statusCode": status, "durationMs": duration_ms, "traceId": trace_id})
71
76
 
72
77
  headers = None
73
78
  if capture_body and getattr(response, "request", None):
@@ -80,6 +85,7 @@ def instrument_flask(app, transport, context, config):
80
85
  "statusCode": status,
81
86
  "durationMs": duration_ms,
82
87
  "headers": headers,
88
+ "traceId": trace_id,
83
89
  "timestamp": int(start),
84
90
  })
85
91
  if duration_ms > SLOW_REQUEST_THRESHOLD_MS:
@@ -88,6 +94,7 @@ def instrument_flask(app, transport, context, config):
88
94
  "method": method,
89
95
  "path": path,
90
96
  "durationMs": duration_ms,
97
+ "traceId": trace_id,
91
98
  "timestamp": int(start),
92
99
  })
93
100
  return response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlite
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: DevLite Python SDK — AI-powered observability with a 2-line integration. Automatic error grouping, source context, user impact tracking, and sensitive-data scrubbing built in.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://devlite.io
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlite"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "DevLite Python SDK — AI-powered observability with a 2-line integration. Automatic error grouping, source context, user impact tracking, and sensitive-data scrubbing built in."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -4,15 +4,40 @@ import sys
4
4
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
5
5
 
6
6
  from devlite._context import ( # noqa: E402
7
+ get_trace_id,
7
8
  get_user,
8
9
  is_sampled_out,
10
+ new_trace_id,
9
11
  reset_context,
10
12
  run_with_context,
11
13
  set_sampled,
14
+ set_trace_id,
12
15
  set_user,
13
16
  )
14
17
 
15
18
 
19
+ def test_trace_id_scope_and_reset():
20
+ assert get_trace_id() is None
21
+
22
+ tid = new_trace_id()
23
+ assert len(tid) == 16
24
+
25
+ set_trace_id(tid)
26
+ assert get_trace_id() == tid
27
+
28
+ reset_context()
29
+ assert get_trace_id() is None
30
+
31
+
32
+ def test_run_with_context_restores_trace_id():
33
+ tid = new_trace_id()
34
+ set_trace_id(tid)
35
+ observed = []
36
+ run_with_context(lambda: observed.append(get_trace_id()))
37
+ assert observed == [None]
38
+ assert get_trace_id() == tid
39
+
40
+
16
41
  def test_reset_context_clears_stale_store():
17
42
  set_user({"id": "stale"})
18
43
  set_sampled(False)
@@ -53,6 +53,7 @@ def test_fastapi_request_and_error_events(ingest):
53
53
 
54
54
  devlite.set_user({"id": "u-7"})
55
55
  devlite.capture_error(Exception("captured in endpoint"))
56
+ devlite.capture_log("log from endpoint")
56
57
  return {"status": "ok"}
57
58
 
58
59
  with TestClient(app, raise_server_exceptions=False) as tc:
@@ -80,4 +81,13 @@ def test_fastapi_request_and_error_events(ingest):
80
81
  assert {r["method"] for r in reqs} == {"GET"}
81
82
  assert all(r["statusCode"] in (200, 500) for r in reqs)
82
83
 
84
+ # every request carries a 16-hex-char traceId, and the /capture request's
85
+ # error + log + span all share that request's traceId
86
+ assert all(r["traceId"] and len(r["traceId"]) == 16 for r in reqs)
87
+ capture_req = next(r for r in reqs if r["path"] == "/capture")
88
+ cap_tid = capture_req["traceId"]
89
+ assert captured["traceId"] == cap_tid
90
+ captured_log = next(e for e in events if e["type"] == "log")
91
+ assert captured_log["traceId"] == cap_tid
92
+
83
93
  c.close()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes