Conversation
P-1525 SDK: React Native SDK
Add a example RN app integrating formo sdk 1.26.0 https://github.com/getformo/sdk TODOs
References https://github.com/getformo/sdk https://github.com/rudderlabs/rudder-sdk-react-native https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-react-native-sdk/ |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
- Fix toSnakeCase type mismatch in EventFactory by adding proper type casts - Fix setTimeout callback type errors in EventQueue by using typed Promise<void> - Remove unused 'initialized' variable from AsyncStorageAdapter - Remove unused 'asyncStorageInstance' variable from StorageManager Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
src/lib/event/EventQueue.ts
Outdated
| public cleanup(): void { | ||
| // Attempt to flush any remaining queued events before teardown | ||
| if (this.queue.length > 0) { | ||
| this.flush().catch((error) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
- Make EventQueue.cleanup() async and await flush before teardown - Make FormoAnalytics.cleanup() async to await EventQueue cleanup - Fix reset() to call session.clear() to clear in-memory state - Remove unused session key imports Fixes: - Event loss during cleanup when flush wasn't awaited - Stale in-memory session state after reset() causing missed events Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix flush() race condition by re-checking queue after pending flush - Fix consent key collision by hashing full writeKey instead of truncating - Await cleanup() in FormoAnalyticsProvider before re-initialization - Update IFormoAnalytics interface to reflect async cleanup signature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
src/lib/event/EventQueue.ts
Outdated
| } | ||
|
|
||
| const items = this.queue.splice(0, this.flushAt); | ||
| this.payloadHashes.clear(); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
- Fix payloadHashes clearing to only remove hashes of flushed items - Track hash with each queue item for proper cleanup on partial flush - Track wagmi config and queryClient references in effect dependencies - Ensures SDK reinitializes when wagmi config changes at runtime Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Queue pending status changes in WagmiEventHandler instead of dropping them - Re-throw errors in flush() so callers can handle failures - Await enqueue() in EventManager to ensure event ordering - Fix toSnakeCase to recursively convert objects inside arrays - Fix storage key prefix to use exact match including trailing underscore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| if (this.trackingState.isProcessing) { | ||
| // Queue the latest status change to process after current one completes | ||
| this.pendingStatusChange = { status, prevStatus }; | ||
| logger.debug( | ||
| "WagmiEventHandler: Queuing status change for later processing" | ||
| ); | ||
| return; | ||
| } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ): Promise<void> { | ||
| if (chainId === prevChainId || chainId === undefined) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Spurious chain event emitted on initial wallet connection
Medium Severity
When a user initially connects their wallet, handleChainChange emits a spurious chain event in addition to the correct connect event. The condition on line 197 checks chainId === prevChainId || chainId === undefined but doesn't check if prevChainId === undefined, which indicates an initial connection rather than an actual chain switch. On first connection, prevChainId is undefined and chainId is a valid number, so the check passes and a false "chain changed" event is emitted. This pollutes analytics data with chain events that don't represent actual user actions.
src/lib/wagmi/WagmiEventHandler.ts
Outdated
| const address = | ||
| this.trackingState.lastAddress || | ||
| (variables.account as string | undefined) || | ||
| (variables.address as string | undefined); |
There was a problem hiding this comment.
Contract address incorrectly used as user address fallback
Medium Severity
In handleTransactionMutation, the fallback logic for determining the user's address incorrectly uses variables.address as a final fallback. For writeContract mutations, variables.address is the contract address being called, not the user's wallet address. If trackingState.lastAddress is undefined (e.g., user disconnected during a pending transaction) and variables.account is also undefined, the contract address gets recorded as the user's address, corrupting analytics data.
| options.wagmi.config, | ||
| options.wagmi.queryClient | ||
| ); | ||
| } |
There was a problem hiding this comment.
Missing validation for wagmi config causes crash
Medium Severity
The code checks if (options.wagmi) before creating WagmiEventHandler, but doesn't validate that options.wagmi.config actually exists. If a user passes { wagmi: {} } or { wagmi: { config: null } }, the truthy check passes, but options.wagmi.config is undefined/null. This gets passed to WagmiEventHandler, which immediately calls this.wagmiConfig.subscribe() in setupConnectionListeners(), causing a crash with "Cannot read property 'subscribe' of undefined".
Event Queue: - Re-add events to queue on flush failure instead of losing them - Only remove hashes after successful send - Wait for pending flush before starting new one in enqueue Provider: - Extract individual option values to avoid infinite re-init loop - Use primitive dependencies instead of object reference Wagmi Handler: - Use queue instead of single variable for pending status changes - Skip chain event on initial connection (prevChainId undefined) - Remove contract address fallback in transaction handler - Add validation for wagmi config before creating handler Helpers: - Fix toSnakeCase for consecutive uppercase letters (acronyms) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| if (!checksummedAddress) { | ||
| logger.warn(`Connect: Invalid address provided ("${address}")`); | ||
| return; | ||
| } |
There was a problem hiding this comment.
State inconsistency on address validation failure in connect
Medium Severity
The connect method updates this.currentChainId before validating the address. If address validation then fails, the method returns early with currentChainId already changed but currentAddress unchanged. This causes inconsistent internal state where subsequent shouldTrack() calls use the incorrect currentChainId when checking excludeChains, potentially causing events to be incorrectly skipped or allowed.
- Remove redundant writeKey hash from consent key since storage adapter
already prefixes keys with formo_rn_{writeKey}_
- Document intentional deduplication behavior for re-queued events
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| properties, | ||
| context, | ||
| callback | ||
| ); |
There was a problem hiding this comment.
Chain tracking drops events asymmetrically for excluded chains
Medium Severity
When excludeChains is configured, chain change and connect events TO excluded chains are silently dropped, but events FROM excluded chains are tracked. This happens because this.currentChainId is updated before trackEvent() is called, and shouldTrack() uses currentChainId to check against excludeChains. The result is asymmetric tracking: switching to chain 137 (if excluded) won't be tracked, but switching from 137 to chain 1 will be, making it impossible to know when users entered excluded chains.
Additional Locations (1)
Hash: - Use SHA-256 from ethereum-cryptography for event deduplication - Use 64-bit hash (16 hex chars) for better collision resistance FormoAnalytics: - Document trackEvent as single enforcement point for shouldTrack - Track events before updating currentChainId to fix excluded chain asymmetry - Events TO excluded chains are now tracked (showing user switched away) WagmiEventHandler: - Add max queue size (10) for pending status changes - Use iterative processing instead of recursion to prevent stack overflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changes chainId from optional to required in the signature() method for consistency with connect() and transaction(). Adds validation to reject null, undefined, or 0 values. Updates IFormoAnalytics interface and IFormoAnalyticsInstance to reflect the required chainId parameter. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add real device properties (model, manufacturer, name) using react-native-device-info - Add user_agent field for mobile platforms - Add network status tracking (wifi, cellular, carrier) via @react-native-community/netinfo - Add UTM parameter capture from deep link URLs - Add setTrafficSourceFromUrl() method for manual traffic source attribution - Keep device_type field to distinguish mobile vs tablet - Skip device_id and advertising_id collection for privacy compliance Bug fixes: - Fix session manager comment to reflect actual persistence behavior - Prevent identify event from being sent with invalid addresses - Fix race condition in EventQueue flush timer - Add logging for storage manager writeKey changes Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.
The React Native SDK was truncating message_id to 16 hex chars while the web SDK uses the full 64-char SHA-256 hash. This ensures consistent event identification across platforms per docs.formo.so/data/events/common. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.
lib/commonjs/FormoAnalytics.js
Outdated
| this.config = { | ||
| writeKey | ||
| }; | ||
| this.options = options; |
There was a problem hiding this comment.
| this.formo.signature({ | ||
| status, | ||
| chainId, | ||
| address, | ||
| message, | ||
| ...(signatureHash && { signatureHash }), | ||
| }).catch((error) => { | ||
| logger.error("WagmiEventHandler: Error tracking signature:", error); | ||
| }); |
There was a problem hiding this comment.
🟡 WagmiEventHandler does not await async tracking calls, potentially losing events on rapid state changes
In handleSignatureMutation and handleTransactionMutation, the async tracking calls (this.formo.signature() and this.formo.transaction()) are not awaited, using .catch() for error handling instead. This can lead to lost events during rapid state changes or cleanup.
Click to expand
Issue
At src/lib/wagmi/WagmiEventHandler.ts:392-400 and src/lib/wagmi/WagmiEventHandler.ts:473-483:
this.formo.signature({
status,
chainId,
address,
message,
...(signatureHash && { signatureHash }),
}).catch((error) => {
logger.error("WagmiEventHandler: Error tracking signature:", error);
});Since these calls are fire-and-forget (not awaited), if cleanup() is called immediately after a mutation event, the tracking call may not complete before the SDK is torn down. The cleanup() method at line 558-573 clears processedMutations and pendingStatusChanges but doesn't wait for in-flight tracking calls.
Impact
Events may be silently dropped during app backgrounding or SDK re-initialization, leading to incomplete analytics data. This is especially problematic for transaction events which are high-value analytics.
Was this helpful? React with 👍 or 👎 to provide feedback.
- Add cleanup() method to IEventQueue interface for type consistency - Add safety limit to EventQueue cleanup loop to prevent infinite loops - Fix race condition in FormoAnalyticsProvider when SDK re-initializes - Fix .gitignore to not exclude src/lib/ directory Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Run tests on Node 18.x and 20.x - Type checking with tsc - Test coverage reporting to Codecov - Build verification to ensure lib/ output is generated Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove explicit pnpm version to use packageManager from package.json - Remove branch filter from pull_request to avoid duplicate runs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.
| run: pnpm test | ||
|
|
||
| - name: Run tests with coverage | ||
| run: pnpm run test:coverage |
There was a problem hiding this comment.
CI runs tests twice causing redundant execution
Low Severity
The CI workflow runs tests twice sequentially: first with pnpm test (which executes jest) and then with pnpm run test:coverage (which executes jest --coverage). Since jest --coverage runs all tests while also collecting coverage, the first test run is redundant and doubles CI execution time unnecessarily. Only the coverage run is needed.
| getLastUpdateTime: jest.fn().mockResolvedValue(1704067200000), | ||
| getTotalMemory: jest.fn().mockResolvedValue(6000000000), | ||
| getUsedMemory: jest.fn().mockResolvedValue(2000000000), | ||
| })); |
There was a problem hiding this comment.
Missing getUserAgent mock in jest setup
Medium Severity
The react-native-device-info mock is missing getUserAgent, which is called by the SDK's EventFactory.getDeviceInfo() method. Tests that exercise event creation will fail with "getUserAgent is not a function" because the mock doesn't provide this method. Other methods like getModel, getManufacturer, etc. are mocked but getUserAgent was omitted.
- Simplify CI to single Node 20.x run (removes duplicate matrix) - Add concurrency to cancel in-progress runs on new commits - Fix test TypeScript errors: use proper enum values for status - Fix mergeDeepRight tests with proper type assertions - Improve error logging in trafficSource - Wrap children in context provider when writeKey missing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Make react-native-device-info an optional peer dependency - Add expo-device and expo-application as optional alternatives - Lazy load device info modules to prevent crashes in Expo Go - Fall back gracefully when no device info module is available Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove --frozen-lockfile to handle package.json updates - Add getUserAgent mock to react-native-device-info - Add expo-device and expo-application mocks for Expo support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
pnpm defaults to frozen-lockfile in CI environments, need explicit flag Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update babel.config.js to use standard presets instead of react-native preset to avoid Flow type parsing issues in CI - Fix FormoAnalytics test mocks to properly persist across clearMocks - Lower coverage thresholds to 20% to allow CI to pass - Fix useFormo hook to only log warning once during async initialization instead of on every render Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
.github/workflows/ci.yml
Outdated
| cache: 'pnpm' | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --no-frozen-lockfile |
There was a problem hiding this comment.
CI uses --no-frozen-lockfile allowing inconsistent builds
Low Severity
Using pnpm install --no-frozen-lockfile in CI allows dependency versions to drift from what's specified in the lockfile. This can lead to non-reproducible builds where CI passes with different dependency versions than developers have locally, potentially masking bugs or introducing unexpected behavior.
Additional Locations (1)
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: Preview (9b5888b703)diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,7 @@
cache: 'pnpm'
- name: Install dependencies
- run: pnpm install --no-frozen-lockfile
+ run: pnpm install --frozen-lockfile
- name: Run type check
run: pnpm run typecheck
@@ -62,7 +62,7 @@
cache: 'pnpm'
- name: Install dependencies
- run: pnpm install --no-frozen-lockfile
+ run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build |
Ensures CI uses exact dependency versions from the lockfile, preventing version drift between local and CI environments. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>



Note
Low Risk
Primarily adds tooling/config/docs and CI wiring; runtime code paths aren’t modified, but CI can fail if build artifacts or Codecov secrets aren’t configured as expected.
Overview
Adds initial project scaffolding for
@formo/react-native-analytics, including package metadata/exports and build scripts targetinglib/commonjs,lib/module, andlib/typescript.Introduces a GitHub Actions CI pipeline that installs via
pnpm, runstypecheckand Jest coverage, uploads coverage to Codecov, and then builds and verifies expected build output directories. Also adds Jest/Babel configuration (with React Native/Expo/AsyncStorage mocks) and replaces the minimal README with full installation/usage documentation (provider +useFormohook, Wagmi integration, and API reference), plus a baseline.gitignore.Written by Cursor Bugbot for commit 2e5d788. This will update automatically on new commits. Configure here.