@@ -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- 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
277458if ($_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
300480MIT
301-
0 commit comments