-
Notifications
You must be signed in to change notification settings - Fork 265
feat: [SDK-5022] add demo test crash flow #1715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fadi/sdk-4998
Are you sure you want to change the base?
Changes from all commits
17ba0ae
46458bf
7f205d3
a0f0e53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /** | ||
| * 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 OneSignalOSCore | ||
| import SwiftUI | ||
|
|
||
| /// Isolated screen for the test-crash action, matching Android's SecondaryActivity. | ||
| struct SecondaryView: View { | ||
| var body: some View { | ||
| VStack(spacing: 32) { | ||
| Spacer() | ||
|
|
||
| Text("Secondary Screen") | ||
| .font(.system(size: 28)) | ||
| .foregroundColor(OS.Color.bodyText) | ||
| .accessibilityIdentifier("secondary_screen_title") | ||
|
|
||
| ActionButton( | ||
| "CRASH", | ||
| style: .outline, | ||
| accessibilityID: "crash_button" | ||
| ) { | ||
| triggerCrash() | ||
| } | ||
|
|
||
| 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. 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" | ||
| let timestamp = formatter.string(from: Date()) | ||
| NSException( | ||
| name: NSExceptionName("RuntimeException"), | ||
| reason: "Test crash from OneSignal Demo App - \(timestamp)", | ||
| userInfo: [OSCrashTestMarker.userInfoKey: true] | ||
| ).raise() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Act on / judo — host-raised crash forces the bypass (C). Raising from app code guarantees no OneSignal frame, which is why the marker exists. An OSCore helper that raises would put Also: |
||
| } | ||
| } | ||
|
|
||
| #Preview { | ||
| NavigationStack { | ||
| SecondaryView() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,15 @@ private func osLogUncaughtExceptionHandler(_ exception: NSException) { | |
| OSLogCrashHandler.handleActive(exception) | ||
| } | ||
|
|
||
| /// Opts an exception into crash reporting that module attribution would otherwise reject. | ||
| /// | ||
| /// Demo and integration apps raise their test crash from their own code, so no OneSignal | ||
| /// frame appears on the stack and `isOneSignalAtFault` correctly returns false. Setting | ||
| /// this key to `true` in `NSException.userInfo` is the only supported way to bypass that. | ||
| public enum OSCrashTestMarker { | ||
| public static let userInfoKey = "com.onesignal.crash.test" | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Act on — undeclared public API (A/B/C/D).
Commit Prefer an OSCore-owned raise helper (throwing frame = |
||
|
|
||
| final class OSCrashLogger: ILogger { | ||
| func error(message: String) { | ||
| NSLog("[OneSignal crash] ERROR: %@", message) | ||
|
|
@@ -131,7 +140,7 @@ final class OSLogCrashHandler: ILogCrashHandler { | |
| } | ||
|
|
||
| func handle(exception: NSException, stackSymbols: [String]) { | ||
| guard Self.isOneSignalAtFault(stackSymbols) else { | ||
| guard Self.isMarkedTestCrash(exception) || Self.isOneSignalAtFault(stackSymbols) else { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Act on — demo concern on the shared fatal path (A/C/D). This Code judo: |
||
| previousExceptionHandler?(exception) | ||
| return | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Act on — marker is gate-only; uploaded records are unlabeled (A/C/D). After the guard admits a marked crash, the marker is discarded. The name-based predecessor self-labeled via |
||
|
|
@@ -199,6 +208,10 @@ final class OSLogCrashHandler: ILogCrashHandler { | |
| } | ||
| } | ||
|
|
||
| static func isMarkedTestCrash(_ exception: NSException) -> Bool { | ||
| exception.userInfo?[OSCrashTestMarker.userInfoKey] as? Bool == true | ||
| } | ||
|
|
||
| static func isOneSignalAtFault(_ stackSymbols: [String]) -> Bool { | ||
| for frame in stackSymbols { | ||
| guard let module = moduleName(from: frame) else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,41 @@ 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"] | ||
| ) | ||
|
|
||
| XCTAssertEqual( | ||
| try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) | ||
| .filter { $0.hasSuffix(".otlp") } | ||
| .count, | ||
| 1 | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider — thin verification vs claimed contract (B/D). Covers Swift |
||
| } | ||
|
|
||
| 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"] | ||
| ) | ||
|
|
||
| XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) | ||
| } | ||
|
|
||
| func testIgnoresOneSignalSubstringOutsideModuleField() { | ||
| XCTAssertFalse( | ||
| OSLogCrashHandler.isOneSignalAtFault( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider — only demo file importing
OneSignalOSCore(C/D).Other app sources use
OneSignalFramework/ feature modules. This import exists solely forOSCrashTestMarker.userInfoKeyand teaches sub-framework coupling.If the raise helper / private marker path lands, this import should disappear.