Skip to content

posthog-react-native: expo-file-system detection broken on Expo SDK 54 stable #3151

@ddelange

Description

@ddelange

Bug description

posthog-react-native@4.36.0 throws Error: Method writeAsStringAsync imported from "expo-file-system" is deprecated on every storage write when used with Expo SDK 54 (stable).

This is distinct from #2229 (SDK 54 beta), which was fixed in #2234. The fix checks whether readAsStringAsync exists on the expo-file-system module to decide between the legacy and new File API paths:

// native-deps.js (simplified)
if (expoFileSystemLegacy.readAsStringAsync) {
  return buildLegacyStorage(_filesystem); // uses writeAsStringAsync
}
// else: use new File(uri) / file.write(value)

In SDK 54 beta, the legacy methods were completely removed (undefined), so this check correctly fell through to the new API.

In SDK 54 stable, the legacy methods are still exported as functions — they just throw a deprecation error when called (source). So the existence check passes, PostHog enters the legacy path, and writeAsStringAsync throws.

The require('expo-file-system/legacy') fallback (Step 1 in the detection logic) also doesn't resolve at runtime — the package ships legacy.ts but no legacy.js at the root, and PostHog's compiled JS can't resolve the TypeScript file.

How to reproduce

  1. Create an Expo SDK 54 app (stable, not beta)
  2. Install posthog-react-native@4.36.0
  3. Set up PostHogProvider with default options (no customStorage)
  4. Run the app — errors appear on every capture(), identify(), or any operation that persists to storage

Workaround

Pass a custom storage provider to bypass the detection logic entirely:

import { MMKV } from 'react-native-mmkv';

const storage = new MMKV({ id: 'posthog' });

<PostHogProvider
  apiKey="..."
  options={{
    customStorage: {
      getItem: (key) => storage.getString(key) ?? null,
      setItem: (key, value) => storage.set(key, value),
    },
  }}
>

Suggested fix

The detection should test whether the function actually works, not just whether it exists. For example:

try {
  // Try calling readAsStringAsync with a non-existent path
  // If it throws with the deprecation message, fall through to new API
  await _filesystem.readAsStringAsync('__posthog_probe__');
} catch (e) {
  if (e.message?.includes('deprecated')) {
    // Functions exist but throw — use new File API
    return buildNewFileStorage(_filesystem);
  }
  // Other error (e.g., file not found) — legacy API works, use it
  return buildLegacyStorage(_filesystem);
}

Or more simply, check _filesystem.Paths to detect the new API:

if (_filesystem.Paths && _filesystem.File) {
  return buildNewFileStorage(_filesystem);
}
if (_filesystem.readAsStringAsync) {
  return buildLegacyStorage(_filesystem);
}

Related sub-libraries

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react
  • @posthog/ai
  • @posthog/nextjs-config

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions