Skip to content

addGroup duplicates groupID in super properties on Android (iOS dedupes) #418

Description

@TMaszko

Summary

When Mixpanel.addGroup(groupKey, groupID) is called more than once with the same (groupKey, groupID) — e.g. across repeated identify calls, React effects, or simply across app launches (the SDK persists super properties) — Android ends up with duplicated entries in the groupKey super-property array. That array then rides along on every emitted event.

iOS dedupes correctly. Android does not. This is a long-standing inconsistency between the two natives that bites every cross-platform consumer.

Reproduction

const mixpanel = new Mixpanel(token, true);
await mixpanel.init();
mixpanel.identify("user-1");

mixpanel.addGroup("workspace_id", "A");
mixpanel.addGroup("workspace_id", "A");
mixpanel.addGroup("workspace_id", "A");

mixpanel.track("Page View");

Expected

workspace_id super property on the emitted event = ["A"].

Actual

  • Android: workspace_id = ["A", "A", "A"]
  • iOS: workspace_id = ["A"]

Because super properties are persisted across launches, on Android the array also grows session over session — even a single addGroup per launch accumulates [A, A, A, A, …] indefinitely.

Root cause

The RN JS wrapper passes through to the native addGroup (https://github.com/mixpanel/mixpanel-react-native/blob/v3.3.0/index.js#L391-L395):

addGroup(groupKey, groupID) {
    // ...
    this.mixpanelImpl.addGroup(this.token, groupKey, groupID);
}

Android (broken)

com.mixpanel.android.mpmetrics.MixpanelAPI.addGroup calls updateSuperProperties(new SuperPropertyUpdate() {...}) whose update(JSONObject in) body is just in.accumulate(groupKey, groupID). JSONObject.accumulate appends to the array unconditionally — no membership check.

https://github.com/mixpanel/mixpanel-android/blob/master/analytics/src/main/java/com/mixpanel/android/mpmetrics/MixpanelAPI.java#L1286-L1304

iOS (correct)

MixpanelInstance.swift dedupes:

if let oldValue = oldValue as? [MixpanelType] {
    var vals = oldValue
    if !vals.contains(where: { $0.equals(rhs: groupID) }) {
        vals.append(groupID)
        superProperties[groupKey] = vals
    }
}

Versions affected

  • mixpanel-react-native: v3.1.2 and v3.3.0 (verified)
  • mixpanel-android: 8.2.0 through master post-v8.7.0 (verified by reading source)

Bumping either package does not fix this.

Suggested fix

Two options, in order of preference:

1. Fix upstream in mixpanel-android

Change MixpanelAPI.addGroup to check existing membership before appending, matching the iOS implementation. This makes the native SDKs consistent and removes the foot-gun for any consumer (not just RN).
If that's the case I can move the issue there.

2. Dedupe at the React Native shim layer

Mirrors what javascript/mixpanel-main.js:525-535 already does for JS-mode fallback:

// index.js
async addGroup(groupKey, groupID) {
    if (!StringHelper.isValid(groupKey)) {
        StringHelper.raiseError(PARAMS.GROUP_KEY);
    }
    // Read current super property, dedupe before forwarding
    const currentValue = await this.mixpanelImpl.getSuperProperty(this.token, groupKey);
    if (Array.isArray(currentValue) && currentValue.includes(groupID)) {
        // Still update the People profile membership server-side
        await this.mixpanelImpl.union?.(this.token, { [groupKey]: [groupID] });
        return;
    }
    this.mixpanelImpl.addGroup(this.token, groupKey, groupID);
}

Option 1 is the cleanest. Option 2 is faster to ship and benefits only RN consumers — but RN is where the cross-platform discrepancy hurts most.

People profile is unaffected

The People-side union call within addGroup is set-valued server-side, so the People profile's groupKey array stays clean. The bug is only visible in raw event payloads — every event carries the bloated workspace_id super property. This is why it's easy to miss when looking at the Mixpanel UI.

Current workaround

Call setGroup(groupKey, groupID) for the first entry of each identify pass (which overwrites the super property via registerSuperProperties), then addGroup for the rest. Each identify then starts from a fresh array:

if (workspaces.length > 0) {
    mixpanel.setGroup("workspace_id", workspaces[0].id);
    for (let i = 1; i < workspaces.length; i++) {
        mixpanel.addGroup("workspace_id", workspaces[i].id);
    }
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions