Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3baf92b
Add configurable import batch size (1-2000)
santigracia Nov 1, 2025
91f7fc8
Add HTTP response logging for import endpoint errors
santigracia Nov 1, 2025
1c3cb2d
Deprecate trackCharge() method - now logs error instead of tracking r…
santigracia Nov 1, 2025
003f8b2
Support decimal increments in increment() method
santigracia Nov 1, 2025
96ae95e
Add customizable connection and read timeouts
santigracia Nov 1, 2025
b2dde50
Add payload chunking for 413 errors and improve tests
santigracia Nov 1, 2025
9440687
Add option to disable strict import validation
santigracia Nov 1, 2025
1001ec8
Improving performance when retrying rejected payloads due to size + H…
santigracia Nov 2, 2025
f4e397a
Document and handle Mixpanel import endpoint strict modes
santigracia Nov 2, 2025
3a6f110
Enforce non-empty token for MessageBuilder and import
santigracia Nov 8, 2025
85ae829
Update org.json dependency version
santigracia Nov 8, 2025
e120a96
Add and improve JavaDoc comments for public APIs
santigracia Nov 16, 2025
4b4b937
Update README.md
santigracia Nov 17, 2025
5b9f50c
Update src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java
santigracia Nov 17, 2025
4f4abaf
Make timeout and import mode fields thread-safe
santigracia Nov 18, 2025
44c9724
Quick Fix for Javadoc code formatting
santigracia Nov 18, 2025
e9c6381
Update src/main/java/com/mixpanel/mixpanelapi/PayloadChunker.java
santigracia Nov 18, 2025
c4079e7
Make last response fields volatile for thread safety
santigracia Nov 18, 2025
4c3e9c4
Merge branch 'master' of https://github.com/santigracia/mixpanel-java
santigracia Nov 18, 2025
fc50a5c
Apply safety margin to payload chunk size limit
santigracia Nov 25, 2025
6c2024c
Merge branch 'master' into master
santigracia Nov 25, 2025
1215296
Improve error diagnostics for server responses
santigracia Nov 25, 2025
136d18f
Merge branch 'master' of https://github.com/santigracia/mixpanel-java
santigracia Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ Gzip compression can reduce bandwidth usage and improve performance, especially

The library supports importing historical events (events older than 5 days that are not accepted using /track) via the `/import` endpoint. Project token will be used for basic auth.

### Custom Import Batch Size

When importing large events through the `/import` endpoint, you may need to control the batch size to prevent exceeding the server's 10MB uncompressed JSON payload limit. The batch size can be configured between 1 and 2000 (default is 2000):

// Import with default batch size (2000)
MixpanelAPI mixpanel = new MixpanelAPI();

// Import with custom batch size (500)
MixpanelAPI mixpanel = new MixpanelAPI(500);

### Disabling Strict Import Validation

By default, the `/import` endpoint enforces strict validation (strict=1). You can disable strict validation by calling `disableStrictImport()` before delivering import messages. See the [Mixpanel Import API documentation](https://developer.mixpanel.com/reference/import-events) for more details about strict.

MixpanelAPI mixpanel = new MixpanelAPI();
mixpanel.disableStrictImport(); // Set strict=0 to skip validation
mixpanel.deliver(delivery);
### High-Performance JSON Serialization (Optional)

For applications that import large batches of events (e.g., using the `/import` endpoint), the library supports optional high-performance JSON serialization using Jackson. When Jackson is available on the classpath, the library automatically uses it for JSON serialization, providing **up to 5x performance improvement** for large batches.
Expand Down Expand Up @@ -71,6 +88,34 @@ The performance improvement is most noticeable when:

No code changes are required to benefit from this optimization - simply add the Jackson dependency to your project.

### Error Handling

When the Mixpanel server rejects messages, a `MixpanelServerException` is thrown. This exception provides detailed information about the error for debugging and logging:

```java
try {
mixpanel.deliver(delivery);
} catch (MixpanelServerException e) {
// Get the HTTP status code (400, 401, 413, 500, etc.)
int statusCode = e.getHttpStatusCode();

// Get the raw response body from the server
String responseBody = e.getResponseBody();

// Get the list of messages that were rejected
List<JSONObject> failedMessages = e.getBadDeliveryContents();

// The exception message includes status code and response body
System.err.println("Mixpanel error: " + e.getMessage());
}
```

Common HTTP status codes:
- **400**: Bad Request - malformed messages or validation errors (in strict mode)
- **401**: Unauthorized - invalid project token
- **413**: Payload Too Large - request exceeds size limit (automatically retried with chunking)
- **500**: Internal Server Error

## Feature Flags

The Mixpanel Java SDK supports feature flags with both local and remote evaluation modes.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
<version>20250517</version>
</dependency>

<!-- Jackson for high-performance JSON serialization (optional) -->
Expand Down
40 changes: 36 additions & 4 deletions src/demo/java/com/mixpanel/mixpanelapi/demo/MixpanelAPIDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public DeliveryThread(Queue<JSONObject> messages, boolean useGzipCompression) {
mUseGzipCompression = useGzipCompression;
}

public DeliveryThread(Queue<JSONObject> messages, int importBatchSize) {
mMixpanel = new MixpanelAPI(importBatchSize);
mMessageQueue = messages;
mUseGzipCompression = false;
}

@Override
public void run() {
try {
Expand Down Expand Up @@ -88,10 +94,15 @@ public static void main(String[] args)
throws IOException, InterruptedException {
Queue<JSONObject> messages = new ConcurrentLinkedQueue<JSONObject>();
Queue<JSONObject> messagesWithGzip = new ConcurrentLinkedQueue<JSONObject>();
Queue<JSONObject> messagesWithCustomBatch = new ConcurrentLinkedQueue<JSONObject>();

// Create two delivery threads - one without gzip and one with gzip compression
// Create three delivery threads:
// 1. Default batching (50 for events, 2000 for imports)
// 2. With gzip compression (50 for events, 2000 for imports)
// 3. With custom import batch size of 500
DeliveryThread worker = new DeliveryThread(messages, false);
DeliveryThread workerWithGzip = new DeliveryThread(messagesWithGzip, true);
DeliveryThread workerWithCustomBatch = new DeliveryThread(messagesWithCustomBatch, 500);

MessageBuilder messageBuilder = new MessageBuilder(PROJECT_TOKEN);

Expand All @@ -102,6 +113,7 @@ public static void main(String[] args)

worker.start();
workerWithGzip.start();
workerWithCustomBatch.start();

String distinctId = args[0];
BufferedReader inputLines = new BufferedReader(new InputStreamReader(System.in));
Expand All @@ -114,9 +126,13 @@ public static void main(String[] args)
JSONObject nameMessage = messageBuilder.set(distinctId, nameProps);
messages.add(nameMessage);

// Charge the user $2.50 for using the program :)
// Demonstrate deprecated trackCharge method (now logs error instead of tracking revenue)
System.out.println("\n=== Demonstrating deprecated trackCharge method ===");
JSONObject transactionMessage = messageBuilder.trackCharge(distinctId, 2.50, null);
messages.add(transactionMessage);
if (transactionMessage != null) {
messages.add(transactionMessage);
}
System.out.println("trackCharge() returns null and logs an error to stderr\n");

// Import a historical event (30 days ago) with explicit time and $insert_id
long thirtyDaysAgo = System.currentTimeMillis() - (30L * 24L * 60L * 60L * 1000L);
Expand Down Expand Up @@ -161,6 +177,21 @@ public static void main(String[] args)

System.out.println("Added events to gzip compression queue\n");

// Demonstrate custom import batch size
System.out.println("\n=== Demonstrating custom import batch size (500) ===");

// Send import events with custom batch size
long customBatchTime = System.currentTimeMillis() - (45L * 24L * 60L * 60L * 1000L);
Map<String, Object> customBatchProps = new HashMap<String, Object>();
customBatchProps.put("time", customBatchTime);
customBatchProps.put("$insert_id", "custom-batch-" + System.currentTimeMillis());
customBatchProps.put("Batch Size", 500);
customBatchProps.put("Event Type", "Custom Batch Size Import");
JSONObject customBatchEvent = messageBuilder.importEvent(distinctId, "Custom Batch Size Import", new JSONObject(customBatchProps));
messagesWithCustomBatch.add(customBatchEvent);

System.out.println("Added import event to custom batch size queue (batch size: 500)\n");

while((line != null) && (line.length() > 0)) {
System.out.println("SENDING LINE: " + line);
Map<String, String> propMap = new HashMap<String, String>();
Expand All @@ -177,11 +208,12 @@ public static void main(String[] args)
line = inputLines.readLine();
}

while(! messages.isEmpty() || ! messagesWithGzip.isEmpty()) {
while(! messages.isEmpty() || ! messagesWithGzip.isEmpty() || ! messagesWithCustomBatch.isEmpty()) {
Thread.sleep(1000);
}

worker.interrupt();
workerWithGzip.interrupt();
workerWithCustomBatch.interrupt();
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/mixpanel/mixpanelapi/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@
public static final String BASE_ENDPOINT = "https://api.mixpanel.com";
public static final int MAX_MESSAGE_SIZE = 50;
public static final int IMPORT_MAX_MESSAGE_SIZE = 2000;

// Payload size limits for different endpoints
// When a 413 Payload Too Large error is received, payloads are chunked to these limits
public static final int IMPORT_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024; // 10 MB
public static final int TRACK_MAX_PAYLOAD_BYTES = 1 * 1024 * 1024; // 1 MB

// HTTP status codes
public static final int HTTP_400_BAD_REQUEST = 400;
public static final int HTTP_413_PAYLOAD_TOO_LARGE = 413;
}
145 changes: 72 additions & 73 deletions src/main/java/com/mixpanel/mixpanelapi/MessageBuilder.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package com.mixpanel.mixpanelapi;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;

import org.json.JSONArray;
Expand All @@ -22,14 +18,30 @@
*/
public class MessageBuilder {

private static final String ENGAGE_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";

private final String mToken;

/**
* Constructs a MessageBuilder with a Mixpanel project token.
*
* @param token the Mixpanel project token (cannot be null or empty)
* @throws IllegalArgumentException if token is null or empty
*/
public MessageBuilder(String token) {
if (token == null || token.trim().isEmpty()) {
throw new IllegalArgumentException("Token cannot be null or empty");
}
mToken = token;
}

/**
* Returns the token associated with this MessageBuilder.
*
* @return the project token
*/
public String getToken() {
return mToken;
}

/***
* Creates a message tracking an event, for consumption by MixpanelAPI
* See:
Expand Down Expand Up @@ -298,51 +310,46 @@ public JSONObject delete(String distinctId, JSONObject modifiers) {
/**
* For each key and value in the properties argument, adds that amount
* to the associated property in the profile with the given distinct id.
* Supports both integer (Long, Integer) and decimal (Double, Float) increments.
*
* So, to maintain a login count for user 12345, one might run the following code
* at every login:
* <pre>
* {@code
* Map<String, Long> updates = new HashMap<String, Long>();
* updates.put('Logins', 1);
* JSONObject message = messageBuilder.set("12345", updates);
* Map<String, Number> updates = new HashMap<String, Number>();
* updates.put("Logins", 1L);
* updates.put("Rating", 4.5); // decimal value
* JSONObject message = messageBuilder.increment("12345", updates);
* mixpanelApi.sendMessage(message);
* }
* </pre>
* @param distinctId a string uniquely identifying the profile to change,
* for example, a user id of an app, or the hostname of a server. If no profile
* exists for the given id, a new one will be created.
* @param properties a collection of properties to change on the associated profile,
* each associated with a numeric value.
* each associated with a numeric value (Long, Integer, Double, Float, etc.)
* @return user profile increment message for consumption by MixpanelAPI
*/
public JSONObject increment(String distinctId, Map<String, Long> properties) {
public JSONObject increment(String distinctId, Map<String, Number> properties) {
return increment(distinctId, properties, null);
}

/**
* For each key and value in the properties argument, adds that amount
* to the associated property in the profile with the given distinct id.
* So, to maintain a login count for user 12345, one might run the following code
* at every login:
* <pre>
* {@code
* Map<String, Long> updates = new HashMap<String, Long>();
* updates.put('Logins', 1);
* JSONObject message = messageBuilder.set("12345", updates);
* mixpanelApi.sendMessage(message);
* }
* </pre>
* Supports both integer (Long, Integer) and decimal (Double, Float) increments.
*
* @param distinctId a string uniquely identifying the profile to change,
* for example, a user id of an app, or the hostname of a server. If no profile
* exists for the given id, a new one will be created.
* @param properties a collection of properties to change on the associated profile,
* each associated with a numeric value.
* each associated with a numeric value (Long, Integer, Double, Float, etc.)
* @param modifiers Modifiers associated with the update message. (for example "$time" or "$ignore_time").
* this can be null- if non-null, the keys and values in the modifiers
* object will be associated directly with the update.
* @return user profile increment message for consumption by MixpanelAPI
*/
public JSONObject increment(String distinctId, Map<String, Long> properties, JSONObject modifiers) {
public JSONObject increment(String distinctId, Map<String, Number> properties, JSONObject modifiers) {
JSONObject jsonProperties = new JSONObject(properties);
return peopleMessage(distinctId, "$add", jsonProperties, modifiers);
}
Expand Down Expand Up @@ -465,55 +472,6 @@ public JSONObject unset(String distinctId, Collection<String> propertyNames, JSO
return peopleMessage(distinctId, "$unset", propNamesArray, modifiers);
}

/**
* Tracks revenue associated with the given distinctId.
*
* @param distinctId an identifier associated with a profile
* @param amount a double revenue amount. Positive amounts represent income for your business.
* @param properties can be null. If provided, a set of properties to associate with
* the individual transaction.
* @return user profile trackCharge message for consumption by MixpanelAPI
*/
public JSONObject trackCharge(String distinctId, double amount, JSONObject properties) {
return trackCharge(distinctId, amount, properties, null);
}

/**
* Tracks revenue associated with the given distinctId.
*
* @param distinctId an identifier associated with a profile
* @param amount a double revenue amount. Positive amounts represent income for your business.
* @param properties can be null. If provided, a set of properties to associate with
* the individual transaction.
* @param modifiers can be null. If provided, the keys and values in the object will
* be merged as modifiers associated with the update message (for example, "$time" or "$ignore_time")
* @return user profile trackCharge message for consumption by MixpanelAPI
*/
public JSONObject trackCharge(String distinctId, double amount, JSONObject properties, JSONObject modifiers) {
JSONObject transactionValue = new JSONObject();
JSONObject appendProperties = new JSONObject();
try {
transactionValue.put("$amount", amount);
DateFormat dateFormat = new SimpleDateFormat(ENGAGE_DATE_FORMAT);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
transactionValue.put("$time", dateFormat.format(new Date()));

if (null != properties) {
for (Iterator<?> iter = properties.keys(); iter.hasNext();) {
String key = (String) iter.next();
transactionValue.put(key, properties.get(key));
}
}

appendProperties.put("$transactions", transactionValue);

return this.append(distinctId, appendProperties, modifiers);
} catch (JSONException e) {
e.printStackTrace();
throw new RuntimeException("Cannot create trackCharge message", e);
}
}

/**
* Formats a generic user profile message.
* Use of this method requires familiarity with the underlying Mixpanel HTTP API,
Expand Down Expand Up @@ -860,4 +818,45 @@ public JSONObject groupMessage(String groupKey, String groupId, String actionTyp
}
}

/**
* @deprecated The trackCharge() method is deprecated. The old version of Mixpanel's Revenue analysis UI
* has been replaced by a newer suite of analysis tools which don't depend on profile properties.
* See https://docs.mixpanel.com/docs/features/revenue_analytics for more information.
*
* This method now only logs an error and returns null. It no longer sets a profile property or produces any other change.
*
* @param distinctId an identifier associated with a profile
* @param amount a double revenue amount (deprecated - no longer used)
* @param properties properties associated with the transaction (deprecated - no longer used)
* @return null
*/
@Deprecated
public JSONObject trackCharge(String distinctId, double amount, JSONObject properties) {
System.err.println("ERROR: The trackCharge() method is deprecated and no longer functional. " +
"The old version of Mixpanel's Revenue analysis UI has been replaced by a newer suite of analysis tools. " +
"See https://docs.mixpanel.com/docs/features/revenue_analytics for more information.");
return null;
}

/**
* @deprecated The trackCharge() method is deprecated. The old version of Mixpanel's Revenue analysis UI
* has been replaced by a newer suite of analysis tools which don't depend on profile properties.
* See https://docs.mixpanel.com/docs/features/revenue_analytics for more information.
*
* This method now only logs an error and returns null. It no longer sets a profile property or produces any other change.
*
* @param distinctId an identifier associated with a profile
* @param amount a double revenue amount (deprecated - no longer used)
* @param properties properties associated with the transaction (deprecated - no longer used)
* @param modifiers modifiers for the message (deprecated - no longer used)
* @return null
*/
@Deprecated
public JSONObject trackCharge(String distinctId, double amount, JSONObject properties, JSONObject modifiers) {
System.err.println("ERROR: The trackCharge() method is deprecated and no longer functional. " +
"The old version of Mixpanel's Revenue analysis UI has been replaced by a newer suite of analysis tools. " +
"See https://docs.mixpanel.com/docs/features/revenue_analytics for more information.");
return null;
}

}
Loading
Loading