Skip to content

fix(analytics): isolate AnalyticsMessages and HttpService per instance [SDK-109]#985

Open
tylerjroach wants to merge 2 commits into
masterfrom
fix/singleton-isolation
Open

fix(analytics): isolate AnalyticsMessages and HttpService per instance [SDK-109]#985
tylerjroach wants to merge 2 commits into
masterfrom
fix/singleton-isolation

Conversation

@tylerjroach

@tylerjroach tylerjroach commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Two MixpanelAPI instances configured with different tokens and no explicit instanceName silently shared a single AnalyticsMessages worker (and therefore its MPConfig and HttpService), so events for the second instance were POSTed to the first instance's server URL. Independently, runtime setServerURL calls updated MPConfig endpoints but never refreshed the HttpService serverHost captured at construction.

Changes

  • AnalyticsMessages.getInstance now takes a token and keys sInstances by MPConfig.resolveInstanceKey(instanceName, token), mirroring MixpanelAPI.getInstance and matching iOS MixpanelManager behavior.
  • AnalyticsMessages.getPoster re-syncs serverHost (in addition to backupHost and the error listener) on every call, so setServerURL propagates to the poster.
  • Added HttpService.setServerHost / getServerHost; the constructor delegates to setServerHost so the empty-check + DEFAULT_SERVER_HOST fallback lives in one place.
  • Extracted the instanceName ?? token fallback into MPConfig.resolveInstanceKey — the five prior inline copies (plus three in tests) now call it, avoiding future drift.
  • Removed the dead mInstanceName field on AnalyticsMessages.

Workaround

Users on older SDK versions can side-step both bugs by giving each MixpanelOptions a distinct instanceName and setting the server URL via MixpanelOptions.serverURL(...) (which is applied before the first getPoster() call, so HttpService is constructed with the right host on first creation).

…e [SDK-109]

Two MixpanelAPI instances configured with different tokens and no explicit
instanceName silently shared a single AnalyticsMessages worker (and hence
its MPConfig and HttpService), so events for the second instance were
POSTed to the first instance's server URL. Additionally, runtime
setServerURL calls updated MPConfig endpoints but never refreshed the
HttpService serverHost captured at construction.

- AnalyticsMessages.getInstance now takes a token and keys by
  MPConfig.resolveInstanceKey(instanceName, token), mirroring
  MixpanelAPI.getInstance.
- getPoster re-syncs serverHost on every call, so setServerURL propagates
  to the HttpService.
- Extracted the instanceName-or-token fallback into
  MPConfig.resolveInstanceKey to keep the five prior copies in sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerjroach tylerjroach requested review from a team and rahul-mixpanel July 1, 2026 13:55
@linear-code

linear-code Bot commented Jul 1, 2026

Copy link
Copy Markdown

SDK-109

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge — the changes are well-scoped bug fixes with direct regression tests, and no new error paths are introduced.

The instance-isolation and setServerURL-propagation fixes are correct and consistent with how MixpanelAPI already keyed its own map. The resolveInstanceKey refactor is mechanical. No logic is removed that could leave a code path uncovered, and the new MultiInstanceIsolationTest directly exercises both fixed scenarios.

No files require special attention.

Important Files Changed

Filename Overview
analytics/src/main/java/com/mixpanel/android/mpmetrics/AnalyticsMessages.java Core fix: sInstances now keyed by resolveInstanceKey(instanceName, token); getPoster() re-syncs serverHost unconditionally; dead mInstanceName field removed.
analytics/src/main/java/com/mixpanel/android/mpmetrics/MPConfig.java New package-private resolveInstanceKey consolidates the instanceName??token fallback in one place.
analytics/src/main/java/com/mixpanel/android/mpmetrics/MixpanelAPI.java Passes mToken to AnalyticsMessages.getInstance; replaces inline instanceName??token copies with resolveInstanceKey.
analytics/src/main/java/com/mixpanel/android/util/HttpService.java Adds setServerHost/getServerHost; constructor delegates to setServerHost centralising the empty-string fallback.
analytics/src/test/java/com/mixpanel/android/mpmetrics/MultiInstanceIsolationTest.java New regression tests covering token-isolated workers and setServerURL propagation to the poster.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App
    participant MixpanelAPI
    participant AnalyticsMessages
    participant MPConfig
    participant HttpService

    App->>MixpanelAPI: getInstance(ctx, token, options)
    MixpanelAPI->>MPConfig: resolveInstanceKey(instanceName, token)
    MPConfig-->>MixpanelAPI: key
    MixpanelAPI->>AnalyticsMessages: getInstance(ctx, config, token)
    AnalyticsMessages->>MPConfig: resolveInstanceKey(instanceName, token)
    MPConfig-->>AnalyticsMessages: key (same as MixpanelAPI key)
    AnalyticsMessages-->>MixpanelAPI: isolated worker per key

    App->>MixpanelAPI: setServerURL(newUrl)
    MixpanelAPI->>MPConfig: setEventsEndpoint(newUrl)
    App->>MixpanelAPI: track(event)
    MixpanelAPI->>AnalyticsMessages: eventsMessage(...)
    AnalyticsMessages->>AnalyticsMessages: getPoster()
    AnalyticsMessages->>HttpService: setServerHost(extractHost(newUrl))
    Note over HttpService: host updated on every getPoster() call
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App
    participant MixpanelAPI
    participant AnalyticsMessages
    participant MPConfig
    participant HttpService

    App->>MixpanelAPI: getInstance(ctx, token, options)
    MixpanelAPI->>MPConfig: resolveInstanceKey(instanceName, token)
    MPConfig-->>MixpanelAPI: key
    MixpanelAPI->>AnalyticsMessages: getInstance(ctx, config, token)
    AnalyticsMessages->>MPConfig: resolveInstanceKey(instanceName, token)
    MPConfig-->>AnalyticsMessages: key (same as MixpanelAPI key)
    AnalyticsMessages-->>MixpanelAPI: isolated worker per key

    App->>MixpanelAPI: setServerURL(newUrl)
    MixpanelAPI->>MPConfig: setEventsEndpoint(newUrl)
    App->>MixpanelAPI: track(event)
    MixpanelAPI->>AnalyticsMessages: eventsMessage(...)
    AnalyticsMessages->>AnalyticsMessages: getPoster()
    AnalyticsMessages->>HttpService: setServerHost(extractHost(newUrl))
    Note over HttpService: host updated on every getPoster() call
Loading

Reviews (2): Last reviewed commit: "test(analytics): use assertSame for Http..." | Re-trigger Greptile

assertEquals falls back to reference equality when the type doesn't
override equals(), which HttpService doesn't — so the assertion works
but reads as a value-equality check. assertSame is the idiomatic JUnit
choice for identity and makes intent explicit.
@tylerjroach

Copy link
Copy Markdown
Contributor Author

Pushed f9407b5 — swapped assertEqualsassertSame and imported the helper. 2 tests pass locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant