@@ -10,14 +10,14 @@ Add to your `pom.xml`:
1010<dependency >
1111 <groupId >com.klime</groupId >
1212 <artifactId >klime</artifactId >
13- <version >1.0.2 </version >
13+ <version >1.1.0 </version >
1414</dependency >
1515```
1616
1717Or with Gradle:
1818
1919``` groovy
20- implementation 'com.klime:klime:1.0.2 '
20+ implementation 'com.klime:klime:1.1.0 '
2121```
2222
2323## Quick Start
@@ -69,28 +69,33 @@ public class Example {
6969Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:
7070
7171```
72- Integrate Klime for B2B customer analytics. Klime tracks user activity to identify which customers (companies) are healthy vs at risk of churning.
72+ Integrate Klime for customer analytics. Klime tracks user activity to identify which customers are healthy vs at risk of churning.
73+
74+ ANALYTICS MODES (determine which applies):
75+ - Companies & Teams: Your customers are companies with multiple team members (SaaS, enterprise tools)
76+ → Use identify() + group() + track()
77+ - Individual Customers: Your customers are individuals with private accounts (consumer apps, creator tools)
78+ → Use identify() + track() only (no group() needed)
7379
7480KEY CONCEPTS:
75- - Groups = companies/organizations (your B2B customers )
76- - Every track() call requires a userId (no anonymous events )
77- - group() links a user to a company AND sets company traits
81+ - Every track() call requires either userId OR groupId (no anonymous events )
82+ - Use groupId alone for org-level events (webhooks, cron jobs, system metrics )
83+ - group() links a user to a company AND sets company traits (only for Companies & Teams mode)
7884- Order doesn't matter - events before identify/group still get attributed correctly
7985
8086BEST PRACTICES:
8187- Initialize client ONCE at app startup (singleton or Spring bean)
8288- Store write key in KLIME_WRITE_KEY environment variable
8389- Call shutdown() on application stop to flush remaining events
84- - Pass user's IP address for geolocation (use .ip() option)
8590
8691Add to pom.xml:
8792<dependency>
8893 <groupId>com.klime</groupId>
8994 <artifactId>klime</artifactId>
90- <version>1.0.2 </version>
95+ <version>1.1.0 </version>
9196</dependency>
9297
93- Or with Gradle: implementation 'com.klime:klime:1.0.2 '
98+ Or with Gradle: implementation 'com.klime:klime:1.1.0 '
9499
95100import com.klime.KlimeClient;
96101import com.klime.TrackOptions;
@@ -102,11 +107,11 @@ KlimeClient client = KlimeClient.builder().writeKey(System.getenv("KLIME_WRITE_K
102107client.identify("usr_abc123", Map.of("email", "jane@acme.com", "name", "Jane Smith"));
103108
104109// Track key activities:
105- client.track("Report Generated", Map.of("report_type", "revenue"), TrackOptions.builder().userId("usr_abc123").ip(clientIp). build());
110+ client.track("Report Generated", Map.of("report_type", "revenue"), TrackOptions.builder().userId("usr_abc123").build());
106111client.track("Feature Used", Map.of("feature", "export", "format", "csv"), TrackOptions.builder().userId("usr_abc123").build());
107112client.track("Teammate Invited", Map.of("role", "member"), TrackOptions.builder().userId("usr_abc123").build());
108113
109- // Link user to their company and set company traits:
114+ // If Companies & Teams mode: link user to their company and set company traits
110115client.group("org_456", Map.of("name", "Acme Inc", "plan", "enterprise"), GroupOptions.builder().userId("usr_abc123").build());
111116
112117INTEGRATION WORKFLOW:
@@ -115,7 +120,9 @@ Phase 1: Discover
115120Explore the codebase to understand:
1161211. What framework is used? (Spring Boot, Quarkus, Micronaut, Jakarta EE, etc.)
1171222. Where is user identity available? (e.g., SecurityContextHolder, @AuthenticationPrincipal, Principal, JWT claims)
118- 3. How are organizations/companies modeled? (look for: organization, workspace, tenant, team, account)
123+ 3. Is this Companies & Teams or Individual Customers?
124+ - Look for: organization, workspace, tenant, team, account models → Companies & Teams (use group())
125+ - No company/org concept, just individual users → Individual Customers (skip group())
1191264. Where do core user actions happen? (controllers, services, event handlers)
1201275. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, track)
121128Match your integration style to the framework's conventions.
@@ -124,7 +131,7 @@ Phase 2: Instrument
124131Add these calls using idiomatic patterns for the framework:
125132- Initialize client once (Spring: @Bean/@Configuration, Quarkus: @ApplicationScoped, Jakarta EE: @WebListener)
126133- identify() in auth/login success handler
127- - group() when user-org association is established
134+ - group() when user-org association is established (Companies & Teams mode only)
128135- track() for key user actions (see below)
129136
130137WHAT TO TRACK:
@@ -139,7 +146,7 @@ Phase 4: Summarize
139146Report what you added:
140147- Files modified and what was added to each
141148- Events being tracked (list event names and what triggers them)
142- - How userId and groupId are obtained
149+ - How userId is obtained ( and groupId if Companies & Teams mode)
143150- Any assumptions made or questions
144151```
145152
@@ -157,6 +164,8 @@ KlimeClient client = KlimeClient.builder()
157164 .retryMaxAttempts(5 ) // Optional
158165 .retryInitialDelay(Duration . ofSeconds(1 )) // Optional
159166 .flushOnShutdown(true ) // Optional
167+ .onError((error, events) - > { ... }) // Optional: callback for batch failures
168+ .onSuccess((response) - > { ... }) // Optional: callback for successful sends
160169 .build();
161170```
162171
@@ -172,7 +181,6 @@ client.track("Feature Used", Map.of(
172181), TrackOptions . builder()
173182 .userId(" user_123" )
174183 .groupId(" org_456" )
175- .ip(" 192.168.1.1" )
176184 .build());
177185
178186// Simple usage
@@ -189,10 +197,6 @@ client.identify("user_123", Map.of(
189197 " name" , " Stefan" ,
190198 " createdAt" , " 2025-01-15T10:30:00Z"
191199));
192-
193- // With IP address
194- client. identify(" user_123" , Map . of(" email" , " user@example.com" ),
195- IdentifyOptions . builder(). ip(" 192.168.1.1" ). build());
196200```
197201
198202### group(groupId, traits, options)
@@ -237,6 +241,36 @@ client.shutdown();
237241client. shutdown(). join();
238242```
239243
244+ ### getQueueSize()
245+
246+ Get the number of events currently queued.
247+
248+ ``` java
249+ int pending = client. getQueueSize();
250+ System . out. println(pending + " events waiting to be sent" );
251+ ```
252+
253+ ### Synchronous Methods
254+
255+ For cases where you need confirmation that events were sent (e.g., in tests, before exit), use the synchronous variants:
256+
257+ ``` java
258+ import com.klime.SendException ;
259+
260+ try {
261+ BatchResponse response = client. trackSync(" Critical Action" , Map . of(" key" , " value" ),
262+ TrackOptions . builder(). userId(" user_123" ). build());
263+ System . out. println(" Sent! Accepted: " + response. getAccepted());
264+ } catch (SendException e) {
265+ System . err. println(" Failed: " + e. getMessage() + " , events: " + e. getEvents(). size());
266+ }
267+ ```
268+
269+ Available sync methods:
270+ - ` trackSync(event, properties, options) ` - Track synchronously
271+ - ` identifySync(userId, traits) ` - Identify synchronously
272+ - ` groupSync(groupId, traits, options) ` - Group synchronously
273+
240274## Features
241275
242276- ** Zero dependencies** : Uses only Java standard library (Java 11+)
@@ -247,6 +281,30 @@ client.shutdown().join();
247281- ** Graceful shutdown** : JVM shutdown hook ensures events are flushed
248282- ** CompletableFuture API** : Async operations return CompletableFuture
249283
284+ ## Performance
285+
286+ When you call ` track() ` , ` identify() ` , or ` group() ` , the SDK:
287+
288+ 1 . Adds the event to a thread-safe ` BlockingQueue ` (microseconds)
289+ 2 . Returns immediately without waiting for network I/O
290+
291+ Events are sent to Klime's servers by a background ` ScheduledExecutorService ` . This means:
292+
293+ - ** No network blocking** : HTTP requests happen asynchronously in background threads
294+ - ** No latency impact** : Tracking calls add < 1ms to your request handling time
295+ - ** Automatic batching** : Events are queued and sent in batches (default: every 2 seconds or 20 events)
296+
297+ ``` java
298+ // This returns immediately - no HTTP request is made here
299+ client. track(" Button Clicked" , Map . of(" button" , " signup" ),
300+ TrackOptions . builder(). userId(" user_123" ). build());
301+
302+ // Your code continues without waiting
303+ return ResponseEntity . ok(Map . of(" success" , true ));
304+ ```
305+
306+ The only blocking operations are ` flush().join() ` and ` shutdown().join() ` , which wait for all queued events to be sent. These are typically only called during graceful shutdown.
307+
250308## Configuration
251309
252310| Option | Default | Description |
@@ -260,6 +318,39 @@ client.shutdown().join();
260318| ` retryInitialDelay ` | ` 1 second ` | Initial retry delay |
261319| ` flushOnShutdown ` | ` true ` | Auto-flush on JVM shutdown |
262320
321+ ### Logging
322+
323+ The SDK uses ` java.util.logging ` (JUL). Configure logging levels via your JUL configuration:
324+
325+ ``` properties
326+ # logging.properties
327+ com.klime.level = FINE
328+ ```
329+
330+ Or programmatically:
331+
332+ ``` java
333+ Logger . getLogger(" com.klime" ). setLevel(Level . FINE );
334+ ```
335+
336+ For frameworks using SLF4J or Log4j, configure the standard JUL-to-SLF4J bridge.
337+
338+ ### Callbacks
339+
340+ ``` java
341+ KlimeClient client = KlimeClient . builder()
342+ .writeKey(" your-write-key" )
343+ .onError((error, events) - > {
344+ // Report to your error tracking service
345+ Sentry . captureException(error);
346+ System . err. println(" Failed to send " + events. size() + " events: " + error. getMessage());
347+ })
348+ .onSuccess((response) - > {
349+ System . out. println(" Sent " + response. getAccepted() + " events" );
350+ })
351+ .build();
352+ ```
353+
263354## Error Handling
264355
265356| Status Code | Behavior |
@@ -270,6 +361,19 @@ client.shutdown().join();
270361| 429 | Rate limited - retry with exponential backoff |
271362| 503 | Service unavailable - retry with backoff |
272363
364+ For synchronous operations, use ` *Sync() ` methods which throw ` SendException ` on failure:
365+
366+ ``` java
367+ import com.klime.SendException ;
368+
369+ try {
370+ BatchResponse response = client. trackSync(" Event" , null ,
371+ TrackOptions . builder(). userId(" user_123" ). build());
372+ } catch (SendException e) {
373+ System . err. println(" Failed: " + e. getMessage() + " , events: " + e. getEvents(). size());
374+ }
375+ ```
376+
273377## Size Limits
274378
275379- ** Per event** : 200KB max
@@ -362,7 +466,6 @@ public class ActionServlet extends HttpServlet {
362466 " action" , " submit"
363467 ), TrackOptions . builder()
364468 .userId(userId)
365- .ip(req. getRemoteAddr())
366469 .build());
367470 }
368471}
0 commit comments