From 82ad98452527451beeec9b09d4b54712ad64034e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 13:34:49 -0700 Subject: [PATCH 1/8] feat(demo): add SecondaryView with crash/ANR actions --- examples/demo/App.xcodeproj/project.pbxproj | 22 +++-- examples/demo/App/Views/ContentView.swift | 10 +++ examples/demo/App/Views/SecondaryView.swift | 93 +++++++++++++++++++++ 3 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 examples/demo/App/Views/SecondaryView.swift diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj index c2eb9fe65..ab0de05ff 100644 --- a/examples/demo/App.xcodeproj/project.pbxproj +++ b/examples/demo/App.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */; }; 2FCC417641D480849E99588B /* SmsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E194A3F19072CB17A8F1A12E /* SmsSection.swift */; }; 3038C8C43A465DFED77AA533 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 38E86618D2F63D37CA764656 /* SecondaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6902EC187E8FAC8926412762 /* SecondaryView.swift */; }; 3927A4BF207695E98A57E445 /* TagsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */; }; 39D2C94F79A62BFF9DE5DBA9 /* OutcomeDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */; }; 3C899E2494DE29756F5451BE /* OneSignalNotifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -331,6 +332,7 @@ 5984B93007C6B85AFE09045A /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = ""; }; 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSection.swift; sourceTree = ""; }; 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalService.swift; sourceTree = ""; }; + 6902EC187E8FAC8926412762 /* SecondaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryView.swift; sourceTree = ""; }; 6B00BC406653BC6B08ECCE26 /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secrets.plist; sourceTree = ""; }; 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetBundle.swift; sourceTree = ""; }; 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -358,7 +360,7 @@ E194A3F19072CB17A8F1A12E /* SmsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsSection.swift; sourceTree = ""; }; ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = ""; }; F46DFACB9F304B9374F3C570 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; - "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; + "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -559,15 +561,16 @@ B9C3E998662065E7D921A5CA /* Components */, 53EC9A9E14C3568A020C0977 /* Sections */, 35F726E64F9B6817F917227C /* ContentView.swift */, + 6902EC187E8FAC8926412762 /* SecondaryView.swift */, B3E7F504B0421F2B6247E2F5 /* Theme.swift */, ); path = Views; sourceTree = ""; }; - "TEMP_9E448277-C21D-48E5-98E2-992293BCF68A" /* demo */ = { + "TEMP_D8085C8F-3CA5-457D-9FB3-7C8E9DA8B66A" /* demo */ = { isa = PBXGroup; children = ( - "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */, + "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */, ); path = demo; sourceTree = ""; @@ -820,6 +823,7 @@ 20792A9930A201E187AA0ABF /* PreferencesService.swift in Sources */, EA93E372AA3E66073487B89C /* PushSection.swift in Sources */, 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */, + 38E86618D2F63D37CA764656 /* SecondaryView.swift in Sources */, 28D491D31B5C07E4D4F48A7D /* SecretsConfig.swift in Sources */, 7B94F48C31E0BEA4B8CB20E2 /* SectionCard.swift in Sources */, 27C72DF35BE082E3E1093F75 /* SendIamSection.swift in Sources */, @@ -936,7 +940,7 @@ /* Begin XCBuildConfiguration section */ 0D2EF3911CA89837C30DB0D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -954,7 +958,7 @@ }; 4A0C935808978B5A7673E412 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1034,7 +1038,7 @@ }; D0E56A85F1C385808720F94B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1056,7 +1060,7 @@ }; EB1CC3A930E09FEBECF9195D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1079,7 +1083,7 @@ }; F305A3E63851EE49DA2D190E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -1102,7 +1106,7 @@ }; F5FD25168D9B32A08A468069 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; DEVELOPMENT_TEAM = 99SW8E36CT; diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index cdc99be60..9e6578488 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -31,6 +31,7 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var viewModel: OneSignalViewModel @EnvironmentObject var toast: ToastPresenter + @State private var showSecondary = false var body: some View { NavigationStack { @@ -51,6 +52,12 @@ struct ContentView: View { CustomEventsSection() LocationSection() LiveActivitySection() + ActionButton( + "NEXT SCREEN", + accessibilityID: "next_screen_button" + ) { + showSecondary = true + } } .padding(.horizontal, OS.Spacing.pagePadding) .padding(.top, OS.Spacing.pagePadding) @@ -73,6 +80,9 @@ struct ContentView: View { .toolbarBackground(.visible, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) .toolbar { toolbarContent } + .navigationDestination(isPresented: $showSecondary) { + SecondaryView() + } } .osCenteredDialog( isPresented: Binding( diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift new file mode 100644 index 000000000..af83fa499 --- /dev/null +++ b/examples/demo/App/Views/SecondaryView.swift @@ -0,0 +1,93 @@ +/** + * Modified MIT License + * + * Copyright 2024 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import Foundation +import SwiftUI + +/// Isolated screen for destructive crash / ANR actions, matching Android's SecondaryActivity. +struct SecondaryView: View { + var body: some View { + VStack(spacing: 16) { + Spacer() + + Text("Secondary Screen") + .font(.system(size: 28)) + .foregroundColor(OS.Color.bodyText) + .accessibilityIdentifier("secondary_screen_title") + + ActionButton( + "CRASH", + style: .outline, + accessibilityID: "crash_button" + ) { + triggerCrash() + } + .padding(.top, 16) + + ActionButton( + "SIMULATE ANR (10s block)", + style: .outline, + accessibilityID: "anr_button" + ) { + triggerAnr() + } + + Spacer() + } + .padding(.horizontal, OS.Spacing.pagePadding) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(OS.Color.lightBackground.ignoresSafeArea()) + .navigationTitle("Secondary Screen") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(OS.Color.primary, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarColorScheme(.dark, for: .navigationBar) + } + + /// NSException reaches the SDK uncaught-exception handler. Swift `fatalError` is a POSIX + /// signal, which that handler does not intercept. + private func triggerCrash() { + let formatter = DateFormatter() + formatter.dateFormat = "MMM dd, yyyy HH:mm:ss" + let timestamp = formatter.string(from: Date()) + NSException( + name: NSExceptionName("RuntimeException"), + reason: "Test crash from OneSignal Demo App - \(timestamp)", + userInfo: nil + ).raise() + } + + private func triggerAnr() { + Thread.sleep(forTimeInterval: 10) + } +} + +#Preview { + NavigationStack { + SecondaryView() + } +} From b84aace320c89a72a67918b69fd8a57308009aab Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 13:56:18 -0700 Subject: [PATCH 2/8] refactor(demo): remove ANR action from SecondaryView --- examples/demo/App/Views/SecondaryView.swift | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift index af83fa499..549f1472c 100644 --- a/examples/demo/App/Views/SecondaryView.swift +++ b/examples/demo/App/Views/SecondaryView.swift @@ -28,10 +28,10 @@ import Foundation import SwiftUI -/// Isolated screen for destructive crash / ANR actions, matching Android's SecondaryActivity. +/// Isolated screen for the test-crash action, matching Android's SecondaryActivity. struct SecondaryView: View { var body: some View { - VStack(spacing: 16) { + VStack(spacing: 32) { Spacer() Text("Secondary Screen") @@ -46,15 +46,6 @@ struct SecondaryView: View { ) { triggerCrash() } - .padding(.top, 16) - - ActionButton( - "SIMULATE ANR (10s block)", - style: .outline, - accessibilityID: "anr_button" - ) { - triggerAnr() - } Spacer() } @@ -80,10 +71,6 @@ struct SecondaryView: View { userInfo: nil ).raise() } - - private func triggerAnr() { - Thread.sleep(forTimeInterval: 10) - } } #Preview { From c45ec8e47edcc3922b8cc504080de44c787d6aab Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 09:51:18 -0700 Subject: [PATCH 3/8] feat(logging): persist marked test crashes Allow demo-generated NSExceptions to bypass stack attribution through an explicit userInfo marker while preserving their runtime exception type. Co-authored-by: Cursor --- examples/demo/App/Views/SecondaryView.swift | 6 ++- .../Source/Logging/OSLogCrashHandler.swift | 15 +++++++- .../OSLogCrashHandlerTests.swift | 37 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift index 549f1472c..dc18d9770 100644 --- a/examples/demo/App/Views/SecondaryView.swift +++ b/examples/demo/App/Views/SecondaryView.swift @@ -26,6 +26,7 @@ */ import Foundation +import OneSignalOSCore import SwiftUI /// Isolated screen for the test-crash action, matching Android's SecondaryActivity. @@ -60,7 +61,8 @@ struct SecondaryView: View { } /// NSException reaches the SDK uncaught-exception handler. Swift `fatalError` is a POSIX - /// signal, which that handler does not intercept. + /// signal, which that handler does not intercept. The marker is required because this + /// exception is raised from app code, so no OneSignal frame appears on the stack. private func triggerCrash() { let formatter = DateFormatter() formatter.dateFormat = "MMM dd, yyyy HH:mm:ss" @@ -68,7 +70,7 @@ struct SecondaryView: View { NSException( name: NSExceptionName("RuntimeException"), reason: "Test crash from OneSignal Demo App - \(timestamp)", - userInfo: nil + userInfo: [OSCrashTestMarker.userInfoKey: true] ).raise() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift index b3e00c70a..91b477349 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift @@ -41,6 +41,15 @@ struct OSResolvedStackFrame: Equatable { let symbolName: String? } +/// Opts an exception into crash reporting that stack attribution would otherwise reject. +/// +/// Demo and integration apps raise their test crash from their own code, so no OneSignal +/// frame appears on the stack. Setting this key to `true` in `NSException.userInfo` is the +/// supported way to bypass attribution for those synthetic crashes. +public enum OSCrashTestMarker { + public static let userInfoKey = "com.onesignal.crash.test" +} + /// Prints KMP crash-reporter diagnostics with `NSLog`. Honors the console log level /// without going through `OneSignalLog` (listeners, alert UI, remote sink). final class OSCrashLogger: ILogger { @@ -160,7 +169,7 @@ final class OSLogCrashHandler: ILogCrashHandler { stackSymbols: [String], resolvedFrames: [OSResolvedStackFrame] ) { - guard Self.isOneSignalAtFault(resolvedFrames) else { + guard Self.isMarkedTestCrash(exception) || Self.isOneSignalAtFault(resolvedFrames) else { previousExceptionHandler?(exception) return } @@ -228,6 +237,10 @@ final class OSLogCrashHandler: ILogCrashHandler { } } + static func isMarkedTestCrash(_ exception: NSException) -> Bool { + exception.userInfo?[OSCrashTestMarker.userInfoKey] as? Bool == true + } + static func isOneSignalAtFault(_ frames: [OSResolvedStackFrame]) -> Bool { frames.contains { frame in guard let imagePath = frame.imagePath, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift index fa237b7a4..9b56ded5c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift @@ -83,6 +83,43 @@ final class OSLogCrashHandlerTests: XCTestCase { XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) } + func testPersistsMarkedTestCrashWithoutOneSignalModule() throws { + let handler = makeCrashHandler() + + handler.handle( + exception: NSException( + name: NSExceptionName("RuntimeException"), + reason: nil, + userInfo: [OSCrashTestMarker.userInfoKey: true] + ), + stackSymbols: ["0 ExampleApp 0x000000 SecondaryView.triggerCrash + 1"], + resolvedFrames: [frame("/private/var/containers/Bundle/Application/App/ExampleApp")] + ) + + XCTAssertEqual( + try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) + .filter { $0.hasSuffix(".otlp") } + .count, + 1 + ) + } + + func testIgnoresUnmarkedHostCrashSharingTheMarkerKey() throws { + let handler = makeCrashHandler() + + handler.handle( + exception: NSException( + name: NSExceptionName("RuntimeException"), + reason: nil, + userInfo: [OSCrashTestMarker.userInfoKey: false] + ), + stackSymbols: ["0 ExampleApp 0x000000 SecondaryView.triggerCrash + 1"], + resolvedFrames: [frame("/private/var/containers/Bundle/Application/App/ExampleApp")] + ) + + XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) + } + private func frame(_ imagePath: String, symbol: String? = nil) -> OSResolvedStackFrame { OSResolvedStackFrame(imagePath: imagePath, symbolName: symbol) } From 19d935ea47eea44db8e29de71fc7bca23a37eb91 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 10:03:56 -0700 Subject: [PATCH 4/8] refactor(logging): raise demo crash from OSCore Keep production crash attribution unchanged by moving the synthetic crash origin into an SPI helper instead of bypassing the host-crash filter. Co-authored-by: Cursor --- examples/demo/App/Views/SecondaryView.swift | 14 +++---- .../Source/Logging/OSLogCrashHandler.swift | 24 ++++++------ .../OSLogCrashHandlerTests.swift | 37 ------------------- 3 files changed, 18 insertions(+), 57 deletions(-) diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift index dc18d9770..ddbb389c9 100644 --- a/examples/demo/App/Views/SecondaryView.swift +++ b/examples/demo/App/Views/SecondaryView.swift @@ -26,7 +26,7 @@ */ import Foundation -import OneSignalOSCore +@_spi(OneSignalTesting) import OneSignalOSCore import SwiftUI /// Isolated screen for the test-crash action, matching Android's SecondaryActivity. @@ -61,17 +61,15 @@ struct SecondaryView: View { } /// NSException reaches the SDK uncaught-exception handler. Swift `fatalError` is a POSIX - /// signal, which that handler does not intercept. The marker is required because this - /// exception is raised from app code, so no OneSignal frame appears on the stack. + /// signal, which that handler does not intercept. private func triggerCrash() { let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "MMM dd, yyyy HH:mm:ss" let timestamp = formatter.string(from: Date()) - NSException( - name: NSExceptionName("RuntimeException"), - reason: "Test crash from OneSignal Demo App - \(timestamp)", - userInfo: [OSCrashTestMarker.userInfoKey: true] - ).raise() + OSCrashTest.raiseException( + reason: "Test crash from OneSignal Demo App - \(timestamp)" + ) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift index 91b477349..03cd00371 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift @@ -41,13 +41,17 @@ struct OSResolvedStackFrame: Equatable { let symbolName: String? } -/// Opts an exception into crash reporting that stack attribution would otherwise reject. -/// -/// Demo and integration apps raise their test crash from their own code, so no OneSignal -/// frame appears on the stack. Setting this key to `true` in `NSException.userInfo` is the -/// supported way to bypass attribution for those synthetic crashes. -public enum OSCrashTestMarker { - public static let userInfoKey = "com.onesignal.crash.test" +@_spi(OneSignalTesting) +public enum OSCrashTest { + /// Raises from OneSignalOSCore so the production attribution path recognizes the crash. + @inline(never) + public static func raiseException(reason: String) { + NSException( + name: NSExceptionName("RuntimeException"), + reason: reason, + userInfo: nil + ).raise() + } } /// Prints KMP crash-reporter diagnostics with `NSLog`. Honors the console log level @@ -169,7 +173,7 @@ final class OSLogCrashHandler: ILogCrashHandler { stackSymbols: [String], resolvedFrames: [OSResolvedStackFrame] ) { - guard Self.isMarkedTestCrash(exception) || Self.isOneSignalAtFault(resolvedFrames) else { + guard Self.isOneSignalAtFault(resolvedFrames) else { previousExceptionHandler?(exception) return } @@ -237,10 +241,6 @@ final class OSLogCrashHandler: ILogCrashHandler { } } - static func isMarkedTestCrash(_ exception: NSException) -> Bool { - exception.userInfo?[OSCrashTestMarker.userInfoKey] as? Bool == true - } - static func isOneSignalAtFault(_ frames: [OSResolvedStackFrame]) -> Bool { frames.contains { frame in guard let imagePath = frame.imagePath, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift index 9b56ded5c..fa237b7a4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift @@ -83,43 +83,6 @@ final class OSLogCrashHandlerTests: XCTestCase { XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) } - func testPersistsMarkedTestCrashWithoutOneSignalModule() throws { - let handler = makeCrashHandler() - - handler.handle( - exception: NSException( - name: NSExceptionName("RuntimeException"), - reason: nil, - userInfo: [OSCrashTestMarker.userInfoKey: true] - ), - stackSymbols: ["0 ExampleApp 0x000000 SecondaryView.triggerCrash + 1"], - resolvedFrames: [frame("/private/var/containers/Bundle/Application/App/ExampleApp")] - ) - - XCTAssertEqual( - try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) - .filter { $0.hasSuffix(".otlp") } - .count, - 1 - ) - } - - func testIgnoresUnmarkedHostCrashSharingTheMarkerKey() throws { - let handler = makeCrashHandler() - - handler.handle( - exception: NSException( - name: NSExceptionName("RuntimeException"), - reason: nil, - userInfo: [OSCrashTestMarker.userInfoKey: false] - ), - stackSymbols: ["0 ExampleApp 0x000000 SecondaryView.triggerCrash + 1"], - resolvedFrames: [frame("/private/var/containers/Bundle/Application/App/ExampleApp")] - ) - - XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) - } - private func frame(_ imagePath: String, symbol: String? = nil) -> OSResolvedStackFrame { OSResolvedStackFrame(imagePath: imagePath, symbolName: symbol) } From 819ccecfead4e92b64bc8f7441e40255d0247467 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 10:05:11 -0700 Subject: [PATCH 5/8] refactor(demo): expose crash helper from facade Keep the demo on the standard framework boundary while retaining an SPI-only SDK-originated crash path. Co-authored-by: Cursor --- examples/demo/App/Views/SecondaryView.swift | 2 +- .../Source/Logging/OSLogCrashHandler.swift | 13 ------------- .../Source/OneSignalSwiftInterface.swift | 13 +++++++++++++ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift index ddbb389c9..186c37559 100644 --- a/examples/demo/App/Views/SecondaryView.swift +++ b/examples/demo/App/Views/SecondaryView.swift @@ -26,7 +26,7 @@ */ import Foundation -@_spi(OneSignalTesting) import OneSignalOSCore +@_spi(OneSignalTesting) import OneSignalFramework import SwiftUI /// Isolated screen for the test-crash action, matching Android's SecondaryActivity. diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift index 03cd00371..b3e00c70a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift @@ -41,19 +41,6 @@ struct OSResolvedStackFrame: Equatable { let symbolName: String? } -@_spi(OneSignalTesting) -public enum OSCrashTest { - /// Raises from OneSignalOSCore so the production attribution path recognizes the crash. - @inline(never) - public static func raiseException(reason: String) { - NSException( - name: NSExceptionName("RuntimeException"), - reason: reason, - userInfo: nil - ).raise() - } -} - /// Prints KMP crash-reporter diagnostics with `NSLog`. Honors the console log level /// without going through `OneSignalLog` (listeners, alert UI, remote sink). final class OSCrashLogger: ILogger { diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift b/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift index bf399005c..beba69fc9 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift @@ -32,6 +32,19 @@ import OneSignalOutcomes import OneSignalNotifications import OneSignalCore +@_spi(OneSignalTesting) +public enum OSCrashTest { + /// Raises from OneSignalFramework so the production attribution path recognizes the crash. + @inline(never) + public static func raiseException(reason: String) { + NSException( + name: NSExceptionName("RuntimeException"), + reason: reason, + userInfo: nil + ).raise() + } +} + public extension OneSignal { static var User: OSUser { return __user() From 6728ad4861905eec0aeee0857ed9338bcdfb5af7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 12:13:59 -0700 Subject: [PATCH 6/8] test(logging): add OSCrashTest attribution tests --- .../OneSignal.xcodeproj/project.pbxproj | 8 +++ .../OSCrashTestAttributionTests.swift | 40 ++++++++++++ .../UnitTests/OSCrashTestTests.swift | 62 +++++++++++++++++++ .../UnitTests/TestHelperFunctions.h | 3 + .../UnitTests/TestHelperFunctions.m | 9 +++ .../UnitTests/UnitTests-Bridging-Header.h | 1 + 6 files changed, 123 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift create mode 100644 iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index ab6fc07bd..8915566b5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -76,10 +76,12 @@ 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */; }; 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */; }; C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */; }; + A0F0E53FDE87E8ABDC1DE05D /* OSCrashTestAttributionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F0E53FDE87E8ABDC1DE05E /* OSCrashTestAttributionTests.swift */; }; B96A3B6BA8CC49EE4796D9BF /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; 25898119922BDCDA7AF0B9CC /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; 9EAF92032D0429FA35E04417 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */; }; + A0F0E53FDE87E8ABDC1DE05B /* OSCrashTestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F0E53FDE87E8ABDC1DE05C /* OSCrashTestTests.swift */; }; 7732574D325D34CC7C498199 /* OSRemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */; }; 698F58A488FCE503DFD5247F /* OSLogCrashHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */; }; 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; }; @@ -1799,9 +1801,11 @@ 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinByteArray+Data.swift"; sourceTree = ""; }; 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = ""; }; 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogCrashHandlerTests.swift; sourceTree = ""; }; + A0F0E53FDE87E8ABDC1DE05E /* OSCrashTestAttributionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCrashTestAttributionTests.swift; sourceTree = ""; }; 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingController.swift; sourceTree = ""; }; C0462F96E1AADF655F3B3765 /* OSRemoteLoggingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSRemoteLoggingController.h; sourceTree = ""; }; 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingControllerTests.swift; sourceTree = ""; }; + A0F0E53FDE87E8ABDC1DE05C /* OSCrashTestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCrashTestTests.swift; sourceTree = ""; }; 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLogger.swift; sourceTree = ""; }; ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogCrashHandler.swift; sourceTree = ""; }; DEF5CCF12539321A0003E9CC /* UnitTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnitTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2575,6 +2579,7 @@ 3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */, 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */, 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */, + A0F0E53FDE87E8ABDC1DE05E /* OSCrashTestAttributionTests.swift */, 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */, 3C23A21E2FCE0AA1001D32E3 /* OSResilientStorageTests.swift */, 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */, @@ -2684,6 +2689,7 @@ 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */, + A0F0E53FDE87E8ABDC1DE05C /* OSCrashTestTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -4612,6 +4618,7 @@ 3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */, 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */, C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */, + A0F0E53FDE87E8ABDC1DE05D /* OSCrashTestAttributionTests.swift in Sources */, 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4653,6 +4660,7 @@ 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */, + A0F0E53FDE87E8ABDC1DE05B /* OSCrashTestTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift new file mode 100644 index 000000000..c343ff609 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift @@ -0,0 +1,40 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +@testable import OneSignalOSCore +import XCTest + +final class OSCrashTestAttributionTests: XCTestCase { + func testRecognizesStaticallyLinkedCrashHelper() { + let frame = OSResolvedStackFrame( + imagePath: "/private/var/containers/Bundle/Application/App/ExampleApp", + symbolName: "_$s18OneSignalFramework11OSCrashTestO14raiseException6reasonySS_tFZ" + ) + + XCTAssertTrue(OSLogCrashHandler.isOneSignalAtFault([frame])) + } +} diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift new file mode 100644 index 000000000..be253d7ae --- /dev/null +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift @@ -0,0 +1,62 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Darwin +import Foundation +@_spi(OneSignalTesting) import OneSignalFramework +import XCTest + +final class OSCrashTestTests: XCTestCase { + func testRaiseExceptionCapturesCrashHelperFrame() { + let reason = "OneSignal crash attribution test" + + guard let exception = OSCatchException({ + OSCrashTest.raiseException(reason: reason) + }) else { + return XCTFail("Expected OSCrashTest to raise an NSException") + } + + XCTAssertEqual(exception.name.rawValue, "RuntimeException") + XCTAssertEqual(exception.reason, reason) + XCTAssertTrue( + exception.callStackReturnAddresses.contains(where: isCrashHelperAddress), + "Expected OSCrashTest.raiseException to remain on the raised exception stack" + ) + } + + private func isCrashHelperAddress(_ address: NSNumber) -> Bool { + guard let pointer = UnsafeRawPointer(bitPattern: address.uintValue) else { + return false + } + var info = Dl_info() + guard dladdr(pointer, &info) != 0, + let symbol = info.dli_sname else { + return false + } + return String(cString: symbol).contains("OSCrashTestO14raiseException") + } +} diff --git a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h index 2a7288b56..9cb3e1b21 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h @@ -25,7 +25,10 @@ * THE SOFTWARE. */ +#import + void DumpObjcMethods(Class clz); BOOL injectStaticSelector(Class newClass, SEL newSel, Class addToClass, SEL makeLikeSel); void swizzleClassMethodWithCategoryImplementation(Class class, SEL original, SEL new); void swizzleExistingSelector(Class targetClass, SEL targetSelector, Class myClass, SEL mySelector); +NSException * _Nullable OSCatchException(void (^block)(void)); diff --git a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m index 6342d1885..aeeead2ca 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m @@ -28,6 +28,15 @@ #import #import +NSException *OSCatchException(void (^block)(void)) { + @try { + block(); + } @catch (NSException *exception) { + return exception; + } + return nil; +} + // Just for debugging void DumpObjcMethods(Class clz) { unsigned int methodCount = 0; diff --git a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h index 05458d465..b63d928ca 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h @@ -4,3 +4,4 @@ #import "OneSignalFramework.h" #import "OSOutcomesRequests.h" +#import "TestHelperFunctions.h" From 150d89386be2d62f8dc1258e840aa91a7ecf5bbe Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 16:30:52 -0700 Subject: [PATCH 7/8] refactor(demo): replace OSCrashTest with local ObjC crash helper --- examples/demo/App.xcodeproj/project.pbxproj | 36 ++++++++--- examples/demo/App/App-Bridging-Header.h | 1 + .../demo/App/Support/OneSignalDemoCrash.h | 38 ++++++++++++ .../demo/App/Support/OneSignalDemoCrash.m | 36 +++++++++++ examples/demo/App/Views/SecondaryView.swift | 5 +- examples/demo/project.yml | 1 + .../OneSignal.xcodeproj/project.pbxproj | 8 --- .../OSCrashTestAttributionTests.swift | 40 ------------ .../Source/OneSignalSwiftInterface.swift | 13 ---- .../UnitTests/OSCrashTestTests.swift | 62 ------------------- .../UnitTests/TestHelperFunctions.h | 3 - .../UnitTests/TestHelperFunctions.m | 9 --- .../UnitTests/UnitTests-Bridging-Header.h | 1 - 13 files changed, 105 insertions(+), 148 deletions(-) create mode 100644 examples/demo/App/App-Bridging-Header.h create mode 100644 examples/demo/App/Support/OneSignalDemoCrash.h create mode 100644 examples/demo/App/Support/OneSignalDemoCrash.m delete mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift delete mode 100644 iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj index ab0de05ff..7de7b5a01 100644 --- a/examples/demo/App.xcodeproj/project.pbxproj +++ b/examples/demo/App.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 8068AFC608E7E82F06733BE7 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; }; 80E3E2B5438CFEBE1316FA84 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; }; 837FCE7A095ED1D7CCFEACF3 /* InAppSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76FFAF18177241F4A47FE23 /* InAppSection.swift */; }; + 8A8610C5E55F9CE47175F59C /* OneSignalDemoCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C9A76C77377EFB4D02AB2C2 /* OneSignalDemoCrash.m */; }; 8E57965CF0E9F1C341E61996 /* OneSignal-Dynamic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8EA2FF24D93691FDC1661913 /* CustomEventsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */; }; 902A116B26B8ECAD8EE29C95 /* ToggleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911376C90AA43F41478596FE /* ToggleRow.swift */; }; @@ -313,6 +314,7 @@ 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailsSection.swift; sourceTree = ""; }; 1EE449E8308FCB038408D7CF /* vine_boom.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = vine_boom.wav; sourceTree = ""; }; 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasesSection.swift; sourceTree = ""; }; + 24CBCBDE9F48219D7EC827C3 /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = ""; }; 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySection.swift; sourceTree = ""; }; 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgets.swift; sourceTree = ""; }; 291D83300C20BA3831824AFD /* OneSignalSDK */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OneSignalSDK; path = ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj; sourceTree = ""; }; @@ -325,6 +327,7 @@ 3FD8258E807E6672642A32E6 /* UserFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchService.swift; sourceTree = ""; }; 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPairInputDialog.swift; sourceTree = ""; }; 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomesSection.swift; sourceTree = ""; }; + 4122BFB1C3A3E6CA17B0C760 /* OneSignalDemoCrash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalDemoCrash.h; sourceTree = ""; }; 432444EA41C495988DFAB422 /* TrackEventDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackEventDialog.swift; sourceTree = ""; }; 497484E7C094D645338BD404 /* SectionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionCard.swift; sourceTree = ""; }; 49D88A349FD70A9153DD2C03 /* App.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -338,6 +341,7 @@ 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7EA9D80191548D49F09D30B3 /* AppModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModels.swift; sourceTree = ""; }; 7EB548D4688766660864F581 /* AppSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSection.swift; sourceTree = ""; }; + 8C9A76C77377EFB4D02AB2C2 /* OneSignalDemoCrash.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalDemoCrash.m; sourceTree = ""; }; 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastPresenter.swift; sourceTree = ""; }; 911376C90AA43F41478596FE /* ToggleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRow.swift; sourceTree = ""; }; 939E1F476EE48B9833A3311C /* TooltipService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipService.swift; sourceTree = ""; }; @@ -360,7 +364,7 @@ E194A3F19072CB17A8F1A12E /* SmsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsSection.swift; sourceTree = ""; }; ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = ""; }; F46DFACB9F304B9374F3C570 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; - "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; + "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -427,8 +431,10 @@ children = ( 4B8DD72AD7356663EB6BAE3A /* Models */, 72F51C4EC858B862B629616C /* Services */, + FDAA070CC64E2A6C66CA3FCC /* Support */, 19317E8C50FA7D56330C6BDF /* ViewModels */, F0D08B397106EA8498C4A8F4 /* Views */, + 24CBCBDE9F48219D7EC827C3 /* App-Bridging-Header.h */, BDB04A33912347325A0155D6 /* App.swift */, 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */, 6B00BC406653BC6B08ECCE26 /* Secrets.plist */, @@ -567,10 +573,19 @@ path = Views; sourceTree = ""; }; - "TEMP_D8085C8F-3CA5-457D-9FB3-7C8E9DA8B66A" /* demo */ = { + FDAA070CC64E2A6C66CA3FCC /* Support */ = { isa = PBXGroup; children = ( - "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */, + 4122BFB1C3A3E6CA17B0C760 /* OneSignalDemoCrash.h */, + 8C9A76C77377EFB4D02AB2C2 /* OneSignalDemoCrash.m */, + ); + path = Support; + sourceTree = ""; + }; + "TEMP_50803CDB-250B-41C8-93E2-9DF8EB2C4802" /* demo */ = { + isa = PBXGroup; + children = ( + "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */, ); path = demo; sourceTree = ""; @@ -816,6 +831,7 @@ E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */, 4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */, 674995A7A55C13341317E19B /* OSDialog.swift in Sources */, + 8A8610C5E55F9CE47175F59C /* OneSignalDemoCrash.m in Sources */, 12597AC14E1783CC87D6E147 /* OneSignalService.swift in Sources */, 5737CABFA55E019B2732B90D /* OneSignalViewModel.swift in Sources */, 39D2C94F79A62BFF9DE5DBA9 /* OutcomeDialog.swift in Sources */, @@ -940,7 +956,7 @@ /* Begin XCBuildConfiguration section */ 0D2EF3911CA89837C30DB0D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -958,7 +974,7 @@ }; 4A0C935808978B5A7673E412 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -971,6 +987,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1038,7 +1055,7 @@ }; D0E56A85F1C385808720F94B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1060,7 +1077,7 @@ }; EB1CC3A930E09FEBECF9195D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1077,13 +1094,14 @@ PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Appium Demo - Main"; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; F305A3E63851EE49DA2D190E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -1106,7 +1124,7 @@ }; F5FD25168D9B32A08A468069 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; DEVELOPMENT_TEAM = 99SW8E36CT; diff --git a/examples/demo/App/App-Bridging-Header.h b/examples/demo/App/App-Bridging-Header.h new file mode 100644 index 000000000..4a4e818eb --- /dev/null +++ b/examples/demo/App/App-Bridging-Header.h @@ -0,0 +1 @@ +#import "Support/OneSignalDemoCrash.h" diff --git a/examples/demo/App/Support/OneSignalDemoCrash.h b/examples/demo/App/Support/OneSignalDemoCrash.h new file mode 100644 index 000000000..6fcbe3978 --- /dev/null +++ b/examples/demo/App/Support/OneSignalDemoCrash.h @@ -0,0 +1,38 @@ +/** + * Modified MIT License + * + * Copyright 2026 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OneSignalDemoCrash : NSObject + ++ (void)raiseExceptionWithReason:(NSString *)reason __attribute__((noinline)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/examples/demo/App/Support/OneSignalDemoCrash.m b/examples/demo/App/Support/OneSignalDemoCrash.m new file mode 100644 index 000000000..04064c1c3 --- /dev/null +++ b/examples/demo/App/Support/OneSignalDemoCrash.m @@ -0,0 +1,36 @@ +/** + * Modified MIT License + * + * Copyright 2026 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import "OneSignalDemoCrash.h" + +@implementation OneSignalDemoCrash + ++ (void)raiseExceptionWithReason:(NSString *)reason { + [NSException raise:@"RuntimeException" format:@"%@", reason]; +} + +@end diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift index 186c37559..d33872923 100644 --- a/examples/demo/App/Views/SecondaryView.swift +++ b/examples/demo/App/Views/SecondaryView.swift @@ -26,7 +26,6 @@ */ import Foundation -@_spi(OneSignalTesting) import OneSignalFramework import SwiftUI /// Isolated screen for the test-crash action, matching Android's SecondaryActivity. @@ -67,8 +66,8 @@ struct SecondaryView: View { formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "MMM dd, yyyy HH:mm:ss" let timestamp = formatter.string(from: Date()) - OSCrashTest.raiseException( - reason: "Test crash from OneSignal Demo App - \(timestamp)" + OneSignalDemoCrash.raiseException( + withReason: "Test crash from OneSignal Demo App - \(timestamp)" ) } } diff --git a/examples/demo/project.yml b/examples/demo/project.yml index fe654b541..16736ca48 100644 --- a/examples/demo/project.yml +++ b/examples/demo/project.yml @@ -59,6 +59,7 @@ targets: TARGETED_DEVICE_FAMILY: "1,2" LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks" ENABLE_USER_SCRIPT_SANDBOXING: NO + SWIFT_OBJC_BRIDGING_HEADER: App/App-Bridging-Header.h configs: Debug: DEVELOPMENT_TEAM: 99SW8E36CT diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 8915566b5..ab6fc07bd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -76,12 +76,10 @@ 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */; }; 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */; }; C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */; }; - A0F0E53FDE87E8ABDC1DE05D /* OSCrashTestAttributionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F0E53FDE87E8ABDC1DE05E /* OSCrashTestAttributionTests.swift */; }; B96A3B6BA8CC49EE4796D9BF /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; 25898119922BDCDA7AF0B9CC /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; 9EAF92032D0429FA35E04417 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */; }; - A0F0E53FDE87E8ABDC1DE05B /* OSCrashTestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F0E53FDE87E8ABDC1DE05C /* OSCrashTestTests.swift */; }; 7732574D325D34CC7C498199 /* OSRemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */; }; 698F58A488FCE503DFD5247F /* OSLogCrashHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */; }; 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; }; @@ -1801,11 +1799,9 @@ 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinByteArray+Data.swift"; sourceTree = ""; }; 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = ""; }; 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogCrashHandlerTests.swift; sourceTree = ""; }; - A0F0E53FDE87E8ABDC1DE05E /* OSCrashTestAttributionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCrashTestAttributionTests.swift; sourceTree = ""; }; 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingController.swift; sourceTree = ""; }; C0462F96E1AADF655F3B3765 /* OSRemoteLoggingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSRemoteLoggingController.h; sourceTree = ""; }; 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingControllerTests.swift; sourceTree = ""; }; - A0F0E53FDE87E8ABDC1DE05C /* OSCrashTestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCrashTestTests.swift; sourceTree = ""; }; 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLogger.swift; sourceTree = ""; }; ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogCrashHandler.swift; sourceTree = ""; }; DEF5CCF12539321A0003E9CC /* UnitTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnitTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2579,7 +2575,6 @@ 3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */, 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */, 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */, - A0F0E53FDE87E8ABDC1DE05E /* OSCrashTestAttributionTests.swift */, 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */, 3C23A21E2FCE0AA1001D32E3 /* OSResilientStorageTests.swift */, 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */, @@ -2689,7 +2684,6 @@ 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */, - A0F0E53FDE87E8ABDC1DE05C /* OSCrashTestTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -4618,7 +4612,6 @@ 3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */, 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */, C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */, - A0F0E53FDE87E8ABDC1DE05D /* OSCrashTestAttributionTests.swift in Sources */, 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4660,7 +4653,6 @@ 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */, - A0F0E53FDE87E8ABDC1DE05B /* OSCrashTestTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift deleted file mode 100644 index c343ff609..000000000 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSCrashTestAttributionTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -/* - Modified MIT License - - Copyright 2026 OneSignal - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - 1. The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - 2. All copies of substantial portions of the Software may only be used in connection - with services provided by OneSignal. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ - -@testable import OneSignalOSCore -import XCTest - -final class OSCrashTestAttributionTests: XCTestCase { - func testRecognizesStaticallyLinkedCrashHelper() { - let frame = OSResolvedStackFrame( - imagePath: "/private/var/containers/Bundle/Application/App/ExampleApp", - symbolName: "_$s18OneSignalFramework11OSCrashTestO14raiseException6reasonySS_tFZ" - ) - - XCTAssertTrue(OSLogCrashHandler.isOneSignalAtFault([frame])) - } -} diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift b/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift index beba69fc9..bf399005c 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift @@ -32,19 +32,6 @@ import OneSignalOutcomes import OneSignalNotifications import OneSignalCore -@_spi(OneSignalTesting) -public enum OSCrashTest { - /// Raises from OneSignalFramework so the production attribution path recognizes the crash. - @inline(never) - public static func raiseException(reason: String) { - NSException( - name: NSExceptionName("RuntimeException"), - reason: reason, - userInfo: nil - ).raise() - } -} - public extension OneSignal { static var User: OSUser { return __user() diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift deleted file mode 100644 index be253d7ae..000000000 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSCrashTestTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* - Modified MIT License - - Copyright 2026 OneSignal - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - 1. The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - 2. All copies of substantial portions of the Software may only be used in connection - with services provided by OneSignal. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ - -import Darwin -import Foundation -@_spi(OneSignalTesting) import OneSignalFramework -import XCTest - -final class OSCrashTestTests: XCTestCase { - func testRaiseExceptionCapturesCrashHelperFrame() { - let reason = "OneSignal crash attribution test" - - guard let exception = OSCatchException({ - OSCrashTest.raiseException(reason: reason) - }) else { - return XCTFail("Expected OSCrashTest to raise an NSException") - } - - XCTAssertEqual(exception.name.rawValue, "RuntimeException") - XCTAssertEqual(exception.reason, reason) - XCTAssertTrue( - exception.callStackReturnAddresses.contains(where: isCrashHelperAddress), - "Expected OSCrashTest.raiseException to remain on the raised exception stack" - ) - } - - private func isCrashHelperAddress(_ address: NSNumber) -> Bool { - guard let pointer = UnsafeRawPointer(bitPattern: address.uintValue) else { - return false - } - var info = Dl_info() - guard dladdr(pointer, &info) != 0, - let symbol = info.dli_sname else { - return false - } - return String(cString: symbol).contains("OSCrashTestO14raiseException") - } -} diff --git a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h index 9cb3e1b21..2a7288b56 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.h @@ -25,10 +25,7 @@ * THE SOFTWARE. */ -#import - void DumpObjcMethods(Class clz); BOOL injectStaticSelector(Class newClass, SEL newSel, Class addToClass, SEL makeLikeSel); void swizzleClassMethodWithCategoryImplementation(Class class, SEL original, SEL new); void swizzleExistingSelector(Class targetClass, SEL targetSelector, Class myClass, SEL mySelector); -NSException * _Nullable OSCatchException(void (^block)(void)); diff --git a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m index aeeead2ca..6342d1885 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/TestHelperFunctions.m @@ -28,15 +28,6 @@ #import #import -NSException *OSCatchException(void (^block)(void)) { - @try { - block(); - } @catch (NSException *exception) { - return exception; - } - return nil; -} - // Just for debugging void DumpObjcMethods(Class clz) { unsigned int methodCount = 0; diff --git a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h index b63d928ca..05458d465 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h @@ -4,4 +4,3 @@ #import "OneSignalFramework.h" #import "OSOutcomesRequests.h" -#import "TestHelperFunctions.h" From f1b53e27937a40191c2b828f19284197aae01f4d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 16:53:27 -0700 Subject: [PATCH 8/8] docs(demo): explain crash handler frame attribution --- examples/demo/App/Support/OneSignalDemoCrash.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/demo/App/Support/OneSignalDemoCrash.h b/examples/demo/App/Support/OneSignalDemoCrash.h index 6fcbe3978..990bed22b 100644 --- a/examples/demo/App/Support/OneSignalDemoCrash.h +++ b/examples/demo/App/Support/OneSignalDemoCrash.h @@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN @interface OneSignalDemoCrash : NSObject +/// The OneSignal-prefixed class and non-inlined method preserve a frame recognized +/// by the crash handler's static-symbol attribution. + (void)raiseExceptionWithReason:(NSString *)reason __attribute__((noinline)); @end