Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,78 @@ 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.

### Service Account Authentication (Recommended)

**Service account authentication is the recommended method for server-side integrations.**

Service accounts provide enhanced security by using unique username/secret pairs instead of relying solely on the project token for authentication:

```java
import com.mixpanel.mixpanelapi.*;

// Create service account credentials
ServiceAccountCredential credentials = new ServiceAccountCredential(
12345L, // project ID
"service-username", // service account username
"service-secret" // service account secret
);

// Configure MixpanelAPI with credentials
MixpanelAPI mixpanel = new MixpanelAPI.Builder()
.credentials(credentials)
.build();

// Use normally - credentials are used for /import endpoint and feature flags
MessageBuilder messages = new MessageBuilder("my token");
JSONObject event = messages.event("user@example.com", "Signup", null);

ClientDelivery delivery = new ClientDelivery();
delivery.addImportMessage(event); // This will use service account auth

mixpanel.deliver(delivery);
```

### Service Accounts with Feature Flags

Service account credentials are passed to MixpanelAPI and automatically used for feature flags:

```java
import com.mixpanel.mixpanelapi.*;
import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig;

// Create service account credentials
ServiceAccountCredential credentials = new ServiceAccountCredential(
12345L, "service-username", "service-secret"
);

// Configure feature flags
LocalFlagsConfig flagsConfig = LocalFlagsConfig.builder()
.projectToken("my-token")
.pollingIntervalSeconds(60)
.build();

// Pass credentials to MixpanelAPI - they'll be used for both /import and feature flags
MixpanelAPI mixpanel = new MixpanelAPI.Builder()
.credentials(credentials) // Credentials used for /import AND feature flags
.flagsConfig(flagsConfig)
.build();

// Feature flag requests will use service account authentication
mixpanel.getLocalFlags().startPollingForDefinitions();
boolean isEnabled = mixpanel.getLocalFlags().isEnabled("new-feature", context);
```

**Important Notes:**
- **Recommended for all new integrations** - Service accounts provide enhanced security
- Service account credentials are used for:
- **`/import` endpoint** - Historical event imports
- **Feature flags** - `/flags` and `/flags/definitions` endpoints
- Regular event tracking (`/track`), people updates (`/engage`), and group updates (`/groups`) continue to use the project token included in the message payload
- When service account credentials are configured:
- Authenticated endpoints use HTTP Basic Authentication with `username:secret`
- The `project_id` is included as a query parameter instead of `token`
- The project token from the message is not used for authentication (but should still be included in tracking events)

### 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. The Jackson extension provides **up to 5x performance improvement** for large batches.
Expand Down
86 changes: 69 additions & 17 deletions src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class MixpanelAPI implements AutoCloseable {
protected final RemoteFlagsProvider mRemoteFlags;
protected final JsonSerializer mJsonSerializer;
protected final OrgJsonSerializer mDefaultJsonSerializer;
protected final ServiceAccountCredential mCredentials;

/**
* Constructs a MixpanelAPI object associated with the production, Mixpanel services.
Expand All @@ -73,7 +74,7 @@ public MixpanelAPI() {
* @param useGzipCompression whether to use gzip compression for network requests
*/
public MixpanelAPI(boolean useGzipCompression) {
this(null, null, null, null, useGzipCompression, null, null, null, null, null, null);
this(null, null, null, null, useGzipCompression, null, null, null, null, null, null, null);
}

/**
Expand Down Expand Up @@ -102,7 +103,7 @@ public MixpanelAPI(RemoteFlagsConfig remoteFlagsConfig) {
* @param remoteFlagsConfig configuration for remote feature flags evaluation (can be null)
*/
private MixpanelAPI(LocalFlagsConfig localFlagsConfig, RemoteFlagsConfig remoteFlagsConfig) {
this(null, null, null, null, false, localFlagsConfig, remoteFlagsConfig, null, null, null, null);
this(null, null, null, null, false, localFlagsConfig, remoteFlagsConfig, null, null, null, null, null);
}

/**
Expand All @@ -115,7 +116,7 @@ private MixpanelAPI(LocalFlagsConfig localFlagsConfig, RemoteFlagsConfig remoteF
* @see #MixpanelAPI()
*/
public MixpanelAPI(String eventsEndpoint, String peopleEndpoint) {
this(eventsEndpoint, peopleEndpoint, null, null, false, null, null, null, null, null, null);
this(eventsEndpoint, peopleEndpoint, null, null, false, null, null, null, null, null, null, null);
}

/**
Expand All @@ -129,7 +130,7 @@ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint) {
* @see #MixpanelAPI()
*/
public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEndpoint) {
this(eventsEndpoint, peopleEndpoint, groupsEndpoint, null, false, null, null, null, null, null, null);
this(eventsEndpoint, peopleEndpoint, groupsEndpoint, null, false, null, null, null, null, null, null, null);
}

/**
Expand All @@ -144,7 +145,7 @@ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEn
* @see #MixpanelAPI()
*/
public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEndpoint, String importEndpoint) {
this(eventsEndpoint, peopleEndpoint, groupsEndpoint, importEndpoint, false, null, null, null, null, null, null);
this(eventsEndpoint, peopleEndpoint, groupsEndpoint, importEndpoint, false, null, null, null, null, null, null, null);
}

/**
Expand All @@ -160,7 +161,7 @@ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEn
* @see #MixpanelAPI()
*/
public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEndpoint, String importEndpoint, boolean useGzipCompression) {
this(eventsEndpoint, peopleEndpoint, groupsEndpoint, importEndpoint, useGzipCompression, null, null, null, null, null, null);
this(eventsEndpoint, peopleEndpoint, groupsEndpoint, importEndpoint, useGzipCompression, null, null, null, null, null, null, null);
}

/**
Expand All @@ -180,7 +181,8 @@ private MixpanelAPI(Builder builder) {
builder.jsonSerializer,
builder.connectTimeout,
builder.readTimeout,
builder.importMaxMessageCount
builder.importMaxMessageCount,
builder.credentials
);
}

Expand All @@ -198,6 +200,7 @@ private MixpanelAPI(Builder builder) {
* @param connectTimeout connection timeout in milliseconds (null uses default)
* @param readTimeout read timeout in milliseconds (null uses default)
* @param importMaxMessageCount maximum messages per import batch (null uses default)
* @param credentials service account credentials for authentication (null uses token-based auth)
*/
private MixpanelAPI(
String eventsEndpoint,
Expand All @@ -210,7 +213,8 @@ private MixpanelAPI(
JsonSerializer jsonSerializer,
Integer connectTimeout,
Integer readTimeout,
Integer importMaxMessageCount
Integer importMaxMessageCount,
ServiceAccountCredential credentials
) {
mEventsEndpoint = eventsEndpoint != null ? eventsEndpoint : Config.BASE_ENDPOINT + "/track";
mPeopleEndpoint = peopleEndpoint != null ? peopleEndpoint : Config.BASE_ENDPOINT + "/engage";
Expand All @@ -221,6 +225,7 @@ private MixpanelAPI(
mReadTimeout = readTimeout != null ? readTimeout : DEFAULT_READ_TIMEOUT_MILLIS;
mImportMaxMessageCount = importMaxMessageCount != null ?
Math.min(importMaxMessageCount, Config.IMPORT_MAX_MESSAGE_SIZE) : Config.IMPORT_MAX_MESSAGE_SIZE;
mCredentials = credentials;
mDefaultJsonSerializer = new OrgJsonSerializer();
if (jsonSerializer != null) {
logger.log(Level.INFO, "Custom JsonSerializer provided: " + jsonSerializer.getClass().getName());
Expand All @@ -231,12 +236,12 @@ private MixpanelAPI(

if (localFlagsConfig != null) {
EventSender eventSender = createEventSender(localFlagsConfig, this);
mLocalFlags = new LocalFlagsProvider(localFlagsConfig, VersionUtil.getVersion(), eventSender);
mLocalFlags = new LocalFlagsProvider(localFlagsConfig, VersionUtil.getVersion(), eventSender, credentials);
mRemoteFlags = null;
} else if (remoteFlagsConfig != null) {
EventSender eventSender = createEventSender(remoteFlagsConfig, this);
mLocalFlags = null;
mRemoteFlags = new RemoteFlagsProvider(remoteFlagsConfig, VersionUtil.getVersion(), eventSender);
mRemoteFlags = new RemoteFlagsProvider(remoteFlagsConfig, VersionUtil.getVersion(), eventSender, credentials);
} else {
mLocalFlags = null;
mRemoteFlags = null;
Expand Down Expand Up @@ -492,30 +497,49 @@ private String dataString(List<JSONObject> messages) {
}

/**
* Sends import data to the /import endpoint with Basic Auth using the project token.
* Sends import data to the /import endpoint with authentication.
* <p>
* When service account credentials are configured, uses HTTP Basic Auth with
* username:secret and includes project_id as a query parameter.
* Otherwise, uses token-based Basic Auth (token as username, empty password).
* </p>
* The /import endpoint requires:
* - JSON content type (not URL-encoded like /track)
* - Basic authentication with token as username and empty password
* - strict=1 parameter for validation
* - Basic authentication (either service account or token)
* - strict parameter for validation mode
*
* @param dataString JSON array of events to import
* @param endpointUrl The import endpoint URL
* @param token The project token for Basic Auth
* @param token The project token (used only when service account credentials are not configured)
* @return true if the server accepted the data
* @throws IOException if there's a network error
*/
/* package */ boolean sendImportData(String dataString, String endpointUrl, String token) throws IOException {
URL endpoint = new URL(endpointUrl);
// If service account credentials are configured, add project_id as query parameter
String finalEndpointUrl = endpointUrl;
if (mCredentials != null) {
String separator = endpointUrl.contains("?") ? "&" : "?";
finalEndpointUrl = endpointUrl + separator + "project_id=" + mCredentials.getProjectId();
}

URL endpoint = new URL(finalEndpointUrl);
HttpURLConnection conn = (HttpURLConnection) endpoint.openConnection();
conn.setReadTimeout(mReadTimeout);
conn.setConnectTimeout(mConnectTimeout);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");

// Add Basic Auth header: username is token, password is empty
// Add Basic Auth header
try {
String authString = token + ":";
String authString;
if (mCredentials != null) {
// Service account auth: username:secret
authString = mCredentials.getUsername() + ":" + mCredentials.getSecret();
} else {
// Token-based auth: token:empty
authString = token + ":";
}
byte[] authBytes = authString.getBytes("utf-8");
String base64Auth = new String(Base64Coder.encode(authBytes));
conn.setRequestProperty("Authorization", "Basic " + base64Auth);
Expand Down Expand Up @@ -734,6 +758,7 @@ public static class Builder {
private Integer connectTimeout;
private Integer readTimeout;
private Integer importMaxMessageCount;
private ServiceAccountCredential credentials;

/**
* Sets the endpoint URL for Mixpanel events messages.
Expand Down Expand Up @@ -859,6 +884,33 @@ public Builder importMaxMessageCount(int importMaxMessageCount) {
return this;
}

/**
* Sets the service account credentials for authentication.
* <p>
* <strong>Recommended:</strong> Service account authentication is the preferred method
* for server-side integrations. Service accounts provide enhanced security by using
* unique username/secret pairs instead of relying solely on the project token for
* authentication.
* </p>
* <p>
* When provided, the /import endpoint will use HTTP Basic Authentication with the
* service account username and secret instead of token-based authentication.
* </p>
* <p>
* Service account credentials are only applied to the /import endpoint (and feature flags).
* Regular event tracking operations (track, people updates, group updates) continue to
* use the project token included in the message payload.
* </p>
*
* @param credentials service account credentials for authentication
* @return this Builder instance for method chaining
* @see ServiceAccountCredential
*/
public Builder credentials(ServiceAccountCredential credentials) {
this.credentials = credentials;
return this;
}

/**
* Builds and returns a new MixpanelAPI instance with the configured settings.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.mixpanel.mixpanelapi;

/**
* Encapsulates service account credentials for server-to-server authentication.
* <p>
* <strong>Recommended:</strong> Service account authentication is the preferred method for
* server-side integrations. Service accounts provide enhanced security by using unique
* username/secret pairs instead of relying solely on the project token for authentication.
* </p>
* <p>
* Service accounts use a project ID, username, and secret for authentication.
* This class ensures all required credential fields are provided together.
* </p>
* <p>
* Service account credentials are only used for the /import endpoint (and feature flags).
* Regular event tracking operations (track, people updates, group updates) use the project
* token provided in the message payload.
* </p>
*
* @see MixpanelAPI.Builder#credentials(ServiceAccountCredential)
*/
public final class ServiceAccountCredential {
private final long projectId;
private final String username;
private final String secret;

/**
* Creates a new ServiceAccountCredential.
*
* @param projectId the Mixpanel project ID
* @param username the service account username
* @param secret the service account secret
* @throws IllegalArgumentException if projectId is invalid or username/secret are null or empty
*/
public ServiceAccountCredential(long projectId, String username, String secret) {
if (projectId <= 0) {
throw new IllegalArgumentException("projectId must be greater than zero");
}
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("username cannot be null or empty");
}
if (secret == null || secret.trim().isEmpty()) {
throw new IllegalArgumentException("secret cannot be null or empty");
}

this.projectId = projectId;
this.username = username;
this.secret = secret;
}

/**
* @return the Mixpanel project ID
*/
public long getProjectId() {
return projectId;
}

/**
* @return the service account username
*/
public String getUsername() {
return username;
}

/**
* @return the service account secret
*/
public String getSecret() {
return secret;
}
}
Loading
Loading