devlite 0.1.0__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 (30) hide show
  1. {devlite-0.1.0/devlite.egg-info → devlite-0.1.2}/PKG-INFO +57 -3
  2. devlite-0.1.0/PKG-INFO → devlite-0.1.2/README.md +176 -154
  3. {devlite-0.1.0 → devlite-0.1.2}/devlite/__init__.py +14 -2
  4. {devlite-0.1.0 → devlite-0.1.2}/devlite/_client.py +16 -6
  5. {devlite-0.1.0 → devlite-0.1.2}/devlite/_context.py +31 -1
  6. devlite-0.1.2/devlite/_django.py +158 -0
  7. {devlite-0.1.0 → devlite-0.1.2}/devlite/_errors.py +2 -1
  8. devlite-0.1.2/devlite/_fastapi.py +139 -0
  9. {devlite-0.1.0 → devlite-0.1.2}/devlite/_flask.py +12 -5
  10. {devlite-0.1.0 → devlite-0.1.2}/devlite/_transport.py +1 -1
  11. devlite-0.1.0/README.md → devlite-0.1.2/devlite.egg-info/PKG-INFO +208 -130
  12. {devlite-0.1.0 → devlite-0.1.2}/devlite.egg-info/SOURCES.txt +5 -0
  13. devlite-0.1.2/devlite.egg-info/requires.txt +16 -0
  14. {devlite-0.1.0 → devlite-0.1.2}/pyproject.toml +4 -2
  15. devlite-0.1.2/tests/test_context.py +76 -0
  16. devlite-0.1.2/tests/test_django.py +121 -0
  17. devlite-0.1.2/tests/test_fastapi.py +93 -0
  18. devlite-0.1.0/devlite.egg-info/requires.txt +0 -6
  19. {devlite-0.1.0 → devlite-0.1.2}/LICENSE +0 -0
  20. {devlite-0.1.0 → devlite-0.1.2}/devlite/_config.py +0 -0
  21. {devlite-0.1.0 → devlite-0.1.2}/devlite/_metrics.py +0 -0
  22. {devlite-0.1.0 → devlite-0.1.2}/devlite/_queue.py +0 -0
  23. {devlite-0.1.0 → devlite-0.1.2}/devlite/_scrub.py +0 -0
  24. {devlite-0.1.0 → devlite-0.1.2}/devlite.egg-info/dependency_links.txt +0 -0
  25. {devlite-0.1.0 → devlite-0.1.2}/devlite.egg-info/top_level.txt +0 -0
  26. {devlite-0.1.0 → devlite-0.1.2}/setup.cfg +0 -0
  27. {devlite-0.1.0 → devlite-0.1.2}/tests/test_errors.py +0 -0
  28. {devlite-0.1.0 → devlite-0.1.2}/tests/test_queue.py +0 -0
  29. {devlite-0.1.0 → devlite-0.1.2}/tests/test_scrub.py +0 -0
  30. {devlite-0.1.0 → 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.0
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
@@ -18,8 +18,16 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Provides-Extra: flask
20
20
  Requires-Dist: flask>=2.0; extra == "flask"
21
+ Provides-Extra: fastapi
22
+ Requires-Dist: fastapi>=0.100; extra == "fastapi"
23
+ Provides-Extra: django
24
+ Requires-Dist: django>=4.0; extra == "django"
21
25
  Provides-Extra: test
22
26
  Requires-Dist: pytest; extra == "test"
27
+ Requires-Dist: flask>=2.0; extra == "test"
28
+ Requires-Dist: fastapi>=0.100; extra == "test"
29
+ Requires-Dist: django>=4.0; extra == "test"
30
+ Requires-Dist: httpx>=0.27; extra == "test"
23
31
  Dynamic: license-file
24
32
 
25
33
  # devlite — Python SDK
@@ -31,8 +39,10 @@ This is the Python counterpart of [`@devlite/nodejs`](https://www.npmjs.com/pack
31
39
  ## Install
32
40
 
33
41
  ```bash
34
- pip install devlite # core (Flask optional)
42
+ pip install devlite # core (Flask/FastAPI/Django optional)
35
43
  pip install "devlite[flask]" # + Flask instrumentation
44
+ pip install "devlite[fastapi]" # + FastAPI instrumentation
45
+ pip install "devlite[django]" # + Django instrumentation
36
46
  ```
37
47
 
38
48
  ## 60-second quickstart (Flask)
@@ -56,6 +66,42 @@ That's it — every HTTP request, slow endpoint, and unhandled exception is now
56
66
 
57
67
  Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
58
68
 
69
+ ## Auto-instrumentation for every framework
70
+
71
+ One call per framework, with the same automatic request/slow-request/error/user tracking:
72
+
73
+ **FastAPI** (any Starlette app) — call right after creating the app:
74
+
75
+ ```python
76
+ from fastapi import FastAPI
77
+ import devlite
78
+
79
+ devlite.init(api_key="dl_live_xxxxx")
80
+
81
+ app = FastAPI()
82
+ devlite.instrument_fastapi(app) # before the server starts
83
+ ```
84
+
85
+ **Django** — `instrument_django()` inserts `devlite._django.InstrumentDjangoMiddleware` at the top of your `MIDDLEWARE` setting:
86
+
87
+ ```python
88
+ import devlite
89
+
90
+ devlite.init(api_key="dl_live_xxxxx")
91
+ devlite.instrument_django()
92
+ ```
93
+
94
+ Or add the middleware manually (must be near the top, index 0 ideally):
95
+
96
+ ```python
97
+ MIDDLEWARE = [
98
+ "devlite._django.InstrumentDjangoMiddleware",
99
+ ...
100
+ ]
101
+ ```
102
+
103
+ Any other framework (or plain WSGI/ASGI) still works through the manual API below.
104
+
59
105
  ## Manual capture API
60
106
 
61
107
  ```python
@@ -91,11 +137,19 @@ devlite.set_tag("region", "lagos")
91
137
  `set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
92
138
 
93
139
  ```python
140
+ # Flask
94
141
  from flask import request
95
142
 
96
143
  @app.before_request
97
144
  def identify():
98
145
  devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
146
+
147
+ # FastAPI — inside any endpoint
148
+ devlite.set_user({"id": "u-123", "email": "a@example.com"})
149
+
150
+ # Django — inside any view
151
+ def my_view(request):
152
+ devlite.set_user({"id": request.user.id, "email": request.user.email})
99
153
  ```
100
154
 
101
155
  ## Configuration options
@@ -145,7 +199,7 @@ devlite.close()
145
199
  ## Development
146
200
 
147
201
  ```bash
148
- pip install -e ".[flask,test]"
202
+ pip install -e ".[flask,fastapi,django,test]"
149
203
  python -m pytest
150
204
  ```
151
205
 
@@ -1,154 +1,176 @@
1
- Metadata-Version: 2.4
2
- Name: devlite
3
- Version: 0.1.0
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
- License: MIT
6
- Project-URL: Homepage, https://devlite.io
7
- Project-URL: Repository, https://github.com/Ishimwe-Kevin/devlite-app
8
- Keywords: observability,monitoring,apm,error-tracking,error-monitoring,logging,ai,devlite
9
- Classifier: Development Status :: 4 - Beta
10
- Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Topic :: Software Development :: Debuggers
15
- Classifier: Topic :: System :: Monitoring
16
- Requires-Python: >=3.9
17
- Description-Content-Type: text/markdown
18
- License-File: LICENSE
19
- Provides-Extra: flask
20
- Requires-Dist: flask>=2.0; extra == "flask"
21
- Provides-Extra: test
22
- Requires-Dist: pytest; extra == "test"
23
- Dynamic: license-file
24
-
25
- # devlite — Python SDK
26
-
27
- AI-powered observability for Python. Add two lines, get automatic request tracking, error capture, error grouping, source context, user tracking, and slow-endpoint detection — no config files, no manual instrumentation, **zero runtime dependencies**.
28
-
29
- This is the Python counterpart of [`@devlite/nodejs`](https://www.npmjs.com/package/@devlite/nodejs) — both SDKs speak the same batch protocol to the [DevLite ingest API](https://github.com/Ishimwe-Kevin/devlite-app).
30
-
31
- ## Install
32
-
33
- ```bash
34
- pip install devlite # core (Flask optional)
35
- pip install "devlite[flask]" # + Flask instrumentation
36
- ```
37
-
38
- ## 60-second quickstart (Flask)
39
-
40
- ```python
41
- from flask import Flask
42
- import devlite
43
-
44
- devlite.init(api_key="dl_live_xxxxx")
45
-
46
- app = Flask(__name__)
47
- devlite.instrument_flask(app)
48
- ```
49
-
50
- That's it every HTTP request, slow endpoint, and unhandled exception is now captured automatically, with:
51
-
52
- - **Grouped errors** — the same bug occurring 1,000 times shows up as one issue, not 1,000
53
- - **Real source code context** — the actual lines around the crash, read live from disk
54
- - **Automatic sensitive-data scrubbing** — emails, tokens, passwords redacted before anything leaves your process
55
- - **User impact tracking** — know exactly which users hit which bugs
56
-
57
- Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
58
-
59
- ## Manual capture API
60
-
61
- ```python
62
- # Capture a handled error with extra context
63
- devlite.capture_error(err, {"userId": "123", "action": "checkout"})
64
- devlite.capture_error(err, {"fingerprint": "payment-timeout"}) # force grouping key
65
-
66
- # Non-error event
67
- devlite.capture_message("Payment retried after timeout", "warning")
68
-
69
- # Custom metric — feeds forecasting/anomaly detection
70
- devlite.report_metric("order.total", 42.5, unit="USD", tags={"region": "lagos"})
71
-
72
- # Structured log line
73
- devlite.capture_log("checkout completed", "info", {"orderId": "ord_123", "durationMs": 250})
74
-
75
- # Trace a unit of work
76
- span = devlite.start_span("checkout.process", {"trace_id": "abc"})
77
- span.end("ok") # "ok" | "error"
78
-
79
- # Tell DevLite about a deployment (powers before/after performance views)
80
- devlite.report_deployment(version="v2.1.3", commit_sha="a1b2c3d")
81
-
82
- # Breadcrumbs attach to the next captured error, improving AI root-cause
83
- devlite.add_breadcrumb({"type": "business_event", "note": "user started checkout"})
84
-
85
- # Tag all subsequent events
86
- devlite.set_tag("region", "lagos")
87
- ```
88
-
89
- ## User impact tracking
90
-
91
- `set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
92
-
93
- ```python
94
- from flask import request
95
-
96
- @app.before_request
97
- def identify():
98
- devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
99
- ```
100
-
101
- ## Configuration options
102
-
103
- ```python
104
- devlite.init({
105
- "api_key": "dl_live_xxxxx", # required
106
- "environment": "production", # default: $DEVLITE_ENVIRONMENT or "development"
107
- "service_name": "payments-api", # default: $DEVLITE_SERVICE_NAME or current folder
108
- "release": "v2.1.3", # e.g. git sha, shown in deployment views
109
- "sample_rate": 1.0, # 0.0–1.0. Sampling is COHERENT: a sampled-out
110
- # request drops its errors/spans/logs/metrics together.
111
- "capture_body": False, # capture (redacted) request headers — off by default
112
- "flush_interval_ms": 5000, # how often batched events are sent
113
- "gzip": True, # compress request bodies (Content-Encoding: gzip)
114
- "debug": False, # log SDK internals
115
- "on_error": lambda err: print(err), # SDK-internal send failures
116
- })
117
- ```
118
-
119
- All of `api_key`, `service_name`, `release`, and `endpoint` can also be set via environment variables (`DEVLITE_API_KEY`, `DEVLITE_SERVICE_NAME`, `DEVLITE_RELEASE`, `DEVLITE_ENDPOINT`).
120
-
121
- ## Serverless / short-lived processes
122
-
123
- Events are batched, so call `flush()` before your function returns:
124
-
125
- ```python
126
- devlite.flush()
127
- ```
128
-
129
- ## Graceful shutdown
130
-
131
- The SDK flushes remaining events on interpreter exit (`atexit`). If you manage shutdown yourself:
132
-
133
- ```python
134
- devlite.close()
135
- ```
136
-
137
- ## What makes this competitive
138
-
139
- - **Automatic error grouping** — the same underlying bug, however many times it fires, is fingerprinted and grouped into one issue, instead of flooding your dashboard with duplicates.
140
- - **Source code context** — every captured error includes the actual lines of code around the crash, read live from disk, not just a bare stack trace.
141
- - **Automatic sensitive-data scrubbing** on by default. Emails, JWTs, bearer tokens, AWS keys, credit card numbers, and any field literally named `password`/`token`/`secret` are redacted before anything leaves your process.
142
- - **Never blocks your app** — all sends are async (background thread), batched, and retried with backoff. If DevLite's backend is unreachable, your app keeps running.
143
- - **Zero dependencies** pure Python standard library.
144
-
145
- ## Development
146
-
147
- ```bash
148
- pip install -e ".[flask,test]"
149
- python -m pytest
150
- ```
151
-
152
- ## License
153
-
154
- MIT
1
+ # devlite — Python SDK
2
+
3
+ AI-powered observability for Python. Add two lines, get automatic request tracking, error capture, error grouping, source context, user tracking, and slow-endpoint detection — no config files, no manual instrumentation, **zero runtime dependencies**.
4
+
5
+ This is the Python counterpart of [`@devlite/nodejs`](https://www.npmjs.com/package/@devlite/nodejs) — both SDKs speak the same batch protocol to the [DevLite ingest API](https://github.com/Ishimwe-Kevin/devlite-app).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install devlite # core (Flask/FastAPI/Django optional)
11
+ pip install "devlite[flask]" # + Flask instrumentation
12
+ pip install "devlite[fastapi]" # + FastAPI instrumentation
13
+ pip install "devlite[django]" # + Django instrumentation
14
+ ```
15
+
16
+ ## 60-second quickstart (Flask)
17
+
18
+ ```python
19
+ from flask import Flask
20
+ import devlite
21
+
22
+ devlite.init(api_key="dl_live_xxxxx")
23
+
24
+ app = Flask(__name__)
25
+ devlite.instrument_flask(app)
26
+ ```
27
+
28
+ That's it — every HTTP request, slow endpoint, and unhandled exception is now captured automatically, with:
29
+
30
+ - **Grouped errors** — the same bug occurring 1,000 times shows up as one issue, not 1,000
31
+ - **Real source code context** — the actual lines around the crash, read live from disk
32
+ - **Automatic sensitive-data scrubbing** — emails, tokens, passwords redacted before anything leaves your process
33
+ - **User impact tracking** — know exactly which users hit which bugs
34
+
35
+ Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
36
+
37
+ ## Auto-instrumentation for every framework
38
+
39
+ One call per framework, with the same automatic request/slow-request/error/user tracking:
40
+
41
+ **FastAPI** (any Starlette app) — call right after creating the app:
42
+
43
+ ```python
44
+ from fastapi import FastAPI
45
+ import devlite
46
+
47
+ devlite.init(api_key="dl_live_xxxxx")
48
+
49
+ app = FastAPI()
50
+ devlite.instrument_fastapi(app) # before the server starts
51
+ ```
52
+
53
+ **Django** — `instrument_django()` inserts `devlite._django.InstrumentDjangoMiddleware` at the top of your `MIDDLEWARE` setting:
54
+
55
+ ```python
56
+ import devlite
57
+
58
+ devlite.init(api_key="dl_live_xxxxx")
59
+ devlite.instrument_django()
60
+ ```
61
+
62
+ Or add the middleware manually (must be near the top, index 0 ideally):
63
+
64
+ ```python
65
+ MIDDLEWARE = [
66
+ "devlite._django.InstrumentDjangoMiddleware",
67
+ ...
68
+ ]
69
+ ```
70
+
71
+ Any other framework (or plain WSGI/ASGI) still works through the manual API below.
72
+
73
+ ## Manual capture API
74
+
75
+ ```python
76
+ # Capture a handled error with extra context
77
+ devlite.capture_error(err, {"userId": "123", "action": "checkout"})
78
+ devlite.capture_error(err, {"fingerprint": "payment-timeout"}) # force grouping key
79
+
80
+ # Non-error event
81
+ devlite.capture_message("Payment retried after timeout", "warning")
82
+
83
+ # Custom metric feeds forecasting/anomaly detection
84
+ devlite.report_metric("order.total", 42.5, unit="USD", tags={"region": "lagos"})
85
+
86
+ # Structured log line
87
+ devlite.capture_log("checkout completed", "info", {"orderId": "ord_123", "durationMs": 250})
88
+
89
+ # Trace a unit of work
90
+ span = devlite.start_span("checkout.process", {"trace_id": "abc"})
91
+ span.end("ok") # "ok" | "error"
92
+
93
+ # Tell DevLite about a deployment (powers before/after performance views)
94
+ devlite.report_deployment(version="v2.1.3", commit_sha="a1b2c3d")
95
+
96
+ # Breadcrumbs attach to the next captured error, improving AI root-cause
97
+ devlite.add_breadcrumb({"type": "business_event", "note": "user started checkout"})
98
+
99
+ # Tag all subsequent events
100
+ devlite.set_tag("region", "lagos")
101
+ ```
102
+
103
+ ## User impact tracking
104
+
105
+ `set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
106
+
107
+ ```python
108
+ # Flask
109
+ from flask import request
110
+
111
+ @app.before_request
112
+ def identify():
113
+ devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
114
+
115
+ # FastAPI inside any endpoint
116
+ devlite.set_user({"id": "u-123", "email": "a@example.com"})
117
+
118
+ # Django — inside any view
119
+ def my_view(request):
120
+ devlite.set_user({"id": request.user.id, "email": request.user.email})
121
+ ```
122
+
123
+ ## Configuration options
124
+
125
+ ```python
126
+ devlite.init({
127
+ "api_key": "dl_live_xxxxx", # required
128
+ "environment": "production", # default: $DEVLITE_ENVIRONMENT or "development"
129
+ "service_name": "payments-api", # default: $DEVLITE_SERVICE_NAME or current folder
130
+ "release": "v2.1.3", # e.g. git sha, shown in deployment views
131
+ "sample_rate": 1.0, # 0.0–1.0. Sampling is COHERENT: a sampled-out
132
+ # request drops its errors/spans/logs/metrics together.
133
+ "capture_body": False, # capture (redacted) request headers — off by default
134
+ "flush_interval_ms": 5000, # how often batched events are sent
135
+ "gzip": True, # compress request bodies (Content-Encoding: gzip)
136
+ "debug": False, # log SDK internals
137
+ "on_error": lambda err: print(err), # SDK-internal send failures
138
+ })
139
+ ```
140
+
141
+ All of `api_key`, `service_name`, `release`, and `endpoint` can also be set via environment variables (`DEVLITE_API_KEY`, `DEVLITE_SERVICE_NAME`, `DEVLITE_RELEASE`, `DEVLITE_ENDPOINT`).
142
+
143
+ ## Serverless / short-lived processes
144
+
145
+ Events are batched, so call `flush()` before your function returns:
146
+
147
+ ```python
148
+ devlite.flush()
149
+ ```
150
+
151
+ ## Graceful shutdown
152
+
153
+ The SDK flushes remaining events on interpreter exit (`atexit`). If you manage shutdown yourself:
154
+
155
+ ```python
156
+ devlite.close()
157
+ ```
158
+
159
+ ## What makes this competitive
160
+
161
+ - **Automatic error grouping** — the same underlying bug, however many times it fires, is fingerprinted and grouped into one issue, instead of flooding your dashboard with duplicates.
162
+ - **Source code context** — every captured error includes the actual lines of code around the crash, read live from disk, not just a bare stack trace.
163
+ - **Automatic sensitive-data scrubbing** — on by default. Emails, JWTs, bearer tokens, AWS keys, credit card numbers, and any field literally named `password`/`token`/`secret` are redacted before anything leaves your process.
164
+ - **Never blocks your app** — all sends are async (background thread), batched, and retried with backoff. If DevLite's backend is unreachable, your app keeps running.
165
+ - **Zero dependencies** — pure Python standard library.
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ pip install -e ".[flask,fastapi,django,test]"
171
+ python -m pytest
172
+ ```
173
+
174
+ ## License
175
+
176
+ MIT
@@ -12,13 +12,13 @@ Public API (all Pythonic snake_case):
12
12
  init, capture_error, capture_message, report_metric, capture_log,
13
13
  start_span, report_span, report_deployment, add_breadcrumb, set_tag,
14
14
  set_user, run_with_context, start_host_metrics, instrument_flask,
15
- flush, close
15
+ instrument_fastapi, instrument_django, flush, close
16
16
  """
17
17
 
18
18
  from ._client import Client, HostMetricsHandle, Span
19
19
  from ._context import run_with_context
20
20
 
21
- __version__ = "0.1.0"
21
+ __version__ = "0.1.1"
22
22
 
23
23
  _client = None
24
24
 
@@ -115,6 +115,16 @@ def instrument_flask(app):
115
115
  return _require_client().instrument_flask(app)
116
116
 
117
117
 
118
+ def instrument_fastapi(app):
119
+ """Wire automatic request/error/user tracking into a FastAPI app."""
120
+ return _require_client().instrument_fastapi(app)
121
+
122
+
123
+ def instrument_django():
124
+ """Insert DevLite's middleware at the top of Django's MIDDLEWARE setting."""
125
+ return _require_client().instrument_django()
126
+
127
+
118
128
  def flush():
119
129
  """Force-send whatever is currently queued (useful in serverless)."""
120
130
  _require_client().flush()
@@ -142,6 +152,8 @@ __all__ = [
142
152
  "run_with_context",
143
153
  "start_host_metrics",
144
154
  "instrument_flask",
155
+ "instrument_fastapi",
156
+ "instrument_django",
145
157
  "flush",
146
158
  "close",
147
159
  ]
@@ -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,
@@ -164,6 +164,16 @@ class Client:
164
164
 
165
165
  return instrument_flask(app, self.transport, self.context, self.config)
166
166
 
167
+ def instrument_fastapi(self, app):
168
+ from ._fastapi import instrument_fastapi
169
+
170
+ return instrument_fastapi(app, self.transport, self.context, self.config)
171
+
172
+ def instrument_django(self):
173
+ from ._django import instrument_django
174
+
175
+ return instrument_django(self)
176
+
167
177
  def flush(self):
168
178
  self.transport.flush()
169
179
 
@@ -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:
@@ -50,6 +56,19 @@ def run_with_context(fn, seed=None):
50
56
  _request_store.set(previous)
51
57
 
52
58
 
59
+ def reset_context():
60
+ """Start a fresh request context for the current thread/task.
61
+
62
+ Unlike run_with_context this does NOT restore the previous value when
63
+ done — the fresh store stays active for the whole request. Framework
64
+ middleware calls this once per request so that a stale store (left
65
+ behind by a previous request on a reused thread, or inherited from the
66
+ caller's context, e.g. by anyio portal threads) can never leak user
67
+ data or breadcrumbs into the current request.
68
+ """
69
+ _request_store.set({})
70
+
71
+
53
72
  def set_user(user):
54
73
  """Scope a user to the current request context. Mirrors devlite.setUser()."""
55
74
  store = _store()
@@ -69,6 +88,17 @@ def set_sampled(value):
69
88
  store["sampled"] = value
70
89
 
71
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
+
72
102
  def is_sampled_out():
73
103
  store = _request_store.get()
74
104
  return bool(store) and store.get("sampled") is False