diff --git a/.changeset/bright-dogs-fly.md b/.changeset/bright-dogs-fly.md new file mode 100644 index 00000000..268416bb --- /dev/null +++ b/.changeset/bright-dogs-fly.md @@ -0,0 +1,5 @@ +--- +'posthog_flutter': minor +--- + +Add native exception capture support for Apple platforms (iOS, macOS, tvOS) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c419dfb..93e33157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: - name: Enable Swift Package Manager if: matrix.package_manager == 'spm' - run: flutter config --enable-swift-package-manager + run: flutter config --enable-swift-package-manager # flutter config --no-enable-swift-package-manager - name: Install dependencies run: flutter pub get diff --git a/example/README.md b/example/README.md index 2aff3946..f3204eba 100644 --- a/example/README.md +++ b/example/README.md @@ -29,4 +29,14 @@ posthog-cli sourcemap upload --directory build/web cd build/web # https://pub.dev/packages/dhttpd dhttpd -``` \ No newline at end of file +``` + +## Running Apple example + +``` +# needs device pairing - enable debug mode on the device +# make sure you have a valid team in Xcode, go to Signing & Capabilities +flutter devices +flutter run --release -d Manoel --verbose +# replace Manoel with your device's name/id +``` diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 3b7dcd39..172242af 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -2,6 +2,8 @@ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" + // uncomment to upload mapping files to PostHog + // id "com.posthog.android" version "1.0.3" } def localProperties = new Properties() diff --git a/example/android/app/src/main/kotlin/com/example/flutter/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/flutter/MainActivity.kt index c248e574..2dcf69bd 100644 --- a/example/android/app/src/main/kotlin/com/example/flutter/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/flutter/MainActivity.kt @@ -1,5 +1,21 @@ package com.example.flutter import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "posthog_flutter_example") + .setMethodCallHandler { call, result -> + if (call.method == "triggerNativeCrash") { + NativeCrashHelper().triggerCrash() + result.success(null) + } else { + result.notImplemented() + } + } + } +} diff --git a/example/android/app/src/main/kotlin/com/example/flutter/NativeCrashHelper.kt b/example/android/app/src/main/kotlin/com/example/flutter/NativeCrashHelper.kt new file mode 100644 index 00000000..a4aceff1 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/flutter/NativeCrashHelper.kt @@ -0,0 +1,24 @@ +package com.example.flutter + +/** + * Custom exception type for testing error tracking symbolication. + * This class will be minified by R8/ProGuard in release builds, + * allowing us to verify that symbolication (ProGuard mapping upload) works correctly. + */ +class PostHogExampleException(message: String) : Exception(message) + +/** + * Helper class to trigger a native crash for testing error tracking. + * This class and its methods will be minified by R8/ProGuard in release builds, + * allowing us to verify that symbolication (ProGuard mapping upload) works correctly. + */ +class NativeCrashHelper { + fun triggerCrash() { + // Crash on a background thread because Flutter wraps and + // swallows exceptions from the method channel handler as + // a PlatformException, preventing the app from actually crashing. + Thread { + throw PostHogExampleException("Test native crash from PostHog Flutter example") + }.start() + } +} diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 22c59040..4f4f1081 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -12,9 +12,11 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A1B2C3D4E5F60001AABBCCDD /* NativeCrashHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60000AABBCCDD /* NativeCrashHelper.swift */; }; BDF5FF89BC2B236EF0B81DD2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BDC5BF357116FAB133D0276D /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -45,7 +47,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 552A4626DB9955B4837A84BA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 72D3235DB526308040CCA83E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; @@ -53,16 +54,19 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 74F175BA4BCEA03CB975D825 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 77D76B05F16B4B148310677D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7E9C12CB711543525381770A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A1B2C3D4E5F60000AABBCCDD /* NativeCrashHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCrashHelper.swift; sourceTree = ""; }; A5B2412C165B920EC346A024 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + A809B0FA2F61A76C00637A80 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A809B0FB2F61A76C00637A80 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; BD641B49EA0880C84FE2BD75 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BDC5BF357116FAB133D0276D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -80,6 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 3144428D6DB02E557C4ECAF0 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -107,6 +112,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -136,7 +142,10 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + A1B2C3D4E5F60000AABBCCDD /* NativeCrashHelper.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + A809B0FA2F61A76C00637A80 /* Runner.app */, + A809B0FB2F61A76C00637A80 /* RunnerTests.xctest */, ); path = Runner; sourceTree = ""; @@ -173,7 +182,7 @@ ); name = RunnerTests; productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productReference = A809B0FB2F61A76C00637A80 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { @@ -187,15 +196,19 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 56F5972CE92C1CD5034EF702 /* [CP] Embed Pods Frameworks */, + 5C9C96BF1E7D401D439369FA /* [CP] Embed Pods Frameworks */, + A809B0FD2F61AD8000637A80 /* PostHog upload symbols */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productReference = A809B0FA2F61A76C00637A80 /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -227,6 +240,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Runner */; projectDirPath = ""; projectRoot = ""; @@ -295,9 +311,9 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; - 56F5972CE92C1CD5034EF702 /* [CP] Embed Pods Frameworks */ = { + 5C9C96BF1E7D401D439369FA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -305,10 +321,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -327,7 +347,26 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + A809B0FD2F61AD8000637A80 /* PostHog upload symbols */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "PostHog upload symbols"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# uncomment to upload mapping files to PostHog\n# POSTHOG_INCLUDE_SOURCE=1 ${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh\n"; }; E0EB70FF4694C64F4A64348B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -367,6 +406,7 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + A1B2C3D4E5F60001AABBCCDD /* NativeCrashHelper.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -458,7 +498,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8WKF8J5LV3; + DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -641,7 +681,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8WKF8J5LV3; + DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -665,7 +705,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8WKF8J5LV3; + DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -715,6 +755,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d42..c3fedb29 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + + let channel = FlutterMethodChannel( + name: "posthog_flutter_example", + binaryMessenger: engineBridge.applicationRegistrar.messenger() + ) + + channel.setMethodCallHandler { (call, result) in + if call.method == "triggerNativeCrash" { + NativeCrashHelper().triggerCrash() + } else { + result(FlutterMethodNotImplemented) + } + } } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 108a08eb..c6eadb92 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -55,5 +55,26 @@ com.posthog.posthog.AUTO_INIT + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/example/ios/Runner/NativeCrashHelper.swift b/example/ios/Runner/NativeCrashHelper.swift new file mode 100644 index 00000000..39b76d02 --- /dev/null +++ b/example/ios/Runner/NativeCrashHelper.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Helper class to trigger a native crash for testing error tracking. +/// Having a dedicated class and method produces a clearer stack trace +/// for verifying symbolication works correctly. +class NativeCrashHelper { + func triggerCrash() { + let array: [Int] = [] + _ = array[99] + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index b089329e..ff9759f8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter_example/error_example.dart'; @@ -55,6 +56,8 @@ Future main() async { true; // Capture Dart runtime errors config.errorTrackingConfig.captureIsolateErrors = true; // Capture isolate errors + config.errorTrackingConfig.captureNativeExceptions = + true; // Capture native exceptions (Android & Apple platforms) if (kIsWeb) { runZonedGuarded( @@ -111,6 +114,7 @@ class InitialScreen extends StatefulWidget { } class InitialScreenState extends State { + static const _exampleChannel = MethodChannel('posthog_flutter_example'); final _posthogFlutterPlugin = Posthog(); dynamic _result = ""; @@ -164,8 +168,10 @@ class InitialScreenState extends State { style: TextStyle(fontWeight: FontWeight.bold), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + Wrap( + alignment: WrapAlignment.spaceEvenly, + spacing: 8.0, + runSpacing: 8.0, children: [ ElevatedButton( onPressed: () { @@ -490,6 +496,33 @@ class InitialScreenState extends State { }, child: const Text("Test Isolate Error Handler"), ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.deepOrange, + foregroundColor: Colors.white, + ), + onPressed: () async { + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Native crash triggered! The app will crash. ' + 'The exception will be sent on next launch.', + ), + backgroundColor: Colors.deepOrange, + duration: Duration(seconds: 3), + ), + ); + } + + // Give the snackbar time to show before crashing + await Future.delayed(const Duration(seconds: 1)); + + // Trigger a native crash via method channel + await _exampleChannel.invokeMethod('triggerNativeCrash'); + }, + child: const Text("Test Native Crash (will crash app!)"), + ), const Divider(), const Padding( padding: EdgeInsets.all(8.0), diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 9d2fde46..d34cf817 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + B1C2D3E4F5A60001DDEEFFAA /* NativeCrashHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C2D3E4F5A60000DDEEFFAA /* NativeCrashHelper.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; @@ -68,6 +69,7 @@ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B1C2D3E4F5A60000DDEEFFAA /* NativeCrashHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCrashHelper.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; @@ -190,6 +192,7 @@ isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + B1C2D3E4F5A60000DDEEFFAA /* NativeCrashHelper.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, @@ -259,7 +262,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -438,6 +441,7 @@ files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + B1C2D3E4F5A60001DDEEFFAA /* NativeCrashHelper.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 397f3d33..ac78810c 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index d53ef643..4f14e498 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,29 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } + + override func applicationDidFinishLaunching(_ notification: Notification) { + let controller = mainFlutterWindow?.contentViewController as! FlutterViewController + let channel = FlutterMethodChannel( + name: "posthog_flutter_example", + binaryMessenger: controller.engine.binaryMessenger + ) + + channel.setMethodCallHandler { (call, result) in + if call.method == "triggerNativeCrash" { + NativeCrashHelper().triggerCrash() + } else { + result(FlutterMethodNotImplemented) + } + } + } } diff --git a/example/macos/Runner/NativeCrashHelper.swift b/example/macos/Runner/NativeCrashHelper.swift new file mode 100644 index 00000000..39b76d02 --- /dev/null +++ b/example/macos/Runner/NativeCrashHelper.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Helper class to trigger a native crash for testing error tracking. +/// Having a dedicated class and method produces a clearer stack trace +/// for verifying symbolication works correctly. +class NativeCrashHelper { + func triggerCrash() { + let array: [Int] = [] + _ = array[99] + } +} diff --git a/posthog_flutter/android/build.gradle b/posthog_flutter/android/build.gradle index 049338ac..9665b754 100644 --- a/posthog_flutter/android/build.gradle +++ b/posthog_flutter/android/build.gradle @@ -54,7 +54,7 @@ android { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' // + Version 3.31.0 and the versions up to 4.0.0, not including 4.0.0 and higher - implementation 'com.posthog:posthog-android:[3.35.0,4.0.0]' + implementation 'com.posthog:posthog-android:[3.37.0,4.0.0]' } testOptions { diff --git a/posthog_flutter/darwin/posthog_flutter.podspec b/posthog_flutter/darwin/posthog_flutter.podspec index e3c33711..2b27dc88 100644 --- a/posthog_flutter/darwin/posthog_flutter.podspec +++ b/posthog_flutter/darwin/posthog_flutter.podspec @@ -22,7 +22,7 @@ Postog flutter plugin s.osx.dependency 'FlutterMacOS' # ~> Version 3.40.0 up to, but not including, 4.0.0 - s.dependency 'PostHog', '>= 3.44.0', '< 4.0.0' + s.dependency 'PostHog', '>= 3.46.0', '< 4.0.0' s.ios.deployment_target = '13.0' # PH iOS SDK 3.0.0 requires >= 10.15 diff --git a/posthog_flutter/darwin/posthog_flutter/Package.swift b/posthog_flutter/darwin/posthog_flutter/Package.swift index e2a61d8c..90596f8b 100644 --- a/posthog_flutter/darwin/posthog_flutter/Package.swift +++ b/posthog_flutter/darwin/posthog_flutter/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework"), - .package(url: "https://github.com/PostHog/posthog-ios", "3.44.0"..<"4.0.0") + .package(url: "https://github.com/PostHog/posthog-ios", "3.46.0"..<"4.0.0") ], targets: [ .target( diff --git a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift index 3d5a7682..f46462ba 100644 --- a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift +++ b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift @@ -1,4 +1,4 @@ -import PostHog +@_spi(Experimental) import PostHog #if os(iOS) import Flutter import UIKit @@ -167,6 +167,27 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { } #endif + // Configure error tracking + if let errorConfig = posthogConfig["errorTrackingConfig"] as? [String: Any] { + // autoCapture is only available on iOS, macOS, and tvOS + #if os(iOS) || os(macOS) || os(tvOS) + if let captureNativeExceptions = errorConfig["captureNativeExceptions"] as? Bool { + config.errorTrackingConfig.autoCapture = captureNativeExceptions + } + #endif + + // inApp configuration works across all platforms (including manual capture) + if let inAppIncludes = errorConfig["inAppIncludes"] as? [String] { + config.errorTrackingConfig.inAppIncludes.append(contentsOf: inAppIncludes) + } + if let inAppExcludes = errorConfig["inAppExcludes"] as? [String] { + config.errorTrackingConfig.inAppExcludes.append(contentsOf: inAppExcludes) + } + if let inAppByDefault = errorConfig["inAppByDefault"] as? Bool { + config.errorTrackingConfig.inAppByDefault = inAppByDefault + } + } + // Update SDK name and version postHogSdkName = "posthog-flutter" postHogVersion = postHogFlutterVersion diff --git a/posthog_flutter/lib/src/posthog_config.dart b/posthog_flutter/lib/src/posthog_config.dart index 8492d118..865cab21 100644 --- a/posthog_flutter/lib/src/posthog_config.dart +++ b/posthog_flutter/lib/src/posthog_config.dart @@ -232,6 +232,7 @@ class PostHogErrorTrackingConfig { /// /// **Note:** /// - Flutter web: Not supported + /// - Android: Not supported /// final inAppExcludes = []; @@ -248,6 +249,7 @@ class PostHogErrorTrackingConfig { /// /// **Note:** /// - Flutter web: Not supported + /// - Android: Not supported /// var inAppByDefault = true; @@ -275,14 +277,30 @@ class PostHogErrorTrackingConfig { /// Default: false var capturePlatformDispatcherErrors = false; - /// Enable automatic capture of exceptions in the native SDKs (Android only for now) + /// Enable automatic capture of exceptions in the native SDKs + /// (Android and Apple platforms). /// /// Controls whether native exceptions are captured. /// - /// **Note:** - /// - iOS: Not supported - /// - Android: Java/Kotlin exceptions only (no native C/C++ crashes) - /// - Android: No stacktrace demangling for minified builds + /// **Apple (iOS, macOS, tvOS):** + /// + /// Captures Mach exceptions (e.g., EXC_BAD_ACCESS), POSIX signals + /// (e.g., SIGSEGV, SIGABRT), and uncaught NSExceptions. + /// Crashes are persisted to disk and sent as `$exception` events with + /// level "fatal" on the next app launch. + /// Not available on watchOS or visionOS due to platform limitations. + /// + /// For symbolicated stack traces, add a build phase script to your + /// Xcode project to upload debug symbols. + /// See: https://posthog.com/docs/error-tracking/upload-source-maps/ios + /// + /// **Android:** + /// + /// Captures Java/Kotlin exceptions only (no native C/C++ crashes). + /// + /// Stacktrace demangling for minified builds is supported by installing + /// the PostHog Gradle plugin to upload ProGuard/R8 mappings. + /// See: https://posthog.com/docs/error-tracking/upload-mappings/android /// /// Default: false var captureNativeExceptions = false;