Skip to content

app: AnalyticsAdapter abstraction + rename Mixpanel-named layer to Analytics#7205

Merged
mdmohsin7 merged 5 commits intomainfrom
caleb/analytics-adapter-refactor
May 8, 2026
Merged

app: AnalyticsAdapter abstraction + rename Mixpanel-named layer to Analytics#7205
mdmohsin7 merged 5 commits intomainfrom
caleb/analytics-adapter-refactor

Conversation

@mdmohsin7
Copy link
Copy Markdown
Member

@mdmohsin7 mdmohsin7 commented May 7, 2026

Summary

The mobile app finished migrating from Mixpanel to PostHog but kept the legacy file/class naming, so today the analytics surface is named after a provider that's no longer running. This PR fixes that AND introduces an abstraction so future provider swaps are one file plus one boot-wiring line.

Architecture

Adapter interfaceapp/lib/utils/analytics/analytics_adapter.dart
Provider primitives only: init, identify, alias, track, enable, disable, reset. Seven methods. Adding a new analytics SDK = one new file in analytics/adapters/.

PostHog implementationapp/lib/utils/analytics/adapters/posthog_adapter.dart
class PostHogAnalyticsAdapter implements AnalyticsAdapter. Owns apiKey, host, captureLifecycleEvents, debug, the Posthog() SDK calls, and the per-instance _initialized flag.

Serviceapp/lib/utils/analytics/analytics_manager.dart
Holds a nullable AnalyticsAdapter. Static configure(adapter) for explicit injection at boot; init() lazily falls back to a PostHogAnalyticsAdapter from Env.posthogApiKey if no adapter was configured (preserves today's "PostHog is the default" behavior). Owns the 132 event helper methods + the _pendingTimedEvents machinery + the _coerceProperty recursive coercion helper. Back-compat surface preserved: setUserAttribute / setUserAttributes / trackEvent keep their PostHog+Intercom user-attribute fan-out (the only fan-out anyone actually consumed); the dead trackEvent → Intercom path is dropped.

Access patternPlatformManager.instance.analytics.X(...)
Call sites reach analytics exclusively through PlatformManager, the same way they reach intercom and crashReporter. Never via AnalyticsManager() direct. Single grep target for audits, decoupled from the class identity for future renames.

Naming notes

Provider, Backend, Service, and Manager are all already overloaded in this codebase (state-management ChangeNotifiers, app/lib/backend/ directory holding the omi-server API clients, app/lib/services/ BLE/FCM bucket, Manager taken at the existing public surface). Adapter is unused except BluetoothAdapter in a different domain — and Adapter is the actual design pattern at play (each SDK exposes its own API, we adapt to one common surface).

Codemod scope

  • 408 sites rewritten from MixpanelManager() (then briefly via AnalyticsManager()) to PlatformManager.instance.analytics
  • 114 imports of analytics/mixpanel.dart consolidated; files that no longer reference AnalyticsManager directly have their imports dropped — only platform_manager.dart itself imports analytics_manager.dart
  • PlatformManager.instance.mixpanel getter → .analytics (return type updated to AnalyticsManager)
  • PlatformService.isMixpanelSupported / isMixpanelNativelySupported consolidated into the existing isAnalyticsSupported. Value is !kIsWeb (matches what the now-removed isMixpanelSupported evaluated to)
  • app/lib/utils/analytics/mixpanel.dart deleted

User-visible "Mixpanel" strings in settings + privacy copy are not touched here — that's a privacy disclosure / legal review concern, not architectural.

Commits

  1. feat(app): introduce AnalyticsAdapter interface + PostHog implementation — additive, nothing breaks.
  2. refactor(app): rewrite analytics layer behind AnalyticsAdapter — atomic rename + first codemod + boot wiring + delete mixpanel.dart. Single commit because splitting it leaves a half-state where the 6 existing back-compat callers either lose their PostHog write (null adapter) or double-write (dual-class active).
  3. refactor(app): route analytics access through PlatformManager.instance.analytics — second codemod that moves all 408 call sites from the direct singleton to the platform-services access path, matching how intercom and crashReporter are already accessed.

Test plan

  • Build verification on Android emulator — pub get, build apk debug, no compile errors. Static checks clean: zero leftover MixpanelManager / Posthog() outside the adapter / isMixpanelSupported / analytics/mixpanel.dart import strings across app/lib.
  • Build verification on iOS — same pub get + flutter build ios.
  • Manual smoke: open the app, complete an action that fires an event, confirm event lands in PostHog dashboard (just like before).
  • Manual smoke: opt-out of analytics in settings → confirm capture stops; opt back in → confirm capture resumes.
  • Manual smoke: log in / log out flow exercises identify / alias / reset.

mdmohsin7 added 2 commits May 7, 2026 10:03
The mobile app finished migrating from Mixpanel to PostHog but kept the
legacy class/file naming, leaving the analytics layer in a state where
the names lie about what's running. This is the first of three commits
that fix that AND add a clean swap point for future provider changes.

This commit is purely additive:

- analytics/analytics_adapter.dart — abstract AnalyticsAdapter interface
  with the seven primitives every SDK has to satisfy: init, identify,
  alias, track, enable, disable, reset.

- analytics/adapters/posthog_adapter.dart — PostHogAnalyticsAdapter
  implementation. Owns apiKey/host/captureLifecycleEvents/debug, the
  Posthog() SDK calls, and the per-instance _initialized flag. Calls
  before init are no-ops at this layer, so the manager doesn't have to
  branch on init state for every method.

setUserProperty is intentionally NOT a primitive — it composes to
`identify(uid, userProperties: {k: v})` and that ergonomic lives at the
manager layer. Keeps the adapter surface narrow so any future provider
(Mixpanel-redux, Amplitude, Segment, …) only has to satisfy seven
methods.

Nothing in the rest of the app imports these yet — added only.
Renames the legacy Mixpanel-named analytics surface to its actual
identity post-PostHog migration, and routes all analytics through the
AnalyticsAdapter introduced in the previous commit so future provider
swaps are one file plus one boot-wiring line.

Architectural changes:

- analytics_manager.dart absorbs the entire MixpanelManager event surface
  (132 helper methods + the timed-event machinery + the property
  coercion helper). Class is now AnalyticsManager. Every adapter call
  goes through the nullable static `_adapter` field; calling a method
  before `configure(...)` runs is a clean no-op (the right behavior for
  environments without a PostHog key, e.g. local dev with no Env).

- The old AnalyticsManager surface (setUserAttribute / setUserAttributes
  / trackEvent — 6 callers across onboarding_provider + home_provider)
  is preserved as back-compat methods on the new class. setUserAttribute
  + setUserAttributes still fan out to Intercom for user attributes —
  that fan-out is the only piece of the multi-target dispatch anyone
  actually consumed. The dead trackEvent → Intercom fan-out is dropped;
  if events ever need to land in Intercom, an IntercomAnalyticsAdapter
  can be configured as a second adapter at boot.

- platform_manager.dart `mixpanel` getter renamed to `analytics` and now
  returns AnalyticsManager. initializeServices calls
  AnalyticsManager.init(), which lazily constructs a
  PostHogAnalyticsAdapter from Env.posthogApiKey if no adapter was
  configured explicitly — preserving today's "PostHog is the default"
  behavior without locking the manager to PostHog at the type level.

- platform_service.dart consolidates the duplicate
  `isMixpanelSupported` / `isMixpanelNativelySupported` /
  `isAnalyticsSupported` flags into a single `isAnalyticsSupported`
  whose value matches the previously-effective gate (`!kIsWeb`). The
  former `isAnalyticsSupported = true` was dead — every call site
  actually checked `isMixpanelSupported`, so behavior is preserved.

- mixpanel.dart deleted. 396 `MixpanelManager()` invocations and 114
  imports across 116 files codemodded to AnalyticsManager /
  analytics_manager.dart.

User-visible "Mixpanel" strings in settings + privacy copy are not
touched here — that's a separate concern (privacy policy / disclosure
review) and not on the architectural path.
@mdmohsin7 mdmohsin7 added app p3 Priority: Backlog (score <14) labels May 7, 2026
…e.analytics

Treats AnalyticsManager as a service held by PlatformManager rather
than a global singleton called directly. Same pattern as
PlatformManager.instance.intercom and .crashReporter — analytics now
sits where readers expect to find platform services, with a single
canonical access path.

Why this is the cleaner shape:

- One way to reach the service. Before, AnalyticsManager()
  (factory-singleton) and PlatformManager.instance.analytics both
  worked, and reviewers had to check both surfaces when auditing
  analytics calls. Single grep target now: `PlatformManager.instance.
  analytics.`.
- Consistent with intercom and crashReporter, which are accessed
  exclusively through PlatformManager today. New readers don't have to
  remember that analytics is a one-off.
- Decouples 408 call sites from the AnalyticsManager class identity.
  Renaming or splitting AnalyticsManager later only touches the
  PlatformManager getter, not the call sites.

Codemod scope: 408 invocations across 116 files rewritten from
AnalyticsManager().X(...) to PlatformManager.instance.analytics.X(...).
Files that no longer reference AnalyticsManager get their now-unused
analytics_manager.dart import dropped; files that newly use
PlatformManager.instance.analytics get the platform_manager.dart
import added if it wasn't already there. The only remaining
AnalyticsManager references in the app are the two in
platform_manager.dart itself: the getter definition and the boot-time
AnalyticsManager.init() call.
@mdmohsin7 mdmohsin7 marked this pull request as ready for review May 7, 2026 14:44
@mdmohsin7
Copy link
Copy Markdown
Member Author

@greptile-apps review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 7, 2026

Greptile Summary

This PR replaces the legacy MixpanelManager / mixpanel.dart layer with a provider-agnostic AnalyticsAdapter interface, a PostHogAnalyticsAdapter implementation, and a renamed AnalyticsManager that holds all 130+ event helpers. All 408 call sites are rewritten from MixpanelManager() to PlatformManager.instance.analytics, matching the existing intercom / crashReporter access pattern.

  • New abstraction layer: AnalyticsAdapter interface + PostHogAnalyticsAdapter implement an injected adapter pattern; future SDK swaps touch only one file.
  • Manager consolidation: analytics_manager.dart absorbs the PostHog calls, timed-event TTL eviction, and property coercion logic previously scattered in mixpanel.dart.
  • configure() post-init() gap: the static configure() method has no runtime guard; calling it after init() replaces the initialized adapter with an uninitialized one, silently killing all analytics until the app restarts.

Confidence Score: 4/5

Safe to merge for most deployments; the configure() ordering gap is a latent API hazard not triggered by the current boot path.

The 408-site codemod and file rename are mechanical and low-risk. The boot flow in PlatformManager never calls configure(), so the unguarded post-init configure() path is not exercised today. The Intercom event-logging removal in trackEvent() is intentional per the PR description but represents a real behavioural delta that should be confirmed with the team. The setUserProperty per-call identify fan-out is pre-existing. No data-loss or crash scenario is introduced on the current code paths.

app/lib/utils/analytics/analytics_manager.dart — configure() ordering contract and trackEvent Intercom removal warrant a second look.

Important Files Changed

Filename Overview
app/lib/utils/analytics/analytics_adapter.dart New abstract interface defining provider-agnostic analytics primitives (init, identify, alias, track, enable, disable, reset) — clean and well-documented.
app/lib/utils/analytics/adapters/posthog_adapter.dart PostHog implementation of AnalyticsAdapter; correctly guards all methods with _initialized flag. Minor: init() failure leaves _initialized false without surfacing an error.
app/lib/utils/analytics/analytics_manager.dart Central service refactored from MixpanelManager; configure() has no runtime guard against post-init calls, which silently stops all analytics if misused. trackEvent() drops previous Intercom event fan-out.
app/lib/utils/platform/platform_manager.dart Getter renamed from mixpanel to analytics returning AnalyticsManager; initializeServices updated to call AnalyticsManager.init(). Clean rename.
app/lib/utils/platform/platform_service.dart isAnalyticsSupported changed from always-true to !(kIsWeb), matching the removed isMixpanelSupported value. Removes isMixpanelSupported and isMixpanelNativelySupported getters.
app/lib/utils/analytics/mixpanel.dart File deleted as part of the rename; all logic migrated to analytics_manager.dart and posthog_adapter.dart.

Sequence Diagram

sequenceDiagram
    participant Boot as App Boot
    participant PM as PlatformManager
    participant AM as AnalyticsManager
    participant PA as PostHogAnalyticsAdapter
    participant PH as Posthog SDK

    Boot->>PM: initializeServices()
    PM->>AM: init()
    AM->>AM: "_adapter == null && Env.posthogApiKey != null?"
    AM->>PA: new PostHogAnalyticsAdapter(apiKey)
    AM->>PA: init()
    PA->>PH: Posthog().setup(config)
    PA-->>AM: "_initialized = true"

    Note over Boot,PH: Runtime analytics calls
    Boot->>PM: instance.analytics.track(event)
    PM->>AM: track(event)
    AM->>AM: executeIfSupported(isAnalyticsSupported)
    AM->>PA: track(eventName, properties)
    PA->>PA: if (!_initialized) return
    PA->>PH: Posthog().capture(eventName)
Loading

Reviews (1): Last reviewed commit: "style(app): apply dart formatter to chan..." | Re-trigger Greptile

Comment on lines +21 to +23
static void configure(AnalyticsAdapter adapter) {
_adapter = adapter;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 configure() after init() silently disables all analytics

If configure() is called after init() has already run (e.g., in a test harness or to hot-swap an adapter), _adapter is replaced with a brand-new, uninitialised instance. Every PostHogAnalyticsAdapter guards all its methods with if (!_initialized) return;, so every subsequent track, identify, enable, and disable call becomes a silent no-op. The comment says "Must be called before init()", but there is no runtime enforcement. Any caller who violates that contract gets zero indication that analytics has stopped working. Consider adding an assert(_adapter == null, 'configure() must be called before init()') or calling adapter.init() inside configure() to make the API safe at any order.

Comment on lines 54 to +56
void trackEvent(String eventName, {Map<String, dynamic>? properties}) {
PlatformManager.instance.mixpanel.track(eventName, properties: properties);
PlatformManager.instance.intercom.logEvent(eventName, metaData: properties);
track(eventName, properties: properties);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 trackEvent silently drops Intercom event logging

In the old AnalyticsManager.trackEvent, the call forwarded to both mixpanel.track(...) and intercom.logEvent(...). The new implementation delegates only to track(), routing exclusively to the PostHog adapter. Any call sites that relied on trackEvent to record Intercom events — e.g., for user lifecycle funnels in Intercom conversations — will no longer receive those events. The PR description notes this drop as intentional, but Intercom event visibility should be verified before merging if Intercom inbox automations depend on these events.

Comment on lines +73 to +82
setUserProperty(String key, dynamic value) =>
PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () {
final adapter = _adapter;
if (adapter == null) return;
final uid = _preferences.uid;
if (uid.isEmpty) return;
final coerced = _coerceProperty(value);
if (coerced == null) return;
adapter.identify(userId: uid, userProperties: {key: coerced});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 setUserProperty fires one identify call per property

Every setUserProperty call invokes adapter.identify with a single-key userProperties map. Callers such as setPeopleValues() chain 8 sequential calls, each issuing its own identify request to PostHog. The old MixpanelManager had the same pattern so this is not a new regression, but now that the architecture is being cleaned up this is a good opportunity to batch all property updates into a single identify call to reduce PostHog ingest load.

… calls

- assert configure() runs before init() so a misordered call no longer
  silently replaces the active adapter with an uninitialised one
- collapse setPeopleValues / setNameAndEmail / setUserProperties from
  N sequential identify() calls into a single batched identify
@mdmohsin7 mdmohsin7 merged commit 47adb07 into main May 8, 2026
2 checks passed
@mdmohsin7 mdmohsin7 deleted the caleb/analytics-adapter-refactor branch May 8, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app p3 Priority: Backlog (score <14)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant