Skip to content

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

@TMaszko

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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions