diff --git a/StripeCore/StripeCore/Source/Helpers/InstallMethod.swift b/StripeCore/StripeCore/Source/Helpers/InstallMethod.swift index 5660e0eebc7..6f22e490f8a 100644 --- a/StripeCore/StripeCore/Source/Helpers/InstallMethod.swift +++ b/StripeCore/StripeCore/Source/Helpers/InstallMethod.swift @@ -7,13 +7,13 @@ import Foundation -enum InstallMethod: String { +@_spi(STP) public enum InstallMethod: String { case cocoapods = "C" case spm = "S" case binary = "B" // Built via export_builds.sh case xcode = "X" // Directly built via Xcode or xcodebuild - static let current: InstallMethod = { + @_spi(STP) public static let current: InstallMethod = { #if COCOAPODS return .cocoapods #elseif SWIFT_PACKAGE diff --git a/StripeCore/StripeCore/Source/Helpers/STPDeviceUtils.swift b/StripeCore/StripeCore/Source/Helpers/STPDeviceUtils.swift index 5bd87007b72..041bf645f89 100644 --- a/StripeCore/StripeCore/Source/Helpers/STPDeviceUtils.swift +++ b/StripeCore/StripeCore/Source/Helpers/STPDeviceUtils.swift @@ -8,8 +8,8 @@ import Foundation -struct STPDeviceUtils { - static var deviceType: String? { +@_spi(STP) public struct STPDeviceUtils { + @_spi(STP) public static var deviceType: String? { var systemInfo: utsname = utsname() uname(&systemInfo) let machineMirror = Mirror(reflecting: systemInfo.machine) diff --git a/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj b/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj index d4d00ff12a8..8905a7ea7d9 100644 --- a/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj +++ b/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj @@ -60,8 +60,15 @@ 492039952CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */; }; 492651662C24C9E7001DDBCA /* TestModeAutofillBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492651652C24C9E7001DDBCA /* TestModeAutofillBannerView.swift */; }; 492651682C25C0C2001DDBCA /* info@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 492651672C25C0C2001DDBCA /* info@3x.png */; }; + 494D32162CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */; }; + 494D32182CC819F000C5EFAF /* SentryContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32172CC819F000C5EFAF /* SentryContext.swift */; }; + 494D321A2CC8237F00C5EFAF /* SentryPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32192CC8237F00C5EFAF /* SentryPayload.swift */; }; + 494D321C2CC8264D00C5EFAF /* SentryStacktrace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */; }; + 494D322A2CC85E9000C5EFAF /* TraceSymbolsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */; }; + 494D322C2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */; }; 494D62072C45B9B700106519 /* link_logo@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 494D62062C45B9B700106519 /* link_logo@3x.png */; }; 495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */; }; + 4967A0EA2CC971AB002513AC /* SentryException.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4967A0E92CC971AB002513AC /* SentryException.swift */; }; 496A6AE72C29E0BB00D34F8E /* testmode@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 496A6AE62C29E0BB00D34F8E /* testmode@3x.png */; }; 497142BC2C514B08000DFA64 /* FlowRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */; }; 49A0B5862C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */; }; @@ -322,8 +329,15 @@ 492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsWebFlowTests.swift; sourceTree = ""; }; 492651652C24C9E7001DDBCA /* TestModeAutofillBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestModeAutofillBannerView.swift; sourceTree = ""; }; 492651672C25C0C2001DDBCA /* info@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info@3x.png"; sourceTree = ""; }; + 494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsSentryClient.swift; sourceTree = ""; }; + 494D32172CC819F000C5EFAF /* SentryContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryContext.swift; sourceTree = ""; }; + 494D32192CC8237F00C5EFAF /* SentryPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPayload.swift; sourceTree = ""; }; + 494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStacktrace.swift; sourceTree = ""; }; + 494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceSymbolsParser.swift; sourceTree = ""; }; + 494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceSymbolsParserTests.swift; sourceTree = ""; }; 494D62062C45B9B700106519 /* link_logo@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "link_logo@3x.png"; sourceTree = ""; }; 495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsTheme.swift; sourceTree = ""; }; + 4967A0E92CC971AB002513AC /* SentryException.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryException.swift; sourceTree = ""; }; 496A6AE62C29E0BB00D34F8E /* testmode@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testmode@3x.png"; sourceTree = ""; }; 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowRouterTests.swift; sourceTree = ""; }; 49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAPIClientTests.swift; sourceTree = ""; }; @@ -636,6 +650,7 @@ 328390D72E3911449BB9FD0B /* Analytics */ = { isa = PBXGroup; children = ( + 494D32132CC7FCCF00C5EFAF /* Sentry */, DBBF5CEE2C9030B2D374BC76 /* FinancialConnectionsAnalyticsClient.swift */, A6038978C79785C18257CD74 /* FinancialConnectionsSheetAnalytics.swift */, ); @@ -671,6 +686,19 @@ path = FinancialConnectionsSDK; sourceTree = ""; }; + 494D32132CC7FCCF00C5EFAF /* Sentry */ = { + isa = PBXGroup; + children = ( + 494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */, + 494D32172CC819F000C5EFAF /* SentryContext.swift */, + 494D32192CC8237F00C5EFAF /* SentryPayload.swift */, + 494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */, + 494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */, + 4967A0E92CC971AB002513AC /* SentryException.swift */, + ); + path = Sentry; + sourceTree = ""; + }; 49C911362C597EAF00589E0D /* LinkLogin */ = { isa = PBXGroup; children = ( @@ -1083,6 +1111,7 @@ CF731140836AE438C7F4F6AB /* StringExtensionsTests.swift */, 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */, 492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */, + 494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */, ); path = StripeFinancialConnectionsTests; sourceTree = ""; @@ -1230,6 +1259,8 @@ "zh-Hant", ); mainGroup = 7879CBA341D7E807714A831B; + packageReferences = ( + ); productRefGroup = 820BF9CF057CF92872BC3C15 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1299,6 +1330,7 @@ 6A3DA1F52C34A37F005C3F6E /* GenericInfoViewController.swift in Sources */, FBF513C7F73002FA30CC7C21 /* ConsumerSessionModels.swift in Sources */, 6A3739142C40558900D1F765 /* GenericInfoBodyView.swift in Sources */, + 494D321A2CC8237F00C5EFAF /* SentryPayload.swift in Sources */, EC74B719F0FA1A977EF4708C /* FinancialConnectionsAccount.swift in Sources */, 460C7685096AA6C693309647 /* FinancialConnectionsAuthSession.swift in Sources */, AB5AFAC3C70D6195075DE5AE /* FinancialConnectionsBulletPoint.swift in Sources */, @@ -1308,8 +1340,10 @@ F67624595BD2CD7B6793BFDA /* FinancialConnectionsImage.swift in Sources */, 07712610C7D2F484AAB96982 /* FinancialConnectionsInstitution.swift in Sources */, 7386E1F9256B23CE29BF996D /* FinancialConnectionsInstitutionSearchResultResource.swift in Sources */, + 494D321C2CC8264D00C5EFAF /* SentryStacktrace.swift in Sources */, C7D2763ACCE2CC71E788E18F /* FinancialConnectionsLegalDetailsNotice.swift in Sources */, B271AAF41C9FE6AE392B88D3 /* FinancialConnectionsMixedOAuthParams.swift in Sources */, + 4967A0EA2CC971AB002513AC /* SentryException.swift in Sources */, DAA51ABB496551074DBA1A20 /* FinancialConnectionsNetworkedAccountsResponse.swift in Sources */, 6A732CA62B69A46D00828CB1 /* PhoneTextField.swift in Sources */, 6FE9F171CF9A5D0EDB2035AA /* FinancialConnectionsNetworkingLinkSignup.swift in Sources */, @@ -1374,6 +1408,7 @@ 3446145FCA3278D51A9D4B80 /* AttachLinkedPaymentAccountDataSource.swift in Sources */, E3F62D2F9C344A1178030E8E /* AttachLinkedPaymentAccountViewController.swift in Sources */, 707C265C4179A8FEC98913FE /* ConsentBodyView.swift in Sources */, + 494D322A2CC85E9000C5EFAF /* TraceSymbolsParser.swift in Sources */, 465AE8A58AD2183E1E2042FE /* ConsentDataSource.swift in Sources */, 97C528CE821C6A55D58F68A4 /* ConsentFooterView.swift in Sources */, 8927328EE28A0C94B5AB69DB /* ConsentLogoView.swift in Sources */, @@ -1431,6 +1466,7 @@ 9AF6EC34D666BEB3C1397092 /* BulletPointLabelView.swift in Sources */, 313F5F7F2B0BE5D100BD98A9 /* Docs.docc in Sources */, F65E8D16DE691EB6C99C4521 /* Button+Extensions.swift in Sources */, + 494D32162CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift in Sources */, 33FA1684CE79F21271D14F23 /* HitTestStackView.swift in Sources */, 691619AE9A989548ABA36535 /* HitTestView.swift in Sources */, 91A3583A0BDE0F8F0C4AD3E2 /* InstitutionIconView.swift in Sources */, @@ -1440,6 +1476,7 @@ 49C911372C597EAF00589E0D /* LinkLoginDataSource.swift in Sources */, 6A7814182B361C5000168992 /* PaneLayoutView+Extensions.swift in Sources */, 99F41681B77ECB0090F34E31 /* SFSafariViewController+Extensions.swift in Sources */, + 494D32182CC819F000C5EFAF /* SentryContext.swift in Sources */, AA80602323C28AFAC391358D /* TimeInterval+Extensions.swift in Sources */, E637387728FA1597B1B51E5D /* UIImage+Extensions.swift in Sources */, 495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */, @@ -1471,6 +1508,7 @@ 492039952CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift in Sources */, 700B745FEF43088D9E34C0E4 /* AccountPickerHelpersTests.swift in Sources */, 6744CB1B182C5F7220B0B804 /* AuthFlowHelpersTests.swift in Sources */, + 494D322C2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift in Sources */, 39E5D4531961150E9CB3262F /* EmptyFinancialConnectionsAPIClient.swift in Sources */, CB734C25A19D38A87876FB2B /* FinancialConnectionsAnalyticsTest.swift in Sources */, 497142BC2C514B08000DFA64 /* FlowRouterTests.swift in Sources */, diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/FinancialConnectionsSentryClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/FinancialConnectionsSentryClient.swift new file mode 100644 index 00000000000..627fe768e71 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/FinancialConnectionsSentryClient.swift @@ -0,0 +1,28 @@ +// +// FinancialConnectionsSentryClient.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-22. +// + +import Foundation +@_spi(STP) import StripeCore + +protocol FinancialConnectionsErrorReporter { + func report(error: Error, parameters: [String: Any]) +} + +class FinancialConnectionsSentryClient: FinancialConnectionsErrorReporter { + private static let endpoint: URL = { + let projectId = "871" + var components = URLComponents() + components.scheme = "https" + components.host = "errors.stripe.com" + components.path = "/api/\(projectId)/envelope/" + return components.url! + }() + + func report(error: Error, parameters: [String: Any]) { + // TODO + } +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryContext.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryContext.swift new file mode 100644 index 00000000000..a60ec1a9696 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryContext.swift @@ -0,0 +1,56 @@ +// +// SentryContext.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-22. +// + +import Foundation +@_spi(STP) import StripeCore +import UIKit + +struct SentryContext: Encodable { + let app: SentryAppContext + let os: SentryOsContext + let device: SentryDeviceContext + + static let shared: SentryContext = { + let app = SentryAppContext( + appIdentifier: Bundle.stp_applicationBundleId() ?? "", + appName: Bundle.stp_applicationName() ?? "", + appVersion: Bundle.stp_applicationVersion() ?? "" + ) + let os = SentryOsContext( + name: "iOS", + version: UIDevice.current.systemVersion, + type: InstallMethod.current.rawValue + ) + let device = SentryDeviceContext( + modelId: UIDevice.current.identifierForVendor?.uuidString ?? "", + model: UIDevice.current.model, + manufacturer: "Apple", + type: STPDeviceUtils.deviceType ?? "" + ) + + return SentryContext(app: app, os: os, device: device) + }() +} + +struct SentryAppContext: Encodable { + let appIdentifier: String + let appName: String + let appVersion: String +} + +struct SentryOsContext: Encodable { + let name: String + let version: String + let type: String +} + +struct SentryDeviceContext: Encodable { + let modelId: String + let model: String + let manufacturer: String + let type: String +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryException.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryException.swift new file mode 100644 index 00000000000..537ac294b93 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryException.swift @@ -0,0 +1,19 @@ +// +// SentryException.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-23. +// + +import Foundation + +struct SentryException: Encodable { + var values: [SentryExceptionValue] = [] +} + +/// https://develop.sentry.dev/sdk/data-model/event-payloads/exception/ +struct SentryExceptionValue: Encodable { + let type: String + let value: String + let stacktrace: SentryStacktrace +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryPayload.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryPayload.swift new file mode 100644 index 00000000000..470cd597613 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryPayload.swift @@ -0,0 +1,17 @@ +// +// SentryPayload.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-22. +// + +import Foundation +@_spi(STP) import StripeCore + +struct SentryPayload: Encodable { + let eventId: String = UUID().uuidString.replacingOccurrences(of: "-", with: "") + let timestamp: TimeInterval = Date().timeIntervalSince1970 + let release: String = StripeAPIConfiguration.STPSDKVersion + let context: SentryContext = .shared + let exception: SentryException +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryStacktrace.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryStacktrace.swift new file mode 100644 index 00000000000..0d0825383c1 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/SentryStacktrace.swift @@ -0,0 +1,66 @@ +// +// SentryStacktrace.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-22. +// + +import Foundation + +/// https://develop.sentry.dev/sdk/data-model/event-payloads/stacktrace/ +struct SentryTrace: Encodable, Equatable { + let module: String? + let file: String? + let function: String + let lineno: Int? + + init(module: String? = nil, file: String? = nil, function: String, lineno: Int? = nil) { + self.module = module + self.file = file + self.function = function + self.lineno = lineno + } + + enum CodingKeys: CodingKey { + case module + case file + case function + case lineno + } + + // Custom encoder to only encode non-nil properties. + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.module, forKey: .module) + try container.encodeIfPresent(self.file, forKey: .file) + try container.encode(self.function, forKey: .function) + try container.encodeIfPresent(self.lineno, forKey: .lineno) + } +} + +struct SentryStacktrace: Encodable { + let frames: [SentryTrace] + + static func capture( + filePath: String = #file, + function: String = #function, + line: Int = #line, + callsiteDepth: Int = 1 + ) -> [SentryTrace] { + var traces: [SentryTrace] = [] + if let file = filePath.components(separatedBy: "/").last { + // Start with a Root trace, which includes the file, function, and lineno. + traces.append(SentryTrace( + file: file, + function: function, + lineno: line + )) + } + + // Add all other traces by adding 1 to the callsite depth. This removes the meta call + // to `SentryStacktrace.capture` from the stacktrace. + let callStackTrace = TraceSymbolsParser.current(callsiteDepth: callsiteDepth + 1) + traces.append(contentsOf: callStackTrace) + return traces + } +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/TraceSymbolsParser.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/TraceSymbolsParser.swift new file mode 100644 index 00000000000..7f7421fc5d8 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/Sentry/TraceSymbolsParser.swift @@ -0,0 +1,52 @@ +// +// TraceSymbolsParser.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-22. +// + +import Foundation + +enum TraceSymbolsParser { + /// Get the current call stack as an `CallStacktrace` array representaion. + /// Removes the first `callsiteDepth` traces from the stack. + /// These traces are usually meta traces from classes calling the parser. + /// i.e. removes `TraceSymbolsParser.current` from the stack trace + static func current(callsiteDepth: Int = 0) -> [SentryTrace] { + let callStackSymbols: [String] = Array(Thread.callStackSymbols.dropFirst(callsiteDepth)) + return callStackSymbols.compactMap { symbols in + Self.parse(symbols: symbols) + } + } + + /// Parses a line of `Thread.callStackSymbols` to `CallStackTrace`. + /// - `symbols`: Input which follows the `DLADDR` format. + /// ``` + /// // {depth} {fname} {fbase} {sname} + {saddr} + /// (number with radix 10) (string) (number with radix 16) (string) + (number with radix 10) + /// ``` + /// This extracts `fname` into the `module`, and `sname` into the `functions.` + static func parse(symbols: String) -> SentryTrace? { + // Split the input string by whitespaces and filter out empty components + let components = symbols.split(whereSeparator: \.isWhitespace).filter { !$0.isEmpty } + guard components.indices.contains(3) else { + return nil // Invalid symbol, not enough components. + } + // The `module` is the second component + let module = String(components[1]) + + // The `function` is everything from the fourth component up to but not including the "+" + let functionComponents = components[3...] + let functionString = functionComponents.joined(separator: " ") + + // Find the index of the "+" symbol in the original string + guard let plusIndex = functionString.range(of: "+")?.lowerBound else { + return nil // "+" symbol not found. + } + + // Extract the final function string from the joined components, trimmed and until the "+" + let functionEndIndex = symbols.distance(from: symbols.startIndex, to: plusIndex) + let function = String(functionString.prefix(functionEndIndex)).trimmingCharacters(in: .whitespaces) + return SentryTrace(module: module, function: function) + } +} diff --git a/StripeFinancialConnections/StripeFinancialConnectionsTests/TraceSymbolsParserTests.swift b/StripeFinancialConnections/StripeFinancialConnectionsTests/TraceSymbolsParserTests.swift new file mode 100644 index 00000000000..4da64c66781 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnectionsTests/TraceSymbolsParserTests.swift @@ -0,0 +1,74 @@ +// +// TraceSymbolsParserTests.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2024-10-22. +// + +import Foundation +@testable import StripeFinancialConnections +import XCTest + +class TraceSymbolsParserTests: XCTestCase { + // An example output of `Thread.callStackSymbols`. + private static let mockCallStackSymbols: [String] = [ + "0 StripeFinancialConnections 0x0000000102315a18 $s26StripeFinancialConnections16SentryStacktraceC7captureSayAC10StackFrameVGyFZ + 200", + "1 StripeFinancialConnections 0x00000001023c53d4 $s26StripeFinancialConnections0bC5SheetC7present4from10completionySo16UIViewControllerC_yAA04HostI6ResultOctF + 1360", + "2 StripeFinancialConnections 0x00000001023c18e4 $s26StripeFinancialConnections0bC17SDKImplementationC07presentbC5Sheet9apiClient12clientSecret9returnURL22elementsSessionContext7onEvent4from10completiony0A4Core12STPAPIClientC_S2SSgAL08ElementsnO0VSgyAL0bcQ0VcSgSo16UIViewControllerCyAL0bC9SDKResultOctF + 724", + "3 StripeFinancialConnections 0x00000001023c27f8 $s26StripeFinancialConnections0bC17SDKImplementationC0A4Core0bC12SDKInterfaceAadEP07presentbC5Sheet9apiClient12clientSecret9returnURL22elementsSessionContext7onEvent4from10completionyAD12STPAPIClientC_S2SSgAD08ElementspQ0VSgyAD0bcS0VcSgSo16UIViewControllerCyAD0bC9SDKResultOctFTW + 60", + "4 StripeCore 0x00000001019ccc2c $s10StripeCore32FinancialConnectionsSDKInterfaceP07presentcD5Sheet9apiClient12clientSecret9returnURL22elementsSessionContext7onEvent4from10completionyAA12STPAPIClientC_S2SSgAA08ElementsoP0VSgyAA0cdR0VcSgSo16UIViewControllerCyAA0cD9SDKResultOctFTj + 64", + "5 StripePayments 0x0000000102bd93ac $s14StripePayments23STPBankAccountCollectorC012_collectBankD10ForPayment33_DA65D4A0CBE8EF79280D20C93D49C0D2LL12clientSecret9returnURL20additionalParameters22elementsSessionContext7onEvent6params4from30financialConnectionsCompletionySS_SSSgSDySSypG0A4Core22ElementsSessionContextVSgyAP25FinancialConnectionsEventVcSgAA010STPCollectgD6ParamsCSo16UIViewControllerCyAP29FinancialConnectionsSDKResultOSg_AA04LinkD7SessionCSgSo7NSErrorCSgtctFyA4__s5Error_pSgtcfU_ + 1100", + "6 StripePayments 0x0000000102bde88c $s14StripePayments23STPBankAccountCollectorC012_collectBankD10ForPayment33_DA65D4A0CBE8EF79280D20C93D49C0D2LL12clientSecret9returnURL20additionalParameters22elementsSessionContext7onEvent6params4from30financialConnectionsCompletionySS_SSSgSDySSypG0A4Core22ElementsSessionContextVSgyAP25FinancialConnectionsEventVcSgAA010STPCollectgD6ParamsCSo16UIViewControllerCyAP29FinancialConnectionsSDKResultOSg_AA04LinkD7SessionCSgSo7NSErrorCSgtctFyA4__s5Error_pSgtcfU_TA + 168", + "7 StripePayments 0x0000000102ba32ec $s10StripeCore12STPAPIClientC0A8PaymentsE19linkAccountSessions33_F5AA4EEE3A66BBA67222962A492D7A9BLL8endpoint12clientSecret17paymentMethodType12customerName0V12EmailAddress21additionalParameteres10completionySS_SSAD010STPPaymenttU0OSSSgAPSDySSypGyAD04LinkF7SessionCSg_s5Error_pSgtctFyAT_So17NSHTTPURLResponseCSgAVtcfU_ + 104", + "8 StripePayments 0x0000000102bf0d4c $s14StripePayments10APIRequestC13parseResponse_4body5error10completionySo13NSURLResponseCSg_10Foundation4DataVSgs5Error_pSgyxSg_So17NSHTTPURLResponseCSgAPtctFZyAQ_APtcfU_yycfU_ + 176", + "9 StripeCore 0x0000000101970fec $sIeg_IeyB_TR + 48", + "10 libdispatch.dylib 0x0000000100724ec0 _dispatch_call_block_and_release + 24", + "11 libdispatch.dylib 0x00000001007267b8 _dispatch_client_callout + 16", + "12 libdispatch.dylib 0x000000010073645c _dispatch_main_queue_drain + 1224", + "13 libdispatch.dylib 0x0000000100735f84 _dispatch_main_queue_callback_4CF + 40", + "14 CoreFoundation 0x000000018041ae3c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12", + "15 CoreFoundation 0x0000000180415534 __CFRunLoopRun + 1944", + "16 CoreFoundation 0x0000000180414960 CFRunLoopRunSpecific + 536", + "17 GraphicsServices 0x0000000190183b10 GSEventRunModal + 160", + "18 UIKitCore 0x0000000185aa2b40 -[UIApplication _run] + 796", + "19 UIKitCore 0x0000000185aa6d38 UIApplicationMain + 124", + "20 PaymentSheetExample.debug.dylib 0x0000000101308074 __debug_main_executable_dylib_entry_point + 64", + "21 dyld 0x0000000100879410 start_sim + 20", + "22 ??? 0x00000001001f2154 0x0 + 4297007444", + "23 ??? 0x690f000000000000 0x0 + 7570269498633093120", + ] + + private static let expectedTraces: [CallStackTrace] = [ + CallStackTrace(module: "StripeFinancialConnections", function: "$s26StripeFinancialConnections16SentryStacktraceC7captureSayAC10StackFrameVGyFZ"), + CallStackTrace(module: "StripeFinancialConnections", function: "$s26StripeFinancialConnections0bC5SheetC7present4from10completionySo16UIViewControllerC_yAA04HostI6ResultOctF"), + CallStackTrace(module: "StripeFinancialConnections", function: "$s26StripeFinancialConnections0bC17SDKImplementationC07presentbC5Sheet9apiClient12clientSecret9returnURL22elementsSessionContext7onEvent4from10completiony0A4Core12STPAPIClientC_S2SSgAL08ElementsnO0VSgyAL0bcQ0VcSgSo16UIViewControllerCyAL0bC9SDKResultOctF"), + CallStackTrace(module: "StripeFinancialConnections", function: "$s26StripeFinancialConnections0bC17SDKImplementationC0A4Core0bC12SDKInterfaceAadEP07presentbC5Sheet9apiClient12clientSecret9returnURL22elementsSessionContext7onEvent4from10completionyAD12STPAPIClientC_S2SSgAD08ElementspQ0VSgyAD0bcS0VcSgSo16UIViewControllerCyAD0bC9SDKResultOctFTW"), + CallStackTrace(module: "StripeCore", function: "$s10StripeCore32FinancialConnectionsSDKInterfaceP07presentcD5Sheet9apiClient12clientSecret9returnURL22elementsSessionContext7onEvent4from10completionyAA12STPAPIClientC_S2SSgAA08ElementsoP0VSgyAA0cdR0VcSgSo16UIViewControllerCyAA0cD9SDKResultOctFTj"), + CallStackTrace(module: "StripePayments", function: "$s14StripePayments23STPBankAccountCollectorC012_collectBankD10ForPayment33_DA65D4A0CBE8EF79280D20C93D49C0D2LL12clientSecret9returnURL20additionalParameters22elementsSessionContext7onEvent6params4from30financialConnectionsCompletionySS_SSSgSDySSypG0A4Core22ElementsSessionContextVSgyAP25FinancialConnectionsEventVcSgAA010STPCollectgD6ParamsCSo16UIViewControllerCyAP29FinancialConnectionsSDKResultOSg_AA04LinkD7SessionCSgSo7NSErrorCSgtctFyA4__s5Error_pSgtcfU_"), + CallStackTrace(module: "StripePayments", function: "$s14StripePayments23STPBankAccountCollectorC012_collectBankD10ForPayment33_DA65D4A0CBE8EF79280D20C93D49C0D2LL12clientSecret9returnURL20additionalParameters22elementsSessionContext7onEvent6params4from30financialConnectionsCompletionySS_SSSgSDySSypG0A4Core22ElementsSessionContextVSgyAP25FinancialConnectionsEventVcSgAA010STPCollectgD6ParamsCSo16UIViewControllerCyAP29FinancialConnectionsSDKResultOSg_AA04LinkD7SessionCSgSo7NSErrorCSgtctFyA4__s5Error_pSgtcfU_TA"), + CallStackTrace(module: "StripePayments", function: "$s10StripeCore12STPAPIClientC0A8PaymentsE19linkAccountSessions33_F5AA4EEE3A66BBA67222962A492D7A9BLL8endpoint12clientSecret17paymentMethodType12customerName0V12EmailAddress21additionalParameteres10completionySS_SSAD010STPPaymenttU0OSSSgAPSDySSypGyAD04LinkF7SessionCSg_s5Error_pSgtctFyAT_So17NSHTTPURLResponseCSgAVtcfU_"), + CallStackTrace(module: "StripePayments", function: "$s14StripePayments10APIRequestC13parseResponse_4body5error10completionySo13NSURLResponseCSg_10Foundation4DataVSgs5Error_pSgyxSg_So17NSHTTPURLResponseCSgAPtctFZyAQ_APtcfU_yycfU_"), + CallStackTrace(module: "StripeCore", function: "$sIeg_IeyB_TR"), + CallStackTrace(module: "libdispatch.dylib", function: "_dispatch_call_block_and_release"), + CallStackTrace(module: "libdispatch.dylib", function: "_dispatch_client_callout"), + CallStackTrace(module: "libdispatch.dylib", function: "_dispatch_main_queue_drain"), + CallStackTrace(module: "libdispatch.dylib", function: "_dispatch_main_queue_callback_4CF"), + CallStackTrace(module: "CoreFoundation", function: "__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__"), + CallStackTrace(module: "CoreFoundation", function: "__CFRunLoopRun"), + CallStackTrace(module: "CoreFoundation", function: "CFRunLoopRunSpecific"), + CallStackTrace(module: "GraphicsServices", function: "GSEventRunModal"), + CallStackTrace(module: "UIKitCore", function: "-[UIApplication _run]"), + CallStackTrace(module: "UIKitCore", function: "UIApplicationMain"), + CallStackTrace(module: "PaymentSheetExample.debug.dylib", function: "__debug_main_executable_dylib_entry_point"), + CallStackTrace(module: "dyld", function: "start_sim"), + CallStackTrace(module: "???", function: "0x0"), + CallStackTrace(module: "???", function: "0x0"), + ] + + func testParser() { + for (symbols, expectedTrace) in zip(Self.mockCallStackSymbols, Self.expectedTraces) { + let trace = TraceSymbolsParser.parse(symbols: symbols) + XCTAssertEqual(trace, expectedTrace, "Expected trace does not match for symbols: \(symbols)") + } + } +}