diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a4e8d76..35a4e1e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,6 +6,9 @@ on:
pull_request:
branches: [ master ]
+permissions:
+ contents: read
+
jobs:
test:
runs-on: ubuntu-latest
@@ -15,16 +18,16 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up JDK ${{ matrix.java-version }}
- uses: actions/setup-java@v4
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
- name: Cache Maven dependencies
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -42,26 +45,63 @@ jobs:
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-java-${{ matrix.java-version }}
path: target/surefire-reports/
+ test-openfeature-provider:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java-version: ['11', '17', '21']
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Set up JDK ${{ matrix.java-version }}
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
+ with:
+ java-version: ${{ matrix.java-version }}
+ distribution: 'temurin'
+
+ - name: Cache Maven dependencies
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Install core library to local Maven repository
+ run: mvn install -DskipTests -Dgpg.skip=true
+
+ - name: Run OpenFeature provider tests
+ run: cd openfeature-provider && mvn clean test
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: openfeature-test-results-java-${{ matrix.java-version }}
+ path: openfeature-provider/target/surefire-reports/
+
code-quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up JDK 8
- uses: actions/setup-java@v4
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: '8'
distribution: 'temurin'
- name: Cache Maven dependencies
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
diff --git a/.github/workflows/release-openfeature.yml b/.github/workflows/release-openfeature.yml
new file mode 100644
index 0000000..5932285
--- /dev/null
+++ b/.github/workflows/release-openfeature.yml
@@ -0,0 +1,107 @@
+name: Release OpenFeature Provider to Maven Central
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to release (e.g., 0.1.0)'
+ required: true
+ type: string
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.set-version.outputs.version }}
+
+ 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@v4
+ 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
+ id: set-version
+ run: |
+ VERSION=${{ github.event.inputs.version }}
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ cd openfeature-provider
+ mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false
+
+ - name: Build and install Main SDK locally
+ run: mvn clean install -DskipTests -Dgpg.skip=true
+
+ - name: Run tests - OpenFeature Provider
+ run: |
+ cd openfeature-provider
+ mvn clean test
+
+ - name: Deploy OpenFeature Provider to Maven Central
+ env:
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+ run: |
+ cd openfeature-provider
+ mvn clean deploy -Dgpg.passphrase=$GPG_PASSPHRASE
+
+ 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-openfeature/${VERSION}/mixpanel-java-openfeature-${VERSION}.jar)
+ if [ $RESPONSE -eq 200 ]; then
+ echo "✅ OpenFeature Provider successfully published to Maven Central"
+ else
+ echo "⚠️ OpenFeature Provider not yet available on Maven Central (HTTP $RESPONSE)"
+ fi
+
+ echo "Note: Artifacts may take up to 30 minutes to appear on Maven Central"
diff --git a/openfeature-provider/README.md b/openfeature-provider/README.md
new file mode 100644
index 0000000..a781b13
--- /dev/null
+++ b/openfeature-provider/README.md
@@ -0,0 +1,307 @@
+# Mixpanel Java OpenFeature Provider
+
+[](https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java-openfeature)
+[](https://openfeature.dev/)
+[](https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE)
+
+An [OpenFeature](https://openfeature.dev/) provider that integrates Mixpanel's feature flags with the OpenFeature Java SDK. This allows you to use Mixpanel's feature flagging capabilities through OpenFeature's standardized, vendor-agnostic API.
+
+## Overview
+
+This package provides a bridge between Mixpanel's native feature flags implementation and the OpenFeature specification. By using this provider, you can:
+
+- Leverage Mixpanel's powerful feature flag and experimentation platform
+- Use OpenFeature's standardized API for flag evaluation
+- Easily switch between feature flag providers without changing your application code
+- Integrate with OpenFeature's ecosystem of tools and frameworks
+
+## Installation
+
+### Maven
+
+```xml
+
+ com.mixpanel
+ mixpanel-java-openfeature
+ 0.1.0
+
+```
+
+### Gradle
+
+```groovy
+implementation 'com.mixpanel:mixpanel-java-openfeature:0.1.0'
+```
+
+You will also need the OpenFeature Java SDK:
+
+```xml
+
+ dev.openfeature
+ sdk
+ 1.20.1
+
+```
+
+## Quick Start
+
+```java
+import com.mixpanel.openfeature.MixpanelProvider;
+import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import dev.openfeature.sdk.Client;
+
+// 1. Create and register the provider with local evaluation
+MixpanelProvider provider = new MixpanelProvider(
+ "YOUR_PROJECT_TOKEN",
+ new LocalFlagsConfig("YOUR_PROJECT_TOKEN")
+);
+OpenFeatureAPI api = OpenFeatureAPI.getInstance();
+api.setProvider(provider);
+
+// 2. Get a client and evaluate flags
+Client client = api.getClient();
+boolean showNewFeature = client.getBooleanValue("new-feature-flag", false);
+
+if (showNewFeature) {
+ System.out.println("New feature is enabled!");
+}
+```
+
+## Initialization
+
+The provider supports three constructors depending on your evaluation strategy:
+
+### Local Evaluation
+
+Evaluates flags locally using cached flag definitions that are polled from Mixpanel. This is the recommended approach for most server-side applications as it minimizes latency.
+
+```java
+MixpanelProvider provider = new MixpanelProvider(
+ "YOUR_PROJECT_TOKEN",
+ new LocalFlagsConfig("YOUR_PROJECT_TOKEN")
+);
+```
+
+This automatically starts polling for flag definitions in the background.
+
+### Remote Evaluation
+
+Evaluates flags by making a request to Mixpanel's servers for each evaluation. Use this when you need real-time flag values and can tolerate the additional network latency.
+
+```java
+MixpanelProvider provider = new MixpanelProvider(
+ "YOUR_PROJECT_TOKEN",
+ new RemoteFlagsConfig("YOUR_PROJECT_TOKEN")
+);
+```
+
+### Using an Existing MixpanelAPI Instance
+
+If your application already has a `MixpanelAPI` instance configured, you can create the provider from its flags provider directly rather than having the provider create a new one:
+
+```java
+// Your existing MixpanelAPI instance
+MixpanelAPI mixpanel = new MixpanelAPI(new LocalFlagsConfig("YOUR_PROJECT_TOKEN"));
+LocalFlagsProvider localFlags = mixpanel.getLocalFlags();
+localFlags.startPollingForDefinitions();
+
+// Wrap the existing flags provider with OpenFeature
+MixpanelProvider provider = new MixpanelProvider(localFlags);
+```
+
+> **Note:** When using this constructor, `provider.getMixpanel()` will return `null` since the provider does not own the `MixpanelAPI` instance.
+
+## Usage Examples
+
+### Basic Boolean Flag
+
+```java
+Client client = api.getClient();
+
+// Get a boolean flag with a default value
+boolean isFeatureEnabled = client.getBooleanValue("my-feature", false);
+
+if (isFeatureEnabled) {
+ // Show the new feature
+}
+```
+
+### Mixpanel Flag Types and OpenFeature Evaluation Methods
+
+Mixpanel feature flags support three flag types. Use the corresponding OpenFeature evaluation method based on your flag's variant values:
+
+| Mixpanel Flag Type | Variant Values | OpenFeature Method |
+|---|---|---|
+| Feature Gate | `true` / `false` | `getBooleanValue()` |
+| Experiment | boolean, string, number, or JSON object | `getBooleanValue()`, `getStringValue()`, `getIntegerValue()`, `getDoubleValue()`, or `getObjectValue()` |
+| Dynamic Config | JSON object | `getObjectValue()` |
+
+```java
+Client client = api.getClient();
+
+// Feature Gate - boolean variants
+boolean isFeatureOn = client.getBooleanValue("new-checkout", false);
+
+// Experiment with string variants
+String buttonColor = client.getStringValue("button-color-test", "blue");
+
+// Experiment with integer variants
+int maxItems = client.getIntegerValue("max-items", 10);
+
+// Experiment with double variants
+double threshold = client.getDoubleValue("score-threshold", 0.5);
+
+// Dynamic Config - JSON object variants
+Value featureConfig = client.getObjectValue("homepage-layout", new Value("default"));
+```
+
+### Getting Full Resolution Details
+
+If you need additional metadata about the flag evaluation:
+
+```java
+Client client = api.getClient();
+
+FlagEvaluationDetails details = client.getBooleanDetails("my-feature", false);
+
+System.out.println(details.getValue()); // The resolved value
+System.out.println(details.getVariant()); // The variant key from Mixpanel
+System.out.println(details.getReason()); // Why this value was returned
+System.out.println(details.getErrorCode()); // Error code if evaluation failed
+```
+
+### Setting Context
+
+You can pass evaluation context that will be sent to Mixpanel for flag evaluation:
+
+```java
+MutableContext context = new MutableContext();
+context.setTargetingKey("user-123");
+context.add("email", "user@example.com");
+context.add("plan", "premium");
+context.add("beta_tester", true);
+
+boolean value = client.getBooleanValue("premium-feature", false, context);
+```
+
+### Accessing the Underlying MixpanelAPI
+
+If you initialized the provider with a token and config, you can access the underlying `MixpanelAPI` instance for sending events or profile updates:
+
+```java
+MixpanelAPI mixpanel = provider.getMixpanel();
+```
+
+> **Note:** This returns `null` if the provider was constructed with a `BaseFlagsProvider` directly.
+
+### Shutdown
+
+When your application is shutting down, call `shutdown()` to clean up resources:
+
+```java
+provider.shutdown();
+```
+
+## Context Mapping
+
+### All Properties Passed Directly
+
+All properties in the OpenFeature `EvaluationContext` are passed directly to Mixpanel's feature flag evaluation. There is no transformation or filtering of properties.
+
+```java
+// This OpenFeature context...
+MutableContext context = new MutableContext();
+context.setTargetingKey("user-123");
+context.add("email", "user@example.com");
+context.add("plan", "premium");
+
+// ...is passed to Mixpanel as-is for flag evaluation
+```
+
+### targetingKey is Not Special
+
+Unlike some feature flag providers, `targetingKey` is **not** used as a special bucketing key in Mixpanel. It is simply passed as another context property. Mixpanel's server-side configuration determines which properties are used for targeting rules and bucketing.
+
+## Error Handling
+
+The provider uses OpenFeature's standard error codes to indicate issues during flag evaluation:
+
+### PROVIDER_NOT_READY
+
+Returned when flags are evaluated before the local flags provider has finished loading flag definitions. This only applies when using local evaluation.
+
+```java
+FlagEvaluationDetails details = client.getBooleanDetails("my-feature", false);
+
+if (details.getErrorCode() == ErrorCode.PROVIDER_NOT_READY) {
+ System.out.println("Provider still loading, using default value");
+}
+```
+
+### FLAG_NOT_FOUND
+
+Returned when the requested flag does not exist in Mixpanel.
+
+```java
+FlagEvaluationDetails details = client.getBooleanDetails("nonexistent-flag", false);
+
+if (details.getErrorCode() == ErrorCode.FLAG_NOT_FOUND) {
+ System.out.println("Flag does not exist, using default value");
+}
+```
+
+### TYPE_MISMATCH
+
+Returned when the flag value type does not match the requested type. The provider supports some numeric coercions (e.g., a `Long` flag value can be retrieved via `getIntegerValue()` if it fits within `Integer` bounds, and any numeric type can be retrieved via `getDoubleValue()`), but incompatible types will return this error.
+
+```java
+// If 'my-flag' is configured as a string in Mixpanel...
+FlagEvaluationDetails details = client.getBooleanDetails("my-flag", false);
+
+if (details.getErrorCode() == ErrorCode.TYPE_MISMATCH) {
+ System.out.println("Flag is not a boolean, using default value");
+}
+```
+
+## Troubleshooting
+
+### Flags Always Return Default Values
+
+**Possible causes:**
+
+1. **Provider not ready (local evaluation):** The local flags provider may still be loading flag definitions. Flag definitions are polled asynchronously after the provider is created. Allow time for the initial fetch to complete, or check the `PROVIDER_NOT_READY` error code.
+
+2. **Invalid project token:** Verify the token passed to the config matches your Mixpanel project.
+
+3. **Flag not configured:** Verify the flag exists in your Mixpanel project and is enabled.
+
+4. **Network issues:** Check that your application can reach Mixpanel's API servers.
+
+### Type Mismatch Errors
+
+If you are getting `TYPE_MISMATCH` errors:
+
+1. **Check flag configuration:** Verify the flag's value type in Mixpanel matches how you are evaluating it. For example, if the flag value is the string `"true"`, use `getStringValue()`, not `getBooleanValue()`.
+
+2. **Use `getObjectValue()` for complex types:** For JSON objects or arrays, use `getObjectValue()`.
+
+3. **Numeric coercion:** Integer evaluation accepts `Long` and whole-number `Double` values within `Integer` bounds. Double evaluation accepts any numeric type.
+
+### Exposure Events Not Tracking
+
+If `$experiment_started` events are not appearing in Mixpanel:
+
+1. **Verify Mixpanel tracking is working:** Test that other Mixpanel events are being tracked successfully.
+
+2. **Check for duplicate evaluations:** Mixpanel only tracks the first exposure per flag per session to avoid duplicate events.
+
+## Requirements
+
+- Java 8 or higher
+- `mixpanel-java` 1.8.0+
+- OpenFeature SDK 1.20.1+
+
+## License
+
+Apache-2.0
diff --git a/openfeature-provider/RELEASE.md b/openfeature-provider/RELEASE.md
new file mode 100644
index 0000000..02de7a0
--- /dev/null
+++ b/openfeature-provider/RELEASE.md
@@ -0,0 +1,53 @@
+# Releasing the OpenFeature Provider
+
+The OpenFeature provider (`com.mixpanel:mixpanel-java-openfeature`) is published to Maven Central independently from the core SDK.
+
+## Prerequisites
+
+The following GitHub secrets must be configured (shared with the core SDK release workflow):
+
+- `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
+
+## Releasing via GitHub Actions
+
+1. Go to **Actions** > **Release OpenFeature Provider to Maven Central**
+2. Click **Run workflow**
+3. Enter the version to release (e.g., `0.1.0`)
+4. The workflow will:
+ - Build and install the core SDK locally
+ - Run OpenFeature provider tests
+ - Sign artifacts with GPG
+ - Deploy to Maven Central Portal
+ - Wait 5 minutes then verify the artifact is available
+
+After deployment, artifacts are visible at:
+- Deployments: https://central.sonatype.com/publishing/deployments
+- Published: https://central.sonatype.com/artifact/com.mixpanel/mixpanel-java-openfeature
+
+Note: `autoPublish` is set to `false` in `pom.xml`, so you may need to manually publish the deployment from the Sonatype Central Portal.
+
+## Releasing manually
+
+```bash
+# 1. Set the version
+cd openfeature-provider
+mvn versions:set -DnewVersion=0.1.0 -DgenerateBackupPoms=false
+
+# 2. Build and install the core SDK locally
+cd ..
+mvn clean install -DskipTests -Dgpg.skip=true
+
+# 3. Run tests
+cd openfeature-provider
+mvn clean test
+
+# 4. Deploy
+mvn clean deploy -Dgpg.passphrase=
+```
+
+## Versioning
+
+The OpenFeature provider is versioned independently from the core SDK. The current core SDK dependency version is pinned in `pom.xml` — update it when the provider needs features from a newer core SDK release.
diff --git a/openfeature-provider/pom.xml b/openfeature-provider/pom.xml
new file mode 100644
index 0000000..bc7e082
--- /dev/null
+++ b/openfeature-provider/pom.xml
@@ -0,0 +1,152 @@
+
+
+ 4.0.0
+
+ com.mixpanel
+ mixpanel-java-openfeature
+ 0.1.0
+ jar
+ Mixpanel Java SDK - OpenFeature Provider
+
+
+
+
+ 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
+
+
+
+
+ central
+ https://central.sonatype.com/repository/maven-snapshots/
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.9.0
+ true
+
+ central
+ mixpanel-java-openfeature-${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
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
+
+
+ com.mixpanel
+ mixpanel-java
+ 1.8.0
+
+
+
+ dev.openfeature
+ sdk
+ 1.20.1
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+
+
diff --git a/openfeature-provider/src/main/java/com/mixpanel/openfeature/MixpanelProvider.java b/openfeature-provider/src/main/java/com/mixpanel/openfeature/MixpanelProvider.java
new file mode 100644
index 0000000..ecb479b
--- /dev/null
+++ b/openfeature-provider/src/main/java/com/mixpanel/openfeature/MixpanelProvider.java
@@ -0,0 +1,315 @@
+package com.mixpanel.openfeature;
+
+import com.mixpanel.mixpanelapi.MixpanelAPI;
+import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig;
+import com.mixpanel.mixpanelapi.featureflags.config.RemoteFlagsConfig;
+import com.mixpanel.mixpanelapi.featureflags.model.SelectedVariant;
+import com.mixpanel.mixpanelapi.featureflags.provider.BaseFlagsProvider;
+import com.mixpanel.mixpanelapi.featureflags.provider.LocalFlagsProvider;
+import dev.openfeature.sdk.*;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MixpanelProvider implements FeatureProvider {
+
+ private static final Logger logger = Logger.getLogger(MixpanelProvider.class.getName());
+ private final BaseFlagsProvider> flagsProvider;
+ private final MixpanelAPI mixpanel;
+
+ public MixpanelProvider(BaseFlagsProvider> flagsProvider) {
+ this.flagsProvider = flagsProvider;
+ this.mixpanel = null;
+ }
+
+ /**
+ * Constructs a MixpanelProvider with local feature flags evaluation.
+ * Creates a MixpanelAPI instance, extracts the local flags provider,
+ * and automatically starts polling for flag definitions.
+ *
+ * @param token the Mixpanel project token (unused, token is read from config)
+ * @param config configuration for local feature flags evaluation
+ */
+ public MixpanelProvider(String token, LocalFlagsConfig config) {
+ MixpanelAPI api = new MixpanelAPI(config);
+ this.mixpanel = api;
+ LocalFlagsProvider localFlags = api.getLocalFlags();
+ localFlags.startPollingForDefinitions();
+ this.flagsProvider = localFlags;
+ }
+
+ /**
+ * Constructs a MixpanelProvider with remote feature flags evaluation.
+ * Creates a MixpanelAPI instance and extracts the remote flags provider.
+ *
+ * @param token the Mixpanel project token (unused, token is read from config)
+ * @param config configuration for remote feature flags evaluation
+ */
+ public MixpanelProvider(String token, RemoteFlagsConfig config) {
+ MixpanelAPI api = new MixpanelAPI(config);
+ this.mixpanel = api;
+ this.flagsProvider = api.getRemoteFlags();
+ }
+
+ /**
+ * Returns the MixpanelAPI instance used by this provider, or null if the provider
+ * was constructed directly with a BaseFlagsProvider.
+ *
+ * @return the MixpanelAPI instance, or null
+ */
+ public MixpanelAPI getMixpanel() {
+ return mixpanel;
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ return () -> "mixpanel-provider";
+ }
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return evaluate(key, defaultValue, Boolean.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
+ return evaluate(key, defaultValue, String.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
+ return evaluate(key, defaultValue, Integer.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
+ return evaluate(key, defaultValue, Double.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
+ return evaluate(key, defaultValue, ctx,
+ result -> objectToValue(result.getVariantValue()),
+ "Expected Value");
+ }
+
+ @Override
+ public void shutdown() {
+ try {
+ flagsProvider.shutdown();
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Error shutting down Mixpanel flags provider", e);
+ }
+ }
+
+ private ProviderEvaluation evaluate(String key, T defaultValue, Class expectedType, EvaluationContext ctx) {
+ return evaluate(key, defaultValue, ctx,
+ result -> coerce(result.getVariantValue(), expectedType),
+ "Expected " + expectedType.getSimpleName());
+ }
+
+ private ProviderEvaluation evaluate(String key, T defaultValue, EvaluationContext ctx,
+ java.util.function.Function, T> mapper,
+ String typeDescription) {
+ ProviderEvaluation notReadyResult = checkNotReady(defaultValue);
+ if (notReadyResult != null) {
+ return notReadyResult;
+ }
+
+ SelectedVariant