From 307f932d0d896dada6a3c1396fcce60f992a3e76 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Wed, 15 Oct 2025 12:33:23 -0700 Subject: [PATCH 1/2] migrate to central portal and add github workflows --- .github/workflows/ci.yml | 72 ++++++++++++++++ .github/workflows/release.yml | 133 ++++++++++++++++++++++++++++ CLAUDE.md | 158 ++++++++++++++++++++++++++++++++++ README.md | 2 +- pom.xml | 41 ++++----- 5 files changed, 382 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 CLAUDE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..974489d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: ['8', '11', '17', '21'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + + - name: Cache Maven dependencies + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Run tests + run: mvn clean test + + - name: Build project + run: mvn clean package + + - name: Generate JavaDoc + run: mvn javadoc:javadoc + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results-java-${{ matrix.java-version }} + path: target/surefire-reports/ + + code-quality: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + + - name: Cache Maven dependencies + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Check for dependency updates + run: mvn versions:display-dependency-updates \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5d58ba3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,133 @@ +name: Release to Maven Central + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 1.5.4)' + required: true + type: string + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + + - name: Cache Maven dependencies + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Import GPG key + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + echo "$GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import + echo "allow-preset-passphrase" >> ~/.gnupg/gpg-agent.conf + echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf + gpg --list-secret-keys --keyid-format LONG + + - name: Configure Maven settings + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} + run: | + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml << EOF + + + + central + ${MAVEN_CENTRAL_USERNAME} + ${MAVEN_CENTRAL_TOKEN} + + + + EOF + + - name: Set version from tag + if: startsWith(github.ref, 'refs/tags/') + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=$VERSION" >> $GITHUB_ENV + mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false + + - name: Set version from input + if: github.event_name == 'workflow_dispatch' + run: | + VERSION=${{ github.event.inputs.version }} + echo "VERSION=$VERSION" >> $GITHUB_ENV + mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false + + - name: Run tests + run: mvn clean test + + - name: Deploy to Maven Central + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + mvn clean deploy -Dgpg.passphrase=$GPG_PASSPHRASE + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ env.VERSION }} + body: | + ## Mixpanel Java SDK v${{ env.VERSION }} + + ### Maven + ```xml + + com.mixpanel + mixpanel-java + ${{ 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/${{ env.VERSION }}) + - [JavaDoc](http://mixpanel.github.io/mixpanel-java/) + draft: false + prerelease: false + + verify: + needs: release + runs-on: ubuntu-latest + if: success() + + steps: + - name: Wait for Maven Central sync + run: sleep 300 # Wait 5 minutes for synchronization + + - name: Verify artifact on Maven Central + run: | + VERSION=${{ needs.release.outputs.version }} + 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" + 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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb0b356 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,158 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the official Mixpanel tracking library for Java - a production-ready library for sending analytics events and user profile updates to Mixpanel from Java server-side applications. + +## Release Process + +### Quick Commands for Releases + +```bash +# 1. Update version (remove -SNAPSHOT from pom.xml) +mvn versions:set -DnewVersion=1.5.4 + +# 2. Run tests +mvn clean test + +# 3. Deploy to Maven Central Portal +mvn clean deploy + +# 4. After release, prepare next version +mvn versions:set -DnewVersion=1.5.5-SNAPSHOT +``` + +### Key Files +- **RELEASE.md**: Complete release documentation with step-by-step instructions +- **.github/workflows/release.yml**: Automated release workflow triggered by version tags +- **.github/workflows/ci.yml**: Continuous integration for all PRs and master commits + +### Maven Central Portal +- The project uses the new Maven Central Portal (not the deprecated OSSRH) +- Deployments are visible at: https://central.sonatype.com/publishing/deployments +- Published artifacts: https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java + +### Required GitHub Secrets for CI/CD +- `GPG_PRIVATE_KEY`: Base64-encoded GPG private key +- `GPG_PASSPHRASE`: GPG key passphrase +- `MAVEN_CENTRAL_USERNAME`: Maven Central Portal username +- `MAVEN_CENTRAL_TOKEN`: Maven Central Portal token + +## Build and Development Commands + +```bash +# Build the project and create JAR +mvn clean package + +# Run all tests +mvn test + +# Run a specific test class +mvn test -Dtest=MixpanelAPITest + +# Run a specific test method +mvn test -Dtest=MixpanelAPITest#testBuildEventMessage + +# Install to local Maven repository +mvn install + +# Generate JavaDoc +mvn javadoc:javadoc + +# Clean build artifacts +mvn clean + +# Run the demo application (after building) +java -cp target/mixpanel-java-1.5.3.jar:target/classes:lib/json-20231013.jar com.mixpanel.mixpanelapi.demo.MixpanelAPIDemo +``` + +## High-Level Architecture + +### Core Design Pattern +The library implements a **Producer-Consumer** pattern with intentional thread separation: + +1. **Message Production** (`MessageBuilder`): Creates properly formatted JSON messages on application threads +2. **Message Batching** (`ClientDelivery`): Collects messages into efficient batches (max 50 per request) +3. **Message Transmission** (`MixpanelAPI`): Sends batched messages to Mixpanel servers + +This separation allows for flexible threading models - the library doesn't impose any specific concurrency pattern, letting applications control their own threading strategy. + +### Key Architectural Decisions + +**No Built-in Threading**: Unlike some analytics libraries, this one doesn't start background threads. Applications must manage their own async patterns, as demonstrated in `MixpanelAPIDemo` which uses a `ConcurrentLinkedQueue` with producer/consumer threads. + +**Message Format Validation**: `MessageBuilder` performs validation during message construction, throwing `MixpanelMessageException` for malformed data before network transmission. + +**Batch Encoding**: Messages are JSON-encoded, then Base64-encoded, then URL-encoded for HTTP POST transmission. This triple encoding ensures compatibility with Mixpanel's API requirements. + +**Network Communication**: Uses Java's built-in `java.net.URL` and `URLConnection` classes - no external HTTP client dependencies. Connection timeout is 2 seconds, read timeout is 10 seconds. + +### Message Types and Endpoints + +The library supports three message categories, each sent to different endpoints: + +- **Events** (`/track`): User actions and behaviors +- **People** (`/engage`): User profile updates (set, increment, append, etc.) +- **Groups** (`/groups`): Group profile updates + +Each message type has specific JSON structure requirements validated by `MessageBuilder`. + +## Package Structure + +All production code is in the `com.mixpanel.mixpanelapi` package: + +- `MixpanelAPI`: HTTP communication with Mixpanel servers +- `MessageBuilder`: Constructs and validates JSON messages +- `ClientDelivery`: Batches multiple messages for efficient transmission +- `Config`: Contains API endpoints and configuration constants +- `Base64Coder`: Base64 encoding utility (modified third-party code) +- `MixpanelMessageException`: Runtime exception for message format errors +- `MixpanelServerException`: IOException for server rejection responses + +## Testing Approach + +Tests extend JUnit 4's `TestCase` and are located in `MixpanelAPITest`. The test suite covers: + +- Message format validation for all message types +- Property operations (set, setOnce, increment, append, union, remove, unset) +- Large batch delivery behavior +- Encoding verification +- Error conditions and exception handling + +When adding new functionality, follow the existing test patterns - each message type operation has corresponding test methods that verify both the JSON structure and the encoded format. + +## Common Development Tasks + +### Adding a New Message Type +1. Add the message construction method to `MessageBuilder` +2. Validate required fields and structure +3. Add corresponding tests in `MixpanelAPITest` +4. Update `ClientDelivery` if special handling is needed + +### Modifying Network Behavior +Network configuration is centralized in `MixpanelAPI.sendData()`. Connection and read timeouts are hardcoded but could be made configurable by modifying the `Config` class. + +### Debugging Failed Deliveries +The library throws `MixpanelServerException` with the HTTP response code and server message. Check: +1. Token validity in `MessageBuilder` constructor +2. Message size (batches limited to 50 messages) +3. JSON structure using the test suite patterns + +## Dependencies + +The library has minimal dependencies: +- **Production**: `org.json:json:20231013` for JSON manipulation +- **Test**: `junit:junit:4.13.2` for unit testing +- **Java Version**: Requires Java 8 or higher + +## API Patterns to Follow + +When working with this codebase: + +1. **Immutable Messages**: Once created by `MessageBuilder`, JSON messages should not be modified +2. **Fail Fast**: Validate message structure early in `MessageBuilder` rather than during transmission +3. **Preserve Thread Safety**: `MessageBuilder` instances are NOT thread-safe; create one per thread +4. **Batch Appropriately**: `ClientDelivery` handles batching; don't exceed 50 messages per delivery +5. **Exception Handling**: Distinguish between `MixpanelMessageException` (client error) and `MixpanelServerException` (server error) \ No newline at end of file diff --git a/README.md b/README.md index a4d254d..f96e562 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Latest Version ``` -You can alternatively download the library jar directly from Maven [here](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.mixpanel%22%20AND%20a%3A%22mixpanel-java%22). +You can alternatively download the library jar directly from Maven Central [here](https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java). How To Use ---------- diff --git a/pom.xml b/pom.xml index 280b269..be3d320 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.mixpanel mixpanel-java - 1.5.3 + 1.5.4-SNAPSHOT jar mixpanel-java @@ -44,22 +44,24 @@ - ossrh - https://oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 true - ossrh - https://oss.sonatype.org/ - false + central + mixpanel-java-${project.version} + + false @@ -93,7 +95,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.4 + 3.2.4 sign-artifacts @@ -101,22 +103,15 @@ sign + + + --pinentry-mode + loopback + + - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - ossrh - https://oss.sonatype.org/ - true - - - From 80f9a362b10b63e398b20ddfa461ae24cfc77fb4 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Wed, 15 Oct 2025 12:40:56 -0700 Subject: [PATCH 2/2] Fix GitHub Actions deprecation warnings and address PR review comments - Update actions/cache from v3 to v4 in all workflows - Update actions/upload-artifact from v3 to v4 - Replace deprecated actions/create-release with actions/github-script@v7 - Add proper job outputs for version passing between jobs - Fix undefined version reference in verify job --- .github/workflows/ci.yml | 6 ++--- .github/workflows/release.yml | 41 ++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 974489d..a4e8d76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: distribution: 'temurin' - name: Cache Maven dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -42,7 +42,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-java-${{ matrix.java-version }} path: target/surefire-reports/ @@ -61,7 +61,7 @@ jobs: distribution: 'temurin' - name: Cache Maven dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d58ba3..64f777c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,8 @@ on: jobs: release: runs-on: ubuntu-latest + outputs: + version: ${{ steps.set-version.outputs.version }} steps: - name: Checkout code @@ -26,7 +28,7 @@ jobs: distribution: 'temurin' - name: Cache Maven dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -62,17 +64,21 @@ jobs: EOF - name: Set version from tag + id: set-version if: startsWith(github.ref, 'refs/tags/') run: | VERSION=${GITHUB_REF#refs/tags/v} echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false - name: Set version from input + id: set-version-input if: github.event_name == 'workflow_dispatch' run: | VERSION=${{ github.event.inputs.version }} echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false - name: Run tests @@ -86,32 +92,37 @@ jobs: - name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/') - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@v7 with: - tag_name: ${{ github.ref }} - release_name: Release ${{ env.VERSION }} - body: | - ## Mixpanel Java SDK v${{ env.VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const releaseBody = `## Mixpanel Java SDK v${process.env.VERSION} ### Maven - ```xml + \`\`\`xml com.mixpanel mixpanel-java - ${{ env.VERSION }} + ${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/${{ env.VERSION }}) - - [JavaDoc](http://mixpanel.github.io/mixpanel-java/) - draft: false - prerelease: false + - [Maven Central](https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java/${process.env.VERSION}) + - [JavaDoc](http://mixpanel.github.io/mixpanel-java/)`; + + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: context.ref.replace('refs/tags/', ''), + name: `Release ${process.env.VERSION}`, + body: releaseBody, + draft: false, + prerelease: false + }); verify: needs: release