Skip to content

Implement posthog - #5

Merged
aymengmar merged 2 commits into
mainfrom
dev
Jun 23, 2026
Merged

aymengmar merged 2 commits into
mainfrom
dev

Conversation

@aymengmar

@aymengmar aymengmar commented Jun 23, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated analytics tracking throughout the app to capture user behavior, including sign-up and sign-in flows, language selection, and session navigation.
  • Chores

    • Added PostHog analytics library and required dependencies.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6af3cfd-c1a3-4954-a1ae-72a47ef29ea8

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1874f and dc054c4.

📒 Files selected for processing (1)
  • .gitignore

📝 Walkthrough

Walkthrough

PostHog analytics is integrated into the Muolingo Expo app via a new lib/posthog.ts singleton (reading credentials from app.config.js extras), a PostHogProvider wrapper with autocapture in app/_layout.tsx, manual screen tracking, and capture/identify/captureException calls across auth screens, tab screens, onboarding, and language selection. A new Claude AI skill (integration-expo) is also added with workflow phase docs, full React Native SDK reference, identify-users reference, and a Burrito Consideration example project.

Changes

App PostHog Integration

Layer / File(s) Summary
PostHog client, config, and dependencies
package.json, lib/posthog.ts, app.config.js, .gitignore
Adds posthog-react-native and react-native-svg deps, creates the posthog singleton reading posthogProjectToken/posthogHost from expo-constants extras, migrates app.json to app.config.js to inject those values from env vars, and ignores .env/.env.local.
PostHogProvider wiring and screen tracking in root layout
app/_layout.tsx
Adds PostHogProvider (with autocapture config) wrapping ClerkProvider, and a useEffect in RootLayoutNav that calls posthog.screen on every pathname/params change, capturing previous_screen via useRef.
Auth screen and VerificationModal instrumentation
app/(auth)/sign-in.tsx, app/(auth)/sign-up.tsx, components/VerificationModal.tsx
Adds usePostHog to sign-in, sign-up, and VerificationModal; records submitted/completed events; calls posthog.identify with email and first-date properties on auth completion; captures exceptions on sendCode/verify failures.
Onboarding, tab, and language-selection event captures
app/onboarding.tsx, app/(tabs)/index.tsx, app/(tabs)/profile.tsx, app/language-selection.tsx
Instruments HomeScreen (3 events), ProfileScreen (sign-out reset + language-change event), LanguageSelection (language_selected), and OnboardingScreen (onboarding_get_started_tapped) using usePostHog and posthog.capture.
PostHog setup report
posthog-setup-report.md
Generated report enumerating all instrumented events with source files, integration wiring summary, and a verification checklist.

Claude Skill: integration-expo

Layer / File(s) Summary
SKILL.md and four workflow phase reference docs
.claude/skills/integration-expo/SKILL.md, .claude/skills/integration-expo/references/1-begin.md, 2-edit.md, 3-revise.md, 4-conclude.md
Creates the skill definition with numbered workflow, Expo/PostHog principles, and four chained phase docs: event planning (.posthog-events.json), implementation (captures/identify), lint/build revision, and conclusion (dashboard creation, report generation, cleanup).
React Native SDK reference doc
.claude/skills/integration-expo/references/react-native.md
Full React Native SDK reference: installation, provider init, config options, tracing headers, event capture, screen tracking across all nav stacks, autocapture, identify/alias/super properties, flushing, feature flags, error tracking, session replay, debug mode, and upgrade guidance.
Identify users reference doc
.claude/skills/integration-expo/references/identify-users.md
Documents distinct_id linking, multi-platform identify examples, lifecycle explanation, best practices (call early, unique IDs, reset on logout), person properties, and cross-platform deep-link handling with alias/identify branching.
Burrito Consideration example project
.claude/skills/integration-expo/references/EXAMPLE.md
Complete Expo example app with PostHogProvider wiring, manual screen tracking, posthog.ts singleton config, AuthContext with identify/login/logout events, AsyncStorage persistence, screen snippets with captureException, babel config for React Compiler, and theme constants.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Screen as Expo Screen
    participant PostHogProvider as PostHogProvider (_layout.tsx)
    participant PostHogTS as lib/posthog.ts
    participant PostHogSDK as posthog-react-native

    User->>Screen: navigates / interacts
    Screen->>PostHogProvider: pathname/params change via usePathname
    PostHogProvider->>PostHogTS: posthog.screen(pathname, { previous_screen, ...params })
    PostHogTS->>PostHogSDK: enqueue screen event

    User->>Screen: taps sign-in / sign-up
    Screen->>PostHogTS: posthog.capture("sign_in_submitted")
    Screen->>PostHogTS: posthog.identify(email, { $set, $set_once })
    Screen->>PostHogTS: posthog.capture("sign_in_completed")
    PostHogTS->>PostHogSDK: flush batch to PostHog API

    User->>Screen: taps sign-out
    Screen->>PostHogTS: posthog.capture("user_signed_out")
    Screen->>PostHogTS: posthog.reset()
    PostHogTS->>PostHogSDK: reset distinct_id
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • aymengmar/react-native-lingua#1: Introduced the onboarding "Get Started" button and handler in app/onboarding.tsx — the same location where this PR adds posthog.capture("onboarding_get_started_tapped").
  • aymengmar/react-native-lingua#3: Added the language-selection confirm flow and app/_layout.tsx route stack entries that this PR extends with PostHog "language_selected" capture and language-selection stack screen reformatting.

Poem

🐇 Hippity-hop, events now flow,
Each tap and screen puts on a show!
posthog.capture hops through the app,
Identify called — no identity gap.
The burrito counts, the wizard's done,
Analytics magic — every bunny's won! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Implement posthog' is vague and generic, using a non-descriptive term that does not clearly convey the specific changes or scope of the changeset. Use a more descriptive title that explains what is being implemented, such as 'Add PostHog analytics integration to Expo app' or 'Integrate PostHog event tracking across authentication and user flows'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/integration-expo/references/identify-users.md:
- Around line 13-62: The markdown heading hierarchy is incorrect, jumping from
the implicit document heading directly to H3 (###) level headings for the
platform sections. Fix this by changing all five platform section headings from
three hashes to two hashes: change "### Web", "### Android", "### iOS", "###
React Native", and "### Dart" to use "##" instead of "###" to maintain proper
heading hierarchy and compliance with the markdown heading-increment rule.
- Around line 195-258: The markdown file has duplicate heading levels creating a
MD024 warning. The file contains two sets of "### iOS" and "### Android"
headings under the same parent section. To fix this, rename the first or second
occurrence of these duplicate headings to make them unique. For example, change
the first pair to "### iOS Implementation" and "### Android Implementation" or
rename the second pair similarly. Alternatively, elevate one complete pair of
iOS and Android sections to use "## " level headings if they represent distinct
top-level subsections within the deep links guidance section.

In @.claude/skills/integration-expo/references/react-native.md:
- Line 5: The opening sentence in the React Native integration reference is
missing the word "SDK" which makes it grammatically incomplete. Find the phrase
"Our React Native enables you to integrate" and add "SDK" between "React Native"
and "enables" so it reads "Our React Native SDK enables you to integrate PostHog
with your React Native project."

In @.claude/skills/integration-expo/SKILL.md:
- Around line 42-50: The SKILL.md file contains contradictory configuration
guidance for PostHog setup. Remove the duplicate statement about
posthog-react-native being the React Native SDK package name that appears on
both line 42 and line 46. Then address the contradiction between the
expo-constants approach (lines 43–44) and the react-native-config approach
(lines 47–48): since this is an Expo-focused skill file, either remove the
react-native-config guidance entirely (lines 47–48) if it is only applicable to
bare React Native projects, or clearly separate the two patterns with explicit
section headers (such as "For Expo:" and "For Bare React Native:") so developers
understand which approach applies to their project type. The codebase currently
implements the expo-constants pattern, so align the documentation with that
approach.

In @.env:
- Around line 1-3: Remove the actual credential values from the .env file.
Replace the concrete key values for EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY and
POSTHOG_PROJECT_TOKEN with placeholder strings (such as empty strings or example
values like pk_test_placeholder). Ensure the .env file is added to .gitignore to
prevent future commits of actual credentials. Note that simply adding to
.gitignore later will not remove these already-committed values from repository
history, so the values must be removed from this commit.

In `@app/_layout.tsx`:
- Around line 35-43: The PostHog configuration has automatic screen capture
enabled, which conflicts with Expo Router since the PostHog provider is placed
outside the navigation context. Locate the PostHog provider initialization code
and set the autocapture.captureScreens option to false. This allows the manual
posthog.screen() calls in the useEffect hook (lines 35-43) to handle screen
tracking without interference from automatic capture that isn't compatible with
React Navigation v7+.
- Around line 37-40: The posthog.screen() call in the analytics tracking block
spreads all params from useGlobalSearchParams() directly without filtering,
which can expose sensitive data like verification codes or reset tokens to
analytics. Create a helper function to filter out known sensitive parameters
(such as those containing tokens, codes, credentials, or verification-related
keys) and apply this filter to params before spreading them into the
posthog.screen() call. Only send non-sensitive, safe parameters to the analytics
service.

In `@app/`(tabs)/profile.tsx:
- Around line 15-20: The handleSignOut function needs error handling for the
signOut() call which can fail per Clerk documentation. Wrap the entire async
flow in a try/catch block. Move the posthog.reset() call to execute only after a
successful signOut() to prevent inconsistent state where the user remains signed
in but analytics are cleared. Additionally, add a call to
clearSelectedLanguage() within the try block for complete user state cleanup. In
the catch block, handle any errors appropriately (consider logging or showing an
error message to the user) and ensure navigation does not occur if sign-out
fails.

In `@components/VerificationModal.tsx`:
- Line 82: The posthog.capture call for "email_verification_code_resent" is
currently being emitted before awaiting the onResend() call, which means failed
resend attempts are incorrectly tracked as successful. Move the
posthog.capture("email_verification_code_resent", { email }) event to execute
only after the await onResend() completes successfully. Additionally, consider
adding a separate failure event (such as
"email_verification_code_resent_failed") in the catch block to track
unsuccessful resend attempts.

In `@posthog-setup-report.md`:
- Line 38: The identify() function is currently only called in sign-in.tsx and
sign-up.tsx during fresh authentication flows, but users restored from Clerk's
tokenCache on app boot are not being identified, leaving them under anonymous
distinct IDs. In the _layout.tsx file, locate the useEffect hook (around lines
45-63) and add an identify() call that triggers when isSignedIn becomes true,
particularly to handle the case where a user already has an authenticated
session restored from the token cache. This ensures all authenticated users—both
fresh and returning from token cache—are properly identified in PostHog rather
than only those completing a manual sign-in flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 00d9e38c-d58d-4873-8e1d-ffed0f9745e5

📥 Commits

Reviewing files that changed from the base of the PR and between 94095f0 and 2f1874f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • .claude/skills/integration-expo/.posthog-wizard
  • .claude/skills/integration-expo/SKILL.md
  • .claude/skills/integration-expo/references/1-begin.md
  • .claude/skills/integration-expo/references/2-edit.md
  • .claude/skills/integration-expo/references/3-revise.md
  • .claude/skills/integration-expo/references/4-conclude.md
  • .claude/skills/integration-expo/references/EXAMPLE.md
  • .claude/skills/integration-expo/references/identify-users.md
  • .claude/skills/integration-expo/references/react-native.md
  • .env
  • .gitignore
  • app.config.js
  • app/(auth)/sign-in.tsx
  • app/(auth)/sign-up.tsx
  • app/(tabs)/index.tsx
  • app/(tabs)/profile.tsx
  • app/_layout.tsx
  • app/language-selection.tsx
  • app/onboarding.tsx
  • components/VerificationModal.tsx
  • lib/posthog.ts
  • package.json
  • posthog-setup-report.md

Comment on lines +13 to +62
### Web

```javascript
posthog.identify(
'distinct_id', // Replace 'distinct_id' with your user's unique identifier
{ email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties
);
```

### Android

```kotlin
PostHog.identify(
distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier
// optional: set additional person properties
userProperties = mapOf(
"name" to "Max Hedgehog",
"email" to "max@hedgehogmail.com"
)
)
```

### iOS

```swift
PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier
userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties
```

### React Native

```jsx
posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier
email: 'max@hedgehogmail.com', // optional: set additional person properties
name: 'Max Hedgehog'
})
```

### Dart

```dart
await Posthog().identify(
userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier
userProperties: {
'email': 'max@hedgehogmail.com', // optional: set additional person properties
'name': 'Max Hedgehog',
},
);
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix heading level increment: "### Web" should be "## Web".

The markdown structure jumps from implicit H1/H2 (from YAML frontmatter) to "### Web" at line 13. This violates the heading-increment rule (MD001): headings should only increase by one level at a time.

Change lines 13, 22, 35, 42, 51 from ### to ## to match the platform examples structure:

  • Line 13: ## Web
  • Line 22: ## Android
  • Line 35: ## iOS
  • Line 42: ## React Native
  • Line 51: ## Dart
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/integration-expo/references/identify-users.md around lines 13
- 62, The markdown heading hierarchy is incorrect, jumping from the implicit
document heading directly to H3 (###) level headings for the platform sections.
Fix this by changing all five platform section headings from three hashes to two
hashes: change "### Web", "### Android", "### iOS", "### React Native", and "###
Dart" to use "##" instead of "###" to maintain proper heading hierarchy and
compliance with the markdown heading-increment rule.

Comment on lines +195 to +258
### iOS

```swift
import PostHog
class DeepLinkIdentityManager {
static let shared = DeepLinkIdentityManager()
// MARK: - Deep Link Received
func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) {
guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)?
.queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else {
return
}
if isAuthenticatedOnMobile {
// The mobile app already knows the current user.
// Alias the incoming web distinct ID to that user.
PostHogSDK.shared.alias(webDistinctId)
} else {
// Reuse the web distinct ID until login on mobile.
PostHogSDK.shared.identify(webDistinctId)
}
}
// MARK: - Login/Signup
func handleLogin(canonicalUserId: String) {
// Switch from the web distinct ID (or a mobile anon ID)
// to your canonical user ID.
PostHogSDK.shared.identify(canonicalUserId)
// Set user properties, track signup event, etc.
}
func handleLogout() {
PostHogSDK.shared.reset()
}
}
```

### Android

```kotlin
import android.net.Uri
import com.posthog.PostHog
object DeepLinkIdentityManager {
// Deep Link Received
fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) {
val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return
if (isAuthenticatedOnMobile) {
// The mobile app already knows the current user.
// Alias the incoming web distinct ID to that user.
PostHog.alias(webDistinctId)
} else {
// Reuse the web distinct ID until login on mobile.
PostHog.identify(webDistinctId)
}
}
// Login/Signup
fun handleLogin(canonicalUserId: String) {
// Switch from the web distinct ID (or a mobile anon ID)
// to your canonical user ID.
PostHog.identify(canonicalUserId)
// Set user properties, track signup event, etc.
}
fun handleLogout() {
PostHog.reset()
}
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Resolve duplicate heading issue: "### iOS" and "### Android" appear twice in the same context.

Lines 195 and 229 both introduce "### iOS" and "### Android" sections under the same parent ("## 5. Use deep links between platforms"). This creates ambiguous duplicate headings (MD024 warning).

Recommend renaming the sections to disambiguate:

  • Line 195: Change ### iOS to ### iOS Implementation (or similar variant)
  • Line 229: Change ### Android to ### Android Implementation

Alternatively, elevate one pair to ## if they represent top-level subsections within the deep links guidance.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 195-195: Multiple headings with the same content

(MD024, no-duplicate-heading)


[warning] 229-229: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/integration-expo/references/identify-users.md around lines
195 - 258, The markdown file has duplicate heading levels creating a MD024
warning. The file contains two sets of "### iOS" and "### Android" headings
under the same parent section. To fix this, rename the first or second
occurrence of these duplicate headings to make them unique. For example, change
the first pair to "### iOS Implementation" and "### Android Implementation" or
rename the second pair similarly. Alternatively, elevate one complete pair of
iOS and Android sections to use "## " level headings if they represent distinct
top-level subsections within the deep links guidance section.


## Installation

Our React Native enables you to integrate PostHog with your React Native project. For React Native projects built with Expo, there are no mobile native dependencies outside of supported Expo packages.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix incomplete phrase: add "SDK" after "React Native".

Line reads: "Our React Native enables you to integrate..." — should be "Our React Native SDK enables you to integrate..."

🧰 Tools
🪛 LanguageTool

[style] ~5-~5: This phrase is redundant. Consider using “outside”.
Context: ...there are no mobile native dependencies outside of supported Expo packages. To install, a...

(OUTSIDE_OF)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/integration-expo/references/react-native.md at line 5, The
opening sentence in the React Native integration reference is missing the word
"SDK" which makes it grammatically incomplete. Find the phrase "Our React Native
enables you to integrate" and add "SDK" between "React Native" and "enables" so
it reads "Our React Native SDK enables you to integrate PostHog with your React
Native project."

Comment on lines +42 to +50
- posthog-react-native is the React Native SDK package name (same as bare RN)
- Use expo-constants with app.config.js extras for POSTHOG_PROJECT_TOKEN and POSTHOG_HOST (NOT react-native-config)
- Access config via `Constants.expoConfig?.extra?.posthogProjectToken` in your posthog.ts config file
- For expo-router, wrap PostHogProvider in app/_layout.tsx and manually track screens with `posthog.screen(pathname, params)` in a useEffect
- posthog-react-native is the React Native SDK package name
- Use react-native-config to load POSTHOG_PROJECT_TOKEN and POSTHOG_HOST from .env (variables are embedded at build time, not runtime)
- react-native-svg is a required peer dependency of posthog-react-native (used by the surveys feature) and must be installed alongside it
- Place PostHogProvider INSIDE NavigationContainer for React Navigation v7 compatibility
- When a reverse proxy is configured, both /static/* AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Contradictory configuration guidance—clarify Expo vs. bare React Native patterns.

Lines 43–44 explicitly state "NOT react-native-config" and direct users to expo-constants, but lines 47–48 then direct users to use react-native-config. These instructions conflict directly.

Additionally, line 42 and line 46 are duplicates (both state "posthog-react-native is the React Native SDK package name").

For an Expo-focused skill, either:

  • Remove the react-native-config guidance (lines 47–48) if it's only for bare RN and not Expo, or
  • Clearly separate the Expo pattern (expo-constants) from the bare React Native pattern (react-native-config), labeling each explicitly.

The codebase context shows the implementation uses expo-constants, which aligns with lines 43–44 but contradicts lines 47–48.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 50-50: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)

🪛 SkillSpector (2.2.3)

[error] 47: [PE3] Credential Access: Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Remediation: Remove references to credential paths. Use environment variables or secrets managers. For docs, use placeholder paths (e.g., /path/to/config). Never load .env or token files in production code paths.

(Privilege Escalation (PE3))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/integration-expo/SKILL.md around lines 42 - 50, The SKILL.md
file contains contradictory configuration guidance for PostHog setup. Remove the
duplicate statement about posthog-react-native being the React Native SDK
package name that appears on both line 42 and line 46. Then address the
contradiction between the expo-constants approach (lines 43–44) and the
react-native-config approach (lines 47–48): since this is an Expo-focused skill
file, either remove the react-native-config guidance entirely (lines 47–48) if
it is only applicable to bare React Native projects, or clearly separate the two
patterns with explicit section headers (such as "For Expo:" and "For Bare React
Native:") so developers understand which approach applies to their project type.
The codebase currently implements the expo-constants pattern, so align the
documentation with that approach.

Comment thread .env Outdated
Comment thread app/_layout.tsx
Comment on lines +35 to +43
useEffect(() => {
if (previousPathname.current !== pathname) {
posthog.screen(pathname, {
previous_screen: previousPathname.current ?? null,
...params,
});
previousPathname.current = pathname;
}
}, [pathname, params]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In posthog-react-native v4.50.0, does autocapture.captureScreens: truealready emit screen-view events for Expo Router/React Navigation, and will manualposthog.screen(...) on route changes cause duplicate screen events?

💡 Result:

In posthog-react-native v4.50.0, the behavior of autocapture.captureScreens depends on the navigation library you are using. For Expo Router, official PostHog documentation explicitly advises against using autocapture.captureScreens due to incompatibility issues [1]. Enabling it can cause errors such as "Couldn't get the navigation state" because Expo Router's structure often places the PostHog provider outside of the required navigation context [1]. Instead, you should manually capture screen views by calling posthog.screen(...) inside the components or hooks of your routes [2][3]. Regarding duplicate events, if you are using a navigation setup where automatic tracking is supported (such as React Navigation v6 or lower, or react-native-navigation), enabling autocapture while also manually calling posthog.screen(...) will typically result in duplicate events [2][3]. If you have manually implemented screen tracking, you should set captureScreens: false in your autocapture configuration to prevent this duplication [2][4]. For users on React Navigation v7 or higher, automatic screen tracking is not supported due to architectural changes in the navigation library [5][6]. In these cases, you must manually call posthog.screen(...) [2][6], and no duplication will occur from the autocapture setting because the automatic mechanism does not function [5][6].

Citations:


🏁 Script executed:

# Check package.json for PostHog version and navigation libraries
cat package.json | grep -A 5 -B 5 "posthog\|expo\|navigation"

Repository: aymengmar/react-native-lingua

Length of output: 1864


🏁 Script executed:

# Read the _layout.tsx file to see the full context
head -n 120 app/_layout.tsx

Repository: aymengmar/react-native-lingua

Length of output: 3573


Disable autocapture.captureScreens for Expo Router compatibility.

With Expo Router, enabling autocapture.captureScreens is incompatible and can cause errors because the PostHog provider is placed outside the navigation context required for automatic tracking. For React Navigation v7+ (which this app uses), automatic screen tracking isn't supported anyway. Your manual posthog.screen() calls (lines 35-43) are the correct approach—set captureScreens: false to prevent conflicts.

🔧 Suggested patch
     <PostHogProvider
       client={posthog}
       autocapture={{
-        captureScreens: true,
+        captureScreens: false,
         captureTouches: true,
         propsToCapture: ["testID"],
       }}
     >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_layout.tsx` around lines 35 - 43, The PostHog configuration has
automatic screen capture enabled, which conflicts with Expo Router since the
PostHog provider is placed outside the navigation context. Locate the PostHog
provider initialization code and set the autocapture.captureScreens option to
false. This allows the manual posthog.screen() calls in the useEffect hook
(lines 35-43) to handle screen tracking without interference from automatic
capture that isn't compatible with React Navigation v7+.

Comment thread app/_layout.tsx
Comment on lines +37 to +40
posthog.screen(pathname, {
previous_screen: previousPathname.current ?? null,
...params,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether analytics calls spread raw route/search params into payloads.
fd -e ts -e tsx . app components lib | xargs rg -nP 'posthog\.(screen|capture)\([^)]*\{[^}]*\.\.\.(params|searchParams|query)[^}]*\}'

Repository: aymengmar/react-native-lingua

Length of output: 171


🏁 Script executed:

cat -n app/_layout.tsx | head -60

Repository: aymengmar/react-native-lingua

Length of output: 2539


🏁 Script executed:

# Search for PostHog calls with simpler pattern
rg -n 'posthog\.' app/_layout.tsx -A 3 -B 1

Repository: aymengmar/react-native-lingua

Length of output: 352


🏁 Script executed:

# Check if params filtering/sanitization exists anywhere
rg -n 'sanitized|filter.*params|sensitive' app/_layout.tsx

Repository: aymengmar/react-native-lingua

Length of output: 167


🏁 Script executed:

# Search for all posthog calls in the codebase to see if this pattern is used elsewhere
rg -n 'posthog\.(screen|capture|identify)' app components lib -A 2

Repository: aymengmar/react-native-lingua

Length of output: 4044


🏁 Script executed:

# Check what types of params are used in route definitions or deep links
rg -n 'useGlobalSearchParams|query.*token|query.*code|query.*email' app components -B 2 -A 2

Repository: aymengmar/react-native-lingua

Length of output: 635


Filter sensitive data from URL params before sending to analytics.

Line 39 spreads params from useGlobalSearchParams() directly into posthog.screen() without filtering. URL query parameters—especially from deep links in auth flows—can contain sensitive data like email verification codes, password reset tokens, or other credentials, which will be leaked to analytics.

🔧 Suggested patch (redaction filter before capture)
   useEffect(() => {
     if (previousPathname.current !== pathname) {
+      const sanitizedParams = Object.fromEntries(
+        Object.entries(params).filter(
+          ([key]) => !/(token|code|secret|password|email)/i.test(key)
+        )
+      );
+
       posthog.screen(pathname, {
         previous_screen: previousPathname.current ?? null,
-        ...params,
+        ...sanitizedParams,
       });
       previousPathname.current = pathname;
     }
   }, [pathname, params]);

This aligns with the guideline: "Never expose secret keys or sensitive credentials in the mobile app."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
posthog.screen(pathname, {
previous_screen: previousPathname.current ?? null,
...params,
});
useEffect(() => {
if (previousPathname.current !== pathname) {
const sanitizedParams = Object.fromEntries(
Object.entries(params).filter(
([key]) => !/(token|code|secret|password|email)/i.test(key)
)
);
posthog.screen(pathname, {
previous_screen: previousPathname.current ?? null,
...sanitizedParams,
});
previousPathname.current = pathname;
}
}, [pathname, params]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_layout.tsx` around lines 37 - 40, The posthog.screen() call in the
analytics tracking block spreads all params from useGlobalSearchParams()
directly without filtering, which can expose sensitive data like verification
codes or reset tokens to analytics. Create a helper function to filter out known
sensitive parameters (such as those containing tokens, codes, credentials, or
verification-related keys) and apply this filter to params before spreading them
into the posthog.screen() call. Only send non-sensitive, safe parameters to the
analytics service.

Source: Coding guidelines

Comment thread app/(tabs)/profile.tsx
Comment on lines 15 to 20
const handleSignOut = async () => {
posthog.capture('user_signed_out');
posthog.reset();
await signOut();
router.replace('/(auth)/sign-in');
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file and the specific function
cat -n app/\(tabs\)/profile.tsx | head -30

Repository: aymengmar/react-native-lingua

Length of output: 1418


🏁 Script executed:

# Search for the signOut function to understand its behavior
rg -B 3 -A 10 'signOut\s*=' --type ts --type tsx | head -50

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Also check how posthog is imported and used in the file
rg -B 2 -A 5 'posthog' app/\(tabs\)/profile.tsx

Repository: aymengmar/react-native-lingua

Length of output: 890


🏁 Script executed:

# Look for signOut implementation in the codebase
fd -e ts -e tsx -exec rg -l 'export.*signOut|function signOut' {} \;

Repository: aymengmar/react-native-lingua

Length of output: 384


🏁 Script executed:

# Search for signOut usage patterns in the codebase
rg 'signOut' --type ts --type tsx -B 2 -A 2

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Check if there's any error handling related to auth operations
rg 'try\s*{' --type ts --type tsx -A 15 | grep -A 15 'signOut\|auth'

Repository: aymengmar/react-native-lingua

Length of output: 245


🌐 Web query:

Clerk expo signOut error handling behavior throws exception

💡 Result:

In the Clerk Expo SDK, the signOut function is an asynchronous operation that returns a Promise [1][2]. Its behavior regarding exception throwing and error handling can be summarized as follows: 1. Asynchronous Error Handling: Because signOut returns a Promise, it can reject if an error occurs during the sign-out process (e.g., network issues or API failures) [3]. You should always wrap calls to signOut in a try/catch block to handle these potential exceptions [3]. 2. Development and Environment Context: - Web vs. Native: The signOut function is designed for both web and native environments, but specific implementation details differ [4]. In some earlier or beta versions, developers reported issues where signOut attempted to access browser-specific objects (like window), which caused errors in native environments [5]. Current, stable versions of the SDK are designed to handle native environments correctly. - Native State Sync: The SDK maintains synchronization between the JavaScript layer and native modules (iOS/Android). Internal native-side sign-out logic is designed to catch and handle errors to ensure that the application's state, such as SecureStore and keychain items, is cleared even if the primary sign-out request fails [6][7]. 3. Recommended Implementation: When building a custom sign-out flow, follow this pattern to ensure robust error handling [3]: async function handleSignOut { try { await signOut; // Perform successful navigation or UI updates } catch (err) { // Log the error or handle it based on the ClerkError interface console.error("Sign-out failed:", err); } } If you encounter specific "already signed in" or session-related errors after signing out, ensure your project is updated to the latest version of @clerk/expo, as recent patches have explicitly addressed issues where stale JWTs in SecureStore could prevent successful re-authentication [6][7].

Citations:


🌐 Web query:

posthog reset timing analytics session sign out

💡 Result:

To manage PostHog analytics sessions during a sign-out event, you should use the posthog.reset method [1][2][3]. This function is specifically designed to be called when a user logs out to ensure that future events on that device are not incorrectly associated with the previous user [1][4]. What posthog.reset does: - It clears the current Session ID and associated super properties [2][3]. - It clears user identification and generates a new, random distinct_id for the subsequent anonymous session [2][3]. - It clears cached data and consent settings [2][3]. - It starts a fresh session, which also triggers a new session replay [5]. Usage and Recommendations: - Call posthog.reset immediately upon logout to prevent data merging across different users who might share the same computer or browser [1][4]. - By default, posthog.reset creates a new distinct ID but keeps the existing device ID. If you need to treat the device as completely new (e.g., for privacy or security reasons), you can call posthog.reset(true), which will also reset the device_id [1][2][3]. - Use this method only when a user explicitly logs out. Calling it at other times may cause unintended split sessions [2][3]. Note on Implementation: Some users have reported potential issues with event capturing or network recording after calling reset in specific SDK versions [6][7]. If you experience dropped events or recording issues, ensure you are on the latest SDK version, as these are tracked as bugs in the library repository [6][7].

Citations:


🏁 Script executed:

# Check if clearSelectedLanguage is used elsewhere in sign-out flow
rg 'clearSelectedLanguage' --type ts --type tsx -B 2 -A 2

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Look for other auth-related error handling in the codebase
rg 'useAuth\|signOut' --type ts --type tsx -A 5 | head -80

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Try without the type specification for tsx files
rg 'clearSelectedLanguage' -B 2 -A 2

Repository: aymengmar/react-native-lingua

Length of output: 167


🏁 Script executed:

# Check other parts of the codebase for signOut error handling patterns
rg 'signOut' -A 5 | head -100

Repository: aymengmar/react-native-lingua

Length of output: 167


🏁 Script executed:

# List the files in the repository to understand structure better
find . -type f -name "*.tsx" -o -name "*.ts" | head -20

Repository: aymengmar/react-native-lingua

Length of output: 894


🏁 Script executed:

# Search for clearSelectedLanguage in any file
grep -r "clearSelectedLanguage" . 2>/dev/null

Repository: aymengmar/react-native-lingua

Length of output: 470


🏁 Script executed:

# Search for other signOut usages
grep -r "signOut" . --include="*.tsx" --include="*.ts" 2>/dev/null

Repository: aymengmar/react-native-lingua

Length of output: 540


🏁 Script executed:

# Check how the profile screen uses clearSelectedLanguage and if it relates to auth
cat -n ./app/\(tabs\)/profile.tsx | head -80

Repository: aymengmar/react-native-lingua

Length of output: 2963


🏁 Script executed:

# Check the languageStore to understand if it's auth-related
cat -n ./store/languageStore.ts

Repository: aymengmar/react-native-lingua

Length of output: 1371


Add error handling to sign-out flow and ensure analytics reset is called correctly.

The signOut() call can fail with network or API errors (per Clerk documentation), but the current code has no error handling. If signOut() fails, the user remains signed in but posthog.reset() has already executed, creating an inconsistent state. Wrap the flow in try/catch and ensure reset happens after successful sign-out. Also consider calling clearSelectedLanguage() for complete user state cleanup.

Suggested fix
   const handleSignOut = async () => {
-    posthog.capture('user_signed_out');
-    posthog.reset();
-    await signOut();
-    router.replace('/(auth)/sign-in');
+    try {
+      posthog.capture('user_signed_out');
+      await signOut();
+      posthog.reset();
+      clearSelectedLanguage();
+      router.replace('/(auth)/sign-in');
+    } catch (error) {
+      console.error('Sign-out failed:', error);
+      posthog.captureException(error as Error);
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(tabs)/profile.tsx around lines 15 - 20, The handleSignOut function
needs error handling for the signOut() call which can fail per Clerk
documentation. Wrap the entire async flow in a try/catch block. Move the
posthog.reset() call to execute only after a successful signOut() to prevent
inconsistent state where the user remains signed in but analytics are cleared.
Additionally, add a call to clearSelectedLanguage() within the try block for
complete user state cleanup. In the catch block, handle any errors appropriately
(consider logging or showing an error message to the user) and ensure navigation
does not occur if sign-out fails.

if (!onResend || loading) return;
setLoading(true);
setError("");
posthog.capture("email_verification_code_resent", { email });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Track resend success only after the resend call succeeds.

email_verification_code_resent is emitted before await onResend(), so failed attempts are counted as successful resends. Emit this event after a successful await, and optionally add a separate failure event in catch.

Proposed fix
   const handleResend = async () => {
     if (!onResend || loading) return;
     setLoading(true);
     setError("");
-    posthog.capture("email_verification_code_resent", { email });
     try {
       await onResend();
+      posthog.capture("email_verification_code_resent", { email });
     } catch (err: any) {
       const message =
         err?.errors?.[0]?.message || "Failed to resend code.";
+      posthog.capture("email_verification_resend_failed", {
+        email,
+        error_message: message,
+      });
       setError(message);
     } finally {
       setLoading(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/VerificationModal.tsx` at line 82, The posthog.capture call for
"email_verification_code_resent" is currently being emitted before awaiting the
onResend() call, which means failed resend attempts are incorrectly tracked as
successful. Move the posthog.capture("email_verification_code_resent", { email
}) event to execute only after the await onResend() completes successfully.
Additionally, consider adding a separate failure event (such as
"email_verification_code_resent_failed") in the catch block to track
unsuccessful resend attempts.

Comment thread posthog-setup-report.md
- [ ] Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code.
- [ ] Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures.
- [ ] Add `POSTHOG_PROJECT_TOKEN` and `POSTHOG_HOST` to `.env.example` and any bootstrap scripts so collaborators know what to set.
- [ ] Confirm the returning-visitor path also calls `identify` — currently `identify` is only called on fresh sign-in/sign-up completion; returning sessions restored from Clerk's token cache will be on anonymous distinct IDs until the user signs in again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where identify is called and whether app-boot/auth-restored paths include it.
fd -e ts -e tsx . app lib | xargs rg -nP '\bidentify\s*\('
fd -e ts -e tsx _layout app | xargs rg -nP 'useAuth|useUser|isSignedIn|tokenCache|onRehydrate|_hasHydrated'

Repository: aymengmar/react-native-lingua

Length of output: 1285


🏁 Script executed:

cat -n app/_layout.tsx

Repository: aymengmar/react-native-lingua

Length of output: 4350


🏁 Script executed:

# Search for all identify calls in the entire codebase, not just app/lib
rg -n 'identify\s*\(' --type ts --type tsx

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Look for useEffect hooks that might handle auth state changes
ast-grep outline app/_layout.tsx

Repository: aymengmar/react-native-lingua

Length of output: 325


🏁 Script executed:

# Search for all identify calls across the entire repo
rg -n 'identify' --type ts --type tsx

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Check if there's a custom hook or utility handling Clerk auth changes
fd -name '*.ts' -o -name '*.tsx' | xargs rg -l 'useAuth|Clerk' | head -20

Repository: aymengmar/react-native-lingua

Length of output: 637


🏁 Script executed:

# Search for identify calls more broadly
rg 'identify' app/

Repository: aymengmar/react-native-lingua

Length of output: 277


🏁 Script executed:

# Check the sign-in and sign-up files to see context
cat -n app/'(auth)'/sign-in.tsx | grep -A 5 -B 5 identify

Repository: aymengmar/react-native-lingua

Length of output: 621


🏁 Script executed:

# Check if there's any auth listener or effect in other files
rg 'useAuth\|useUser' app/ --type ts --type tsx -l

Repository: aymengmar/react-native-lingua

Length of output: 195


🏁 Script executed:

# Check all files that use useAuth
rg 'useAuth' app/ -l

Repository: aymengmar/react-native-lingua

Length of output: 229


🏁 Script executed:

# Look for any PostHog identify in the entire codebase
find app -name "*.tsx" -o -name "*.ts" | xargs rg 'identify'

Repository: aymengmar/react-native-lingua

Length of output: 277


🏁 Script executed:

# Check if there's a Clerk auth listener or setup elsewhere
find . -name "*.tsx" -o -name "*.ts" | xargs rg 'onSignIn|onSignUp|onClerkLoaded'

Repository: aymengmar/react-native-lingua

Length of output: 171


🏁 Script executed:

# Check profile.tsx to see how it uses useAuth
cat -n app/'(tabs)'/profile.tsx | head -80

Repository: aymengmar/react-native-lingua

Length of output: 2963


🏁 Script executed:

# Check auth/_layout.tsx to see its full content
cat -n app/'(auth)'/_layout.tsx

Repository: aymengmar/react-native-lingua

Length of output: 565


Add identify() call on app boot for already-authenticated sessions to close the returning-session identity gap.

The codebase currently calls identify() only during fresh sign-in/sign-up flows (sign-in.tsx line 49, sign-up.tsx line 63). Users restored from Clerk's tokenCache on app boot will have isSignedIn=true but will NOT trigger identify(), leaving them tracked under anonymous distinct IDs until their next manual sign-in. This breaks funnel attribution and retention cohorts.

Add an identify() call in the _layout.tsx useEffect (lines 45-63) when isSignedIn transitions from false to true or on initial app boot with an existing session, ensuring all authenticated users—whether fresh or restored from token cache—are properly identified in PostHog.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@posthog-setup-report.md` at line 38, The identify() function is currently
only called in sign-in.tsx and sign-up.tsx during fresh authentication flows,
but users restored from Clerk's tokenCache on app boot are not being identified,
leaving them under anonymous distinct IDs. In the _layout.tsx file, locate the
useEffect hook (around lines 45-63) and add an identify() call that triggers
when isSignedIn becomes true, particularly to handle the case where a user
already has an authenticated session restored from the token cache. This ensures
all authenticated users—both fresh and returning from token cache—are properly
identified in PostHog rather than only those completing a manual sign-in flow.

@aymengmar
aymengmar merged commit eeff3fa into main Jun 23, 2026
1 check was pending
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