22Tracer factory with automatic tier detection.
33
44Provides convenient factory function for creating tracers with cloud upload support.
5+
6+ Key Features:
7+ - Automatic cloud upload when API key is provided
8+ - Auto-close on process exit (atexit) to prevent data loss
9+ - Context manager support for both sync and async workflows
10+ - Orphaned trace recovery from previous crashes
511"""
612
13+ import atexit
714import gzip
815import os
916import uuid
17+ import weakref
1018from collections .abc import Callable
1119from pathlib import Path
1220from typing import Any , Optional
1725from predicate .constants import PREDICATE_API_URL
1826from predicate .tracing import JsonlTraceSink , Tracer
1927
28+ # Global registry of active tracers for atexit cleanup
29+ # Using a set of tracer IDs mapped to weak references
30+ _active_tracers : dict [int , weakref .ref [Tracer ]] = {}
31+ _atexit_registered = False
32+
33+
34+ def _cleanup_tracers_on_exit () -> None :
35+ """
36+ Cleanup handler called on process exit.
37+
38+ Closes all active tracers to ensure trace data is uploaded to cloud.
39+ This prevents data loss when users forget to call tracer.close().
40+ """
41+ for tracer_id , tracer_ref in list (_active_tracers .items ()):
42+ tracer = tracer_ref ()
43+ if tracer is not None :
44+ try :
45+ tracer .close ()
46+ except Exception :
47+ pass # Best effort - don't raise during exit
48+
49+
50+ def _register_tracer_for_cleanup (tracer : Tracer ) -> None :
51+ """
52+ Register a tracer for automatic cleanup on process exit.
53+
54+ Args:
55+ tracer: Tracer instance to register
56+ """
57+ global _atexit_registered
58+
59+ # Use id() as key to avoid hashability issues
60+ tracer_id = id (tracer )
61+ _active_tracers [tracer_id ] = weakref .ref (tracer )
62+
63+ # Set callback on tracer so it unregisters itself when closed
64+ tracer ._on_close_callback = _unregister_tracer
65+
66+ # Register atexit handler on first tracer creation
67+ if not _atexit_registered :
68+ atexit .register (_cleanup_tracers_on_exit )
69+ _atexit_registered = True
70+
71+
72+ def _unregister_tracer (tracer : Tracer ) -> None :
73+ """
74+ Unregister a tracer from cleanup (called when tracer.close() is invoked).
75+
76+ Args:
77+ tracer: Tracer instance to unregister
78+ """
79+ tracer_id = id (tracer )
80+ _active_tracers .pop (tracer_id , None )
81+
2082
2183def _emit_run_start (
2284 tracer : Tracer ,
@@ -58,12 +120,17 @@ def create_tracer(
58120 auto_emit_run_start : bool = True ,
59121) -> Tracer :
60122 """
61- Create tracer with automatic tier detection.
123+ Create tracer with automatic tier detection and auto-cleanup .
62124
63125 Tier Detection:
64126 - If api_key is provided: Try to initialize CloudTraceSink (Pro/Enterprise)
65127 - If cloud init fails or no api_key: Fall back to JsonlTraceSink (Free tier)
66128
129+ Auto-Cleanup:
130+ - Tracers are automatically registered for cleanup on process exit (atexit)
131+ - This ensures trace data is uploaded even if tracer.close() is not called
132+ - For best practice, still call tracer.close() explicitly or use context manager
133+
67134 Args:
68135 api_key: Sentience API key (e.g., "sk_pro_xxxxx")
69136 - Free tier: None or empty
@@ -92,7 +159,21 @@ def create_tracer(
92159 Tracer configured with appropriate sink
93160
94161 Example:
95- >>> # Pro tier user with goal
162+ >>> # RECOMMENDED: Use as context manager (auto-closes on exit)
163+ >>> with create_tracer(api_key="sk_pro_xyz", goal="Add to cart") as tracer:
164+ ... agent = SentienceAgent(browser, llm, tracer=tracer)
165+ ... agent.act("Click search")
166+ >>> # tracer.close() called automatically
167+ >>>
168+ >>> # ALTERNATIVE: Manual close (still safe - atexit cleanup as fallback)
169+ >>> tracer = create_tracer(api_key="sk_pro_xyz", goal="Add to cart")
170+ >>> try:
171+ ... agent = SentienceAgent(browser, llm, tracer=tracer)
172+ ... agent.act("Click search")
173+ ... finally:
174+ ... tracer.close() # Best practice: explicit close
175+ >>>
176+ >>> # Pro tier with all metadata
96177 >>> tracer = create_tracer(
97178 ... api_key="sk_pro_xyz",
98179 ... run_id="demo",
@@ -101,8 +182,6 @@ def create_tracer(
101182 ... llm_model="gpt-4-turbo",
102183 ... start_url="https://amazon.com"
103184 ... )
104- >>> # Returns: Tracer with CloudTraceSink
105- >>> # run_start event is automatically emitted
106185 >>>
107186 >>> # With screenshot processor for PII redaction
108187 >>> def redact_pii(screenshot_base64: str) -> str:
@@ -113,20 +192,9 @@ def create_tracer(
113192 ... api_key="sk_pro_xyz",
114193 ... screenshot_processor=redact_pii
115194 ... )
116- >>> # Screenshots will be processed before upload
117195 >>>
118- >>> # Free tier user
196+ >>> # Free tier user (local-only traces)
119197 >>> tracer = create_tracer(run_id="demo")
120- >>> # Returns: Tracer with JsonlTraceSink (local-only)
121- >>>
122- >>> # Disable auto-emit for manual control
123- >>> tracer = create_tracer(run_id="demo", auto_emit_run_start=False)
124- >>> tracer.emit_run_start("MyAgent", "gpt-4o") # Manual emit
125- >>>
126- >>> # Use with agent
127- >>> agent = SentienceAgent(browser, llm, tracer=tracer)
128- >>> agent.act("Click search")
129- >>> tracer.close() # Uploads to cloud if Pro tier
130198 """
131199 if run_id is None :
132200 run_id = str (uuid .uuid4 ())
@@ -187,6 +255,8 @@ def create_tracer(
187255 ),
188256 screenshot_processor = screenshot_processor ,
189257 )
258+ # Register for atexit cleanup (safety net for forgotten close())
259+ _register_tracer_for_cleanup (tracer )
190260 # Auto-emit run_start for complete trace structure
191261 if auto_emit_run_start :
192262 _emit_run_start (tracer , agent_type , llm_model , goal , start_url )
@@ -254,6 +324,9 @@ def create_tracer(
254324 screenshot_processor = screenshot_processor ,
255325 )
256326
327+ # Register for atexit cleanup (ensures file is properly closed)
328+ _register_tracer_for_cleanup (tracer )
329+
257330 # Auto-emit run_start for complete trace structure
258331 if auto_emit_run_start :
259332 _emit_run_start (tracer , agent_type , llm_model , goal , start_url )
0 commit comments