trodo-python 1.0.1__py3-none-any.whl → 1.2.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
@@ -21,7 +21,10 @@ from typing import Any, Dict, List, Optional, Union
21
21
  from .client import TrodoClient
22
22
  from .user_context import UserContext
23
23
  from .managers.group_manager import GroupProfile
24
- from .types import ApiResult, IdentifyResult, ResetResult, WalletAddressResult
24
+ from .types import (
25
+ ApiResult, ResetResult, WalletAddressResult,
26
+ AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps, FeedbackProps,
27
+ )
25
28
 
26
29
  __all__ = [
27
30
  "TrodoClient",
@@ -37,6 +40,17 @@ __all__ = [
37
40
  "disable_auto_events",
38
41
  "flush",
39
42
  "shutdown",
43
+ # Agent analytics
44
+ "AgentCallProps",
45
+ "ToolUseProps",
46
+ "AgentResponseProps",
47
+ "AgentErrorProps",
48
+ "FeedbackProps",
49
+ "track_agent_call",
50
+ "track_tool_use",
51
+ "track_agent_response",
52
+ "track_agent_error",
53
+ "track_feedback",
40
54
  ]
41
55
 
42
56
  # ============================================================================
@@ -101,9 +115,9 @@ def track(
101
115
  _get_client().track(distinct_id, event_name, properties, category)
102
116
 
103
117
 
104
- def identify(distinct_id: str, identify_id: str) -> IdentifyResult:
105
- """Alias a user's distinctId to an external identifier."""
106
- return _get_client().identify(distinct_id, identify_id)
118
+ def identify(identify_id: str, session_id: Optional[str] = None) -> UserContext:
119
+ """Create or retrieve a UserContext for an identified user. Fires identify API on first call."""
120
+ return _get_client().identify(identify_id, session_id)
107
121
 
108
122
 
109
123
  def wallet_address(distinct_id: str, wallet_addr: str) -> WalletAddressResult:
@@ -132,3 +146,32 @@ def flush() -> None:
132
146
  def shutdown() -> None:
133
147
  """Flush, stop timers, and disable auto events."""
134
148
  _get_client().shutdown()
149
+
150
+
151
+ # ----------------------------------------------------------------------------
152
+ # Agent Analytics — singleton wrappers
153
+ # ----------------------------------------------------------------------------
154
+
155
+ def track_agent_call(props: AgentCallProps) -> None:
156
+ """Track an LLM invocation / inbound message."""
157
+ _get_client().track_agent_call(props)
158
+
159
+
160
+ def track_tool_use(props: ToolUseProps) -> None:
161
+ """Track a tool invocation within an agent turn."""
162
+ _get_client().track_tool_use(props)
163
+
164
+
165
+ def track_agent_response(props: AgentResponseProps) -> None:
166
+ """Track an LLM response / completion."""
167
+ _get_client().track_agent_response(props)
168
+
169
+
170
+ def track_agent_error(props: AgentErrorProps) -> None:
171
+ """Track an error during an agent turn."""
172
+ _get_client().track_agent_error(props)
173
+
174
+
175
+ def track_feedback(props: FeedbackProps) -> None:
176
+ """Track a user feedback reaction on an agent response."""
177
+ _get_client().track_feedback(props)
trodo/api/endpoints.py CHANGED
@@ -1,4 +1,5 @@
1
1
  TRACK = "/api/sdk/track"
2
+ TRACK_AGENT = "/api/sdk/track-agent"
2
3
  EVENTS = "/api/events"
3
4
  EVENTS_BULK = "/api/events/bulk"
4
5
  IDENTIFY = "/api/sdk/identify"
trodo/api/http_client.py CHANGED
@@ -85,3 +85,6 @@ class HttpClient:
85
85
 
86
86
  def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
87
87
  return self._request(path, payload)
88
+
89
+ def post_agent_event(self, payload: Dict[str, Any]) -> ApiResult:
90
+ return self._request("/api/sdk/track-agent", payload)
trodo/client.py CHANGED
@@ -14,9 +14,13 @@ from .auto.auto_event_manager import AutoEventManager
14
14
  from .user_context import UserContext
15
15
  from .types import (
16
16
  ApiResult,
17
- IdentifyResult,
18
17
  ResetResult,
19
18
  WalletAddressResult,
19
+ AgentCallProps,
20
+ ToolUseProps,
21
+ AgentResponseProps,
22
+ AgentErrorProps,
23
+ FeedbackProps,
20
24
  )
21
25
 
22
26
 
@@ -107,8 +111,12 @@ class TrodoClient:
107
111
  ) -> None:
108
112
  self.for_user(distinct_id).track(event_name, properties, category)
109
113
 
110
- def identify(self, distinct_id: str, identify_id: str) -> IdentifyResult:
111
- return self.for_user(distinct_id).identify(identify_id)
114
+ def identify(self, identify_id: str, session_id: Optional[str] = None) -> "UserContext":
115
+ if identify_id in self._user_context_cache:
116
+ return self._user_context_cache[identify_id]
117
+ ctx = self.for_user(identify_id, session_id)
118
+ ctx.identify(identify_id)
119
+ return ctx
112
120
 
113
121
  def wallet_address(self, distinct_id: str, wallet_addr: str) -> WalletAddressResult:
114
122
  return self.for_user(distinct_id).wallet_address(wallet_addr)
@@ -193,3 +201,118 @@ class TrodoClient:
193
201
  if self._batch_flusher:
194
202
  self._batch_flusher.stop()
195
203
  self._batch_flusher.flush()
204
+
205
+ # --------------------------------------------------------------------------
206
+ # Agent Analytics
207
+ # --------------------------------------------------------------------------
208
+
209
+ def _build_agent_base(
210
+ self,
211
+ agent_id: str,
212
+ conversation_id: str,
213
+ message_id: str,
214
+ event_type: str,
215
+ distinct_id: Optional[str] = None,
216
+ timestamp: Optional[str] = None,
217
+ ) -> Dict[str, Any]:
218
+ payload: Dict[str, Any] = {
219
+ "event_type": event_type,
220
+ "agent_id": agent_id,
221
+ "conversation_id": conversation_id,
222
+ "message_id": message_id,
223
+ }
224
+ if distinct_id is not None:
225
+ payload["distinct_id"] = distinct_id
226
+ if timestamp is not None:
227
+ payload["timestamp"] = timestamp
228
+ return payload
229
+
230
+ def track_agent_call(self, props: AgentCallProps) -> None:
231
+ """Track an LLM invocation / inbound message."""
232
+ payload = self._build_agent_base(
233
+ props.agent_id, props.conversation_id, props.message_id,
234
+ "agent_call", props.distinct_id, props.timestamp,
235
+ )
236
+ if props.prompt is not None:
237
+ payload["prompt"] = props.prompt
238
+ if props.model is not None:
239
+ payload["model"] = props.model
240
+ if props.temperature is not None:
241
+ payload["temperature"] = props.temperature
242
+ if props.system_prompt_version is not None:
243
+ payload["system_prompt_version"] = props.system_prompt_version
244
+ if props.provider is not None:
245
+ payload["provider"] = props.provider
246
+ if props.properties:
247
+ payload.update(props.properties)
248
+ self._http.post_agent_event(payload)
249
+
250
+ def track_tool_use(self, props: ToolUseProps) -> None:
251
+ """Track a tool invocation within an agent turn."""
252
+ payload = self._build_agent_base(
253
+ props.agent_id, props.conversation_id, props.message_id,
254
+ "tool_use", props.distinct_id, props.timestamp,
255
+ )
256
+ payload["tool_name"] = props.tool_name
257
+ if props.input is not None:
258
+ payload["input"] = props.input
259
+ if props.output is not None:
260
+ payload["output"] = props.output
261
+ if props.latency_ms is not None:
262
+ payload["latency_ms"] = props.latency_ms
263
+ if props.status is not None:
264
+ payload["status"] = props.status
265
+ if props.properties:
266
+ payload.update(props.properties)
267
+ self._http.post_agent_event(payload)
268
+
269
+ def track_agent_response(self, props: AgentResponseProps) -> None:
270
+ """Track an LLM response / completion."""
271
+ payload = self._build_agent_base(
272
+ props.agent_id, props.conversation_id, props.message_id,
273
+ "agent_response", props.distinct_id, props.timestamp,
274
+ )
275
+ if props.output is not None:
276
+ payload["output"] = props.output
277
+ if props.model is not None:
278
+ payload["model"] = props.model
279
+ if props.completion_tokens is not None:
280
+ payload["completion_tokens"] = props.completion_tokens
281
+ if props.prompt_tokens is not None:
282
+ payload["prompt_tokens"] = props.prompt_tokens
283
+ if props.total_tokens is not None:
284
+ payload["total_tokens"] = props.total_tokens
285
+ if props.finish_reason is not None:
286
+ payload["finish_reason"] = props.finish_reason
287
+ if props.properties:
288
+ payload.update(props.properties)
289
+ self._http.post_agent_event(payload)
290
+
291
+ def track_agent_error(self, props: AgentErrorProps) -> None:
292
+ """Track an error during an agent turn."""
293
+ payload = self._build_agent_base(
294
+ props.agent_id, props.conversation_id, props.message_id,
295
+ "agent_error", props.distinct_id, props.timestamp,
296
+ )
297
+ if props.error_type is not None:
298
+ payload["error_type"] = props.error_type
299
+ if props.error_message is not None:
300
+ payload["error_message"] = props.error_message
301
+ if props.failed_tool is not None:
302
+ payload["failed_tool"] = props.failed_tool
303
+ if props.traceback is not None:
304
+ payload["traceback"] = props.traceback
305
+ if props.properties:
306
+ payload.update(props.properties)
307
+ self._http.post_agent_event(payload)
308
+
309
+ def track_feedback(self, props: FeedbackProps) -> None:
310
+ """Track a user feedback reaction on an agent response."""
311
+ payload = self._build_agent_base(
312
+ props.agent_id, props.conversation_id, props.message_id,
313
+ "feedback", props.distinct_id, props.timestamp,
314
+ )
315
+ payload["feedback"] = props.feedback
316
+ if props.properties:
317
+ payload.update(props.properties)
318
+ self._http.post_agent_event(payload)
trodo/types.py CHANGED
@@ -77,3 +77,78 @@ ApiResult = Dict[str, Any]
77
77
  IdentifyResult = Dict[str, Any]
78
78
  WalletAddressResult = Dict[str, Any]
79
79
  ResetResult = Dict[str, Any]
80
+
81
+
82
+ # ----------------------------------------------------------------------------
83
+ # Agent Analytics
84
+ # ----------------------------------------------------------------------------
85
+
86
+ @dataclass
87
+ class AgentCallProps:
88
+ agent_id: str
89
+ conversation_id: str
90
+ message_id: str
91
+ distinct_id: Optional[str] = None
92
+ prompt: Optional[str] = None
93
+ model: Optional[str] = None
94
+ temperature: Optional[float] = None
95
+ system_prompt_version: Optional[str] = None
96
+ provider: Optional[str] = None
97
+ timestamp: Optional[str] = None
98
+ properties: Dict[str, Any] = field(default_factory=dict)
99
+
100
+
101
+ @dataclass
102
+ class ToolUseProps:
103
+ agent_id: str
104
+ conversation_id: str
105
+ message_id: str
106
+ tool_name: str
107
+ distinct_id: Optional[str] = None
108
+ input: Optional[Any] = None
109
+ output: Optional[Any] = None
110
+ latency_ms: Optional[int] = None
111
+ status: Optional[str] = None # 'success' | 'failure'
112
+ timestamp: Optional[str] = None
113
+ properties: Dict[str, Any] = field(default_factory=dict)
114
+
115
+
116
+ @dataclass
117
+ class AgentResponseProps:
118
+ agent_id: str
119
+ conversation_id: str
120
+ message_id: str
121
+ distinct_id: Optional[str] = None
122
+ output: Optional[str] = None
123
+ model: Optional[str] = None
124
+ completion_tokens: Optional[int] = None
125
+ prompt_tokens: Optional[int] = None
126
+ total_tokens: Optional[int] = None
127
+ finish_reason: Optional[str] = None
128
+ timestamp: Optional[str] = None
129
+ properties: Dict[str, Any] = field(default_factory=dict)
130
+
131
+
132
+ @dataclass
133
+ class AgentErrorProps:
134
+ agent_id: str
135
+ conversation_id: str
136
+ message_id: str
137
+ distinct_id: Optional[str] = None
138
+ error_type: Optional[str] = None
139
+ error_message: Optional[str] = None
140
+ failed_tool: Optional[str] = None
141
+ traceback: Optional[str] = None
142
+ timestamp: Optional[str] = None
143
+ properties: Dict[str, Any] = field(default_factory=dict)
144
+
145
+
146
+ @dataclass
147
+ class FeedbackProps:
148
+ agent_id: str
149
+ conversation_id: str
150
+ message_id: str
151
+ feedback: str # 'positive' | 'negative' | 'unreact'
152
+ distinct_id: Optional[str] = None
153
+ timestamp: Optional[str] = None
154
+ properties: Dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,358 @@
1
+ Metadata-Version: 2.4
2
+ Name: trodo-python
3
+ Version: 1.2.0
4
+ Summary: Trodo Analytics SDK for Python — server-side event tracking
5
+ License: ISC
6
+ Keywords: analytics,tracking,trodo,server-side
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.8
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests>=2.28.0
20
+ Provides-Extra: async
21
+ Requires-Dist: httpx>=0.27.0; extra == "async"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-cov; extra == "dev"
25
+ Requires-Dist: responses>=0.25.0; extra == "dev"
26
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
27
+
28
+ # trodo-python
29
+
30
+ Server-side Python SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, manage people/groups, and instrument AI agents — all unified with your frontend data under the same `site_id`.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install trodo-python
36
+ ```
37
+
38
+ Requires Python 3.8+.
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ import trodo
44
+
45
+ trodo.init(site_id='your-site-id')
46
+
47
+ # User-bound context (recommended)
48
+ user = trodo.for_user('user-123')
49
+ user.track('purchase_completed', {'amount': 99.99, 'plan': 'pro'})
50
+ user.people.set({'plan': 'pro', 'company': 'Acme'})
51
+
52
+ # Flush before process exit if using batching
53
+ trodo.shutdown()
54
+ ```
55
+
56
+ ## Core API
57
+
58
+ ### `trodo.init(config)`
59
+
60
+ Call once at app startup.
61
+
62
+ | Parameter | Default | Description |
63
+ |-----------|---------|-------------|
64
+ | `site_id` | required | Your Trodo site ID |
65
+ | `api_base` | `https://sdkapi.trodo.ai` | API base URL |
66
+ | `timeout` | `10` s | HTTP request timeout |
67
+ | `retries` | `2` | Retries on network/5xx errors |
68
+ | `auto_events` | `False` | Hook `sys.excepthook` / `threading.excepthook` as `server_error` events |
69
+ | `batch_enabled` | `False` | Queue events and flush in batches |
70
+ | `batch_size` | `50` | Flush when this many events are queued |
71
+ | `batch_flush_interval` | `5.0` s | Also flush every N seconds |
72
+ | `on_error` | — | Callable on API errors (silent by default) |
73
+ | `debug` | `False` | Log API calls to stderr |
74
+
75
+ ### `trodo.for_user(distinct_id, session_id=None)`
76
+
77
+ Returns a user-bound context. No API call is made until you track an event.
78
+
79
+ ```python
80
+ user = trodo.for_user('user-123', session_id=request.cookies.get('trodo_session'))
81
+ ```
82
+
83
+ ### `trodo.identify(identify_id, session_id=None)`
84
+
85
+ Creates the session and fires `POST /api/sdk/identify`. Use to link a `distinct_id` to an external identifier (email, DB id). Returns the user context.
86
+
87
+ ```python
88
+ user = trodo.identify('user@example.com', session_id=request.cookies.get('trodo_session'))
89
+ # distinct_id is now id_user@example.com — merges with browser events
90
+ user.track('login')
91
+ ```
92
+
93
+ ### User context methods
94
+
95
+ ```python
96
+ user.track(event_name, properties=None) # Custom event
97
+ user.identify(identify_id) # Merge identity
98
+ user.wallet_address(address) # Set wallet address
99
+ user.reset() # Clear session
100
+ user.capture_error(exc, severity='error') # Track server_error ('critical' | 'error' | 'warning')
101
+
102
+ # People profile
103
+ user.people.set(properties)
104
+ user.people.set_once(properties)
105
+ user.people.unset(keys)
106
+ user.people.increment(key, amount=1)
107
+ user.people.append(key, values)
108
+ user.people.union(key, values)
109
+ user.people.remove(key, values)
110
+ user.people.track_charge(amount, properties=None)
111
+ user.people.clear_charges()
112
+ user.people.delete_user()
113
+
114
+ # Groups
115
+ user.set_group(group_key, group_id)
116
+ user.add_group(group_key, group_id)
117
+ user.remove_group(group_key, group_id)
118
+ group = user.get_group(group_key, group_id)
119
+ group.set(properties)
120
+ group.set_once(properties)
121
+ group.increment(key, amount=1)
122
+ group.append(key, values)
123
+ group.union(key, values)
124
+ group.remove(key, values)
125
+ group.unset(keys)
126
+ group.delete()
127
+ ```
128
+
129
+ ### Direct call pattern
130
+
131
+ ```python
132
+ trodo.track('user-123', 'event_name', {'key': 'value'})
133
+ trodo.people_set('user-123', {'plan': 'pro'})
134
+ trodo.set_group('user-123', 'company', 'acme')
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Agent Analytics
140
+
141
+ Track every step of your LLM agents. Each call counts as one event toward your plan limit.
142
+
143
+ **Before you start:** register your agent in **Integrations → AI Agents** in the dashboard to get an `agent_id` (`agt_xxxxxxxx`).
144
+
145
+ ```python
146
+ from trodo import (
147
+ AgentCallProps, ToolUseProps, AgentResponseProps,
148
+ AgentErrorProps, FeedbackProps,
149
+ )
150
+ ```
151
+
152
+ ### `track_agent_call` — inbound message / LLM invocation
153
+
154
+ ```python
155
+ trodo.track_agent_call(AgentCallProps(
156
+ agent_id='agt_abc12345',
157
+ conversation_id='conv_xyz',
158
+ message_id='msg_001',
159
+ prompt=user_message,
160
+ model='claude-3-5-sonnet',
161
+ provider='anthropic',
162
+ system_prompt_version='v2', # optional — track prompt iterations
163
+ distinct_id=user_id, # optional — link to a Trodo user
164
+ ))
165
+ ```
166
+
167
+ ### `track_tool_use` — tool/function call within a turn
168
+
169
+ ```python
170
+ trodo.track_tool_use(ToolUseProps(
171
+ agent_id='agt_abc12345',
172
+ conversation_id='conv_xyz',
173
+ message_id='msg_001',
174
+ tool_name='fetch_billing_info',
175
+ latency_ms=143,
176
+ status='success', # 'success' | 'failure'
177
+ input={'user_id': '123'}, # optional
178
+ output={'plan': 'pro'}, # optional
179
+ ))
180
+ ```
181
+
182
+ ### `track_agent_response` — LLM output and token usage
183
+
184
+ ```python
185
+ trodo.track_agent_response(AgentResponseProps(
186
+ agent_id='agt_abc12345',
187
+ conversation_id='conv_xyz',
188
+ message_id='msg_001',
189
+ model='claude-3-5-sonnet',
190
+ completion_tokens=response.usage.output_tokens,
191
+ prompt_tokens=response.usage.input_tokens,
192
+ total_tokens=response.usage.input_tokens + response.usage.output_tokens,
193
+ finish_reason=response.stop_reason,
194
+ distinct_id=user_id,
195
+ ))
196
+ ```
197
+
198
+ ### `track_agent_error` — errors and failures
199
+
200
+ ```python
201
+ import traceback
202
+
203
+ trodo.track_agent_error(AgentErrorProps(
204
+ agent_id='agt_abc12345',
205
+ conversation_id='conv_xyz',
206
+ message_id='msg_001',
207
+ error_type='rate_limit', # 'timeout' | 'rate_limit' | 'guardrail_block' | ...
208
+ error_message=str(exc),
209
+ failed_tool='fetch_billing_info', # optional
210
+ traceback=traceback.format_exc(), # optional
211
+ ))
212
+ ```
213
+
214
+ ### `track_feedback` — user thumbs up/down
215
+
216
+ ```python
217
+ trodo.track_feedback(FeedbackProps(
218
+ agent_id='agt_abc12345',
219
+ conversation_id='conv_xyz',
220
+ message_id='msg_001', # same message_id as the response it refers to
221
+ feedback='positive', # 'positive' | 'negative' | 'unreact'
222
+ distinct_id=user_id,
223
+ ))
224
+ ```
225
+
226
+ ### Full turn example
227
+
228
+ ```python
229
+ import traceback
230
+ from trodo import AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps
231
+
232
+ def run_agent_turn(user_id, conversation_id, user_message):
233
+ agent_id = 'agt_abc12345'
234
+ message_id = f'msg_{int(time.time() * 1000)}'
235
+
236
+ trodo.track_agent_call(AgentCallProps(
237
+ agent_id=agent_id, conversation_id=conversation_id,
238
+ message_id=message_id, prompt=user_message, distinct_id=user_id,
239
+ ))
240
+
241
+ try:
242
+ trodo.track_tool_use(ToolUseProps(
243
+ agent_id=agent_id, conversation_id=conversation_id,
244
+ message_id=message_id, tool_name='search', status='success', latency_ms=80,
245
+ ))
246
+
247
+ response = llm_client.complete(user_message)
248
+
249
+ trodo.track_agent_response(AgentResponseProps(
250
+ agent_id=agent_id, conversation_id=conversation_id, message_id=message_id,
251
+ model=response.model,
252
+ completion_tokens=response.usage.output_tokens,
253
+ prompt_tokens=response.usage.input_tokens,
254
+ total_tokens=response.usage.input_tokens + response.usage.output_tokens,
255
+ distinct_id=user_id,
256
+ ))
257
+
258
+ return response.text
259
+
260
+ except Exception as exc:
261
+ trodo.track_agent_error(AgentErrorProps(
262
+ agent_id=agent_id, conversation_id=conversation_id, message_id=message_id,
263
+ error_type=type(exc).__name__, error_message=str(exc),
264
+ traceback=traceback.format_exc(), distinct_id=user_id,
265
+ ))
266
+ raise
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Identity Merging (Cross-SDK)
272
+
273
+ Call `identify()` with the **same value** on the browser and server to merge all events under one user profile:
274
+
275
+ ```python
276
+ # Python
277
+ user.identify('user@example.com') # → id_user@example.com
278
+
279
+ # Browser (same value)
280
+ # Trodo.identify('user@example.com') → id_user@example.com
281
+ # Events from both sides now appear together in the dashboard
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Flask / FastAPI Example
287
+
288
+ ```python
289
+ # Flask
290
+ from flask import Flask, request
291
+ import trodo
292
+
293
+ app = Flask(__name__)
294
+ trodo.init(site_id='your-site-id')
295
+
296
+ @app.route('/purchase', methods=['POST'])
297
+ def purchase():
298
+ user = trodo.for_user(request.json['user_id'])
299
+ user.track('purchase_completed', {'amount': request.json['amount']})
300
+ return {'ok': True}
301
+ ```
302
+
303
+ ```python
304
+ # FastAPI
305
+ from fastapi import FastAPI, Request
306
+ import trodo
307
+
308
+ app = FastAPI()
309
+ trodo.init(site_id='your-site-id')
310
+
311
+ @app.post('/purchase')
312
+ async def purchase(request: Request):
313
+ body = await request.json()
314
+ user = trodo.for_user(body['user_id'])
315
+ user.track('purchase_completed', {'amount': body['amount']})
316
+ return {'ok': True}
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Batching
322
+
323
+ ```python
324
+ trodo.init(
325
+ site_id='your-site-id',
326
+ batch_enabled=True,
327
+ batch_size=50,
328
+ batch_flush_interval=5.0,
329
+ )
330
+
331
+ # Always flush before process exit
332
+ import atexit
333
+ atexit.register(trodo.shutdown)
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Auto Events
339
+
340
+ ```python
341
+ trodo.init(site_id='your-site-id', auto_events=True)
342
+ # Hooks sys.excepthook and threading.excepthook
343
+ # Sends server_error events with distinct_id: 'server_global'
344
+
345
+ # Toggle at runtime
346
+ trodo.enable_auto_events()
347
+ trodo.disable_auto_events()
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Thread Safety
353
+
354
+ The SDK is thread-safe. `SessionManager`, `EventQueue`, and `BatchFlusher` all use `threading.Lock` internally. Safe for multi-threaded Flask/Django/FastAPI apps.
355
+
356
+ ## License
357
+
358
+ ISC
@@ -1,11 +1,11 @@
1
- trodo/__init__.py,sha256=RkATreCecKMFz7ZgJATq52CWVxkWn5ANWzNIGOEweaQ,3499
2
- trodo/client.py,sha256=8EodhgXyBeVhouFytmnuSVUJlp7tTvyC_szSJ8h0yns,7056
3
- trodo/types.py,sha256=6XjAU9-ue7uRUMcW4aFq93t80d41jdTmCLEGks4R5n4,2438
1
+ trodo/__init__.py,sha256=xbXCxPZtQfCgJeB1Yg_Rv067DOXCDztLXpo-f8XkL68,4837
2
+ trodo/client.py,sha256=FkcUYymCMVJA9U509I5Lba9EZAYkjHb5-Fpoi3fI3oc,12122
3
+ trodo/types.py,sha256=aX8aQrENDwSpouxqshHxDbjjzkvAveeClSSL0TDEygQ,4610
4
4
  trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
5
5
  trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  trodo/api/async_client.py,sha256=rZN4aJ2QiKyrHBK260bApCUB9JaMWU6BQtzoSJZh7xk,3408
7
- trodo/api/endpoints.py,sha256=ZdEQh-aH62DVO5zSn8-HOKAjnxqAA4z_YXktfFgmEXw,798
8
- trodo/api/http_client.py,sha256=oKh3YXVQJG2TlFf6euRG92F91JU7DCnD7eeduQsIYOo,3119
7
+ trodo/api/endpoints.py,sha256=JaJ5hjjKPi4aO6R25oasQTasEptg1FFFkDUIqO7uOXs,835
8
+ trodo/api/http_client.py,sha256=h1p37Z-kmVxwCnodb5j8MXiP-cRwOKiF2lznoIW1bFI,3252
9
9
  trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
11
11
  trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,7 +17,7 @@ trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,87
17
17
  trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  trodo/session/server_session.py,sha256=uBAq1QSYPUUaHFSeoOyM5Yr65dLb8T82OOx3D1BrdrE,1970
19
19
  trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
20
- trodo_python-1.0.1.dist-info/METADATA,sha256=ja5ICQgHmloQMv8O8hA8cbBJT5uSSwc_CXPSgCaeEPo,6620
21
- trodo_python-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
- trodo_python-1.0.1.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
23
- trodo_python-1.0.1.dist-info/RECORD,,
20
+ trodo_python-1.2.0.dist-info/METADATA,sha256=GNdzVrFziphK3z0HtY02YyMNNBgGrFuYpsf8EgML54Y,10179
21
+ trodo_python-1.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ trodo_python-1.2.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
23
+ trodo_python-1.2.0.dist-info/RECORD,,
@@ -1,227 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: trodo-python
3
- Version: 1.0.1
4
- Summary: Trodo Analytics SDK for Python — server-side event tracking
5
- License: ISC
6
- Keywords: analytics,tracking,trodo,server-side
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Programming Language :: Python :: 3.8
9
- Classifier: Programming Language :: Python :: 3.9
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Classifier: License :: OSI Approved :: ISC License (ISCL)
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Intended Audience :: Developers
16
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
- Requires-Python: >=3.8
18
- Description-Content-Type: text/markdown
19
- Requires-Dist: requests>=2.28.0
20
- Provides-Extra: async
21
- Requires-Dist: httpx>=0.27.0; extra == "async"
22
- Provides-Extra: dev
23
- Requires-Dist: pytest>=7.0; extra == "dev"
24
- Requires-Dist: pytest-cov; extra == "dev"
25
- Requires-Dist: responses>=0.25.0; extra == "dev"
26
- Requires-Dist: httpx>=0.27.0; extra == "dev"
27
-
28
- # trodo-python
29
-
30
- Server-side Python SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, and manage people/groups — all merging seamlessly with your frontend Trodo data under the same `site_id`.
31
-
32
- ## Installation
33
-
34
- ```bash
35
- pip install trodo-python
36
- ```
37
-
38
- For async support (optional):
39
-
40
- ```bash
41
- pip install trodo-python[async]
42
- ```
43
-
44
- Requires Python 3.8+.
45
-
46
- ## Quick Start
47
-
48
- ```python
49
- import trodo
50
-
51
- # Initialize once at app startup
52
- trodo.init(
53
- site_id='your-site-id',
54
- debug=False, # optional: log API calls
55
- auto_events=True, # optional: hook sys.excepthook
56
- )
57
-
58
- # Get a user context
59
- user = trodo.for_user('user-123')
60
-
61
- # Track a custom event
62
- user.track('purchase_completed', {'amount': 99.99, 'plan': 'pro'})
63
-
64
- # Identify the user (merges with frontend events under the same identity)
65
- user.identify('user@example.com') # distinct_id becomes id_user@example.com
66
-
67
- # Update people profile
68
- user.people.set({'plan': 'pro', 'company': 'Acme'})
69
-
70
- # Track a server-side error
71
- user.capture_error(Exception('payment failed'))
72
-
73
- # Flush queued events before process exit
74
- trodo.shutdown()
75
- ```
76
-
77
- ## Flask / FastAPI Example
78
-
79
- ```python
80
- # Flask
81
- from flask import Flask, request
82
- import trodo
83
-
84
- app = Flask(__name__)
85
- trodo.init(site_id='your-site-id')
86
-
87
- @app.route('/purchase', methods=['POST'])
88
- def purchase():
89
- user_id = request.json['user_id']
90
- user = trodo.for_user(user_id)
91
- user.track('purchase_completed', {'amount': request.json['amount']})
92
- return {'ok': True}
93
- ```
94
-
95
- ## Cross-SDK Identity Merging
96
-
97
- Frontend and backend events merge when both sides call `identify()` with the same value:
98
-
99
- ```python
100
- # Python SDK
101
- user.identify('user@example.com') # → id_user@example.com
102
-
103
- # Browser SDK (same value)
104
- # Trodo.identify('user@example.com') → id_user@example.com
105
-
106
- # Both event streams now appear together in the Trodo dashboard
107
- ```
108
-
109
- ## API Reference
110
-
111
- ### `trodo.init(config)`
112
-
113
- | Parameter | Type | Default | Description |
114
- |-----------|------|---------|-------------|
115
- | `site_id` | `str` | required | Your Trodo site ID |
116
- | `api_base` | `str` | `https://sdkapi.trodo.ai` | API base URL |
117
- | `debug` | `bool` | `False` | Log API requests/responses |
118
- | `auto_events` | `bool` | `False` | Hook `sys.excepthook` + `threading.excepthook` |
119
- | `retries` | `int` | `3` | HTTP retry attempts on 5xx errors |
120
- | `timeout` | `int` | `10` | HTTP timeout in seconds |
121
- | `batch_enabled` | `bool` | `False` | Queue events and flush in bulk |
122
- | `batch_size` | `int` | `50` | Max events per batch flush |
123
- | `batch_flush_interval` | `float` | `5.0` | Flush interval in seconds |
124
- | `on_error` | `callable` | `None` | Callback for SDK errors |
125
-
126
- ### `trodo.for_user(distinct_id, session_id=None)`
127
-
128
- Returns a user-bound context. All subsequent calls use this user's session.
129
-
130
- ```python
131
- user = trodo.for_user(
132
- 'user-123',
133
- session_id=request.cookies.get('trodo_session'), # optional: correlate with browser session
134
- )
135
- ```
136
-
137
- ### User Context Methods
138
-
139
- ```python
140
- user.track(event_name, properties=None) # Track custom event
141
- user.track_event(event_name, properties=None) # Alias for track()
142
- user.identify(identify_id) # Merge identity
143
- user.wallet_address(address) # Set crypto wallet address
144
- user.reset() # Clear session context
145
- user.capture_error(exception) # Track server_error event
146
-
147
- # People profile
148
- user.people.set(properties)
149
- user.people.set_once(properties)
150
- user.people.unset(keys)
151
- user.people.increment(properties)
152
- user.people.append(properties)
153
- user.people.union(properties)
154
- user.people.remove(properties)
155
- user.people.track_charge(amount, properties=None)
156
- user.people.clear_charges()
157
- user.people.delete_user()
158
-
159
- # Groups
160
- user.set_group(group_key, group_id)
161
- user.add_group(group_key, group_id)
162
- user.remove_group(group_key, group_id)
163
- group = user.get_group(group_key, group_id)
164
- group.set(properties)
165
- group.set_once(properties)
166
- group.union(properties)
167
- group.remove(properties)
168
- group.unset(keys)
169
- group.increment(properties)
170
- group.append(properties)
171
- group.delete()
172
- ```
173
-
174
- ### Direct Call Pattern (for pipelines/scripts)
175
-
176
- ```python
177
- trodo.track('user-123', 'event_name', {'key': 'value'})
178
- trodo.identify('user-123', 'identify_id')
179
- trodo.people_set('user-123', {'plan': 'pro'})
180
- trodo.set_group('user-123', 'company', 'acme')
181
- ```
182
-
183
- ### Global Methods
184
-
185
- ```python
186
- trodo.enable_auto_events() # Enable sys.excepthook hooks
187
- trodo.disable_auto_events() # Disable hooks
188
- trodo.flush() # Flush pending batch queue
189
- trodo.shutdown() # Flush + stop background timers
190
- ```
191
-
192
- ## Auto Events
193
-
194
- When `auto_events=True`, the SDK wraps Python's exception hooks and sends `server_error` events to Trodo:
195
-
196
- - `sys.excepthook` — unhandled exceptions in the main thread
197
- - `threading.excepthook` — unhandled exceptions in threads
198
-
199
- These events use `distinct_id: 'server_global'` in the dashboard.
200
-
201
- Per-user error capture: `user.capture_error(e)` uses the user's own `distinct_id`.
202
-
203
- ## Batching
204
-
205
- ```python
206
- trodo.init(
207
- site_id='your-site-id',
208
- batch_enabled=True,
209
- batch_size=100,
210
- batch_flush_interval=3.0,
211
- )
212
-
213
- # Events are queued and flushed every 3s or when 100 events accumulate
214
- user.track('page_view')
215
-
216
- # Always flush before process exit
217
- import atexit
218
- atexit.register(trodo.shutdown)
219
- ```
220
-
221
- ## Thread Safety
222
-
223
- The SDK is thread-safe. `SessionManager`, `EventQueue`, and `BatchFlusher` all use `threading.Lock` internally. Safe to use in multi-threaded Flask/Django/FastAPI applications.
224
-
225
- ## License
226
-
227
- ISC