diff --git a/PostHog.podspec b/PostHog.podspec index 2380b6221..d203b7cb6 100644 --- a/PostHog.podspec +++ b/PostHog.podspec @@ -30,4 +30,7 @@ Pod::Spec.new do |s| 'vendor/libwebp/**/*.{h,c}' ] s.resource_bundles = { "PostHog" => "PostHog/Resources/PrivacyInfo.xcprivacy" } + + # Include the upload script for dSYM uploads + s.preserve_paths = 'build-tools/upload-symbols' end diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index ace2f04e1..94c28c6c4 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 3AE3FB49299391DF00AFFC18 /* PostHogStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB48299391DF00AFFC18 /* PostHogStorage.swift */; }; 3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */; }; 3AE3FB4E2993D1D600AFFC18 /* PostHogStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4D2993D1D600AFFC18 /* PostHogStorageManager.swift */; }; + 628564DE224D783306683EEE /* PostHogDebugImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EA6EED2443DFD84061990F /* PostHogDebugImageProvider.swift */; }; 690B2DF32C205B5600AE3B45 /* TimeBasedEpochGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690B2DF22C205B5600AE3B45 /* TimeBasedEpochGenerator.swift */; }; 690FF05F2AE7E2D400A0B06B /* Data+Gzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */; }; 690FF0AF2AEB9C1400A0B06B /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */; }; @@ -172,6 +173,12 @@ DA30AE692D3EFB4F00465A64 /* Optional+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA30AE682D3EFB4F00465A64 /* Optional+Util.swift */; }; DA30AE812D3FE63F00465A64 /* PostHogSurvey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA30AE802D3FE63F00465A64 /* PostHogSurvey.swift */; }; DA37933C2DBA571D005C6AA3 /* PostHogSurveysConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3793352DBA5718005C6AA3 /* PostHogSurveysConfig.swift */; }; + DA3BB4AA2ED82E410097A97A /* PostHogStackTrace.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3BB4A62ED82E410097A97A /* PostHogStackTrace.swift */; }; + DA3BB4AC2ED82EE80097A97A /* PostHogExceptionProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3BB4AB2ED82EE80097A97A /* PostHogExceptionProcessor.swift */; }; + DA3BB4AF2ED88DE90097A97A /* ExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = DA3BB4AE2ED88DE90097A97A /* ExceptionHandler.m */; }; + DA3BB4DF2ED992780097A97A /* PostHogBinaryImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3BB4DD2ED992780097A97A /* PostHogBinaryImageInfo.swift */; }; + DA3BB4E02ED992780097A97A /* PostHogDebugImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3BB4DE2ED992780097A97A /* PostHogDebugImageProvider.swift */; }; + DA3BB5052EDF15320097A97A /* PostHogStackFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3BB5042EDF15320097A97A /* PostHogStackFrame.swift */; }; DA4AF61F2D1195D20053EA38 /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; DA4AF6202D1195D20053EA38 /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DA4AF6242D119FC60053EA38 /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; @@ -210,6 +217,7 @@ DA71854D2D07E11200396388 /* input_1.png in Resources */ = {isa = PBXBuildFile; fileRef = DA7185422D07E11200396388 /* input_1.png */; }; DA74644F2DC8D58100C7D394 /* PostHogConsoleLogInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA74644E2DC8D58100C7D394 /* PostHogConsoleLogInterceptor.swift */; }; DA86D0E32E02D78C00CF3065 /* PostHogSurveysDefaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA86D0E22E02D78C00CF3065 /* PostHogSurveysDefaultDelegate.swift */; }; + DA8C9B982EC633FA00C6EADB /* PostHogErrorTrackingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8C9B962EC633FA00C6EADB /* PostHogErrorTrackingConfig.swift */; }; DA929C082D0B3A1D0018369C /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; DA929C092D0B3A1D0018369C /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DA92C6412DCE09C700C02F3E /* PostHogLogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA92C6402DCE09C700C02F3E /* PostHogLogEntry.swift */; }; @@ -532,6 +540,7 @@ /* Begin PBXFileReference section */ 1F55581F2DFC47E8007643C0 /* PostHogStorageMergeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorageMergeTest.swift; sourceTree = ""; }; + 34EA6EED2443DFD84061990F /* PostHogDebugImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PostHogDebugImageProvider.swift; path = "PostHog/Error Tracking/Utils/PostHogDebugImageProvider.swift"; sourceTree = ""; }; 3A0F108229C47940002C0084 /* UIViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExample.swift; sourceTree = ""; }; 3A0F108429C9ABB6002C0084 /* ReadWriteLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 3A0F108829C9BD76002C0084 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; @@ -708,6 +717,14 @@ DA30AE682D3EFB4F00465A64 /* Optional+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Util.swift"; sourceTree = ""; }; DA30AE802D3FE63F00465A64 /* PostHogSurvey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSurvey.swift; sourceTree = ""; }; DA3793352DBA5718005C6AA3 /* PostHogSurveysConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSurveysConfig.swift; sourceTree = ""; }; + DA3BB4A62ED82E410097A97A /* PostHogStackTrace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStackTrace.swift; sourceTree = ""; }; + DA3BB4AB2ED82EE80097A97A /* PostHogExceptionProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogExceptionProcessor.swift; sourceTree = ""; }; + DA3BB4AD2ED88DE90097A97A /* ExceptionHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExceptionHandler.h; sourceTree = ""; }; + DA3BB4AE2ED88DE90097A97A /* ExceptionHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExceptionHandler.m; sourceTree = ""; }; + DA3BB4B02ED88DEC0097A97A /* PostHogExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PostHogExample-Bridging-Header.h"; sourceTree = ""; }; + DA3BB4DD2ED992780097A97A /* PostHogBinaryImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogBinaryImageInfo.swift; sourceTree = ""; }; + DA3BB4DE2ED992780097A97A /* PostHogDebugImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogDebugImageProvider.swift; sourceTree = ""; }; + DA3BB5042EDF15320097A97A /* PostHogStackFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStackFrame.swift; sourceTree = ""; }; DA4FFB142DA93C78006BAEEA /* PostHogSessionReplayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayTest.swift; sourceTree = ""; }; DA4FFBB42DAD5AF9006BAEEA /* PostHogIdentityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogIdentityTests.swift; sourceTree = ""; }; DA53DE702D3E299F00C38DCA /* fixture_remote_config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = fixture_remote_config.json; sourceTree = ""; }; @@ -738,6 +755,7 @@ DA7185472D07E11200396388 /* output_3.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = output_3.webp; sourceTree = ""; }; DA74644E2DC8D58100C7D394 /* PostHogConsoleLogInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogConsoleLogInterceptor.swift; sourceTree = ""; }; DA86D0E22E02D78C00CF3065 /* PostHogSurveysDefaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSurveysDefaultDelegate.swift; sourceTree = ""; }; + DA8C9B962EC633FA00C6EADB /* PostHogErrorTrackingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogErrorTrackingConfig.swift; sourceTree = ""; }; DA8D37242CBEAC02005EBD27 /* PostHogExampleAutocapture.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleAutocapture.xcodeproj; path = PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj; sourceTree = ""; }; DA92C6402DCE09C700C02F3E /* PostHogLogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogLogEntry.swift; sourceTree = ""; }; DA92C6482DCE09DE00C02F3E /* PostHogLogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogLogLevel.swift; sourceTree = ""; }; @@ -991,6 +1009,9 @@ 3AA34CFB296D951A003398F4 /* ContentView.swift */, 3AA34CFD296D951B003398F4 /* Assets.xcassets */, 3AA34CFF296D951B003398F4 /* Preview Content */, + DA3BB4AD2ED88DE90097A97A /* ExceptionHandler.h */, + DA3BB4AE2ED88DE90097A97A /* ExceptionHandler.m */, + DA3BB4B02ED88DEC0097A97A /* PostHogExample-Bridging-Header.h */, ); path = PostHogExample; sourceTree = ""; @@ -1044,6 +1065,7 @@ 3AC745B6296D6FE60025C109 /* Products */, 69261D152AD92D6C00232EC7 /* Frameworks */, DAE8AEC02D9D0E7700A1FE3A /* Recovered References */, + 34EA6EED2443DFD84061990F /* PostHogDebugImageProvider.swift */, ); sourceTree = ""; }; @@ -1073,6 +1095,7 @@ DA26419B2CC0499300CB427B /* Autocapture */, 69EE82B82BA9C4DA00EB9542 /* Replay */, DA30AE872D40173D00465A64 /* Surveys */, + DA8C9B972EC633FA00C6EADB /* Error Tracking */, 69BA38E62B893F2200AA69D6 /* Resources */, 69779BED2AE6B29E00D7A48E /* Models */, 3AA4C09B2988315D006C4731 /* Utils */, @@ -1346,6 +1369,24 @@ path = Surveys; sourceTree = ""; }; + DA3BB49E2ED82E250097A97A /* Utils */ = { + isa = PBXGroup; + children = ( + DA3BB4A62ED82E410097A97A /* PostHogStackTrace.swift */, + DA3BB4DE2ED992780097A97A /* PostHogDebugImageProvider.swift */, + ); + path = Utils; + sourceTree = ""; + }; + DA3BB4E72ED9929C0097A97A /* Models */ = { + isa = PBXGroup; + children = ( + DA3BB4DD2ED992780097A97A /* PostHogBinaryImageInfo.swift */, + DA3BB5042EDF15320097A97A /* PostHogStackFrame.swift */, + ); + path = Models; + sourceTree = ""; + }; DA578E912D6768A100B3A56C /* Screen Views */ = { isa = PBXGroup; children = ( @@ -1377,6 +1418,17 @@ path = "App Life Cycle"; sourceTree = ""; }; + DA8C9B972EC633FA00C6EADB /* Error Tracking */ = { + isa = PBXGroup; + children = ( + DA3BB4E72ED9929C0097A97A /* Models */, + DA3BB49E2ED82E250097A97A /* Utils */, + DA8C9B962EC633FA00C6EADB /* PostHogErrorTrackingConfig.swift */, + DA3BB4AB2ED82EE80097A97A /* PostHogExceptionProcessor.swift */, + ); + path = "Error Tracking"; + sourceTree = ""; + }; DA8D37252CBEAC02005EBD27 /* Products */ = { isa = PBXGroup; children = ( @@ -1852,6 +1904,7 @@ TargetAttributes = { 3AA34CF6296D951A003398F4 = { CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 2610; }; 3AC745B4296D6FE60025C109 = { CreatedOnToolsVersion = 14.2; @@ -2076,6 +2129,7 @@ files = ( 3AA34D17296D9993003398F4 /* AppDelegate.swift in Sources */, 3AA34CFC296D951A003398F4 /* ContentView.swift in Sources */, + DA3BB4AF2ED88DE90097A97A /* ExceptionHandler.m in Sources */, 3AE3FB2C2991320300AFFC18 /* Api.swift in Sources */, 3A0F108329C47940002C0084 /* UIViewExample.swift in Sources */, 3AA34CFA296D951A003398F4 /* PostHogExampleApp.swift in Sources */, @@ -2103,12 +2157,16 @@ DA30AE692D3EFB4F00465A64 /* Optional+Util.swift in Sources */, DA9AE8D52D841994002F1B44 /* Survey+Util.swift in Sources */, 690FF0BF2AEFA97F00A0B06B /* FileUtils.swift in Sources */, + DA3BB4DF2ED992780097A97A /* PostHogBinaryImageInfo.swift in Sources */, + DA3BB4E02ED992780097A97A /* PostHogDebugImageProvider.swift in Sources */, 69261D252AD9787A00232EC7 /* PostHogExtensions.swift in Sources */, DA1D295E2D10B7B2003A31DA /* ApplicationLifecyclePublisher.swift in Sources */, 3AE3FB4E2993D1D600AFFC18 /* PostHogStorageManager.swift in Sources */, + DA8C9B982EC633FA00C6EADB /* PostHogErrorTrackingConfig.swift in Sources */, 3AE3FB49299391DF00AFFC18 /* PostHogStorage.swift in Sources */, DA9AE8D12D835BEA002F1B44 /* QuestionHeader.swift in Sources */, 69261D232AD9784200232EC7 /* PostHogVersion.swift in Sources */, + DA3BB4AA2ED82E410097A97A /* PostHogStackTrace.swift in Sources */, 69779BEC2AE68E6900D7A48E /* UIViewController.swift in Sources */, 3A0F108929C9BD76002C0084 /* Errors.swift in Sources */, 3AE3FB37299162EA00AFFC18 /* PostHogApi.swift in Sources */, @@ -2228,6 +2286,7 @@ DA9CE3792D3108AD00DFE652 /* huffman_utils.c in Sources */, DA9CE37A2D3108AD00DFE652 /* alpha_processing.c in Sources */, DA9CE37B2D3108AD00DFE652 /* picture_rescale_enc.c in Sources */, + DA3BB4AC2ED82EE80097A97A /* PostHogExceptionProcessor.swift in Sources */, DA9CE37C2D3108AD00DFE652 /* cost.c in Sources */, DA9CE37D2D3108AD00DFE652 /* thread_utils.c in Sources */, DA9CE37E2D3108AD00DFE652 /* filters_neon.c in Sources */, @@ -2251,6 +2310,7 @@ 69F518382BB2BA0100F52C14 /* PostHogSwizzler.swift in Sources */, DA9AE8CF2D8357D4002F1B44 /* ConfirmationMessage.swift in Sources */, DA263D472D801117004C100D /* NumberRating.swift in Sources */, + DA3BB5052EDF15320097A97A /* PostHogStackFrame.swift in Sources */, DACC08BB2D6E3C6D00115E46 /* PostHogIntegration.swift in Sources */, DAD5DD0C2CB6DEF30087387B /* PostHogMaskViewModifier.swift in Sources */, 690FF0C52AEFAE8200A0B06B /* PostHogLegacyQueue.swift in Sources */, @@ -2286,6 +2346,7 @@ 69261D1D2AD967CD00232EC7 /* PostHogFileBackedQueue.swift in Sources */, 3AE3FB432992985A00AFFC18 /* Reachability.swift in Sources */, 69F518122BAC783300F52C14 /* CGColor+Util.swift in Sources */, + 628564DE224D783306683EEE /* PostHogDebugImageProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2453,6 +2514,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PostHogExample/PostHogExample.entitlements; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; @@ -2460,6 +2522,7 @@ DEVELOPMENT_ASSET_PATHS = "\"PostHogExample/Preview Content\""; DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = PostHogExample; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -2479,6 +2542,8 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PostHogExample/PostHogExample-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; TVOS_DEPLOYMENT_TARGET = 13.0; @@ -2491,13 +2556,17 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PostHogExample/PostHogExample.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 10; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEPLOYMENT_POSTPROCESSING = YES; DEVELOPMENT_ASSET_PATHS = "\"PostHogExample/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = PostHogExample; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -2512,11 +2581,14 @@ PRODUCT_BUNDLE_IDENTIFIER = com.posthog.PostHogExample; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + STRIP_INSTALLED_PRODUCT = YES; + STRIP_STYLE = all; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PostHogExample/PostHogExample-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; TVOS_DEPLOYMENT_TARGET = 13.0; @@ -3248,6 +3320,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PostHogExample/PostHogExample.entitlements; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; @@ -3255,6 +3328,7 @@ DEVELOPMENT_ASSET_PATHS = "\"PostHogExample/Preview Content\""; DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = PostHogExample; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3274,6 +3348,8 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PostHogExample/PostHogExample-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; TVOS_DEPLOYMENT_TARGET = 13.0; diff --git a/PostHog/Error Tracking/Models/PostHogBinaryImageInfo.swift b/PostHog/Error Tracking/Models/PostHogBinaryImageInfo.swift new file mode 100644 index 000000000..5e7a9e2e8 --- /dev/null +++ b/PostHog/Error Tracking/Models/PostHogBinaryImageInfo.swift @@ -0,0 +1,51 @@ +// +// PostHogBinaryImageInfo.swift +// PostHog +// +// Created by Ioannis Josephides on 27/11/2025. +// + +import Foundation + +/// Information about a loaded binary image (executable or dynamic library) +/// +/// This struct contains the metadata needed for server-side symbolication: +/// - UUID for matching uploaded dSYM files +/// - Load addresses for calculating symbol offsets +/// - Size for determining address ranges +struct PostHogBinaryImageInfo { + /// Full path to the binary image (e.g., "/usr/lib/system/libsystem_kernel.dylib") + let name: String + + /// UUID of the binary image, used for dSYM matching + /// Format: "A1B2C3D4-E5F6-7890-ABCD-EF1234567890" + let uuid: String? + + /// Virtual memory address from the Mach-O headers (preferred load address) + let vmAddress: UInt64 + + /// Actual load address in memory (may differ from vmAddress due to ASLR) + let address: UInt64 + + /// Size of the binary image in bytes + let size: UInt64 + + var toDictionary: [String: Any] { + var dict: [String: Any] = [ + "type": "macho", + "code_file": name, + "image_addr": String(format: "0x%llx", address), + "image_size": size, + ] + + if let uuid = uuid { + dict["debug_id"] = uuid + } + + if vmAddress > 0 { + dict["image_vmaddr"] = String(format: "0x%llx", vmAddress) + } + + return dict + } +} diff --git a/PostHog/Error Tracking/Models/PostHogStackFrame.swift b/PostHog/Error Tracking/Models/PostHogStackFrame.swift new file mode 100644 index 000000000..37f473c1e --- /dev/null +++ b/PostHog/Error Tracking/Models/PostHogStackFrame.swift @@ -0,0 +1,62 @@ +// +// PostHogStackFrame.swift +// PostHog +// +// Created by Ioannis Josephides on 02/12/2025. +// + +import Foundation + +/// Information about a single stack frame +struct PostHogStackFrame { + /// The instruction address where the frame was executing + let instructionAddress: UInt64 + + /// Name of the binary module (e.g., "MyApp", "Foundation") + let module: String? + + /// Corresponding package + let package: String? + + /// Load address of the binary image in memory + let imageAddress: UInt64? + + /// Whether this frame is considered part of the application code + let inApp: Bool + + /// Function or symbol name (raw symbol without demangling) + let function: String? + + /// Address of the symbol/function + let symbolAddress: UInt64? + + var toDictionary: [String: Any] { + var dict: [String: Any] = [:] + + dict["instruction_addr"] = String(format: "0x%llx", instructionAddress) + dict["platform"] = "ios" // always the same for iOS SDK (may need to revisit) + dict["in_app"] = inApp + + if let module = module { + dict["module"] = module + } + + if let package = package { + dict["package"] = package + } + + if let imageAddress = imageAddress { + dict["image_addr"] = String(format: "0x%llx", imageAddress) + } + + if let function = function { + dict["function"] = function + } + + if let symbolAddress = symbolAddress { + dict["symbol_addr"] = String(format: "0x%llx", symbolAddress) + } + + return dict + } +} diff --git a/PostHog/Error Tracking/PostHogErrorTrackingConfig.swift b/PostHog/Error Tracking/PostHogErrorTrackingConfig.swift new file mode 100644 index 000000000..c1e26f547 --- /dev/null +++ b/PostHog/Error Tracking/PostHogErrorTrackingConfig.swift @@ -0,0 +1,85 @@ +// +// PostHogErrorTrackingConfig.swift +// PostHog +// +// Created by Ioannis Josephides on 11/11/2025. +// + +import Foundation + +/// Configuration for error tracking and exception capture +/// +/// This class controls how exceptions are captured and processed, +/// including which stack trace frames are marked as "in-app" code. +@objc public class PostHogErrorTrackingConfig: NSObject { + // MARK: - In-App Detection Configuration + + /// List of package/bundle identifiers to be considered in-app frames + /// + /// Takes precedence over `inAppExcludes`. + /// If a frame's module matches any prefix in this list, + /// it will be marked as in-app. + /// + /// Example: + /// ```swift + /// config.errorTrackingConfig.inAppIncludes = [ + /// "com.mycompany.MyApp", + /// "com.mycompany.SharedUtils" + /// ] + /// ``` + /// + /// **Default behavior:** + /// - Automatically includes main bundle identifier + /// - Automatically includes executable name + /// + /// **Precedence:** Priority 1 (highest) + @objc public var inAppIncludes: [String] = [] + + /// List of package/bundle identifiers to be excluded from in-app frames + /// + /// Frames matching these prefixes will be marked as not in-app, + /// unless they also match `inAppIncludes` (which takes precedence). + /// + /// Example: + /// ```swift + /// config.errorTrackingConfig.inAppExcludes = [ + /// "ThirdPartySDK", + /// "AnalyticsLib" + /// ] + /// ``` + /// + /// **Precedence:** Priority 2 (after inAppIncludes) + @objc public var inAppExcludes: [String] = [] + + /// Configures whether stack trace frames are considered in-app by default + /// when the origin cannot be determined or no explicit includes/excludes match. + /// + /// - If `true` (default): Frames are in-app unless explicitly excluded (allowlist approach) + /// - If `false`: Frames are external unless explicitly included (denylist approach) + /// + /// **Default behavior when true:** + /// - Known system frameworks (Foundation, UIKit, etc.) are excluded + /// - All other packages are in-app unless in `inAppExcludes` + /// + /// **Precedence:** Priority 4 (final fallback) + /// + /// Default: true + @objc public var inAppByDefault: Bool = true + + // MARK: - Initialization + + override public init() { + super.init() + + // Auto-add main bundle identifier + if let bundleId = Bundle.main.bundleIdentifier { + inAppIncludes.append(bundleId) + } + + // Auto-add executable name + // This helps catch app code when bundle ID might not be in module name + if let executableName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String { + inAppIncludes.append(executableName) + } + } +} diff --git a/PostHog/Error Tracking/PostHogExceptionProcessor.swift b/PostHog/Error Tracking/PostHogExceptionProcessor.swift new file mode 100644 index 000000000..80b03576a --- /dev/null +++ b/PostHog/Error Tracking/PostHogExceptionProcessor.swift @@ -0,0 +1,371 @@ +// +// PostHogExceptionProcessor.swift +// PostHog +// +// Created by Ioannis Josephides on 14/11/2025. +// + +import Darwin +import Foundation + +/// Processes errors and exceptions into PostHog's $exception event format +/// +/// This class converts Swift Error and NSException instances into PostHog exception event properties. +/// It automatically attaches binary image metadata (`$debug_images`) needed for server-side symbolication. +/// +enum PostHogExceptionProcessor { + // MARK: - Public API + + /// Convert Error/NSError to properties + /// + /// - Parameters: + /// - error: The error to convert + /// - handled: Whether the error was caught/handled + /// - mechanismType: The mechanism that captured the error (e.g., "generic", "NSException") + /// - config: Error tracking configuration for in-app detection + /// - Returns: Dictionary of properties in PostHog's $exception event format + static func errorToProperties( + _ error: Error, + handled: Bool, + mechanismType: String, + config: PostHogErrorTrackingConfig + ) -> [String: Any] { + var properties: [String: Any] = [:] + + properties["$exception_level"] = "error" // TODO: figure this out from error wrapped type + + let exceptions = buildExceptionList( + from: error, + handled: handled, + mechanismType: mechanismType, + config: config + ) + + attachExceptionsAndDebugImages(exceptions, to: &properties) + + return properties + } + + /// Convert NSException to properties + /// + /// Note: Uses the exception's own stack trace (`callStackReturnAddresses`) if available, + /// otherwise falls back to capturing the current thread's stack (synthetic). + /// + /// - Parameters: + /// - exception: The NSException to convert + /// - handled: Whether the exception was caught/handled + /// - mechanismType: The mechanism type for categorizing the exception + /// - config: Error tracking configuration for in-app detection + /// - Returns: Dictionary of properties in PostHog's $exception event format + static func exceptionToProperties( + _ exception: NSException, + handled: Bool, + mechanismType: String, + config: PostHogErrorTrackingConfig + ) -> [String: Any] { + var properties: [String: Any] = [:] + properties["$exception_level"] = "error" // TODO: figure this out from error wrapped type + + let exceptions = buildExceptionList( + from: exception, + handled: handled, + mechanismType: mechanismType, + config: config + ) + + attachExceptionsAndDebugImages(exceptions, to: &properties) + + return properties + } + + /// Convert a message string to properties + /// + /// - Parameters: + /// - message: The error message to convert + /// - type: Optional exception type name (defaults to "String") + /// - config: Error tracking configuration for in-app detection + /// - Returns: Dictionary of properties in PostHog's $exception event format + static func messageToProperties( + _ message: String, + config: PostHogErrorTrackingConfig + ) -> [String: Any] { + var properties: [String: Any] = [:] + + properties["$exception_level"] = "error" + + var exception: [String: Any] = [:] + exception["type"] = "Message" + exception["value"] = message + exception["thread_id"] = Thread.current.threadId + + exception["mechanism"] = [ + "type": "generic-message", + "handled": true, + "synthetic": true, // always true for message exceptions - we capture current stack + ] + + if let stacktrace = buildStacktrace(config: config) { + exception["stacktrace"] = stacktrace + } + + let exceptions = [exception] + attachExceptionsAndDebugImages(exceptions, to: &properties) + + return properties + } + + // MARK: - Internal Exception Building + + /// Build list of exceptions from NSException chain + /// + /// Walks the NSException chain via NSUnderlyingErrorKey to capture all related exceptions. + /// The list is ordered from root exception to underlying exceptions (same as Android). + private static func buildExceptionList( + from exception: NSException, + handled: Bool, + mechanismType: String, + config: PostHogErrorTrackingConfig + ) -> [[String: Any]] { + var exceptions: [[String: Any]] = [] + var nsExceptions: [NSException] = [] + + // Walk exception chain via NSUnderlyingErrorKey + nsExceptions.append(exception) + + var current = exception + while let underlying = current.userInfo?[NSUnderlyingErrorKey] as? NSException { + nsExceptions.append(underlying) + current = underlying + } + + // Build exceptions (same order as Android) + for exc in nsExceptions { + if let exceptionDict = buildException( + from: exc, + handled: handled, + mechanismType: mechanismType, + config: config + ) { + exceptions.append(exceptionDict) + } + } + + return exceptions + } + + /// Build list of exceptions from error chain + /// + /// Walks the error chain via NSUnderlyingErrorKey to capture all related errors. + /// The list is ordered from root error to underlying errors (same as Android). + private static func buildExceptionList( + from error: Error, + handled: Bool, + mechanismType: String, + config: PostHogErrorTrackingConfig + ) -> [[String: Any]] { + var exceptions: [[String: Any]] = [] + var errors: [NSError] = [] + + // Walk error chain via NSUnderlyingErrorKey + let nsError = error as NSError + errors.append(nsError) + + var current = nsError + while let underlying = current.userInfo[NSUnderlyingErrorKey] as? NSError { + errors.append(underlying) + current = underlying + } + + // Build exceptions (same order as Android) + for err in errors { + if let exception = buildException( + from: err, + handled: handled, + mechanismType: mechanismType, + config: config + ) { + exceptions.append(exception) + } + } + + return exceptions + } + + /// Build a single exception dictionary from an NSError + private static func buildException( + from error: NSError, + handled: Bool, + mechanismType: String, + config: PostHogErrorTrackingConfig + ) -> [String: Any]? { + var exception: [String: Any] = [:] + + exception["type"] = extractTypeName(from: error) + + if let message = extractErrorMessage(from: error) { + exception["value"] = message + } + + if let moduleName = extractModule(from: error) { + exception["module"] = moduleName + } + + exception["thread_id"] = Thread.current.threadId + + exception["mechanism"] = [ + "type": mechanismType, + "handled": handled, + "synthetic": true, // Always true for NSError - we capture current stack + ] + + if let stacktrace = buildStacktrace(config: config) { + exception["stacktrace"] = stacktrace + } + + return exception + } + + /// Build a single exception dictionary from an NSException + private static func buildException( + from exception: NSException, + handled: Bool, + mechanismType: String, + config: PostHogErrorTrackingConfig + ) -> [String: Any]? { + var exceptionDict: [String: Any] = [:] + + exceptionDict["type"] = exception.name.rawValue + exceptionDict["value"] = exception.reason ?? "Unknown exception" + exceptionDict["thread_id"] = Thread.current.threadId + + // Use exception's real stack if available, otherwise capture current (synthetic) + let exceptionAddresses = exception.callStackReturnAddresses + let isSynthetic: Bool + let stacktrace: [String: Any]? + + if !exceptionAddresses.isEmpty { + // Use exception's actual stack trace (captured when exception was raised) + stacktrace = buildStacktraceFromAddresses(exceptionAddresses, config: config) + isSynthetic = false + } else { + // Fall back to current stack (synthetic - captured at reporting site) + stacktrace = buildStacktrace(config: config) + isSynthetic = true + } + + exceptionDict["mechanism"] = [ + "type": mechanismType, + "handled": handled, + "synthetic": isSynthetic, + ] + + if let stacktrace = stacktrace { + exceptionDict["stacktrace"] = stacktrace + } + + return exceptionDict + } + + // MARK: - Error Message Extraction + + /// Extract user-friendly error message + /// + /// Priority: + /// 1. Debug description (NSDebugDescriptionErrorKey) + /// 2. Localized description (NSLocalizedDescriptionKey) + private static func extractErrorMessage(from error: NSError) -> String? { + if let debugDesc = error.userInfo[NSDebugDescriptionErrorKey] as? String { + return "\(debugDesc) (Code: \(error.code))" + } + + return "\(error.localizedDescription) (Code: \(error.code))" + } + + /// Extract clean type name from error + /// + /// Uses Swift's type reflection to get the actual type name. + /// Falls back to error domain for Objective-C errors. + private static func extractTypeName(from error: NSError) -> String { + // Get the actual Swift type name using reflection + let typeName = String(describing: type(of: error as Error)) + + // If it's a plain NSError (not a Swift error bridged to NSError), + // the type will just be "NSError" - use domain instead + if typeName == "NSError" { + return error.domain + } + + return typeName + } + + /// Extract module name from error domain + /// + /// For Swift errors, the domain contains the full module path (e.g., "MyApp.Networking.APIError"). + /// We extract everything except the type name at the end. + private static func extractModule(from error: NSError) -> String? { + let domain = error.domain + + // For domains without dots (e.g., NSCocoaErrorDomain), return nil + guard let lastDot = domain.lastIndex(of: ".") else { + return nil + } + + // For dotted domains, extract everything before the last component (the type name) + let module = String(domain[.. [String: Any]? { + let frames = PostHogStackTrace.captureCurrentStackTraceWithMetadata(config: config) + + guard !frames.isEmpty else { return nil } + + return [ + "frames": frames.map(\.toDictionary), + "type": "raw", + ] + } + + /// Build stacktrace dictionary from raw addresses (e.g., NSException.callStackReturnAddresses) + private static func buildStacktraceFromAddresses( + _ addresses: [NSNumber], + config: PostHogErrorTrackingConfig + ) -> [String: Any]? { + // Don't strip PostHog frames for NSException - the addresses are from the exception itself + let frames = PostHogStackTrace.symbolicateAddresses(addresses, config: config, stripTopPostHogFrames: false) + + guard !frames.isEmpty else { return nil } + + return [ + "frames": frames.map(\.toDictionary), + "type": "raw", + ] + } +} + +private extension Thread { + /// Get the current thread's Mach thread ID + var threadId: Int { + Int(pthread_mach_thread_np(pthread_self())) + } +} diff --git a/PostHog/Error Tracking/Utils/PostHogDebugImageProvider.swift b/PostHog/Error Tracking/Utils/PostHogDebugImageProvider.swift new file mode 100644 index 000000000..e3922864a --- /dev/null +++ b/PostHog/Error Tracking/Utils/PostHogDebugImageProvider.swift @@ -0,0 +1,226 @@ +// +// PostHogDebugImageProvider.swift +// PostHog +// +// Created by Ioannis Josephides on 28/11/2025. +// + +import Foundation +import MachO + +/// Utility for extracting debug image metadata from loaded binary images +/// +/// This provider extracts information needed for server-side symbolication: +/// - UUID from LC_UUID load command (for dSYM matching) +/// - Text segment address and size (for address range calculation) +/// - Load addresses (for offset calculation) +/// +enum PostHogDebugImageProvider { + private static let segmentText = "__TEXT" + + /// Get all currently loaded binary images with their metadata + /// + /// Uses dyld APIs to enumerate all loaded images and parse their Mach-O headers. + /// + /// NOTE: + /// This will do for now, but we should eventually use dyld callbacks to track + /// image loading/unloading and cache images via + /// `_dyld_register_func_for_add_image` / `_dyld_register_func_for_remove_image` + /// for quicker lookups (this is what Sentry is doing as well). + /// + /// - Returns: Array of binary image info for all loaded images + static func getAllBinaryImages() -> [PostHogBinaryImageInfo] { + var images: [PostHogBinaryImageInfo] = [] + + let imageCount = _dyld_image_count() + + for index in 0 ..< imageCount { + guard let header = _dyld_get_image_header(index) else { continue } + let slide = _dyld_get_image_vmaddr_slide(index) + let name = _dyld_get_image_name(index).map { String(cString: $0) } ?? "unknown" + + if let imageInfo = parseImageInfo(header: header, slide: slide, name: name) { + images.append(imageInfo) + } + } + + return images + } + + /// Get debug images for stack frames + /// + /// Extracts unique image addresses from frames and returns their binary metadata. + /// + /// - Parameter frames: Array of stack frame dictionaries containing "image_addr" keys + /// - Returns: Array of debug image dictionaries + static func getDebugImages(for frames: [[String: Any]]) -> [[String: Any]] { + let addresses = Set(frames.compactMap { $0["image_addr"] as? String }) + guard !addresses.isEmpty else { return [] } + return getImages(for: addresses).map(\.toDictionary) + } + + /// Get debug images for exception list + /// + /// Extracts frames from all exceptions and returns their binary metadata. + /// + /// - Parameter exceptions: Array of exception dictionaries (from $exception_list) + /// - Returns: Array of debug image dictionaries + static func getDebugImages(fromExceptions exceptions: [[String: Any]]) -> [[String: Any]] { + let frames = exceptions.flatMap { exception -> [[String: Any]] in + guard let stacktrace = exception["stacktrace"] as? [String: Any], + let frames = stacktrace["frames"] as? [[String: Any]] else { return [] } + return frames + } + return getDebugImages(for: frames) + } + + // MARK: - Internal + + /// Get binary images for a set of image addresses + private static func getImages(for imageAddresses: Set) -> [PostHogBinaryImageInfo] { + guard !imageAddresses.isEmpty else { return [] } + + // Convert hex strings to UInt64 for comparison + let addressValues = Set(imageAddresses.compactMap { hexToUInt64($0) }) + guard !addressValues.isEmpty else { return [] } + + var matchedImages: [PostHogBinaryImageInfo] = [] + let imageCount = _dyld_image_count() + + for index in 0 ..< imageCount { + guard let header = _dyld_get_image_header(index) else { continue } + let slide = _dyld_get_image_vmaddr_slide(index) + let name = _dyld_get_image_name(index).map { String(cString: $0) } ?? "unknown" + + if let imageInfo = parseImageInfo(header: header, slide: slide, name: name), + addressValues.contains(imageInfo.address) + { + matchedImages.append(imageInfo) + } + } + + return matchedImages + } + + /// Parse binary image info from a Mach-O header + /// + /// Supports both 32-bit and 64-bit Mach-O formats by detecting the magic number + /// and using the appropriate header size and segment command type. + private static func parseImageInfo( + header: UnsafePointer, + slide: Int, + name: String + ) -> PostHogBinaryImageInfo? { + let is64Bit = header.pointee.magic == MH_MAGIC_64 || header.pointee.magic == MH_CIGAM_64 + + // Configuration based on architecture + let headerSize = is64Bit ? MemoryLayout.size : MemoryLayout.size + let segmentCmd = is64Bit ? UInt32(LC_SEGMENT_64) : UInt32(LC_SEGMENT) + + var uuid: String? + var textVMAddr: UInt64 = 0 + var textSize: UInt64 = 0 + + // Start of load commands (right after header) + var cmdPtr = UnsafeRawPointer(header).advanced(by: headerSize) + let ncmds = header.pointee.ncmds + + for _ in 0 ..< ncmds { + let cmd = cmdPtr.assumingMemoryBound(to: load_command.self).pointee + + switch cmd.cmd { + case UInt32(LC_UUID): + uuid = extractUUID(from: cmdPtr) + + case segmentCmd: + let segment = extractTextSegment(from: cmdPtr, is64Bit: is64Bit) + if let segment = segment { + textVMAddr = segment.vmaddr + textSize = segment.vmsize + } + + default: + break + } + + // Early exit once we have both UUID and __TEXT segment + if uuid != nil, textSize > 0 { + break + } + + cmdPtr = cmdPtr.advanced(by: Int(cmd.cmdsize)) + } + + // Calculate actual load address (vmaddr + slide) + let loadAddress = UInt64(Int64(textVMAddr) + Int64(slide)) + + return PostHogBinaryImageInfo( + name: name, + uuid: uuid, + vmAddress: textVMAddr, + address: loadAddress, + size: textSize + ) + } + + /// Extract __TEXT segment info from a segment command + /// + /// - Parameters: + /// - cmdPtr: Pointer to the segment command + /// - is64Bit: Whether this is a 64-bit segment command + /// - Returns: Tuple of (vmaddr, vmsize) if this is the __TEXT segment, nil otherwise + private static func extractTextSegment( + from cmdPtr: UnsafeRawPointer, + is64Bit: Bool + ) -> (vmaddr: UInt64, vmsize: UInt64)? { + if is64Bit { + let segCmd = cmdPtr.assumingMemoryBound(to: segment_command_64.self).pointee + let segName = withUnsafeBytes(of: segCmd.segname) { ptr -> String? in + let bytes = ptr.bindMemory(to: CChar.self) + guard let baseAddress = bytes.baseAddress else { return nil } + return String(cString: baseAddress) + } + guard segName == segmentText else { return nil } + return (segCmd.vmaddr, segCmd.vmsize) + } else { + let segCmd = cmdPtr.assumingMemoryBound(to: segment_command.self).pointee + let segName = withUnsafeBytes(of: segCmd.segname) { ptr -> String? in + let bytes = ptr.bindMemory(to: CChar.self) + guard let baseAddress = bytes.baseAddress else { return nil } + return String(cString: baseAddress) + } + guard segName == segmentText else { return nil } + return (UInt64(segCmd.vmaddr), UInt64(segCmd.vmsize)) + } + } + + /// Extract UUID from LC_UUID load command + /// + /// The UUID is stored as 16 bytes in the uuid_command structure. + /// We format it as a standard UUID string with hyphens. + private static func extractUUID(from cmdPtr: UnsafeRawPointer) -> String? { + let uuidCmd = cmdPtr.assumingMemoryBound(to: uuid_command.self).pointee + let uuid = uuidCmd.uuid + + // Format as UUID string: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + return String( + format: "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", + uuid.0, uuid.1, uuid.2, uuid.3, + uuid.4, uuid.5, + uuid.6, uuid.7, + uuid.8, uuid.9, + uuid.10, uuid.11, uuid.12, uuid.13, uuid.14, uuid.15 + ) + } + + // MARK: - Helpers + + /// Convert hex string (e.g., "0x100abc000") to UInt64 + private static func hexToUInt64(_ hex: String) -> UInt64? { + var hexString = hex + if hexString.hasPrefix("0x") || hexString.hasPrefix("0X") { + hexString = String(hexString.dropFirst(2)) + } + return UInt64(hexString, radix: 16) + } +} diff --git a/PostHog/Error Tracking/Utils/PostHogStackTrace.swift b/PostHog/Error Tracking/Utils/PostHogStackTrace.swift new file mode 100644 index 000000000..0a6edaeca --- /dev/null +++ b/PostHog/Error Tracking/Utils/PostHogStackTrace.swift @@ -0,0 +1,181 @@ +// +// PostHogStackTrace.swift +// PostHog +// +// Created by Ioannis Josephides on 13/11/2025. +// + +import Darwin +import Foundation +import MachO + +/// Utility for capturing and processing stack traces +/// +/// This class provides methods to capture stack traces from the current thread +/// and format them consistently for error tracking. +/// +enum PostHogStackTrace { + + // MARK: - Stack Trace Capture + + /// Captures current stack trace using dladdr() for rich metadata + /// + /// Automatically strips PostHog SDK frames from the top of the stack trace + /// so the trace starts at user code. + static func captureCurrentStackTraceWithMetadata( + config: PostHogErrorTrackingConfig + ) -> [PostHogStackFrame] { + let addresses = Thread.callStackReturnAddresses + return symbolicateAddresses(addresses, config: config, stripTopPostHogFrames: true) + } + + /// Symbolicate an array of return addresses using dladdr() + /// + /// - Parameters: + /// - addresses: Array of return addresses to symbolicate + /// - config: Error tracking configuration + /// - stripTopPostHogFrames: If true, strips PostHog SDK frames from the top of the stack + static func symbolicateAddresses( + _ addresses: [NSNumber], + config: PostHogErrorTrackingConfig, + stripTopPostHogFrames: Bool = false + ) -> [PostHogStackFrame] { + var frames: [PostHogStackFrame] = [] + var shouldCollectFrame = !stripTopPostHogFrames + + for addressNum in addresses { + let address = addressNum.uintValue + var info = Dl_info() + + guard dladdr(UnsafeRawPointer(bitPattern: UInt(address)), &info) != 0 else { + continue + } + + var module: String? + var package: String? + var imageAddress: UInt64? + var inApp = false + + // Binary image info + if let imageName = info.dli_fname { + let path = String(cString: imageName) + let moduleName = (path as NSString).lastPathComponent + module = moduleName + package = path + imageAddress = UInt64(UInt(bitPattern: info.dli_fbase)) + inApp = isInApp(module: moduleName, config: config) + } + + // Skip PostHog frames at the top of the stack + if !shouldCollectFrame { + if isPostHogModule(module) { + continue + } + shouldCollectFrame = true + } + + // Function/symbol info (raw symbols without demangling) + var function: String? + var symbolAddress: UInt64? + if let symbolName = info.dli_sname { + function = String(cString: symbolName) // Use raw symbol + symbolAddress = UInt64(UInt(bitPattern: info.dli_saddr)) + } + + let frame = PostHogStackFrame( + instructionAddress: UInt64(address), + module: module, + package: package, + imageAddress: imageAddress, + inApp: inApp, + function: function, + symbolAddress: symbolAddress + ) + + frames.append(frame) + } + + return frames + } + + // MARK: - In-App Detection + + /// Determines if a frame is considered in-app + /// + /// Priority system (matches posthog-flutter's _isInAppFrame): + /// 1. inAppIncludes (highest priority) + /// 2. inAppExcludes + /// 3. Known system frameworks (hardcoded) + /// 4. inAppByDefault (final fallback) + /// + /// Note: Uses prefix matching, unlike Flutter which uses exact package matching. + /// This matches Android's behavior for consistency. + /// + /// - Parameters: + /// - module: The module/binary name to check + /// - config: Error tracking configuration + /// - Returns: true if the frame should be marked as in-app + static func isInApp(module: String, config: PostHogErrorTrackingConfig) -> Bool { + // Priority 1: Check includes (highest priority) + if config.inAppIncludes.contains(where: { module.hasPrefix($0) }) { + return true + } + + // Priority 2: Check excludes + if config.inAppExcludes.contains(where: { module.hasPrefix($0) }) { + return false + } + + // Priority 3: Check known system frameworks (hardcoded) + if isSystemFramework(module) { + return false + } + + // Priority 4: Use default (final fallback) + return config.inAppByDefault + } + + /// Known system framework prefixes + /// + /// Note: This list is based on common system frameworks and dylibs on iOS. + /// It may need to be updated based on real-world usage, or moved to cymbal + /// which can further categorize frames and override in-app frames based on module paths + private static let systemPrefixes = [ + "Foundation", + "UIKit", + "CoreFoundation", + "libsystem_kernel.dylib", + "libsystem_pthread.dylib", + "libdispatch.dylib", + "CoreGraphics", + "QuartzCore", + "Security", + "SystemConfiguration", + "CFNetwork", + "CoreData", + "CoreLocation", + "AVFoundation", + "Metal", + "MetalKit", + "SwiftUI", + "Combine", + "AppKit", + "libswift", + "IOKit", + "WebKit", + "GraphicsServices", + ] + + /// Check if a module is a known system framework + private static func isSystemFramework(_ module: String) -> Bool { + systemPrefixes.contains { module.hasPrefix($0) } + } + + // MARK: - PostHog Frame Detection + + /// Check if a module belongs to the PostHog SDK + private static func isPostHogModule(_ module: String?) -> Bool { + guard let module = module else { return false } + return module == "PostHog" || module.hasPrefix("PostHog.") + } +} diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index 2542efdbd..a1d6e73f0 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -146,6 +146,9 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? @objc public let sessionReplayConfig: PostHogSessionReplayConfig = .init() #endif + /// Configuration for error tracking + @objc public let errorTrackingConfig: PostHogErrorTrackingConfig = .init() + /// Enable mobile surveys /// /// Default: true diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index d316326cb..23239ea33 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -1438,6 +1438,115 @@ let maxRetryDelay = 30.0 } #endif + // MARK: - Error Tracking + + /// Capture a Swift Error or NSError + /// + /// Captures an error as a `$exception` event with full stack trace and error chain information. + /// The error will be marked as handled by default. + /// + /// Example: + /// ```swift + /// do { + /// try FileManager.default.removeItem(at: badFileUrl) + /// } catch { + /// PostHog.shared.captureException(error) + /// } + /// ``` + /// + /// - Parameters: + /// - error: The error to capture (can be any Error or NSError) + /// - properties: Optional additional properties to attach to the event + @objc(captureExceptionWithError:properties:) + public func captureException( + _ error: Error, + properties: [String: Any]? = nil + ) { + guard isEnabled() else { return } + + let errorProperties = PostHogExceptionProcessor.errorToProperties( + error, + handled: true, + mechanismType: "generic-error", + config: config.errorTrackingConfig + ) + + var mergedProperties = errorProperties + properties?.forEach { mergedProperties[$0.key] = $0.value } + + capture("$exception", properties: mergedProperties) + } + + /// Capture an NSException + /// + /// Captures an NSException as a `$exception` event with full stack trace. + /// This is useful for Objective-C code that uses NSException. + /// + /// Example: + /// ```objc + /// @try { + /// [self riskyOperation]; + /// } @catch (NSException *exception) { + /// [[PostHog shared] captureExceptionWithNSException:exception properties:nil]; + /// } + /// ``` + /// + /// - Parameters: + /// - exception: The NSException to capture + /// - properties: Optional additional properties to attach to the event + @objc(captureExceptionWithNSException:properties:) + public func captureException( + _ exception: NSException, + properties: [String: Any]? = nil + ) { + guard isEnabled() else { return } + + let exceptionProperties = PostHogExceptionProcessor.exceptionToProperties( + exception, + handled: true, + mechanismType: "generic-nsexception", + config: config.errorTrackingConfig + ) + + var mergedProperties = exceptionProperties + properties?.forEach { mergedProperties[$0.key] = $0.value } + + capture("$exception", properties: mergedProperties) + } + + /// Capture an error message as an exception + /// + /// Captures a string message as a `$exception` event with stack trace from the capture point. + /// Useful when you want to report an error condition without an actual Error object. + /// + /// Example: + /// ```swift + /// if unexpectedCondition { + /// PostHog.shared.captureException("Unexpected state detected") + /// } + /// ``` + /// + /// - Parameters: + /// - message: The error message to capture + /// - properties: Optional additional properties to attach to the event + @objc(captureExceptionWithMessage:properties:) + public func captureException( + _ message: String, + properties: [String: Any]? = nil + ) { + guard isEnabled() else { return } + + let messageProperties = PostHogExceptionProcessor.messageToProperties( + message, + config: config.errorTrackingConfig + ) + + var mergedProperties = messageProperties + properties?.forEach { mergedProperties[$0.key] = $0.value } + + capture("$exception", properties: mergedProperties) + } + private func installIntegrations() { guard installedIntegrations.isEmpty else { hedgeLog("Integrations already installed. Call uninstallIntegrations() first.") diff --git a/PostHogExample/AppDelegate.swift b/PostHogExample/AppDelegate.swift index dc9cd83db..64dc8842b 100644 --- a/PostHogExample/AppDelegate.swift +++ b/PostHogExample/AppDelegate.swift @@ -12,7 +12,8 @@ import UIKit class AppDelegate: NSObject, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { let config = PostHogConfig( - apiKey: "phc_WKfvDfedaJEDCoUmt9pVa3OWtbbUP1W2ctxwXkt3A3n" + apiKey: "phc_DOkauJvMj0YFtJsPHqzH6BgpFm79CvU9DPE5E22yRMk", + host: "http://localhost:8010" ) // the ScreenViews for SwiftUI does not work, the names are not useful config.captureScreenViews = false @@ -20,10 +21,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { // config.flushAt = 1 // config.flushIntervalSeconds = 30 config.debug = true + config.flushAt = 1 config.sendFeatureFlagEvent = false #if os(iOS) - config.sessionReplay = true + config.sessionReplay = false config.sessionReplayConfig.screenshotMode = true config.sessionReplayConfig.maskAllTextInputs = true config.sessionReplayConfig.maskAllImages = true diff --git a/PostHogExample/ContentView.swift b/PostHogExample/ContentView.swift index 7b8722825..7de839891 100644 --- a/PostHogExample/ContentView.swift +++ b/PostHogExample/ContentView.swift @@ -117,6 +117,42 @@ struct ContentView: View { } } + /// Creates a multi-level async call chain to test stack trace capture + func captureAsyncError() async { + do { + await try asyncLevel1() + } catch { + PostHogSDK.shared.captureException(error, properties: [ + "is_test": true, + "error_type": "async_await_chain", + "capture_point": "top_level_catch", + ]) + } + } + + private func asyncLevel1() async throws { + // Simulate some async work + await Task.sleep(10_000_000) // 0.01 seconds + try await asyncLevel2() + } + + private func asyncLevel2() async throws { + // Simulate more async work + await Task.sleep(20_000_000) // 0.02 seconds + try await asyncLevel3() + } + + private func asyncLevel3() async throws { + // Simulate final async work before error + await Task.sleep(30_000_000) // 0.03 seconds + + // Throw an error from deep in the async chain + throw AsyncTestError.deepAsyncError( + message: "Error occurred in async level 3", + context: ["level": 3, "operation": "data_processing"] + ) + } + func triggerAuthentication() { signInViewModel.triggerAuthentication() } @@ -265,6 +301,97 @@ struct ContentView: View { } } + Section("Error tracking") { + Button("Capture Swift Enum Error (with associated value)") { + do { + throw SampleAppError.generalAppError(ErrorDetails(code: 10, reason: "some reason")) + } catch { + PostHogSDK.shared.captureException(error, properties: [ + "is_test": true, + ]) + } + } + + Button("Capture NSException (Constructed)") { + let exception = NSException( + name: NSExceptionName("PostHogTestException"), + reason: "Manual test exception for error tracking validation", + userInfo: [ + "test_scenario": "manual_button_press", + "app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown", + ] + ) + + PostHogSDK.shared.captureException(exception, properties: [ + "is_test": true, + "user_initiated": true, + "exception_type": "safe_nsexception", + ]) + } + + Button("Trigger Real NSRangeException") { + ExceptionHandler.try({ + ExceptionHandler.triggerSampleRangeException() + }, catch: { exception in + PostHogSDK.shared.captureException(exception, properties: [ + "is_test": true, + "exception_type": "real_nsrange_exception", + "caught_by": "objective_c_wrapper", + ]) + }) + } + + Button("Trigger Real NSInvalidArgumentException") { + ExceptionHandler.try({ + ExceptionHandler.triggerSampleInvalidArgumentException() + }, catch: { exception in + PostHogSDK.shared.captureException(exception, properties: [ + "is_test": true, + "exception_type": "real_invalid_argument_exception", + "caught_by": "objective_c_wrapper", + ]) + }) + } + + Button("Trigger Custom NSException") { + ExceptionHandler.try({ + ExceptionHandler.triggerSampleGenericException() + }, catch: { exception in + PostHogSDK.shared.captureException(exception, properties: [ + "is_test": true, + "exception_type": "real_custom_exception", + "caught_by": "objective_c_wrapper", + ]) + }) + } + + Button("Trigger Chained NSException") { + ExceptionHandler.try({ + ExceptionHandler.triggerChainedException() + }, catch: { exception in + PostHogSDK.shared.captureException(exception, properties: [ + "is_test": true, + "exception_type": "chained_exception", + "caught_by": "objective_c_wrapper", + "scenario": "network_database_business_chain", + ]) + }) + } + + Button("Trigger with Message") { + PostHogSDK.shared.captureException("Unexpected state detected", properties: [ + "is_test": true, + "app_state": "some_state", + ]) + } + + Button("Capture Async/Await Error") { + Task { + await captureAsyncError() + } + } + } + Section("PostHog beers") { if !api.beers.isEmpty { ForEach(api.beers) { beer in @@ -304,3 +431,30 @@ struct ContentView_Previews: PreviewProvider { ContentView() } } + +enum SampleAppError: LocalizedError { + case generalAppError(ErrorDetails) + + var errorDescription: String? { + switch self { + case let .generalAppError(details): + return "Custom error description for SampleAppError.generalAppError with details: \(details)" + } + } +} + +struct ErrorDetails { + let code: Int + let reason: String +} + +enum AsyncTestError: LocalizedError { + case deepAsyncError(message: String, context: [String: Any]) + + var errorDescription: String? { + switch self { + case let .deepAsyncError(message, context): + return "Async Error: \(message) | Context: \(context)" + } + } +} diff --git a/PostHogExample/ExceptionHandler.h b/PostHogExample/ExceptionHandler.h new file mode 100644 index 000000000..fb2520275 --- /dev/null +++ b/PostHogExample/ExceptionHandler.h @@ -0,0 +1,44 @@ +// +// ExceptionHandler.h +// PostHogExample +// +// Created for NSException handling in Swift +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ExceptionHandler : NSObject + +/// Execute a block and catch any NSExceptions that occur +/// @param tryBlock The block to execute that might throw an NSException +/// @param catchBlock The block to execute if an NSException is caught ++ (void)tryBlock:(void(^)(void))tryBlock + catch:(void(^)(NSException *exception))catchBlock; + +/// Execute a block and catch any NSExceptions, with optional finally block +/// @param tryBlock The block to execute that might throw an NSException +/// @param catchBlock The block to execute if an NSException is caught +/// @param finallyBlock The block to execute regardless of whether an exception occurred ++ (void)tryBlock:(void(^)(void))tryBlock + catch:(void(^)(NSException *exception))catchBlock + finally:(nullable void(^)(void))finallyBlock; + +/// Trigger a sample NSRangeException for testing purposes ++ (void)triggerSampleRangeException; + +/// Trigger a sample NSInvalidArgumentException for testing purposes ++ (void)triggerSampleInvalidArgumentException; + +/// Trigger a sample NSGenericException for testing purposes ++ (void)triggerSampleGenericException; + +/// Trigger a chained exception scenario for testing exception chaining +/// This demonstrates how exceptions can be caught and rethrown with additional context ++ (void)triggerChainedException; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/PostHogExample/ExceptionHandler.m b/PostHogExample/ExceptionHandler.m new file mode 100644 index 000000000..f96a46c81 --- /dev/null +++ b/PostHogExample/ExceptionHandler.m @@ -0,0 +1,120 @@ +// +// ExceptionHandler.m +// PostHogExample +// +// Created for NSException handling in Swift +// + +#import "ExceptionHandler.h" + +@implementation ExceptionHandler + ++ (void)tryBlock:(void(^)(void))tryBlock + catch:(void(^)(NSException *exception))catchBlock { + [self tryBlock:tryBlock catch:catchBlock finally:nil]; +} + ++ (void)tryBlock:(void(^)(void))tryBlock + catch:(void(^)(NSException *exception))catchBlock + finally:(nullable void(^)(void))finallyBlock { + @try { + if (tryBlock) { + tryBlock(); + } + } + @catch (NSException *exception) { + if (catchBlock) { + catchBlock(exception); + } + } + @finally { + if (finallyBlock) { + finallyBlock(); + } + } +} + ++ (void)triggerSampleRangeException { + // This will throw an NSRangeException + NSArray *array = @[@"item1", @"item2", @"item3"]; + [array objectAtIndex:10]; // Index out of bounds +} + ++ (void)triggerSampleInvalidArgumentException { + // This will throw an NSInvalidArgumentException + NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; + [mutableArray insertObject:nil atIndex:0]; // Inserting nil object +} + ++ (void)triggerSampleGenericException { + // This will throw a custom NSException + @throw [NSException exceptionWithName:@"CustomTestException" + reason:@"This is a manually triggered exception for testing" + userInfo:@{ + @"test_type": @"manual_trigger", + @"timestamp": [NSDate date], + @"source": @"ExceptionHandler.triggerSampleGenericException" + }]; +} + ++ (void)triggerChainedException { + // Start the chained exception scenario + [self performDatabaseOperation]; +} + +// MARK: - Private Helper Methods for Exception Chaining + +/// Simulates a high-level business operation that calls lower-level functions ++ (void)performDatabaseOperation { + @try { + [self connectToDatabase]; + } + @catch (NSException *exception) { + // Catch the lower-level exception and wrap it with business context + NSException *businessException = [NSException exceptionWithName:@"DatabaseOperationException" + reason:@"Failed to perform user data synchronization" + userInfo:@{ + @"operation": @"user_sync", + @"retry_count": @3, + @"timestamp": [NSDate date], + NSUnderlyingErrorKey: exception // This creates the exception chain + }]; + @throw businessException; + } +} + +/// Simulates a database connection that calls even lower-level network functions ++ (void)connectToDatabase { + @try { + [self establishNetworkConnection]; + } + @catch (NSException *exception) { + // Catch the network exception and wrap it with database context + NSException *dbException = [NSException exceptionWithName:@"DatabaseConnectionException" + reason:@"Unable to establish database connection" + userInfo:@{ + @"database_host": @"db.example.com", + @"connection_timeout": @30, + @"retry_attempts": @2, + NSUnderlyingErrorKey: exception // Chain the network exception + }]; + @throw dbException; + } +} + +/// Simulates the lowest-level network operation that throws the original exception ++ (void)establishNetworkConnection { + // This is the root cause - a network connectivity issue + @throw [NSException exceptionWithName:@"NetworkException" + reason:@"Connection refused by remote server" + userInfo:@{ + @"error_code": @"ECONNREFUSED", + @"host": @"api.example.com", + @"port": @443, + @"protocol": @"HTTPS", + @"timestamp": [NSDate date] + }]; +} + + +@end diff --git a/PostHogExample/PostHogExample-Bridging-Header.h b/PostHogExample/PostHogExample-Bridging-Header.h new file mode 100644 index 000000000..e0566fd55 --- /dev/null +++ b/PostHogExample/PostHogExample-Bridging-Header.h @@ -0,0 +1,8 @@ +// +// PostHogExample-Bridging-Header.h +// PostHogExample +// +// Bridging header to expose Objective-C code to Swift +// + +#import "ExceptionHandler.h" diff --git a/build-tools/upload-symbols b/build-tools/upload-symbols new file mode 100755 index 000000000..f46664f95 --- /dev/null +++ b/build-tools/upload-symbols @@ -0,0 +1,86 @@ +#!/bin/bash +# +# PostHog Debug Symbols Upload Script +# https://posthog.com/docs/error-tracking/upload-source-maps/ios +# +# Xcode Build Phase Setup: +# SPM: "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols" +# CocoaPods: "${PODS_ROOT}/PostHog/build-tools/upload-symbols" +# +# Input Files (required for script sandboxing): +# ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME} +# ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME} +# ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist +# $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH) +# +# Build Settings: +# DEBUG_INFORMATION_FORMAT = DWARF with dSYM File (script will skip DWARF-only builds) +# +# Environment Variables (optional): +# POSTHOG_CLI_INSTALL_DIR - Custom directory containing posthog-cli binary +# + +# Validate environment +if [ -z "${DWARF_DSYM_FOLDER_PATH}" ]; then + echo "warning: DWARF_DSYM_FOLDER_PATH not set" + exit 0 +fi + +if [ ! -d "${DWARF_DSYM_FOLDER_PATH}" ]; then + echo "warning: dSYM folder not found: ${DWARF_DSYM_FOLDER_PATH}" + exit 0 +fi + +# Check if folder contains any dSYM bundles +if [ -z "$(find "${DWARF_DSYM_FOLDER_PATH}" -name '*.dSYM' -type d 2>/dev/null)" ]; then + echo "info: No dSYM bundles found in ${DWARF_DSYM_FOLDER_PATH}" + exit 0 +fi + +# Find posthog-cli (Xcode doesn't load shell profiles) +# Priority: env var override > well-known location > npm global > PATH fallback +if [ -f "$HOME/.posthog/posthog-cli" ]; then + PH_CLI_PATH="$HOME/.posthog/posthog-cli" +else + # Check if installed via npm -g @posthog/cli + NPM_GLOBAL_PREFIX=$(npm prefix -g 2>/dev/null) + if [ -n "$NPM_GLOBAL_PREFIX" ] && [ -f "$NPM_GLOBAL_PREFIX/bin/posthog-cli" ]; then + PH_CLI_PATH="$NPM_GLOBAL_PREFIX/bin/posthog-cli" + else + # Check if installed as local dependency + NPM_LOCAL_ROOT=$(npm root 2>/dev/null) + if [ -n "$NPM_LOCAL_ROOT" ] && [ -f "$NPM_LOCAL_ROOT/.bin/posthog-cli" ]; then + PH_CLI_PATH="$NPM_LOCAL_ROOT/.bin/posthog-cli" + else + # Fallback to searching common locations + export PATH="/usr/local/bin:/opt/homebrew/bin:$HOME/.cargo/bin:$HOME/.local/bin:$HOME/.posthog:$PATH" + PH_CLI_PATH=$(command -v posthog-cli 2>/dev/null) + fi + fi +fi + +if [ -z "$PH_CLI_PATH" ] || [ ! -x "$PH_CLI_PATH" ]; then + echo "error: posthog-cli not found, install with: npm install -g @posthog/cli" + exit 1 +fi + +# Build CLI arguments +CLI_ARGS="--directory $DWARF_DSYM_FOLDER_PATH" + +# Pass main target dSYM name for accurate version extraction +if [ -n "${DWARF_DSYM_FILE_NAME}" ]; then + CLI_ARGS="$CLI_ARGS --main-dsym $DWARF_DSYM_FILE_NAME" +fi + +# Pass version info from Xcode build settings (overrides plist extraction) +if [ -n "${PRODUCT_BUNDLE_IDENTIFIER}" ]; then + CLI_ARGS="$CLI_ARGS --project $PRODUCT_BUNDLE_IDENTIFIER" +fi +if [ -n "${MARKETING_VERSION}" ]; then + CLI_ARGS="$CLI_ARGS --version $MARKETING_VERSION" +fi +if [ -n "${CURRENT_PROJECT_VERSION}" ]; then + CLI_ARGS="$CLI_ARGS --build $CURRENT_PROJECT_VERSION" +fi + +$PH_CLI_PATH exp dsym upload $CLI_ARGS || exit 1 \ No newline at end of file