Skip to content
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

Apple client send and receive support for crash report cohort IDs (CRCIDs) #1116

Merged
merged 32 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
88ae14a
Adopt dependency injection support for CrashCollection
jdjackson Dec 1, 2024
8583c2d
CrashCollectionTests: First test and supporting implementation nearly…
jdjackson Dec 3, 2024
24cb701
Support for CRCID send/receive
jdjackson Dec 4, 2024
418c64c
CrashCollection supports CRCID store and retrieve
jdjackson Dec 4, 2024
3b5d42a
Indentation fix
jdjackson Dec 4, 2024
1f6b78a
2 more tests implemented
jdjackson Dec 4, 2024
8440d72
Clarified remaining potential future tests
jdjackson Dec 4, 2024
4dbf3f0
Cleanup pre-code-review
jdjackson Dec 5, 2024
34ddc9c
Add support for clearing CRCID from client apps
jdjackson Dec 6, 2024
28595e2
Removing debug-only code
jdjackson Dec 6, 2024
acc46b7
Refactor CRCID storage management into new CRCIDManager class
jdjackson Dec 6, 2024
4cb1360
Linting fixes
jdjackson Dec 9, 2024
1d3a373
More linting fixes
jdjackson Dec 9, 2024
3d944d3
More linting fixes, and removing unnecessary TODOs
jdjackson Dec 9, 2024
c56f50f
More linting fixes
jdjackson Dec 9, 2024
2f921b1
Prep for pixels to detect crash reporting failures
jdjackson Dec 9, 2024
4683fb9
Getting close to a good solution for pixel tracking of failed crash s…
jdjackson Dec 12, 2024
f920d03
Removing irrelevant or infisible tests
jdjackson Dec 12, 2024
d949750
Change to submit CRCID header even when not present locally
jdjackson Dec 12, 2024
57f8280
Clean up naming of CrashReportSenderError.crcidMissing
jdjackson Dec 12, 2024
073fff9
Fixing sending of empty CRCID header when no value is present
jdjackson Dec 16, 2024
8053808
Clearing unneeded TODO
jdjackson Dec 16, 2024
e202122
Restoring proper crash.js url
jdjackson Dec 18, 2024
4fd8049
Clean up trailing whitespace for swiftlint compliance
jdjackson Dec 18, 2024
b6e731d
Code review cleanup
jdjackson Dec 19, 2024
07ca1c4
Cleaner handling of CRCID as optional
jdjackson Dec 19, 2024
15b0726
Move CRCIDManager out into its own file.
samsymons Dec 19, 2024
1c91259
Replace `print` statement with `Logger`.
samsymons Dec 19, 2024
c42eb19
Fix header comment formatting.
samsymons Dec 19, 2024
c28ed68
Use `UserDefaults.standard`.
samsymons Dec 19, 2024
88449bb
Merge branch 'main' into jjackson/crcid-send-receive-support
samsymons Dec 19, 2024
bcb85ec
Merge branch 'main' into jjackson/crcid-send-receive-support
samsymons Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,8 @@ let package = Package(
.testTarget(
name: "CrashesTests",
dependencies: [
"Crashes"
"Crashes",
"TestUtils"
]
),
.testTarget(
Expand Down
46 changes: 38 additions & 8 deletions Sources/Crashes/CrashCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import Foundation
import MetricKit
import Persistence
import os.log

public enum CrashCollectionPlatform {
case iOS, macOS, macOSAppStore
Expand All @@ -38,9 +40,10 @@
@available(iOS 13, macOS 12, *)
public final class CrashCollection {

public init(platform: CrashCollectionPlatform) {
crashHandler = CrashHandler()
crashSender = CrashReportSender(platform: platform)
public init(crashReportSender: CrashReportSending, crashCollectionStorage: KeyValueStoring = UserDefaults()) {
self.crashHandler = CrashHandler()
self.crashSender = crashReportSender
self.crashCollectionStorage = crashCollectionStorage
}

public func start(didFindCrashReports: @escaping (_ pixelParameters: [[String: String]], _ payloads: [Data], _ uploadReports: @escaping () -> Void) -> Void) {
Expand All @@ -55,7 +58,11 @@
/// - didFindCrashReports: callback called after payload preprocessing is finished.
/// Provides processed JSON data to be presented to the user and Pixel parameters to fire a crash Pixel.
/// `uploadReports` callback is used when the user accepts uploading the crash report and starts crash upload to the server.
public func start(process: @escaping ([MXDiagnosticPayload]) -> [Data], didFindCrashReports: @escaping (_ pixelParameters: [[String: String]], _ payloads: [Data], _ uploadReports: @escaping () -> Void) -> Void) {
public func start(process: @escaping ([MXDiagnosticPayload]) -> [Data],
didFindCrashReports: @escaping (_ pixelParameters: [[String: String]],
_ payloads: [Data],
_ uploadReports: @escaping () -> Void) -> Void,
didFinishHandlingResponse: @escaping (() -> Void) = {}) {
let first = isFirstCrash
isFirstCrash = false

Expand All @@ -80,8 +87,29 @@
didFindCrashReports(pixelParameters, processedData) {
Task {
for payload in processedData {
await self.crashSender.send(payload)
let crcid = self.crashCollectionStorage.object(forKey: Const.crcidKey) as? String
let result = await self.crashSender.send(payload, crcid: crcid)

switch result.result {
case .success:
Logger.general.debug("Crash Collection - Sending Crash Report: succeeded")
if let receivedCRCID = result.response?.allHeaderFields[CrashReportSender.httpHeaderCRCID] as? String {
if crcid != receivedCRCID {
samsymons marked this conversation as resolved.
Show resolved Hide resolved
Logger.general.debug("Received new value for CRCID: \(receivedCRCID), setting local crcid value")
self.crashCollectionStorage.set(receivedCRCID, forKey: Const.crcidKey)
} else {
Logger.general.debug("Received matching value for CRCID: \(receivedCRCID), no update necessary")
}
} else {
Logger.general.debug("No value for CRCID header: \(Const.crcidKey), clearing local crcid value if present")
self.crashCollectionStorage.removeObject(forKey: Const.crcidKey)
}
case .failure(let failure):
// TODO: Is it worth sending a pixel for this case, so that we can monitor for missing crash reports?

Check failure on line 108 in Sources/Crashes/CrashCollection.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

TODOs should be resolved (Is it worth sending a pixel fo...) (todo)
samsymons marked this conversation as resolved.
Show resolved Hide resolved
Logger.general.debug("Crash Collection - Sending Crash Report: failed (\(failure))")
}
}
didFinishHandlingResponse()
}
}
}
Expand Down Expand Up @@ -173,18 +201,20 @@

var isFirstCrash: Bool {
get {
UserDefaults().object(forKey: Const.firstCrashKey) as? Bool ?? true
crashCollectionStorage.object(forKey: Const.firstCrashKey) as? Bool ?? true
}

set {
UserDefaults().set(newValue, forKey: Const.firstCrashKey)
crashCollectionStorage.set(newValue, forKey: Const.firstCrashKey)
}
}

let crashHandler: CrashHandler
let crashSender: CrashReportSender
let crashSender: CrashReportSending
let crashCollectionStorage: KeyValueStoring

enum Const {
static let firstCrashKey = "CrashCollection.first"
static let crcidKey = "CrashCollection.crcid"
}
}
76 changes: 67 additions & 9 deletions Sources/Crashes/CrashReportSender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,86 @@
import Foundation
import MetricKit

public final class CrashReportSender {
public protocol CrashReportSending {
init(platform: CrashCollectionPlatform)
func send(_ crashReportData: Data, crcid: String?) async -> (result: Result<Data?, Error>, response: HTTPURLResponse?)
func send(_ crashReportData: Data, crcid: String?, completion: @escaping (_ result: Result<Data?, Error>, _ response: HTTPURLResponse?) -> Void)
}

enum CrashReportSenderError: Error {
case invalidResponse
}

// By conforming to a protocol, we can sub in mocks more easily
public final class CrashReportSender: CrashReportSending {

static let reportServiceUrl = URL(string: "https://duckduckgo.com/crash.js")!

static let httpHeaderCRCID = "crcid"

Check failure on line 38 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
samsymons marked this conversation as resolved.
Show resolved Hide resolved
public let platform: CrashCollectionPlatform

Check failure on line 40 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
private let session = URLSession(configuration: .ephemeral)

public init(platform: CrashCollectionPlatform) {
self.platform = platform
}

public func send(_ crashReportData: Data) async {
public func send(_ crashReportData: Data, crcid: String?, completion: @escaping (_ result: Result<Data?, Error>, _ response: HTTPURLResponse?) -> Void) {
samsymons marked this conversation as resolved.
Show resolved Hide resolved
var request = URLRequest(url: Self.reportServiceUrl)
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
request.setValue(platform.userAgent, forHTTPHeaderField: "User-Agent")
if let crcid {
request.setValue(crcid, forHTTPHeaderField: CrashReportSender.httpHeaderCRCID)
Logger.general.debug("Configured crash report HTTP request with crcid: \(crcid)")
}
request.httpMethod = "POST"
request.httpBody = crashReportData

do {
_ = try await session.data(for: request)
} catch {
assertionFailure("CrashReportSender: Failed to send the crash report")

Check failure on line 57 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
Logger.general.debug("CrashReportSender: Awaiting session data")
let task = session.dataTask(with: request) { data, response, error in
// TODO: Consider pixels for failures that mean we may have lost crash info?

Check failure on line 60 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

TODOs should be resolved (Consider pixels for failures t...) (todo)
if let response = response as? HTTPURLResponse {
Logger.general.debug("CrashReportSender: Received HTTP response code: \(response.statusCode)")
if response.statusCode == 200 {
response.allHeaderFields.forEach { print("\($0.key): \($0.value)") } // TODO: Why do we straight-up print these, rather than debug logging?

Check failure on line 64 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

TODOs should be resolved (Why do we straight-up print th...) (todo)
samsymons marked this conversation as resolved.
Show resolved Hide resolved
} else {
assertionFailure("CrashReportSender: Failed to send the crash report: \(response.statusCode)")
}

Check failure on line 68 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
if let data {
completion(.success(data), response)
} else if let error {
completion(.failure(error), response)
} else {
completion(.failure(CrashReportSenderError.invalidResponse), response)
}
} else {
completion(.failure(CrashReportSenderError.invalidResponse), nil)
}
}
task.resume()
}

Check failure on line 82 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
public func send(_ crashReportData: Data, crcid: String?) async -> (result: Result<Data?, Error>, response: HTTPURLResponse?) {
await withCheckedContinuation { continuation in
send(crashReportData, crcid: crcid) { result, response in
continuation.resume(returning: (result, response))
}
}
}

Check failure on line 90 in Sources/Crashes/CrashReportSender.swift

View workflow job for this annotation

GitHub Actions / Run SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
static let crashReportCohortIDKey = "CrashReportSender.crashReportCohortID"
var crashReportCohortID: String? {
get {
if let crcid = UserDefaults().string(forKey: CrashReportSender.crashReportCohortIDKey) {
return crcid
} else {
return nil
}
}
set {
UserDefaults().setValue(newValue, forKey: CrashReportSender.crashReportCohortIDKey)
}
}

private let session = URLSession(configuration: .ephemeral)
}
4 changes: 2 additions & 2 deletions Sources/TestUtils/MockKeyValueStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
import Foundation
import Persistence

public class MockKeyValueStore: KeyValueStoring {
public class MockKeyValueStore: NSObject, KeyValueStoring {
samsymons marked this conversation as resolved.
Show resolved Hide resolved

public var store = [String: Any?]()

public init() { }
public override init() { }

public func object(forKey defaultName: String) -> Any? {
return store[defaultName] as Any?
Expand Down
Loading
Loading