@@ -42,6 +42,84 @@ client.group('org_456', user_id='user_123')
4242client.shutdown()
4343```
4444
45+ ## Installation Prompt
46+
47+ Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:
48+
49+ ```
50+ Integrate Klime for customer analytics. Klime tracks user activity to identify which customers are healthy vs at risk of churning.
51+
52+ ANALYTICS MODES (determine which applies):
53+ - Companies & Teams: Your customers are companies with multiple team members (SaaS, enterprise tools)
54+ → Use identify() + group() + track()
55+ - Individual Customers: Your customers are individuals with private accounts (consumer apps, creator tools)
56+ → Use identify() + track() only (no group() needed)
57+
58+ KEY CONCEPTS:
59+ - Every track() call requires either user_id OR group_id (no anonymous events)
60+ - Use group_id alone for org-level events (webhooks, cron jobs, system metrics)
61+ - group() links a user to a company AND sets company traits (only for Companies & Teams mode)
62+ - Order doesn't matter - events before identify/group still get attributed correctly
63+
64+ BEST PRACTICES:
65+ - Initialize client ONCE at app startup (singleton pattern)
66+ - Store write key in KLIME_WRITE_KEY environment variable
67+ - Call shutdown() on process exit to flush remaining events (auto-registered via atexit)
68+
69+ Install: pip install klime
70+
71+ import os
72+ from klime import KlimeClient
73+
74+ client = KlimeClient(write_key=os.environ['KLIME_WRITE_KEY'])
75+
76+ # Identify users at signup/login:
77+ client.identify('usr_abc123', {'email': 'jane@acme.com', 'name': 'Jane Smith'})
78+
79+ # Track key activities:
80+ client.track('Report Generated', {'report_type': 'revenue'}, user_id='usr_abc123')
81+ client.track('Feature Used', {'feature': 'export', 'format': 'csv'}, user_id='usr_abc123')
82+ client.track('Teammate Invited', {'role': 'member'}, user_id='usr_abc123')
83+
84+ # If Companies & Teams mode: link user to their company and set company traits
85+ client.group('org_456', {'name': 'Acme Inc', 'plan': 'enterprise'}, user_id='usr_abc123')
86+
87+ INTEGRATION WORKFLOW:
88+
89+ Phase 1: Discover
90+ Explore the codebase to understand:
91+ 1. What framework is used? (Django, Flask, FastAPI, Starlette, etc.)
92+ 2. Where is user identity available? (e.g., request.user.id, current_user.id, g.user, Depends() injection)
93+ 3. Is this Companies & Teams or Individual Customers?
94+ - Look for: organization, workspace, tenant, team, account models → Companies & Teams (use group())
95+ - No company/org concept, just individual users → Individual Customers (skip group())
96+ 4. Where do core user actions happen? (views, routes, API endpoints, services)
97+ 5. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, track)
98+ Match your integration style to the framework's conventions.
99+
100+ Phase 2: Instrument
101+ Add these calls using idiomatic patterns for the framework:
102+ - Initialize client once (Django: apps.py/settings, Flask: app factory, FastAPI: lifespan/startup)
103+ - identify() in auth/login success handler
104+ - group() when user-org association is established (Companies & Teams mode only)
105+ - track() for key user actions (see below)
106+
107+ WHAT TO TRACK:
108+ Active engagement (primary): feature usage, resource creation, collaboration, completing flows
109+ Session signals (secondary): login/session start, dashboard access - distinguishes "low usage" from "churned"
110+ Do NOT track: every request, health checks, middleware passthrough, background tasks
111+
112+ Phase 3: Verify
113+ Confirm: client initialized, shutdown handled, identify/group/track calls added
114+
115+ Phase 4: Summarize
116+ Report what you added:
117+ - Files modified and what was added to each
118+ - Events being tracked (list event names and what triggers them)
119+ - How user_id is obtained (and group_id if Companies & Teams mode)
120+ - Any assumptions made or questions
121+ ```
122+
45123## API Reference
46124
47125### Constructor
@@ -55,43 +133,49 @@ KlimeClient(
55133 max_queue_size: Optional[int ] = None , # Optional: Max queued events (default: 1000)
56134 retry_max_attempts: Optional[int ] = None , # Optional: Max retry attempts (default: 5)
57135 retry_initial_delay: Optional[int ] = None , # Optional: Initial retry delay in ms (default: 1000)
58- flush_on_shutdown: Optional[bool ] = None # Optional: Auto-flush on exit (default: True)
136+ flush_on_shutdown: Optional[bool ] = None , # Optional: Auto-flush on exit (default: True)
137+ logger: Optional[logging.Logger] = None , # Optional: Custom logger (default: logging.getLogger("klime"))
138+ on_error: Optional[Callable] = None , # Optional: Callback for batch failures
139+ on_success: Optional[Callable] = None # Optional: Callback for successful sends
59140)
60141```
61142
62143### Methods
63144
64- #### ` track(event: str, properties: Optional[Dict] = None, user_id: Optional[str] = None, group_id: Optional[str] = None, ip: Optional[str] = None ) -> None `
145+ #### ` track(event: str, properties: Optional[Dict] = None, user_id: Optional[str] = None, group_id: Optional[str] = None) -> None `
65146
66- Track a user event. A ` user_id ` is required for events to be useful in Klime.
147+ Track an event. Events can be attributed in two ways:
148+ - ** User events** : Provide ` user_id ` to track user activity (most common)
149+ - ** Group events** : Provide ` group_id ` without ` user_id ` for organization-level events
67150
68151``` python
152+ # User event (most common)
69153client.track(' Button Clicked' , {
70154 ' button_name' : ' Sign up' ,
71155 ' plan' : ' pro'
72156}, user_id = ' user_123' )
73157
74- # With IP address (for geolocation )
75- client.track(' Button Clicked ' , {
76- ' button_name ' : ' Sign up ' ,
77- ' plan ' : ' pro '
78- }, user_id = ' user_123 ' , ip = ' 192.168.1.1 ' )
158+ # Group event (for webhooks, cron jobs, system events )
159+ client.track(' Events Received ' , {
160+ ' count ' : 100 ,
161+ ' source ' : ' webhook '
162+ }, group_id = ' org_456 ' )
79163```
80164
81- > ** Advanced ** : The ` group_id ` parameter is available for multi-tenant scenarios where a user belongs to multiple organizations and you need to specify which organization context the event occurred in.
165+ > ** Note ** : The ` group_id ` parameter can also be combined with ` user_id ` for multi-tenant scenarios where you need to specify which organization context a user event occurred in.
82166
83- #### ` identify(user_id: str, traits: Optional[Dict] = None, ip: Optional[str] = None ) -> None `
167+ #### ` identify(user_id: str, traits: Optional[Dict] = None) -> None `
84168
85169Identify a user with traits.
86170
87171``` python
88172client.identify(' user_123' , {
89173 ' email' : ' user@example.com' ,
90174 ' name' : ' Stefan'
91- }, ip = ' 192.168.1.1 ' )
175+ })
92176```
93177
94- #### ` group(group_id: str, traits: Optional[Dict] = None, user_id: Optional[str] = None, ip: Optional[str] = None ) -> None `
178+ #### ` group(group_id: str, traits: Optional[Dict] = None, user_id: Optional[str] = None) -> None `
95179
96180Associate a user with a group and/or set group traits.
97181
@@ -128,6 +212,41 @@ Gracefully shutdown the client, flushing remaining events.
128212client.shutdown()
129213```
130214
215+ #### ` queue_size() -> int `
216+
217+ Return the number of events currently in the queue.
218+
219+ ``` python
220+ pending = client.queue_size()
221+ print (f " { pending} events waiting to be sent " )
222+ ```
223+
224+ ### Synchronous Methods
225+
226+ For cases where you need confirmation that events were sent (e.g., before process exit, in tests), use the synchronous variants:
227+
228+ #### ` track_sync(event, properties, user_id, group_id) -> BatchResponse `
229+
230+ Track an event synchronously. Raises ` SendError ` on failure.
231+
232+ ``` python
233+ from klime import SendError
234+
235+ try :
236+ response = client.track_sync(' Critical Action' , {' key' : ' value' }, user_id = ' user_123' )
237+ print (f " Sent! Accepted: { response.accepted} " )
238+ except SendError as e:
239+ print (f " Failed to send: { e.message} " )
240+ ```
241+
242+ #### ` identify_sync(user_id, traits) -> BatchResponse `
243+
244+ Identify a user synchronously. Raises ` SendError ` on failure.
245+
246+ #### ` group_sync(group_id, traits, user_id) -> BatchResponse `
247+
248+ Associate a user with a group synchronously. Raises ` SendError ` on failure.
249+
131250## Features
132251
133252- ** Automatic Batching** : Events are automatically batched and sent every 2 seconds or when the batch size reaches 20 events
@@ -136,6 +255,29 @@ client.shutdown()
136255- ** Process Exit Handling** : Automatically flushes events on process exit (via ` atexit ` )
137256- ** Zero Dependencies** : Uses only Python standard library
138257
258+ ## Performance
259+
260+ When you call ` track() ` , ` identify() ` , or ` group() ` , the SDK:
261+
262+ 1 . Adds the event to a thread-safe queue (microseconds)
263+ 2 . Returns immediately without waiting for network I/O
264+
265+ Events are sent to Klime's servers in background threads. This means:
266+
267+ - ** No network blocking** : HTTP requests happen asynchronously in background threads
268+ - ** No latency impact** : Tracking calls add < 1ms to your request handling time
269+ - ** Automatic batching** : Events are queued and sent in batches (default: every 2 seconds or 20 events)
270+
271+ ``` python
272+ # This returns immediately - no HTTP request is made here
273+ client.track(' Button Clicked' , {' button' : ' signup' }, user_id = ' user_123' )
274+
275+ # Your code continues without waiting
276+ return {' success' : True }
277+ ```
278+
279+ The only blocking operation is ` flush() ` , which waits for all queued events to be sent. This is typically only called during graceful shutdown.
280+
139281## Configuration
140282
141283### Default Values
@@ -147,6 +289,43 @@ client.shutdown()
147289- ` retry_initial_delay ` : 1000ms
148290- ` flush_on_shutdown ` : True
149291
292+ ### Logging
293+
294+ The SDK uses Python's built-in ` logging ` module. By default, it logs to ` logging.getLogger("klime") ` .
295+
296+ ``` python
297+ import logging
298+
299+ # Enable debug logging
300+ logging.getLogger(" klime" ).setLevel(logging.DEBUG )
301+
302+ # Or provide a custom logger
303+ client = KlimeClient(
304+ write_key = ' your-write-key' ,
305+ logger = logging.getLogger(" myapp.klime" )
306+ )
307+ ```
308+
309+ For Django/Flask, the SDK automatically integrates with your app's logging configuration.
310+
311+ ### Callbacks
312+
313+ ``` python
314+ def handle_error (error , events ):
315+ # Report to your error tracking service
316+ sentry_sdk.capture_exception(error)
317+ print (f " Failed to send { len (events)} events: { error} " )
318+
319+ def handle_success (response ):
320+ print (f " Sent { response.accepted} events " )
321+
322+ client = KlimeClient(
323+ write_key = ' your-write-key' ,
324+ on_error = handle_error,
325+ on_success = handle_success
326+ )
327+ ```
328+
150329## Error Handling
151330
152331The SDK automatically handles:
@@ -155,6 +334,17 @@ The SDK automatically handles:
155334- ** Permanent errors** (400, 401): Logs error and drops event
156335- ** Rate limiting** : Respects ` Retry-After ` header
157336
337+ For synchronous operations, use ` *_sync() ` methods which raise ` SendError ` on failure:
338+
339+ ``` python
340+ from klime import KlimeClient, SendError
341+
342+ try :
343+ response = client.track_sync(' Event' , user_id = ' user_123' )
344+ except SendError as e:
345+ print (f " Failed: { e.message} , events: { len (e.events)} " )
346+ ```
347+
158348## Size Limits
159349
160350- Maximum event size: 200KB
@@ -176,7 +366,7 @@ client = KlimeClient(write_key='your-write-key')
176366def button_clicked ():
177367 client.track(' Button Clicked' , {
178368 ' button_name' : request.json.get(' buttonName' )
179- }, user_id = request.json.get(' userId' ), ip = request.remote_addr )
369+ }, user_id = request.json.get(' userId' ))
180370
181371 return {' success' : True }
182372
@@ -198,7 +388,7 @@ class ButtonClickView(View):
198388 def post (self , request ):
199389 client.track(' Button Clicked' , {
200390 ' button_name' : request.POST .get(' buttonName' )
201- }, user_id = str (request.user.id), ip = request. META .get( ' REMOTE_ADDR ' ) )
391+ }, user_id = str (request.user.id))
202392
203393 return JsonResponse({' success' : True })
204394```
0 commit comments