Skip to content

Mixpanel.getGroup caches a single MixpanelGroup instance regardless of (groupKey, groupID) #417

Description

@TMaszko

Summary

Mixpanel.getGroup(groupKey, groupID) caches the returned MixpanelGroup instance in a single field this.groupnot keyed by (groupKey, groupID). The first call seeds the cache with the first pair it sees; every subsequent call returns the same instance regardless of the requested key/id, so .set(...) / .setOnce(...) / .increment(...) etc. end up forwarded to whichever group was first cached.

Reproduction

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

mixpanel.getGroup("workspace_id", "A").set("$name", "Workspace A");
mixpanel.getGroup("workspace_id", "B").set("$name", "Workspace B");
mixpanel.getGroup("workspace_id", "C").set("$name", "Workspace C");

Expected

Three group profiles A, B, C each receive their own $name.

Actual

Workspace A's profile receives all three writes in sequence:

  1. groupSetProperties("workspace_id", "A", {$name: "Workspace A"})
  2. groupSetProperties("workspace_id", "A", {$name: "Workspace B"}) ← wrong group, overwrites
  3. groupSetProperties("workspace_id", "A", {$name: "Workspace C"}) ← wrong group again

Workspaces B and C never receive a $name write. The native bridge gets the cached instance's frozen groupKey/groupID regardless of what the caller asked for.

Source

https://github.com/mixpanel/mixpanel-react-native/blob/v3.3.0/index.js#L371-L383

getGroup(groupKey, groupID) {
    if (this.group) {
        return this.group;
    } else {
        this.group = new MixpanelGroup(
            this.token,
            groupKey,
            groupID,
            this.mixpanelImpl
        );
        return this.group;
    }
}

MixpanelGroup's constructor freezes the first (groupKey, groupID) (https://github.com/mixpanel/mixpanel-react-native/blob/v3.3.0/index.js#L807-L815) and .set() always forwards those frozen values to groupSetProperties (https://github.com/mixpanel/mixpanel-react-native/blob/v3.3.0/index.js#L825-L841).

Native SDKs do this correctly

Both native SDKs key the MixpanelGroup map by (groupKey, groupID):

  • iOS: MixpanelInstance.swift uses makeMapKey(groupKey:, groupID:) and looks up in a dictionary.
  • Android: MixpanelAPI.getGroup calls makeMapKey("<key>_<id>") and stores in mGroups: java.util.Map.

So the per-key caching exists in the native layer, but the JS shim short-circuits before reaching it.

Versions affected

Verified on both mixpanel-react-native@3.1.2 and mixpanel-react-native@3.3.0. Same cache shape on both.

Suggested fix

Key the JS-side cache by (groupKey, groupID):

getGroup(groupKey, groupID) {
    const cacheKey = `${groupKey}_${JSON.stringify(groupID)}`;
    if (!this.groupCache) this.groupCache = new Map();
    let group = this.groupCache.get(cacheKey);
    if (!group) {
        group = new MixpanelGroup(this.token, groupKey, groupID, this.mixpanelImpl);
        this.groupCache.set(cacheKey, group);
    }
    return group;
}

Current workaround

Instantiate MixpanelGroup directly, bypassing the cached shim:

import { MixpanelGroup } from "mixpanel-react-native";

const group = new MixpanelGroup(token, groupKey, groupID, mixpanel.mixpanelImpl);
group.set("$name", workspace.name);

This reaches into the private mixpanelImpl field — brittle but works.

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