trodo-python 2.8.0__py3-none-any.whl → 2.10.0__py3-none-any.whl

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.
trodo/__init__.py CHANGED
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
40
40
 
41
41
  from __future__ import annotations
42
42
 
43
- __version__ = "2.8.0"
43
+ __version__ = "2.10.0"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -22,6 +22,13 @@ def _hr_to_iso(nanos: Optional[int]) -> Optional[str]:
22
22
  return datetime.fromtimestamp(nanos / 1e9, tz=timezone.utc).isoformat().replace("+00:00", "Z")
23
23
 
24
24
 
25
+ def _trunc(s: Any, max_len: int) -> Optional[str]:
26
+ if s is None:
27
+ return None
28
+ s = str(s)
29
+ return s[:max_len] if len(s) > max_len else s
30
+
31
+
25
32
  def _infer_kind(attrs: Dict[str, Any]) -> str:
26
33
  if not attrs:
27
34
  return "generic"
@@ -111,6 +118,29 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
111
118
  except Exception:
112
119
  ok = "error" if (status_code and str(status_code).endswith("ERROR")) else "ok"
113
120
 
121
+ # Rich error detail. The real exception class + stacktrace live in the OTel
122
+ # `exception` event (record_exception — emitted by the Anthropic/OpenAI/
123
+ # LangChain instrumentors), NOT span.status. The old bridge read only the
124
+ # status code and dropped all of it.
125
+ status_desc = getattr(status, "description", None)
126
+ exc_attrs: Dict[str, Any] = {}
127
+ for ev in (getattr(otel_span, "events", None) or []):
128
+ if getattr(ev, "name", None) == "exception":
129
+ exc_attrs = dict(getattr(ev, "attributes", {}) or {})
130
+ break
131
+ err_type = exc_attrs.get("exception.type") or attrs.get("exception.type") or attrs.get("error.type")
132
+ err_msg = exc_attrs.get("exception.message") or attrs.get("exception.message") or status_desc
133
+ err_stack = exc_attrs.get("exception.stacktrace") or attrs.get("exception.stacktrace")
134
+ status_code_val = (
135
+ attrs.get("http.response.status_code")
136
+ or attrs.get("http.status_code")
137
+ or attrs.get("gen_ai.response.status_code")
138
+ or attrs.get("error.code")
139
+ )
140
+ has_error = ok == "error" or err_type is not None
141
+ ok = "error" if has_error else "ok"
142
+ level = "error" if has_error else "default"
143
+
114
144
  # Accept both stable and experimental GenAI semconv keys.
115
145
  in_toks = (
116
146
  attrs.get("gen_ai.usage.input_tokens")
@@ -138,6 +168,11 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
138
168
  kind=kind,
139
169
  name=getattr(otel_span, "name", kind),
140
170
  status=ok,
171
+ level=level,
172
+ error_type=_trunc(err_type, 128) if err_type else None,
173
+ error_message=_trunc(err_msg, 4_000) if err_msg else None,
174
+ status_code=_trunc(status_code_val, 32) if status_code_val is not None else None,
175
+ stack_trace=_trunc(err_stack, 20_000) if err_stack else None,
141
176
  started_at=started_at,
142
177
  ended_at=ended_at,
143
178
  duration_ms=duration_ms,
trodo/otel/helpers.py CHANGED
@@ -413,6 +413,7 @@ def track_mcp(
413
413
  "kind": "tool",
414
414
  "name": f"tool.{tool}",
415
415
  "status": status,
416
+ "level": "error" if error else "default",
416
417
  "input": _stringify({"tool": tool, "params": input}) if input is not None else None,
417
418
  "output": _stringify(output_to_record),
418
419
  "tool_name": tool,
trodo/otel/processor.py CHANGED
@@ -20,12 +20,18 @@ class TrodoRun:
20
20
  conversation_id: Optional[str] = None
21
21
  parent_run_id: Optional[str] = None
22
22
  status: str = "ok" # 'running' | 'ok' | 'error'
23
+ # Severity (Langfuse parity): 'debug' | 'default' | 'warning' | 'error'.
24
+ # Optional — backend derives it from status when omitted.
25
+ level: Optional[str] = None
23
26
  input: Optional[Union[str, Dict[str, Any]]] = None
24
27
  output: Optional[Union[str, Dict[str, Any]]] = None
25
28
  started_at: Optional[str] = None
26
29
  ended_at: Optional[str] = None
27
30
  duration_ms: Optional[int] = None
28
31
  error_summary: Optional[str] = None
32
+ # Exception class of the run-level failure (runs previously only had the
33
+ # free-text error_summary).
34
+ error_type: Optional[str] = None
29
35
  metadata: Optional[Dict[str, Any]] = None
30
36
  # Aggregates summed from child spans at finalisation.
31
37
  total_tokens_in: Optional[int] = None
@@ -47,6 +53,8 @@ class TrodoSpan:
47
53
  kind: str = "generic" # 'llm' | 'tool' | 'agent' | 'retrieval' | 'generic'
48
54
  name: str = ""
49
55
  status: str = "ok"
56
+ # Severity (Langfuse parity): 'debug' | 'default' | 'warning' | 'error'.
57
+ level: Optional[str] = None
50
58
  started_at: Optional[str] = None
51
59
  ended_at: Optional[str] = None
52
60
  duration_ms: Optional[int] = None
@@ -54,6 +62,10 @@ class TrodoSpan:
54
62
  output: Optional[Union[str, Dict[str, Any]]] = None
55
63
  error_type: Optional[str] = None
56
64
  error_message: Optional[str] = None
65
+ # HTTP/provider status code (e.g. '429', 'rate_limit_exceeded').
66
+ status_code: Optional[str] = None
67
+ # Truncated exception stacktrace.
68
+ stack_trace: Optional[str] = None
57
69
  model: Optional[str] = None
58
70
  provider: Optional[str] = None
59
71
  input_tokens: Optional[int] = None
trodo/otel/wrap_agent.py CHANGED
@@ -26,6 +26,7 @@ from __future__ import annotations
26
26
 
27
27
  import json
28
28
  import time
29
+ import traceback
29
30
  import uuid
30
31
  from datetime import datetime, timezone
31
32
  from typing import Any, Callable, Dict, Optional, Union
@@ -79,6 +80,75 @@ def _truncate(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[str]:
79
80
  return s[:max_len] if len(s) > max_len else s
80
81
 
81
82
 
83
+ def describe_error(exc_type, exc, tb=None) -> Dict[str, Optional[str]]:
84
+ """Extract rich error detail from a caught exception so spans carry error
85
+ TYPE, HTTP/provider STATUS CODE, and STACK TRACE — not just a message.
86
+
87
+ Works generically across provider SDKs: OpenAI (``exc.status`` /
88
+ ``exc.code``), Anthropic (``exc.status``), httpx/requests
89
+ (``exc.response.status_code``), and stdlib errors (``exc.errno``). Never
90
+ raises. Returns keys: error_type, error_message, status_code, stack_trace,
91
+ level.
92
+ """
93
+ if exc is None:
94
+ return {"error_type": None, "error_message": None, "status_code": None,
95
+ "stack_trace": None, "level": "error"}
96
+ error_type = getattr(exc_type, "__name__", None) or type(exc).__name__
97
+ error_message = _truncate(str(exc), 4_000)
98
+
99
+ status_code: Optional[str] = None
100
+ # HTTP status first (numeric), then a provider/system error code.
101
+ for attr in ("status", "status_code"):
102
+ v = getattr(exc, attr, None)
103
+ if v is not None:
104
+ status_code = str(v)[:32]
105
+ break
106
+ if status_code is None:
107
+ resp = getattr(exc, "response", None)
108
+ rc = getattr(resp, "status_code", None) if resp is not None else None
109
+ if rc is not None:
110
+ status_code = str(rc)[:32]
111
+ if status_code is None:
112
+ code = getattr(exc, "code", None) or getattr(exc, "errno", None)
113
+ if code is not None:
114
+ status_code = str(code)[:32]
115
+
116
+ stack_trace: Optional[str] = None
117
+ try:
118
+ stack_trace = _truncate("".join(traceback.format_exception(exc_type, exc, tb)), 20_000)
119
+ except Exception:
120
+ stack_trace = None
121
+
122
+ return {
123
+ "error_type": (error_type or "Error")[:128],
124
+ "error_message": error_message,
125
+ "status_code": status_code,
126
+ "stack_trace": stack_trace,
127
+ "level": "error",
128
+ }
129
+
130
+
131
+ def _resolve_error(handle, exc_type, exc, tb) -> Dict[str, Optional[str]]:
132
+ """Resolve a span's final error state from either a raised exception (wins)
133
+ or a manual ``handle.set_error(...)``. Returns describe_error's keys plus a
134
+ ``status`` of 'ok'|'error'."""
135
+ if exc is not None:
136
+ info = describe_error(exc_type, exc, tb)
137
+ info["status"] = "error"
138
+ return info
139
+ if handle is not None and getattr(handle, "has_error", False):
140
+ return {
141
+ "status": "error",
142
+ "error_type": handle.error_type,
143
+ "error_message": handle.error_message,
144
+ "status_code": handle.status_code,
145
+ "stack_trace": handle.stack_trace,
146
+ "level": "error",
147
+ }
148
+ return {"status": "ok", "error_type": None, "error_message": None,
149
+ "status_code": None, "stack_trace": None, "level": None}
150
+
151
+
82
152
  def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any]]]:
83
153
  """Prepare a value for storage in the JSONB input/output column.
84
154
 
@@ -172,6 +242,10 @@ class RunHandle:
172
242
  self.input: Optional[Union[str, Dict[str, Any]]] = None
173
243
  self.output: Optional[Union[str, Dict[str, Any]]] = None
174
244
  self.metadata: Dict[str, Any] = {}
245
+ # Manually-recorded run-level error (via set_error_summary). When set
246
+ # without a raised exception, the run finalises as ``error``.
247
+ self.error_summary: Optional[str] = None
248
+ self.error_type: Optional[str] = None
175
249
 
176
250
  def set_input(self, value: Any) -> None:
177
251
  self.input = _prepare_value(value)
@@ -182,6 +256,15 @@ class RunHandle:
182
256
  def set_metadata(self, **kwargs: Any) -> None:
183
257
  self.metadata.update(kwargs)
184
258
 
259
+ def set_error_summary(self, summary: str, *, type: Optional[str] = None) -> None:
260
+ """Mark the run as errored without raising — e.g. a soft failure the
261
+ agent recovered from but you still want recorded. Sets ``status='error'``
262
+ and ``error_summary`` on the run at finalisation.
263
+ """
264
+ self.error_summary = _truncate(summary, 4_000)
265
+ if type:
266
+ self.error_type = str(type)[:128]
267
+
185
268
 
186
269
  class SpanHandle:
187
270
  """Handle returned by span context manager for setting output/attrs."""
@@ -204,6 +287,48 @@ class SpanHandle:
204
287
  self.cost_details: Optional[Dict[str, float]] = None
205
288
  self.temperature: Optional[float] = None
206
289
  self.tool_name: Optional[str] = None
290
+ # Severity for this span (Langfuse parity). Leave None for the default
291
+ # behaviour (backend derives 'error' on a thrown exception, else
292
+ # 'default'). Set 'warning' for a recovered/retried step, 'debug' for
293
+ # verbose spans.
294
+ self.level: Optional[str] = None
295
+ # Manually-recorded error (via set_error) — applied at span close even
296
+ # when the body didn't raise. A raised exception still wins over these.
297
+ self.error_type: Optional[str] = None
298
+ self.error_message: Optional[str] = None
299
+ self.status_code: Optional[str] = None
300
+ self.stack_trace: Optional[str] = None
301
+
302
+ def set_level(self, level: str) -> None:
303
+ """Mark this span's severity (does not change ok/error status)."""
304
+ self.level = level
305
+
306
+ def set_error(
307
+ self,
308
+ message: str,
309
+ *,
310
+ type: Optional[str] = None,
311
+ status_code: Optional[Any] = None,
312
+ stack_trace: Optional[str] = None,
313
+ ) -> None:
314
+ """Record an error on this span without raising — for when you catch an
315
+ exception to recover but still want it captured. Sets ``status='error'``
316
+ plus ``error_type`` / ``error_message`` (and optional ``status_code``),
317
+ and defaults the severity to ``error``.
318
+ """
319
+ self.error_message = _truncate(message, 4_000)
320
+ self.error_type = str(type or "Error")[:128]
321
+ if status_code is not None and status_code != "":
322
+ self.status_code = str(status_code)[:32]
323
+ if stack_trace:
324
+ self.stack_trace = _truncate(stack_trace, 20_000)
325
+ if self.level is None:
326
+ self.level = "error"
327
+
328
+ @property
329
+ def has_error(self) -> bool:
330
+ """True when set_error was called (span should finalise as error)."""
331
+ return self.error_message is not None or self.error_type is not None
207
332
 
208
333
  def set_input(self, value: Any) -> None:
209
334
  self.input = _prepare_value(value)
@@ -418,10 +543,22 @@ class wrap_agent:
418
543
  assert self.handle is not None
419
544
  ended_iso = _now_iso()
420
545
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
421
- status = "error" if exc is not None else "ok"
422
- error_summary = None
546
+ # Error precedence: a raised exception wins; otherwise a manual
547
+ # run.set_error_summary(...) marks the run errored without raising.
548
+ manual_run_error = self.handle.error_summary is not None
423
549
  if exc is not None:
424
- error_summary = _truncate(str(exc), 4_000)
550
+ status = "error"
551
+ einfo = describe_error(exc_type, exc, tb)
552
+ error_summary = einfo["error_message"]
553
+ error_type = einfo["error_type"]
554
+ elif manual_run_error:
555
+ status = "error"
556
+ error_summary = self.handle.error_summary
557
+ error_type = self.handle.error_type
558
+ else:
559
+ status = "ok"
560
+ error_summary = None
561
+ error_type = None
425
562
 
426
563
  pending = self._processor.get_pending(self.handle.run_id)
427
564
  agg = _aggregate(pending)
@@ -433,12 +570,14 @@ class wrap_agent:
433
570
  conversation_id=self._conversation_id,
434
571
  parent_run_id=self._parent_run_id,
435
572
  status=status,
573
+ level="error" if status == "error" else None,
436
574
  input=self.handle.input,
437
575
  output=self.handle.output,
438
576
  started_at=self._started_iso,
439
577
  ended_at=ended_iso,
440
578
  duration_ms=duration_ms,
441
579
  error_summary=error_summary,
580
+ error_type=error_type,
442
581
  metadata={**(self._metadata or {}), **self.handle.metadata} or None,
443
582
  total_tokens_in=agg["total_tokens_in"],
444
583
  total_tokens_out=agg["total_tokens_out"],
@@ -583,9 +722,9 @@ class join_run:
583
722
  return None
584
723
  ended_iso = _now_iso()
585
724
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
586
- status = "error" if exc is not None else "ok"
587
- error_type = exc_type.__name__ if exc_type else None
588
- error_message = _truncate(str(exc), 4_000) if exc else None
725
+ einfo = _resolve_error(self.handle, exc_type, exc, tb)
726
+ status = einfo["status"]
727
+ level = self.handle.level or einfo["level"]
589
728
 
590
729
  trodo_span = TrodoSpan(
591
730
  span_id=self._span_id,
@@ -594,13 +733,16 @@ class join_run:
594
733
  kind=self._kind,
595
734
  name=self._name,
596
735
  status=status,
736
+ level=level,
597
737
  started_at=self._started_iso,
598
738
  ended_at=ended_iso,
599
739
  duration_ms=duration_ms,
600
740
  input=self.handle.input,
601
741
  output=self.handle.output,
602
- error_type=error_type,
603
- error_message=error_message,
742
+ error_type=einfo["error_type"],
743
+ error_message=einfo["error_message"],
744
+ status_code=einfo["status_code"],
745
+ stack_trace=einfo["stack_trace"],
604
746
  model=self.handle.model,
605
747
  provider=self.handle.provider,
606
748
  input_tokens=self.handle.input_tokens,
@@ -675,9 +817,9 @@ class span:
675
817
  return None
676
818
  ended_iso = _now_iso()
677
819
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
678
- status = "error" if exc is not None else "ok"
679
- error_type = exc_type.__name__ if exc_type else None
680
- error_message = _truncate(str(exc), 4_000) if exc else None
820
+ einfo = _resolve_error(self.handle, exc_type, exc, tb)
821
+ status = einfo["status"]
822
+ level = self.handle.level or einfo["level"]
681
823
 
682
824
  trodo_span = TrodoSpan(
683
825
  span_id=self._span_id,
@@ -686,13 +828,16 @@ class span:
686
828
  kind=self._kind,
687
829
  name=self._name,
688
830
  status=status,
831
+ level=level,
689
832
  started_at=self._started_iso,
690
833
  ended_at=ended_iso,
691
834
  duration_ms=duration_ms,
692
835
  input=self.handle.input,
693
836
  output=self.handle.output,
694
- error_type=error_type,
695
- error_message=error_message,
837
+ error_type=einfo["error_type"],
838
+ error_message=einfo["error_message"],
839
+ status_code=einfo["status_code"],
840
+ stack_trace=einfo["stack_trace"],
696
841
  model=self.handle.model,
697
842
  provider=self.handle.provider,
698
843
  input_tokens=self.handle.input_tokens,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.8.0
3
+ Version: 2.10.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -1,4 +1,4 @@
1
- trodo/__init__.py,sha256=UhZyFiLF3cVeDzreNw_4QCHkcP-wM1ifPF0adtbUHyk,16678
1
+ trodo/__init__.py,sha256=qkeqpKx-jNb_bkLxbo-JmFR6tfMrc3QAuVvKRdJSMgk,16679
2
2
  trodo/client.py,sha256=8DsKoLh_eaNxj93qkHynfeee-QsdomB_kXfUQjGnWDk,18607
3
3
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
4
4
  trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
@@ -12,20 +12,20 @@ trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
13
13
  trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8pK1E,2882
14
14
  trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
15
- trodo/otel/auto_instrument.py,sha256=7uKhir0o0Mo_od1H2oMf5PHZovcUocHtgV18mRm2Erc,11193
15
+ trodo/otel/auto_instrument.py,sha256=gym90cYD6NzVDVsNtUjKHdKANy17t4AnU4lYGEGHyo8,12866
16
16
  trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
17
- trodo/otel/helpers.py,sha256=7N1Iyi9IsDHkXpKnGnHl6fuLynQKX0tx61cgeuspCy4,20004
18
- trodo/otel/processor.py,sha256=Qtc5QEIKKv5EaGO0KF7kp02DUZbCziKvWDIHxCss8H0,6884
17
+ trodo/otel/helpers.py,sha256=4HsjMOrE-7zuvaRSiGXxV7ZyfXQ5gLxtR3HdpLut9sk,20054
18
+ trodo/otel/processor.py,sha256=aqcTmzTw9cESgIp829pu_XCa5_dG_2MaeJNsqJZeqQU,7495
19
19
  trodo/otel/register.py,sha256=YV2EnkUoa-_54YAuChOe-Mg28UUKg8JO7-qhVP9G6u4,7644
20
20
  trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
21
- trodo/otel/wrap_agent.py,sha256=NZ3yc2grRyoImjWDqCOuhBxJGbXxGQzKL8b2Yqp_iQc,27370
21
+ trodo/otel/wrap_agent.py,sha256=zci2Nkoxwojz2f565fTCXfKUbQtaqfo8t7H2K3xr1CU,33511
22
22
  trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  trodo/queue/batch_flusher.py,sha256=4Lg6T3Urwi9U0Q4FpFGPmjDYKg4ZliCTR-ND8BJvWaY,1298
24
24
  trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,872
25
25
  trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrTjs,2136
27
27
  trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
28
- trodo_python-2.8.0.dist-info/METADATA,sha256=RfMusdv9xrhyXjlg_zXFxeaEMfppXm8yg2mw7ClATKA,20482
29
- trodo_python-2.8.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
30
- trodo_python-2.8.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
- trodo_python-2.8.0.dist-info/RECORD,,
28
+ trodo_python-2.10.0.dist-info/METADATA,sha256=ygRdfLxMzy896pqXhPsUEHeAULJ2P2BopImCjynGC08,20483
29
+ trodo_python-2.10.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
30
+ trodo_python-2.10.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
+ trodo_python-2.10.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5