trodo-python 3.0.2__py3-none-any.whl → 3.0.3__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
@@ -180,7 +180,7 @@ def init(
180
180
  are present, otherwise 'background'. Default.
181
181
  ``delivery_options``: flush_interval_s (1), flush_at (50), max_batch_bytes
182
182
  (4 MB), max_queue_items (10,000), max_queue_bytes (64 MB), max_retry_s
183
- (600), exit_flush_s (5).
183
+ (600), exit_flush_s (5), max_in_flight (3).
184
184
 
185
185
  ``disable_instrumentations`` turns OFF specific instrumentors by id
186
186
  (e.g. ``['openai']`` when LangChain is your primary framework, to avoid
trodo/_version.py CHANGED
@@ -1 +1 @@
1
- SDK_VERSION = "3.0.2"
1
+ SDK_VERSION = "3.0.3"
trodo/delivery/sender.py CHANGED
@@ -5,8 +5,12 @@ retry and Retry-After — on the caller's thread before handing the agent's
5
5
  result back: up to ~90 s added to one agent call when a team was over its
6
6
  rate ceiling, the event loop frozen in an asyncio app, and then the trace was
7
7
  dropped. Now a finished run (or a flush of spans) goes into this queue and
8
- the agent returns at once. A daemon thread drains the queue to
9
- ``POST /api/sdk/batch`` every second or 50 items, gzipped.
8
+ the agent returns at once. Daemon threads drain the queue to
9
+ ``POST /api/sdk/batch`` every second or 50 items, gzipped, up to
10
+ ``max_in_flight`` (3) requests at a time (3.0.3; one at a time before, which
11
+ let a script that exits after a burst deliver only ~150 runs in its 5 s exit
12
+ window). Feedback never travels while its run is in flight in another
13
+ request: the server refuses feedback for a run it has not stored yet.
10
14
 
11
15
  The rules match what Langfuse, LangSmith, Braintrust, OpenTelemetry, Sentry
12
16
  and PostHog have converged on (1 Oct 2026 survey):
@@ -53,7 +57,7 @@ DEFAULTS = dict(
53
57
  flush_interval_s=1.0, flush_at=50, max_batch_bytes=4 * 1024 * 1024,
54
58
  max_queue_items=10_000, max_queue_bytes=64 * 1024 * 1024,
55
59
  max_retry_s=600.0, max_retry_after_s=600.0, immediate_retry_s=15.0,
56
- exit_flush_s=5.0, request_timeout_s=20.0,
60
+ exit_flush_s=5.0, request_timeout_s=20.0, max_in_flight=3,
57
61
  )
58
62
  _MIN_BACKOFF_S = 1.0
59
63
  _MAX_BACKOFF_S = 60.0
@@ -133,7 +137,8 @@ class Sender:
133
137
  self._unit_cap: Optional[int] = None # the team's ceiling, once a 429 names it
134
138
  self._last_rate: Optional[Dict[str, Any]] = None
135
139
  self._warned_paced_at = 0.0
136
- self._thread: Optional[threading.Thread] = None
140
+ self._threads: List[threading.Thread] = []
141
+ self._runs_in_flight: Dict[str, int] = {} # run_id -> requests carrying it
137
142
  if self.mode == "background" and register_atexit:
138
143
  atexit.register(self._atexit)
139
144
 
@@ -203,9 +208,11 @@ class Sender:
203
208
  # ── background thread ─────────────────────────────────────────────────────
204
209
 
205
210
  def _ensure_thread_locked(self) -> None:
206
- if self._thread is None or not self._thread.is_alive():
207
- self._thread = threading.Thread(target=self._run, name="trodo-sender", daemon=True)
208
- self._thread.start()
211
+ self._threads = [t for t in self._threads if t.is_alive()]
212
+ while len(self._threads) < max(1, int(self._o["max_in_flight"])):
213
+ t = threading.Thread(target=self._run, name=f"trodo-sender-{len(self._threads) + 1}", daemon=True)
214
+ self._threads.append(t)
215
+ t.start()
209
216
 
210
217
  def _run(self) -> None:
211
218
  interval = self._o["flush_interval_s"]
@@ -229,12 +236,25 @@ class Sender:
229
236
  break
230
237
  self._wake.wait(timeout=interval - age)
231
238
  batch = self._take_batch_locked()
239
+ if not batch:
240
+ # The head is feedback whose run is in flight: wait for it.
241
+ self._wake.wait(timeout=interval)
242
+ continue
243
+ runs = _run_ids(batch)
244
+ for r in runs:
245
+ self._runs_in_flight[r] = self._runs_in_flight.get(r, 0) + 1
232
246
  self._in_flight += 1
233
247
  try:
234
248
  self._send_batch(batch)
235
249
  finally:
236
250
  with self._lock:
237
251
  self._in_flight -= 1
252
+ for r in runs:
253
+ left = self._runs_in_flight.get(r, 1) - 1
254
+ if left > 0:
255
+ self._runs_in_flight[r] = left
256
+ else:
257
+ self._runs_in_flight.pop(r, None)
238
258
  self._wake.notify_all()
239
259
 
240
260
  def _take_batch_locked(self) -> List[_Queued]:
@@ -249,6 +269,9 @@ class Sender:
249
269
  u = _units_of(nxt.item)
250
270
  if out and cap and units + u > cap:
251
271
  break
272
+ fb = nxt.item.get("run_id") if isinstance(nxt.item, dict) and "feedback" in nxt.item else None
273
+ if fb and fb in self._runs_in_flight:
274
+ break
252
275
  out.append(self._queue.popleft())
253
276
  nbytes += nxt.nbytes
254
277
  units += u
@@ -580,6 +603,16 @@ def _legacy_route(item: Item):
580
603
  return endpoints.SPANS_APPEND, {"spans": item["spans"]}
581
604
 
582
605
 
606
+ def _run_ids(batch: List[_Queued]) -> List[str]:
607
+ """The runs a request carries (the ``run`` items), so their feedback waits for them."""
608
+ out = []
609
+ for q in batch:
610
+ run = q.item.get("run") if isinstance(q.item, dict) else None
611
+ if isinstance(run, dict) and run.get("run_id"):
612
+ out.append(str(run["run_id"]))
613
+ return out
614
+
615
+
583
616
  def _units_of(item: Any) -> int:
584
617
  """What an item costs against the rate ceiling: one per span, one for the run."""
585
618
  if not isinstance(item, dict):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 3.0.2
3
+ Version: 3.0.3
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,5 +1,5 @@
1
- trodo/__init__.py,sha256=x7zE63FK8CFJ6ATUvsXxeTkvZUaLKXqAJr99I6nnyZc,27099
2
- trodo/_version.py,sha256=GmU1Hng0hfRPQUBIrIvzRCIVKBsPLckerj1Erc-Dt1g,22
1
+ trodo/__init__.py,sha256=b6NdLadR5zQ_wPNLhHXFGLJgoDNNeaI9iNpeSoG9sdw,27118
2
+ trodo/_version.py,sha256=mHUYAxoRGG4Dl3kgX7AySWcAjRVyw9veTe7GVztx0Zk,22
3
3
  trodo/client.py,sha256=eaZtHMtOI-VLgfoqM_KVWMuP_I5Ivs7VFnR0oU96Yqk,23743
4
4
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
5
5
  trodo/user_context.py,sha256=uHCI2WYoOI3cNwdIUEuZdX4VYSu14pzm348ksn6hn_c,8195
@@ -12,7 +12,7 @@ trodo/api/management.py,sha256=uwhr0ZI1pvguz5-itW1KcgRj0TXM2n4KDa-hGIaBdTA,4023
12
12
  trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
14
14
  trodo/delivery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- trodo/delivery/sender.py,sha256=kp9kNvS_nJZpRKGD05zXcytwuxCOYD8A27rNJySGqR0,26119
15
+ trodo/delivery/sender.py,sha256=l3itziOJ3-DRRir9-zL7LPf36npzM-whZXzkNbwHuW4,27802
16
16
  trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  trodo/managers/dataset_manager.py,sha256=HTFszK-wKmwjW-aw7xJWr5EKvDpJ_om496MLDr3gelg,13920
18
18
  trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
@@ -40,7 +40,7 @@ trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrT
40
40
  trodo/session/session_manager.py,sha256=7ht5LeeZX1HLLfPeNV_a8NkXGbHl3up0CRtCGr3EzjQ,2995
41
41
  trodo/util/__init__.py,sha256=Z9c4rPPdKg06Kk3byKheDqksSgR4WNvS475oQ5sNljc,54
42
42
  trodo/util/lru.py,sha256=QIsM7s6J_E9ZjiXatSyReZIEeGypTAcgrPWWy8YsRa4,2465
43
- trodo_python-3.0.2.dist-info/METADATA,sha256=o047XtyjWl3fOEhJOR12qlWtDqlBelbYvAsbY3wT4xc,25307
44
- trodo_python-3.0.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
45
- trodo_python-3.0.2.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
46
- trodo_python-3.0.2.dist-info/RECORD,,
43
+ trodo_python-3.0.3.dist-info/METADATA,sha256=efWaheZ0wohogmc70u4LjB59TDMqVLKL5wF8w9okSok,25307
44
+ trodo_python-3.0.3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
45
+ trodo_python-3.0.3.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
46
+ trodo_python-3.0.3.dist-info/RECORD,,