Java SDK
Official Klime SDK for Java. Track events, identify users, and associate them with groups.
Installation
Add to your pom.xml:
<dependency>
<groupId>com.klime</groupId>
<artifactId>klime</artifactId>
<version>1.1.0</version>
</dependency>Or with Gradle:
implementation 'com.klime:klime:1.1.0'Quick Start
import com.klime.KlimeClient;
import com.klime.TrackOptions;
import java.util.Map;
public class Example {
public static void main(String[] args) {
// Initialize the client
KlimeClient client = KlimeClient.builder()
.writeKey("your-write-key")
.build();
// Track an event
client.track("Button Clicked", Map.of(
"buttonName", "Sign up",
"page", "/pricing"
), TrackOptions.builder()
.userId("user_123")
.build());
// Identify a user
client.identify("user_123", Map.of(
"email", "user@example.com",
"name", "Stefan",
"plan", "pro"
));
// Associate user with a group
client.group("org_456", Map.of(
"name", "Acme Inc",
"plan", "enterprise"
), GroupOptions.builder()
.userId("user_123")
.build());
// Graceful shutdown (flushes remaining events)
client.shutdown().join();
}
}Good to know
- No anonymous tracking. Every
track()call needs auserIdorgroupId. Wait until the user is identified before sending events. - Name your events in past tense with an Object + Action pattern:
"Report Generated","User Invited","Export Completed". - Always set
emailandnametraits inidentify()andgroup(). These are used for display and search in the dashboard. - Traits can be strings, numbers, booleans, or ISO 8601 date strings. Use camelCase for trait keys.
- In Group mode, your dashboard stays empty until you call
group(). Events fromidentify()andtrack()are recorded, but customers won't appear until users are linked to groups. - Order doesn't matter. Events sent before
identify()orgroup()are retroactively attributed once the relationships are established.
Installation Prompt
Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:
Integrate Klime for customer analytics. Klime tracks user activity to identify which customers are healthy vs at risk of churning.
ANALYTICS MODES (determine which applies):
- Companies & Teams: Your customers are companies with multiple team members (SaaS, enterprise tools)
→ Use identify() + group() + track()
- Individual Customers: Your customers are individuals with private accounts (consumer apps, creator tools)
→ Use identify() + track() only (no group() needed)
KEY CONCEPTS:
- Every track() call requires either userId OR groupId (no anonymous events)
- Use groupId alone for org-level events (webhooks, cron jobs, system metrics)
- group() links a user to a company AND sets company traits (only for Companies & Teams mode)
- Order doesn't matter - events before identify/group still get attributed correctly
BEST PRACTICES:
- Initialize client ONCE at app startup (singleton or Spring bean)
- Store write key in KLIME_WRITE_KEY environment variable
- Call shutdown() on application stop to flush remaining events
Add to pom.xml:
<dependency>
<groupId>com.klime</groupId>
<artifactId>klime</artifactId>
<version>1.1.0</version>
</dependency>
Or with Gradle: implementation 'com.klime:klime:1.1.0'
import com.klime.KlimeClient;
import com.klime.TrackOptions;
import com.klime.GroupOptions;
KlimeClient client = KlimeClient.builder().writeKey(System.getenv("KLIME_WRITE_KEY")).build();
// Identify users at signup/login:
client.identify("usr_abc123", Map.of("email", "jane@acme.com", "name", "Jane Smith"));
// Track key activities:
client.track("Report Generated", Map.of("report_type", "revenue"), TrackOptions.builder().userId("usr_abc123").build());
client.track("Feature Used", Map.of("feature", "export", "format", "csv"), TrackOptions.builder().userId("usr_abc123").build());
client.track("Teammate Invited", Map.of("role", "member"), TrackOptions.builder().userId("usr_abc123").build());
// If Companies & Teams mode: link user to their company and set company traits
client.group("org_456", Map.of("name", "Acme Inc", "plan", "enterprise"), GroupOptions.builder().userId("usr_abc123").build());
INTEGRATION WORKFLOW:
Phase 1: Discover
Explore the codebase to understand:
1. What framework is used? (Spring Boot, Quarkus, Micronaut, Jakarta EE, etc.)
2. Where is user identity available? (e.g., SecurityContextHolder, @AuthenticationPrincipal, Principal, JWT claims)
3. Is this Companies & Teams or Individual Customers?
- Look for: organization, workspace, tenant, team, account models → Companies & Teams (use group())
- No company/org concept, just individual users → Individual Customers (skip group())
4. Where do core user actions happen? (controllers, services, event handlers)
5. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, track)
Match your integration style to the framework's conventions.
Phase 2: Instrument
Add these calls using idiomatic patterns for the framework:
- Initialize client once (Spring: @Bean/@Configuration, Quarkus: @ApplicationScoped, Jakarta EE: @WebListener)
- identify() in auth/login success handler
- group() when user-org association is established (Companies & Teams mode only)
- track() for key user actions (see below)
WHAT TO TRACK:
Active engagement (primary): feature usage, resource creation, collaboration, completing flows
Session signals (secondary): login/session start, dashboard access - distinguishes "low usage" from "churned"
Do NOT track: every endpoint, health checks, actuator calls, background jobs
Phase 3: Verify
Confirm: client initialized, shutdown handled, identify/group/track calls added
Phase 4: Summarize
Report what you added:
- Files modified and what was added to each
- Events being tracked (list event names and what triggers them)
- How userId is obtained (and groupId if Companies & Teams mode)
- Any assumptions made or questionsAPI Reference
Initialization
KlimeClient client = KlimeClient.builder()
.writeKey("your-write-key") // Required
.endpoint("https://i.klime.com") // Optional, default shown
.flushInterval(Duration.ofSeconds(2)) // Optional
.maxBatchSize(20) // Optional, max 100
.maxQueueSize(1000) // Optional
.retryMaxAttempts(5) // Optional
.retryInitialDelay(Duration.ofSeconds(1)) // Optional
.flushOnShutdown(true) // Optional
.onError((error, events) -> { ... }) // Optional: callback for batch failures
.onSuccess((response) -> { ... }) // Optional: callback for successful sends
.build();track(event, properties, options)
Track a user event.
// With all options
client.track("Feature Used", Map.of(
"feature", "export",
"format", "csv"
), TrackOptions.builder()
.userId("user_123")
.groupId("org_456")
.build());
// Simple usage
client.track("Page Viewed", Map.of("page", "/home"));identify(userId, traits, options)
Identify a user with traits.
client.identify("user_123", Map.of(
"email", "user@example.com",
"name", "Stefan",
"createdAt", "2025-01-15T10:30:00Z"
));group(groupId, traits, options)
Associate a user with a group and/or set group traits.
// Associate user with group and set traits
client.group("org_456", Map.of(
"name", "Acme Inc",
"plan", "enterprise",
"employeeCount", 50
), GroupOptions.builder()
.userId("user_123")
.build());
// Just update group traits (no user)
client.group("org_456", Map.of("plan", "enterprise"));flush()
Manually flush queued events immediately.
// Async flush
client.flush();
// Wait for flush to complete
client.flush().join();shutdown()
Gracefully shutdown the client, flushing remaining events.
// Async shutdown
client.shutdown();
// Wait for shutdown to complete
client.shutdown().join();getQueueSize()
Get the number of events currently queued.
int pending = client.getQueueSize();
System.out.println(pending + " events waiting to be sent");Synchronous Methods
For cases where you need confirmation that events were sent (e.g., in tests, before exit), use the synchronous variants:
import com.klime.SendException;
try {
BatchResponse response = client.trackSync("Critical Action", Map.of("key", "value"),
TrackOptions.builder().userId("user_123").build());
System.out.println("Sent! Accepted: " + response.getAccepted());
} catch (SendException e) {
System.err.println("Failed: " + e.getMessage() + ", events: " + e.getEvents().size());
}Available sync methods:
trackSync(event, properties, options)- Track synchronouslyidentifySync(userId, traits)- Identify synchronouslygroupSync(groupId, traits, options)- Group synchronously
Features
- Zero dependencies: Uses only Java standard library (Java 11+)
- Automatic batching: Events are batched for efficient delivery
- Retry with backoff: Transient failures are automatically retried
- Thread-safe: Safe for concurrent use from multiple threads
- Fire-and-forget: Non-blocking event submission
- Graceful shutdown: JVM shutdown hook ensures events are flushed
- CompletableFuture API: Async operations return CompletableFuture
Performance
When you call track(), identify(), or group(), the SDK:
- Adds the event to a thread-safe
BlockingQueue(microseconds) - Returns immediately without waiting for network I/O
Events are sent to Klime's servers by a background ScheduledExecutorService. This means:
- No network blocking: HTTP requests happen asynchronously in background threads
- No latency impact: Tracking calls add < 1ms to your request handling time
- Automatic batching: Events are queued and sent in batches (default: every 2 seconds or 20 events)
// This returns immediately - no HTTP request is made here
client.track("Button Clicked", Map.of("button", "signup"),
TrackOptions.builder().userId("user_123").build());
// Your code continues without waiting
return ResponseEntity.ok(Map.of("success", true));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.
Configuration
| Option | Default | Description |
|---|---|---|
writeKey | (required) | Your Klime write key |
endpoint | https://i.klime.com | API endpoint URL |
flushInterval | 2 seconds | Time between automatic flushes |
maxBatchSize | 20 | Max events per batch (max: 100) |
maxQueueSize | 1000 | Max queued events |
retryMaxAttempts | 5 | Max retry attempts |
retryInitialDelay | 1 second | Initial retry delay |
flushOnShutdown | true | Auto-flush on JVM shutdown |
Logging
The SDK uses java.util.logging (JUL). Configure logging levels via your JUL configuration:
# logging.properties
com.klime.level = FINEOr programmatically:
Logger.getLogger("com.klime").setLevel(Level.FINE);For frameworks using SLF4J or Log4j, configure the standard JUL-to-SLF4J bridge.
Callbacks
KlimeClient client = KlimeClient.builder()
.writeKey("your-write-key")
.onError((error, events) -> {
// Report to your error tracking service
Sentry.captureException(error);
System.err.println("Failed to send " + events.size() + " events: " + error.getMessage());
})
.onSuccess((response) -> {
System.out.println("Sent " + response.getAccepted() + " events");
})
.build();Error Handling
| Status Code | Behavior |
|---|---|
| 200 | Success (may contain partial failures) |
| 400 | Malformed request - events dropped, no retry |
| 401 | Invalid write key - events dropped, no retry |
| 429 | Rate limited - retry with exponential backoff |
| 503 | Service unavailable - retry with backoff |
For synchronous operations, use *Sync() methods which throw SendException on failure:
import com.klime.SendException;
try {
BatchResponse response = client.trackSync("Event", null,
TrackOptions.builder().userId("user_123").build());
} catch (SendException e) {
System.err.println("Failed: " + e.getMessage() + ", events: " + e.getEvents().size());
}