diff --git a/ios/DemoApp/Podfile.lock b/ios/DemoApp/Podfile.lock index 512a085e7..087c7b657 100644 --- a/ios/DemoApp/Podfile.lock +++ b/ios/DemoApp/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - measure-sh (0.8.1): + - measure-sh (0.9.1): - PLCrashReporter (= 1.12.0) - PLCrashReporter (1.12.0) @@ -15,7 +15,7 @@ EXTERNAL SOURCES: :path: "../../" SPEC CHECKSUMS: - measure-sh: 22611ef94b48051ec62bd0f572976b0718558f06 + measure-sh: 34a0c21e3b908da77bc4cfc02f633413de11e32b PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 PODFILE CHECKSUM: 828d9701ce9dd588cdabe7bc1a8cb829d2a17f97 diff --git a/ios/MeasureSDK.xcodeproj/project.pbxproj b/ios/MeasureSDK.xcodeproj/project.pbxproj index 9df340064..5aa66127d 100644 --- a/ios/MeasureSDK.xcodeproj/project.pbxproj +++ b/ios/MeasureSDK.xcodeproj/project.pbxproj @@ -226,6 +226,7 @@ CF3095292E9D011E002CAC1C /* AttachmentOb+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3095272E9D011E002CAC1C /* AttachmentOb+CoreDataClass.swift */; }; CF30952A2E9D011E002CAC1C /* AttachmentOb+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3095282E9D011E002CAC1C /* AttachmentOb+CoreDataProperties.swift */; }; CF3095422E9D4D0B002CAC1C /* MockAttachmentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3095412E9D4D0B002CAC1C /* MockAttachmentStore.swift */; }; + CF35CE092F559E4D006D7A0D /* SessionAttributeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF35CE082F559E4D006D7A0D /* SessionAttributeProcessor.swift */; }; CF48773F2DEEB26B00311F57 /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48773E2DEEB26B00311F57 /* UIApplication+Extension.swift */; }; CF4877412DEEB2C000311F57 /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF4877402DEEB2C000311F57 /* UIWindow+Extension.swift */; }; CF48E3562DF95E8100FCD26D /* ExceptionGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48E3552DF95E8100FCD26D /* ExceptionGenerator.swift */; }; @@ -571,6 +572,7 @@ CF3095272E9D011E002CAC1C /* AttachmentOb+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentOb+CoreDataClass.swift"; sourceTree = ""; }; CF3095282E9D011E002CAC1C /* AttachmentOb+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentOb+CoreDataProperties.swift"; sourceTree = ""; }; CF3095412E9D4D0B002CAC1C /* MockAttachmentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAttachmentStore.swift; sourceTree = ""; }; + CF35CE082F559E4D006D7A0D /* SessionAttributeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAttributeProcessor.swift; sourceTree = ""; }; CF48773E2DEEB26B00311F57 /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; CF4877402DEEB2C000311F57 /* UIWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extension.swift"; sourceTree = ""; }; CF48E3552DF95E8100FCD26D /* ExceptionGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExceptionGenerator.swift; sourceTree = ""; }; @@ -728,15 +730,16 @@ 526D17A42D5A1913009A2E90 /* Attribute */ = { isa = PBXGroup; children = ( - CF12A68D2E97A761001C8112 /* AttributeValueValidator.swift */, 526D179B2D5A1913009A2E90 /* AppAttributeProcessor.swift */, 526D179C2D5A1913009A2E90 /* Attribute.swift */, 526D179D2D5A1913009A2E90 /* AttributeProcessor.swift */, 526D179E2D5A1913009A2E90 /* AttributeValue.swift */, + CF12A68D2E97A761001C8112 /* AttributeValueValidator.swift */, 526D179F2D5A1913009A2E90 /* ComputeOnceAttributeProcessor.swift */, 526D17A02D5A1913009A2E90 /* DeviceAttributeProcessor.swift */, 526D17A12D5A1913009A2E90 /* InstallationIdAttributeProcessor.swift */, 526D17A22D5A1913009A2E90 /* NetworkStateAttributeProcessor.swift */, + CF35CE082F559E4D006D7A0D /* SessionAttributeProcessor.swift */, 526D17A32D5A1913009A2E90 /* UserAttributeProcessor.swift */, ); path = Attribute; @@ -1697,6 +1700,7 @@ 526D18502D5A1913009A2E90 /* ExceptionDetail.swift in Sources */, 526D182E2D5A1913009A2E90 /* NetworkStateAttributeProcessor.swift in Sources */, 526D18272D5A1913009A2E90 /* AppAttributeProcessor.swift in Sources */, + CF35CE092F559E4D006D7A0D /* SessionAttributeProcessor.swift in Sources */, 526D186F2D5A1913009A2E90 /* NetworkChangeData.swift in Sources */, CFD88D482DACD3710095115E /* SpanStore.swift in Sources */, 526D187D2D5A1913009A2E90 /* UIColor+Extension.swift in Sources */, diff --git a/ios/PublicApi/Measure.swiftinterface b/ios/PublicApi/Measure.swiftinterface index 376b34645..810b2a898 100644 --- a/ios/PublicApi/Measure.swiftinterface +++ b/ios/PublicApi/Measure.swiftinterface @@ -1,7 +1,7 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 clang-1600.0.30.1) -// swift-module-flags: -target arm64-apple-ios12-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -enable-experimental-feature OpaqueTypeErasure -enable-bare-slash-regex -module-name Measure -// swift-module-flags-ignorable: -no-verify-emitted-module-interface +// swift-compiler-version: Apple Swift version 6.2 effective-5.10 (swiftlang-6.2.0.19.9 clang-1700.3.19.1) +// swift-module-flags: -target arm64-apple-ios12-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -module-name Measure +// swift-module-flags-ignorable: -no-verify-emitted-module-interface -formal-cxx-interoperability-mode=off -interface-compiler-version 6.2 import AVKit import CoreData import CoreMotion @@ -37,37 +37,45 @@ extension Measure.AttributeValue : Swift.Codable { public init(from decoder: any Swift.Decoder) throws } public typealias SessionObCoreDataClassSet = Foundation.NSSet -@_inheritsConvenienceInitializers @objc(SessionOb) public class SessionOb : CoreData.NSManagedObject { - @objc override dynamic public init(entity: CoreData.NSEntityDescription, insertInto context: CoreData.NSManagedObjectContext?) +@_inheritsConvenienceInitializers @objc(SessionOb) nonisolated public class SessionOb : CoreData.NSManagedObject { + #if compiler(>=5.3) && $NonescapableTypes + @objc override nonisolated dynamic public init(entity: CoreData.NSEntityDescription, insertInto context: CoreData.NSManagedObjectContext?) + #endif @objc deinit } public typealias SessionObCoreDataPropertiesSet = Foundation.NSSet extension Measure.SessionOb { @nonobjc public class func fetchRequest() -> CoreData.NSFetchRequest - @objc @NSManaged dynamic public var crashed: Swift.Bool { + @objc @NSManaged nonisolated dynamic public var crashed: Swift.Bool { @objc get @objc set } - @objc @NSManaged dynamic public var createdAt: Swift.Int64 { + @objc @NSManaged nonisolated dynamic public var createdAt: Swift.Int64 { @objc get @objc set } - @objc @NSManaged dynamic public var needsReporting: Swift.Bool { + @objc @NSManaged nonisolated dynamic public var needsReporting: Swift.Bool { @objc get @objc set } - @objc @NSManaged dynamic public var pid: Swift.Int32 { + @objc @NSManaged nonisolated dynamic public var pid: Swift.Int32 { @objc get @objc set } - @objc @NSManaged dynamic public var sessionId: Swift.String? { + #if compiler(>=5.3) && $NonescapableTypes + @objc @NSManaged nonisolated dynamic public var sessionId: Swift.String? { @objc get @objc set } + #endif } @objc final public class BaseMeasureConfig : ObjectiveC.NSObject, Swift.Codable { + #if compiler(>=5.3) && $NonescapableTypes public init(enableLogging: Swift.Bool? = nil, autoStart: Swift.Bool? = nil, requestHeadersProvider: (any Measure.MsrRequestHeadersProvider)? = nil, maxDiskUsageInMb: Swift.Int? = nil, enableFullCollectionMode: Swift.Bool? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc convenience public init(enableLogging: Swift.Bool, autoStart: Swift.Bool, requestHeadersProvider: (any Measure.MsrRequestHeadersProvider)?, maxDiskUsageInMb: Foundation.NSNumber?, enableFullCollectionMode: Swift.Bool) + #endif required public init(from decoder: any Swift.Decoder) throws final public func encode(to encoder: any Swift.Encoder) throws @objc deinit @@ -75,7 +83,9 @@ extension Measure.SessionOb { public protocol Span { var traceId: Swift.String { get } var spanId: Swift.String { get } + #if compiler(>=5.3) && $NonescapableTypes var parentId: Swift.String? { get } + #endif var isSampled: Swift.Bool { get } @discardableResult func setStatus(_ status: Measure.SpanStatus) -> any Measure.Span @@ -108,7 +118,9 @@ public enum SpanStatus : Swift.Int64, Swift.Codable { case unset case ok case error + #if compiler(>=5.3) && $NonescapableTypes public init?(rawValue: Swift.Int64) + #endif public typealias RawValue = Swift.Int64 public var rawValue: Swift.Int64 { get @@ -127,7 +139,9 @@ public protocol SpanBuilder { final public let galleryButton: Swift.String final public let exitScreenshotMode: Swift.String public init(reportBugTitle: Swift.String, descriptionPlaceholder: Swift.String, sendButton: Swift.String, screenshotButton: Swift.String, galleryButton: Swift.String, exitScreenshotMode: Swift.String) + #if compiler(>=5.3) && $NonescapableTypes public func update(reportBugTitle: Swift.String? = nil, descriptionPlaceholder: Swift.String? = nil, sendButton: Swift.String? = nil, screenshotButton: Swift.String? = nil, galleryButton: Swift.String? = nil, exitScreenshotMode: Swift.String? = nil) -> Measure.MsrText + #endif @objc deinit } @objc final public class ClientInfo : ObjectiveC.NSObject, Swift.Codable { @@ -141,7 +155,9 @@ public protocol SpanBuilder { final public let button: UIKit.UIFont final public let placeholder: UIKit.UIFont public init(title: UIKit.UIFont, button: UIKit.UIFont, placeholder: UIKit.UIFont) + #if compiler(>=5.3) && $NonescapableTypes public func update(title: UIKit.UIFont? = nil, button: UIKit.UIFont? = nil, placeholder: UIKit.UIFont? = nil) -> Measure.MsrFonts + #endif @objc deinit } @_inheritsConvenienceInitializers @objc(LifecycleManagerInternal) public class LifecycleManagerInternal : ObjectiveC.NSObject { @@ -155,7 +171,9 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl case allText case allTextExceptClickable case sensitiveFieldsOnly + #if compiler(>=5.3) && $NonescapableTypes public init?(rawValue: Swift.String) + #endif public typealias AllCases = [Measure.ScreenshotMaskLevel] public typealias RawValue = Swift.String nonisolated public static var allCases: [Measure.ScreenshotMaskLevel] { @@ -170,7 +188,9 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl case allText case allTextExceptClickable case sensitiveFieldsOnly + #if compiler(>=5.3) && $NonescapableTypes public init?(rawValue: Swift.Int) + #endif public typealias RawValue = Swift.Int public var rawValue: Swift.Int { get @@ -225,7 +245,9 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl get } public init(darkBackground: UIKit.UIColor, lightBackground: UIKit.UIColor, darkButtonBackground: UIKit.UIColor, lightButtonBackground: UIKit.UIColor, darkText: UIKit.UIColor, lightText: UIKit.UIColor, darkPlaceholder: UIKit.UIColor, lightPlaceholder: UIKit.UIColor, darkFloatingButtonBackground: UIKit.UIColor, lightFloatingButtonBackground: UIKit.UIColor, darkFloatingButtonIcon: UIKit.UIColor, lightFloatingButtonIcon: UIKit.UIColor, darkfloatingExitButtonText: UIKit.UIColor, lightfloatingExitButtonText: UIKit.UIColor, badgeColor: UIKit.UIColor, badgeTextColor: UIKit.UIColor, isDarkMode: Swift.Bool) + #if compiler(>=5.3) && $NonescapableTypes public func update(darkBackground: UIKit.UIColor? = nil, lightBackground: UIKit.UIColor? = nil, darkButtonBackground: UIKit.UIColor? = nil, lightButtonBackground: UIKit.UIColor? = nil, darkText: UIKit.UIColor? = nil, lightText: UIKit.UIColor? = nil, darkPlaceholder: UIKit.UIColor? = nil, lightPlaceholder: UIKit.UIColor? = nil, darkFloatingButtonBackground: UIKit.UIColor? = nil, lightFloatingButtonBackground: UIKit.UIColor? = nil, darkFloatingButtonIcon: UIKit.UIColor? = nil, lightFloatingButtonIcon: UIKit.UIColor? = nil, darkfloatingExitButtonText: UIKit.UIColor? = nil, lightfloatingExitButtonText: UIKit.UIColor? = nil, badgeColor: UIKit.UIColor? = nil, badgeTextColor: UIKit.UIColor? = nil, isDarkMode: Swift.Bool? = nil) -> Measure.MsrColors + #endif @objc deinit } @objc public protocol MsrRequestHeadersProvider : ObjectiveC.NSObjectProtocol { @@ -242,7 +264,9 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl case initWithCoder = 7 case loadView = 8 case vcDeinit = 9 + #if compiler(>=5.3) && $NonescapableTypes public init?(rawValue: Swift.Int) + #endif public typealias RawValue = Swift.Int public var rawValue: Swift.Int { get @@ -255,7 +279,9 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl public var path: Swift.String? public var size: Swift.Int64 public var id: Swift.String + #if compiler(>=5.3) && $NonescapableTypes public init(name: Swift.String, type: Measure.AttachmentType, size: Swift.Int64, id: Swift.String, bytes: Foundation.Data? = nil, path: Swift.String? = nil) + #endif @objc deinit public func encode(to encoder: any Swift.Encoder) throws required public init(from decoder: any Swift.Decoder) throws @@ -263,12 +289,16 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl @objc public class MsrDimensions : ObjectiveC.NSObject { final public let topPadding: CoreFoundation.CGFloat public init(topPadding: CoreFoundation.CGFloat) + #if compiler(>=5.3) && $NonescapableTypes public func update(topPadding: CoreFoundation.CGFloat? = nil) -> Measure.MsrDimensions + #endif @objc deinit } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) @_Concurrency.MainActor @preconcurrency public struct MsrMoniterView : SwiftUICore.View where Content : SwiftUICore.View { + #if compiler(>=5.3) && $NonescapableTypes @_Concurrency.MainActor @preconcurrency public init(_ viewName: Swift.String? = nil, content: @escaping () -> Content) + #endif @_Concurrency.MainActor @preconcurrency public var body: some SwiftUICore.View { get } @@ -277,43 +307,87 @@ public enum ScreenshotMaskLevel : Swift.String, Swift.Codable, Swift.CaseIterabl } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) extension SwiftUICore.View { + #if compiler(>=5.3) && $NonescapableTypes @_Concurrency.MainActor @preconcurrency public func moniterWithMsr(_ viewName: Swift.String? = nil) -> some SwiftUICore.View + #endif } @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @objc final public class Measure : ObjectiveC.NSObject { @objc deinit } extension Measure.Measure { + #if compiler(>=5.3) && $NonescapableTypes @objc public static func initialize(with client: Measure.ClientInfo, config: Measure.BaseMeasureConfig? = nil) + #endif @objc public static func start() @objc public static func stop() + #if compiler(>=5.3) && $NonescapableTypes @objc public static func getSessionId() -> Swift.String? + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func internalTrackEvent(data: inout [Swift.String : Any?], type: Swift.String, timestamp: Swift.Int64, attributes: [Swift.String : Any?], userDefinedAttrs: [Swift.String : Measure.AttributeValue], userTriggered: Swift.Bool, sessionId: Swift.String?, threadName: Swift.String?, attachments: [Measure.MsrAttachment]) + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func internalTrackSpan(name: Swift.String, traceId: Swift.String, spanId: Swift.String, parentId: Swift.String?, startTime: Swift.Int64, endTime: Swift.Int64, duration: Swift.Int64, status: Swift.Int64, attributes: [Swift.String : Any?], userDefinedAttrs: [Swift.String : Measure.AttributeValue], checkpoints: [Swift.String : Swift.Int64], hasEnded: Swift.Bool, isSampled: Swift.Bool) + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func trackEvent(name: Swift.String, attributes: [Swift.String : Measure.AttributeValue], timestamp: Swift.Int64? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func trackEvent(_ name: Swift.String, attributes: [Swift.String : Any], timestamp: Foundation.NSNumber? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func trackScreenView(_ screenName: Swift.String, attributes: [Swift.String : Measure.AttributeValue]?) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func trackScreenView(_ screenName: Swift.String, attributes: [Swift.String : Any]?) + #endif @objc public static func setUserId(_ userId: Swift.String) @objc public static func clearUserId() @objc public static func getCurrentTime() -> Swift.Int64 public static func startSpan(name: Swift.String) -> any Measure.Span public static func startSpan(name: Swift.String, timestamp: Swift.Int64) -> any Measure.Span + #if compiler(>=5.3) && $NonescapableTypes public static func createSpanBuilder(name: Swift.String) -> (any Measure.SpanBuilder)? + #endif public static func getTraceParentHeaderValue(span: any Measure.Span) -> Swift.String public static func getTraceParentHeaderKey() -> Swift.String + #if compiler(>=5.3) && $NonescapableTypes public static func launchBugReport(takeScreenshot: Swift.Bool = true, bugReportConfig: Measure.BugReportConfig = .default, attributes: [Swift.String : Measure.AttributeValue]? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func launchBugReport(takeScreenshot: Swift.Bool = true, bugReportConfig: Measure.BugReportConfig = .default, attributes: [Swift.String : Any]? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func onShake(_ handler: (() -> Swift.Void)?) + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func trackBugReport(description: Swift.String, attachments: [Measure.MsrAttachment] = [], attributes: [Swift.String : Measure.AttributeValue]? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func trackBugReport(description: Swift.String, attachments: [Measure.MsrAttachment] = [], attributes: [Swift.String : Any]? = nil) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func captureScreenshot(for viewController: UIKit.UIViewController, completion: @escaping (Measure.MsrAttachment?) -> Swift.Void) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func captureLayoutSnapshot(for viewController: UIKit.UIViewController, completion: @escaping (Measure.MsrAttachment?) -> Swift.Void) + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func trackError(_ error: any Swift.Error, attributes: [Swift.String : Measure.AttributeValue]? = nil, collectStackTraces: Swift.Bool = false) + #endif + #if compiler(>=5.3) && $NonescapableTypes @objc public static func trackError(_ error: Foundation.NSError, attributes: [Swift.String : Any]? = nil, collectStackTraces: Swift.Bool = false) + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func internalGetAttachmentDirectory() -> Swift.String? + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func internalGetDynamicConfigPath() -> Swift.String? + #endif + #if compiler(>=5.3) && $NonescapableTypes public static func trackHttpEvent(url: Swift.String, method: Swift.String, startTime: Swift.UInt64, endTime: Swift.UInt64, client: Swift.String = "unknown", statusCode: Swift.Int? = nil, error: (any Swift.Error)? = nil, requestHeaders: [Swift.String : Swift.String]? = nil, responseHeaders: [Swift.String : Swift.String]? = nil, requestBody: Swift.String? = nil, responseBody: Swift.String? = nil) + #endif } @_inheritsConvenienceInitializers @objc public class MSRNetworkInterceptor : ObjectiveC.NSObject { @objc(enableOn:) public static func enable(on sessionConfiguration: Foundation.URLSessionConfiguration) @@ -324,7 +398,9 @@ public enum AttachmentType : Swift.String, Swift.Codable { case screenshot case layoutSnapshot case layoutSnapshotJson + #if compiler(>=5.3) && $NonescapableTypes public init?(rawValue: Swift.String) + #endif public typealias RawValue = Swift.String public var rawValue: Swift.String { get @@ -333,8 +409,12 @@ public enum AttachmentType : Swift.String, Swift.Codable { @objc @_inheritsConvenienceInitializers @_Concurrency.MainActor @preconcurrency open class MsrViewController : UIKit.UIViewController { @_Concurrency.MainActor @preconcurrency @objc override dynamic open func loadView() @objc deinit + #if compiler(>=5.3) && $NonescapableTypes @_Concurrency.MainActor @preconcurrency @objc override dynamic public init(nibName nibNameOrNil: Swift.String?, bundle nibBundleOrNil: Foundation.Bundle?) + #endif + #if compiler(>=5.3) && $NonescapableTypes @_Concurrency.MainActor @preconcurrency @objc required dynamic public init?(coder: Foundation.NSCoder) + #endif } extension Measure.SpanStatus : Swift.Equatable {} extension Measure.SpanStatus : Swift.Hashable {} diff --git a/ios/Sources/MeasureSDK/Swift/Attribute/Attribute.swift b/ios/Sources/MeasureSDK/Swift/Attribute/Attribute.swift index 04bf6c2b6..e093c4d40 100644 --- a/ios/Sources/MeasureSDK/Swift/Attribute/Attribute.swift +++ b/ios/Sources/MeasureSDK/Swift/Attribute/Attribute.swift @@ -57,6 +57,7 @@ class Attributes: Codable { var deviceThermalThrottlingEnabled: Bool? var deviceLowPowerMode: Bool? var osPageSize: UInt8? + var sessionStartTime: Number? enum CodingKeys: String, CodingKey { case threadName = "thread_name" @@ -87,6 +88,7 @@ class Attributes: Codable { case osPageSize = "os_page_size" case deviceThermalThrottlingEnabled = "device_thermal_throttling_enabled" case deviceLowPowerMode = "device_low_power_mode" + case sessionStartTime = "session_start_time" } init( @@ -117,7 +119,8 @@ class Attributes: Codable { appUniqueId: String = "", deviceThermalThrottlingEnabled: Bool? = nil, deviceLowPowerMode: Bool? = nil, - osPageSize: UInt8? = nil) { + osPageSize: UInt8? = nil, + sessionStartTime: Number? = nil) { self.threadName = threadName self.deviceName = deviceName self.deviceModel = deviceModel @@ -146,6 +149,7 @@ class Attributes: Codable { self.deviceThermalThrottlingEnabled = deviceThermalThrottlingEnabled self.deviceLowPowerMode = deviceLowPowerMode self.osPageSize = osPageSize + self.sessionStartTime = sessionStartTime } init(dict: [String: Any?]) { @@ -177,5 +181,6 @@ class Attributes: Codable { self.deviceThermalThrottlingEnabled = dict["device_thermal_throttling_enabled"] as? Bool self.deviceLowPowerMode = dict["device_low_power_mode"] as? Bool self.osPageSize = dict["os_page_size"] as? UInt8 + self.sessionStartTime = dict["session_start_time"] as? Number } } diff --git a/ios/Sources/MeasureSDK/Swift/Attribute/SessionAttributeProcessor.swift b/ios/Sources/MeasureSDK/Swift/Attribute/SessionAttributeProcessor.swift new file mode 100644 index 000000000..f9deca383 --- /dev/null +++ b/ios/Sources/MeasureSDK/Swift/Attribute/SessionAttributeProcessor.swift @@ -0,0 +1,20 @@ +// +// SessionAttributeProcessor.swift +// Measure +// +// Created by Adwin Ross on 02/03/26. +// + +import Foundation + +final class SessionAttributeProcessor: AttributeProcessor { + private let sessionManager: SessionManager + + init(sessionManager: SessionManager) { + self.sessionManager = sessionManager + } + + func appendAttributes(_ attribute: Attributes) { + attribute.sessionStartTime = sessionManager.getSessionStartTime() + } +} diff --git a/ios/Sources/MeasureSDK/Swift/MeasureInitializer.swift b/ios/Sources/MeasureSDK/Swift/MeasureInitializer.swift index a6f104b68..88a16016c 100644 --- a/ios/Sources/MeasureSDK/Swift/MeasureInitializer.swift +++ b/ios/Sources/MeasureSDK/Swift/MeasureInitializer.swift @@ -23,6 +23,7 @@ protocol MeasureInitializer { var installationIdAttributeProcessor: InstallationIdAttributeProcessor { get } var networkStateAttributeProcessor: NetworkStateAttributeProcessor { get } var userAttributeProcessor: UserAttributeProcessor { get } + var sessionAttributeProcessor: SessionAttributeProcessor { get } var attributeProcessors: [AttributeProcessor] { get } var signalProcessor: SignalProcessor { get } var crashReportManager: CrashReportManager { get } @@ -92,6 +93,7 @@ protocol MeasureInitializer { /// - `installationIdAttributeProcessor`: `InstallationIdAttributeProcessor` object used to process installation_id. /// - `networkStateAttributeProcessor`: `NetworkStateAttributeProcessor` object used to process network info. /// - `userAttributeProcessor`: `UserAttributeProcessor` object used to process user_id. +/// - `sessionAttributeProcessor`: `SessionAttributeProcessor` object responsible for adding session start time. /// - `attributeProcessors`: An array containing all the `AttributeProcessor`. /// - `signalProcessor`: `SignalProcessor` object used to track events, traces and spans. /// - `crashReportManager`: `CrashReportManager` object used to manage crash reports. @@ -157,6 +159,7 @@ final class BaseMeasureInitializer: MeasureInitializer { let installationIdAttributeProcessor: InstallationIdAttributeProcessor let networkStateAttributeProcessor: NetworkStateAttributeProcessor let userAttributeProcessor: UserAttributeProcessor + let sessionAttributeProcessor: SessionAttributeProcessor let attributeProcessors: [AttributeProcessor] let signalProcessor: SignalProcessor let crashReportManager: CrashReportManager @@ -271,11 +274,13 @@ final class BaseMeasureInitializer: MeasureInitializer { self.networkStateAttributeProcessor = NetworkStateAttributeProcessor(measureDispatchQueue: measureDispatchQueue) self.userAttributeProcessor = UserAttributeProcessor(userDefaultStorage: userDefaultStorage, measureDispatchQueue: measureDispatchQueue) + self.sessionAttributeProcessor = SessionAttributeProcessor(sessionManager: sessionManager) self.attributeProcessors = [appAttributeProcessor, deviceAttributeProcessor, installationIdAttributeProcessor, networkStateAttributeProcessor, - userAttributeProcessor] + userAttributeProcessor, + sessionAttributeProcessor] self.crashDataPersistence = BaseCrashDataPersistence(logger: logger, systemFileManager: systemFileManager) CrashDataWriter.shared.setCrashDataPersistence(crashDataPersistence) diff --git a/ios/Sources/MeasureSDK/Swift/MeasureInternal.swift b/ios/Sources/MeasureSDK/Swift/MeasureInternal.swift index 1a9954f4a..19f3e8d8b 100644 --- a/ios/Sources/MeasureSDK/Swift/MeasureInternal.swift +++ b/ios/Sources/MeasureSDK/Swift/MeasureInternal.swift @@ -169,9 +169,13 @@ final class MeasureInternal { // swiftlint:disable:this type_body_length self.lifecycleObserver.applicationWillResignActive = applicationWillResignActive self.logger.log(level: .info, message: "Initializing Measure SDK", error: nil, data: nil) self.sessionManager.setPreviousSessionCrashed(crashReportManager.hasPendingCrashReport) - self.sessionManager.start { sessionId in - self.trackSessionStart(sessionId: sessionId) + self.sessionManager.setOnSessionStarted { [weak self] sessionId in + guard let self else { return } + if let timestamp = self.sessionManager.getSessionStartTime() { + self.trackSessionStart(sessionId, timestamp: timestamp) + } } + self.sessionManager.start() self.crashDataPersistence.prepareCrashFile() self.crashDataPersistence.sessionId = sessionManager.sessionId self.crashReportManager.trackException() @@ -453,15 +457,15 @@ final class MeasureInternal { // swiftlint:disable:this type_body_length return transformedAttributes } - private func trackSessionStart(sessionId: String?) { - signalProcessor.track(data: SessionStartData(), - timestamp: timeProvider.now(), - type: .sessionStart, - attributes: nil, - sessionId: sessionId, - attachments: nil, - userDefinedAttributes: nil, - threadName: nil, - needsReporting: true) + private func trackSessionStart(_ sessionId: String, timestamp: Number) { + self.signalProcessor.track(data: SessionStartData(), + timestamp: timestamp, + type: .sessionStart, + attributes: nil, + sessionId: sessionId, + attachments: nil, + userDefinedAttributes: nil, + threadName: nil, + needsReporting: true) } } diff --git a/ios/Sources/MeasureSDK/Swift/SessionManager.swift b/ios/Sources/MeasureSDK/Swift/SessionManager.swift index 23a0c875f..1a67fd17b 100644 --- a/ios/Sources/MeasureSDK/Swift/SessionManager.swift +++ b/ios/Sources/MeasureSDK/Swift/SessionManager.swift @@ -11,7 +11,7 @@ import Foundation protocol SessionManager { var sessionId: String { get } var shouldReportJourneyEvents: Bool { get } - func start(onNewSession: (String?) -> Void) + func start() func applicationDidEnterBackground() func applicationWillEnterForeground() func applicationWillTerminate() @@ -19,6 +19,8 @@ protocol SessionManager { func setPreviousSessionCrashed(_ crashed: Bool) func markCurrentSessionAsCrashed() func onConfigLoaded() + func getSessionStartTime() -> Number? + func setOnSessionStarted(_ callback: ((String) -> Void)?) } /// `BaseSessionManager` is responsible for creating and managing sessions within the Measure SDK. @@ -38,8 +40,10 @@ final class BaseSessionManager: SessionManager { private var previousSessionCrashed = false private let versionCode: String private let signalSampler: SignalSampler + private var sessionStartTime: Number? + private var onSessionStarted: ((String) -> Void)? var shouldReportJourneyEvents: Bool - + /// The current session ID. var sessionId: String { if let id = currentSessionId { @@ -49,7 +53,7 @@ final class BaseSessionManager: SessionManager { return "" } } - + init(idProvider: IdProvider, logger: Logger, timeProvider: TimeProvider, @@ -72,6 +76,10 @@ final class BaseSessionManager: SessionManager { self.signalSampler = signalSampler } + func setOnSessionStarted(_ callback: ((String) -> Void)?) { + self.onSessionStarted = callback + } + private func createNewSession() { currentSessionId = idProvider.uuid() logger.log(level: .info, message: "New session created: \(currentSessionId ?? "nil")", error: nil, data: nil) @@ -85,11 +93,11 @@ final class BaseSessionManager: SessionManager { createdAt: session.createdAt, versionCode: versionCode) userDefaultStorage.setRecentSession(recentSession) + trackSessionStart(sessionId: currentSessionId) } - func start(onNewSession: (String?) -> Void) { + func start() { createNewSession() - onNewSession(currentSessionId) } func applicationDidEnterBackground() { @@ -158,6 +166,10 @@ final class BaseSessionManager: SessionManager { self.shouldReportJourneyEvents = true } + func getSessionStartTime() -> Number? { + return sessionStartTime + } + private func shouldEndSession() -> Bool { let durationInBackground = timeProvider.millisTime - appBackgroundTimeMs @@ -168,4 +180,11 @@ final class BaseSessionManager: SessionManager { return false } + + private func trackSessionStart(sessionId: String?) { + sessionStartTime = timeProvider.now() + if let onSessionStartedCallback = onSessionStarted, let sessionId = sessionId { + onSessionStartedCallback(sessionId) + } + } } diff --git a/ios/Tests/MeasureSDKTests/Mocks/MockMeasureInitializer.swift b/ios/Tests/MeasureSDKTests/Mocks/MockMeasureInitializer.swift index d1f1c1435..d049120b3 100644 --- a/ios/Tests/MeasureSDKTests/Mocks/MockMeasureInitializer.swift +++ b/ios/Tests/MeasureSDKTests/Mocks/MockMeasureInitializer.swift @@ -8,8 +8,7 @@ import Foundation @testable import Measure -final class MockMeasureInitializer: MeasureInitializer { - // swiftlint:disable:this type_body_length +final class MockMeasureInitializer: MeasureInitializer { // swiftlint:disable:this type_body_length let configLoader: ConfigLoader let signalSampler: SignalSampler let configProvider: ConfigProvider @@ -24,6 +23,7 @@ final class MockMeasureInitializer: MeasureInitializer { let installationIdAttributeProcessor: InstallationIdAttributeProcessor let networkStateAttributeProcessor: NetworkStateAttributeProcessor let userAttributeProcessor: UserAttributeProcessor + let sessionAttributeProcessor: SessionAttributeProcessor let attributeProcessors: [AttributeProcessor] let signalProcessor: SignalProcessor let crashReportManager: CrashReportManager @@ -122,6 +122,7 @@ final class MockMeasureInitializer: MeasureInitializer { deviceAttributeProcessor: DeviceAttributeProcessor? = nil, installationIdAttributeProcessor: InstallationIdAttributeProcessor? = nil, networkStateAttributeProcessor: NetworkStateAttributeProcessor? = nil, + sessionAttributeProcessor: SessionAttributeProcessor? = nil, userAttributeProcessor: UserAttributeProcessor? = nil, randomizer: Randomizer? = nil, spanProcessor: SpanProcessor? = nil, @@ -188,12 +189,14 @@ final class MockMeasureInitializer: MeasureInitializer { self.networkStateAttributeProcessor = networkStateAttributeProcessor ?? NetworkStateAttributeProcessor(measureDispatchQueue: self.measureDispatchQueue) self.userAttributeProcessor = userAttributeProcessor ?? UserAttributeProcessor(userDefaultStorage: self.userDefaultStorage, measureDispatchQueue: self.measureDispatchQueue) + self.sessionAttributeProcessor = sessionAttributeProcessor ?? SessionAttributeProcessor(sessionManager: self.sessionManager) self.attributeProcessors = [ self.appAttributeProcessor, self.deviceAttributeProcessor, self.installationIdAttributeProcessor, self.networkStateAttributeProcessor, - self.userAttributeProcessor + self.userAttributeProcessor, + self.sessionAttributeProcessor ] self.crashDataPersistence = crashDataPersistence ?? BaseCrashDataPersistence(logger: logger ?? MockLogger(), systemFileManager: self.systemFileManager) @@ -208,7 +211,6 @@ final class MockMeasureInitializer: MeasureInitializer { timeProvider: self.timeProvider, attachmentProcessor: self.attachmentProcessor, svgGenerator: self.svgGenerator) - self.attributeValueValidator = attributeValueValidator ?? BaseAttributeValueValidator(configProvider: self.configProvider, logger: self.logger) self.signalProcessor = signalProcessor ?? BaseSignalProcessor(logger: self.logger, diff --git a/ios/Tests/MeasureSDKTests/Mocks/MockSessionManager.swift b/ios/Tests/MeasureSDKTests/Mocks/MockSessionManager.swift index 615705ce5..745257a02 100644 --- a/ios/Tests/MeasureSDKTests/Mocks/MockSessionManager.swift +++ b/ios/Tests/MeasureSDKTests/Mocks/MockSessionManager.swift @@ -12,6 +12,7 @@ final class MockSessionManager: SessionManager { var sessionId: String var shouldReportJourneyEvents: Bool private(set) var startCalled = false + private(set) var signalProcessor: SignalProcessor? private(set) var applicationDidEnterBackgroundCalled = false private(set) var applicationWillEnterForegroundCalled = false private(set) var applicationWillTerminateCalled = false @@ -21,16 +22,20 @@ final class MockSessionManager: SessionManager { private(set) var isCrashed = false private(set) var onConfigLoadedCalled = false private(set) var trackedEvent: Any? + var sessionStartTime: Number? + var onSessionStartedCallback: ((String) -> Void)? init(sessionId: String = "mock-session-id", shouldReportJourneyEvents: Bool = true) { self.sessionId = sessionId self.shouldReportJourneyEvents = shouldReportJourneyEvents } - func start(onNewSession: (String?) -> Void) { + func start() { startCalled = true + } - onNewSession(sessionId) + func getSessionStartTime() -> Number? { + return sessionStartTime } func applicationDidEnterBackground() { @@ -62,4 +67,8 @@ final class MockSessionManager: SessionManager { func onConfigLoaded() { onConfigLoadedCalled = true } + + func setOnSessionStarted(_ callback: ((String) -> Void)?) { + onSessionStartedCallback = callback + } } diff --git a/ios/Tests/MeasureSDKTests/SessionManagerTests.swift b/ios/Tests/MeasureSDKTests/SessionManagerTests.swift index 9788a6f24..acd51a2b3 100644 --- a/ios/Tests/MeasureSDKTests/SessionManagerTests.swift +++ b/ios/Tests/MeasureSDKTests/SessionManagerTests.swift @@ -17,6 +17,7 @@ final class SessionManagerTests: XCTestCase { var timeProvider: MockTimeProvider! var sessionStore: SessionStore! var userDefaultStorage: MockUserDefaultStorage! + var signalProcessor: MockSignalProcessor! override func setUp() { super.setUp() @@ -32,6 +33,7 @@ final class SessionManagerTests: XCTestCase { maxAttachmentSizeInEventsBatchInBytes: 30000, maxEventsInBatch: 500) randomizer = MockRandomizer() + signalProcessor = MockSignalProcessor() randomizer.randomFloat = 0.5 sessionStore = BaseSessionStore(coreDataManager: MockCoreDataManager(), logger: logger) @@ -45,6 +47,17 @@ final class SessionManagerTests: XCTestCase { versionCode: "1.0.0", signalSampler: BaseSignalSampler(configProvider: configProvider, randomizer: randomizer)) + sessionManager.setOnSessionStarted { sessionId in + self.signalProcessor.track(data: SessionStartData(), + timestamp: self.sessionManager.getSessionStartTime()!, + type: .sessionStart, + attributes: nil, + sessionId: sessionId, + attachments: nil, + userDefinedAttributes: nil, + threadName: nil, + needsReporting: true) + } userDefaultStorage.setRecentAppVersion("1.0.0") userDefaultStorage.setRecentBuildNumber("1") } @@ -58,6 +71,7 @@ final class SessionManagerTests: XCTestCase { timeProvider = nil userDefaultStorage = nil sessionStore = nil + signalProcessor = nil super.tearDown() } @@ -67,7 +81,7 @@ final class SessionManagerTests: XCTestCase { } func testSessionStart() { - sessionManager.start() { _ in } + sessionManager.start() XCTAssertEqual(sessionManager.sessionId, "test-session-id-1", "Expected session ID to be 'test-session-id-1' after initialisation.") } @@ -80,13 +94,13 @@ final class SessionManagerTests: XCTestCase { timeProvider.current = lastEventTime + 1000 configProvider.sessionBackgroundTimeoutThresholdMs = 10000 - sessionManager.start() { _ in } + sessionManager.start() XCTAssertEqual(sessionManager.sessionId, "new-session-id", "Expected a new session to be created when the framework version is updated.") } func testSessionContinues_WhenEnteringForegroundBeforeThreshold() { timeProvider.millisTime = 1000 - sessionManager.start() { _ in } + sessionManager.start() sessionManager.applicationDidEnterBackground() // simulate time passage @@ -100,7 +114,7 @@ final class SessionManagerTests: XCTestCase { func testNewSessionCreated_WhenEnteringForegroundAfterThreshold() { timeProvider.millisTime = 1000 - sessionManager.start() { _ in } + sessionManager.start() sessionManager.applicationDidEnterBackground() // simulate time passage @@ -113,7 +127,7 @@ final class SessionManagerTests: XCTestCase { } func testSessionStore() { - sessionManager.start { _ in } + sessionManager.start() let sessions = sessionStore.getAllSessions() XCTAssertEqual(sessions.count, 1, "Expected 1 session in session store.") @@ -124,7 +138,7 @@ final class SessionManagerTests: XCTestCase { idProvider.uuId = expectedSessionId userDefaultStorage.recentSession = nil - sessionManager.start() { _ in } + sessionManager.start() let sessionId = sessionManager.sessionId XCTAssertEqual(sessionId, expectedSessionId, "Expected a new session to be created.") @@ -139,7 +153,7 @@ final class SessionManagerTests: XCTestCase { timeProvider.current = lastEventTime + 5000 configProvider.sessionBackgroundTimeoutThresholdMs = 1000 - sessionManager.start() { _ in } + sessionManager.start() let sessionId = sessionManager.sessionId XCTAssertEqual(sessionId, expectedSessionId, "Expected a new session to be created after the threshold time.") @@ -149,7 +163,7 @@ final class SessionManagerTests: XCTestCase { userDefaultStorage.setRecentAppVersion("1.0.1") let newSessionId = "new-session-id" idProvider.uuId = newSessionId - sessionManager.start() { _ in } + sessionManager.start() XCTAssertEqual(sessionManager.sessionId, newSessionId, "Expected a new session to be created after the previous session crashed, even within the threshold time.") } @@ -158,7 +172,7 @@ final class SessionManagerTests: XCTestCase { userDefaultStorage.setRecentBuildNumber("2") let newSessionId = "new-session-id" idProvider.uuId = newSessionId - sessionManager.start() { _ in } + sessionManager.start() XCTAssertEqual(sessionManager.sessionId, newSessionId, "Expected a new session to be created after the previous session crashed, even within the threshold time.") } @@ -167,15 +181,52 @@ final class SessionManagerTests: XCTestCase { configProvider.sessionBackgroundTimeoutThresholdMs = 100000 let sessionCreatedAt: Int64 = 1000 timeProvider.current = sessionCreatedAt - sessionManager.start() { _ in } + sessionManager.start() let newSessionId = "new-session-id" idProvider.uuId = newSessionId let lastEventTime: Int64 = sessionCreatedAt + 1000 timeProvider.current = lastEventTime + 1000 sessionManager.setPreviousSessionCrashed(true) - sessionManager.start() { _ in } + sessionManager.start() XCTAssertEqual(sessionManager.sessionId, newSessionId, "Expected a new session to be created after the previous session crashed, even within the threshold time.") } + + func testSessionStart_TracksSessionStartEvent() { + sessionManager.start() + + XCTAssertEqual(signalProcessor.type, .sessionStart, "Expected a sessionStart event to be tracked on start.") + XCTAssertEqual(signalProcessor.sessionId, "test-session-id-1", "Expected tracked event to have the correct session ID.") + } + + func testSessionForeground_TracksSessionStartEvent_AfterThreshold() { + timeProvider.millisTime = 1000 + sessionManager.start() + sessionManager.applicationDidEnterBackground() + + timeProvider.millisTime += configProvider.sessionBackgroundTimeoutThresholdMs + 1 + idProvider.uuId = "new-session-id-after-bg" + sessionManager.applicationWillEnterForeground() + + XCTAssertEqual(sessionManager.sessionId, "new-session-id-after-bg") + XCTAssertEqual(signalProcessor.type, .sessionStart, "Expected sessionStart event for the new session after backgrounding.") + XCTAssertEqual(signalProcessor.sessionId, "new-session-id-after-bg") + } + + func testSessionStart_DoesNotTrackEvent_IfProcessorNotSet() { + let managerWithoutProcessor = BaseSessionManager(idProvider: idProvider, + logger: logger, + timeProvider: timeProvider, + configProvider: configProvider, + sessionStore: sessionStore, + eventStore: MockEventStore(), + userDefaultStorage: userDefaultStorage, + versionCode: "1.0.0", + signalSampler: BaseSignalSampler(configProvider: configProvider, + randomizer: randomizer)) + + managerWithoutProcessor.start() + XCTAssertNil(signalProcessor.type, "No event should be tracked if processor is nil.") + } }