trodo-python 2.9.0__tar.gz → 2.10.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.
Files changed (47) hide show
  1. {trodo_python-2.9.0 → trodo_python-2.10.0}/PKG-INFO +1 -1
  2. {trodo_python-2.9.0 → trodo_python-2.10.0}/pyproject.toml +1 -1
  3. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_error_enrichment.py +23 -0
  4. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/__init__.py +1 -1
  5. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/wrap_agent.py +99 -19
  6. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo_python.egg-info/PKG-INFO +1 -1
  7. {trodo_python-2.9.0 → trodo_python-2.10.0}/README.md +0 -0
  8. {trodo_python-2.9.0 → trodo_python-2.10.0}/setup.cfg +0 -0
  9. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_anon_distinct_id 2.py +0 -0
  10. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_anon_distinct_id.py +0 -0
  11. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_auto_instrument_fixes.py +0 -0
  12. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_cross_process_session.py +0 -0
  13. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_end_run.py +0 -0
  14. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_llm_usage_cost.py +0 -0
  15. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_processor_methods.py +0 -0
  16. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_register_otel.py +0 -0
  17. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_start_run.py +0 -0
  18. {trodo_python-2.9.0 → trodo_python-2.10.0}/tests/test_wrap_agent_unchanged.py +0 -0
  19. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/api/__init__.py +0 -0
  20. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/api/async_client.py +0 -0
  21. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/api/endpoints.py +0 -0
  22. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/api/http_client.py +0 -0
  23. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/auto/__init__.py +0 -0
  24. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/auto/auto_event_manager.py +0 -0
  25. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/client.py +0 -0
  26. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/managers/__init__.py +0 -0
  27. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/managers/group_manager.py +0 -0
  28. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/managers/people_manager.py +0 -0
  29. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/__init__.py +0 -0
  30. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/auto_instrument.py +0 -0
  31. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/context.py +0 -0
  32. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/helpers.py +0 -0
  33. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/processor.py +0 -0
  34. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/register.py +0 -0
  35. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/otel/transport.py +0 -0
  36. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/queue/__init__.py +0 -0
  37. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/queue/batch_flusher.py +0 -0
  38. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/queue/event_queue.py +0 -0
  39. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/session/__init__.py +0 -0
  40. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/session/server_session.py +0 -0
  41. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/session/session_manager.py +0 -0
  42. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/types.py +0 -0
  43. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo/user_context.py +0 -0
  44. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo_python.egg-info/SOURCES.txt +0 -0
  45. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo_python.egg-info/dependency_links.txt +0 -0
  46. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo_python.egg-info/requires.txt +0 -0
  47. {trodo_python-2.9.0 → trodo_python-2.10.0}/trodo_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.9.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trodo-python"
7
- version = "2.9.0"
7
+ version = "2.10.0"
8
8
  description = "Trodo Analytics SDK for Python — server-side event tracking"
9
9
  readme = "README.md"
10
10
  license = { text = "ISC" }
@@ -70,6 +70,29 @@ def test_errored_span_and_run_carry_rich_error_fields(processor, http):
70
70
  assert "rate limit exceeded" in run_payload["error_summary"]
71
71
 
72
72
 
73
+ def test_set_error_records_without_raising(processor, http):
74
+ with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat") as run:
75
+ with span("soft-step", kind="tool") as s:
76
+ s.set_error("bad input", type="ValidationError", status_code=422)
77
+ # no raise — the span still finalises as error
78
+ run.set_error_summary("partial failure", type="PartialFailure")
79
+
80
+ ingest = http.run_ingest[0]
81
+ run_payload = ingest["run"]
82
+ s = next(x for x in ingest.get("spans", []) if x["name"] == "soft-step")
83
+
84
+ assert s["status"] == "error"
85
+ assert s["error_type"] == "ValidationError"
86
+ assert s["error_message"] == "bad input"
87
+ assert s["status_code"] == "422"
88
+ assert s["level"] == "error"
89
+
90
+ assert run_payload["status"] == "error"
91
+ assert run_payload["error_summary"] == "partial failure"
92
+ assert run_payload["error_type"] == "PartialFailure"
93
+ assert run_payload["level"] == "error"
94
+
95
+
73
96
  def test_healthy_span_omits_error_fields(processor, http):
74
97
  with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat"):
75
98
  with span("ok-step", kind="generic"):
@@ -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.9.0"
43
+ __version__ = "2.10.0"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -128,6 +128,27 @@ def describe_error(exc_type, exc, tb=None) -> Dict[str, Optional[str]]:
128
128
  }
129
129
 
130
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
+
131
152
  def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any]]]:
132
153
  """Prepare a value for storage in the JSONB input/output column.
133
154
 
@@ -221,6 +242,10 @@ class RunHandle:
221
242
  self.input: Optional[Union[str, Dict[str, Any]]] = None
222
243
  self.output: Optional[Union[str, Dict[str, Any]]] = None
223
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
224
249
 
225
250
  def set_input(self, value: Any) -> None:
226
251
  self.input = _prepare_value(value)
@@ -231,6 +256,15 @@ class RunHandle:
231
256
  def set_metadata(self, **kwargs: Any) -> None:
232
257
  self.metadata.update(kwargs)
233
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
+
234
268
 
235
269
  class SpanHandle:
236
270
  """Handle returned by span context manager for setting output/attrs."""
@@ -258,11 +292,44 @@ class SpanHandle:
258
292
  # 'default'). Set 'warning' for a recovered/retried step, 'debug' for
259
293
  # verbose spans.
260
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
261
301
 
262
302
  def set_level(self, level: str) -> None:
263
303
  """Mark this span's severity (does not change ok/error status)."""
264
304
  self.level = level
265
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
332
+
266
333
  def set_input(self, value: Any) -> None:
267
334
  self.input = _prepare_value(value)
268
335
 
@@ -476,9 +543,22 @@ class wrap_agent:
476
543
  assert self.handle is not None
477
544
  ended_iso = _now_iso()
478
545
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
479
- status = "error" if exc is not None else "ok"
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
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
549
+ if exc is not None:
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
482
562
 
483
563
  pending = self._processor.get_pending(self.handle.run_id)
484
564
  agg = _aggregate(pending)
@@ -490,14 +570,14 @@ class wrap_agent:
490
570
  conversation_id=self._conversation_id,
491
571
  parent_run_id=self._parent_run_id,
492
572
  status=status,
493
- level="error" if exc is not None else None,
573
+ level="error" if status == "error" else None,
494
574
  input=self.handle.input,
495
575
  output=self.handle.output,
496
576
  started_at=self._started_iso,
497
577
  ended_at=ended_iso,
498
578
  duration_ms=duration_ms,
499
579
  error_summary=error_summary,
500
- error_type=einfo["error_type"] if einfo else None,
580
+ error_type=error_type,
501
581
  metadata={**(self._metadata or {}), **self.handle.metadata} or None,
502
582
  total_tokens_in=agg["total_tokens_in"],
503
583
  total_tokens_out=agg["total_tokens_out"],
@@ -642,9 +722,9 @@ class join_run:
642
722
  return None
643
723
  ended_iso = _now_iso()
644
724
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
645
- status = "error" if exc is not None else "ok"
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)
725
+ einfo = _resolve_error(self.handle, exc_type, exc, tb)
726
+ status = einfo["status"]
727
+ level = self.handle.level or einfo["level"]
648
728
 
649
729
  trodo_span = TrodoSpan(
650
730
  span_id=self._span_id,
@@ -659,10 +739,10 @@ class join_run:
659
739
  duration_ms=duration_ms,
660
740
  input=self.handle.input,
661
741
  output=self.handle.output,
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,
742
+ error_type=einfo["error_type"],
743
+ error_message=einfo["error_message"],
744
+ status_code=einfo["status_code"],
745
+ stack_trace=einfo["stack_trace"],
666
746
  model=self.handle.model,
667
747
  provider=self.handle.provider,
668
748
  input_tokens=self.handle.input_tokens,
@@ -737,9 +817,9 @@ class span:
737
817
  return None
738
818
  ended_iso = _now_iso()
739
819
  duration_ms = int(time.time() * 1000.0 - self._started_ms)
740
- status = "error" if exc is not None else "ok"
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)
820
+ einfo = _resolve_error(self.handle, exc_type, exc, tb)
821
+ status = einfo["status"]
822
+ level = self.handle.level or einfo["level"]
743
823
 
744
824
  trodo_span = TrodoSpan(
745
825
  span_id=self._span_id,
@@ -754,10 +834,10 @@ class span:
754
834
  duration_ms=duration_ms,
755
835
  input=self.handle.input,
756
836
  output=self.handle.output,
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,
837
+ error_type=einfo["error_type"],
838
+ error_message=einfo["error_message"],
839
+ status_code=einfo["status_code"],
840
+ stack_trace=einfo["stack_trace"],
761
841
  model=self.handle.model,
762
842
  provider=self.handle.provider,
763
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.9.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
File without changes
File without changes