Skip to content

fix(analytics-react-native): lazy-load AsyncStorage so customers can opt out (SDKRN-8)#1772

Open
Mercy811 wants to merge 18 commits into
mainfrom
xinyiye/sdkrn-8-improve-asyncstorage-support-in-analytics-react-native
Open

fix(analytics-react-native): lazy-load AsyncStorage so customers can opt out (SDKRN-8)#1772
Mercy811 wants to merge 18 commits into
mainfrom
xinyiye/sdkrn-8-improve-asyncstorage-support-in-analytics-react-native

Conversation

@Mercy811
Copy link
Copy Markdown
Contributor

@Mercy811 Mercy811 commented May 22, 2026

Summary

Replace the static import AsyncStorage from '@react-native-async-storage/async-storage' at the top of packages/analytics-react-native/src/storage/local-storage.ts with a lazy require wrapped in try/catch. When the package can't be resolved, LocalStorage.isEnabled() returns false and the existing fallback chain in createFlexibleStorage drops down to MemoryStorage instead of throwing at module-load.

Linear: SDKRN-8

Problem

@react-native-async-storage/async-storage is currently a regular dependency and the SDK statically imports it. Customers who:

  • provide their own config.storageProvider (e.g. react-native-mmkv), and
  • want to exclude AsyncStorage from their native build (to drop the redundant native module / shrink APK / avoid Expo Doctor duplicate warnings / avoid pnpm autolinking edge cases)

…still hit:

Error: [@RNC/AsyncStorage]: NativeModule: AsyncStorage is null

Reported across #266, #540, #1283, #1595 and several customer support threads.

Reproduce steps

  1. pull latest main
  2. cd examples/react-native-app
  3. remove @react-native-async-storage/async-storage in package.json
  4. cd ios && bundle install && bundle exec pod install
  5. cd examples/react-native-app && pnpm start to start metro
  6. in another terminal pnpm ios
Screenshot 2026-05-25 at 6 06 39 PM

What changed

  • packages/analytics-react-native/src/storage/local-storage.ts: lazy-resolve AsyncStorage inside a getAsyncStorage() helper. Each method checks for availability before calling. The require is wrapped in try/catch so a missing JS package no longer crashes module-load. The existing try/catch around individual storage calls still handles the "JS present but native module null" case.
  • packages/analytics-react-native/README.md: document the opt-out recipe (custom storageProvider + react-native.config.js autolinking exclusion).
  • packages/analytics-react-native/test/storage/local-storage.test.ts: new test verifying LocalStorage degrades to a no-op when the package can't be resolved (jest.resetModules + jest.doMock with a throwing factory).

No package.json changes — @react-native-async-storage/async-storage remains a regular dependency, so the default install path is unchanged and existing customers keep their identity continuity.

Customer opt-out recipe (now documented in README)

// 1. Provide a custom storage provider
import { init } from '@amplitude/analytics-react-native';
init(API_KEY, { storageProvider: myMMKVProvider });
// 2. Exclude AsyncStorage from native autolinking
// react-native.config.js
module.exports = {
  dependencies: {
    '@react-native-async-storage/async-storage': {
      platforms: { ios: null, android: null },
    },
  },
};

The JS package stays in node_modules (~1 KB) but is never invoked; the iOS and Android binaries no longer include AsyncStorage.

Out of scope

  • Widening the async-storage version range pin — tracked separately in SDKRN-3.
  • Major version bump / removing async-storage entirely — explicitly decided against in the design discussion; would break user/device ID continuity for the default-install majority.

Test plan

  • pnpm test — 88 tests pass, coverage thresholds met (statements 86.66%, branches 84.32%, functions 87%, lines 86.77%)
  • pnpm typecheck — clean
  • pnpm lint — clean (8 pre-existing warnings in unrelated files)
  • Manual: install in the RN example app with storageProvider override + react-native.config.js exclusion, confirm no NativeModule: AsyncStorage is null error and that events still queue/flush.
  • Manual: install in the RN example app without any overrides, confirm identity + event persistence still work as on main.

🤖 Generated with Claude Code


Note

Medium Risk
Changes default persistence resolution for all RN customers when AsyncStorage is missing or native-linked incorrectly; wrong error handling could silently drop identity/event persistence without crashing.

Overview
Replaces the top-level AsyncStorage import in local-storage.ts with a cached lazy require, so apps can exclude the native module without crashing at SDK load. LocalStorage now treats a missing package, load failures (with selective console.warn), and null native bridge errors as disabled persistence—isEnabled() is false and read/write paths no-op or return undefined, including getRaw for identity migration callers.

Documents the full AsyncStorage opt-out (override storageProvider and cookieStorage, autolinking exclusion, RN Web stub/alias patterns) in the package README.

Adds unit tests for missing package, broken installs, and native-bridge rejection; wires the RN example app with in-memory storage, autolinking exclusion, expanded Maestro smoke (boot + track/identify), and removes RNCAsyncStorage from the example Podfile.lock.

Reviewed by Cursor Bugbot for commit 36c1d53. Bugbot is set up for automated code reviews on this repo. Configure here.

…opt out (SDKRN-8)

Replace the static `import AsyncStorage from '@react-native-async-storage/async-storage'`
at the top of `local-storage.ts` with a lazy `require` wrapped in try/catch. When
the package isn't resolvable (e.g. the customer has supplied their own
`storageProvider` and excluded AsyncStorage from native autolinking via
`react-native.config.js`), `LocalStorage.isEnabled()` returns false and the
existing fallback chain in `createFlexibleStorage` drops down to `MemoryStorage`
instead of throwing at module-load.

Document the opt-out recipe in the README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 22, 2026

SDKRN-8

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

size-limit report 📦

Path Size
packages/analytics-browser/lib/scripts/amplitude-min.js.gz 58.31 KB (0%)
packages/session-replay-browser/lib/scripts/session-replay-browser-min.js.gz 131.96 KB (0%)
packages/unified/lib/scripts/amplitude-min.umd.js.gz 208.94 KB (0%)

…KRN-8)

- Document why getAsyncStorage caches resolution permanently (no retry):
  require() is synchronous and RN's module registry is stable for the app
  lifetime, so retrying after failure would never produce a different result.
- Strengthen the "no AsyncStorage installed" test: factory is now a jest.fn()
  that we assert is called exactly once, proving each storage method
  short-circuits on the cached undefined value instead of re-attempting
  require() or invoking methods on undefined.
- Fix README: Metro does not tree-shake. Reword RN Web step to point at
  webpack aliasing instead.
- Consolidate the four eslint-disable-next-line comments into a single
  block directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the React Native SDK’s local storage implementation to avoid crashing at module-load time when @react-native-async-storage/async-storage is not available/linked, enabling customers to opt out of AsyncStorage when providing a custom storageProvider.

Changes:

  • Replaced the static AsyncStorage import with a lazy require() (cached) guarded by availability checks.
  • Added a Jest test to ensure LocalStorage becomes a no-op when AsyncStorage cannot be resolved.
  • Documented the recommended opt-out steps (custom storageProvider + native autolinking exclusion) in the RN package README.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/analytics-react-native/src/storage/local-storage.ts Lazy-resolve AsyncStorage and short-circuit storage operations when unavailable.
packages/analytics-react-native/test/storage/local-storage.test.ts Adds coverage for the “module cannot be resolved” degradation path.
packages/analytics-react-native/README.md Documents how to opt out of AsyncStorage in native builds while using a custom provider.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/analytics-react-native/src/storage/local-storage.ts Outdated
Comment thread packages/analytics-react-native/src/storage/local-storage.ts Outdated
…RN-8)

- Add try/catch in LocalStorage.getRaw so direct callers like
  parseOldCookies (cookie-migration/index.ts:10) don't propagate a native
  bridge "AsyncStorage is null" error when the JS package is present but
  the native module is opted out.
- Discriminate require() errors in getAsyncStorage: only MODULE_NOT_FOUND
  is silently swallowed (the supported opt-out path). Any other error
  (broken install, syntax error, permission issue) is surfaced via
  console.warn so misconfigurations can be diagnosed instead of silently
  degrading to in-memory storage.
- Test additions:
  - Update the "module not resolvable" test to throw a real MODULE_NOT_FOUND
    error and assert no warning is logged.
  - New test: non-MODULE_NOT_FOUND errors produce a warning and still
    degrade to no-op.
  - New test: getRaw swallows native bridge errors when the JS package is
    resolvable but methods reject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread packages/analytics-react-native/README.md Outdated
Comment thread packages/analytics-react-native/README.md Outdated
Comment thread packages/analytics-react-native/src/storage/local-storage.ts Outdated
…ut recipe (SDKRN-8)

Address Copilot review comments 1 and 2 from round 2:

- Customers who only override `storageProvider` still hit AsyncStorage via
  the default cookieStorage fallback chain. Document both slots and the
  trade-off if only one is overridden (identity degrades to in-memory and
  resets per launch).
- Soften the "never invoked" claim: AsyncStorage methods aren't invoked
  only when both storage slots are overridden. The JS package staying in
  node_modules is just about `require()` resolvability, not call patterns.

Pushed back on comment 3 (silent MODULE_NOT_FOUND) in the PR thread —
broken-install case is already covered via console.warn for non-MNF
errors; silent fallback is intentional for the supported opt-out path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread packages/analytics-react-native/src/storage/local-storage.ts Outdated
…ckage only (SDKRN-8)

Address Copilot review round 3:

Node sets `code === 'MODULE_NOT_FOUND'` for *any* missing module in the
require() chain — including transitive deps of @react-native-async-storage/
async-storage itself. The previous check would silently disable persistence
on a broken transitive install, masking the bug.

Tighten the silent-opt-out condition: the error message must specifically
mention `@react-native-async-storage/async-storage`. Anything else (broken
transitive, syntax error, etc.) hits the console.warn path so customers can
diagnose it.

Add a test for the transitive-MODULE_NOT_FOUND scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Mercy811
Copy link
Copy Markdown
Contributor Author

@bug Bot

@Mercy811
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e187a6f. Configure here.

Mercy811 and others added 3 commits May 25, 2026 10:28
…stro (SDKRN-8)

Make the React Native example app a regression test for the SDKRN-8 opt-out
recipe. The app now:

  - Excludes RNCAsyncStorage from native autolinking via
    react-native.config.js (so the iOS / Android binary doesn't link it).
  - Overrides both `storageProvider` and `cookieStorage` with an in-memory
    Storage implementation defined inline in App.tsx, fully bypassing the
    SDK's default LocalStorage chain.
  - Calls init with the correct (apiKey, userId, options) signature — pass
    `undefined` for userId so the overrides land in the options slot rather
    than being silently bound to userId.

Extend .maestro/smoke.yaml to tap both Track Event and Track Identify
buttons and assert each one produces an "Amplitude Response" toast. If the
SDK regresses to a static `import AsyncStorage` or any code path that
touches the native module without graceful handling, the app crashes at
boot with `[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null` and
none of the new assertions can pass.

Validated locally on iPhone 16 simulator (iOS 18.6) — Release build, all
Maestro steps pass, simctl log confirms no AsyncStorage errors. The
`.github/workflows/rn-smoke.yml` workflow runs the same flow on iPhone 15
(macos-14), so the regression guard ships with the PR.

Also fix the README opt-out snippet to use the correct three-arg init
signature (apiKey, userId, options) — the previous two-arg example was
misleading and would silently fall back to the default storage chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…js (SDKRN-8)

The example app's eslint+prettier config strips spaces inside inline
object literals. Match the rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n (SDKRN-8)

The smoke.yaml comment claimed @react-native-async-storage/async-storage
was removed from the example app's package.json. It isn't — we kept it
in the example app's deps so the lockfile doesn't churn. The opt-out is
delivered entirely by the autolinking exclusion + storage-slot overrides.

Update the comment to reflect that, and add an explicit note that the
JS package is still resolvable but the native module is null — which is
the trigger condition for the regression we're guarding against.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Mercy811
Copy link
Copy Markdown
Contributor Author

Re: code-reviewer Should-fix on InMemoryStorage.getRaw (App.tsx) — pushing back on this one.

The reviewer flagged JSON.stringify(value) as a "subtly wrong reference implementation" that would double-encode strings. But this is the canonical pattern used by the SDK's own MemoryStorage<T> at packages/analytics-core/src/storage/memory.ts:13-16:

async getRaw(key: string): Promise<string | undefined> {
  const value = await this.get(key);
  return value ? JSON.stringify(value) : undefined;
}

My InMemoryStorage in the example app mirrors that pattern exactly, which is intentional — the example should match how the SDK's own in-memory backend behaves, not invent a different shape. Customers copying this pattern get the same getRaw semantics as the SDK's built-in fallback.

The "double-encoded string" concern is also moot in practice for this example app: storageProvider stores Event[] (objects) and cookieStorage stores UserSession (an object), so the string-primitive case doesn't apply to either slot.

Addressed Nit 1 (toast lingering between Track Event and Track Identify) in the next commit by waiting for the first toast to dismiss before tapping. Locally re-verified — all Maestro steps pass.

Keeping Nit 2 (README "Common gotchas" SSR note) as-is — RN Web in Next.js is a real customer setup and the gotcha is genuine for those users. Trimming the note would lose useful information.

…ps (SDKRN-8)

Address code-reviewer Nit: the second `extendedWaitUntil: Amplitude
Response` could be satisfied by the lingering toast from the previous tap
instead of waiting for a new toast from `Track Identify`. Add an explicit
`extendedWaitUntil: notVisible` between the two button taps so the second
assertion actually proves the second SDK call produced its own toast.

react-native-toast-message auto-dismisses after ~4s, well within the
10s timeout.

Pushback on the Should-fix (InMemoryStorage.getRaw) is in the PR thread —
the pattern matches the SDK's own canonical `MemoryStorage<T>`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated no new comments.

@Mercy811
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f42f4b4. Configure here.

@Mercy811 Mercy811 marked this pull request as ready for review May 25, 2026 18:06
…Maestro flakiness (SDKRN-8)

The previous App.tsx tapped track()/identify() and showed the toast only
inside the .then of the returned promise. That promise resolves once the
event flushes to api2.amplitude.com — fast on a dev mac (~1-3s) but slow
or unreachable on macos-14 CI runners. The Maestro flow timed out waiting
for the toast in the CI iOS smoke job:

  Tap on "Track Event"... COMPLETED
  Assert that "Amplitude Response" is visible... FAILED  (10s timeout)

The SDK itself is fine — sim logs show no AsyncStorage error and the
title renders before the failed assertion. The flakiness is purely about
network timing.

Show the toast immediately on tap and route the SDK's eventual response
(or rejection) to a follow-up toast. The regression we actually care
about — that track()/identify() don't crash the app when AsyncStorage
isn't linked — is what the immediate toast verifies. The follow-up
toast still surfaces the real SDK response when network is healthy
(local dev).

Update the smoke.yaml comment to reflect that the assertion now proves
the synchronous SDK call returned, not the full promise pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
if (resolved) {
return asyncStorage;
}
resolved = true;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Having a "resolved" variable is redundant. You can just say:

if (asyncStorage) {
  return asyncStorage;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think I understand now, resolved could also mean it failed to load and we don't wish to try again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In a clean full-opt-out the scenario it is indeed dead code because getAsyncStorage() will never be called. But in the partial-opt-out case where, for example, it gets called from the cookieStorage fallback and async-storage is also missing, Metro doesn't cache failed resolutions, so without the flag we'd re-throw and re-log on every storage operation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes!

resolved = true;
/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
try {
const mod = require('@react-native-async-storage/async-storage');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that's async, it's better to be sync to keep the existing storage methods clean.

//
// The result is cached for the app lifetime: `require()` is synchronous in
// React Native and the module registry is stable, so retrying after a failed
// resolution would never produce a different result.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think these comments are a tad verbose. A lot of this information could and should be gleaned from just looking at the source code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd still like to keep them just for readability if you don't have strong opinion on this. No harm of having comments IMHO

// directly without a `default` wrapper.
asyncStorage = mod?.default ?? mod;
} catch (e) {
asyncStorage = undefined;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Optional: you could set this to null to indicate that it's not available at all, then you wouldn't need the resolved variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I like this for simplicity! Updated at 79f9d9a

Comment thread packages/analytics-react-native/README.md Outdated
Comment thread packages/analytics-react-native/README.md Outdated

AsyncStorage is no longer linked into your iOS or Android binaries. The JS package stays in `node_modules` so `require()` still resolves, but with both storage slots overridden, the SDK never invokes any AsyncStorage methods.

3. For React Native Web, exclude AsyncStorage from your web bundle.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This feels like overkill to me. From looking at Bundlephobia it looks like async-storage has a min+GZ size of 0.25 kilobytes. Could we at least maybe move this to another .md that's something like an Advanced Usage markdown file?

Copy link
Copy Markdown
Contributor Author

@Mercy811 Mercy811 May 25, 2026

Choose a reason for hiding this comment

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

I will move them to dev doc center. It doesn't affect the mini file and the gzip size. It only affects the tarball size which adds download cost of npm install this package.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment thread packages/analytics-react-native/README.md Outdated
@daniel-graham-amplitude
Copy link
Copy Markdown
Collaborator

@Mercy811 can I confirm that you were able to:

  1. successfully run the example app without async-storage autolinking
  2. successfully reproduce the import error
  3. successfully observe that your changes caused the import error to stop happening

cursoragent and others added 2 commits May 25, 2026 18:53
…e backend'

Co-authored-by: Xinyi Ye <xinyi.ye@amplitude.com>
…a value

Co-authored-by: Xinyi Ye <xinyi.ye@amplitude.com>
@Mercy811
Copy link
Copy Markdown
Contributor Author

Mercy811 commented May 25, 2026

@Mercy811 can I confirm that you were able to:
successfully run the example app without async-storage autolinking
successfully reproduce the import error
successfully observe that your changes caused the import error to stop happening

@daniel-graham-amplitude that's right, by the example app. And also added a maestro test to prevent regression too. Enriched PR description.

Mercy811 and others added 6 commits May 25, 2026 12:15
…cs site (SDKRN-16)

The opt-out recipe now lives in the React Native SDK developer docs:
https://amplitude.com/docs/sdks/analytics/react-native/react-native-sdk#opt-out-of-asyncstorage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rage (SDKRN-8)

Address review feedback: collapse the `resolved` boolean and `asyncStorage`
variable into a single tri-state variable using `null` to mark "tried and
unavailable". Widen `getAsyncStorage`'s return type to `AsyncStorageLike |
null | undefined` so callers (all in this file, all using `if (!storage)`
truthy checks) don't need any changes — TypeScript narrows the union out
on the falsy branch.

Net change: one fewer module-level variable, no `?? undefined` adapter
required at the return sites, identical behavior, identical test coverage
(jest.fn factory still invoked exactly once on the failed-resolution path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ke test (SDKRN-8)

The previous smoke test relied on react-native.config.js to exclude
RNCAsyncStorage from autolinking. That config-driven path turns out not
to exercise the customer-reported failure mode — RN CLI / Metro stubs
the JS package when autolinking is excluded by config, so the package's
top-level NativeModule-null throw is never even in the bundle. The test
passed regardless of whether the SDK lazy-loaded the require.

Switch the example app to the actual customer-shaped recipe — also the
recipe we'll document on the dev docs site:

  1. Don't depend on @react-native-async-storage/async-storage in your
     own package.json. In a pnpm setup, this keeps the package out of
     top-level node_modules, which keeps RN autolinking from finding
     the native module, which keeps RNCAsyncStorage out of the iOS
     binary (verifiable: `grep -c RNCAsyncStorage ios/Podfile.lock` = 0).

  2. Pass your own storageProvider AND cookieStorage in init(). With
     both slots overridden, the SDK never reaches the default
     LocalStorage fallback chain, so the lazy require of async-storage
     is never triggered.

Changes:
- Delete examples/react-native/app/react-native.config.js — no longer
  needed (and was masking the actual failure mode).
- Drop @react-native-async-storage/async-storage from
  examples/react-native/app/package.json.
- Regenerate pnpm-lock.yaml.
- Update .maestro/smoke.yaml header comments to describe the new recipe.

Validated locally on iPhone 16 simulator (iOS 18.6) — Release build,
Maestro flow runs to completion with all 9 steps passing, sim log shows
zero AsyncStorage / NativeModule errors. ios/Podfile.lock has 0
references to RNCAsyncStorage.

This is also a tighter regression guard: if the SDK regresses to its
pre-PR static \`import AsyncStorage from ...\`, the app will crash at
boot with \`[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null\`,
which the Maestro flow will catch (the title assertion fails before any
tap step runs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…L parse (SDKRN-8)

Previous comment-block edit accidentally included a markdown blockquote
character at the start of a comment line, which Maestro's YAML parser
rejects:

  Parsing Failed at .../smoke.yaml:2:4

iOS Maestro CI failed for that reason on the last push. This is the
one-character fix to unbreak the parse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SDKRN-8)

Mirror the .env / VITE_AMPLITUDE_API_KEY pattern used by the Playwright
E2E tests (see .github/actions/e2e-test/action.yml). The Maestro smoke
test now sends events end-to-end to a real Amplitude project under our
test API key when running in CI.

Changes:
- examples/react-native/app/babel.config.js: enable
  babel-plugin-transform-inline-environment-variables for the single
  whitelisted var AMPLITUDE_API_KEY. The plugin inlines the value as a
  string literal in the JS bundle at Metro bundle time.
- examples/react-native/app/App.tsx: read process.env.AMPLITUDE_API_KEY
  with a 'YOUR_API_KEY' literal fallback so the example app is still
  self-contained without any env set.
- examples/react-native/app/package.json + pnpm-lock.yaml: add
  babel-plugin-transform-inline-environment-variables to devDependencies.
- .github/workflows/rn-smoke.yml: pass AMPLITUDE_API_KEY=${{
  secrets.AMPLITUDE_API_KEY }} as env to the xcodebuild step. With no
  secret, the fallback 'YOUR_API_KEY' is bundled and the crash-guard
  Maestro assertions still pass; only the api2 round-trip is skipped.

Local dev:
  export AMPLITUDE_API_KEY=…   # then pnpm ios / xcodebuild
  # (or use direnv for repo-scoped scoping)

Verified locally on iPhone 16 / iOS 18.6 Release build:
- With no env: bundle contains 'YOUR_API_KEY', no leaked real key.
- With env set: bundle contains the real key, 'YOUR_API_KEY' literal
  is absent.
- Maestro flow runs end-to-end with all 9 steps passing in both cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…RN-8)

Prettier wants the plugin tuple split across multiple lines. Auto-fix applied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@daniel-graham-amplitude daniel-graham-amplitude left a comment

Choose a reason for hiding this comment

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

Lgtm

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.

4 participants