feat(unified-react-native): add unified-react-native support - #1945
feat(unified-react-native): add unified-react-native support#1945Mercy811 wants to merge 11 commits into
Conversation
size-limit report 📦
|
01c6537 to
5f61309
Compare
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Maestro asserts missing UI buttons
- Updated smoke.yaml to assert the single "Initialize all SDKs" button that App.tsx renders instead of the removed multi-button labels.
You can send follow-ups to the cloud agent here.
2be24e8 to
a1cbf5c
Compare
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Session Replay double-started
- Removed the redundant sessionReplay.start() call after add since the plugin already auto-starts during setup when autoStart is true.
- ✅ Fixed: Re-init skips blade teardown
- Re-init now awaits teardown on Experiment and Session Replay plugins and creates fresh instances instead of reusing stale blade objects via ??=.
Or push these changes by commenting:
@cursor push 867ed7360f
Preview (867ed7360f)
diff --git a/packages/unified-react-native/src/unified-client-factory.ts b/packages/unified-react-native/src/unified-client-factory.ts
--- a/packages/unified-react-native/src/unified-client-factory.ts
+++ b/packages/unified-react-native/src/unified-client-factory.ts
@@ -79,7 +79,8 @@
}
hasInitialized = true;
- experiment ??= experimentPlugin({
+ await experiment?.teardown?.();
+ experiment = experimentPlugin({
...getSharedExperimentOptions(unifiedOptions),
...unifiedOptions?.experiment,
});
@@ -91,16 +92,14 @@
await experimentClient.start();
}
- sessionReplay ??= new SessionReplayPlugin({
+ await sessionReplay?.teardown?.();
+ sessionReplay = new SessionReplayPlugin({
...getSharedSessionReplayOptions(unifiedOptions),
...unifiedOptions?.sessionReplay,
});
await analyticsClient.add(sessionReplay).promise;
- if (sessionReplay.sessionReplayConfig.autoStart) {
- await sessionReplay.start();
- }
- engagement ??= getPlugin({
+ engagement = getPlugin({
...getSharedEngagementOptions(unifiedOptions),
...unifiedOptions?.engagement,
});
diff --git a/packages/unified-react-native/test/unified-client-factory.test.ts b/packages/unified-react-native/test/unified-client-factory.test.ts
--- a/packages/unified-react-native/test/unified-client-factory.test.ts
+++ b/packages/unified-react-native/test/unified-client-factory.test.ts
@@ -16,19 +16,27 @@
config,
sessionReplayConfig: { autoStart: config?.autoStart ?? true },
start: jest.fn(() => Promise.resolve()),
+ teardown: jest.fn(() => Promise.resolve()),
})),
}));
-jest.mock('@amplitude/plugin-engagement-react-native', () => ({
- boot: jest.fn(() => Promise.resolve()),
- getPlugin: jest.fn().mockImplementation((config) => ({ name: 'engagement', config })),
-}));
+jest.mock('@amplitude/plugin-engagement-react-native', () => {
+ let plugin: { name: string; config: unknown } | undefined;
+ return {
+ boot: jest.fn(() => Promise.resolve()),
+ getPlugin: jest.fn().mockImplementation((config) => {
+ plugin ??= { name: 'engagement', config };
+ return plugin;
+ }),
+ };
+});
jest.mock('@amplitude/plugin-experiment-react-native', () => ({
experimentPlugin: jest.fn().mockImplementation((config) => ({
name: 'experiment',
config,
experiment: { start: jest.fn(), variant: jest.fn() },
+ teardown: jest.fn(() => Promise.resolve()),
})),
}));
@@ -88,8 +96,6 @@
deploymentKey: 'deployment-key',
});
expect(MockSessionReplayPlugin).toHaveBeenCalledWith({ logLevel: LogLevel.Debug, sampleRate: 0.5 });
- const sessionReplayStart = (client.sessionReplay() as unknown as { start: jest.Mock }).start;
- expect(sessionReplayStart).toHaveBeenCalledTimes(1);
expect(mockGetPlugin).toHaveBeenCalledWith({ serverZone: 'EU', logLevel: 'debug', locale: 'fr-FR' });
expect(mockBoot).toHaveBeenCalledWith('user-id', 'device-id');
const experimentStart = (client.experiment() as unknown as { start: jest.Mock }).start;
@@ -179,7 +185,7 @@
expect(mockGetPlugin).toHaveBeenCalledWith({});
});
- test('reuses blade instances on sequential initialization', async () => {
+ test('creates fresh blade instances on sequential initialization', async () => {
const client = createInstance();
await client.init('first-api-key');
@@ -189,11 +195,11 @@
await client.init('second-api-key');
expect(analyticsInit).toHaveBeenCalledTimes(2);
- expect(mockExperimentPlugin).toHaveBeenCalledTimes(1);
- expect(MockSessionReplayPlugin).toHaveBeenCalledTimes(1);
- expect(mockGetPlugin).toHaveBeenCalledTimes(1);
- expect(client.sessionReplay()).toBe(sessionReplay);
- expect(client.experiment()).toBe(experiment);
+ expect(mockExperimentPlugin).toHaveBeenCalledTimes(2);
+ expect(MockSessionReplayPlugin).toHaveBeenCalledTimes(2);
+ expect(mockGetPlugin).toHaveBeenCalledTimes(2);
+ expect(client.sessionReplay()).not.toBe(sessionReplay);
+ expect(client.experiment()).not.toBe(experiment);
expect(client.engagement()).toBe(engagement);
});You can send follow-ups to the cloud agent here.
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Personal Xcode development team
- Removed the personal DEVELOPMENT_TEAM (7CA8BGS9U9) from both Debug and Release build configurations in the unified React Native example iOS project.
- ✅ Fixed: Failed init permanently blocks retries
- Clear initPromise on initialization failure via a catch handler so subsequent init() calls can retry, while keeping the resolved promise cached after success.
Or push these changes by commenting:
@cursor push 53f0a436f8
Preview (53f0a436f8)
diff --git a/examples/unified/react-native-app/ios/app.xcodeproj/project.pbxproj b/examples/unified/react-native-app/ios/app.xcodeproj/project.pbxproj
--- a/examples/unified/react-native-app/ios/app.xcodeproj/project.pbxproj
+++ b/examples/unified/react-native-app/ios/app.xcodeproj/project.pbxproj
@@ -474,7 +474,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 7CA8BGS9U9;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = app/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -503,7 +502,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 7CA8BGS9U9;
INFOPLIST_FILE = app/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
diff --git a/packages/unified-react-native/src/unified-client-factory.ts b/packages/unified-react-native/src/unified-client-factory.ts
--- a/packages/unified-react-native/src/unified-client-factory.ts
+++ b/packages/unified-react-native/src/unified-client-factory.ts
@@ -56,7 +56,7 @@
return initPromise;
}
- initPromise = (async () => {
+ const promise = (async () => {
const analyticsOptions: ReactNativeOptions = {
...getSharedAnalyticsOptions(unifiedOptions),
...unifiedOptions?.analytics,
@@ -97,6 +97,11 @@
await bootEngagement(analyticsClient.getUserId(), analyticsClient.getDeviceId());
})();
+ initPromise = promise.catch((error) => {
+ initPromise = undefined;
+ throw error;
+ });
+
return initPromise;
};You can send follow-ups to the cloud agent here.
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Library plugin lost after failed retry
- Track whether analytics init was previously attempted and re-add the library plugin after init on retries, so timeline.reset() no longer drops it when recovery succeeds.
Or push these changes by commenting:
@cursor push d2a4bf6210
Preview (d2a4bf6210)
diff --git a/packages/unified-react-native/src/unified-client-factory.ts b/packages/unified-react-native/src/unified-client-factory.ts
--- a/packages/unified-react-native/src/unified-client-factory.ts
+++ b/packages/unified-react-native/src/unified-client-factory.ts
@@ -56,6 +56,7 @@
let sessionReplay: SessionReplayPlugin | undefined;
let engagement: ReturnType<typeof getPlugin> | undefined;
let hasInitializedAnalytics = false;
+ let hasAttemptedAnalyticsInit = false;
let initPromise: Promise<void> | undefined;
const init = (apiKey: string, unifiedOptions?: UnifiedOptions): Promise<void> => {
@@ -74,6 +75,8 @@
}
analyticsOptions.loggerProvider = loggerProvider;
+ const shouldAddLibraryPluginAfterInit = hasInitializedAnalytics || hasAttemptedAnalyticsInit;
+
if (hasInitializedAnalytics) {
for (const blade of [experiment, sessionReplay, engagement]) {
if (blade !== undefined) {
@@ -83,13 +86,14 @@
experiment = undefined;
sessionReplay = undefined;
engagement = undefined;
- } else {
+ } else if (!shouldAddLibraryPluginAfterInit) {
analyticsClient.add(libraryPlugin());
}
+ hasAttemptedAnalyticsInit = true;
await analyticsClient.init(apiKey, analyticsOptions.userId, analyticsOptions).promise;
- if (hasInitializedAnalytics) {
+ if (shouldAddLibraryPluginAfterInit) {
await analyticsClient.add(libraryPlugin()).promise;
}
hasInitializedAnalytics = true;You can send follow-ups to the cloud agent here.
Log through the Analytics logger provider and continue blade initialization. Remove the unreleased initAll alias and obsolete Maestro smoke test.
Rely on Session Replay's native auto-start behavior. Make unified init idempotent to prevent reusing torn-down blade instances.
Clear failed init promises and remove partially registered blades before retrying. Remove the personal development team from the example Xcode project.
4e4cdf1 to
8358dca
Compare
|
bugbot run |
There was a problem hiding this comment.
✅ 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 8358dca. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8358dca6a2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (experimentClient === undefined) { | ||
| loggerProvider.debug(`${initializedExperiment.name} plugin is not initialized.`); | ||
| } else { | ||
| await experimentClient.start(); |
There was a problem hiding this comment.
I notice that all of the blades are being initialized sequentially. Analytics initializes, and then Experiment, and then Session Replay. It seems like we may have some unnecessary idle time (especially here at await experimentClient.start()).
For each blade, could we put the awaits into a promise, and then run Promise.all at the end?
There was a problem hiding this comment.
Could look like this...
async function initExperiment() {
try {
const initializedExperiment = experimentPlugin({
...getSharedExperimentOptions(unifiedOptions),
...unifiedOptions?.experiment,
});
await analyticsClient.add(initializedExperiment).promise;
experiment = initializedExperiment;
const experimentClient = initializedExperiment.experiment;
if (experimentClient === undefined) {
loggerProvider.debug(`${initializedExperiment.name} plugin is not initialized.`);
} else {
await experimentClient.start();
}
} catch (error) {
logInitializationError(loggerProvider, 'Experiment', error);
}
}
//....
await Promise.all([
initAnalytics(),
initExperiment(),
]);
Summary
@amplitude/unified-react-nativebeta package.initAll.serverZone,instanceName, and translatedlogLeveldefaults while allowing blade-specific overrides.Stacked on #1944, which provides
@amplitude/plugin-experiment-react-native.Local test with the example app

examples/unified/react-native-app. Able to see requests from all blades.Testing
pnpm install --frozen-lockfile:app:generateAutolinkingPackageListpod install(five native modules plus Engagement and AsyncStorage codegen)pnpm docs:checkpnpm lintpnpm lint:depspnpm test(30 projects)pnpm build— the new package and its dependencies build successfully, but the repository-wide command stalls in the unrelated@amplitude/segment-session-replay-pluginRollup taskpnpm test:examples— the base branch references a missing rootjest.setup.examples.jsChecklist
Note
Medium Risk
New beta SDK orchestrates multi-blade init and native autolinking for customer apps; behavior is well-tested but affects integration surfaces (RN ≥0.76, New Architecture for Engagement).
Overview
Introduces
@amplitude/unified-react-native(1.0.0-beta.0) as a single install that bundles Analytics, Experiment, Session Replay, and Guides and Surveys. A newinit()path initializes Analytics, registers blade plugins, starts Experiment, and boots Engagement, with sharedserverZone,instanceName, andlogLeveldefaults and per-blade overrides. Initialization is idempotent; failed init can retry after removing partially added blades.The package ships
react-native.config.js, an autolinking preset that exposes five transitive native modules (analytics, experiment, engagement, session replay, AsyncStorage) so apps only depend on the unified package. Public API re-exports Analytics helpers plusexperiment()/sessionReplay()accessors,AmpMaskView, and a library enrichment plugin for event metadata.Adds a bare React Native 0.76 example under
examples/unified/react-native-appthat loads the preset, enables Android New Architecture, dedupes Metro resolution for pnpm workspaces, and demonstrates manualinit()via a button (API key from root.env). Includes unit tests for the client factory, autolinking preset, and library plugin.Reviewed by Cursor Bugbot for commit 8358dca. Configure here.