Skip to content

Commit e1ed083

Browse files
committed
Add base Sentry client, and stack trace parser
1 parent 2de3f87 commit e1ed083

File tree

10 files changed

+354
-4
lines changed

10 files changed

+354
-4
lines changed

StripeCore/StripeCore/Source/Helpers/InstallMethod.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
import Foundation
99

10-
enum InstallMethod: String {
10+
@_spi(STP) public enum InstallMethod: String {
1111
case cocoapods = "C"
1212
case spm = "S"
1313
case binary = "B" // Built via export_builds.sh
1414
case xcode = "X" // Directly built via Xcode or xcodebuild
1515

16-
static let current: InstallMethod = {
16+
@_spi(STP) public static let current: InstallMethod = {
1717
#if COCOAPODS
1818
return .cocoapods
1919
#elseif SWIFT_PACKAGE

StripeCore/StripeCore/Source/Helpers/STPDeviceUtils.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import Foundation
1010

11-
struct STPDeviceUtils {
12-
static var deviceType: String? {
11+
@_spi(STP) public struct STPDeviceUtils {
12+
@_spi(STP) public static var deviceType: String? {
1313
var systemInfo: utsname = utsname()
1414
uname(&systemInfo)
1515
let machineMirror = Mirror(reflecting: systemInfo.machine)

StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj

+38
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,15 @@
6060
492039952CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */; };
6161
492651662C24C9E7001DDBCA /* TestModeAutofillBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492651652C24C9E7001DDBCA /* TestModeAutofillBannerView.swift */; };
6262
492651682C25C0C2001DDBCA /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 492651672C25C0C2001DDBCA /* [email protected] */; };
63+
494D32162CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */; };
64+
494D32182CC819F000C5EFAF /* SentryContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32172CC819F000C5EFAF /* SentryContext.swift */; };
65+
494D321A2CC8237F00C5EFAF /* SentryPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32192CC8237F00C5EFAF /* SentryPayload.swift */; };
66+
494D321C2CC8264D00C5EFAF /* SentryStacktrace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */; };
67+
494D322A2CC85E9000C5EFAF /* TraceSymbolsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */; };
68+
494D322C2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */; };
6369
494D62072C45B9B700106519 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 494D62062C45B9B700106519 /* [email protected] */; };
6470
495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */; };
71+
4967A0EA2CC971AB002513AC /* SentryException.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4967A0E92CC971AB002513AC /* SentryException.swift */; };
6572
496A6AE72C29E0BB00D34F8E /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 496A6AE62C29E0BB00D34F8E /* [email protected] */; };
6673
497142BC2C514B08000DFA64 /* FlowRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */; };
6774
49A0B5862C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */; };
@@ -322,8 +329,15 @@
322329
492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsWebFlowTests.swift; sourceTree = "<group>"; };
323330
492651652C24C9E7001DDBCA /* TestModeAutofillBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestModeAutofillBannerView.swift; sourceTree = "<group>"; };
324331
492651672C25C0C2001DDBCA /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
332+
494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsSentryClient.swift; sourceTree = "<group>"; };
333+
494D32172CC819F000C5EFAF /* SentryContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryContext.swift; sourceTree = "<group>"; };
334+
494D32192CC8237F00C5EFAF /* SentryPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPayload.swift; sourceTree = "<group>"; };
335+
494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStacktrace.swift; sourceTree = "<group>"; };
336+
494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceSymbolsParser.swift; sourceTree = "<group>"; };
337+
494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceSymbolsParserTests.swift; sourceTree = "<group>"; };
325338
494D62062C45B9B700106519 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
326339
495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsTheme.swift; sourceTree = "<group>"; };
340+
4967A0E92CC971AB002513AC /* SentryException.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryException.swift; sourceTree = "<group>"; };
327341
496A6AE62C29E0BB00D34F8E /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
328342
497142BB2C514B08000DFA64 /* FlowRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowRouterTests.swift; sourceTree = "<group>"; };
329343
49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAPIClientTests.swift; sourceTree = "<group>"; };
@@ -636,6 +650,7 @@
636650
328390D72E3911449BB9FD0B /* Analytics */ = {
637651
isa = PBXGroup;
638652
children = (
653+
494D32132CC7FCCF00C5EFAF /* Sentry */,
639654
DBBF5CEE2C9030B2D374BC76 /* FinancialConnectionsAnalyticsClient.swift */,
640655
A6038978C79785C18257CD74 /* FinancialConnectionsSheetAnalytics.swift */,
641656
);
@@ -671,6 +686,19 @@
671686
path = FinancialConnectionsSDK;
672687
sourceTree = "<group>";
673688
};
689+
494D32132CC7FCCF00C5EFAF /* Sentry */ = {
690+
isa = PBXGroup;
691+
children = (
692+
494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */,
693+
494D32172CC819F000C5EFAF /* SentryContext.swift */,
694+
494D32192CC8237F00C5EFAF /* SentryPayload.swift */,
695+
494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */,
696+
494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */,
697+
4967A0E92CC971AB002513AC /* SentryException.swift */,
698+
);
699+
path = Sentry;
700+
sourceTree = "<group>";
701+
};
674702
49C911362C597EAF00589E0D /* LinkLogin */ = {
675703
isa = PBXGroup;
676704
children = (
@@ -1083,6 +1111,7 @@
10831111
CF731140836AE438C7F4F6AB /* StringExtensionsTests.swift */,
10841112
497142BB2C514B08000DFA64 /* FlowRouterTests.swift */,
10851113
492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */,
1114+
494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */,
10861115
);
10871116
path = StripeFinancialConnectionsTests;
10881117
sourceTree = "<group>";
@@ -1230,6 +1259,8 @@
12301259
"zh-Hant",
12311260
);
12321261
mainGroup = 7879CBA341D7E807714A831B;
1262+
packageReferences = (
1263+
);
12331264
productRefGroup = 820BF9CF057CF92872BC3C15 /* Products */;
12341265
projectDirPath = "";
12351266
projectRoot = "";
@@ -1299,6 +1330,7 @@
12991330
6A3DA1F52C34A37F005C3F6E /* GenericInfoViewController.swift in Sources */,
13001331
FBF513C7F73002FA30CC7C21 /* ConsumerSessionModels.swift in Sources */,
13011332
6A3739142C40558900D1F765 /* GenericInfoBodyView.swift in Sources */,
1333+
494D321A2CC8237F00C5EFAF /* SentryPayload.swift in Sources */,
13021334
EC74B719F0FA1A977EF4708C /* FinancialConnectionsAccount.swift in Sources */,
13031335
460C7685096AA6C693309647 /* FinancialConnectionsAuthSession.swift in Sources */,
13041336
AB5AFAC3C70D6195075DE5AE /* FinancialConnectionsBulletPoint.swift in Sources */,
@@ -1308,8 +1340,10 @@
13081340
F67624595BD2CD7B6793BFDA /* FinancialConnectionsImage.swift in Sources */,
13091341
07712610C7D2F484AAB96982 /* FinancialConnectionsInstitution.swift in Sources */,
13101342
7386E1F9256B23CE29BF996D /* FinancialConnectionsInstitutionSearchResultResource.swift in Sources */,
1343+
494D321C2CC8264D00C5EFAF /* SentryStacktrace.swift in Sources */,
13111344
C7D2763ACCE2CC71E788E18F /* FinancialConnectionsLegalDetailsNotice.swift in Sources */,
13121345
B271AAF41C9FE6AE392B88D3 /* FinancialConnectionsMixedOAuthParams.swift in Sources */,
1346+
4967A0EA2CC971AB002513AC /* SentryException.swift in Sources */,
13131347
DAA51ABB496551074DBA1A20 /* FinancialConnectionsNetworkedAccountsResponse.swift in Sources */,
13141348
6A732CA62B69A46D00828CB1 /* PhoneTextField.swift in Sources */,
13151349
6FE9F171CF9A5D0EDB2035AA /* FinancialConnectionsNetworkingLinkSignup.swift in Sources */,
@@ -1374,6 +1408,7 @@
13741408
3446145FCA3278D51A9D4B80 /* AttachLinkedPaymentAccountDataSource.swift in Sources */,
13751409
E3F62D2F9C344A1178030E8E /* AttachLinkedPaymentAccountViewController.swift in Sources */,
13761410
707C265C4179A8FEC98913FE /* ConsentBodyView.swift in Sources */,
1411+
494D322A2CC85E9000C5EFAF /* TraceSymbolsParser.swift in Sources */,
13771412
465AE8A58AD2183E1E2042FE /* ConsentDataSource.swift in Sources */,
13781413
97C528CE821C6A55D58F68A4 /* ConsentFooterView.swift in Sources */,
13791414
8927328EE28A0C94B5AB69DB /* ConsentLogoView.swift in Sources */,
@@ -1431,6 +1466,7 @@
14311466
9AF6EC34D666BEB3C1397092 /* BulletPointLabelView.swift in Sources */,
14321467
313F5F7F2B0BE5D100BD98A9 /* Docs.docc in Sources */,
14331468
F65E8D16DE691EB6C99C4521 /* Button+Extensions.swift in Sources */,
1469+
494D32162CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift in Sources */,
14341470
33FA1684CE79F21271D14F23 /* HitTestStackView.swift in Sources */,
14351471
691619AE9A989548ABA36535 /* HitTestView.swift in Sources */,
14361472
91A3583A0BDE0F8F0C4AD3E2 /* InstitutionIconView.swift in Sources */,
@@ -1440,6 +1476,7 @@
14401476
49C911372C597EAF00589E0D /* LinkLoginDataSource.swift in Sources */,
14411477
6A7814182B361C5000168992 /* PaneLayoutView+Extensions.swift in Sources */,
14421478
99F41681B77ECB0090F34E31 /* SFSafariViewController+Extensions.swift in Sources */,
1479+
494D32182CC819F000C5EFAF /* SentryContext.swift in Sources */,
14431480
AA80602323C28AFAC391358D /* TimeInterval+Extensions.swift in Sources */,
14441481
E637387728FA1597B1B51E5D /* UIImage+Extensions.swift in Sources */,
14451482
495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */,
@@ -1471,6 +1508,7 @@
14711508
492039952CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift in Sources */,
14721509
700B745FEF43088D9E34C0E4 /* AccountPickerHelpersTests.swift in Sources */,
14731510
6744CB1B182C5F7220B0B804 /* AuthFlowHelpersTests.swift in Sources */,
1511+
494D322C2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift in Sources */,
14741512
39E5D4531961150E9CB3262F /* EmptyFinancialConnectionsAPIClient.swift in Sources */,
14751513
CB734C25A19D38A87876FB2B /* FinancialConnectionsAnalyticsTest.swift in Sources */,
14761514
497142BC2C514B08000DFA64 /* FlowRouterTests.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// FinancialConnectionsSentryClient.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-22.
6+
//
7+
8+
import Foundation
9+
@_spi(STP) import StripeCore
10+
11+
protocol FinancialConnectionsErrorReporter {
12+
func report(error: Error, parameters: [String: Any])
13+
}
14+
15+
class FinancialConnectionsSentryClient: FinancialConnectionsErrorReporter {
16+
private static let endpoint: URL = {
17+
let projectId = "871"
18+
var components = URLComponents()
19+
components.scheme = "https"
20+
components.host = "errors.stripe.com"
21+
components.path = "/api/\(projectId)/envelope/"
22+
return components.url!
23+
}()
24+
25+
func report(error: Error, parameters: [String: Any]) {
26+
// TODO
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// SentryContext.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-22.
6+
//
7+
8+
import Foundation
9+
@_spi(STP) import StripeCore
10+
import UIKit
11+
12+
struct SentryContext: Encodable {
13+
let app: SentryAppContext
14+
let os: SentryOsContext
15+
let device: SentryDeviceContext
16+
17+
static let shared: SentryContext = {
18+
let app = SentryAppContext(
19+
appIdentifier: Bundle.stp_applicationBundleId() ?? "",
20+
appName: Bundle.stp_applicationName() ?? "",
21+
appVersion: Bundle.stp_applicationVersion() ?? ""
22+
)
23+
let os = SentryOsContext(
24+
name: "iOS",
25+
version: UIDevice.current.systemVersion,
26+
type: InstallMethod.current.rawValue
27+
)
28+
let device = SentryDeviceContext(
29+
modelId: UIDevice.current.identifierForVendor?.uuidString ?? "",
30+
model: UIDevice.current.model,
31+
manufacturer: "Apple",
32+
type: STPDeviceUtils.deviceType ?? ""
33+
)
34+
35+
return SentryContext(app: app, os: os, device: device)
36+
}()
37+
}
38+
39+
struct SentryAppContext: Encodable {
40+
let appIdentifier: String
41+
let appName: String
42+
let appVersion: String
43+
}
44+
45+
struct SentryOsContext: Encodable {
46+
let name: String
47+
let version: String
48+
let type: String
49+
}
50+
51+
struct SentryDeviceContext: Encodable {
52+
let modelId: String
53+
let model: String
54+
let manufacturer: String
55+
let type: String
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// SentryException.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-23.
6+
//
7+
8+
import Foundation
9+
10+
struct SentryException: Encodable {
11+
var values: [SentryExceptionValue] = []
12+
}
13+
14+
/// https://develop.sentry.dev/sdk/data-model/event-payloads/exception/
15+
struct SentryExceptionValue: Encodable {
16+
let type: String
17+
let value: String
18+
let stacktrace: SentryStacktrace
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// SentryPayload.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-22.
6+
//
7+
8+
import Foundation
9+
@_spi(STP) import StripeCore
10+
11+
struct SentryPayload: Encodable {
12+
let eventId: String = UUID().uuidString.replacingOccurrences(of: "-", with: "")
13+
let timestamp: TimeInterval = Date().timeIntervalSince1970
14+
let release: String = StripeAPIConfiguration.STPSDKVersion
15+
let context: SentryContext = .shared
16+
let exception: SentryException
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// SentryStacktrace.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-22.
6+
//
7+
8+
import Foundation
9+
10+
/// https://develop.sentry.dev/sdk/data-model/event-payloads/stacktrace/
11+
struct SentryTrace: Encodable, Equatable {
12+
let module: String?
13+
let file: String?
14+
let function: String
15+
let lineno: Int?
16+
17+
init(module: String? = nil, file: String? = nil, function: String, lineno: Int? = nil) {
18+
self.module = module
19+
self.file = file
20+
self.function = function
21+
self.lineno = lineno
22+
}
23+
24+
enum CodingKeys: CodingKey {
25+
case module
26+
case file
27+
case function
28+
case lineno
29+
}
30+
31+
// Custom encoder to only encode non-nil properties.
32+
func encode(to encoder: any Encoder) throws {
33+
var container = encoder.container(keyedBy: CodingKeys.self)
34+
try container.encodeIfPresent(self.module, forKey: .module)
35+
try container.encodeIfPresent(self.file, forKey: .file)
36+
try container.encode(self.function, forKey: .function)
37+
try container.encodeIfPresent(self.lineno, forKey: .lineno)
38+
}
39+
}
40+
41+
struct SentryStacktrace: Encodable {
42+
let frames: [SentryTrace]
43+
44+
static func capture(
45+
filePath: String = #file,
46+
function: String = #function,
47+
line: Int = #line,
48+
callsiteDepth: Int = 1
49+
) -> [SentryTrace] {
50+
var traces: [SentryTrace] = []
51+
if let file = filePath.components(separatedBy: "/").last {
52+
// Start with a Root trace, which includes the file, function, and lineno.
53+
traces.append(SentryTrace(
54+
file: file,
55+
function: function,
56+
lineno: line
57+
))
58+
}
59+
60+
// Add all other traces by adding 1 to the callsite depth. This removes the meta call
61+
// to `SentryStacktrace.capture` from the stacktrace.
62+
let callStackTrace = TraceSymbolsParser.current(callsiteDepth: callsiteDepth + 1)
63+
traces.append(contentsOf: callStackTrace)
64+
return traces
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// TraceSymbolsParser.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-22.
6+
//
7+
8+
import Foundation
9+
10+
enum TraceSymbolsParser {
11+
/// Get the current call stack as an `CallStacktrace` array representaion.
12+
/// Removes the first `callsiteDepth` traces from the stack.
13+
/// These traces are usually meta traces from classes calling the parser.
14+
/// i.e. removes `TraceSymbolsParser.current` from the stack trace
15+
static func current(callsiteDepth: Int = 0) -> [SentryTrace] {
16+
let callStackSymbols: [String] = Array(Thread.callStackSymbols.dropFirst(callsiteDepth))
17+
return callStackSymbols.compactMap { symbols in
18+
Self.parse(symbols: symbols)
19+
}
20+
}
21+
22+
/// Parses a line of `Thread.callStackSymbols` to `CallStackTrace`.
23+
/// - `symbols`: Input which follows the `DLADDR` format.
24+
/// ```
25+
/// // {depth} {fname} {fbase} {sname} + {saddr}
26+
/// (number with radix 10) (string) (number with radix 16) (string) + (number with radix 10)
27+
/// ```
28+
/// This extracts `fname` into the `module`, and `sname` into the `functions.`
29+
static func parse(symbols: String) -> SentryTrace? {
30+
// Split the input string by whitespaces and filter out empty components
31+
let components = symbols.split(whereSeparator: \.isWhitespace).filter { !$0.isEmpty }
32+
guard components.indices.contains(3) else {
33+
return nil // Invalid symbol, not enough components.
34+
}
35+
// The `module` is the second component
36+
let module = String(components[1])
37+
38+
// The `function` is everything from the fourth component up to but not including the "+"
39+
let functionComponents = components[3...]
40+
let functionString = functionComponents.joined(separator: " ")
41+
42+
// Find the index of the "+" symbol in the original string
43+
guard let plusIndex = functionString.range(of: "+")?.lowerBound else {
44+
return nil // "+" symbol not found.
45+
}
46+
47+
// Extract the final function string from the joined components, trimmed and until the "+"
48+
let functionEndIndex = symbols.distance(from: symbols.startIndex, to: plusIndex)
49+
let function = String(functionString.prefix(functionEndIndex)).trimmingCharacters(in: .whitespaces)
50+
return SentryTrace(module: module, function: function)
51+
}
52+
}

0 commit comments

Comments
 (0)