Skip to content
Closed
Changes from all commits
Commits
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
114 changes: 110 additions & 4 deletions Automattic-Tracks-iOS/Crash Logging/CrashLogging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,27 @@ public class CrashLogging {
return self
}

public func shouldSendEvent() -> Bool {
#if DEBUG
return UserDefaults.standard.bool(forKey: "force-crash-logging")
#else
return !dataProvider.userHasOptedOut
#endif
}
Comment on lines +53 to +59
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I thought that beforeSend would be called in all cases but looks like that for capturing envelopes this is skipped. For this reason, I moved out this logic from beforeSend and expose it, this way Gutenberg can access it and determine if an event should be sent.


func beforeSend(event: Sentry.Event?) -> Sentry.Event? {

DDLogDebug("📜 Firing `beforeSend`")

#if DEBUG
DDLogDebug("📜 This is a debug build")
let shouldSendEvent = UserDefaults.standard.bool(forKey: "force-crash-logging")
#else
let shouldSendEvent = !dataProvider.userHasOptedOut
#endif

/// If we shouldn't send the event we have nothing else to do here
guard let event = event, shouldSendEvent else {
if !shouldSendEvent() {
return nil
}
guard let event = event else {
return nil
}

Expand Down Expand Up @@ -278,6 +286,104 @@ extension CrashLogging {
}
}

// MARK: - Helpers for hybrid SDKs
extension CrashLogging {
/// Returns the options required to initialize Sentry in other platforms.
public func getOptionsDict() -> [String: Any] {
return [
"dsn": self.dataProvider.sentryDSN,
"environment": self.dataProvider.buildType,
"releaseName": self.dataProvider.releaseName
]
}
Comment on lines +292 to +298
Copy link
Contributor Author

@fluiddot fluiddot Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hybrid SDKs like React Native (used in Gutenberg), we need to initialize the SDK so I exposed the required options to match the same values we use for the native SDK.


/// Return the current Sentry user.
/// This helper allows events triggered by other platforms to include the current user.
public func getSentryUserDict() -> [String: Any]? {
return dataProvider.currentUser?.sentryUser.serialize()
}
Comment on lines +302 to +304
Copy link
Contributor Author

@fluiddot fluiddot Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to attachScopeToEvent, I exposed the Sentry user for including it in the events generated in the hybrid SDKs. Initially, I thought that the user would be part of the current scope but it's not as it's assigned to the event in the beforeSend callback.


/// Attachs the current scope to an event and returns it.
/// This helper allows events triggered by other platforms to include the same scope as if they would be triggered in this platform.
///
/// - Parameters:
/// - eventDict: The event object
public func attachScopeToEvent(_ eventDict: [String: Any]) -> [String: Any] {
let scope = SentrySDK.currentHub().getScope().serialize()

// Setup tags
var tags = scope["tags"] as? [String: String] ?? [String: String]()
tags["locale"] = NSLocale.current.languageCode

/// Always provide a value in order to determine how often we're unable to retrieve application state
tags["app.state"] = ApplicationFacade().applicationState ?? "unknown"

tags["release"] = self.dataProvider.releaseName

// Assign scope to event
var eventWithScope = eventDict
eventWithScope["tags"] = tags
eventWithScope["breadcrumbs"] = scope["breadcrumbs"]
eventWithScope["contexts"] = scope["context"]

return eventWithScope
}
Comment on lines +311 to +330
Copy link
Contributor Author

@fluiddot fluiddot Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scope used in the events generated in hybrid SDKs is different from the one of the native side so I expose this function to incorporate the same data we see in native crashes.

This logic serializes the current scope and includes the same extra tags added in the beforeSend callback, finally, a new object is created with the following data set:

  • Tags
  • Breadcrumbs
  • Contexts


/// Writes the envelope to the Crash Logging system, the envelope contains all the data required for data ingestion in Sentry.
/// This function is based on the original Sentry implementation for React native: https://github.com/getsentry/sentry-react-native/blob/aa4eb11415cbb73bbd0033e4f0926b539d22315b/ios/RNSentry.m#L118-L158
///
/// - Parameters:
/// - envelopeDict: The envelope object.
public func logEnvelope(_ envelopeDict: [String: Any]) {
if JSONSerialization.isValidJSONObject(envelopeDict) {
guard let headerDict = envelopeDict["header"] as? [String: Any] else {
DDLogError("⛔️ Unable to send envelope to Sentry – header is not defined in the envelope.")
return
}
guard let headerEventId = headerDict["event_id"] as? String else {
DDLogError("⛔️ Unable to send envelope to Sentry – event id is not defined in the envelope header.")
return
}
guard let payloadDict = envelopeDict["payload"] as? [String: Any] else {
DDLogError("⛔️ Unable to send envelope to Sentry – payload is not defined in the envelope.")
return
}
guard let eventLevel = payloadDict["level"] as? String else {
DDLogError("⛔️ Unable to send envelope to Sentry – level is not defined in the envelope payload.")
return
}

// Define the envelope header
let sdkInfo = SentrySdkInfo.init(dict: headerDict)
let eventId = SentryId.init(uuidString: headerEventId)
let envelopeHeader = SentryEnvelopeHeader.init(id: eventId, andSdkInfo: sdkInfo)

guard let envelopeItemData = try? JSONSerialization.data(withJSONObject: payloadDict) else {
DDLogError("⛔️ Unable to send envelope to Sentry – payload could not be serialized.")
return
}

let itemType = payloadDict["type"] as? String ?? "event"
let envelopeItemHeader = SentryEnvelopeItemHeader.init(type: itemType, length: UInt(bitPattern: envelopeItemData.count))
let envelopeItem = SentryEnvelopeItem.init(header: envelopeItemHeader, data: envelopeItemData)
let envelope = SentryEnvelope.init(header: envelopeHeader, singleItem: envelopeItem)

#if DEBUG
SentrySDK.currentHub().getClient()?.capture(envelope: envelope)
#else
if eventLevel == "fatal" {
// Storing to disk happens asynchronously with captureEnvelope
SentrySDK.currentHub().getClient()?.store(envelope)
} else {
SentrySDK.currentHub().getClient()?.capture(envelope: envelope)
}
#endif
} else {
DDLogError("⛔️ Unable to send envelope to Sentry – envelope is not a valid JSON object.")
}
}
}
Comment on lines +337 to +385
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hybrid SDKs send the events as envelopes, it's basically an object that contains all the data required to ingest an event into Sentry.

This implementation is based on the original version of the Sentry React native SDK (version 2.4.2). I'd like to note that the code reference is from version 2.4.2 because is the last version that uses Sentry iOS SDK version 6.x that should match the version we use for crash logging (currently 6.x from the podspec).


// MARK: - Event Logging
extension Event {

Expand Down