Skip to content

Commit

Permalink
chore(ios): add attachment on crash
Browse files Browse the repository at this point in the history
  • Loading branch information
Adwin Ronald Ross committed Jan 29, 2025
1 parent 750cf4e commit da367aa
Show file tree
Hide file tree
Showing 23 changed files with 483 additions and 23 deletions.
2 changes: 2 additions & 0 deletions ios/DemoApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>Some description to explain why access is required</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
Expand Down
16 changes: 16 additions & 0 deletions ios/MeasureSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
5229D16E2CCB533C00EFFE44 /* RecentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5229D16D2CCB533C00EFFE44 /* RecentSession.swift */; };
522BA9D42D36579100DBF4A3 /* DataCleanupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */; };
522BA9D62D37C2A000DBF4A3 /* DataCleanupServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */; };
522BA9DA2D392BCD00DBF4A3 /* ScreenshotGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522BA9D92D392BCD00DBF4A3 /* ScreenshotGenerator.swift */; };
522BA9DC2D3A4DE500DBF4A3 /* AttachmentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522BA9DB2D3A4DE500DBF4A3 /* AttachmentProcessor.swift */; };
523287692C85E07B000EE268 /* LifecycleObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287682C85E07B000EE268 /* LifecycleObserverTests.swift */; };
523287732C86195E000EE268 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287722C86195E000EE268 /* SessionManagerTests.swift */; };
523287752C8619C4000EE268 /* MockIdProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287742C8619C4000EE268 /* MockIdProvider.swift */; };
Expand Down Expand Up @@ -209,6 +211,8 @@
52FA6A8B2CE21EDF0091F089 /* MockMemoryUsageCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FA6A8A2CE21EDF0091F089 /* MockMemoryUsageCalculator.swift */; };
52FA6A8D2CE222030091F089 /* CpuUsageCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FA6A8C2CE222030091F089 /* CpuUsageCalculatorTests.swift */; };
52FA6A8F2CE222360091F089 /* MemoryUsageCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FA6A8E2CE222360091F089 /* MemoryUsageCalculatorTests.swift */; };
52FD08D62D48FF9600897EA2 /* CrashScreenshotSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FD08D52D48FF9600897EA2 /* CrashScreenshotSaver.swift */; };
52FD08D82D4A432000897EA2 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FD08D72D4A431F00897EA2 /* UIColor+Extension.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -373,6 +377,8 @@
5229D16D2CCB533C00EFFE44 /* RecentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSession.swift; sourceTree = "<group>"; };
522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCleanupService.swift; sourceTree = "<group>"; };
522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCleanupServiceTests.swift; sourceTree = "<group>"; };
522BA9D92D392BCD00DBF4A3 /* ScreenshotGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotGenerator.swift; sourceTree = "<group>"; };
522BA9DB2D3A4DE500DBF4A3 /* AttachmentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentProcessor.swift; sourceTree = "<group>"; };
523287682C85E07B000EE268 /* LifecycleObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleObserverTests.swift; sourceTree = "<group>"; };
523287722C86195E000EE268 /* SessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = "<group>"; };
523287742C8619C4000EE268 /* MockIdProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -528,6 +534,8 @@
52FA6A8A2CE21EDF0091F089 /* MockMemoryUsageCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMemoryUsageCalculator.swift; sourceTree = "<group>"; };
52FA6A8C2CE222030091F089 /* CpuUsageCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuUsageCalculatorTests.swift; sourceTree = "<group>"; };
52FA6A8E2CE222360091F089 /* MemoryUsageCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryUsageCalculatorTests.swift; sourceTree = "<group>"; };
52FD08D52D48FF9600897EA2 /* CrashScreenshotSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashScreenshotSaver.swift; sourceTree = "<group>"; };
52FD08D72D4A431F00897EA2 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -605,6 +613,7 @@
52A1A9472CA31E8E00461103 /* NSManagedObjectContext+Extension.swift */,
52AE72072CABAEAB00F2830A /* NSObject+Extension.swift */,
5225D0342D0AEB1A00FD240D /* String+Extension.swift */,
52FD08D72D4A431F00897EA2 /* UIColor+Extension.swift */,
5202BE432C89600200A3496E /* UIDevice+Extension.swift */,
52D51A5B2CCE77A5008F30A6 /* UIViewController+Extension.swift */,
52AE72062CABAEAB00F2830A /* UIWindow+Extension.swift */,
Expand Down Expand Up @@ -635,6 +644,7 @@
isa = PBXGroup;
children = (
5202BE732C8B117900A3496E /* Attachment.swift */,
522BA9DB2D3A4DE500DBF4A3 /* AttachmentProcessor.swift */,
5202BE742C8B117900A3496E /* AttachmentType.swift */,
52B2A8782D1A89EF00C6B5CF /* CustomEventCollector.swift */,
52B2A8762D1A790200C6B5CF /* CustomEventData.swift */,
Expand Down Expand Up @@ -831,6 +841,7 @@
52CC63C22C9C609F00F7CA0A /* CrashDataWriter.swift */,
52CC63C62C9D870B00F7CA0A /* CrashReport.swift */,
52A853362C983FFC00B2A39F /* CrashReportingManager.swift */,
52FD08D52D48FF9600897EA2 /* CrashScreenshotSaver.swift */,
52CC63C82C9DE71300F7CA0A /* SystemCrashReporter.swift */,
);
path = CrashReporter;
Expand Down Expand Up @@ -861,6 +872,7 @@
528EAB8C2C80824200CB1574 /* Logger.swift */,
52A1A9452CA1786A00461103 /* MeasureQueue.swift */,
528EAB972C845AF400CB1574 /* Randomizer.swift */,
522BA9D92D392BCD00DBF4A3 /* ScreenshotGenerator.swift */,
52EB380B2C8C7334002D63EC /* SignPost.swift */,
52FA6A7F2CE212320091F089 /* SysCtl.swift */,
52CC63C42C9D714200F7CA0A /* SystemFileManager.swift */,
Expand Down Expand Up @@ -1392,6 +1404,7 @@
528EAB8B2C807BA800CB1574 /* IdProvider.swift in Sources */,
5243D7C72CEB87C700401D92 /* LaunchCallbacks.swift in Sources */,
5202BE3E2C895FC800A3496E /* InstallationIdAttributeProcessor.swift in Sources */,
52FD08D82D4A432000897EA2 /* UIColor+Extension.swift in Sources */,
52CD91242C7C3D0F000189BA /* MeasureInternal.swift in Sources */,
52A1A94D2CA3D3CA00461103 /* EventEntity.swift in Sources */,
5242B2A02C7F4AF600BE19F7 /* ClientInfo.swift in Sources */,
Expand All @@ -1405,6 +1418,7 @@
5225D0352D0AEB1A00FD240D /* String+Extension.swift in Sources */,
524576772CC1366E00B288E5 /* EventExporter.swift in Sources */,
5202BE472C89600200A3496E /* UIDevice+Extension.swift in Sources */,
52FD08D62D48FF9600897EA2 /* CrashScreenshotSaver.swift in Sources */,
52AE72012CABAE9000F2830A /* GestureCollector.swift in Sources */,
5202BE3B2C895FC800A3496E /* AttributeProcessor.swift in Sources */,
522BA9D42D36579100DBF4A3 /* DataCleanupService.swift in Sources */,
Expand All @@ -1423,6 +1437,7 @@
5202BE482C89600200A3496E /* UserDefaultStorage.swift in Sources */,
5225D0502D0FECFF00FD240D /* InputStream+Extension.swift in Sources */,
5202BE7A2C8B117900A3496E /* AttachmentType.swift in Sources */,
522BA9DA2D392BCD00DBF4A3 /* ScreenshotGenerator.swift in Sources */,
528EAB8D2C80824200CB1574 /* Logger.swift in Sources */,
528EAB8F2C81B4C700CB1574 /* TimeProvider.swift in Sources */,
52CD91222C7C318C000189BA /* ConfigLoader.swift in Sources */,
Expand Down Expand Up @@ -1487,6 +1502,7 @@
52A1A9462CA1786A00461103 /* MeasureQueue.swift in Sources */,
52AE72092CABAEAB00F2830A /* NSObject+Extension.swift in Sources */,
5225D0312D088B7100FD240D /* NetworkInterceptorProtocol.swift in Sources */,
522BA9DC2D3A4DE500DBF4A3 /* AttachmentProcessor.swift in Sources */,
52CD91202C7B39AE000189BA /* Config.swift in Sources */,
52AE72082CABAEAB00F2830A /* UIWindow+Extension.swift in Sources */,
52D51A5E2CCE7BE8008F30A6 /* LifecycleManager.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions ios/MeasureSDK/Config/BaseConfigProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ final class BaseConfigProvider: ConfigProvider {
self.cachedConfig = configLoader.getCachedConfig()
}

var screenshotMaskHexColor: String {
return getMergedConfig(\.screenshotMaskHexColor)
}

var screenshotCompressionQuality: CGFloat {
return getMergedConfig(\.screenshotCompressionQuality)
}

var eventTypeExportAllowList: [EventType] {
return getMergedConfig(\.eventTypeExportAllowList)
}
Expand Down
4 changes: 4 additions & 0 deletions ios/MeasureSDK/Config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ struct Config: InternalConfig, MeasureConfig {
let maxUserDefinedAttributeValueLength: Int
let maxUserDefinedAttributesPerEvent: Int
let eventTypeExportAllowList: [EventType]
let screenshotMaskHexColor: String
let screenshotCompressionQuality: CGFloat

internal init(enableLogging: Bool = DefaultConfig.enableLogging,
trackScreenshotOnCrash: Bool = DefaultConfig.trackScreenshotOnCrash,
Expand Down Expand Up @@ -71,5 +73,7 @@ struct Config: InternalConfig, MeasureConfig {
.lifecycleSwiftUI,
.lifecycleViewController,
.screenView]
self.screenshotMaskHexColor = "222222"
self.screenshotCompressionQuality = 0.25
}
}
5 changes: 5 additions & 0 deletions ios/MeasureSDK/Config/InternalConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ protocol InternalConfig {
/// All `EventType`s that are always exported, regardless of other filters like session sampling rate and whether the session crashed or not.
var eventTypeExportAllowList: [EventType] { get }

/// The color of the mask to apply to the screenshot. The value should be a hex color string. For example, "#222222".
var screenshotMaskHexColor: String { get }

/// The compression quality of the screenshot. Must be between 0 and 1, where 0 is lowest quality and smallest size while 1 is highest quality and largest size.
var screenshotCompressionQuality: CGFloat { get }
}
16 changes: 14 additions & 2 deletions ios/MeasureSDK/CoreData/Entities/EventEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct EventEntity { // swiftlint:disable:this type_body_length
self.type = event.type.rawValue
self.userTriggered = event.userTriggered
self.timestampInMillis = event.timestampInMillis ?? 0
self.attachmentSize = 0
self.attachmentSize = event.attachments?.reduce(0) { $0 + $1.size } ?? 0
self.batchId = nil
self.userDefinedAttributes = event.userDefinedAttributes
self.needsReporting = needsReporting
Expand Down Expand Up @@ -466,9 +466,21 @@ struct EventEntity { // swiftlint:disable:this type_body_length
timestampInMillis: self.timestampInMillis,
type: EventType(rawValue: self.type) ?? .exception,
data: decodedData,
attachments: decodedAttachments ?? [Attachment](),
attachments: decodedAttachments,
attributes: decodedAttributes,
userTriggered: self.userTriggered,
userDefinedAttributes: self.userDefinedAttributes)
}

func getAttachments() -> [Attachment]? {
if let attachmentData = self.attachments {
do {
return try JSONDecoder().decode([Attachment].self, from: attachmentData)
} catch {
return nil
}
} else {
return nil
}
}
}
16 changes: 14 additions & 2 deletions ios/MeasureSDK/CrashReporter/CrashReportingManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@ final class CrashReportingManager: CrashReportManager {
private let logger: Logger
private let eventProcessor: EventProcessor
private let crashDataPersistence: CrashDataPersistence
private let systemFileManager: SystemFileManager
private let idProvider: IdProvider
let hasPendingCrashReport: Bool

init(logger: Logger, eventProcessor: EventProcessor, crashDataPersistence: CrashDataPersistence, crashReporter: SystemCrashReporter) {
init(logger: Logger, eventProcessor: EventProcessor, crashDataPersistence: CrashDataPersistence, crashReporter: SystemCrashReporter, systemFileManager: SystemFileManager, idProvider: IdProvider) {
self.logger = logger
self.eventProcessor = eventProcessor
self.crashDataPersistence = crashDataPersistence
self.crashReporter = crashReporter
self.crashReporter.setCrashCallback(measureCrashCallback)
self.hasPendingCrashReport = crashReporter.hasPendingCrashReport
self.systemFileManager = systemFileManager
self.idProvider = idProvider
}

func enableCrashReporting() {
Expand All @@ -58,6 +62,14 @@ final class CrashReportingManager: CrashReportManager {
return
}

var attachments: [Attachment]?
if let latestScreenshot = systemFileManager.retrieveFile(name: crashScreenshotName, folderName: crashScreenshotDirectoryName, directory: .documentDirectory) {
attachments = [Attachment(name: crashScreenshotName,
type: .screenshot,
size: Int64(latestScreenshot.count),
id: idProvider.createId(),
bytes: latestScreenshot)]
}
let crashDataAttributes = crashDataPersistence.readCrashData()
if let attributes = crashDataAttributes.attribute, let sessionId = crashDataPersistence.sessionId {
exception.foreground = crashDataPersistence.isForeground
Expand All @@ -66,7 +78,7 @@ final class CrashReportingManager: CrashReportManager {
type: .exception,
attributes: attributes,
sessionId: sessionId,
attachments: nil,
attachments: attachments,
userDefinedAttributes: nil)
}

Expand Down
51 changes: 51 additions & 0 deletions ios/MeasureSDK/CrashReporter/CrashScreenshotSaver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// CrashScreenshotSaver.swift
// MeasureSDK
//
// Created by Adwin Ross on 28/01/25.
//

import Foundation

protocol CrashScreenshotSaver {
func onViewDidAppearCalled()
func enable()
}

final class BaseCrashScreenshotSaver: CrashScreenshotSaver {
private let logger: Logger
private var lastUpdateTime: Date?
private let screenshotGenerator: ScreenshotGenerator
private let systemFileManager: SystemFileManager
private var isEnabled = false

init(logger: Logger, screenshotGenerator: ScreenshotGenerator, systemFileManager: SystemFileManager) {
self.logger = logger
self.screenshotGenerator = screenshotGenerator
self.systemFileManager = systemFileManager
}

func enable() {
isEnabled = true
}

func onViewDidAppearCalled() {
guard isEnabled else { return }

let now = Date()

if let lastUpdateTime = self.lastUpdateTime, now.timeIntervalSince(lastUpdateTime) < 1.0 {
return
}

self.lastUpdateTime = now

guard let screenshot = screenshotGenerator.generate(), let screenshotData = screenshot.pngData() else {
return
}

if let filepath = systemFileManager.saveFile(data: screenshotData, name: crashScreenshotName, folderName: crashScreenshotDirectoryName, directory: .documentDirectory) {
logger.log(level: .debug, message: "Screenshot saved at \(filepath).", error: nil, data: nil)
}
}
}
10 changes: 10 additions & 0 deletions ios/MeasureSDK/Events/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ struct Attachment: Codable {
/// An optional path to the attachment (not encoded).
var path: String?

/// Size of the attachment in bytes
var size: Int64

/// A unique id for the image
var id: String

init(name: String,
type: AttachmentType,
size: Int64,
id: String,
bytes: Data? = nil,
path: String? = nil) {
precondition(bytes != nil || path != nil, "Failed to create Attachment. Either bytes or path must be provided")
Expand All @@ -31,5 +39,7 @@ struct Attachment: Codable {
self.type = type
self.bytes = bytes
self.path = path
self.size = size
self.id = id
}
}
57 changes: 57 additions & 0 deletions ios/MeasureSDK/Events/AttachmentProcessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// AttachmentProcessor.swift
// MeasureSDK
//
// Created by Adwin Ross on 17/01/25.
//

import Foundation

enum AttachmentStorageType {
case data
case fileStorage
}

protocol AttachmentProcessor {
func getAttachmentObject(for image: UIImage, name: String, storageType: AttachmentStorageType, attachmentType: AttachmentType, compressionRatio: CGFloat) -> Attachment?
}

final class BaseAttachmentProcessor: AttachmentProcessor {
private let fileManager: SystemFileManager
private let logger: Logger
private let idProvider: IdProvider

init(logger: Logger, fileManager: SystemFileManager, idProvider: IdProvider) {
self.fileManager = fileManager
self.logger = logger
self.idProvider = idProvider
}

func getAttachmentObject(for image: UIImage,
name: String,
storageType: AttachmentStorageType,
attachmentType: AttachmentType,
compressionRatio: CGFloat) -> Attachment? {
guard compressionRatio >= 0, compressionRatio <= 1 else {
logger.internalLog(level: .error, message: "Invalid compression ratio: \(compressionRatio). Must be between 0 and 1.", error: nil, data: nil)
return nil
}

guard let imageData = image.jpegData(compressionQuality: compressionRatio) else {
logger.internalLog(level: .error, message: "Failed to compress image with ratio: \(compressionRatio).", error: nil, data: nil)
return nil
}

let uuid = idProvider.createId()
switch storageType {
case .data:
return Attachment(name: name, type: attachmentType, size: Int64(imageData.count), id: uuid, bytes: imageData, path: nil)
case .fileStorage:
guard let fileURL = fileManager.saveFile(data: imageData, name: name, folderName: "attachments", directory: .documentDirectory) else {
logger.internalLog(level: .error, message: "Failed to save compressed image to file storage.", error: nil, data: nil)
return nil
}
return Attachment(name: name, type: attachmentType, size: Int64(imageData.count), id: uuid, bytes: nil, path: fileURL.path)
}
}
}
1 change: 1 addition & 0 deletions ios/MeasureSDK/Events/AttachmentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ import Foundation

enum AttachmentType: String, Codable {
case screenshot
case layoutSnapshot
}
Loading

0 comments on commit da367aa

Please sign in to comment.