Skip to content

Commit 96a02d7

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

File tree

9 files changed

+311
-4
lines changed

9 files changed

+311
-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

+34
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
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 */; };
6571
496A6AE72C29E0BB00D34F8E /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 496A6AE62C29E0BB00D34F8E /* [email protected] */; };
@@ -322,6 +328,12 @@
322328
492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsWebFlowTests.swift; sourceTree = "<group>"; };
323329
492651652C24C9E7001DDBCA /* TestModeAutofillBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestModeAutofillBannerView.swift; sourceTree = "<group>"; };
324330
492651672C25C0C2001DDBCA /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
331+
494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsSentryClient.swift; sourceTree = "<group>"; };
332+
494D32172CC819F000C5EFAF /* SentryContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryContext.swift; sourceTree = "<group>"; };
333+
494D32192CC8237F00C5EFAF /* SentryPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPayload.swift; sourceTree = "<group>"; };
334+
494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStacktrace.swift; sourceTree = "<group>"; };
335+
494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceSymbolsParser.swift; sourceTree = "<group>"; };
336+
494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceSymbolsParserTests.swift; sourceTree = "<group>"; };
325337
494D62062C45B9B700106519 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
326338
495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsTheme.swift; sourceTree = "<group>"; };
327339
496A6AE62C29E0BB00D34F8E /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
@@ -636,6 +648,7 @@
636648
328390D72E3911449BB9FD0B /* Analytics */ = {
637649
isa = PBXGroup;
638650
children = (
651+
494D32132CC7FCCF00C5EFAF /* Sentry */,
639652
DBBF5CEE2C9030B2D374BC76 /* FinancialConnectionsAnalyticsClient.swift */,
640653
A6038978C79785C18257CD74 /* FinancialConnectionsSheetAnalytics.swift */,
641654
);
@@ -671,6 +684,18 @@
671684
path = FinancialConnectionsSDK;
672685
sourceTree = "<group>";
673686
};
687+
494D32132CC7FCCF00C5EFAF /* Sentry */ = {
688+
isa = PBXGroup;
689+
children = (
690+
494D32152CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift */,
691+
494D32172CC819F000C5EFAF /* SentryContext.swift */,
692+
494D32192CC8237F00C5EFAF /* SentryPayload.swift */,
693+
494D321B2CC8264D00C5EFAF /* SentryStacktrace.swift */,
694+
494D32292CC85E9000C5EFAF /* TraceSymbolsParser.swift */,
695+
);
696+
path = Sentry;
697+
sourceTree = "<group>";
698+
};
674699
49C911362C597EAF00589E0D /* LinkLogin */ = {
675700
isa = PBXGroup;
676701
children = (
@@ -1083,6 +1108,7 @@
10831108
CF731140836AE438C7F4F6AB /* StringExtensionsTests.swift */,
10841109
497142BB2C514B08000DFA64 /* FlowRouterTests.swift */,
10851110
492039942CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift */,
1111+
494D322B2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift */,
10861112
);
10871113
path = StripeFinancialConnectionsTests;
10881114
sourceTree = "<group>";
@@ -1230,6 +1256,8 @@
12301256
"zh-Hant",
12311257
);
12321258
mainGroup = 7879CBA341D7E807714A831B;
1259+
packageReferences = (
1260+
);
12331261
productRefGroup = 820BF9CF057CF92872BC3C15 /* Products */;
12341262
projectDirPath = "";
12351263
projectRoot = "";
@@ -1299,6 +1327,7 @@
12991327
6A3DA1F52C34A37F005C3F6E /* GenericInfoViewController.swift in Sources */,
13001328
FBF513C7F73002FA30CC7C21 /* ConsumerSessionModels.swift in Sources */,
13011329
6A3739142C40558900D1F765 /* GenericInfoBodyView.swift in Sources */,
1330+
494D321A2CC8237F00C5EFAF /* SentryPayload.swift in Sources */,
13021331
EC74B719F0FA1A977EF4708C /* FinancialConnectionsAccount.swift in Sources */,
13031332
460C7685096AA6C693309647 /* FinancialConnectionsAuthSession.swift in Sources */,
13041333
AB5AFAC3C70D6195075DE5AE /* FinancialConnectionsBulletPoint.swift in Sources */,
@@ -1308,6 +1337,7 @@
13081337
F67624595BD2CD7B6793BFDA /* FinancialConnectionsImage.swift in Sources */,
13091338
07712610C7D2F484AAB96982 /* FinancialConnectionsInstitution.swift in Sources */,
13101339
7386E1F9256B23CE29BF996D /* FinancialConnectionsInstitutionSearchResultResource.swift in Sources */,
1340+
494D321C2CC8264D00C5EFAF /* SentryStacktrace.swift in Sources */,
13111341
C7D2763ACCE2CC71E788E18F /* FinancialConnectionsLegalDetailsNotice.swift in Sources */,
13121342
B271AAF41C9FE6AE392B88D3 /* FinancialConnectionsMixedOAuthParams.swift in Sources */,
13131343
DAA51ABB496551074DBA1A20 /* FinancialConnectionsNetworkedAccountsResponse.swift in Sources */,
@@ -1374,6 +1404,7 @@
13741404
3446145FCA3278D51A9D4B80 /* AttachLinkedPaymentAccountDataSource.swift in Sources */,
13751405
E3F62D2F9C344A1178030E8E /* AttachLinkedPaymentAccountViewController.swift in Sources */,
13761406
707C265C4179A8FEC98913FE /* ConsentBodyView.swift in Sources */,
1407+
494D322A2CC85E9000C5EFAF /* TraceSymbolsParser.swift in Sources */,
13771408
465AE8A58AD2183E1E2042FE /* ConsentDataSource.swift in Sources */,
13781409
97C528CE821C6A55D58F68A4 /* ConsentFooterView.swift in Sources */,
13791410
8927328EE28A0C94B5AB69DB /* ConsentLogoView.swift in Sources */,
@@ -1431,6 +1462,7 @@
14311462
9AF6EC34D666BEB3C1397092 /* BulletPointLabelView.swift in Sources */,
14321463
313F5F7F2B0BE5D100BD98A9 /* Docs.docc in Sources */,
14331464
F65E8D16DE691EB6C99C4521 /* Button+Extensions.swift in Sources */,
1465+
494D32162CC7FCE500C5EFAF /* FinancialConnectionsSentryClient.swift in Sources */,
14341466
33FA1684CE79F21271D14F23 /* HitTestStackView.swift in Sources */,
14351467
691619AE9A989548ABA36535 /* HitTestView.swift in Sources */,
14361468
91A3583A0BDE0F8F0C4AD3E2 /* InstitutionIconView.swift in Sources */,
@@ -1440,6 +1472,7 @@
14401472
49C911372C597EAF00589E0D /* LinkLoginDataSource.swift in Sources */,
14411473
6A7814182B361C5000168992 /* PaneLayoutView+Extensions.swift in Sources */,
14421474
99F41681B77ECB0090F34E31 /* SFSafariViewController+Extensions.swift in Sources */,
1475+
494D32182CC819F000C5EFAF /* SentryContext.swift in Sources */,
14431476
AA80602323C28AFAC391358D /* TimeInterval+Extensions.swift in Sources */,
14441477
E637387728FA1597B1B51E5D /* UIImage+Extensions.swift in Sources */,
14451478
495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */,
@@ -1471,6 +1504,7 @@
14711504
492039952CA4972B00CE2072 /* FinancialConnectionsWebFlowTests.swift in Sources */,
14721505
700B745FEF43088D9E34C0E4 /* AccountPickerHelpersTests.swift in Sources */,
14731506
6744CB1B182C5F7220B0B804 /* AuthFlowHelpersTests.swift in Sources */,
1507+
494D322C2CC85FE100C5EFAF /* TraceSymbolsParserTests.swift in Sources */,
14741508
39E5D4531961150E9CB3262F /* EmptyFinancialConnectionsAPIClient.swift in Sources */,
14751509
CB734C25A19D38A87876FB2B /* FinancialConnectionsAnalyticsTest.swift in Sources */,
14761510
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,16 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// SentryStacktrace.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2024-10-22.
6+
//
7+
8+
import Foundation
9+
10+
protocol SentryTrace: Encodable, Equatable {
11+
var function: String { get }
12+
}
13+
14+
struct RootTrace: SentryTrace {
15+
let file: String
16+
let function: String
17+
let lineno: Int
18+
}
19+
20+
struct CallStacktrace: SentryTrace {
21+
let module: String
22+
let function: String
23+
}
24+
25+
enum SentryStacktrace {
26+
static func capture(
27+
filePath: String = #file,
28+
function: String = #function,
29+
line: Int = #line,
30+
callsiteDepth: Int = 1
31+
) -> [any SentryTrace] {
32+
var traces: [any SentryTrace] = []
33+
if let file = filePath.components(separatedBy: "/").last {
34+
// Start with a Root trace, which includes the file, function, and lineno.
35+
traces.append(RootTrace(
36+
file: file,
37+
function: function,
38+
lineno: line
39+
))
40+
}
41+
42+
// Add all other traces by adding 1 to the callsite depth. This removes the meta call
43+
// to `SentryStacktrace.capture` from the stacktrace.
44+
let callStackTrace = TraceSymbolsParser.current(callsiteDepth: callsiteDepth + 1)
45+
traces.append(contentsOf: callStackTrace)
46+
return traces
47+
}
48+
}
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) -> [CallStacktrace] {
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) -> CallStacktrace? {
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 CallStacktrace(module: module, function: function)
51+
}
52+
}

0 commit comments

Comments
 (0)