trodo-python 2.8.0__py3-none-any.whl → 2.9.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.9.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,54 @@ 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
+
82
131
  def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any]]]:
83
132
  """Prepare a value for storage in the JSONB input/output column.
84
133
 
@@ -204,6 +253,15 @@ class SpanHandle:
204
253
  self.cost_details: Optional[Dict[str, float]] = None
205
254
  self.temperature: Optional[float] = None
206
255
  self.tool_name: Optional[str] = None
256
+ # Severity for this span (Langfuse parity). Leave None for the default
257
+ # behaviour (backend derives 'error' on a thrown exception, else
258
+ # 'default'). Set 'warning' for a recovered/retried step, 'debug' for
259
+ # verbose spans.
260
+ self.level: Optional[str] = None
261
+
262
+ def set_level(self, level: str) -> None:
263
+ """Mark this span's severity (does not change ok/error status)."""
264
+ self.level = level
207
265
 
208
266
  def set_input(self, value: Any) -> None:
209
267
  self.input = _prepare_value(value)
@@ -419,9 +477,8 @@ class wrap_agent:
419
477
  ended_iso = _now_iso()
420
478
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
421
479
  status = "error" if exc is not None else "ok"
422
- error_summary = None
423
- if exc is not None:
424
- error_summary = _truncate(str(exc), 4_000)
480
+ einfo = describe_error(exc_type, exc, tb) if exc is not None else None
481
+ error_summary = einfo["error_message"] if einfo else None
425
482
 
426
483
  pending = self._processor.get_pending(self.handle.run_id)
427
484
  agg = _aggregate(pending)
@@ -433,12 +490,14 @@ class wrap_agent:
433
490
  conversation_id=self._conversation_id,
434
491
  parent_run_id=self._parent_run_id,
435
492
  status=status,
493
+ level="error" if exc is not None else None,
436
494
  input=self.handle.input,
437
495
  output=self.handle.output,
438
496
  started_at=self._started_iso,
439
497
  ended_at=ended_iso,
440
498
  duration_ms=duration_ms,
441
499
  error_summary=error_summary,
500
+ error_type=einfo["error_type"] if einfo else None,
442
501
  metadata={**(self._metadata or {}), **self.handle.metadata} or None,
443
502
  total_tokens_in=agg["total_tokens_in"],
444
503
  total_tokens_out=agg["total_tokens_out"],
@@ -584,8 +643,8 @@ class join_run:
584
643
  ended_iso = _now_iso()
585
644
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
586
645
  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
646
+ einfo = describe_error(exc_type, exc, tb) if exc is not None else None
647
+ level = self.handle.level or ("error" if exc is not None else None)
589
648
 
590
649
  trodo_span = TrodoSpan(
591
650
  span_id=self._span_id,
@@ -594,13 +653,16 @@ class join_run:
594
653
  kind=self._kind,
595
654
  name=self._name,
596
655
  status=status,
656
+ level=level,
597
657
  started_at=self._started_iso,
598
658
  ended_at=ended_iso,
599
659
  duration_ms=duration_ms,
600
660
  input=self.handle.input,
601
661
  output=self.handle.output,
602
- error_type=error_type,
603
- error_message=error_message,
662
+ error_type=einfo["error_type"] if einfo else None,
663
+ error_message=einfo["error_message"] if einfo else None,
664
+ status_code=einfo["status_code"] if einfo else None,
665
+ stack_trace=einfo["stack_trace"] if einfo else None,
604
666
  model=self.handle.model,
605
667
  provider=self.handle.provider,
606
668
  input_tokens=self.handle.input_tokens,
@@ -676,8 +738,8 @@ class span:
676
738
  ended_iso = _now_iso()
677
739
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
678
740
  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
741
+ einfo = describe_error(exc_type, exc, tb) if exc is not None else None
742
+ level = self.handle.level or ("error" if exc is not None else None)
681
743
 
682
744
  trodo_span = TrodoSpan(
683
745
  span_id=self._span_id,
@@ -686,13 +748,16 @@ class span:
686
748
  kind=self._kind,
687
749
  name=self._name,
688
750
  status=status,
751
+ level=level,
689
752
  started_at=self._started_iso,
690
753
  ended_at=ended_iso,
691
754
  duration_ms=duration_ms,
692
755
  input=self.handle.input,
693
756
  output=self.handle.output,
694
- error_type=error_type,
695
- error_message=error_message,
757
+ error_type=einfo["error_type"] if einfo else None,
758
+ error_message=einfo["error_message"] if einfo else None,
759
+ status_code=einfo["status_code"] if einfo else None,
760
+ stack_trace=einfo["stack_trace"] if einfo else None,
696
761
  model=self.handle.model,
697
762
  provider=self.handle.provider,
698
763
  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.9.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=p9NGic9WzF53OYKtk-MSdQNcCTbXq8DNZeXYWl9OCUo,16678
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=Nvjj8CjIGHqfWNV_heHno9HKJbKU3lHqHInX1EyRhnw,30334
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.9.0.dist-info/METADATA,sha256=Ab8QIUgDMayOyTjdtrdFP_EMk5bCcLHtX4hZJaaKEac,20482
29
+ trodo_python-2.9.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
30
+ trodo_python-2.9.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
+ trodo_python-2.9.0.dist-info/RECORD,,