From 18ac9eebec2708d43ddecdc21f631f074207d164 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 26 Nov 2025 10:36:22 -0500 Subject: [PATCH 01/10] Simplify MixpanelApi construction through builder pattern --- .../com/mixpanel/mixpanelapi/MixpanelAPI.java | 178 ++++++++--- .../mixpanel/mixpanelapi/MixpanelAPITest.java | 277 ++++++++++++++++++ 2 files changed, 420 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java index 955d4f6..69018ed 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java +++ b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java @@ -67,7 +67,7 @@ public MixpanelAPI() { * @param useGzipCompression whether to use gzip compression for network requests */ public MixpanelAPI(boolean useGzipCompression) { - this(Config.BASE_ENDPOINT + "/track", Config.BASE_ENDPOINT + "/engage", Config.BASE_ENDPOINT + "/groups", Config.BASE_ENDPOINT + "/import", useGzipCompression, null, null); + this(null, null, null, null, useGzipCompression, null, null); } /** @@ -96,24 +96,7 @@ public MixpanelAPI(RemoteFlagsConfig remoteFlagsConfig) { * @param remoteFlagsConfig configuration for remote feature flags evaluation (can be null) */ private MixpanelAPI(LocalFlagsConfig localFlagsConfig, RemoteFlagsConfig remoteFlagsConfig) { - mEventsEndpoint = Config.BASE_ENDPOINT + "/track"; - mPeopleEndpoint = Config.BASE_ENDPOINT + "/engage"; - mGroupsEndpoint = Config.BASE_ENDPOINT + "/groups"; - mImportEndpoint = Config.BASE_ENDPOINT + "/import"; - mUseGzipCompression = false; - - if (localFlagsConfig != null) { - EventSender eventSender = createEventSender(localFlagsConfig, this); - mLocalFlags = new LocalFlagsProvider(localFlagsConfig, VersionUtil.getVersion(), eventSender); - mRemoteFlags = null; - } else if (remoteFlagsConfig != null) { - EventSender eventSender = createEventSender(remoteFlagsConfig, this); - mLocalFlags = null; - mRemoteFlags = new RemoteFlagsProvider(remoteFlagsConfig, VersionUtil.getVersion(), eventSender); - } else { - mLocalFlags = null; - mRemoteFlags = null; - } + this(null, null, null, null, false, localFlagsConfig, remoteFlagsConfig); } /** @@ -126,7 +109,7 @@ private MixpanelAPI(LocalFlagsConfig localFlagsConfig, RemoteFlagsConfig remoteF * @see #MixpanelAPI() */ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint) { - this(eventsEndpoint, peopleEndpoint, Config.BASE_ENDPOINT + "/groups", Config.BASE_ENDPOINT + "/import", false, null, null); + this(eventsEndpoint, peopleEndpoint, null, null, false, null, null); } /** @@ -140,7 +123,7 @@ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint) { * @see #MixpanelAPI() */ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEndpoint) { - this(eventsEndpoint, peopleEndpoint, groupsEndpoint, Config.BASE_ENDPOINT + "/import", false, null, null); + this(eventsEndpoint, peopleEndpoint, groupsEndpoint, null, false, null, null); } /** @@ -175,24 +158,60 @@ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEn } /** - * Main constructor used by all other constructors. + * Constructs a MixpanelAPI object using a builder. * - * @param eventsEndpoint a URL that will accept Mixpanel events messages - * @param peopleEndpoint a URL that will accept Mixpanel people messages - * @param groupsEndpoint a URL that will accept Mixpanel groups messages - * @param importEndpoint a URL that will accept Mixpanel import messages + * @param builder the Builder instance containing configuration + */ + private MixpanelAPI(Builder builder) { + this( + builder.eventsEndpoint, + builder.peopleEndpoint, + builder.groupsEndpoint, + builder.importEndpoint, + builder.useGzipCompression, + builder.flagsConfig instanceof LocalFlagsConfig ? (LocalFlagsConfig) builder.flagsConfig : null, + builder.flagsConfig instanceof RemoteFlagsConfig ? (RemoteFlagsConfig) builder.flagsConfig : null + ); + } + + /** + * Main private constructor used by all other constructors. + * + * @param eventsEndpoint a URL that will accept Mixpanel events messages (null uses default) + * @param peopleEndpoint a URL that will accept Mixpanel people messages (null uses default) + * @param groupsEndpoint a URL that will accept Mixpanel groups messages (null uses default) + * @param importEndpoint a URL that will accept Mixpanel import messages (null uses default) * @param useGzipCompression whether to use gzip compression for network requests - * @param localFlags optional LocalFlagsProvider for local feature flags (can be null) - * @param remoteFlags optional RemoteFlagsProvider for remote feature flags (can be null) + * @param localFlagsConfig configuration for local feature flags + * @param remoteFlagsConfig configuration for remote feature flags */ - private MixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEndpoint, String importEndpoint, boolean useGzipCompression, LocalFlagsProvider localFlags, RemoteFlagsProvider remoteFlags) { - mEventsEndpoint = eventsEndpoint; - mPeopleEndpoint = peopleEndpoint; - mGroupsEndpoint = groupsEndpoint; - mImportEndpoint = importEndpoint; + private MixpanelAPI( + String eventsEndpoint, + String peopleEndpoint, + String groupsEndpoint, + String importEndpoint, + boolean useGzipCompression, + LocalFlagsConfig localFlagsConfig, + RemoteFlagsConfig remoteFlagsConfig + ) { + mEventsEndpoint = eventsEndpoint != null ? eventsEndpoint : Config.BASE_ENDPOINT + "/track"; + mPeopleEndpoint = peopleEndpoint != null ? peopleEndpoint : Config.BASE_ENDPOINT + "/engage"; + mGroupsEndpoint = groupsEndpoint != null ? groupsEndpoint : Config.BASE_ENDPOINT + "/groups"; + mImportEndpoint = importEndpoint != null ? importEndpoint : Config.BASE_ENDPOINT + "/import"; mUseGzipCompression = useGzipCompression; - mLocalFlags = localFlags; - mRemoteFlags = remoteFlags; + + if (localFlagsConfig != null) { + EventSender eventSender = createEventSender(localFlagsConfig, this); + mLocalFlags = new LocalFlagsProvider(localFlagsConfig, VersionUtil.getVersion(), eventSender); + mRemoteFlags = null; + } else if (remoteFlagsConfig != null) { + EventSender eventSender = createEventSender(remoteFlagsConfig, this); + mLocalFlags = null; + mRemoteFlags = new RemoteFlagsProvider(remoteFlagsConfig, VersionUtil.getVersion(), eventSender); + } else { + mLocalFlags = null; + mRemoteFlags = null; + } } /** @@ -607,4 +626,93 @@ public void close() { } } + /** + * Builder class for constructing a MixpanelAPI instance with optional configuration options. + * + */ + public static class Builder { + private String eventsEndpoint; + private String peopleEndpoint; + private String groupsEndpoint; + private String importEndpoint; + private boolean useGzipCompression; + private BaseFlagsConfig flagsConfig; + + /** + * Sets the endpoint URL for Mixpanel events messages. + * + * @param eventsEndpoint the URL that will accept Mixpanel events messages + * @return this Builder instance for method chaining + */ + public Builder eventsEndpoint(String eventsEndpoint) { + this.eventsEndpoint = eventsEndpoint; + return this; + } + + /** + * Sets the endpoint URL for Mixpanel people messages. + * + * @param peopleEndpoint the URL that will accept Mixpanel people messages + * @return this Builder instance for method chaining + */ + public Builder peopleEndpoint(String peopleEndpoint) { + this.peopleEndpoint = peopleEndpoint; + return this; + } + + /** + * Sets the endpoint URL for Mixpanel groups messages. + * + * @param groupsEndpoint the URL that will accept Mixpanel groups messages + * @return this Builder instance for method chaining + */ + public Builder groupsEndpoint(String groupsEndpoint) { + this.groupsEndpoint = groupsEndpoint; + return this; + } + + /** + * Sets the endpoint URL for Mixpanel import messages. + * + * @param importEndpoint the URL that will accept Mixpanel import messages + * @return this Builder instance for method chaining + */ + public Builder importEndpoint(String importEndpoint) { + this.importEndpoint = importEndpoint; + return this; + } + + /** + * Sets whether to use gzip compression for network requests. + * + * @param useGzipCompression true to enable gzip compression, false otherwise + * @return this Builder instance for method chaining + */ + public Builder useGzipCompression(boolean useGzipCompression) { + this.useGzipCompression = useGzipCompression; + return this; + } + + /** + * Sets the configuration for feature flags evaluation. + * Accepts either LocalFlagsConfig or RemoteFlagsConfig. + * + * @param flagsConfig configuration for feature flags evaluation + * @return this Builder instance for method chaining + */ + public Builder flagsConfig(BaseFlagsConfig flagsConfig) { + this.flagsConfig = flagsConfig; + return this; + } + + /** + * Builds and returns a new MixpanelAPI instance with the configured settings. + * + * @return a new MixpanelAPI instance + */ + public MixpanelAPI build() { + return new MixpanelAPI(this); + } + } + } diff --git a/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java b/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java index d86155d..9f99123 100644 --- a/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java +++ b/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java @@ -15,6 +15,9 @@ import org.json.JSONException; import org.json.JSONObject; +import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig; +import com.mixpanel.mixpanelapi.featureflags.config.RemoteFlagsConfig; + import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; @@ -1119,4 +1122,278 @@ public boolean sendImportData(String dataString, String endpointUrl, String toke } } + /** + * Test builder with no options set uses default values + */ + public void testBuilderWithDefaults() { + // WHEN + MixpanelAPI api = new MixpanelAPI.Builder().build(); + + // THEN + assertEquals(Config.BASE_ENDPOINT + "/track", api.mEventsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/engage", api.mPeopleEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/groups", api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test builder with all options set + */ + public void testBuilderWithAllOptions() { + // GIVEN + String expectedEventsEndpoint = "https://custom.example.com/events"; + String expectedPeopleEndpoint = "https://custom.example.com/people"; + String expectedGroupsEndpoint = "https://custom.example.com/groups"; + String expectedImportEndpoint = "https://custom.example.com/import"; + boolean expectedGzipCompression = true; + LocalFlagsConfig expectedLocalFlagsConfig = + new LocalFlagsConfig.Builder().build(); + + // WHEN + MixpanelAPI api = new MixpanelAPI.Builder() + .eventsEndpoint(expectedEventsEndpoint) + .peopleEndpoint(expectedPeopleEndpoint) + .groupsEndpoint(expectedGroupsEndpoint) + .importEndpoint(expectedImportEndpoint) + .useGzipCompression(expectedGzipCompression) + .flagsConfig(expectedLocalFlagsConfig) + .build(); + + // THEN + assertEquals(expectedEventsEndpoint, api.mEventsEndpoint); + assertEquals(expectedPeopleEndpoint, api.mPeopleEndpoint); + assertEquals(expectedGroupsEndpoint, api.mGroupsEndpoint); + assertEquals(expectedImportEndpoint, api.mImportEndpoint); + assertEquals(expectedGzipCompression, api.mUseGzipCompression); + assertNotNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test builder with LocalFlagsConfig + */ + public void testBuilderWithLocalFlagsConfig() { + LocalFlagsConfig localConfig = + new LocalFlagsConfig.Builder().build(); + + MixpanelAPI api = new MixpanelAPI.Builder() + .flagsConfig(localConfig) + .build(); + + assertNotNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test builder with RemoteFlagsConfig + */ + public void testBuilderWithRemoteFlagsConfig() { + RemoteFlagsConfig remoteConfig = + new RemoteFlagsConfig.Builder().build(); + + MixpanelAPI api = new MixpanelAPI.Builder() + .flagsConfig(remoteConfig) + .build(); + + assertNull(api.mLocalFlags); + assertNotNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test default constructor with no arguments + */ + public void testConstructorNoArgs() { + // WHEN + MixpanelAPI api = new MixpanelAPI(); + + // THEN + assertEquals(Config.BASE_ENDPOINT + "/track", api.mEventsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/engage", api.mPeopleEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/groups", api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with gzip compression parameter + */ + public void testConstructorWithGzipCompression() { + // GIVEN + boolean expectedGzipCompression = true; + + // WHEN + MixpanelAPI api = new MixpanelAPI(expectedGzipCompression); + + // THEN + assertEquals(Config.BASE_ENDPOINT + "/track", api.mEventsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/engage", api.mPeopleEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/groups", api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertEquals(expectedGzipCompression, api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with LocalFlagsConfig + */ + public void testConstructorWithLocalFlagsConfig() { + // GIVEN + LocalFlagsConfig expectedLocalFlagsConfig = + new LocalFlagsConfig.Builder().build(); + + // WHEN + MixpanelAPI api = new MixpanelAPI(expectedLocalFlagsConfig); + + // THEN + assertEquals(Config.BASE_ENDPOINT + "/track", api.mEventsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/engage", api.mPeopleEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/groups", api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNotNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with RemoteFlagsConfig + */ + public void testConstructorWithRemoteFlagsConfig() { + // GIVEN + RemoteFlagsConfig expectedRemoteFlagsConfig = RemoteFlagsConfig.builder().build(); + + // WHEN + MixpanelAPI api = new MixpanelAPI(expectedRemoteFlagsConfig); + + // THEN + assertEquals(Config.BASE_ENDPOINT + "/track", api.mEventsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/engage", api.mPeopleEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/groups", api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNotNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with custom events and people endpoints + */ + public void testConstructorWithTwoEndpoints() { + // GIVEN + String expectedEventsEndpoint = "https://custom.example.com/events"; + String expectedPeopleEndpoint = "https://custom.example.com/people"; + + // WHEN + MixpanelAPI api = new MixpanelAPI(expectedEventsEndpoint, expectedPeopleEndpoint); + + // THEN + assertEquals(expectedEventsEndpoint, api.mEventsEndpoint); + assertEquals(expectedPeopleEndpoint, api.mPeopleEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/groups", api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with custom events, people, and groups endpoints + */ + public void testConstructorWithThreeEndpoints() { + // GIVEN + String expectedEventsEndpoint = "https://custom.example.com/events"; + String expectedPeopleEndpoint = "https://custom.example.com/people"; + String expectedGroupsEndpoint = "https://custom.example.com/groups"; + + // WHEN + MixpanelAPI api = new MixpanelAPI( + expectedEventsEndpoint, + expectedPeopleEndpoint, + expectedGroupsEndpoint + ); + + // THEN + assertEquals(expectedEventsEndpoint, api.mEventsEndpoint); + assertEquals(expectedPeopleEndpoint, api.mPeopleEndpoint); + assertEquals(expectedGroupsEndpoint, api.mGroupsEndpoint); + assertEquals(Config.BASE_ENDPOINT + "/import", api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with custom events, people, groups, and import endpoints + */ + public void testConstructorWithFourEndpoints() { + // GIVEN + String expectedEventsEndpoint = "https://custom.example.com/events"; + String expectedPeopleEndpoint = "https://custom.example.com/people"; + String expectedGroupsEndpoint = "https://custom.example.com/groups"; + String expectedImportEndpoint = "https://custom.example.com/import"; + + // WHEN + MixpanelAPI api = new MixpanelAPI( + expectedEventsEndpoint, + expectedPeopleEndpoint, + expectedGroupsEndpoint, + expectedImportEndpoint + ); + + // THEN + assertEquals(expectedEventsEndpoint, api.mEventsEndpoint); + assertEquals(expectedPeopleEndpoint, api.mPeopleEndpoint); + assertEquals(expectedGroupsEndpoint, api.mGroupsEndpoint); + assertEquals(expectedImportEndpoint, api.mImportEndpoint); + assertFalse(api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } + + /** + * Test constructor with all four endpoints and gzip compression + */ + public void testConstructorWithFourEndpointsAndGzip() { + // GIVEN + String expectedEventsEndpoint = "https://custom.example.com/events"; + String expectedPeopleEndpoint = "https://custom.example.com/people"; + String expectedGroupsEndpoint = "https://custom.example.com/groups"; + String expectedImportEndpoint = "https://custom.example.com/import"; + boolean expectedGzipCompression = true; + + // WHEN + MixpanelAPI api = new MixpanelAPI( + expectedEventsEndpoint, + expectedPeopleEndpoint, + expectedGroupsEndpoint, + expectedImportEndpoint, + expectedGzipCompression + ); + + // THEN + assertEquals(expectedEventsEndpoint, api.mEventsEndpoint); + assertEquals(expectedPeopleEndpoint, api.mPeopleEndpoint); + assertEquals(expectedGroupsEndpoint, api.mGroupsEndpoint); + assertEquals(expectedImportEndpoint, api.mImportEndpoint); + assertEquals(expectedGzipCompression, api.mUseGzipCompression); + assertNull(api.mLocalFlags); + assertNull(api.mRemoteFlags); + api.close(); + } } From 9e31e2c4d70d0140f6af1b28e80bca55c8283b39 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 26 Nov 2025 12:19:45 -0500 Subject: [PATCH 02/10] Add JsonSerializer option on MixpanelApi --- .github/workflows/release.yml | 58 +++- README.md | 49 ++-- mixpanel-java-extension-jackson/README.md | 53 ++++ mixpanel-java-extension-jackson/pom.xml | 144 ++++++++++ .../internal/JacksonSerializer.java | 20 +- .../internal/JacksonSerializerTest.java | 130 +++++++++ pom.xml | 266 +++++++++--------- .../com/mixpanel/mixpanelapi/MixpanelAPI.java | 56 +++- .../mixpanelapi/internal/JsonSerializer.java | 8 - .../internal/OrgJsonSerializer.java | 5 - .../internal/SerializerFactory.java | 84 ------ .../mixpanel/mixpanelapi/MixpanelAPITest.java | 4 + .../internal/JsonSerializerTest.java | 121 +------- .../internal/SerializerBenchmark.java | 204 -------------- 14 files changed, 603 insertions(+), 599 deletions(-) create mode 100644 mixpanel-java-extension-jackson/README.md create mode 100644 mixpanel-java-extension-jackson/pom.xml rename {src => mixpanel-java-extension-jackson/src}/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java (93%) create mode 100644 mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java delete mode 100644 src/main/java/com/mixpanel/mixpanelapi/internal/SerializerFactory.java delete mode 100644 src/test/java/com/mixpanel/mixpanelapi/internal/SerializerBenchmark.java diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64f777c..75669e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,6 +71,9 @@ jobs: echo "VERSION=$VERSION" >> $GITHUB_ENV echo "version=$VERSION" >> $GITHUB_OUTPUT mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false + cd mixpanel-java-extension-jackson + mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false + cd .. - name: Set version from input id: set-version-input @@ -80,15 +83,32 @@ jobs: echo "VERSION=$VERSION" >> $GITHUB_ENV echo "version=$VERSION" >> $GITHUB_OUTPUT mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false + cd mixpanel-java-extension-jackson + mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false + cd .. - - name: Run tests + - name: Run tests - Main SDK run: mvn clean test - - name: Deploy to Maven Central + - name: Run tests - Jackson Extension + run: | + cd mixpanel-java-extension-jackson + mvn clean test + cd .. + + - name: Deploy Main SDK to Maven Central + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + mvn clean deploy -Dgpg.passphrase=$GPG_PASSPHRASE + + - name: Deploy Jackson Extension to Maven Central env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | + cd mixpanel-java-extension-jackson mvn clean deploy -Dgpg.passphrase=$GPG_PASSPHRASE + cd .. - name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/') @@ -98,7 +118,7 @@ jobs: script: | const releaseBody = `## Mixpanel Java SDK v${process.env.VERSION} - ### Maven + ### Maven - Main SDK \`\`\`xml com.mixpanel @@ -107,11 +127,21 @@ jobs: \`\`\` + ### Maven - Jackson Extension (Optional) + \`\`\`xml + + com.mixpanel + mixpanel-java-extension-jackson + ${process.env.VERSION} + + \`\`\` + ### Changes See [CHANGELOG](https://github.com/mixpanel/mixpanel-java/blob/master/CHANGELOG.md) for details. ### Links - - [Maven Central](https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java/${process.env.VERSION}) + - [Maven Central - Main SDK](https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java/${process.env.VERSION}) + - [Maven Central - Jackson Extension](https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java-extension-jackson/${process.env.VERSION}) - [JavaDoc](http://mixpanel.github.io/mixpanel-java/)`; await github.rest.repos.createRelease({ @@ -133,12 +163,24 @@ jobs: - name: Wait for Maven Central sync run: sleep 300 # Wait 5 minutes for synchronization - - name: Verify artifact on Maven Central + - name: Verify artifacts on Maven Central run: | VERSION=${{ needs.release.outputs.version }} + + # Verify main SDK RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://repo1.maven.org/maven2/com/mixpanel/mixpanel-java/${VERSION}/mixpanel-java-${VERSION}.jar) if [ $RESPONSE -eq 200 ]; then - echo "✅ Artifact successfully published to Maven Central" + echo "✅ Main SDK successfully published to Maven Central" + else + echo "⚠️ Main SDK not yet available on Maven Central (HTTP $RESPONSE)" + fi + + # Verify Jackson extension + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://repo1.maven.org/maven2/com/mixpanel/mixpanel-java-extension-jackson/${VERSION}/mixpanel-java-extension-jackson-${VERSION}.jar) + if [ $RESPONSE -eq 200 ]; then + echo "✅ Jackson extension successfully published to Maven Central" else - echo "⚠️ Artifact not yet available on Maven Central (HTTP $RESPONSE). This is normal - it may take up to 30 minutes to appear." - fi \ No newline at end of file + echo "⚠️ Jackson extension not yet available on Maven Central (HTTP $RESPONSE)" + fi + + echo "Note: Artifacts may take up to 30 minutes to appear on Maven Central" \ No newline at end of file diff --git a/README.md b/README.md index 3386d8c..566e2f2 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ This is the official Mixpanel tracking library for Java. ## Latest Version -##### _May 08, 2024_ - [v1.5.3](https://github.com/mixpanel/mixpanel-java/releases/tag/mixpanel-java-1.5.3) +See the [releases page](https://github.com/mixpanel/mixpanel-java/releases) for the latest version. -``` +```xml com.mixpanel mixpanel-java - 1.5.3 + 1.6.0-flags ``` @@ -33,9 +33,13 @@ are built by `MessageBuilder` objects, and those messages can be consumed by the ### Gzip Compression -The library supports gzip compression for both tracking events (`/track`) and importing historical events (`/import`). To enable gzip compression, pass `true` to the `MixpanelAPI` constructor: +The library supports gzip compression for both tracking events (`/track`) and importing historical events (`/import`). To enable gzip compression, use the builder: - MixpanelAPI mixpanel = new MixpanelAPI(true); // Enable gzip compression +```java +MixpanelAPI mixpanel = new MixpanelAPI.Builder() + .useGzipCompression(true) + .build(); +``` Gzip compression can reduce bandwidth usage and improve performance, especially when sending large batches of events. @@ -45,32 +49,37 @@ The library supports importing historical events (events older than 5 days that ### 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. +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. -To enable high-performance serialization, add the Jackson dependency to your project: +To enable high-performance serialization, add the Jackson extension to your project: ```xml - com.fasterxml.jackson.core - jackson-databind - 2.20.0 + com.mixpanel + mixpanel-java-extension-jackson + 1.6.0-flags ``` +Then configure the MixpanelAPI to use it: + +```java +import com.mixpanel.mixpanelapi.internal.JacksonSerializer; + +MixpanelAPI mixpanel = new MixpanelAPI.Builder() + .jsonSerializer(new JacksonSerializer()) + .build(); +``` + **Key benefits:** -- **Automatic detection**: The library automatically detects and uses Jackson when available -- **Backward compatible**: No code changes required - all public APIs remain unchanged - **Significant performance gains**: 2-5x faster serialization for batches of 50+ messages - **Optimal for `/import`**: Most beneficial when importing large batches (up to 2000 events) -- **Fallback support**: Gracefully falls back to org.json if Jackson is not available The performance improvement is most noticeable when: - Importing historical data via the `/import` endpoint - Sending batches of 50+ events - Processing high-volume event streams -No code changes are required to benefit from this optimization - simply add the Jackson dependency to your project. - ## Feature Flags The Mixpanel Java SDK supports feature flags with both local and remote evaluation modes. @@ -90,7 +99,9 @@ LocalFlagsConfig config = LocalFlagsConfig.builder() .pollingIntervalSeconds(60) .build(); -MixpanelAPI mixpanel = new MixpanelAPI(config); +MixpanelAPI mixpanel = new MixpanelAPI.Builder() + .flagsConfig(config) + .build(); // Start polling for flag definitions mixpanel.getLocalFlags().startPollingForDefinitions(); @@ -127,7 +138,11 @@ RemoteFlagsConfig config = RemoteFlagsConfig.builder() .projectToken("YOUR_PROJECT_TOKEN") .build(); -try (MixpanelAPI mixpanel = new MixpanelAPI(config)) { +MixpanelAPI mixpanel = new MixpanelAPI.Builder() + .flagsConfig(config) + .build(); + +try (mixpanel) { Map context = new HashMap<>(); context.put("distinct_id", "user-456"); diff --git a/mixpanel-java-extension-jackson/README.md b/mixpanel-java-extension-jackson/README.md new file mode 100644 index 0000000..9d49141 --- /dev/null +++ b/mixpanel-java-extension-jackson/README.md @@ -0,0 +1,53 @@ +# Mixpanel Java SDK - Jackson Extension + +High-performance Jackson serializer extension for the Mixpanel Java SDK. This extension provides improved JSON serialization performance for large batch operations. + +## Installation + +Add this dependency to your project: + +### Maven +```xml + + com.mixpanel + mixpanel-java-extension-jackson + 1.6.0-flags + +``` + +### Gradle +```gradle +implementation 'com.mixpanel:mixpanel-java-extension-jackson:1.6.0-flags' +``` + +This extension includes: +- `mixpanel-java` (core SDK) +- `jackson-core` 2.20.0 + +## Usage + +To use the Jackson serializer, pass an instance to the MixpanelAPI builder: + +```java +import com.mixpanel.mixpanelapi.MixpanelAPI; +import com.mixpanel.mixpanelapi.internal.JacksonSerializer; + +MixpanelAPI mixpanel = new MixpanelAPI.Builder() + .jsonSerializer(new JacksonSerializer()) + .build(); +``` + +## Key benefits +- **Significant performance gains**: 2-5x faster serialization for batches of 50+ messages +- **Optimal for `/import`**: Most beneficial when importing large batches (up to 2000 events) + +The performance improvement is most noticeable when: +- Importing historical data via the `/import` endpoint +- Sending batches of 50+ events +- Processing high-volume event streams + +## License + +``` +See LICENSE File for details. +``` \ No newline at end of file diff --git a/mixpanel-java-extension-jackson/pom.xml b/mixpanel-java-extension-jackson/pom.xml new file mode 100644 index 0000000..a3c54a3 --- /dev/null +++ b/mixpanel-java-extension-jackson/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + com.mixpanel + mixpanel-java-extension-jackson + 1.6.0-flags + jar + Mixpanel Java SDK - Jackson Extension + + + + + https://github.com/mixpanel/mixpanel-java + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + scm:git:https://github.com/mixpanel/mixpanel-java.git + scm:git:git@github.com:mixpanel/mixpanel-java.git + https://github.com/mixpanel/mixpanel-java + + + + + mixpanel + Mixpanel, Inc + dev@mixpanel.com + http://www.mixpanel.com + + + + + UTF-8 + 1.8 + 1.8 + 2.20.0 + + + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + mixpanel-java-extension-jackson-${project.version} + + false + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + + + com.mixpanel + mixpanel-java + ${project.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + junit + junit + 4.13.2 + test + + + diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java similarity index 93% rename from src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java rename to mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java index 8646cf9..6dad01e 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java +++ b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java @@ -16,6 +16,16 @@ * High-performance JSON serialization implementation using Jackson's streaming API. * This implementation provides significant performance improvements for large batches * while maintaining compatibility with org.json JSONObjects. + * + *

This class is part of the mixpanel-java-extension-jackson module and requires + * Jackson dependencies to be available on the classpath.

+ * + *

Usage example:

+ *
+ * MixpanelAPI api = new MixpanelAPI.Builder()
+ *     .jsonSerializer(new JacksonSerializer())
+ *     .build();
+ * 
* * @since 1.6.0 */ @@ -23,6 +33,9 @@ public class JacksonSerializer implements JsonSerializer { private final JsonFactory jsonFactory; + /** + * Constructs a new JacksonSerializer with default settings. + */ public JacksonSerializer() { this.jsonFactory = new JsonFactory(); } @@ -53,11 +66,6 @@ public byte[] serializeArrayToBytes(List messages) throws IOExceptio return outputStream.toByteArray(); } - @Override - public String getImplementationName() { - return "Jackson"; - } - /** * Writes a JSON array of messages using the Jackson streaming API. */ @@ -153,4 +161,4 @@ private void writeJsonArray(JsonGenerator generator, JSONArray jsonArray) throws generator.writeEndArray(); } -} \ No newline at end of file +} diff --git a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java new file mode 100644 index 0000000..7045cb6 --- /dev/null +++ b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java @@ -0,0 +1,130 @@ +package com.mixpanel.mixpanelapi.internal; + +import junit.framework.TestCase; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class JacksonSerializerTest extends TestCase { + + public void testJacksonSerializer() throws IOException { + JsonSerializer serializer = new JacksonSerializer(); + + // Test empty list + List messages = new ArrayList<>(); + String result = serializer.serializeArray(messages); + assertEquals("[]", result); + + // Test single message + JSONObject message = new JSONObject(); + message.put("event", "jackson_test"); + message.put("value", 123); + messages = Arrays.asList(message); + + result = serializer.serializeArray(messages); + JSONArray array = new JSONArray(result); + assertEquals(1, array.length()); + JSONObject parsed = array.getJSONObject(0); + assertEquals("jackson_test", parsed.getString("event")); + assertEquals(123, parsed.getInt("value")); + + // Test implementation name + assertEquals("com.mixpanel.mixpanelapi.internal.JacksonSerializer", serializer.getClass().getName()); + } + + public void testJacksonSerializerComplexObject() throws IOException { + JsonSerializer serializer = new JacksonSerializer(); + + JSONObject message = new JSONObject(); + message.put("event", "complex_jackson_event"); + message.put("null_value", JSONObject.NULL); + message.put("boolean_value", false); + message.put("int_value", 42); + message.put("long_value", 9999999999L); + message.put("double_value", 3.14159); + message.put("float_value", 2.5f); + message.put("string_value", "test with \"quotes\" and special chars: \n\t"); + + JSONObject nested = new JSONObject(); + nested.put("level2", new JSONObject().put("level3", "deep value")); + message.put("nested", nested); + + JSONArray array = new JSONArray(); + array.put("string"); + array.put(100); + array.put(false); + array.put(JSONObject.NULL); + array.put(new JSONObject().put("in_array", true)); + message.put("array", array); + + List messages = Arrays.asList(message); + String result = serializer.serializeArray(messages); + + // Verify the result can be parsed back correctly + JSONArray parsedArray = new JSONArray(result); + JSONObject parsed = parsedArray.getJSONObject(0); + + assertEquals("complex_jackson_event", parsed.getString("event")); + assertTrue(parsed.isNull("null_value")); + assertEquals(false, parsed.getBoolean("boolean_value")); + assertEquals(42, parsed.getInt("int_value")); + assertEquals(9999999999L, parsed.getLong("long_value")); + assertEquals(3.14159, parsed.getDouble("double_value"), 0.00001); + assertEquals(2.5f, parsed.getFloat("float_value"), 0.01); + assertEquals("test with \"quotes\" and special chars: \n\t", parsed.getString("string_value")); + + assertEquals("deep value", + parsed.getJSONObject("nested") + .getJSONObject("level2") + .getString("level3")); + + JSONArray parsedInnerArray = parsed.getJSONArray("array"); + assertEquals(5, parsedInnerArray.length()); + assertEquals("string", parsedInnerArray.getString(0)); + assertEquals(100, parsedInnerArray.getInt(1)); + assertEquals(false, parsedInnerArray.getBoolean(2)); + assertTrue(parsedInnerArray.isNull(3)); + assertEquals(true, parsedInnerArray.getJSONObject(4).getBoolean("in_array")); + } + + public void testLargeBatchSerialization() throws IOException { + // Test with a large batch to verify performance doesn't degrade + JsonSerializer serializer = new JacksonSerializer(); + List messages = new ArrayList<>(); + + // Create 2000 messages (max batch size for /import) + for (int i = 0; i < 2000; i++) { + JSONObject message = new JSONObject(); + message.put("event", "batch_event"); + message.put("properties", new JSONObject() + .put("index", i) + .put("timestamp", System.currentTimeMillis()) + .put("data", "Some test data for message " + i)); + messages.add(message); + } + + long startTime = System.currentTimeMillis(); + String result = serializer.serializeArray(messages); + long endTime = System.currentTimeMillis(); + + // Verify the result + assertNotNull(result); + assertTrue(result.startsWith("[")); + assertTrue(result.endsWith("]")); + + // Parse to verify correctness (just check a few) + JSONArray array = new JSONArray(result); + assertEquals(2000, array.length()); + assertEquals("batch_event", array.getJSONObject(0).getString("event")); + assertEquals(0, array.getJSONObject(0).getJSONObject("properties").getInt("index")); + assertEquals(1999, array.getJSONObject(1999).getJSONObject("properties").getInt("index")); + + // Log serialization time for reference + System.out.println("Serialized 2000 messages in " + (endTime - startTime) + + "ms using " + serializer.getClass().getName()); + } +} diff --git a/pom.xml b/pom.xml index d312d09..4c2adc2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,151 +1,143 @@ - - 4.0.0 + + 4.0.0 - com.mixpanel - mixpanel-java - 1.6.0-flags - jar - mixpanel-java - - - + com.mixpanel + mixpanel-java + 1.6.0-flags + jar + mixpanel-java + + + - https://github.com/mixpanel/mixpanel-java - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - A business-friendly OSS license - - - - - scm:git:https://github.com/mixpanel/mixpanel-java.git - scm:git:git@github.com:mixpanel/mixpanel-java.git https://github.com/mixpanel/mixpanel-java - - - - mixpanel - Mixpanel, Inc - dev@mixpanel.com - http://www.mixpanel.com - - + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + scm:git:https://github.com/mixpanel/mixpanel-java.git + scm:git:git@github.com:mixpanel/mixpanel-java.git + https://github.com/mixpanel/mixpanel-java + - - UTF-8 - 1.8 - 1.8 - + + + mixpanel + Mixpanel, Inc + dev@mixpanel.com + http://www.mixpanel.com + + - - - central - https://central.sonatype.com/repository/maven-snapshots/ - - + + UTF-8 + 1.8 + 1.8 + - - - - src/main/resources - true - - **/*.properties - - - + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + - - - - org.sonatype.central - central-publishing-maven-plugin - 0.9.0 - true - - central - mixpanel-java-${project.version} - - false - - + + + + src/main/resources + true + + **/*.properties + + + - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + mixpanel-java-${project.version} + + false + + - - org.apache.maven.plugins - maven-gpg-plugin - 3.2.4 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - - - - - - - + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + - - - junit - junit - 4.13.2 - test - + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + - - org.json - json - 20231013 - + + + junit + junit + 4.13.2 + test + - - - - com.fasterxml.jackson.core - jackson-databind - 2.20.0 - provided - - + + org.json + json + 20231013 + + diff --git a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java index 69018ed..010d6d9 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java +++ b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java @@ -13,6 +13,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.GZIPOutputStream; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -25,7 +26,7 @@ import com.mixpanel.mixpanelapi.featureflags.provider.RemoteFlagsProvider; import com.mixpanel.mixpanelapi.featureflags.util.VersionUtil; import com.mixpanel.mixpanelapi.internal.JsonSerializer; -import com.mixpanel.mixpanelapi.internal.SerializerFactory; +import com.mixpanel.mixpanelapi.internal.OrgJsonSerializer; /** * Simple interface to the Mixpanel tracking API, intended for use in @@ -53,6 +54,7 @@ public class MixpanelAPI implements AutoCloseable { protected final boolean mUseGzipCompression; protected final LocalFlagsProvider mLocalFlags; protected final RemoteFlagsProvider mRemoteFlags; + protected final JsonSerializer mJsonSerializer; /** * Constructs a MixpanelAPI object associated with the production, Mixpanel services. @@ -67,7 +69,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); + this(null, null, null, null, useGzipCompression, null, null, null); } /** @@ -96,7 +98,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); + this(null, null, null, null, false, localFlagsConfig, remoteFlagsConfig, null); } /** @@ -109,7 +111,7 @@ private MixpanelAPI(LocalFlagsConfig localFlagsConfig, RemoteFlagsConfig remoteF * @see #MixpanelAPI() */ public MixpanelAPI(String eventsEndpoint, String peopleEndpoint) { - this(eventsEndpoint, peopleEndpoint, null, null, false, null, null); + this(eventsEndpoint, peopleEndpoint, null, null, false, null, null, null); } /** @@ -123,7 +125,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); + this(eventsEndpoint, peopleEndpoint, groupsEndpoint, null, false, null, null, null); } /** @@ -138,7 +140,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); + this(eventsEndpoint, peopleEndpoint, groupsEndpoint, importEndpoint, false, null, null, null); } /** @@ -154,7 +156,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); + this(eventsEndpoint, peopleEndpoint, groupsEndpoint, importEndpoint, useGzipCompression, null, null, null); } /** @@ -170,7 +172,8 @@ private MixpanelAPI(Builder builder) { builder.importEndpoint, builder.useGzipCompression, builder.flagsConfig instanceof LocalFlagsConfig ? (LocalFlagsConfig) builder.flagsConfig : null, - builder.flagsConfig instanceof RemoteFlagsConfig ? (RemoteFlagsConfig) builder.flagsConfig : null + builder.flagsConfig instanceof RemoteFlagsConfig ? (RemoteFlagsConfig) builder.flagsConfig : null, + builder.jsonSerializer ); } @@ -184,6 +187,7 @@ private MixpanelAPI(Builder builder) { * @param useGzipCompression whether to use gzip compression for network requests * @param localFlagsConfig configuration for local feature flags * @param remoteFlagsConfig configuration for remote feature flags + * @param jsonSerializer custom JSON serializer (null uses default) */ private MixpanelAPI( String eventsEndpoint, @@ -192,13 +196,26 @@ private MixpanelAPI( String importEndpoint, boolean useGzipCompression, LocalFlagsConfig localFlagsConfig, - RemoteFlagsConfig remoteFlagsConfig + RemoteFlagsConfig remoteFlagsConfig, + JsonSerializer jsonSerializer ) { mEventsEndpoint = eventsEndpoint != null ? eventsEndpoint : Config.BASE_ENDPOINT + "/track"; mPeopleEndpoint = peopleEndpoint != null ? peopleEndpoint : Config.BASE_ENDPOINT + "/engage"; mGroupsEndpoint = groupsEndpoint != null ? groupsEndpoint : Config.BASE_ENDPOINT + "/groups"; mImportEndpoint = importEndpoint != null ? importEndpoint : Config.BASE_ENDPOINT + "/import"; mUseGzipCompression = useGzipCompression; + if (jsonSerializer != null) { + String jsonSerializerName; + try { + jsonSerializerName = jsonSerializer.getClass().getName(); + } catch (NullPointerException npe) { + jsonSerializerName = "unknown"; + } + logger.log(Level.INFO, "Custom JsonSerializer provided: " + jsonSerializerName); + mJsonSerializer = jsonSerializer; + } else { + mJsonSerializer = new OrgJsonSerializer(); + } if (localFlagsConfig != null) { EventSender eventSender = createEventSender(localFlagsConfig, this); @@ -427,8 +444,7 @@ private void sendImportMessages(List messages, String endpointUrl) t private String dataString(List messages) { try { - JsonSerializer serializer = SerializerFactory.getInstance(); - return serializer.serializeArray(messages); + return mJsonSerializer.serializeArray(messages); } catch (IOException e) { // Fallback to original implementation if serialization fails logger.log(Level.WARNING, "JSON serialization failed unexpectedly; falling back to org.json implementation", e); @@ -627,8 +643,12 @@ public void close() { } /** - * Builder class for constructing a MixpanelAPI instance with optional configuration options. + * Builder class for constructing a MixpanelAPI instance with optional configuration. * + *

The Builder pattern provides a flexible way to configure MixpanelAPI with various + * options including custom endpoints, gzip compression, feature flags, and JSON serializers.

+ * + * @since 1.6.0 */ public static class Builder { private String eventsEndpoint; @@ -637,6 +657,7 @@ public static class Builder { private String importEndpoint; private boolean useGzipCompression; private BaseFlagsConfig flagsConfig; + private JsonSerializer jsonSerializer; /** * Sets the endpoint URL for Mixpanel events messages. @@ -705,6 +726,17 @@ public Builder flagsConfig(BaseFlagsConfig flagsConfig) { return this; } + /** + * Sets a custom JSON serializer for message serialization. + * + * @param jsonSerializer custom JSON serializer implementation + * @return this Builder instance for method chaining + */ + public Builder jsonSerializer(JsonSerializer jsonSerializer) { + this.jsonSerializer = jsonSerializer; + return this; + } + /** * Builds and returns a new MixpanelAPI instance with the configured settings. * diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java b/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java index f7acf2e..1fc4a9e 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java +++ b/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java @@ -33,12 +33,4 @@ public interface JsonSerializer { * @throws IOException if serialization fails */ byte[] serializeArrayToBytes(List messages) throws IOException; - - /** - * Returns the name of this serializer implementation. - * Useful for logging and debugging purposes. - * - * @return The implementation name (e.g., "org.json", "Jackson") - */ - String getImplementationName(); } \ No newline at end of file diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java b/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java index fa93526..0e20179 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java +++ b/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java @@ -31,9 +31,4 @@ public String serializeArray(List messages) throws IOException { public byte[] serializeArrayToBytes(List messages) throws IOException { return serializeArray(messages).getBytes(StandardCharsets.UTF_8); } - - @Override - public String getImplementationName() { - return "org.json"; - } } \ No newline at end of file diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/SerializerFactory.java b/src/main/java/com/mixpanel/mixpanelapi/internal/SerializerFactory.java deleted file mode 100644 index 9a6d8b7..0000000 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/SerializerFactory.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.mixpanel.mixpanelapi.internal; - -import java.util.logging.Logger; - -/** - * Factory for creating JsonSerializer instances. - * Automatically detects if Jackson is available on the classpath and returns - * the appropriate implementation for optimal performance. - * - * @since 1.6.0 - */ -public class SerializerFactory { - - private static final Logger LOGGER = Logger.getLogger(SerializerFactory.class.getName()); - private static final boolean JACKSON_AVAILABLE; - private static JsonSerializer instance; - - static { - boolean jacksonFound = false; - try { - // Check if Jackson classes are available - Class.forName("com.fasterxml.jackson.core.JsonFactory"); - Class.forName("com.fasterxml.jackson.core.JsonGenerator"); - jacksonFound = true; - LOGGER.info("Jackson detected on classpath. High-performance JSON serialization will be used for large batches."); - } catch (ClassNotFoundException e) { - LOGGER.info("Jackson not found on classpath. Using standard org.json serialization. " + - "Add jackson-databind dependency for improved performance with large batches."); - } - JACKSON_AVAILABLE = jacksonFound; - } - - /** - * Returns a singleton JsonSerializer instance. - * If Jackson is available on the classpath, returns a JacksonSerializer for better performance. - * Otherwise, returns an OrgJsonSerializer for compatibility. - * - * @return A JsonSerializer instance - */ - public static synchronized JsonSerializer getInstance() { - if (instance == null) { - if (JACKSON_AVAILABLE) { - try { - instance = new JacksonSerializer(); - LOGGER.fine("Using Jackson serializer for high performance"); - } catch (NoClassDefFoundError e) { - // Fallback if runtime loading fails - LOGGER.warning("Failed to initialize Jackson serializer, falling back to org.json: " + e.getMessage()); - instance = new OrgJsonSerializer(); - } - } else { - instance = new OrgJsonSerializer(); - LOGGER.fine("Using org.json serializer"); - } - } - return instance; - } - - /** - * Checks if Jackson is available on the classpath. - * - * @return true if Jackson is available, false otherwise - */ - public static boolean isJacksonAvailable() { - return JACKSON_AVAILABLE; - } - - /** - * Gets the name of the current serializer implementation. - * - * @return The implementation name - */ - public static String getCurrentImplementation() { - return getInstance().getImplementationName(); - } - - /** - * For testing purposes - allows resetting the singleton instance. - * Should not be used in production code. - */ - static void resetInstance() { - instance = null; - } -} \ No newline at end of file diff --git a/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java b/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java index 9f99123..98db34d 100644 --- a/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java +++ b/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.zip.GZIPInputStream; +import com.mixpanel.mixpanelapi.internal.OrgJsonSerializer; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -1152,6 +1153,7 @@ public void testBuilderWithAllOptions() { boolean expectedGzipCompression = true; LocalFlagsConfig expectedLocalFlagsConfig = new LocalFlagsConfig.Builder().build(); + OrgJsonSerializer expectedJsonSerializer = new OrgJsonSerializer(); // WHEN MixpanelAPI api = new MixpanelAPI.Builder() @@ -1161,6 +1163,7 @@ public void testBuilderWithAllOptions() { .importEndpoint(expectedImportEndpoint) .useGzipCompression(expectedGzipCompression) .flagsConfig(expectedLocalFlagsConfig) + .jsonSerializer(expectedJsonSerializer) .build(); // THEN @@ -1169,6 +1172,7 @@ public void testBuilderWithAllOptions() { assertEquals(expectedGroupsEndpoint, api.mGroupsEndpoint); assertEquals(expectedImportEndpoint, api.mImportEndpoint); assertEquals(expectedGzipCompression, api.mUseGzipCompression); + assertEquals(expectedJsonSerializer, api.mJsonSerializer); assertNotNull(api.mLocalFlags); assertNull(api.mRemoteFlags); api.close(); diff --git a/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java b/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java index 4feccd3..8a82a8a 100644 --- a/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java +++ b/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java @@ -107,127 +107,12 @@ public void testOrgJsonSerializerComplexObject() throws IOException { public void testOrgJsonSerializerImplementationName() { JsonSerializer serializer = new OrgJsonSerializer(); - assertEquals("org.json", serializer.getImplementationName()); - } - - public void testJacksonSerializerIfAvailable() throws IOException { - // This test will only run if Jackson is on the classpath - boolean jacksonAvailable = false; - try { - Class.forName("com.fasterxml.jackson.core.JsonFactory"); - jacksonAvailable = true; - } catch (ClassNotFoundException e) { - // Jackson not available, skip Jackson-specific tests - } - - if (jacksonAvailable) { - JsonSerializer serializer = new JacksonSerializer(); - - // Test empty list - List messages = new ArrayList<>(); - String result = serializer.serializeArray(messages); - assertEquals("[]", result); - - // Test single message - JSONObject message = new JSONObject(); - message.put("event", "jackson_test"); - message.put("value", 123); - messages = Arrays.asList(message); - - result = serializer.serializeArray(messages); - JSONArray array = new JSONArray(result); - assertEquals(1, array.length()); - JSONObject parsed = array.getJSONObject(0); - assertEquals("jackson_test", parsed.getString("event")); - assertEquals(123, parsed.getInt("value")); - - // Test implementation name - assertEquals("Jackson", serializer.getImplementationName()); - } - } - - public void testJacksonSerializerComplexObjectIfAvailable() throws IOException { - // This test will only run if Jackson is on the classpath - boolean jacksonAvailable = false; - try { - Class.forName("com.fasterxml.jackson.core.JsonFactory"); - jacksonAvailable = true; - } catch (ClassNotFoundException e) { - // Jackson not available, skip Jackson-specific tests - } - - if (jacksonAvailable) { - JsonSerializer serializer = new JacksonSerializer(); - - JSONObject message = new JSONObject(); - message.put("event", "complex_jackson_event"); - message.put("null_value", JSONObject.NULL); - message.put("boolean_value", false); - message.put("int_value", 42); - message.put("long_value", 9999999999L); - message.put("double_value", 3.14159); - message.put("float_value", 2.5f); - message.put("string_value", "test with \"quotes\" and special chars: \n\t"); - - JSONObject nested = new JSONObject(); - nested.put("level2", new JSONObject().put("level3", "deep value")); - message.put("nested", nested); - - JSONArray array = new JSONArray(); - array.put("string"); - array.put(100); - array.put(false); - array.put(JSONObject.NULL); - array.put(new JSONObject().put("in_array", true)); - message.put("array", array); - - List messages = Arrays.asList(message); - String result = serializer.serializeArray(messages); - - // Verify the result can be parsed back correctly - JSONArray parsedArray = new JSONArray(result); - JSONObject parsed = parsedArray.getJSONObject(0); - - assertEquals("complex_jackson_event", parsed.getString("event")); - assertTrue(parsed.isNull("null_value")); - assertEquals(false, parsed.getBoolean("boolean_value")); - assertEquals(42, parsed.getInt("int_value")); - assertEquals(9999999999L, parsed.getLong("long_value")); - assertEquals(3.14159, parsed.getDouble("double_value"), 0.00001); - assertEquals(2.5f, parsed.getFloat("float_value"), 0.01); - assertEquals("test with \"quotes\" and special chars: \n\t", parsed.getString("string_value")); - - assertEquals("deep value", - parsed.getJSONObject("nested") - .getJSONObject("level2") - .getString("level3")); - - JSONArray parsedInnerArray = parsed.getJSONArray("array"); - assertEquals(5, parsedInnerArray.length()); - assertEquals("string", parsedInnerArray.getString(0)); - assertEquals(100, parsedInnerArray.getInt(1)); - assertEquals(false, parsedInnerArray.getBoolean(2)); - assertTrue(parsedInnerArray.isNull(3)); - assertEquals(true, parsedInnerArray.getJSONObject(4).getBoolean("in_array")); - } - } - - public void testSerializerFactoryReturnsCorrectImplementation() { - JsonSerializer serializer = SerializerFactory.getInstance(); - assertNotNull(serializer); - - // Check that we get a valid implementation - String implName = serializer.getImplementationName(); - assertTrue("org.json".equals(implName) || "Jackson".equals(implName)); - - // Verify it's the same instance on subsequent calls (singleton) - JsonSerializer serializer2 = SerializerFactory.getInstance(); - assertSame(serializer, serializer2); + assertEquals("com.mixpanel.mixpanelapi.internal.OrgJsonSerializer", serializer.getClass().getName()); } public void testLargeBatchSerialization() throws IOException { // Test with a large batch to verify performance doesn't degrade - JsonSerializer serializer = SerializerFactory.getInstance(); + JsonSerializer serializer = new OrgJsonSerializer(); List messages = new ArrayList<>(); // Create 2000 messages (max batch size for /import) @@ -259,6 +144,6 @@ public void testLargeBatchSerialization() throws IOException { // Log serialization time for reference System.out.println("Serialized 2000 messages in " + (endTime - startTime) + - "ms using " + serializer.getImplementationName()); + "ms using " + serializer.getClass().getName()); } } \ No newline at end of file diff --git a/src/test/java/com/mixpanel/mixpanelapi/internal/SerializerBenchmark.java b/src/test/java/com/mixpanel/mixpanelapi/internal/SerializerBenchmark.java deleted file mode 100644 index 7c9040d..0000000 --- a/src/test/java/com/mixpanel/mixpanelapi/internal/SerializerBenchmark.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.mixpanel.mixpanelapi.internal; - -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Performance benchmark for comparing JSON serialization implementations. - * Run this class directly to see performance comparisons. - */ -public class SerializerBenchmark { - - private static final int WARMUP_ITERATIONS = 100; - private static final int BENCHMARK_ITERATIONS = 1000; - private static final int[] MESSAGE_COUNTS = {1, 10, 50, 100, 500, 1000, 2000}; - - public static void main(String[] args) { - System.out.println("JSON Serializer Performance Benchmark"); - System.out.println("=====================================\n"); - - // Check if Jackson is available - boolean jacksonAvailable = false; - try { - Class.forName("com.fasterxml.jackson.core.JsonFactory"); - jacksonAvailable = true; - System.out.println("✓ Jackson is available on classpath"); - } catch (ClassNotFoundException e) { - System.out.println("✗ Jackson is NOT available on classpath"); - System.out.println(" Add jackson-databind dependency to enable high-performance serialization\n"); - } - - // Create serializers - JsonSerializer orgJsonSerializer = new OrgJsonSerializer(); - JsonSerializer jacksonSerializer = null; - if (jacksonAvailable) { - try { - jacksonSerializer = new JacksonSerializer(); - } catch (NoClassDefFoundError e) { - System.out.println("Failed to initialize Jackson serializer"); - jacksonAvailable = false; - } - } - - System.out.println("\nRunning benchmarks...\n"); - - // Run benchmarks for different message counts - for (int messageCount : MESSAGE_COUNTS) { - System.out.println("Testing with " + messageCount + " messages:"); - - List messages = createTestMessages(messageCount); - - // Warmup - warmup(orgJsonSerializer, messages); - if (jacksonAvailable) { - warmup(jacksonSerializer, messages); - } - - // Benchmark org.json - long orgJsonTime = benchmark(orgJsonSerializer, messages); - System.out.printf(" org.json: %,d ms (%.2f ms/msg)\n", - orgJsonTime, (double) orgJsonTime / messageCount); - - // Benchmark Jackson if available - if (jacksonAvailable) { - long jacksonTime = benchmark(jacksonSerializer, messages); - System.out.printf(" Jackson: %,d ms (%.2f ms/msg)\n", - jacksonTime, (double) jacksonTime / messageCount); - - // Calculate improvement - double improvement = (double) orgJsonTime / jacksonTime; - System.out.printf(" Speedup: %.2fx faster\n", improvement); - } - - System.out.println(); - } - - // Memory usage comparison for large batch - System.out.println("Memory Usage Test (2000 messages):"); - List largeMessages = createTestMessages(2000); - - Runtime runtime = Runtime.getRuntime(); - System.gc(); - long beforeMemory = runtime.totalMemory() - runtime.freeMemory(); - - // Test org.json memory usage - try { - for (int i = 0; i < 100; i++) { - orgJsonSerializer.serializeArray(largeMessages); - } - } catch (IOException e) { - e.printStackTrace(); - } - - System.gc(); - long afterOrgJson = runtime.totalMemory() - runtime.freeMemory(); - long orgJsonMemory = afterOrgJson - beforeMemory; - System.out.printf(" org.json memory usage: %,d bytes\n", orgJsonMemory); - - if (jacksonAvailable) { - System.gc(); - beforeMemory = runtime.totalMemory() - runtime.freeMemory(); - - try { - for (int i = 0; i < 100; i++) { - jacksonSerializer.serializeArray(largeMessages); - } - } catch (IOException e) { - e.printStackTrace(); - } - - System.gc(); - long afterJackson = runtime.totalMemory() - runtime.freeMemory(); - long jacksonMemory = afterJackson - beforeMemory; - System.out.printf(" Jackson memory usage: %,d bytes\n", jacksonMemory); - System.out.printf(" Memory savings: %,d bytes (%.1f%%)\n", - orgJsonMemory - jacksonMemory, - ((double)(orgJsonMemory - jacksonMemory) / orgJsonMemory) * 100); - } - - System.out.println("\nBenchmark complete!"); - System.out.println("\nRecommendation:"); - if (jacksonAvailable) { - System.out.println("✓ Jackson is providing significant performance improvements."); - System.out.println(" The library will automatically use Jackson for JSON serialization."); - } else { - System.out.println("⚠ Consider adding Jackson dependency for better performance:"); - System.out.println(" "); - System.out.println(" com.fasterxml.jackson.core"); - System.out.println(" jackson-databind"); - System.out.println(" 2.20.0"); - System.out.println(" "); - } - } - - private static List createTestMessages(int count) { - List messages = new ArrayList<>(count); - long timestamp = System.currentTimeMillis(); - - for (int i = 0; i < count; i++) { - JSONObject message = new JSONObject(); - message.put("event", "test_event_" + i); - message.put("$insert_id", "id_" + timestamp + "_" + i); - message.put("time", timestamp - ((long) i * 1000)); - - JSONObject properties = new JSONObject(); - properties.put("$token", "test_token_12345"); - properties.put("distinct_id", "user_" + (i % 100)); - properties.put("mp_lib", "java"); - properties.put("$lib_version", "1.6.0"); - properties.put("index", i); - properties.put("batch_size", count); - properties.put("test_string", "This is a test string with some content to make it more realistic"); - properties.put("test_number", Math.random() * 1000); - properties.put("test_boolean", i % 2 == 0); - - // Add nested object - JSONObject nested = new JSONObject(); - nested.put("nested_value", "value_" + i); - nested.put("nested_number", i * 10); - properties.put("nested_object", nested); - - // Add array - JSONArray array = new JSONArray(); - for (int j = 0; j < 5; j++) { - array.put("item_" + j); - } - properties.put("test_array", array); - - message.put("properties", properties); - messages.add(message); - } - - return messages; - } - - private static void warmup(JsonSerializer serializer, List messages) { - try { - for (int i = 0; i < WARMUP_ITERATIONS; i++) { - serializer.serializeArray(messages); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static long benchmark(JsonSerializer serializer, List messages) { - long startTime = System.nanoTime(); - - try { - for (int i = 0; i < BENCHMARK_ITERATIONS; i++) { - serializer.serializeArray(messages); - } - } catch (IOException e) { - e.printStackTrace(); - return -1; - } - - long endTime = System.nanoTime(); - return (endTime - startTime) / 1_000_000; // Convert to milliseconds - } -} \ No newline at end of file From 42c325f132306203891ebe01c377147e08a3c31a Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 26 Nov 2025 12:32:06 -0500 Subject: [PATCH 03/10] Correct a few unintentional changes --- README.md | 4 +- mixpanel-java-extension-jackson/README.md | 4 +- mixpanel-java-extension-jackson/pom.xml | 2 +- .../internal/JacksonSerializer.java | 10 - pom.xml | 257 +++++++++--------- .../mixpanel/mixpanelapi/MixpanelAPITest.java | 6 +- 6 files changed, 136 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 566e2f2..8679560 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ See the [releases page](https://github.com/mixpanel/mixpanel-java/releases) for com.mixpanel mixpanel-java - 1.6.0-flags + 1.6.0 ``` @@ -57,7 +57,7 @@ To enable high-performance serialization, add the Jackson extension to your proj com.mixpanel mixpanel-java-extension-jackson - 1.6.0-flags + 1.6.0 ``` diff --git a/mixpanel-java-extension-jackson/README.md b/mixpanel-java-extension-jackson/README.md index 9d49141..a1aaf04 100644 --- a/mixpanel-java-extension-jackson/README.md +++ b/mixpanel-java-extension-jackson/README.md @@ -11,13 +11,13 @@ Add this dependency to your project: com.mixpanel mixpanel-java-extension-jackson - 1.6.0-flags + 1.6.0 ``` ### Gradle ```gradle -implementation 'com.mixpanel:mixpanel-java-extension-jackson:1.6.0-flags' +implementation 'com.mixpanel:mixpanel-java-extension-jackson:1.6.0' ``` This extension includes: diff --git a/mixpanel-java-extension-jackson/pom.xml b/mixpanel-java-extension-jackson/pom.xml index a3c54a3..ab29439 100644 --- a/mixpanel-java-extension-jackson/pom.xml +++ b/mixpanel-java-extension-jackson/pom.xml @@ -4,7 +4,7 @@ com.mixpanel mixpanel-java-extension-jackson - 1.6.0-flags + 1.6.0 jar Mixpanel Java SDK - Jackson Extension diff --git a/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java index 6dad01e..21b30d7 100644 --- a/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java +++ b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java @@ -16,16 +16,6 @@ * High-performance JSON serialization implementation using Jackson's streaming API. * This implementation provides significant performance improvements for large batches * while maintaining compatibility with org.json JSONObjects. - * - *

This class is part of the mixpanel-java-extension-jackson module and requires - * Jackson dependencies to be available on the classpath.

- * - *

Usage example:

- *
- * MixpanelAPI api = new MixpanelAPI.Builder()
- *     .jsonSerializer(new JacksonSerializer())
- *     .build();
- * 
* * @since 1.6.0 */ diff --git a/pom.xml b/pom.xml index 4c2adc2..2cb9bf1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,143 +1,142 @@ - - 4.0.0 + + 4.0.0 - com.mixpanel - mixpanel-java - 1.6.0-flags - jar - mixpanel-java - - - + com.mixpanel + mixpanel-java + 1.6.0 + jar + mixpanel-java + + + - https://github.com/mixpanel/mixpanel-java + https://github.com/mixpanel/mixpanel-java - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - A business-friendly OSS license - - + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + - - scm:git:https://github.com/mixpanel/mixpanel-java.git - scm:git:git@github.com:mixpanel/mixpanel-java.git - https://github.com/mixpanel/mixpanel-java - + + scm:git:https://github.com/mixpanel/mixpanel-java.git + scm:git:git@github.com:mixpanel/mixpanel-java.git + https://github.com/mixpanel/mixpanel-java + - - - mixpanel - Mixpanel, Inc - dev@mixpanel.com - http://www.mixpanel.com - - + + + mixpanel + Mixpanel, Inc + dev@mixpanel.com + http://www.mixpanel.com + + - - UTF-8 - 1.8 - 1.8 - + + UTF-8 + 1.8 + 1.8 + - - - central - https://central.sonatype.com/repository/maven-snapshots/ - - + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + - - - - src/main/resources - true - - **/*.properties - - - + + + + src/main/resources + true + + **/*.properties + + + - - - - org.sonatype.central - central-publishing-maven-plugin - 0.9.0 - true - - central - mixpanel-java-${project.version} - - false - - + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + mixpanel-java-${project.version} + + false + + - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + - - org.apache.maven.plugins - maven-gpg-plugin - 3.2.4 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - - - - - - - + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + - - - junit - junit - 4.13.2 - test - + + + junit + junit + 4.13.2 + test + - - org.json - json - 20231013 - - + + org.json + json + 20231013 + + diff --git a/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java b/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java index 98db34d..841054a 100644 --- a/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java +++ b/src/test/java/com/mixpanel/mixpanelapi/MixpanelAPITest.java @@ -11,14 +11,14 @@ import java.util.Set; import java.util.zip.GZIPInputStream; +import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig; +import com.mixpanel.mixpanelapi.featureflags.config.RemoteFlagsConfig; import com.mixpanel.mixpanelapi.internal.OrgJsonSerializer; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig; -import com.mixpanel.mixpanelapi.featureflags.config.RemoteFlagsConfig; - import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; From 36677616a89ea0da6913f7992ca3ae8516a83090 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 26 Nov 2025 12:41:37 -0500 Subject: [PATCH 04/10] Improve Jackson test cases --- mixpanel-java-extension-jackson/pom.xml | 7 ++ .../internal/JacksonSerializerTest.java | 117 ++++++++++++------ 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/mixpanel-java-extension-jackson/pom.xml b/mixpanel-java-extension-jackson/pom.xml index ab29439..a6cc87e 100644 --- a/mixpanel-java-extension-jackson/pom.xml +++ b/mixpanel-java-extension-jackson/pom.xml @@ -140,5 +140,12 @@ 4.13.2 test + + + org.skyscreamer + jsonassert + 1.5.1 + test + diff --git a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java index 7045cb6..54002ce 100644 --- a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java +++ b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java @@ -3,44 +3,54 @@ import junit.framework.TestCase; import org.json.JSONArray; import org.json.JSONObject; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class JacksonSerializerTest extends TestCase { - public void testJacksonSerializer() throws IOException { - JsonSerializer serializer = new JacksonSerializer(); + public void testJacksonMatchesOrgJsonEmptyList() throws Exception { + JsonSerializer jacksonSerializer = new JacksonSerializer(); + JsonSerializer orgSerializer = new OrgJsonSerializer(); - // Test empty list List messages = new ArrayList<>(); - String result = serializer.serializeArray(messages); - assertEquals("[]", result); + String jacksonResult = jacksonSerializer.serializeArray(messages); + String orgResult = orgSerializer.serializeArray(messages); + + assertEquals("[]", jacksonResult); + JSONAssert.assertEquals(orgResult, jacksonResult, JSONCompareMode.STRICT); + } + + public void testJacksonMatchesOrgJsonSingleMessage() throws Exception { + JsonSerializer jacksonSerializer = new JacksonSerializer(); + JsonSerializer orgSerializer = new OrgJsonSerializer(); - // Test single message JSONObject message = new JSONObject(); - message.put("event", "jackson_test"); + message.put("event", "test_event"); message.put("value", 123); - messages = Arrays.asList(message); + List messages = Arrays.asList(message); - result = serializer.serializeArray(messages); - JSONArray array = new JSONArray(result); + String jacksonResult = jacksonSerializer.serializeArray(messages); + String orgResult = orgSerializer.serializeArray(messages); + + jacksonResult = jacksonSerializer.serializeArray(messages); + JSONArray array = new JSONArray(jacksonResult); assertEquals(1, array.length()); JSONObject parsed = array.getJSONObject(0); - assertEquals("jackson_test", parsed.getString("event")); + assertEquals("test_event", parsed.getString("event")); assertEquals(123, parsed.getInt("value")); - - // Test implementation name - assertEquals("com.mixpanel.mixpanelapi.internal.JacksonSerializer", serializer.getClass().getName()); + JSONAssert.assertEquals(orgResult, jacksonResult, JSONCompareMode.STRICT); } - public void testJacksonSerializerComplexObject() throws IOException { - JsonSerializer serializer = new JacksonSerializer(); + public void testJacksonMatchesOrgJsonComplexObject() throws Exception { + JsonSerializer jacksonSerializer = new JacksonSerializer(); + JsonSerializer orgSerializer = new OrgJsonSerializer(); JSONObject message = new JSONObject(); - message.put("event", "complex_jackson_event"); + message.put("event", "complex_event"); message.put("null_value", JSONObject.NULL); message.put("boolean_value", false); message.put("int_value", 42); @@ -62,13 +72,14 @@ public void testJacksonSerializerComplexObject() throws IOException { message.put("array", array); List messages = Arrays.asList(message); - String result = serializer.serializeArray(messages); + + String jacksonResult = jacksonSerializer.serializeArray(messages); + String orgResult = orgSerializer.serializeArray(messages); - // Verify the result can be parsed back correctly - JSONArray parsedArray = new JSONArray(result); + JSONArray parsedArray = new JSONArray(jacksonResult); JSONObject parsed = parsedArray.getJSONObject(0); - assertEquals("complex_jackson_event", parsed.getString("event")); + assertEquals("complex_event", parsed.getString("event")); assertTrue(parsed.isNull("null_value")); assertEquals(false, parsed.getBoolean("boolean_value")); assertEquals(42, parsed.getInt("int_value")); @@ -89,11 +100,38 @@ public void testJacksonSerializerComplexObject() throws IOException { assertEquals(false, parsedInnerArray.getBoolean(2)); assertTrue(parsedInnerArray.isNull(3)); assertEquals(true, parsedInnerArray.getJSONObject(4).getBoolean("in_array")); + // Verify both serializers produce equivalent JSON + JSONAssert.assertEquals(orgResult, jacksonResult, JSONCompareMode.STRICT); + } + + public void testJacksonMatchesOrgJsonMultipleMessages() throws Exception { + JsonSerializer jacksonSerializer = new JacksonSerializer(); + JsonSerializer orgSerializer = new OrgJsonSerializer(); + + List messages = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + JSONObject message = new JSONObject(); + message.put("event", "event_" + i); + message.put("index", i); + message.put("timestamp", System.currentTimeMillis()); + message.put("properties", new JSONObject() + .put("user_id", "user_" + i) + .put("amount", i * 10.5)); + messages.add(message); + } + + String jacksonResult = jacksonSerializer.serializeArray(messages); + String orgResult = orgSerializer.serializeArray(messages); + + // Verify both serializers produce equivalent JSON + JSONAssert.assertEquals(orgResult, jacksonResult, JSONCompareMode.STRICT); } - public void testLargeBatchSerialization() throws IOException { - // Test with a large batch to verify performance doesn't degrade - JsonSerializer serializer = new JacksonSerializer(); + public void testLargeBatchSerialization() throws Exception { + // Test with a large batch to verify performance and that output matches OrgJson + JsonSerializer jacksonSerializer = new JacksonSerializer(); + JsonSerializer orgSerializer = new OrgJsonSerializer(); List messages = new ArrayList<>(); // Create 2000 messages (max batch size for /import) @@ -107,24 +145,29 @@ public void testLargeBatchSerialization() throws IOException { messages.add(message); } - long startTime = System.currentTimeMillis(); - String result = serializer.serializeArray(messages); - long endTime = System.currentTimeMillis(); + long jacksonStart = System.currentTimeMillis(); + String jacksonResult = jacksonSerializer.serializeArray(messages); + long jacksonEnd = System.currentTimeMillis(); + + long orgStart = System.currentTimeMillis(); + String orgResult = orgSerializer.serializeArray(messages); + long orgEnd = System.currentTimeMillis(); - // Verify the result - assertNotNull(result); - assertTrue(result.startsWith("[")); - assertTrue(result.endsWith("]")); + // Verify both produce equivalent JSON + JSONAssert.assertEquals(orgResult, jacksonResult, JSONCompareMode.STRICT); - // Parse to verify correctness (just check a few) - JSONArray array = new JSONArray(result); + // Parse to verify correctness + JSONArray array = new JSONArray(jacksonResult); assertEquals(2000, array.length()); assertEquals("batch_event", array.getJSONObject(0).getString("event")); assertEquals(0, array.getJSONObject(0).getJSONObject("properties").getInt("index")); assertEquals(1999, array.getJSONObject(1999).getJSONObject("properties").getInt("index")); - // Log serialization time for reference - System.out.println("Serialized 2000 messages in " + (endTime - startTime) + - "ms using " + serializer.getClass().getName()); + // Log serialization times for comparison + long jacksonTime = jacksonEnd - jacksonStart; + long orgTime = orgEnd - orgStart; + System.out.println("Jackson serialized 2000 messages in " + jacksonTime + "ms"); + System.out.println("OrgJson serialized 2000 messages in " + orgTime + "ms"); + System.out.println("Performance improvement: " + String.format("%.2fx", (double) orgTime / jacksonTime)); } } From 18e459067687814709962b10db782ab38b435a58 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 26 Nov 2025 15:51:16 -0500 Subject: [PATCH 05/10] Review updates --- README.md | 2 +- .../mixpanelapi/internal/JacksonSerializerTest.java | 7 +++---- src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java | 8 +------- .../mixpanel/mixpanelapi/internal/JsonSerializerTest.java | 5 ----- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8679560..ad6b547 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ MixpanelAPI mixpanel = new MixpanelAPI.Builder() .flagsConfig(config) .build(); -try (mixpanel) { +try (MixpanelAPI mixpanel = new MixpanelAPI.Builder().flagsConfig(config).build()) { Map context = new HashMap<>(); context.put("distinct_id", "user-456"); diff --git a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java index 54002ce..0f999c6 100644 --- a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java +++ b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java @@ -36,7 +36,6 @@ public void testJacksonMatchesOrgJsonSingleMessage() throws Exception { String jacksonResult = jacksonSerializer.serializeArray(messages); String orgResult = orgSerializer.serializeArray(messages); - jacksonResult = jacksonSerializer.serializeArray(messages); JSONArray array = new JSONArray(jacksonResult); assertEquals(1, array.length()); JSONObject parsed = array.getJSONObject(0); @@ -81,7 +80,7 @@ public void testJacksonMatchesOrgJsonComplexObject() throws Exception { assertEquals("complex_event", parsed.getString("event")); assertTrue(parsed.isNull("null_value")); - assertEquals(false, parsed.getBoolean("boolean_value")); + assertFalse(parsed.getBoolean("boolean_value")); assertEquals(42, parsed.getInt("int_value")); assertEquals(9999999999L, parsed.getLong("long_value")); assertEquals(3.14159, parsed.getDouble("double_value"), 0.00001); @@ -97,9 +96,9 @@ public void testJacksonMatchesOrgJsonComplexObject() throws Exception { assertEquals(5, parsedInnerArray.length()); assertEquals("string", parsedInnerArray.getString(0)); assertEquals(100, parsedInnerArray.getInt(1)); - assertEquals(false, parsedInnerArray.getBoolean(2)); + assertFalse(parsedInnerArray.getBoolean(2)); assertTrue(parsedInnerArray.isNull(3)); - assertEquals(true, parsedInnerArray.getJSONObject(4).getBoolean("in_array")); + assertTrue(parsedInnerArray.getJSONObject(4).getBoolean("in_array")); // Verify both serializers produce equivalent JSON JSONAssert.assertEquals(orgResult, jacksonResult, JSONCompareMode.STRICT); } diff --git a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java index 010d6d9..21f7aa3 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java +++ b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java @@ -205,13 +205,7 @@ private MixpanelAPI( mImportEndpoint = importEndpoint != null ? importEndpoint : Config.BASE_ENDPOINT + "/import"; mUseGzipCompression = useGzipCompression; if (jsonSerializer != null) { - String jsonSerializerName; - try { - jsonSerializerName = jsonSerializer.getClass().getName(); - } catch (NullPointerException npe) { - jsonSerializerName = "unknown"; - } - logger.log(Level.INFO, "Custom JsonSerializer provided: " + jsonSerializerName); + logger.log(Level.INFO, "Custom JsonSerializer provided: " + jsonSerializer.getClass().getName()); mJsonSerializer = jsonSerializer; } else { mJsonSerializer = new OrgJsonSerializer(); diff --git a/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java b/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java index 8a82a8a..6acb73f 100644 --- a/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java +++ b/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java @@ -105,11 +105,6 @@ public void testOrgJsonSerializerComplexObject() throws IOException { assertEquals(true, parsedInnerArray.getBoolean(2)); } - public void testOrgJsonSerializerImplementationName() { - JsonSerializer serializer = new OrgJsonSerializer(); - assertEquals("com.mixpanel.mixpanelapi.internal.OrgJsonSerializer", serializer.getClass().getName()); - } - public void testLargeBatchSerialization() throws IOException { // Test with a large batch to verify performance doesn't degrade JsonSerializer serializer = new OrgJsonSerializer(); From cd70a5d8750c8bb5232353035b6f26d165513d0b Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 26 Nov 2025 17:05:20 -0500 Subject: [PATCH 06/10] Publish under the same process --- .github/workflows/release.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75669e1..11e0f87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,16 +96,11 @@ jobs: mvn clean test cd .. - - name: Deploy Main SDK to Maven Central + - name: Deploy to Maven Central env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | mvn clean deploy -Dgpg.passphrase=$GPG_PASSPHRASE - - - name: Deploy Jackson Extension to Maven Central - env: - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: | cd mixpanel-java-extension-jackson mvn clean deploy -Dgpg.passphrase=$GPG_PASSPHRASE cd .. From 8df84c51da627fa148b76d6ee66fbf16d645e16e Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 1 Dec 2025 13:48:37 -0500 Subject: [PATCH 07/10] Update serializer logic --- .../mixpanelapi/internal/JacksonSerializer.java | 15 --------------- .../com/mixpanel/mixpanelapi/MixpanelAPI.java | 11 ++++------- .../mixpanelapi/internal/JsonSerializer.java | 11 ----------- .../mixpanelapi/internal/OrgJsonSerializer.java | 9 +-------- .../mixpanelapi/internal/JsonSerializerTest.java | 3 --- 5 files changed, 5 insertions(+), 44 deletions(-) diff --git a/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java index 21b30d7..d2a8718 100644 --- a/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java +++ b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java @@ -5,10 +5,8 @@ import org.json.JSONArray; import org.json.JSONObject; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; -import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.List; @@ -43,19 +41,6 @@ public String serializeArray(List messages) throws IOException { return writer.toString(); } - @Override - public byte[] serializeArrayToBytes(List messages) throws IOException { - if (messages == null || messages.isEmpty()) { - return "[]".getBytes(StandardCharsets.UTF_8); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (JsonGenerator generator = jsonFactory.createGenerator(outputStream)) { - writeJsonArray(generator, messages); - } - return outputStream.toByteArray(); - } - /** * Writes a JSON array of messages using the Jackson streaming API. */ diff --git a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java index 21f7aa3..00329ea 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java +++ b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java @@ -14,7 +14,6 @@ import java.util.logging.Logger; import java.util.zip.GZIPOutputStream; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -55,6 +54,7 @@ public class MixpanelAPI implements AutoCloseable { protected final LocalFlagsProvider mLocalFlags; protected final RemoteFlagsProvider mRemoteFlags; protected final JsonSerializer mJsonSerializer; + protected final OrgJsonSerializer mDefaultJsonSerializer; /** * Constructs a MixpanelAPI object associated with the production, Mixpanel services. @@ -204,11 +204,12 @@ private MixpanelAPI( mGroupsEndpoint = groupsEndpoint != null ? groupsEndpoint : Config.BASE_ENDPOINT + "/groups"; mImportEndpoint = importEndpoint != null ? importEndpoint : Config.BASE_ENDPOINT + "/import"; mUseGzipCompression = useGzipCompression; + mDefaultJsonSerializer = new OrgJsonSerializer(); if (jsonSerializer != null) { logger.log(Level.INFO, "Custom JsonSerializer provided: " + jsonSerializer.getClass().getName()); mJsonSerializer = jsonSerializer; } else { - mJsonSerializer = new OrgJsonSerializer(); + mJsonSerializer = mDefaultJsonSerializer; } if (localFlagsConfig != null) { @@ -442,11 +443,7 @@ private String dataString(List messages) { } catch (IOException e) { // Fallback to original implementation if serialization fails logger.log(Level.WARNING, "JSON serialization failed unexpectedly; falling back to org.json implementation", e); - JSONArray array = new JSONArray(); - for (JSONObject message:messages) { - array.put(message); - } - return array.toString(); + return mDefaultJsonSerializer.serializeArray(messages); } } diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java b/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java index 1fc4a9e..8a6ffe7 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java +++ b/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java @@ -22,15 +22,4 @@ public interface JsonSerializer { * @throws IOException if serialization fails */ String serializeArray(List messages) throws IOException; - - /** - * Serializes a list of JSONObjects directly to UTF-8 encoded bytes. - * This method can be more efficient for large payloads as it avoids - * the intermediate String creation. - * - * @param messages The list of JSONObjects to serialize - * @return UTF-8 encoded bytes of the JSON array - * @throws IOException if serialization fails - */ - byte[] serializeArrayToBytes(List messages) throws IOException; } \ No newline at end of file diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java b/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java index 0e20179..931bf59 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java +++ b/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java @@ -2,8 +2,6 @@ import org.json.JSONArray; import org.json.JSONObject; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; /** @@ -15,7 +13,7 @@ public class OrgJsonSerializer implements JsonSerializer { @Override - public String serializeArray(List messages) throws IOException { + public String serializeArray(List messages) { if (messages == null || messages.isEmpty()) { return "[]"; } @@ -26,9 +24,4 @@ public String serializeArray(List messages) throws IOException { } return array.toString(); } - - @Override - public byte[] serializeArrayToBytes(List messages) throws IOException { - return serializeArray(messages).getBytes(StandardCharsets.UTF_8); - } } \ No newline at end of file diff --git a/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java b/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java index 6acb73f..9b09511 100644 --- a/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java +++ b/src/test/java/com/mixpanel/mixpanelapi/internal/JsonSerializerTest.java @@ -20,9 +20,6 @@ public void testOrgJsonSerializerEmptyList() throws IOException { String result = serializer.serializeArray(messages); assertEquals("[]", result); - - byte[] bytes = serializer.serializeArrayToBytes(messages); - assertEquals("[]", new String(bytes, "UTF-8")); } public void testOrgJsonSerializerSingleMessage() throws IOException { From 2a10c1aebe36d3ed9e8186088d41963c786104f8 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 1 Dec 2025 15:56:28 -0500 Subject: [PATCH 08/10] Add additional serialization test check --- .../mixpanelapi/internal/JacksonSerializerTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java index 0f999c6..8259180 100644 --- a/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java +++ b/mixpanel-java-extension-jackson/src/test/java/com/mixpanel/mixpanelapi/internal/JacksonSerializerTest.java @@ -6,8 +6,12 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; import java.util.List; public class JacksonSerializerTest extends TestCase { @@ -58,6 +62,14 @@ public void testJacksonMatchesOrgJsonComplexObject() throws Exception { message.put("float_value", 2.5f); message.put("string_value", "test with \"quotes\" and special chars: \n\t"); + //This block is testing different serialized types to ensure it matches OrgJsonSerializer + message.put("big_decimal_value", new BigDecimal("1234567890.123456789")); + message.put("big_integer_value", new BigInteger("12345678901234567890")); + message.put("date", new Date(1704067200000L)); + Calendar testCalendar = Calendar.getInstance(); + testCalendar.setTimeInMillis(1704067200000L); // 2024-01-01 00:00:00 UTC + message.put("calendar", testCalendar); + JSONObject nested = new JSONObject(); nested.put("level2", new JSONObject().put("level3", "deep value")); message.put("nested", nested); From 9fbf2142bc02b99e1fccd2a786ca662c94f88fd2 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 1 Dec 2025 15:58:10 -0500 Subject: [PATCH 09/10] Change version to 1.6.1 --- README.md | 4 ++-- mixpanel-java-extension-jackson/README.md | 4 ++-- mixpanel-java-extension-jackson/pom.xml | 2 +- .../com/mixpanel/mixpanelapi/internal/JacksonSerializer.java | 2 +- pom.xml | 2 +- src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java | 2 +- .../com/mixpanel/mixpanelapi/internal/JsonSerializer.java | 2 +- .../com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ad6b547..a74e2dc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ See the [releases page](https://github.com/mixpanel/mixpanel-java/releases) for com.mixpanel mixpanel-java - 1.6.0 + 1.6.1 ``` @@ -57,7 +57,7 @@ To enable high-performance serialization, add the Jackson extension to your proj com.mixpanel mixpanel-java-extension-jackson - 1.6.0 + 1.6.1 ``` diff --git a/mixpanel-java-extension-jackson/README.md b/mixpanel-java-extension-jackson/README.md index a1aaf04..eb0d216 100644 --- a/mixpanel-java-extension-jackson/README.md +++ b/mixpanel-java-extension-jackson/README.md @@ -11,13 +11,13 @@ Add this dependency to your project: com.mixpanel mixpanel-java-extension-jackson - 1.6.0 + 1.6.1 ``` ### Gradle ```gradle -implementation 'com.mixpanel:mixpanel-java-extension-jackson:1.6.0' +implementation 'com.mixpanel:mixpanel-java-extension-jackson:1.6.1' ``` This extension includes: diff --git a/mixpanel-java-extension-jackson/pom.xml b/mixpanel-java-extension-jackson/pom.xml index a6cc87e..190e475 100644 --- a/mixpanel-java-extension-jackson/pom.xml +++ b/mixpanel-java-extension-jackson/pom.xml @@ -4,7 +4,7 @@ com.mixpanel mixpanel-java-extension-jackson - 1.6.0 + 1.6.1 jar Mixpanel Java SDK - Jackson Extension diff --git a/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java index d2a8718..eba0ffe 100644 --- a/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java +++ b/mixpanel-java-extension-jackson/src/main/java/com/mixpanel/mixpanelapi/internal/JacksonSerializer.java @@ -15,7 +15,7 @@ * This implementation provides significant performance improvements for large batches * while maintaining compatibility with org.json JSONObjects. * - * @since 1.6.0 + * @since 1.6.1 */ public class JacksonSerializer implements JsonSerializer { diff --git a/pom.xml b/pom.xml index 2cb9bf1..a77f301 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.mixpanel mixpanel-java - 1.6.0 + 1.6.1 jar mixpanel-java diff --git a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java index 00329ea..309b9a0 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java +++ b/src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java @@ -639,7 +639,7 @@ public void close() { *

The Builder pattern provides a flexible way to configure MixpanelAPI with various * options including custom endpoints, gzip compression, feature flags, and JSON serializers.

* - * @since 1.6.0 + * @since 1.6.1 */ public static class Builder { private String eventsEndpoint; diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java b/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java index 8a6ffe7..d047b43 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java +++ b/src/main/java/com/mixpanel/mixpanelapi/internal/JsonSerializer.java @@ -10,7 +10,7 @@ * This allows for different implementations (org.json, Jackson) to be used * based on performance requirements and available dependencies. * - * @since 1.6.0 + * @since 1.6.1 */ public interface JsonSerializer { diff --git a/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java b/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java index 931bf59..9d0e268 100644 --- a/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java +++ b/src/main/java/com/mixpanel/mixpanelapi/internal/OrgJsonSerializer.java @@ -8,7 +8,7 @@ * JSON serialization implementation using org.json library. * This is the default implementation that maintains backward compatibility. * - * @since 1.6.0 + * @since 1.6.1 */ public class OrgJsonSerializer implements JsonSerializer { From a0f6fef3ef8e75a54f8e3c5ec230f546c6613a2a Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 1 Dec 2025 17:00:19 -0500 Subject: [PATCH 10/10] pr comment --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index a74e2dc..b8aed0d 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,6 @@ RemoteFlagsConfig config = RemoteFlagsConfig.builder() .projectToken("YOUR_PROJECT_TOKEN") .build(); -MixpanelAPI mixpanel = new MixpanelAPI.Builder() - .flagsConfig(config) - .build(); - try (MixpanelAPI mixpanel = new MixpanelAPI.Builder().flagsConfig(config).build()) { Map context = new HashMap<>(); context.put("distinct_id", "user-456");