Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
de720a2
feat(analytics): hierarchical groups support
tylerjroach Nov 2, 2026
9b0bbc9
feat(flags): resolve local assignment keys from context_value_template
tylerjroach Nov 2, 2026
72127be
feat(analytics): throw for unsupported groupMessage operations on hie…
tylerjroach Nov 2, 2026
f96f5a3
feat(analytics): add company* profile methods to forCompany builders
tylerjroach Nov 3, 2026
9489510
chore(analytics): point the company-only identity error at the compan…
tylerjroach Nov 3, 2026
1a67734
revert(analytics): drop the company profile methods from for_company …
tylerjroach Nov 3, 2026
ba8300e
feat(analytics): infer the company for $company_id group profile updates
tylerjroach Nov 3, 2026
84fdbb1
feat(analytics): restore the company profile on for_company clients
tylerjroach Nov 3, 2026
651f0d9
feat(analytics): refuse a user from another company on a forCompany b…
tylerjroach Nov 3, 2026
a635c14
feat(analytics): send a user without a company as given on hierarchic…
tylerjroach Nov 3, 2026
9664c61
fix(analytics): keep a user-only profile update within the bound company
tylerjroach Nov 4, 2026
ee93df0
fix(analytics): drop identity keys the identity leaves unset on ident…
tylerjroach Nov 4, 2026
f8291e4
fix(flags): send exposures with an empty distinct_id as before withou…
tylerjroach Nov 4, 2026
7a070cb
fix(flags): keep the context value template regex linear
tylerjroach Nov 4, 2026
da4504e
fix: log master's exposure warning call when hierarchical groups is off
tylerjroach Nov 9, 2026
571302b
fix: keep unset identity keys, refuse unsupported modifier operations…
tylerjroach Nov 9, 2026
71dd951
fix: document that event and importEvent keep identity keys the ident…
tylerjroach Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions src/demo/java/com/mixpanel/mixpanelapi/demo/HierarchicalFlagsDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.mixpanel.mixpanelapi.demo;

import com.mixpanel.mixpanelapi.MixpanelAPI;
import com.mixpanel.mixpanelapi.MixpanelIdentity;
import com.mixpanel.mixpanelapi.ServiceAccountCredential;
import com.mixpanel.mixpanelapi.featureflags.FlagContext;
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 java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Evaluates one feature flag against a real Mixpanel project, once per identity shape,
* so the hierarchical groups changes can be seen end to end rather than only in tests.
*
* Reads credentials from the environment so nothing lands in the shell history:
*
* <pre>
* MIXPANEL_TOKEN required, the project token
* MIXPANEL_FLAG_KEY optional, defaults to hierchical-group-testing
* MIXPANEL_COMPANY_ID optional, defaults to acme
* MIXPANEL_USER_ID optional, defaults to user_123
* MIXPANEL_FLAGS_MODE local or remote, defaults to remote
* MIXPANEL_SA_USERNAME service account, optional for local evaluation
* MIXPANEL_SA_SECRET service account
* MIXPANEL_PROJECT_ID service account
* </pre>
*
* With a service account it evaluates locally; with only a token it evaluates remotely.
*/
public class HierarchicalFlagsDemo {

public static void main(String[] args) throws Exception {
String token = env("MIXPANEL_TOKEN", null);
if (token == null) {
System.err.println("Set MIXPANEL_TOKEN to the project token first.");
System.exit(2);
}
String flagKey = env("MIXPANEL_FLAG_KEY", "hierchical-group-testing");
String companyId = env("MIXPANEL_COMPANY_ID", "acme");
String userId = env("MIXPANEL_USER_ID", "user_123");
boolean local = "local".equalsIgnoreCase(env("MIXPANEL_FLAGS_MODE", "remote"));

showSdkWarnings();

System.out.println("flag " + flagKey);
System.out.println("evaluation " + (local ? "local" : "remote"));
System.out.println("company " + companyId);
System.out.println("user " + userId);
System.out.println();

MixpanelAPI mixpanel = build(token, local);
BaseFlagsProvider<?> flags = local ? mixpanel.getLocalFlags() : mixpanel.getRemoteFlags();
if (local) {
mixpanel.getLocalFlags().startPollingForDefinitions();
Thread.sleep(3000); // let the first definition fetch land
}

evaluate(flags, flagKey, "hierarchical: user + company",
FlagContext.builder()
.identity(MixpanelIdentity.builder().userId(userId).companyId(companyId).build())
.build());

evaluate(flags, flagKey, "hierarchical: company only",
FlagContext.builder()
.identity(MixpanelIdentity.builder().companyId(companyId).build())
.build());

evaluate(flags, flagKey, "anonymous: device only",
FlagContext.builder()
.identity(MixpanelIdentity.builder().deviceId("device_abc").build())
.build());

evaluate(flags, flagKey, "combined distinct id",
FlagContext.builder()
.identity(MixpanelIdentity.builder()
.distinctId("$company:" + companyId + "|$user:" + userId).build())
.build());

evaluate(flags, flagKey, "combined distinct id + hierarchical keys",
FlagContext.builder()
.identity(MixpanelIdentity.builder()
.distinctId("$company:" + companyId + "|$user:" + userId)
.userId(userId).companyId(companyId).build())
.build());

mixpanel.close();
}

private static MixpanelAPI build(String token, boolean local) {
if (!local) {
return new MixpanelAPI(RemoteFlagsConfig.builder()
.projectToken(token)
.hierarchicalGroups(true)
.build());
}
LocalFlagsConfig config = LocalFlagsConfig.builder()
.projectToken(token)
.hierarchicalGroups(true)
.enablePolling(false)
.build();
MixpanelAPI.Builder builder = new MixpanelAPI.Builder().flagsConfig(config);
// The definitions endpoint accepts the project token, so a service account is optional.
if (env("MIXPANEL_SA_USERNAME", null) != null) {
builder.credentials(new ServiceAccountCredential(
Long.parseLong(env("MIXPANEL_PROJECT_ID", "0")),
env("MIXPANEL_SA_USERNAME", null),
env("MIXPANEL_SA_SECRET", null)));
}
return builder.build();
}

private static void evaluate(BaseFlagsProvider<?> flags, String flagKey, String label,
Map<String, Object> context) {
System.out.println(label);
System.out.println(" context " + new LinkedHashMap<>(context));
try {
SelectedVariant<Object> variant =
flags.getVariant(flagKey, new SelectedVariant<Object>((Object) false),
context);
System.out.println(" variant " + variant.getVariantKey()
+ " value=" + variant.getVariantValue()
+ " source=" + variant.getSource());
} catch (Throwable t) {
System.out.println(" THREW " + t.getClass().getSimpleName() + ": " + t.getMessage());
}
System.out.println();
}

/** The SDK reports dropped exposures and missing context through java.util.logging. */
private static void showSdkWarnings() {
Logger root = Logger.getLogger("com.mixpanel");
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
root.addHandler(handler);
root.setLevel(Level.ALL);
root.setUseParentHandlers(false);
}

private static String env(String name, String fallback) {
String value = System.getenv(name);
return value == null || value.isEmpty() ? fallback : value;
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/mixpanel/mixpanelapi/ClientDelivery.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ else if (!messageType.equals("event") && !messageType.equals("people") && !messa
ret = false;
}
}
} catch (JSONException e) {
} catch (Exception e) {
ret = false;
}

Expand Down
Loading
Loading