devlite 0.1.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.
devlite-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ishvexa Hub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
devlite-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,154 @@
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
@@ -0,0 +1,130 @@
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 optional)
11
+ pip install "devlite[flask]" # + Flask instrumentation
12
+ ```
13
+
14
+ ## 60-second quickstart (Flask)
15
+
16
+ ```python
17
+ from flask import Flask
18
+ import devlite
19
+
20
+ devlite.init(api_key="dl_live_xxxxx")
21
+
22
+ app = Flask(__name__)
23
+ devlite.instrument_flask(app)
24
+ ```
25
+
26
+ That's it — every HTTP request, slow endpoint, and unhandled exception is now captured automatically, with:
27
+
28
+ - **Grouped errors** — the same bug occurring 1,000 times shows up as one issue, not 1,000
29
+ - **Real source code context** — the actual lines around the crash, read live from disk
30
+ - **Automatic sensitive-data scrubbing** — emails, tokens, passwords redacted before anything leaves your process
31
+ - **User impact tracking** — know exactly which users hit which bugs
32
+
33
+ Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
34
+
35
+ ## Manual capture API
36
+
37
+ ```python
38
+ # Capture a handled error with extra context
39
+ devlite.capture_error(err, {"userId": "123", "action": "checkout"})
40
+ devlite.capture_error(err, {"fingerprint": "payment-timeout"}) # force grouping key
41
+
42
+ # Non-error event
43
+ devlite.capture_message("Payment retried after timeout", "warning")
44
+
45
+ # Custom metric — feeds forecasting/anomaly detection
46
+ devlite.report_metric("order.total", 42.5, unit="USD", tags={"region": "lagos"})
47
+
48
+ # Structured log line
49
+ devlite.capture_log("checkout completed", "info", {"orderId": "ord_123", "durationMs": 250})
50
+
51
+ # Trace a unit of work
52
+ span = devlite.start_span("checkout.process", {"trace_id": "abc"})
53
+ span.end("ok") # "ok" | "error"
54
+
55
+ # Tell DevLite about a deployment (powers before/after performance views)
56
+ devlite.report_deployment(version="v2.1.3", commit_sha="a1b2c3d")
57
+
58
+ # Breadcrumbs attach to the next captured error, improving AI root-cause
59
+ devlite.add_breadcrumb({"type": "business_event", "note": "user started checkout"})
60
+
61
+ # Tag all subsequent events
62
+ devlite.set_tag("region", "lagos")
63
+ ```
64
+
65
+ ## User impact tracking
66
+
67
+ `set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
68
+
69
+ ```python
70
+ from flask import request
71
+
72
+ @app.before_request
73
+ def identify():
74
+ devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
75
+ ```
76
+
77
+ ## Configuration options
78
+
79
+ ```python
80
+ devlite.init({
81
+ "api_key": "dl_live_xxxxx", # required
82
+ "environment": "production", # default: $DEVLITE_ENVIRONMENT or "development"
83
+ "service_name": "payments-api", # default: $DEVLITE_SERVICE_NAME or current folder
84
+ "release": "v2.1.3", # e.g. git sha, shown in deployment views
85
+ "sample_rate": 1.0, # 0.0–1.0. Sampling is COHERENT: a sampled-out
86
+ # request drops its errors/spans/logs/metrics together.
87
+ "capture_body": False, # capture (redacted) request headers — off by default
88
+ "flush_interval_ms": 5000, # how often batched events are sent
89
+ "gzip": True, # compress request bodies (Content-Encoding: gzip)
90
+ "debug": False, # log SDK internals
91
+ "on_error": lambda err: print(err), # SDK-internal send failures
92
+ })
93
+ ```
94
+
95
+ 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`).
96
+
97
+ ## Serverless / short-lived processes
98
+
99
+ Events are batched, so call `flush()` before your function returns:
100
+
101
+ ```python
102
+ devlite.flush()
103
+ ```
104
+
105
+ ## Graceful shutdown
106
+
107
+ The SDK flushes remaining events on interpreter exit (`atexit`). If you manage shutdown yourself:
108
+
109
+ ```python
110
+ devlite.close()
111
+ ```
112
+
113
+ ## What makes this competitive
114
+
115
+ - **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.
116
+ - **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.
117
+ - **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.
118
+ - **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.
119
+ - **Zero dependencies** — pure Python standard library.
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ pip install -e ".[flask,test]"
125
+ python -m pytest
126
+ ```
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,147 @@
1
+ """DevLite Python SDK — AI-powered observability with a 2-line integration.
2
+
3
+ import devlite
4
+
5
+ devlite.init({api_key: "dl_live_xxxxx"})
6
+
7
+ Automatic request tracking, error grouping, source context, user impact
8
+ tracking, and sensitive-data scrubbing — mirroring the @devlite/nodejs npm
9
+ SDK, with zero runtime dependencies.
10
+
11
+ Public API (all Pythonic snake_case):
12
+ init, capture_error, capture_message, report_metric, capture_log,
13
+ start_span, report_span, report_deployment, add_breadcrumb, set_tag,
14
+ set_user, run_with_context, start_host_metrics, instrument_flask,
15
+ flush, close
16
+ """
17
+
18
+ from ._client import Client, HostMetricsHandle, Span
19
+ from ._context import run_with_context
20
+
21
+ __version__ = "0.1.0"
22
+
23
+ _client = None
24
+
25
+
26
+ def init(options=None):
27
+ """Initialize DevLite. The only required call.
28
+
29
+ Args:
30
+ options: dict with keys like api_key, environment, service_name,
31
+ release, sample_rate, gzip, debug, endpoint, on_error, ...
32
+ `api_key` is required (or set DEVLITE_API_KEY env var).
33
+ """
34
+ global _client
35
+ if _client is not None:
36
+ import warnings
37
+
38
+ warnings.warn("[DevLite] init() called more than once — ignoring subsequent call.")
39
+ return
40
+ _client = Client(options or {})
41
+
42
+
43
+ def _require_client():
44
+ if _client is None:
45
+ raise RuntimeError("[DevLite] SDK not initialized. Call devlite.init(api_key=...) first.")
46
+ return _client
47
+
48
+
49
+ def capture_error(error, extra=None):
50
+ """Capture a handled error with optional extra context."""
51
+ _require_client().capture_error(error, extra=extra)
52
+
53
+
54
+ def capture_message(message, level="info", extra=None):
55
+ """Capture a non-error event, e.g. a business-logic message."""
56
+ _require_client().capture_message(message, level=level, extra=extra)
57
+
58
+
59
+ def report_metric(name, value, unit=None, tags=None):
60
+ """Report a metric data point (feeds forecasting/anomaly detection)."""
61
+ _require_client().report_metric(name, value, unit=unit, tags=tags)
62
+
63
+
64
+ def capture_log(message, level="info", fields=None):
65
+ """Capture a structured log line with arbitrary fields."""
66
+ _require_client().capture_log(message, level=level, fields=fields)
67
+
68
+
69
+ def start_span(name, options=None):
70
+ """Start a trace span; returns an object with id/trace_id and end(status)."""
71
+ return _require_client().start_span(name, options=options)
72
+
73
+
74
+ def report_span(name, trace_id=None, parent_id=None, tags=None, start_time=None, end_time=None, duration_ms=None, status="ok"):
75
+ """Report a finished span if you track timing yourself."""
76
+ _require_client().report_span(
77
+ name,
78
+ trace_id=trace_id,
79
+ parent_id=parent_id,
80
+ tags=tags,
81
+ start_time=start_time,
82
+ end_time=end_time,
83
+ duration_ms=duration_ms,
84
+ status=status,
85
+ )
86
+
87
+
88
+ def report_deployment(version=None, commit_sha=None, notes=None):
89
+ """Report a deployment — powers performance before/after deployment views."""
90
+ _require_client().report_deployment(version=version, commit_sha=commit_sha, notes=notes)
91
+
92
+
93
+ def add_breadcrumb(entry):
94
+ """Add a manual breadcrumb (e.g. 'user started checkout') for richer AI context."""
95
+ _require_client().add_breadcrumb(entry)
96
+
97
+
98
+ def set_tag(key, value):
99
+ """Tag all subsequent events, e.g. set_tag('region', 'lagos')."""
100
+ _require_client().set_tag(key, value)
101
+
102
+
103
+ def set_user(user):
104
+ """Identify the user for the CURRENT request (scoped to the request)."""
105
+ _require_client().set_user(user)
106
+
107
+
108
+ def start_host_metrics(interval_ms=15000):
109
+ """Start sampling host CPU/memory (plus network/disk on Linux) as metrics."""
110
+ return _require_client().start_host_metrics(interval_ms=interval_ms)
111
+
112
+
113
+ def instrument_flask(app):
114
+ """Wire automatic request/error/user tracking into a Flask app."""
115
+ return _require_client().instrument_flask(app)
116
+
117
+
118
+ def flush():
119
+ """Force-send whatever is currently queued (useful in serverless)."""
120
+ _require_client().flush()
121
+
122
+
123
+ def close():
124
+ """Cleanly stop the SDK — flushes remaining events."""
125
+ if _client is not None:
126
+ _client.close()
127
+
128
+
129
+ __all__ = [
130
+ "__version__",
131
+ "init",
132
+ "capture_error",
133
+ "capture_message",
134
+ "report_metric",
135
+ "capture_log",
136
+ "start_span",
137
+ "report_span",
138
+ "report_deployment",
139
+ "add_breadcrumb",
140
+ "set_tag",
141
+ "set_user",
142
+ "run_with_context",
143
+ "start_host_metrics",
144
+ "instrument_flask",
145
+ "flush",
146
+ "close",
147
+ ]
@@ -0,0 +1,184 @@
1
+ """DevLite client — the singleton that owns config, transport, and context."""
2
+
3
+ import atexit
4
+ import threading
5
+ import uuid
6
+
7
+ from ._config import build_config
8
+ from ._context import Context, get_user, now_ms, run_with_context
9
+ from ._errors import build_error_event
10
+ from ._metrics import collect_host_metrics
11
+ from ._scrub import scrub_deep
12
+ from ._transport import Transport
13
+
14
+
15
+ def _new_hex(nbytes):
16
+ return uuid.uuid4().hex[: nbytes * 2]
17
+
18
+
19
+ class Span:
20
+ """A trace span you can end() once the work completes."""
21
+
22
+ def __init__(self, client, name, options=None):
23
+ options = options or {}
24
+ self.client = client
25
+ self.type = "span"
26
+ self.id = options.get("id") or _new_hex(6)
27
+ self.name = name
28
+ self.trace_id = options.get("trace_id") or _new_hex(8)
29
+ self.parent_id = options.get("parent_id")
30
+ self.tags = options.get("tags") or {}
31
+ self.start_time = now_ms()
32
+ self.end_time = None
33
+ self.duration_ms = None
34
+ self.status = None
35
+
36
+ def end(self, status="ok"):
37
+ self.end_time = now_ms()
38
+ self.duration_ms = self.end_time - self.start_time
39
+ self.status = status
40
+ event = {
41
+ "type": "span",
42
+ "id": self.id,
43
+ "name": self.name,
44
+ "traceId": self.trace_id,
45
+ "parentId": self.parent_id,
46
+ "tags": self.tags,
47
+ "startTime": self.start_time,
48
+ "endTime": self.end_time,
49
+ "durationMs": self.duration_ms,
50
+ "status": status,
51
+ }
52
+ self.client._enqueue_scubbed(event)
53
+
54
+
55
+ class HostMetricsHandle:
56
+ def __init__(self, client, interval_ms):
57
+ self.client = client
58
+ self.interval_ms = max(1000, interval_ms)
59
+ self._stop = threading.Event()
60
+ self._thread = threading.Thread(target=self._run, daemon=True, name="devlite-host-metrics")
61
+ self._thread.start()
62
+
63
+ def _run(self):
64
+ self._sample()
65
+ while not self._stop.wait(self.interval_ms / 1000.0):
66
+ self._sample()
67
+
68
+ def _sample(self):
69
+ try:
70
+ for metric in collect_host_metrics():
71
+ self.client._transport.enqueue(metric)
72
+ except Exception:
73
+ pass
74
+
75
+ def stop(self):
76
+ self._stop.set()
77
+
78
+
79
+ class _NoopHandle:
80
+ def stop(self):
81
+ pass
82
+
83
+
84
+ class Client:
85
+ def __init__(self, options):
86
+ self.config = build_config(options)
87
+ self.context = Context()
88
+ self.transport = Transport(self.config)
89
+ self._host_metrics = None
90
+ self._closed = False
91
+ atexit.register(self.close)
92
+
93
+ # --- public API (also exposed at package top level) -------------------
94
+
95
+ def capture_error(self, error, extra=None):
96
+ event = build_error_event(
97
+ error,
98
+ self.config,
99
+ breadcrumbs=self.context.snapshot().get("breadcrumbs", []),
100
+ user=get_user(),
101
+ extra=extra or {},
102
+ )
103
+ self.transport.enqueue(event)
104
+
105
+ def capture_message(self, message, level="info", extra=None):
106
+ raw = {"type": "message", "level": level, "message": message, "extra": extra or {}, "timestamp": now_ms()}
107
+ self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
108
+
109
+ def report_metric(self, name, value, unit=None, tags=None):
110
+ if not isinstance(name, str) or not name:
111
+ raise ValueError("[DevLite] report_metric() requires a string `name`.")
112
+ if not isinstance(value, (int, float)):
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()}
115
+ self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
116
+
117
+ def capture_log(self, message, level="info", fields=None):
118
+ raw = {"type": "log", "level": level, "message": message, "fields": fields or {}, "timestamp": now_ms()}
119
+ self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
120
+
121
+ def start_span(self, name, options=None):
122
+ return Span(self, name, options)
123
+
124
+ def report_span(self, name, trace_id=None, parent_id=None, tags=None, start_time=None, end_time=None, duration_ms=None, status="ok"):
125
+ raw = {
126
+ "type": "span",
127
+ "id": _new_hex(6),
128
+ "name": name,
129
+ "traceId": trace_id or _new_hex(8),
130
+ "parentId": parent_id,
131
+ "tags": tags or {},
132
+ "startTime": start_time,
133
+ "endTime": end_time,
134
+ "durationMs": duration_ms,
135
+ "status": status,
136
+ "timestamp": now_ms(),
137
+ }
138
+ self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
139
+
140
+ def report_deployment(self, version=None, commit_sha=None, notes=None):
141
+ self.transport.enqueue(
142
+ {"type": "deployment", "version": version, "commitSha": commit_sha, "notes": notes, "timestamp": now_ms()}
143
+ )
144
+
145
+ def add_breadcrumb(self, entry):
146
+ self.context.add_breadcrumb(entry or {})
147
+
148
+ def set_tag(self, key, value):
149
+ self.context.set_tag(key, value)
150
+
151
+ def set_user(self, user):
152
+ from ._context import set_user as _set_user
153
+
154
+ _set_user(user)
155
+
156
+ def start_host_metrics(self, interval_ms=15000):
157
+ if self._host_metrics:
158
+ return _NoopHandle() # no-op if already running
159
+ self._host_metrics = HostMetricsHandle(self, interval_ms)
160
+ return self._host_metrics
161
+
162
+ def instrument_flask(self, app):
163
+ from ._flask import instrument_flask
164
+
165
+ return instrument_flask(app, self.transport, self.context, self.config)
166
+
167
+ def flush(self):
168
+ self.transport.flush()
169
+
170
+ def close(self):
171
+ if self._closed:
172
+ return
173
+ self._closed = True
174
+ if self._host_metrics:
175
+ self._host_metrics.stop()
176
+ self.transport.close()
177
+
178
+ # --- internals --------------------------------------------------------
179
+
180
+ def _should_scrub(self):
181
+ return self.config.get("scrub_sensitive_data", True) is not False
182
+
183
+ def _enqueue_scubbed(self, event):
184
+ self.transport.enqueue(scrub_deep(event) if self._should_scrub() else event)