Repository navigation
feat(analytics): support Swift Package Manager on React Native 0.88+ - #471
ketanmixpanel wants to merge 13 commits into
Conversation
Adds ios/Package.swift for React Native 0.88's experimental SwiftPM autolinking (`swiftpmConfig.name` in package.json). SwiftPM cannot mix Swift and Objective-C in one target, so the sources move to ios/Sources and split into MixpanelReactNativeSwift and MixpanelReactNative (the RCT_EXTERN_MODULE registration). Under SwiftPM the Swift target imports React directly, since there is no bridging header. CocoaPods is unchanged apart from the podspec's source path. mixpanel-swift is pinned at 7.0.0 in both the podspec and Package.swift. SwiftPM build output is kept out of the npm package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Samples/MixpanelExampleSpm is the MixpanelExample screen on React Native 0.88 with the iOS app linked through SwiftPM (`npm run spm`), so the package's ios/Package.swift is built and run. ios/Podfile is a placeholder the React Native CLI needs to recognize the iOS project. MixpanelExample (CocoaPods) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds test_ios_spm and test_android_spm, copies of test_ios and test_android for Samples/MixpanelExampleSpm (React Native 0.88). The iOS job runs `npm run spm` in place of `pod install`, so the package's ios/Package.swift is built, then launches the app and checks it is still running, as test_ios does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The changes appear safe to merge; no new actionable issues were found.
|
CI appends org.gradle.jvmargs to this file with `echo >>`. Without a trailing newline it joined the last line, so android.newDsl failed to parse as a boolean and test_android_spm could not build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…module The iOS and Android SPM jobs built Debug, which ships no JavaScript bundle, so the app launched without running the sample and nothing exercised the SDK. They now build Release and look for a line the sample logs after init(). It reports whether the native module is present, since the SDK falls back to JavaScript mode, where init() still succeeds, when the module is missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heme The scheme still listed MixpanelExampleSpmTests from the React Native template, but the project has no such target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rahul-mixpanel
left a comment
There was a problem hiding this comment.
Reviewed the library changes and the moved sources against master. No blocking issues, and CocoaPods users are unaffected.
Checked and looks good: import React under #if SWIFT_PACKAGE is correctly scoped; @objc(MixpanelReactNative) lets RCT_EXTERN_MODULE link to the Swift class; the podspec and legacy .xcodeproj paths are updated; the sample's native= check uses the same NativeModules.MixpanelReactNative lookup as the SDK (index.js:5); and npm pack --dry-run ships Package.swift and Sources/**, leaving out .build, .swiftpm and Package.resolved. Using this together with mixpanel/mixpanel-react-native-session-replay#95 under SwiftPM resolves cleanly, since both pinned SDKs depend on mixpanel-swift-common from: "2.0.0".
Inline comments are mostly nits, plus one versioning decision.
| ], | ||
| dependencies: [ | ||
| // Keep in step with the Mixpanel-swift version in MixpanelReactNative.podspec. | ||
| .package(url: "https://github.com/mixpanel/mixpanel-swift.git", exact: "7.0.0"), |
There was a problem hiding this comment.
This matches the podspec, so it's a decision rather than a bug, but it's worth making on purpose. Apps that use mixpanel-swift natively and through React Native are common, and under SwiftPM they can't resolve to two different versions. .upToNextMinor(from: "7.0.0") would avoid that, or the README could document the constraint.
There was a problem hiding this comment.
exact is intentional, and it matches the podspec. It means the mixpanel-swift an app builds against is the same version CI built and tested with this wrapper. A range would let a developer's build pick up a release we haven't tested together, and it would push native SDK updates into apps without the developer choosing to upgrade. mixpanel-swift bumps reach apps through a wrapper release.
You're right about the trade-off: an app that also depends on mixpanel-swift directly at another version won't resolve. I've added a README note in bd9a0a4 saying the package pins mixpanel-swift, and that apps using it directly should use the version this release pins.
| name: "MixpanelReactNative", | ||
| dependencies: ["MixpanelReactNativeSwift"] + reactNativeProducts, | ||
| path: "Sources/MixpanelReactNative", | ||
| publicHeadersPath: ".", |
There was a problem hiding this comment.
Nit: this target has no headers, so publicHeadersPath can go. linkerSettings on the next line isn't needed either, since UIKit and Foundation are linked automatically, and the session-replay package doesn't set it.
There was a problem hiding this comment.
Dropped linkerSettings in 9a661cb. publicHeadersPath has to stay. SwiftPM requires an Objective-C target's public headers folder to exist, and it defaults to include/, which this target doesn't have. Without the setting, the manifest fails to load with "public headers ("include") directory path for 'MixpanelReactNative' is invalid or not contained in the target". I reproduced it with a minimal package, just one .m file and no React Native, on Swift 6.3.3 (Xcode 26) and 6.4 (Xcode 27).
An empty include/ would also work, but git doesn't track empty folders and npm wouldn't ship one. So it points at the target's own folder instead, which holds only the .m file and so exports nothing. 212babf updates the comment to explain this.
| .product(name: "ReactHeaders", package: "ReactNative"), | ||
| .product(name: "ReactNativeHeaders", package: "ReactNative"), | ||
| .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"), | ||
| .product(name: "ReactAppHeaders", package: "React-GeneratedCode"), |
There was a problem hiding this comment.
Nit: ReactAppHeaders holds the codegen output, but this is a legacy bridge module that doesn't use codegen. It could be dropped from both targets.
There was a problem hiding this comment.
Keeping it for now. We plan to convert this module to a TurboModule, which needs the codegen headers, so I'd rather not drop it and add it back.
| - name: Install dependencies | ||
| run: | | ||
| npm install | ||
| npm install -g react-native-cli |
There was a problem hiding this comment.
Nit: copied from test_android. react-native-cli is deprecated, and npm i react-native-gradle-plugin modifies the root package.json. Neither should be needed for a sample that runs npx react-native.
There was a problem hiding this comment.
Removed from the SPM job in a7a2836. I've left test_android as it is for a separate change.
| path: | | ||
| ~/.android/avd/* | ||
| ~/.android/adb* | ||
| key: avd-${{ matrix.api-level }}-google-apis-x86_64-nexus5x |
There was a problem hiding this comment.
Nit: this is the same AVD cache key as test_android, so the two jobs race to save the same entry.
There was a problem hiding this comment.
The SPM job now has its own key (…-nexus5x-spm), in f805024.
| resolver: { | ||
| nodeModulesPaths: [ | ||
| path.resolve(__dirname, 'node_modules'), | ||
| path.resolve(__dirname, '../../node_modules'), |
There was a problem hiding this comment.
Not verified, and MixpanelExample already did the same: the repo root's node_modules has react-native ^0.76 from the library's dev dependencies. That could put a second copy of React Native into the sample's bundle. CI passes, so it may be fine, but it's worth a quick look.
There was a problem hiding this comment.
Confirmed. A Release bundle had 403 modules from the root's react-native 0.76.9 as well as the sample's 0.88. d1dbffe blocks the root's react and react-native, so the library resolves to the sample's copies. The bundle now has a single React Native and went from 1.8 MB to 985 KB. The sample still logs native=true on iOS and Android. MixpanelExample has the same setup, and I've left it for a separate change.
UIKit and Foundation are linked automatically. The target keeps its public headers path although it has no headers: without it SwiftPM looks for include/ and fails to resolve the package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sample runs its own React Native CLI and Gradle plugin through npx, so the deprecated react-native-cli and the root react-native-gradle-plugin install aren't needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_android uses the same key and runs at the same time, so the two jobs raced to save one entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library resolves to the repo root, whose node_modules has react-native 0.76 and react from the library's dev dependencies, so the bundle held both those and the sample's 0.88 copies. Block the root copies so the library's imports resolve to the sample's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ping Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rahul-mixpanel
left a comment
There was a problem hiding this comment.
Thanks for addressing the feedback, and for the correction on publicHeadersPath. CI is green. LGTM.
Summary
Adds Swift Package Manager (SPM) support to the iOS native module, so apps on React Native 0.88+ can use React Native's SPM autolinking instead of CocoaPods. The podspec still works, so existing CocoaPods apps need no changes.
Changes
Library
ios/Package.swift, which React Native's autolinker picks up through theswiftpmConfigentry inpackage.json. SPM can't compile Swift and Objective-C in one target, so the module is split in two:MixpanelReactNativeSwift: the Swift implementation. It depends onmixpanel-swift7.0.0 and React's header products.MixpanelReactNative: the Objective-C file that registers the module withRCT_EXTERN_MODULE.ios/Sources/MixpanelReactNative/andios/Sources/MixpanelReactNativeSwift/. The files are unchanged apart from the import below. The podspecsource_filesand the legacy.xcodeprojnow point at the new paths.MixpanelReactNative.swiftaddsimport Reactunder#if SWIFT_PACKAGE, because SPM targets have no bridging header. The CocoaPods build is unchanged.@objc(MixpanelReactNative)keeps the module's runtime name, so the Objective-C registration links to the Swift class under both CocoaPods and SPM.ios/.build,ios/.swiftpm,ios/Package.resolved) is git-ignored and excluded from the npm package.npx react-native spm --deintegrate, andspm deinitto undo).Sample
Samples/MixpanelExampleSpmapp on React Native 0.88.0 / React 19.3.0. It has the same screen asMixpanelExample, links iOS through SPM, and is a regular RN 0.88 app on Android.init(), the sample logsMixpanelExampleSpm ready: native=<true|false>for CI. When the native module is missing, the SDK falls back to JavaScript mode andinit()still succeeds, so this line is what shows which mode actually started.CI
test_ios_spm(Xcode 26 and 27) runsnpm run spm, then launches a Release build on a simulator. Debug simulator builds skip the JavaScript bundle, so they would launch without running the sample. The job checks the app is still running after 15 s, then checks the simulator log fornative=true.test_android_spmlaunches a Release build on an emulator and checks logcat fornative=true.Testing
Manual
Samples/MixpanelExample(existing sample)Samples/MixpanelExample(existing sample)Samples/MixpanelExampleSpm(new sample)Samples/MixpanelExampleSpm(new sample)Automated
iOS(Xcode 26 and 27), existing sampleiOS SPM(Xcode 26 and 27), new sample: launch andnative=truetest_android, existing sampletest_android_spm, new sample: launch andnative=truetest_main_code(JS tests)native=truenative=trueFixes SDK-105