Skip to content

Commit e32c57e

Browse files
committed
Release v1.1.0
1 parent 46a5c28 commit e32c57e

File tree

9 files changed

+564
-91
lines changed

9 files changed

+564
-91
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
python-version: ["3.9", "3.10", "3.11", "3.12"]
15+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1616

1717
steps:
1818
- uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.0.3] - 2025-12-10
8+
## [1.1.0] - 2026-01-27
99

10-
- Update SDK endpoints for improved consistency
10+
- Add synchronous methods `track_sync()`, `identify_sync()`, and `group_sync()` that return `BatchResponse` and raise `SendError` on failure
11+
- Add `queue_size()` method to check pending events in queue
12+
- Add `on_error` and `on_success` callback parameters to constructor for handling batch results
13+
- Add `logger` parameter to constructor for custom logging configuration
14+
- Support group-only events by allowing `track()` with `group_id` alone (without `user_id`) for organization-level tracking
15+
- Export `BatchResponse` and `SendError` types from package
1116

12-
- Audit and modernize SDK for better reliability
17+
- Expand Python version support to 3.10, 3.11, 3.12, 3.13, and 3.14 (dropped 3.9)
18+
19+
- **BREAKING:** Remove `ip` parameter from `track()`, `identify()`, and `group()` methods
1320

README.md

Lines changed: 204 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,84 @@ client.group('org_456', user_id='user_123')
4242
client.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)
69153
client.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

85169
Identify a user with traits.
86170

87171
```python
88172
client.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

96180
Associate a user with a group and/or set group traits.
97181

@@ -128,6 +212,41 @@ Gracefully shutdown the client, flushing remaining events.
128212
client.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

152331
The 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')
176366
def 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
```

klime/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .client import KlimeClient
2+
from .types import BatchResponse, SendError
23

3-
__version__ = "1.0.2"
4-
__all__ = ["KlimeClient"]
4+
__version__ = "1.1.0"
5+
__all__ = ["KlimeClient", "BatchResponse", "SendError"]
56

0 commit comments

Comments
 (0)