Skip to content

Commit 054a86b

Browse files
committed
Release v1.1.0
1 parent 2797933 commit 054a86b

9 files changed

Lines changed: 489 additions & 83 deletions

File tree

.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-
php-version: ["8.1", "8.2", "8.3"]
15+
php-version: ["8.2", "8.3", "8.4", "8.5"]
1616

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

CHANGELOG.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +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.1] - 2025-12-10
8+
## [1.1.0] - 2026-01-27
99

10-
- Implement PHP SDK with core functionality for Klime API integration
10+
- Add synchronous methods `trackSync()`, `identifySync()`, and `groupSync()` that return `BatchResponse` or throw `SendException`
11+
- Add `getQueueSize()` method to check pending events in queue
12+
- Add `logger` callback option for custom logging integration
13+
- Add `onError` callback option for error handling (receives exception and failed events)
14+
- Add `onSuccess` callback option (receives `BatchResponse`)
15+
- Support group-only events by passing `groupId` without `userId` for organization-level tracking (webhooks, cron jobs, system events)
16+
17+
- **BREAKING:** Minimum PHP version increased from 8.1 to 8.2
18+
19+
- **BREAKING:** Remove IP tracking - the `ip` option is no longer supported in `track()`, `identify()`, or `group()` methods
1120

README.md

Lines changed: 211 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,81 @@ $client->group('org_456', [
4545
$client->shutdown();
4646
```
4747

48+
## Installation Prompt
49+
50+
Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:
51+
52+
```
53+
Integrate Klime for customer analytics. Klime tracks user activity to identify which customers are healthy vs at risk of churning.
54+
55+
ANALYTICS MODES (determine which applies):
56+
- Companies & Teams: Your customers are companies with multiple team members (SaaS, enterprise tools)
57+
→ Use identify() + group() + track()
58+
- Individual Customers: Your customers are individuals with private accounts (consumer apps, creator tools)
59+
→ Use identify() + track() only (no group() needed)
60+
61+
KEY CONCEPTS:
62+
- Every track() call requires either userId OR groupId (no anonymous events)
63+
- Use groupId alone for org-level events (webhooks, cron jobs, system metrics)
64+
- group() links a user to a company AND sets company traits (only for Companies & Teams mode)
65+
- Order doesn't matter - events before identify/group still get attributed correctly
66+
67+
BEST PRACTICES:
68+
- Initialize client ONCE (singleton or service container)
69+
- Store write key in KLIME_WRITE_KEY environment variable
70+
- shutdown() auto-called on script end, but call explicitly for long-running processes
71+
72+
Install: composer require klime/klime
73+
74+
$client = new \Klime\Client(['writeKey' => getenv('KLIME_WRITE_KEY')]);
75+
76+
// Identify users at signup/login:
77+
$client->identify('usr_abc123', ['email' => '[email protected]', 'name' => 'Jane Smith']);
78+
79+
// Track key activities:
80+
$client->track('Report Generated', ['report_type' => 'revenue'], ['userId' => 'usr_abc123']);
81+
$client->track('Feature Used', ['feature' => 'export', 'format' => 'csv'], ['userId' => 'usr_abc123']);
82+
$client->track('Teammate Invited', ['role' => 'member'], ['userId' => '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'], ['userId' => 'usr_abc123']);
86+
87+
INTEGRATION WORKFLOW:
88+
89+
Phase 1: Discover
90+
Explore the codebase to understand:
91+
1. What framework is used? (Laravel, Symfony, plain PHP, etc.)
92+
2. Where is user identity available? (e.g., Auth::id(), $request->user()->id, $this->getUser()->getId(), $_SESSION)
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? (controllers, services, event listeners, commands)
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 (Laravel: service provider, Symfony: service definition, plain PHP: bootstrap)
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, queued jobs
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 userId is obtained (and groupId if Companies & Teams mode)
120+
- Any assumptions made or questions
121+
```
122+
48123
## API Reference
49124

50125
### Constructor
@@ -59,33 +134,38 @@ $client = new \Klime\Client([
59134
'retryMaxAttempts' => 5, // Optional: Max retry attempts
60135
'retryInitialDelay' => 1000, // Optional: Initial retry delay in ms
61136
'flushOnShutdown' => true, // Optional: Auto-flush on script end
137+
'logger' => function($level, $msg, $ctx) { ... }, // Optional: Logging callback
138+
'onError' => function($error, $events) { ... }, // Optional: Error callback
139+
'onSuccess' => function($response) { ... }, // Optional: Success callback
62140
]);
63141
```
64142

65143
### Methods
66144

67145
#### `track(string $event, ?array $properties = null, ?array $options = null)`
68146

69-
Track a user event. A `userId` is required for events to be useful in Klime.
147+
Track an event. Events can be attributed in two ways:
148+
- **User events**: Provide `userId` to track user activity (most common)
149+
- **Group events**: Provide `groupId` without `userId` for organization-level events
70150

71151
```php
72-
// Basic tracking
152+
// User event (most common)
73153
$client->track('Button Clicked', [
74154
'button_name' => 'Sign up',
75155
'plan' => 'pro'
76156
], [
77157
'userId' => 'user_123'
78158
]);
79159

80-
// With IP address (for geolocation)
81-
$client->track('Button Clicked', [
82-
'button_name' => 'Sign up'
160+
// Group event (for webhooks, cron jobs, system events)
161+
$client->track('Events Received', [
162+
'count' => 100,
163+
'source' => 'webhook'
83164
], [
84-
'userId' => 'user_123',
85-
'ip' => $_SERVER['REMOTE_ADDR']
165+
'groupId' => 'org_456'
86166
]);
87167

88-
// With group context
168+
// User event with explicit group context
89169
$client->track('Feature Used', [
90170
'feature' => 'export'
91171
], [
@@ -104,13 +184,6 @@ $client->identify('user_123', [
104184
'name' => 'Stefan',
105185
'plan' => 'pro'
106186
]);
107-
108-
// With IP address
109-
$client->identify('user_123', [
110-
'email' => '[email protected]'
111-
], [
112-
'ip' => $_SERVER['REMOTE_ADDR']
113-
]);
114187
```
115188

116189
#### `group(string $groupId, ?array $traits = null, ?array $options = null)`
@@ -152,25 +225,123 @@ Gracefully shutdown the client, flushing remaining events.
152225
$client->shutdown();
153226
```
154227

228+
#### `getQueueSize(): int`
229+
230+
Return the number of events currently in the queue.
231+
232+
```php
233+
$pending = $client->getQueueSize();
234+
echo "{$pending} events waiting to be sent";
235+
```
236+
237+
### Synchronous Methods
238+
239+
For cases where you need confirmation that events were sent (e.g., in tests, before exit), use the synchronous variants:
240+
241+
#### `trackSync(string $event, ?array $properties, ?array $options): BatchResponse`
242+
243+
Track an event synchronously. Returns `BatchResponse` or throws `SendException`.
244+
245+
```php
246+
use Klime\SendException;
247+
248+
try {
249+
$response = $client->trackSync('Critical Action', ['key' => 'value'], ['userId' => 'user_123']);
250+
echo "Sent! Accepted: {$response->accepted}";
251+
} catch (SendException $e) {
252+
echo "Failed to send: {$e->getMessage()}";
253+
}
254+
```
255+
256+
#### `identifySync(string $userId, ?array $traits): BatchResponse`
257+
258+
Identify a user synchronously. Returns `BatchResponse` or throws `SendException`.
259+
260+
#### `groupSync(string $groupId, ?array $traits, ?array $options): BatchResponse`
261+
262+
Associate a user with a group synchronously. Returns `BatchResponse` or throws `SendException`.
263+
155264
## Features
156265

157266
- **Automatic Batching**: Events are automatically batched and sent every 2 seconds or when the batch reaches 20 events
158267
- **Automatic Retries**: Failed requests are automatically retried with exponential backoff
159268
- **Process Exit Handling**: Automatically flushes events on script end via `register_shutdown_function`
160269
- **Zero Dependencies**: Uses only PHP standard library (curl, json)
161270

271+
## Performance
272+
273+
Tracking methods are designed for minimal overhead in PHP's request-response model. When you call `track()`, `identify()`, or `group()`, the SDK:
274+
275+
1. Adds the event to an in-memory array (microseconds)
276+
2. Returns immediately
277+
278+
Events are flushed and sent to Klime's servers:
279+
280+
- **Automatically at script end**: via `register_shutdown_function()` - no explicit call needed
281+
- **When batch is full**: if 20+ events queue up, they're sent immediately
282+
- **On explicit `flush()`**: for long-running processes
283+
284+
```php
285+
// This adds to the queue - no HTTP request yet
286+
$client->track('Button Clicked', ['button' => 'signup'], ['userId' => 'user_123']);
287+
288+
// Your code continues without waiting
289+
return response()->json(['success' => true]);
290+
291+
// Events are automatically flushed when the PHP script ends
292+
```
293+
294+
**For typical web requests**, the HTTP request to Klime happens after your response is sent to the user (during PHP's shutdown phase), so it doesn't add latency to your application's response time.
295+
296+
**For long-running processes** (queue workers, daemons), call `flush()` periodically or when appropriate to send queued events.
297+
162298
## Configuration
163299

164300
### Default Values
165301

166-
| Setting | Default |
167-
|---------|---------|
168-
| `flushInterval` | 2000ms |
169-
| `maxBatchSize` | 20 events |
170-
| `maxQueueSize` | 1000 events |
171-
| `retryMaxAttempts` | 5 attempts |
172-
| `retryInitialDelay` | 1000ms |
173-
| `flushOnShutdown` | true |
302+
| Setting | Default |
303+
| ------------------- | ----------- |
304+
| `flushInterval` | 2000ms |
305+
| `maxBatchSize` | 20 events |
306+
| `maxQueueSize` | 1000 events |
307+
| `retryMaxAttempts` | 5 attempts |
308+
| `retryInitialDelay` | 1000ms |
309+
| `flushOnShutdown` | true |
310+
311+
### Logging
312+
313+
The SDK accepts a callable for logging (no PSR-3 dependency required):
314+
315+
```php
316+
$client = new \Klime\Client([
317+
'writeKey' => 'your-write-key',
318+
'logger' => function(string $level, string $message, array $context): void {
319+
error_log("[Klime][$level] $message " . json_encode($context));
320+
},
321+
]);
322+
323+
// Or wrap a PSR-3 logger
324+
$client = new \Klime\Client([
325+
'writeKey' => 'your-write-key',
326+
'logger' => fn($level, $msg, $ctx) => $psrLogger->log($level, $msg, $ctx),
327+
]);
328+
```
329+
330+
### Callbacks
331+
332+
```php
333+
$client = new \Klime\Client([
334+
'writeKey' => 'your-write-key',
335+
'onError' => function(\Throwable $error, array $events): void {
336+
// Report to your error tracking service
337+
\Sentry\captureException($error);
338+
error_log("Failed to send " . count($events) . " events: " . $error->getMessage());
339+
},
340+
'onSuccess' => function(\Klime\BatchResponse $response): void {
341+
error_log("Sent {$response->accepted} events");
342+
},
343+
]);
344+
```
174345

175346
## Error Handling
176347

@@ -180,6 +351,18 @@ The SDK automatically handles:
180351
- **Permanent errors** (400, 401): Logs error and drops batch
181352
- **Rate limiting**: Respects `Retry-After` header
182353

354+
For synchronous operations, use `*Sync()` methods which throw `SendException` on failure:
355+
356+
```php
357+
use Klime\SendException;
358+
359+
try {
360+
$response = $client->trackSync('Event', [], ['userId' => 'user_123']);
361+
} catch (SendException $e) {
362+
echo "Failed: {$e->getMessage()}, events: " . count($e->events);
363+
}
364+
```
365+
183366
## Size Limits
184367

185368
- Maximum event size: 200KB
@@ -214,8 +397,7 @@ class AnalyticsController extends Controller
214397
$this->klime->track('Button Clicked', [
215398
'button_name' => $request->input('buttonName')
216399
], [
217-
'userId' => (string) $request->user()->id,
218-
'ip' => $request->ip()
400+
'userId' => (string) $request->user()->id
219401
]);
220402

221403
return response()->json(['success' => true]);
@@ -253,8 +435,7 @@ class AnalyticsController extends AbstractController
253435
$this->klime->track('Button Clicked', [
254436
'button_name' => $data['buttonName'] ?? null
255437
], [
256-
'userId' => (string) $this->getUser()->getId(),
257-
'ip' => $request->getClientIp()
438+
'userId' => (string) $this->getUser()->getId()
258439
]);
259440

260441
return $this->json(['success' => true]);
@@ -276,14 +457,13 @@ $client = new \Klime\Client([
276457
// Handle POST request
277458
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
278459
$data = json_decode(file_get_contents('php://input'), true);
279-
460+
280461
$client->track('Button Clicked', [
281462
'button_name' => $data['buttonName'] ?? null
282463
], [
283-
'userId' => $data['userId'] ?? null,
284-
'ip' => $_SERVER['REMOTE_ADDR']
464+
'userId' => $data['userId'] ?? null
285465
]);
286-
466+
287467
header('Content-Type: application/json');
288468
echo json_encode(['success' => true]);
289469
}
@@ -298,4 +478,3 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
298478
## License
299479

300480
MIT
301-

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
}
1313
],
1414
"require": {
15-
"php": ">=8.1",
15+
"php": ">=8.2",
1616
"ext-curl": "*",
1717
"ext-json": "*"
1818
},

0 commit comments

Comments
 (0)