Skip to content

Commit 0af74de

Browse files
committed
Release v1.0.2
1 parent 2e1d207 commit 0af74de

File tree

8 files changed

+233
-151
lines changed

8 files changed

+233
-151
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ 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.0.2] - 2025-12-16
99

10-
- Java SDK with Maven support and JUnit 5 testing framework
10+
- Installation prompt for AI-assisted integration (Cursor, Copilot, etc.) with phased workflow guidance
11+
12+
- Event names in examples now use Title Case (e.g., "Report Generated", "Feature Used") instead of snake_case
1113

README.md

Lines changed: 100 additions & 22 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.0</version>
13+
<version>1.0.2</version>
1414
</dependency>
1515
```
1616

1717
Or with Gradle:
1818

1919
```groovy
20-
implementation 'com.klime:klime:1.0.0'
20+
implementation 'com.klime:klime:1.0.2'
2121
```
2222

2323
## Quick Start
@@ -64,6 +64,85 @@ public class Example {
6464
}
6565
```
6666

67+
## Installation Prompt
68+
69+
Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:
70+
71+
```
72+
Integrate Klime for B2B customer analytics. Klime tracks user activity to identify which customers (companies) are healthy vs at risk of churning.
73+
74+
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
78+
- Order doesn't matter - events before identify/group still get attributed correctly
79+
80+
BEST PRACTICES:
81+
- Initialize client ONCE at app startup (singleton or Spring bean)
82+
- Store write key in KLIME_WRITE_KEY environment variable
83+
- Call shutdown() on application stop to flush remaining events
84+
- Pass user's IP address for geolocation (use .ip() option)
85+
86+
Add to pom.xml:
87+
<dependency>
88+
<groupId>com.klime</groupId>
89+
<artifactId>klime</artifactId>
90+
<version>1.0.2</version>
91+
</dependency>
92+
93+
Or with Gradle: implementation 'com.klime:klime:1.0.2'
94+
95+
import com.klime.KlimeClient;
96+
import com.klime.TrackOptions;
97+
import com.klime.GroupOptions;
98+
99+
KlimeClient client = KlimeClient.builder().writeKey(System.getenv("KLIME_WRITE_KEY")).build();
100+
101+
// Identify users at signup/login:
102+
client.identify("usr_abc123", Map.of("email", "jane@acme.com", "name", "Jane Smith"));
103+
104+
// Track key activities:
105+
client.track("Report Generated", Map.of("report_type", "revenue"), TrackOptions.builder().userId("usr_abc123").ip(clientIp).build());
106+
client.track("Feature Used", Map.of("feature", "export", "format", "csv"), TrackOptions.builder().userId("usr_abc123").build());
107+
client.track("Teammate Invited", Map.of("role", "member"), TrackOptions.builder().userId("usr_abc123").build());
108+
109+
// Link user to their company and set company traits:
110+
client.group("org_456", Map.of("name", "Acme Inc", "plan", "enterprise"), GroupOptions.builder().userId("usr_abc123").build());
111+
112+
INTEGRATION WORKFLOW:
113+
114+
Phase 1: Discover
115+
Explore the codebase to understand:
116+
1. What framework is used? (Spring Boot, Quarkus, Micronaut, Jakarta EE, etc.)
117+
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)
119+
4. Where do core user actions happen? (controllers, services, event handlers)
120+
5. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, track)
121+
Match your integration style to the framework's conventions.
122+
123+
Phase 2: Instrument
124+
Add these calls using idiomatic patterns for the framework:
125+
- Initialize client once (Spring: @Bean/@Configuration, Quarkus: @ApplicationScoped, Jakarta EE: @WebListener)
126+
- identify() in auth/login success handler
127+
- group() when user-org association is established
128+
- track() for key user actions (see below)
129+
130+
WHAT TO TRACK:
131+
Active engagement (primary): feature usage, resource creation, collaboration, completing flows
132+
Session signals (secondary): login/session start, dashboard access - distinguishes "low usage" from "churned"
133+
Do NOT track: every endpoint, health checks, actuator calls, background jobs
134+
135+
Phase 3: Verify
136+
Confirm: client initialized, shutdown handled, identify/group/track calls added
137+
138+
Phase 4: Summarize
139+
Report what you added:
140+
- Files modified and what was added to each
141+
- Events being tracked (list event names and what triggers them)
142+
- How userId and groupId are obtained
143+
- Any assumptions made or questions
144+
```
145+
67146
## API Reference
68147

69148
### Initialization
@@ -170,26 +249,26 @@ client.shutdown().join();
170249

171250
## Configuration
172251

173-
| Option | Default | Description |
174-
| ------------------- | ----------------------- | --------------------------------- |
175-
| `writeKey` | (required) | Your Klime write key |
176-
| `endpoint` | `https://i.klime.com` | API endpoint URL |
177-
| `flushInterval` | `2 seconds` | Time between automatic flushes |
178-
| `maxBatchSize` | `20` | Max events per batch (max: 100) |
179-
| `maxQueueSize` | `1000` | Max queued events |
180-
| `retryMaxAttempts` | `5` | Max retry attempts |
181-
| `retryInitialDelay` | `1 second` | Initial retry delay |
182-
| `flushOnShutdown` | `true` | Auto-flush on JVM shutdown |
252+
| Option | Default | Description |
253+
| ------------------- | --------------------- | ------------------------------- |
254+
| `writeKey` | (required) | Your Klime write key |
255+
| `endpoint` | `https://i.klime.com` | API endpoint URL |
256+
| `flushInterval` | `2 seconds` | Time between automatic flushes |
257+
| `maxBatchSize` | `20` | Max events per batch (max: 100) |
258+
| `maxQueueSize` | `1000` | Max queued events |
259+
| `retryMaxAttempts` | `5` | Max retry attempts |
260+
| `retryInitialDelay` | `1 second` | Initial retry delay |
261+
| `flushOnShutdown` | `true` | Auto-flush on JVM shutdown |
183262

184263
## Error Handling
185264

186-
| Status Code | Behavior |
187-
| ----------- | ----------------------------------------------- |
188-
| 200 | Success (may contain partial failures) |
189-
| 400 | Malformed request - events dropped, no retry |
190-
| 401 | Invalid write key - events dropped, no retry |
191-
| 429 | Rate limited - retry with exponential backoff |
192-
| 503 | Service unavailable - retry with backoff |
265+
| Status Code | Behavior |
266+
| ----------- | --------------------------------------------- |
267+
| 200 | Success (may contain partial failures) |
268+
| 400 | Malformed request - events dropped, no retry |
269+
| 401 | Invalid write key - events dropped, no retry |
270+
| 429 | Rate limited - retry with exponential backoff |
271+
| 503 | Service unavailable - retry with backoff |
193272

194273
## Size Limits
195274

@@ -230,7 +309,7 @@ public class UserService {
230309

231310
public void registerUser(String userId, String email) {
232311
// Your business logic...
233-
312+
234313
klime.identify(userId, Map.of(
235314
"email", email,
236315
"registeredAt", Instant.now().toString()
@@ -278,7 +357,7 @@ public class ActionServlet extends HttpServlet {
278357
@Override
279358
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
280359
String userId = req.getParameter("userId");
281-
360+
282361
KlimeListener.getClient().track("Action Performed", Map.of(
283362
"action", "submit"
284363
), TrackOptions.builder()
@@ -329,4 +408,3 @@ public class KlimeProducer {
329408
## License
330409

331410
MIT License - see [LICENSE.md](LICENSE.md) for details.
332-

pom.xml

Lines changed: 38 additions & 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.1</version>
9+
<version>1.0.2</version>
1010
<packaging>jar</packaging>
1111

1212
<name>Klime Java SDK</name>
@@ -101,5 +101,42 @@
101101
</plugin>
102102
</plugins>
103103
</build>
104+
105+
<profiles>
106+
<profile>
107+
<id>release</id>
108+
<build>
109+
<plugins>
110+
<!-- GPG signing -->
111+
<plugin>
112+
<groupId>org.apache.maven.plugins</groupId>
113+
<artifactId>maven-gpg-plugin</artifactId>
114+
<version>3.2.4</version>
115+
<executions>
116+
<execution>
117+
<id>sign-artifacts</id>
118+
<phase>verify</phase>
119+
<goals>
120+
<goal>sign</goal>
121+
</goals>
122+
</execution>
123+
</executions>
124+
</plugin>
125+
<!-- Publish to Maven Central -->
126+
<plugin>
127+
<groupId>org.sonatype.central</groupId>
128+
<artifactId>central-publishing-maven-plugin</artifactId>
129+
<version>0.6.0</version>
130+
<extensions>true</extensions>
131+
<configuration>
132+
<publishingServerId>central</publishingServerId>
133+
<autoPublish>true</autoPublish>
134+
<waitUntil>published</waitUntil>
135+
</configuration>
136+
</plugin>
137+
</plugins>
138+
</build>
139+
</profile>
140+
</profiles>
104141
</project>
105142

src/main/java/com/klime/KlimeClient.java

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import java.net.http.HttpResponse;
88
import java.time.Duration;
99
import java.util.ArrayList;
10-
import java.util.Collections;
1110
import java.util.List;
1211
import java.util.Map;
1312
import java.util.concurrent.BlockingQueue;
@@ -25,31 +24,34 @@
2524
/**
2625
* Main client for the Klime analytics SDK.
2726
*
28-
* <p>Example usage:</p>
27+
* <p>
28+
* Example usage:
29+
* </p>
30+
*
2931
* <pre>{@code
3032
* KlimeClient client = KlimeClient.builder()
31-
* .writeKey("your-write-key")
32-
* .build();
33+
* .writeKey("your-write-key")
34+
* .build();
3335
*
3436
* client.track("Button Clicked", Map.of("buttonName", "Sign up"),
35-
* TrackOptions.builder().userId("user_123").build());
37+
* TrackOptions.builder().userId("user_123").build());
3638
*
3739
* client.shutdown();
3840
* }</pre>
3941
*/
4042
public class KlimeClient {
4143
private static final Logger LOGGER = Logger.getLogger(KlimeClient.class.getName());
42-
43-
public static final String SDK_VERSION = "1.0.0";
44+
45+
public static final String SDK_VERSION = "1.0.2";
4446
public static final String SDK_NAME = "java-sdk";
45-
47+
4648
public static final String DEFAULT_ENDPOINT = "https://i.klime.com";
4749
public static final Duration DEFAULT_FLUSH_INTERVAL = Duration.ofMillis(2000);
4850
public static final int DEFAULT_MAX_BATCH_SIZE = 20;
4951
public static final int DEFAULT_MAX_QUEUE_SIZE = 1000;
5052
public static final int DEFAULT_RETRY_MAX_ATTEMPTS = 5;
5153
public static final Duration DEFAULT_RETRY_INITIAL_DELAY = Duration.ofMillis(1000);
52-
54+
5355
public static final int MAX_BATCH_SIZE = 100;
5456
public static final int MAX_EVENT_SIZE_BYTES = 200 * 1024; // 200KB
5557
public static final int MAX_BATCH_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
@@ -80,18 +82,19 @@ private KlimeClient(Builder builder) {
8082
this.endpoint = builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT;
8183
this.flushInterval = builder.flushInterval != null ? builder.flushInterval : DEFAULT_FLUSH_INTERVAL;
8284
this.maxBatchSize = Math.min(
83-
builder.maxBatchSize != null ? builder.maxBatchSize : DEFAULT_MAX_BATCH_SIZE,
84-
MAX_BATCH_SIZE
85-
);
85+
builder.maxBatchSize != null ? builder.maxBatchSize : DEFAULT_MAX_BATCH_SIZE,
86+
MAX_BATCH_SIZE);
8687
this.maxQueueSize = builder.maxQueueSize != null ? builder.maxQueueSize : DEFAULT_MAX_QUEUE_SIZE;
87-
this.retryMaxAttempts = builder.retryMaxAttempts != null ? builder.retryMaxAttempts : DEFAULT_RETRY_MAX_ATTEMPTS;
88-
this.retryInitialDelay = builder.retryInitialDelay != null ? builder.retryInitialDelay : DEFAULT_RETRY_INITIAL_DELAY;
88+
this.retryMaxAttempts = builder.retryMaxAttempts != null ? builder.retryMaxAttempts
89+
: DEFAULT_RETRY_MAX_ATTEMPTS;
90+
this.retryInitialDelay = builder.retryInitialDelay != null ? builder.retryInitialDelay
91+
: DEFAULT_RETRY_INITIAL_DELAY;
8992
this.flushOnShutdown = builder.flushOnShutdown != null ? builder.flushOnShutdown : true;
9093

9194
this.queue = new LinkedBlockingQueue<>(maxQueueSize);
9295
this.httpClient = HttpClient.newBuilder()
93-
.connectTimeout(Duration.ofSeconds(10))
94-
.build();
96+
.connectTimeout(Duration.ofSeconds(10))
97+
.build();
9598
this.scheduler = new ScheduledThreadPoolExecutor(1, r -> {
9699
Thread t = new Thread(r, "klime-flush");
97100
t.setDaemon(true);
@@ -124,13 +127,13 @@ public void track(String event, Map<String, Object> properties, TrackOptions opt
124127
String ip = options != null ? options.getIp() : null;
125128

126129
Event e = Event.builder()
127-
.type(EventType.TRACK)
128-
.event(event)
129-
.properties(properties)
130-
.userId(userId)
131-
.groupId(groupId)
132-
.context(buildContext(ip))
133-
.build();
130+
.type(EventType.TRACK)
131+
.event(event)
132+
.properties(properties)
133+
.userId(userId)
134+
.groupId(groupId)
135+
.context(buildContext(ip))
136+
.build();
134137

135138
enqueue(e);
136139
}
@@ -160,11 +163,11 @@ public void identify(String userId, Map<String, Object> traits, IdentifyOptions
160163
String ip = options != null ? options.getIp() : null;
161164

162165
Event e = Event.builder()
163-
.type(EventType.IDENTIFY)
164-
.userId(userId)
165-
.traits(traits)
166-
.context(buildContext(ip))
167-
.build();
166+
.type(EventType.IDENTIFY)
167+
.userId(userId)
168+
.traits(traits)
169+
.context(buildContext(ip))
170+
.build();
168171

169172
enqueue(e);
170173
}
@@ -195,12 +198,12 @@ public void group(String groupId, Map<String, Object> traits, GroupOptions optio
195198
String ip = options != null ? options.getIp() : null;
196199

197200
Event e = Event.builder()
198-
.type(EventType.GROUP)
199-
.groupId(groupId)
200-
.userId(userId)
201-
.traits(traits)
202-
.context(buildContext(ip))
203-
.build();
201+
.type(EventType.GROUP)
202+
.groupId(groupId)
203+
.userId(userId)
204+
.traits(traits)
205+
.context(buildContext(ip))
206+
.build();
204207

205208
enqueue(e);
206209
}
@@ -385,12 +388,12 @@ private void sendBatch(List<Event> batch) {
385388
while (attempt < retryMaxAttempts) {
386389
try {
387390
HttpRequest request = HttpRequest.newBuilder()
388-
.uri(uri)
389-
.header("Content-Type", "application/json")
390-
.header("Authorization", "Bearer " + writeKey)
391-
.timeout(Duration.ofSeconds(10))
392-
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
393-
.build();
391+
.uri(uri)
392+
.header("Content-Type", "application/json")
393+
.header("Authorization", "Bearer " + writeKey)
394+
.timeout(Duration.ofSeconds(10))
395+
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
396+
.build();
394397

395398
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
396399
int statusCode = response.statusCode();
@@ -596,4 +599,3 @@ public KlimeClient build() {
596599
}
597600
}
598601
}
599-

0 commit comments

Comments
 (0)