Skip to content

Commit 07eb02f

Browse files
committed
Release v1.1.0
1 parent 0af74de commit 07eb02f

File tree

12 files changed

+453
-224
lines changed

12 files changed

+453
-224
lines changed

CHANGELOG.md

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

10-
- Installation prompt for AI-assisted integration (Cursor, Copilot, etc.) with phased workflow guidance
10+
- Synchronous methods `trackSync()`, `identifySync()`, and `groupSync()` for cases requiring send confirmation
11+
- `getQueueSize()` method to check number of queued events
12+
- `onError` and `onSuccess` callback options for batch send results
13+
- `SendException` class for handling synchronous operation failures
14+
- Support for group-only events using `groupId` without `userId` for organization-level tracking
1115

12-
- Event names in examples now use Title Case (e.g., "Report Generated", "Feature Used") instead of snake_case
16+
- Documentation updated to support both analytics modes (Companies & Teams vs Individual Customers)
17+
18+
- **BREAKING:** Remove IP tracking - the `ip()` option has been removed from `TrackOptions`, `IdentifyOptions`, and `GroupOptions`
1319

README.md

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1717
Or 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 {
6969
Copy 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
7480
KEY 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
8086
BEST 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
8691
Add 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
95100
import com.klime.KlimeClient;
96101
import com.klime.TrackOptions;
@@ -102,11 +107,11 @@ KlimeClient client = KlimeClient.builder().writeKey(System.getenv("KLIME_WRITE_K
102107
client.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());
106111
client.track("Feature Used", Map.of("feature", "export", "format", "csv"), TrackOptions.builder().userId("usr_abc123").build());
107112
client.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
110115
client.group("org_456", Map.of("name", "Acme Inc", "plan", "enterprise"), GroupOptions.builder().userId("usr_abc123").build());
111116
112117
INTEGRATION WORKFLOW:
@@ -115,7 +120,9 @@ Phase 1: Discover
115120
Explore the codebase to understand:
116121
1. What framework is used? (Spring Boot, Quarkus, Micronaut, Jakarta EE, etc.)
117122
2. 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())
119126
4. Where do core user actions happen? (controllers, services, event handlers)
120127
5. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, track)
121128
Match your integration style to the framework's conventions.
@@ -124,7 +131,7 @@ Phase 2: Instrument
124131
Add 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
130137
WHAT TO TRACK:
@@ -139,7 +146,7 @@ Phase 4: Summarize
139146
Report 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();
237241
client.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
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.klime</groupId>
88
<artifactId>klime</artifactId>
9-
<version>1.0.2</version>
9+
<version>1.1.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>Klime Java SDK</name>

src/main/java/com/klime/EventContext.java

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,20 @@
77
*/
88
public final class EventContext {
99
private final LibraryInfo library;
10-
private final String ip;
1110

1211
/**
13-
* Create a new EventContext with library info only.
12+
* Create a new EventContext with library info.
1413
*
1514
* @param library the library info
1615
*/
1716
public EventContext(LibraryInfo library) {
18-
this(library, null);
19-
}
20-
21-
/**
22-
* Create a new EventContext with library info and IP.
23-
*
24-
* @param library the library info
25-
* @param ip the IP address (optional)
26-
*/
27-
public EventContext(LibraryInfo library, String ip) {
2817
this.library = Objects.requireNonNull(library, "library cannot be null");
29-
this.ip = ip;
3018
}
3119

3220
public LibraryInfo getLibrary() {
3321
return library;
3422
}
3523

36-
public String getIp() {
37-
return ip;
38-
}
39-
4024
/**
4125
* Serialize to JSON string.
4226
*
@@ -45,9 +29,6 @@ public String getIp() {
4529
public String toJson() {
4630
StringBuilder sb = new StringBuilder();
4731
sb.append("{\"library\":").append(library.toJson());
48-
if (ip != null && !ip.isEmpty()) {
49-
sb.append(",\"ip\":\"").append(JsonUtils.escape(ip)).append("\"");
50-
}
5132
sb.append("}");
5233
return sb.toString();
5334
}
@@ -57,17 +38,17 @@ public boolean equals(Object o) {
5738
if (this == o) return true;
5839
if (o == null || getClass() != o.getClass()) return false;
5940
EventContext that = (EventContext) o;
60-
return Objects.equals(library, that.library) && Objects.equals(ip, that.ip);
41+
return Objects.equals(library, that.library);
6142
}
6243

6344
@Override
6445
public int hashCode() {
65-
return Objects.hash(library, ip);
46+
return Objects.hash(library);
6647
}
6748

6849
@Override
6950
public String toString() {
70-
return "EventContext{library=" + library + ", ip='" + ip + "'}";
51+
return "EventContext{library=" + library + "}";
7152
}
7253
}
7354

0 commit comments

Comments
 (0)