Skip to content

feat(mobile): enable session replay and align analytics with desktop#2633

Merged
charlesvien merged 1 commit into
mainfrom
tom/mobile-posthog-analytics
Jun 12, 2026
Merged

feat(mobile): enable session replay and align analytics with desktop#2633
charlesvien merged 1 commit into
mainfrom
tom/mobile-posthog-analytics

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Problem

The mobile app's PostHog instrumentation has several gaps that leave us blind on errors, replays, and identity:

  • Session replay has never worked. enableSessionReplay: true has been set since January, but the required companion native package posthog-react-native-session-replay was never installed, so the SDK silently no-ops — zero recordings have ever been ingested.
  • Replay masking was unsafe. maskAllTextInputs: false would capture credentials typed on the auth screen once replay starts working.
  • Mobile and desktop report to different projects. Desktop sends everything to the shared posthog.com project tagged with a team: "posthog-code" super property; mobile events lack that property, so they can't be segmented alongside desktop's once consolidated.
  • The auth funnel is a black box. ~42% of users who reach the auth screen never make it to the tasks screen, and there is no instrumentation inside the sign-in flow to explain why.

Changes

  • Add posthog-react-native-session-replay@1.6.0 so session replay actually records (verified it links natively via pod install).
  • Set maskAllTextInputs: true in the replay config.
  • Replace registerAppVersion with registerPersistentSuperProperties, which always registers team: "posthog-code" (matching desktop) plus app_version when available, and is re-applied after posthog.reset() on logout since reset wipes super properties.
  • Add typed Sign in started / Sign in completed / Sign in failed events across all three sign-in paths (OAuth, dev API key, QR scan) with method, region, and failure reason (cancelled / timeout / error) properties.
  • Add a committed apps/mobile/.env.example documenting the PostHog env vars.

Note: this only takes effect once a new build ships. Repointing EXPO_PUBLIC_POSTHOG_API_KEY in the EAS production environment to the shared project is a separate operational step.

How did you test this?

  • pnpm test in apps/mobile: 263 tests pass, including new coverage for the team super property in posthog.test.ts.
  • tsc --noEmit: no errors in changed files.
  • biome check: clean.
  • pod install in apps/mobile/ios: confirms posthog-react-native-session-replay (1.6.0) and native PostHog (3.58.3) link via autolinking.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

@github-actions

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 0045eab.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. apps/mobile/src/app/auth.tsx, line 118-141 (link)

    P2 Sign in started fires before project-ID validation

    trackSignInStarted("dev_api_key") is called on line 121, before the Number.isFinite guard on lines 123-126. When a user submits an invalid project ID, both a Sign in started and a Sign in failed (with reason: "error") are emitted even though no network request was attempted, inflating the apparent started-count for this path. Moving trackSignInStarted to just after the validation — so it only fires when an actual sign-in attempt is being made — keeps the funnel numbers accurate. This path is dev-only, but the same fix prevents confusion if the pattern is copied to a production path later.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/mobile/src/app/auth.tsx
    Line: 118-141
    
    Comment:
    **`Sign in started` fires before project-ID validation**
    
    `trackSignInStarted("dev_api_key")` is called on line 121, before the `Number.isFinite` guard on lines 123-126. When a user submits an invalid project ID, both a `Sign in started` and a `Sign in failed` (with `reason: "error"`) are emitted even though no network request was attempted, inflating the apparent started-count for this path. Moving `trackSignInStarted` to just after the validation — so it only fires when an actual sign-in attempt is being made — keeps the funnel numbers accurate. This path is dev-only, but the same fix prevents confusion if the pattern is copied to a production path later.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/mobile/src/app/auth.tsx:118-141
**`Sign in started` fires before project-ID validation**

`trackSignInStarted("dev_api_key")` is called on line 121, before the `Number.isFinite` guard on lines 123-126. When a user submits an invalid project ID, both a `Sign in started` and a `Sign in failed` (with `reason: "error"`) are emitted even though no network request was attempted, inflating the apparent started-count for this path. Moving `trackSignInStarted` to just after the validation — so it only fires when an actual sign-in attempt is being made — keeps the funnel numbers accurate. This path is dev-only, but the same fix prevents confusion if the pattern is copied to a production path later.

### Issue 2 of 2
apps/mobile/src/lib/posthog.test.ts:81-125
**Prefer parameterised tests for the `registerPersistentSuperProperties` suite**

Three of the four cases here test the same code path (client present, call `register`) and differ only in the `version` input and the expected properties object. Per the team's simplicity rules, consolidating them into a single `it.each` table (e.g., `[version, expected]`) removes the repeated `vi.fn()` / `import` / `expect` boilerplate and makes it easier to add a new combination in the future.

Reviews (1): Last reviewed commit: "feat(mobile): enable session replay and ..." | Re-trigger Greptile

Comment on lines +81 to 125
describe("registerPersistentSuperProperties", () => {
it("registers team and app_version as super properties on the PostHog client", async () => {
const register = vi.fn();
const { registerAppVersion } = await import("./posthog");
const { registerPersistentSuperProperties } = await import("./posthog");

registerAppVersion({ register }, "1.2.3");
registerPersistentSuperProperties({ register }, "1.2.3");

expect(register).toHaveBeenCalledTimes(1);
expect(register).toHaveBeenCalledWith({ app_version: "1.2.3" });
expect(register).toHaveBeenCalledWith({
team: "posthog-code",
app_version: "1.2.3",
});
});

it("does nothing when the PostHog client is not yet available", async () => {
const { registerAppVersion } = await import("./posthog");
const { registerPersistentSuperProperties } = await import("./posthog");

expect(() => registerAppVersion(null, "1.2.3")).not.toThrow();
expect(() =>
registerPersistentSuperProperties(null, "1.2.3"),
).not.toThrow();
});

it("does nothing when no app version can be resolved", async () => {
it("still registers team when no app version can be resolved", async () => {
const register = vi.fn();
const { registerAppVersion } = await import("./posthog");
const { registerPersistentSuperProperties } = await import("./posthog");

registerAppVersion({ register }, null);
registerPersistentSuperProperties({ register }, null);

expect(register).not.toHaveBeenCalled();
expect(register).toHaveBeenCalledTimes(1);
expect(register).toHaveBeenCalledWith({ team: "posthog-code" });
});

it("resolves the version from getAppVersion when none is provided", async () => {
expoApplication.nativeApplicationVersion = "4.5.6";
const register = vi.fn();
const { registerAppVersion } = await import("./posthog");
const { registerPersistentSuperProperties } = await import("./posthog");

registerAppVersion({ register });
registerPersistentSuperProperties({ register });

expect(register).toHaveBeenCalledWith({ app_version: "4.5.6" });
expect(register).toHaveBeenCalledWith({
team: "posthog-code",
app_version: "4.5.6",
});
});
});

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 Prefer parameterised tests for the registerPersistentSuperProperties suite

Three of the four cases here test the same code path (client present, call register) and differ only in the version input and the expected properties object. Per the team's simplicity rules, consolidating them into a single it.each table (e.g., [version, expected]) removes the repeated vi.fn() / import / expect boilerplate and makes it easier to add a new combination in the future.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/lib/posthog.test.ts
Line: 81-125

Comment:
**Prefer parameterised tests for the `registerPersistentSuperProperties` suite**

Three of the four cases here test the same code path (client present, call `register`) and differ only in the `version` input and the expected properties object. Per the team's simplicity rules, consolidating them into a single `it.each` table (e.g., `[version, expected]`) removes the repeated `vi.fn()` / `import` / `expect` boilerplate and makes it easier to add a new combination in the future.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@charlesvien charlesvien merged commit ca9db47 into main Jun 12, 2026
20 checks passed
@charlesvien charlesvien deleted the tom/mobile-posthog-analytics branch June 12, 2026 18:36
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.

2 participants