trodo-python 2.4.0__py3-none-any.whl → 2.4.1__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.
@@ -34,11 +34,17 @@ def _infer_kind(attrs: Dict[str, Any]) -> str:
34
34
  return "generic"
35
35
 
36
36
 
37
+ _ATTR_TRODO_RUN_ID = "trodo.run_id"
38
+ _ATTR_TRODO_PARENT_SPAN_ID = "trodo.parent_span_id"
39
+
40
+
37
41
  def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
38
- """Translate an OTel ReadableSpan to our TrodoSpan using GenAI semconv."""
39
- ctx = get_active_context()
40
- if ctx is None:
41
- return None # span emitted outside of wrap_agent drop
42
+ """Translate an OTel ReadableSpan to our TrodoSpan using GenAI semconv.
43
+
44
+ Recovers the active run from on_start-stamped attributes when contextvars
45
+ have been clobbered by httpx/asyncio context loss at span end. Mirrors
46
+ the trodo-node 2.4.3 bridge behaviour.
47
+ """
42
48
  try:
43
49
  span_ctx = (
44
50
  otel_span.get_span_context()
@@ -53,15 +59,36 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
53
59
  return None
54
60
  span_id = f"{span_id_int:016x}" if isinstance(span_id_int, int) else str(span_id_int)
55
61
 
62
+ attrs = dict(getattr(otel_span, "attributes", {}) or {})
63
+
64
+ # Prefer on_start-stamped run/parent so async-context loss can't drop
65
+ # auto-instrumented LLM spans.
66
+ stamped_run_id = attrs.get(_ATTR_TRODO_RUN_ID)
67
+ stamped_parent_span_id = attrs.get(_ATTR_TRODO_PARENT_SPAN_ID)
68
+
69
+ ctx = get_active_context()
70
+ run_id = stamped_run_id if isinstance(stamped_run_id, str) else (ctx.run_id if ctx else None)
71
+ if not run_id:
72
+ return None # emitted outside any wrap_agent — drop
73
+
56
74
  parent = getattr(otel_span, "parent", None)
57
- parent_span_id: Optional[str] = None
75
+ otel_parent_span_id: Optional[str] = None
58
76
  if parent is not None:
59
77
  pid = getattr(parent, "span_id", None)
60
- parent_span_id = f"{pid:016x}" if isinstance(pid, int) else str(pid) if pid else None
61
- if parent_span_id is None:
62
- parent_span_id = ctx.span_id
78
+ otel_parent_span_id = (
79
+ f"{pid:016x}" if isinstance(pid, int) else str(pid) if pid else None
80
+ )
63
81
 
64
- attrs = dict(getattr(otel_span, "attributes", {}) or {})
82
+ if isinstance(stamped_parent_span_id, str):
83
+ parent_span_id: Optional[str] = stamped_parent_span_id
84
+ elif otel_parent_span_id:
85
+ parent_span_id = otel_parent_span_id
86
+ else:
87
+ parent_span_id = ctx.span_id if ctx else None
88
+
89
+ # Strip the internal attrs so they don't leak into the persisted span.
90
+ attrs.pop(_ATTR_TRODO_RUN_ID, None)
91
+ attrs.pop(_ATTR_TRODO_PARENT_SPAN_ID, None)
65
92
  kind = _infer_kind(attrs)
66
93
 
67
94
  start_time = getattr(otel_span, "start_time", None)
@@ -70,7 +97,9 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
70
97
  ended_at = _hr_to_iso(end_time)
71
98
  duration_ms = None
72
99
  if start_time and end_time:
73
- duration_ms = max(0, int((end_time - start_time) / 1e6))
100
+ # round-half-to-even semantics are fine here; the constraint is
101
+ # "integer ms" matching the agent_spans.duration_ms column.
102
+ duration_ms = max(0, round((end_time - start_time) / 1e6))
74
103
 
75
104
  status = getattr(otel_span, "status", None)
76
105
  status_code = getattr(status, "status_code", None)
@@ -104,7 +133,7 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
104
133
 
105
134
  return TrodoSpan(
106
135
  span_id=span_id,
107
- run_id=ctx.run_id,
136
+ run_id=run_id,
108
137
  parent_span_id=parent_span_id,
109
138
  kind=kind,
110
139
  name=getattr(otel_span, "name", kind),
@@ -131,7 +160,20 @@ class _OtelAdapter:
131
160
  self._processor = processor
132
161
 
133
162
  def on_start(self, span: Any, parent_context: Any = None) -> None:
134
- pass
163
+ # Stamp active run/parent ids while contextvars are still alive.
164
+ # See otel_span_to_trodo_span() docstring for the failure mode this
165
+ # guards against (async-context loss across httpx await boundaries).
166
+ ctx = get_active_context()
167
+ if ctx is None:
168
+ return
169
+ try:
170
+ set_attr = getattr(span, "set_attribute", None)
171
+ if callable(set_attr):
172
+ set_attr(_ATTR_TRODO_RUN_ID, ctx.run_id)
173
+ if ctx.span_id:
174
+ set_attr(_ATTR_TRODO_PARENT_SPAN_ID, ctx.span_id)
175
+ except Exception:
176
+ pass # never break user code
135
177
 
136
178
  def on_end(self, span: Any) -> None:
137
179
  trodo_span = otel_span_to_trodo_span(span)
trodo/otel/helpers.py CHANGED
@@ -320,14 +320,19 @@ def track_mcp(
320
320
  span_id = str(_uuid.uuid4())
321
321
  conv_id = session_id or str(_uuid.uuid4())
322
322
 
323
+ # Coerce duration to a non-negative integer. agent_spans.duration_ms is an
324
+ # integer column and callers occasionally pass floats from time.perf_counter()
325
+ # deltas — without this, fractional values 500 the ingest.
326
+ if isinstance(duration_ms, (int, float)) and duration_ms > 0:
327
+ duration_ms = int(round(duration_ms))
328
+ else:
329
+ duration_ms = 0
330
+
323
331
  now = datetime.now(timezone.utc)
324
332
  if ended_at is None:
325
333
  ended_at = now.isoformat()
326
334
  if started_at is None:
327
- offset_ms = duration_ms if isinstance(duration_ms, int) and duration_ms > 0 else 0
328
- started_at = (now - timedelta(milliseconds=offset_ms)).isoformat()
329
- if duration_ms is None:
330
- duration_ms = 0
335
+ started_at = (now - timedelta(milliseconds=duration_ms)).isoformat()
331
336
 
332
337
  status = "error" if error else "ok"
333
338
  if error:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.4.0
3
+ Version: 2.4.1
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -12,9 +12,9 @@ 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=J_neFxvO-3YACUvtetY4RdM8xYA_79SZUgPry6hXrm8,9434
15
+ trodo/otel/auto_instrument.py,sha256=7uKhir0o0Mo_od1H2oMf5PHZovcUocHtgV18mRm2Erc,11193
16
16
  trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
17
- trodo/otel/helpers.py,sha256=_ImekkJOWWT0pD8Vm1NuKBIps4wjCjx-72SUu7MpAyk,16172
17
+ trodo/otel/helpers.py,sha256=IEAHxAEN-Bvv_ZODrmRzC6PCGGhGTXU7IPcp6iO2nbA,16405
18
18
  trodo/otel/processor.py,sha256=jVZkslZlw50G5uRAa7-GMRgn_yvae58EmlWTZL8tMkQ,6285
19
19
  trodo/otel/register.py,sha256=YV2EnkUoa-_54YAuChOe-Mg28UUKg8JO7-qhVP9G6u4,7644
20
20
  trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
@@ -25,7 +25,7 @@ trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,87
25
25
  trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  trodo/session/server_session.py,sha256=4bQZc_Zxktmu8RVoyh0qI7tvr8AKsHI5xkGf3jEpWVE,2005
27
27
  trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
28
- trodo_python-2.4.0.dist-info/METADATA,sha256=E-lbxpb5rFqecvK342J_gDgpg3QSSM_jfyYi2O0a1mk,17882
29
- trodo_python-2.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
30
- trodo_python-2.4.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
- trodo_python-2.4.0.dist-info/RECORD,,
28
+ trodo_python-2.4.1.dist-info/METADATA,sha256=_V5plcgVHmz0vb9S9fQXgivuVvmsYs2Aict8OViy5FU,17882
29
+ trodo_python-2.4.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
30
+ trodo_python-2.4.1.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
+ trodo_python-2.4.1.dist-info/RECORD,,