From b1f64d9174dea5853c1f794a1211332942d38854 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 10 Jun 2024 17:04:28 +0200 Subject: [PATCH 001/110] RUM-1660 Enhance RUM session debugging by adding more options to Example app --- .../DebugRUMSessionViewController.swift | 60 ++++++++++++++----- .../Example/Debugging/Helpers/SwiftUI.swift | 6 +- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Datadog/Example/Debugging/DebugRUMSessionViewController.swift b/Datadog/Example/Debugging/DebugRUMSessionViewController.swift index 5a6e81ff9a..73cb64e9e4 100644 --- a/Datadog/Example/Debugging/DebugRUMSessionViewController.swift +++ b/Datadog/Example/Debugging/DebugRUMSessionViewController.swift @@ -33,7 +33,10 @@ private class DebugRUMSessionViewModel: ObservableObject { var id: UUID = UUID() } - @Published var sessionItems: [SessionItem] = [] + @Published var sessionItems: [SessionItem] = [] { + didSet { updateSessionID() } + } + @Published var sessionID: String = "" @Published var viewKey: String = "" @Published var actionName: String = "" @@ -46,12 +49,18 @@ private class DebugRUMSessionViewModel: ObservableObject { var urlSessions: [URLSession] = [] + init() { + updateSessionID() + } + func startView() { guard !viewKey.isEmpty else { return } let key = viewKey + RUMMonitor.shared().startView(key: key) + sessionItems.append( SessionItem( label: key, @@ -67,7 +76,6 @@ private class DebugRUMSessionViewModel: ObservableObject { ) ) - RUMMonitor.shared().startView(key: key) self.viewKey = "" } @@ -76,11 +84,11 @@ private class DebugRUMSessionViewModel: ObservableObject { return } + RUMMonitor.shared().addAction(type: .custom, name: actionName) sessionItems.append( SessionItem(label: actionName, type: .action, isPending: false, stopAction: nil) ) - RUMMonitor.shared().addAction(type: .custom, name: actionName) self.actionName = "" } @@ -89,11 +97,11 @@ private class DebugRUMSessionViewModel: ObservableObject { return } + RUMMonitor.shared().addError(message: errorMessage) sessionItems.append( SessionItem(label: errorMessage, type: .error, isPending: false, stopAction: nil) ) - RUMMonitor.shared().addError(message: errorMessage) self.errorMessage = "" } @@ -103,6 +111,7 @@ private class DebugRUMSessionViewModel: ObservableObject { } let key = self.resourceKey + RUMMonitor.shared().startResource(resourceKey: key, url: mockURL()) sessionItems.append( SessionItem( label: key, @@ -118,7 +127,6 @@ private class DebugRUMSessionViewModel: ObservableObject { ) ) - RUMMonitor.shared().startResource(resourceKey: key, url: mockURL()) self.resourceKey = "" } @@ -161,6 +169,11 @@ private class DebugRUMSessionViewModel: ObservableObject { urlSessions.append(session) // keep session } + func stopSession() { + RUMMonitor.shared().stopSession() + sessionItems = [] + } + // MARK: - Private private func modifySessionItem(type: SessionItemType, label: String, change: (inout SessionItem) -> Void) { @@ -176,6 +189,14 @@ private class DebugRUMSessionViewModel: ObservableObject { private func mockURL() -> URL { return URL(string: "https://foo.com/\(UUID().uuidString)")! } + + private func updateSessionID() { + RUMMonitor.shared().currentSessionID { [weak self] id in + DispatchQueue.main.async { + self?.sessionID = id ?? "-" + } + } + } } @available(iOS 13.0, *) @@ -215,6 +236,10 @@ internal struct DebugRUMSessionView: View { ) Button("START") { viewModel.startResource() } } + HStack { + Button("STOP SESSION") { viewModel.stopSession() } + Spacer() + } Divider() } Group { @@ -248,9 +273,12 @@ internal struct DebugRUMSessionView: View { Divider() } Group { - Text("Current RUM Session:") + Text("Current RUM Session") .frame(maxWidth: .infinity, alignment: .leading) .font(.caption.weight(.bold)) + Text("UUID: \(viewModel.sessionID)") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.ultraLight)) List(viewModel.sessionItems) { sessionItem in SessionItemView(item: sessionItem) .listRowInsets(EdgeInsets()) @@ -277,20 +305,20 @@ private struct FormItemView: View { Text(title) .bold() .font(.system(size: 10)) - .padding(8) + .padding(4) .background(accent) .foregroundColor(Color.white) - .cornerRadius(8) + .cornerRadius(4) TextField(placeholder, text: $value) .font(.system(size: 12)) - .padding(8) + .padding(4) .background(Color(UIColor.secondarySystemFill)) - .cornerRadius(8) + .cornerRadius(4) } - .padding(8) + .padding(4) .background(Color(UIColor.systemFill)) .foregroundColor(Color.secondary) - .cornerRadius(8) + .cornerRadius(4) } } @@ -304,20 +332,20 @@ private struct SessionItemView: View { Text(label(for: item.type)) .bold() .font(.system(size: 10)) - .padding(8) + .padding(4) .background(color(for: item.type)) .foregroundColor(Color.white) - .cornerRadius(8) + .cornerRadius(4) Text(item.label) .bold() .font(.system(size: 14)) Spacer() } - .padding(8) + .padding(4) .frame(maxWidth: .infinity) .background(Color(UIColor.systemFill)) .foregroundColor(Color.secondary) - .cornerRadius(8) + .cornerRadius(4) if item.isPending { Button("STOP") { item.stopAction?() } diff --git a/Datadog/Example/Debugging/Helpers/SwiftUI.swift b/Datadog/Example/Debugging/Helpers/SwiftUI.swift index 8659a9e38c..e06c1c384c 100644 --- a/Datadog/Example/Debugging/Helpers/SwiftUI.swift +++ b/Datadog/Example/Debugging/Helpers/SwiftUI.swift @@ -34,10 +34,10 @@ extension Color { internal struct DatadogButtonStyle: ButtonStyle { func makeBody(configuration: DatadogButtonStyle.Configuration) -> some View { return configuration.label - .font(.system(size: 14, weight: .medium)) - .padding(10) + .font(.system(size: 12, weight: .medium)) + .padding(6) .background(Color.datadogPurple) .foregroundColor(.white) - .cornerRadius(8) + .cornerRadius(6) } } From ac5936175053d9a0f413dfa3e8e7e58b85f71934 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 13 Jun 2024 11:31:38 +0200 Subject: [PATCH 002/110] chore: Fix E2E tests build --- .../SessionReplayWebViewController.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift b/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift index 605231815a..75c4d34e6f 100644 --- a/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift +++ b/E2ETests/Runner/Scenarios/SessionReplayWebView/SessionReplayWebViewController.swift @@ -22,10 +22,7 @@ class SessionReplayWebViewController: UIViewController, WKUIDelegate { super.viewDidLoad() WebViewTracking.enable( webView: webView, - hosts: ["datadoghq.dev"], - sessionReplayConfiguration: WebViewTracking.SessionReplayConfiguration( - privacyLevel: .allow - ) + hosts: ["datadoghq.dev"] ) } From 6f6ba1626773987d61cfb407dc0d248e4a8b906a Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 11 Jun 2024 21:25:17 +0200 Subject: [PATCH 003/110] RUM-4591 Add an entry point for tracking view `instrumentationType` in SE metric --- .../Tests/Datadog/Mocks/RUMFeatureMocks.swift | 6 ++++-- .../Instrumentation/Views/RUMViewsHandler.swift | 15 +++++++++++---- DatadogRUM/Sources/RUMMonitor/Monitor.swift | 6 ++++-- DatadogRUM/Sources/RUMMonitor/RUMCommand.swift | 7 ++++++- .../Sources/RUMMonitor/Scopes/RUMViewScope.swift | 1 + .../Sources/SDKMetrics/SessionEndedMetric.swift | 6 ++++++ DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift | 6 ++++-- 7 files changed, 36 insertions(+), 11 deletions(-) diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index 9642415537..508c53b93f 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -200,14 +200,16 @@ extension RUMStartViewCommand: AnyMockable, RandomMockable { attributes: [AttributeKey: AttributeValue] = [:], identity: ViewIdentifier = .mockViewIdentifier(), name: String = .mockAny(), - path: String = .mockAny() + path: String = .mockAny(), + instrumentationType: SessionEndedMetric.ViewInstrumentationType = .manual ) -> RUMStartViewCommand { return RUMStartViewCommand( time: time, identity: identity, name: name, path: path, - attributes: attributes + attributes: attributes, + instrumentationType: instrumentationType ) } } diff --git a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift index 5d805a4319..770e4d1af8 100644 --- a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift @@ -25,6 +25,9 @@ internal final class RUMViewsHandler { /// Custom attributes to attach to the View. let attributes: [AttributeKey: AttributeValue] + + /// The type of instrumentation that started this view. + let instrumentationType: SessionEndedMetric.ViewInstrumentationType } /// The current date provider. @@ -157,7 +160,8 @@ internal final class RUMViewsHandler { identity: view.identity, name: view.name, path: view.path, - attributes: view.attributes + attributes: view.attributes, + instrumentationType: view.instrumentationType ) ) } @@ -205,7 +209,8 @@ extension RUMViewsHandler: UIViewControllerHandler { name: rumView.name, path: rumView.path ?? viewController.canonicalClassName, isUntrackedModal: rumView.isUntrackedModal, - attributes: rumView.attributes + attributes: rumView.attributes, + instrumentationType: .uikit ) ) } else if #available(iOS 13, tvOS 13, *), viewController.isModalInPresentation { @@ -215,7 +220,8 @@ extension RUMViewsHandler: UIViewControllerHandler { name: "RUMUntrackedModal", path: viewController.canonicalClassName, isUntrackedModal: true, - attributes: [:] + attributes: [:], + instrumentationType: .uikit ) ) } @@ -240,7 +246,8 @@ extension RUMViewsHandler: SwiftUIViewHandler { name: name, path: path, isUntrackedModal: false, - attributes: attributes + attributes: attributes, + instrumentationType: .swiftui ) ) } diff --git a/DatadogRUM/Sources/RUMMonitor/Monitor.swift b/DatadogRUM/Sources/RUMMonitor/Monitor.swift index 4621b81e45..20ae4d8350 100644 --- a/DatadogRUM/Sources/RUMMonitor/Monitor.swift +++ b/DatadogRUM/Sources/RUMMonitor/Monitor.swift @@ -250,7 +250,8 @@ extension Monitor: RUMMonitorProtocol { identity: ViewIdentifier(viewController), name: name ?? viewController.canonicalClassName, path: viewController.canonicalClassName, - attributes: attributes + attributes: attributes, + instrumentationType: .manual ) ) } @@ -272,7 +273,8 @@ extension Monitor: RUMMonitorProtocol { identity: ViewIdentifier(key), name: name ?? key, path: key, - attributes: attributes + attributes: attributes, + instrumentationType: .manual ) ) } diff --git a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift index 8c29297761..6a30d78f2d 100644 --- a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift +++ b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift @@ -62,18 +62,23 @@ internal struct RUMStartViewCommand: RUMCommand, RUMViewScopePropagatableAttribu /// The path of this View, rendered in RUM Explorer as `VIEW URL`. let path: String + /// The type of instrumentation that started this view. + let instrumentationType: SessionEndedMetric.ViewInstrumentationType + init( time: Date, identity: ViewIdentifier, name: String, path: String, - attributes: [AttributeKey: AttributeValue] + attributes: [AttributeKey: AttributeValue], + instrumentationType: SessionEndedMetric.ViewInstrumentationType ) { self.time = time self.attributes = attributes self.identity = identity self.name = name self.path = path + self.instrumentationType = instrumentationType } } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index cf30cce593..5d7a3eba4a 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -556,6 +556,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { dependencies.fatalErrorContext.view = event // Track this view in Session Ended metric: + _ = (command as? RUMStartViewCommand)?.instrumentationType // TODO: RUM-1660 pass instrumentation type to SE metric dependencies.sessionEndedMetric.track(view: event, in: self.context.sessionID) } else { // if event was dropped by mapper version -= 1 diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 3d417d7c9e..57e671feb3 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -35,6 +35,12 @@ internal struct SessionEndedMetric { static let rseKey = "rse" } + internal enum ViewInstrumentationType: String { + case manual + case uikit + case swiftui + } + /// An ID of the session being tracked through this metric object. let sessionID: RUMUUID diff --git a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift index 9145d1b56b..a77068ae37 100644 --- a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift @@ -218,14 +218,16 @@ extension RUMStartViewCommand: AnyMockable, RandomMockable { attributes: [AttributeKey: AttributeValue] = [:], identity: ViewIdentifier = .mockViewIdentifier(), name: String = .mockAny(), - path: String = .mockAny() + path: String = .mockAny(), + instrumentationType: SessionEndedMetric.ViewInstrumentationType = .manual ) -> RUMStartViewCommand { return RUMStartViewCommand( time: time, identity: identity, name: name, path: path, - attributes: attributes + attributes: attributes, + instrumentationType: instrumentationType ) } } From d2acc838e6412c849050ce4d0183df7bd850c5de Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 12 Jun 2024 18:39:54 +0200 Subject: [PATCH 004/110] RUM-4591 Add an entry point for tracking off-view events in SE metric --- .../Sources/RUMMonitor/Scopes/RUMSessionScope.swift | 8 +++++--- .../Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index cee8ae1a75..3c15078e67 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -325,12 +325,14 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // As no view scope will handle this command, warn the user on dropping it. DD.logger.warn( """ - \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. + \(String(describing: command)) was detected, but no view is active. To track views automatically, configure + `RUM.Configuration.uiKitViewsPredicate` or use `.trackRUMView()` modifier in SwiftUI. You can also track views manually + with `RUMMonitor.shared().startView()` and `RUMMonitor.shared().stopView()`. """ ) } + + _ = dependencies.sessionEndedMetric // TODO: RUM-1660 pass the command to SE metric for tracking off-view events } } diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 4c63c5d0d1..2eb6013b39 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -623,9 +623,9 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual( randomCommandLog, """ - \(String(describing: randomCommand)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. + \(String(describing: randomCommand)) was detected, but no view is active. To track views automatically, configure + `RUM.Configuration.uiKitViewsPredicate` or use `.trackRUMView()` modifier in SwiftUI. You can also track views manually + with `RUMMonitor.shared().startView()` and `RUMMonitor.shared().stopView()`. """ ) From 0fa33c5b3c21cf7607504e8e5af5dde164118666 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 12 Jun 2024 18:43:31 +0200 Subject: [PATCH 005/110] RUM-4591 Add entry points for tracking more context in SE metric: - if background events tracking is enabled - off-view events - has replay - NTP offset --- DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift | 6 ++++-- DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift | 2 +- DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift | 2 ++ .../Sources/SDKMetrics/SessionEndedMetricController.swift | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index 3c15078e67..0bb361020c 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -109,6 +109,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { sessionID: sessionUUID, precondition: startPrecondition, context: context + // TODO: RUM-4591 pass `trackBackgroundEvents` to SE metric + // TODO: RUM-4591 pass NTP offset at session start to SE metric ) if let viewScope = resumingViewScope { @@ -330,9 +332,9 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { with `RUMMonitor.shared().startView()` and `RUMMonitor.shared().stopView()`. """ ) - } - _ = dependencies.sessionEndedMetric // TODO: RUM-1660 pass the command to SE metric for tracking off-view events + _ = dependencies.sessionEndedMetric // TODO: RUM-4591 pass the command to SE metric for tracking off-view events + } } } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index 5d7a3eba4a..1a49f90fc7 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -556,7 +556,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { dependencies.fatalErrorContext.view = event // Track this view in Session Ended metric: - _ = (command as? RUMStartViewCommand)?.instrumentationType // TODO: RUM-1660 pass instrumentation type to SE metric + _ = (command as? RUMStartViewCommand)?.instrumentationType // TODO: RUM-4591 pass instrumentation type to SE metric dependencies.sessionEndedMetric.track(view: event, in: self.context.sessionID) } else { // if event was dropped by mapper version -= 1 diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 57e671feb3..7efe89c1e0 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -116,6 +116,8 @@ internal struct SessionEndedMetric { firstTrackedView = info } lastTrackedView = info + + _ = view.session.hasReplay // TODO: RUM-4591 track replay information } /// Tracks the kind of SDK error that occurred during the session. diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index 6a1ca81354..a1d40b33af 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -68,7 +68,7 @@ internal final class SessionEndedMetricController { guard let metric = metricsBySessionID[sessionID] else { return } - telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes()) + telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes()) // TODO: RUM-4591 track NTP offset at session end _metricsBySessionID.mutate { metrics in metrics[sessionID] = nil pendingSessionIDs.removeAll(where: { $0 == sessionID }) // O(n), but "ending the metric" is very rare event From 4a326f17969b5b2346338032d673aef723da9a90 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 13 Jun 2024 12:32:56 +0200 Subject: [PATCH 006/110] RUM-4591 Count views by their instrumentation type in SE metric --- ...UMSessionEndedMetricIntegrationTests.swift | 1 + .../RUMMonitor/Scopes/RUMViewScope.swift | 7 +- .../SDKMetrics/SessionEndedMetric.swift | 34 +++++- .../SessionEndedMetricController.swift | 10 +- .../SessionEndedMetricControllerTests.swift | 10 +- .../SDKMetrics/SessionEndedMetricTests.swift | 115 +++++++++++++++--- 6 files changed, 143 insertions(+), 34 deletions(-) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index 307ec2fe97..4887fdd8c3 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -180,6 +180,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { XCTAssertEqual(metricAttributes.viewsCount.total, 10) XCTAssertEqual(metricAttributes.viewsCount.applicationLaunch, 1) XCTAssertEqual(metricAttributes.viewsCount.background, 3) + XCTAssertEqual(metricAttributes.viewsCount.byInstrumentation, ["manual": 6]) } func testTrackingSDKErrors() throws { diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index 1a49f90fc7..b58a71b249 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -556,8 +556,11 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { dependencies.fatalErrorContext.view = event // Track this view in Session Ended metric: - _ = (command as? RUMStartViewCommand)?.instrumentationType // TODO: RUM-4591 pass instrumentation type to SE metric - dependencies.sessionEndedMetric.track(view: event, in: self.context.sessionID) + dependencies.sessionEndedMetric.track( + view: event, + instrumentationType: (command as? RUMStartViewCommand)?.instrumentationType, + in: self.context.sessionID + ) } else { // if event was dropped by mapper version -= 1 } diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 7efe89c1e0..89a1acf008 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -35,9 +35,13 @@ internal struct SessionEndedMetric { static let rseKey = "rse" } - internal enum ViewInstrumentationType: String { + /// Represents the type of instrumentation used to start a view. + internal enum ViewInstrumentationType: String, Encodable { + /// View was started manually through `RUMMonitor.shared().startView()` API. case manual + /// View was started automatically with `UIKitRUMViewsPredicate`. case uikit + /// View was started through `trackRUMView()` SwiftUI modifier. case swiftui } @@ -51,12 +55,15 @@ internal struct SessionEndedMetric { private let precondition: RUMSessionPrecondition? private struct TrackedViewInfo { + /// The view URL as reported in RUM data. let viewURL: String + /// The type of instrumentation that started this view. + /// It can be `nil` if view was started implicitly by RUM, which is the case for "ApplicationLaunch" and "Background" views. + let instrumentationType: ViewInstrumentationType? + /// The start of the view in milliseconds from from epoch. let startMs: Int64 + /// The duration of the view in nanoseconds. var durationNs: Int64 - - // TODO: RUM-4591 Track diagnostic attributes: - // - `instrumentationType`: manual | uikit | swiftui } /// Stores information about tracked views, referencing them by their view ID. @@ -98,13 +105,18 @@ internal struct SessionEndedMetric { } /// Tracks the view event that occurred during the session. - mutating func track(view: RUMViewEvent) throws { + /// - Parameters: + /// - view: the view event to track + /// - instrumentationType: the type of instrumentation used to start this view (only the first value for each `view.id` is tracked; succeeding values + /// will be ignored so it is okay to pass value on first call and then follow with `nil` for next updates of given `view.id`) + mutating func track(view: RUMViewEvent, instrumentationType: ViewInstrumentationType?) throws { guard view.session.id == sessionID.toRUMDataFormat else { throw SessionEndedMetricError.trackingViewInForeignSession(viewURL: view.view.url, sessionID: sessionID) } var info = trackedViews[view.view.id] ?? TrackedViewInfo( viewURL: view.view.url, + instrumentationType: instrumentationType, startMs: view.date, durationNs: view.view.timeSpent ) @@ -161,11 +173,14 @@ internal struct SessionEndedMetric { let background: Int /// The number of standard "ApplicationLaunch" views tracked during this session (sanity check: we expect `0` or `1`). let applicationLaunch: Int + /// The map of view instrumentation types to the number of views tracked with each instrumentation. + let byInstrumentation: [String: Int] enum CodingKeys: String, CodingKey { case total case background case applicationLaunch = "app_launch" + case byInstrumentation = "by_instrumentation" } } @@ -210,6 +225,12 @@ internal struct SessionEndedMetric { let totalViewsCount = trackedViews.count let backgroundViewsCount = trackedViews.values.filter({ $0.viewURL == RUMOffViewEventsHandlingRule.Constants.backgroundViewURL }).count let appLaunchViewsCount = trackedViews.values.filter({ $0.viewURL == RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL }).count + var byInstrumentationViewsCount: [String: Int] = [:] + trackedViews.values.forEach { + if let instrumentationType = $0.instrumentationType?.rawValue { + byInstrumentationViewsCount[instrumentationType] = (byInstrumentationViewsCount[instrumentationType] ?? 0) + 1 + } + } // Compute SDK errors count let totalSDKErrors = trackedSDKErrors.values.reduce(0, +) @@ -231,7 +252,8 @@ internal struct SessionEndedMetric { viewsCount: .init( total: totalViewsCount, background: backgroundViewsCount, - applicationLaunch: appLaunchViewsCount + applicationLaunch: appLaunchViewsCount, + byInstrumentation: byInstrumentationViewsCount ), sdkErrorsCount: .init( total: totalSDKErrors, diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index a1d40b33af..28c7c06800 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -43,9 +43,15 @@ internal final class SessionEndedMetricController { /// Tracks the view event that occurred during the session. /// - Parameters: /// - view: the view event to track + /// - instrumentationType: the type of instrumentation used to start this view (only the first value for each `view.id` is tracked; succeeding values + /// will be ignored so it is okay to pass value on first call and then follow with `nil` for next updates of given `view.id`) /// - sessionID: session ID to track this view in (pass `nil` to track it for the last started session) - func track(view: RUMViewEvent, in sessionID: RUMUUID?) { - updateMetric(for: sessionID) { try $0?.track(view: view) } + func track( + view: RUMViewEvent, + instrumentationType: SessionEndedMetric.ViewInstrumentationType?, + in sessionID: RUMUUID? + ) { + updateMetric(for: sessionID) { try $0?.track(view: view, instrumentationType: instrumentationType) } } /// Tracks the kind of SDK error that occurred during the session. diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift index 9452988c7f..9b8c671f4e 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift @@ -22,7 +22,7 @@ class SessionEndedMetricControllerTests: XCTestCase { controller.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) // When - viewIDs.forEach { controller.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), in: sessionID) } + viewIDs.forEach { controller.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil, in: sessionID) } errorKinds.forEach { controller.track(sdkErrorKind: $0, in: sessionID) } controller.trackWasStopped(sessionID: sessionID) controller.endMetric(sessionID: sessionID) @@ -43,7 +43,7 @@ class SessionEndedMetricControllerTests: XCTestCase { controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom()) controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom()) // Session 1: - controller.track(view: .mockRandomWith(sessionID: sessionID1), in: sessionID1) + controller.track(view: .mockRandomWith(sessionID: sessionID1), instrumentationType: nil, in: sessionID1) controller.track(sdkErrorKind: "error.kind1", in: sessionID1) controller.trackWasStopped(sessionID: sessionID1) // Session 2: @@ -75,7 +75,7 @@ class SessionEndedMetricControllerTests: XCTestCase { controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom()) controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom()) // Track latest session (`sessionID: nil`) - controller.track(view: .mockRandomWith(sessionID: sessionID2), in: nil) + controller.track(view: .mockRandomWith(sessionID: sessionID2), instrumentationType: nil, in: nil) controller.track(sdkErrorKind: "error.kind1", in: nil) controller.trackWasStopped(sessionID: nil) // Send 2nd: @@ -100,10 +100,10 @@ class SessionEndedMetricControllerTests: XCTestCase { { controller.startMetric( sessionID: sessionIDs.randomElement()!, precondition: .mockRandom(), context: .mockRandom() ) }, - { controller.track(view: .mockRandom(), in: sessionIDs.randomElement()!) }, + { controller.track(view: .mockRandom(), instrumentationType: nil, in: sessionIDs.randomElement()!) }, { controller.track(sdkErrorKind: .mockRandom(), in: sessionIDs.randomElement()!) }, { controller.trackWasStopped(sessionID: sessionIDs.randomElement()!) }, - { controller.track(view: .mockRandom(), in: nil) }, + { controller.track(view: .mockRandom(), instrumentationType: nil, in: nil) }, { controller.track(sdkErrorKind: .mockRandom(), in: nil) }, { controller.trackWasStopped(sessionID: nil) }, { controller.endMetric(sessionID: sessionIDs.randomElement()!) }, diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index a0d0e5846a..99d7a70412 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -112,7 +112,7 @@ class SessionEndedMetricTests: XCTestCase { let view: RUMViewEvent = .mockRandomWith(sessionID: sessionID) // When - try metric.track(view: view) + try metric.track(view: view, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -128,9 +128,9 @@ class SessionEndedMetricTests: XCTestCase { let view3: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms + 20.s2ms, viewTimeSpent: 50.s2ns) // When - try metric.track(view: view1) - try metric.track(view: view2) - try metric.track(view: view3) + try metric.track(view: view1, instrumentationType: nil) + try metric.track(view: view2, instrumentationType: nil) + try metric.track(view: view3, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -145,8 +145,8 @@ class SessionEndedMetricTests: XCTestCase { let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 15.s2ms, viewTimeSpent: 20.s2ns) // starts in the middle of `view1` // When - try metric.track(view: view1) - try metric.track(view: view2) + try metric.track(view: view1, instrumentationType: nil) + try metric.track(view: view2, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -161,9 +161,9 @@ class SessionEndedMetricTests: XCTestCase { let lastView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) // When - try metric.track(view: firstView) - try (0..<10).forEach { _ in try metric.track(view: .mockRandomWith(sessionID: sessionID)) } // middle views should not alter the duration - try metric.track(view: lastView) + try metric.track(view: firstView, instrumentationType: nil) + try (0..<10).forEach { _ in try metric.track(view: .mockRandomWith(sessionID: sessionID), instrumentationType: nil) } // middle views should not alter the duration + try metric.track(view: lastView, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -176,8 +176,8 @@ class SessionEndedMetricTests: XCTestCase { var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) // When - XCTAssertThrowsError(try metric.track(view: .mockRandom())) - XCTAssertThrowsError(try metric.track(view: .mockRandom())) + XCTAssertThrowsError(try metric.track(view: .mockRandom(), instrumentationType: nil)) + XCTAssertThrowsError(try metric.track(view: .mockRandom(), instrumentationType: nil)) let attributes = metric.asMetricAttributes() // Then @@ -221,7 +221,7 @@ class SessionEndedMetricTests: XCTestCase { var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) // When - try viewIDs.forEach { try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0)) } + try viewIDs.forEach { try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil) } let attributes = metric.asMetricAttributes() // Then @@ -229,7 +229,26 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.viewsCount.total, viewIDs.count) } - func testReportingBackgorundViewsCount() throws { + func testWhenReportingTotalViewsCount_itCountsEachViewIDOnlyOnce() throws { + let viewID1: String = .mockRandom() + let viewID2: String = .mockRandom(otherThan: [viewID1]) + + // Given + var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + + // When + try (0..<5).forEach { _ in // repeat few times + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID1), instrumentationType: nil) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID2), instrumentationType: nil) + } + let attributes = metric.asMetricAttributes() + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.viewsCount.total, 2) + } + + func testReportingBackgroundViewsCount() throws { let backgroundViewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) let otherViewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) let viewIDs = backgroundViewIDs.union(otherViewIDs) @@ -240,7 +259,7 @@ class SessionEndedMetricTests: XCTestCase { // When try viewIDs.forEach { viewID in let viewURL = backgroundViewIDs.contains(viewID) ? RUMOffViewEventsHandlingRule.Constants.backgroundViewURL : .mockRandom() - try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL)) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL), instrumentationType: nil) } let attributes = metric.asMetricAttributes() @@ -260,7 +279,7 @@ class SessionEndedMetricTests: XCTestCase { // When try viewIDs.forEach { viewID in let viewURL = appLaunchViewIDs.contains(viewID) ? RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL : .mockRandom() - try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL)) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL), instrumentationType: nil) } let attributes = metric.asMetricAttributes() @@ -269,13 +288,66 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.viewsCount.applicationLaunch, appLaunchViewIDs.count) } - func testReportingViewsCount_itIgnoresViewsFromDifferentSession() throws { + func testReportingViewsCountByInstrumentationType() throws { + let manualViewsCount: Int = .mockRandom(min: 1, max: 10) + let swiftuiViewsCount: Int = .mockRandom(min: 1, max: 10) + let uikitViewsCount: Int = .mockRandom(min: 1, max: 10) + let unknownViewsCount: Int = .mockRandom(min: 1, max: 10) + + // Given + var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + + // When + try (0.. Date: Thu, 13 Jun 2024 12:56:23 +0200 Subject: [PATCH 007/110] RUM-4591 Report `hasBackgroundEventsTrackingEnabled` in SE metric --- ...UMSessionEndedMetricIntegrationTests.swift | 3 +- .../RUMMonitor/Scopes/RUMSessionScope.swift | 4 +- .../SDKMetrics/SessionEndedMetric.swift | 13 ++- .../SessionEndedMetricController.swift | 5 +- .../TelemetryInterceptorTests.swift | 2 +- .../SessionEndedMetricControllerTests.swift | 12 +-- .../SDKMetrics/SessionEndedMetricTests.swift | 85 ++++++++++++------- 7 files changed, 78 insertions(+), 46 deletions(-) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index 4887fdd8c3..ec663cf4ab 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -101,7 +101,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { // MARK: - Reporting Session Attributes - func testReportingSessionID() throws { + func testReportingSessionInformation() throws { var currentSessionID: String? RUM.enable(with: rumConfig, in: core) @@ -118,6 +118,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { let metric = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()) let expectedSessionID = try XCTUnwrap(currentSessionID) XCTAssertEqual(metric.session?.id, expectedSessionID.lowercased()) + XCTAssertEqual(metric.attributes?.hasBackgroundEventsTrackingEnabled, rumConfig.trackBackgroundEvents) } func testTrackingSessionDuration() throws { diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index 0bb361020c..79649081c4 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -108,8 +108,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { dependencies.sessionEndedMetric.startMetric( sessionID: sessionUUID, precondition: startPrecondition, - context: context - // TODO: RUM-4591 pass `trackBackgroundEvents` to SE metric + context: context, + tracksBackgroundEvents: trackBackgroundEvents // TODO: RUM-4591 pass NTP offset at session start to SE metric ) diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 89a1acf008..75767cccb9 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -81,9 +81,11 @@ internal struct SessionEndedMetric { /// Indicates if the session was stopped through `stopSession()` API. private var wasStopped = false + /// If `RUM.Configuration.trackBackgroundEvents` was enabled for this session. + private let tracksBackgroundEvents: Bool + // TODO: RUM-4591 Track diagnostic attributes: // - no_view_events_count - // - has_background_events_tracking_enabled // - has_replay // - ntp_offset @@ -94,14 +96,17 @@ internal struct SessionEndedMetric { /// - sessionID: An ID of the session that is being tracked with this metric. /// - precondition: The precondition that led to starting this session. /// - context: The SDK context at the moment of starting this session. + /// - tracksBackgroundEvents: If background events tracking is enabled for this session. init( sessionID: RUMUUID, precondition: RUMSessionPrecondition?, - context: DatadogContext + context: DatadogContext, + tracksBackgroundEvents: Bool ) { self.sessionID = sessionID self.bundleType = context.applicationBundleType self.precondition = precondition + self.tracksBackgroundEvents = tracksBackgroundEvents } /// Tracks the view event that occurred during the session. @@ -165,6 +170,8 @@ internal struct SessionEndedMetric { let duration: Int64? /// Indicates if the session was stopped through `stopSession()` API. let wasStopped: Bool + /// If background events tracking is enabled for this session. + let hasBackgroundEventsTrackingEnabled: Bool struct ViewsCount: Encodable { /// The number of distinct views (view UUIDs) sent during this session. @@ -207,6 +214,7 @@ internal struct SessionEndedMetric { case precondition case duration case wasStopped = "was_stopped" + case hasBackgroundEventsTrackingEnabled = "has_background_events_tracking_enabled" case viewsCount = "views_count" case sdkErrorsCount = "sdk_errors_count" } @@ -249,6 +257,7 @@ internal struct SessionEndedMetric { precondition: precondition?.rawValue, duration: durationNs, wasStopped: wasStopped, + hasBackgroundEventsTrackingEnabled: tracksBackgroundEvents, viewsCount: .init( total: totalViewsCount, background: backgroundViewsCount, diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index 28c7c06800..c4971ec406 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -29,13 +29,14 @@ internal final class SessionEndedMetricController { /// - sessionID: The ID of the session to track. /// - precondition: The precondition that led to starting this session. /// - context: The SDK context at the moment of starting this session. + /// - tracksBackgroundEvents: If background events tracking is enabled for this session. /// - Returns: The newly created `SessionEndedMetric` instance. - func startMetric(sessionID: RUMUUID, precondition: RUMSessionPrecondition?, context: DatadogContext) { + func startMetric(sessionID: RUMUUID, precondition: RUMSessionPrecondition?, context: DatadogContext, tracksBackgroundEvents: Bool) { guard sessionID != RUMUUID.nullUUID else { return // do not track metric when session is not sampled } _metricsBySessionID.mutate { metrics in - metrics[sessionID] = SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context) + metrics[sessionID] = SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context, tracksBackgroundEvents: tracksBackgroundEvents) pendingSessionIDs.append(sessionID) } } diff --git a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift index 0fe552c2c1..54703abab8 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift @@ -20,7 +20,7 @@ class TelemetryInterceptorTests: XCTestCase { let interceptor = TelemetryInterceptor(sessionEndedMetric: metricController) // When - metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny()) + metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny(), tracksBackgroundEvents: .mockRandom()) let errorTelemetry: TelemetryMessage = .error(id: .mockAny(), message: .mockAny(), kind: .mockAny(), stack: .mockAny()) let result = interceptor.receive(message: .telemetry(errorTelemetry), from: NOPDatadogCore()) XCTAssertFalse(result) diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift index 9b8c671f4e..f1de18d5d1 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift @@ -19,7 +19,7 @@ class SessionEndedMetricControllerTests: XCTestCase { // Given let controller = SessionEndedMetricController(telemetry: telemetry) - controller.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + controller.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) // When viewIDs.forEach { controller.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil, in: sessionID) } @@ -40,8 +40,8 @@ class SessionEndedMetricControllerTests: XCTestCase { // When let controller = SessionEndedMetricController(telemetry: telemetry) - controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom()) - controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom()) + controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) + controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) // Session 1: controller.track(view: .mockRandomWith(sessionID: sessionID1), instrumentationType: nil, in: sessionID1) controller.track(sdkErrorKind: "error.kind1", in: sessionID1) @@ -72,8 +72,8 @@ class SessionEndedMetricControllerTests: XCTestCase { // When let controller = SessionEndedMetricController(telemetry: telemetry) - controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom()) - controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom()) + controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) + controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) // Track latest session (`sessionID: nil`) controller.track(view: .mockRandomWith(sessionID: sessionID2), instrumentationType: nil, in: nil) controller.track(sdkErrorKind: "error.kind1", in: nil) @@ -98,7 +98,7 @@ class SessionEndedMetricControllerTests: XCTestCase { callConcurrently( closures: [ { controller.startMetric( - sessionID: sessionIDs.randomElement()!, precondition: .mockRandom(), context: .mockRandom() + sessionID: sessionIDs.randomElement()!, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom() ) }, { controller.track(view: .mockRandom(), instrumentationType: nil, in: sessionIDs.randomElement()!) }, { controller.track(sdkErrorKind: .mockRandom(), in: sessionIDs.randomElement()!) }, diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index 99d7a70412..5cbaf2f9b9 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -16,7 +16,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingEmptyMetric() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -35,7 +35,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingMetricType() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -48,7 +48,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionID() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -61,9 +61,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingAppProcessType() throws { // Given - let metric = SessionEndedMetric( - sessionID: sessionID, precondition: .mockRandom(), context: .mockWith(applicationBundleType: .iOSApp) - ) + let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(applicationBundleType: .iOSApp)) // When let attributes = metric.asMetricAttributes() @@ -75,9 +73,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingExtensionProcessType() throws { // Given - let metric = SessionEndedMetric( - sessionID: sessionID, precondition: .mockRandom(), context: .mockWith(applicationBundleType: .iOSAppExtension) - ) + let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(applicationBundleType: .iOSAppExtension)) // When let attributes = metric.asMetricAttributes() @@ -92,9 +88,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionPrecondition() throws { // Given let expectedPrecondition: RUMSessionPrecondition = .mockRandom() - let metric = SessionEndedMetric( - sessionID: sessionID, precondition: expectedPrecondition, context: .mockRandom() - ) + let metric = SessionEndedMetric.with(sessionID: sessionID, precondition: expectedPrecondition) // When let attributes = metric.asMetricAttributes() @@ -104,11 +98,26 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.precondition, expectedPrecondition.rawValue) } + // MARK: - Tracks Background Events + + func testReportingTracksBackgroundEvents() throws { + // Given + let expected: Bool = .mockRandom() + let metric = SessionEndedMetric.with(sessionID: sessionID, tracksBackgroundEvents: expected) + + // When + let attributes = metric.asMetricAttributes() + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.hasBackgroundEventsTrackingEnabled, expected) + } + // MARK: - Duration func testComputingDurationFromSingleView() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) let view: RUMViewEvent = .mockRandomWith(sessionID: sessionID) // When @@ -122,7 +131,7 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromMultipleViews() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) let view1: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms, viewTimeSpent: 10.s2ns) let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) let view3: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms + 20.s2ms, viewTimeSpent: 50.s2ns) @@ -140,7 +149,7 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromOverlappingViews() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) let view1: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms, viewTimeSpent: 10.s2ns) let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 15.s2ms, viewTimeSpent: 20.s2ns) // starts in the middle of `view1` @@ -156,7 +165,7 @@ class SessionEndedMetricTests: XCTestCase { func testDurationIsAlwaysComputedFromTheFirstAndLastView() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) let firstView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms, viewTimeSpent: 10.s2ns) let lastView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) @@ -173,7 +182,7 @@ class SessionEndedMetricTests: XCTestCase { func testWhenComputingDuration_itIgnoresViewsFromDifferentSession() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When XCTAssertThrowsError(try metric.track(view: .mockRandom(), instrumentationType: nil)) @@ -189,7 +198,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionThatWasStopped() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When metric.trackWasStopped() @@ -202,7 +211,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionThatWasNotStopped() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -218,7 +227,7 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil) } @@ -234,7 +243,7 @@ class SessionEndedMetricTests: XCTestCase { let viewID2: String = .mockRandom(otherThan: [viewID1]) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When try (0..<5).forEach { _ in // repeat few times @@ -254,7 +263,7 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs = backgroundViewIDs.union(otherViewIDs) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { viewID in @@ -274,7 +283,7 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs = appLaunchViewIDs.union(otherViewIDs) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { viewID in @@ -295,7 +304,7 @@ class SessionEndedMetricTests: XCTestCase { let unknownViewsCount: Int = .mockRandom(min: 1, max: 10) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + var metric = SessionEndedMetric.with(sessionID: sessionID) // When try (0.. SessionEndedMetric { + SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context, tracksBackgroundEvents: tracksBackgroundEvents) + } +} From 3c4471f0cb3f3e73fb26d933572114aff3a6806d Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 13 Jun 2024 13:15:15 +0200 Subject: [PATCH 008/110] RUM-4591 Report `ntp_offset` in SE metric --- ...UMSessionEndedMetricIntegrationTests.swift | 21 ++++++++++++ .../Scopes/RUMApplicationScope.swift | 2 +- .../RUMMonitor/Scopes/RUMSessionScope.swift | 1 - .../SDKMetrics/SessionEndedMetric.swift | 33 +++++++++++++++++-- .../SessionEndedMetricController.swift | 4 +-- .../TelemetryInterceptorTests.swift | 2 +- .../SessionEndedMetricControllerTests.swift | 10 +++--- .../SDKMetrics/SessionEndedMetricTests.swift | 24 ++++++++++++++ 8 files changed, 84 insertions(+), 13 deletions(-) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index ec663cf4ab..b58359efeb 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -212,6 +212,27 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { "It should report TOP 5 error kinds" ) } + + func testTrackingNTPOffset() throws { + let offsetAtStart: TimeInterval = .mockRandom(min: -10, max: 10) + let offsetAtEnd: TimeInterval = .mockRandom(min: -10, max: 10) + + core.context.serverTimeOffset = offsetAtStart + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = RUMMonitor.shared(in: core) + monitor.startView(key: "key", name: "View") + + // When + core.context.serverTimeOffset = offsetAtEnd + monitor.stopSession() + + // Then + let metric = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()) + XCTAssertEqual(metric.attributes?.ntpOffset.atStart, offsetAtStart.toInt64Milliseconds) + XCTAssertEqual(metric.attributes?.ntpOffset.atEnd, offsetAtEnd.toInt64Milliseconds) + } } // MARK: - Helpers diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift index 305ef959d2..979522394a 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -102,7 +102,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // proccss(command:context:writer) returned false, so the scope will be deallocated at the end of // this execution context. End the "RUM Session Ended" metric: - defer { dependencies.sessionEndedMetric.endMetric(sessionID: scope.sessionUUID) } + defer { dependencies.sessionEndedMetric.endMetric(sessionID: scope.sessionUUID, with: context) } // proccss(command:context:writer) returned false, but if the scope is still active // it means the session reached one of the end reasons diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index 79649081c4..fd7c851710 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -110,7 +110,6 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { precondition: startPrecondition, context: context, tracksBackgroundEvents: trackBackgroundEvents - // TODO: RUM-4591 pass NTP offset at session start to SE metric ) if let viewScope = resumingViewScope { diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 75767cccb9..0ed5d9b62b 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -84,10 +84,12 @@ internal struct SessionEndedMetric { /// If `RUM.Configuration.trackBackgroundEvents` was enabled for this session. private let tracksBackgroundEvents: Bool + /// The current value of NTP offset at session start. + private let ntpOffsetAtStart: TimeInterval + // TODO: RUM-4591 Track diagnostic attributes: // - no_view_events_count // - has_replay - // - ntp_offset // MARK: - Tracking Metric State @@ -107,6 +109,7 @@ internal struct SessionEndedMetric { self.bundleType = context.applicationBundleType self.precondition = precondition self.tracksBackgroundEvents = tracksBackgroundEvents + self.ntpOffsetAtStart = context.serverTimeOffset } /// Tracks the view event that occurred during the session. @@ -209,6 +212,21 @@ internal struct SessionEndedMetric { let sdkErrorsCount: SDKErrorsCount + struct NTPOffset: Encodable { + /// The NTP offset at session start, in milliseconds. + let atStart: Int64 + /// The NTP offset at session end, in milliseconds. + let atEnd: Int64 + + enum CodingKeys: String, CodingKey { + case atStart = "at_start" + case atEnd = "at_end" + } + } + + /// NTP offset information tracked for this session. + let ntpOffset: NTPOffset + enum CodingKeys: String, CodingKey { case processType = "process_type" case precondition @@ -217,11 +235,16 @@ internal struct SessionEndedMetric { case hasBackgroundEventsTrackingEnabled = "has_background_events_tracking_enabled" case viewsCount = "views_count" case sdkErrorsCount = "sdk_errors_count" + case ntpOffset = "ntp_offset" } } - /// Exports metric attributes for `Telemetry.metric(name:attributes:)`. - func asMetricAttributes() -> [String: Encodable] { + /// Exports metric attributes for `Telemetry.metric(name:attributes:)`. This method is expected to be called + /// at session end with providing the SDK `context` valid at the moment of call. + /// + /// - Parameter context: the SDK context valid at the moment of this call + /// - Returns: metric attributes + func asMetricAttributes(with context: DatadogContext) -> [String: Encodable] { // Compute duration var durationNs: Int64? if let firstView = firstTrackedView, let lastView = lastTrackedView { @@ -267,6 +290,10 @@ internal struct SessionEndedMetric { sdkErrorsCount: .init( total: totalSDKErrors, byKind: top5SDKErrorsByKind + ), + ntpOffset: .init( + atStart: ntpOffsetAtStart.toInt64Milliseconds, + atEnd: context.serverTimeOffset.toInt64Milliseconds ) ) ] diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index c4971ec406..c94b075e55 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -71,11 +71,11 @@ internal final class SessionEndedMetricController { /// Ends the metric for a given session, sending it to telemetry and removing it from pending metrics. /// - Parameter sessionID: The ID of the session to end the metric for. - func endMetric(sessionID: RUMUUID) { + func endMetric(sessionID: RUMUUID, with context: DatadogContext) { guard let metric = metricsBySessionID[sessionID] else { return } - telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes()) // TODO: RUM-4591 track NTP offset at session end + telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes(with: context)) _metricsBySessionID.mutate { metrics in metrics[sessionID] = nil pendingSessionIDs.removeAll(where: { $0 == sessionID }) // O(n), but "ending the metric" is very rare event diff --git a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift index 54703abab8..deb686f5ec 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift @@ -26,7 +26,7 @@ class TelemetryInterceptorTests: XCTestCase { XCTAssertFalse(result) // Then - metricController.endMetric(sessionID: sessionID) + metricController.endMetric(sessionID: sessionID, with: .mockRandom()) let metric = try XCTUnwrap(telemetry.messages.lastMetric(named: SessionEndedMetric.Constants.name)) let rse = try XCTUnwrap(metric.attributes[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes) XCTAssertEqual(rse.sdkErrorsCount.total, 1) diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift index f1de18d5d1..7cdee2ebcd 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift @@ -25,7 +25,7 @@ class SessionEndedMetricControllerTests: XCTestCase { viewIDs.forEach { controller.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil, in: sessionID) } errorKinds.forEach { controller.track(sdkErrorKind: $0, in: sessionID) } controller.trackWasStopped(sessionID: sessionID) - controller.endMetric(sessionID: sessionID) + controller.endMetric(sessionID: sessionID, with: .mockRandom()) // Then let metric = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) @@ -49,9 +49,9 @@ class SessionEndedMetricControllerTests: XCTestCase { // Session 2: controller.track(sdkErrorKind: "error.kind2", in: sessionID2) // Send 1st and 2nd: - controller.endMetric(sessionID: sessionID1) + controller.endMetric(sessionID: sessionID1, with: .mockRandom()) let metric1 = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) - controller.endMetric(sessionID: sessionID2) + controller.endMetric(sessionID: sessionID2, with: .mockRandom()) let metric2 = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) // Then @@ -79,7 +79,7 @@ class SessionEndedMetricControllerTests: XCTestCase { controller.track(sdkErrorKind: "error.kind1", in: nil) controller.trackWasStopped(sessionID: nil) // Send 2nd: - controller.endMetric(sessionID: sessionID2) + controller.endMetric(sessionID: sessionID2, with: .mockRandom()) let metric = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) // Then @@ -106,7 +106,7 @@ class SessionEndedMetricControllerTests: XCTestCase { { controller.track(view: .mockRandom(), instrumentationType: nil, in: nil) }, { controller.track(sdkErrorKind: .mockRandom(), in: nil) }, { controller.trackWasStopped(sessionID: nil) }, - { controller.endMetric(sessionID: sessionIDs.randomElement()!) }, + { controller.endMetric(sessionID: sessionIDs.randomElement()!, with: .mockRandom()) }, ], iterations: 100 ) diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index 5cbaf2f9b9..1a36c85ac0 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -221,6 +221,24 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertFalse(rse.wasStopped) } + // MARK: - NTP Offset + + func testReportingNTPOffset() throws { + let offsetAtStart: TimeInterval = .mockRandom(min: -10, max: 10) + let offsetAtEnd: TimeInterval = .mockRandom(min: -10, max: 10) + + // Given + let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(serverTimeOffset: offsetAtStart)) + + // When + let attributes = metric.asMetricAttributes(with: .mockWith(serverTimeOffset: offsetAtEnd)) + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.ntpOffset.atStart, offsetAtStart.toInt64Milliseconds) + XCTAssertEqual(rse.ntpOffset.atEnd, offsetAtEnd.toInt64Milliseconds) + } + // MARK: - Views Count func testReportingTotalViewsCount() throws { @@ -455,6 +473,8 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertNotNil(try matcher.value("rse.views_count.by_instrumentation.uikit") as Int) XCTAssertNotNil(try matcher.value("rse.sdk_errors_count.total") as Int) XCTAssertNotNil(try matcher.value("rse.sdk_errors_count.by_kind") as [String: Int]) + XCTAssertNotNil(try matcher.value("rse.ntp_offset.at_start") as Int) + XCTAssertNotNil(try matcher.value("rse.ntp_offset.at_end") as Int) } } @@ -478,4 +498,8 @@ private extension SessionEndedMetric { ) -> SessionEndedMetric { SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context, tracksBackgroundEvents: tracksBackgroundEvents) } + + func asMetricAttributes() -> [String: Encodable] { + asMetricAttributes(with: .mockRandom()) + } } From a21bd12e3e037209bf453fb57f804555cc24d287 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 13 Jun 2024 13:35:55 +0200 Subject: [PATCH 009/110] RUM-4591 Count views with `has_replay` in SE metric --- ...UMSessionEndedMetricIntegrationTests.swift | 1 + .../SDKMetrics/SessionEndedMetric.swift | 19 ++++++++++++------ .../Tests/Mocks/RUMDataModelMocks.swift | 5 +++-- .../SDKMetrics/SessionEndedMetricTests.swift | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index b58359efeb..5e5315e360 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -182,6 +182,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { XCTAssertEqual(metricAttributes.viewsCount.applicationLaunch, 1) XCTAssertEqual(metricAttributes.viewsCount.background, 3) XCTAssertEqual(metricAttributes.viewsCount.byInstrumentation, ["manual": 6]) + XCTAssertEqual(metricAttributes.viewsCount.withHasReplay, 0) } func testTrackingSDKErrors() throws { diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 0ed5d9b62b..1d6bfc31e2 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -54,6 +54,7 @@ internal struct SessionEndedMetric { /// The session precondition that led to the creation of this session. private let precondition: RUMSessionPrecondition? + /// Tracks view information for certain `view.id`. private struct TrackedViewInfo { /// The view URL as reported in RUM data. let viewURL: String @@ -64,9 +65,11 @@ internal struct SessionEndedMetric { let startMs: Int64 /// The duration of the view in nanoseconds. var durationNs: Int64 + /// If any of view updates to this `view.id` had `session.has_replay == true`. + var hasReplay: Bool } - /// Stores information about tracked views, referencing them by their view ID. + /// Stores information about tracked views, referencing them by their `view.id`. private var trackedViews: [String: TrackedViewInfo] = [:] /// Info about the first tracked view. @@ -89,7 +92,6 @@ internal struct SessionEndedMetric { // TODO: RUM-4591 Track diagnostic attributes: // - no_view_events_count - // - has_replay // MARK: - Tracking Metric State @@ -126,18 +128,18 @@ internal struct SessionEndedMetric { viewURL: view.view.url, instrumentationType: instrumentationType, startMs: view.date, - durationNs: view.view.timeSpent + durationNs: view.view.timeSpent, + hasReplay: view.session.hasReplay ?? false ) info.durationNs = view.view.timeSpent + info.hasReplay = info.hasReplay || (view.session.hasReplay ?? false) trackedViews[view.view.id] = info if firstTrackedView == nil { firstTrackedView = info } lastTrackedView = info - - _ = view.session.hasReplay // TODO: RUM-4591 track replay information } /// Tracks the kind of SDK error that occurred during the session. @@ -185,12 +187,15 @@ internal struct SessionEndedMetric { let applicationLaunch: Int /// The map of view instrumentation types to the number of views tracked with each instrumentation. let byInstrumentation: [String: Int] + /// The number of distinct views that had `has_replay == true` in any of their view events. + let withHasReplay: Int enum CodingKeys: String, CodingKey { case total case background case applicationLaunch = "app_launch" case byInstrumentation = "by_instrumentation" + case withHasReplay = "with_has_replay" } } @@ -262,6 +267,7 @@ internal struct SessionEndedMetric { byInstrumentationViewsCount[instrumentationType] = (byInstrumentationViewsCount[instrumentationType] ?? 0) + 1 } } + let withHasReplayCount = trackedViews.values.reduce(0, { acc, next in acc + (next.hasReplay ? 1 : 0) }) // Compute SDK errors count let totalSDKErrors = trackedSDKErrors.values.reduce(0, +) @@ -285,7 +291,8 @@ internal struct SessionEndedMetric { total: totalViewsCount, background: backgroundViewsCount, applicationLaunch: appLaunchViewsCount, - byInstrumentation: byInstrumentationViewsCount + byInstrumentation: byInstrumentationViewsCount, + withHasReplay: withHasReplayCount ), sdkErrorsCount: .init( total: totalSDKErrors, diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index aea3ee7700..da51cdca9c 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -137,7 +137,8 @@ extension RUMViewEvent: RandomMockable { viewIsActive: Bool? = .random(), viewTimeSpent: Int64 = .mockRandom(), viewURL: String = .mockRandom(), - crashCount: Int64? = nil + crashCount: Int64? = nil, + hasReplay: Bool? = nil ) -> RUMViewEvent { return RUMViewEvent( dd: .init( @@ -165,7 +166,7 @@ extension RUMViewEvent: RandomMockable { privacy: nil, service: .mockRandom(), session: .init( - hasReplay: nil, + hasReplay: hasReplay, id: sessionID.toRUMDataFormat, isActive: true, sampledForReplay: nil, diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index 1a36c85ac0..32da8ed3aa 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -368,6 +368,25 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.viewsCount.byInstrumentation, ["manual": 1]) } + func testReportingHasReplayViewsCount() throws { + // Given + var metric = SessionEndedMetric.with(sessionID: sessionID) + + // When + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: "1", hasReplay: nil), instrumentationType: nil) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: "1", hasReplay: false), instrumentationType: nil) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: "1", hasReplay: true), instrumentationType: nil) // count + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: "2", hasReplay: false), instrumentationType: nil) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: "3", hasReplay: true), instrumentationType: nil) // count + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: "3", hasReplay: false), instrumentationType: nil) // ignore + let attributes = metric.asMetricAttributes() + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.viewsCount.total, 3) + XCTAssertEqual(rse.viewsCount.withHasReplay, 2) + } + func testWhenReportingViewsCount_itIgnoresViewsFromDifferentSession() throws { // Given var metric = SessionEndedMetric.with(sessionID: sessionID) @@ -471,6 +490,7 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertNotNil(try matcher.value("rse.views_count.by_instrumentation.manual") as Int) XCTAssertNotNil(try matcher.value("rse.views_count.by_instrumentation.swiftui") as Int) XCTAssertNotNil(try matcher.value("rse.views_count.by_instrumentation.uikit") as Int) + XCTAssertNotNil(try matcher.value("rse.views_count.with_has_replay") as Int) XCTAssertNotNil(try matcher.value("rse.sdk_errors_count.total") as Int) XCTAssertNotNil(try matcher.value("rse.sdk_errors_count.by_kind") as [String: Int]) XCTAssertNotNil(try matcher.value("rse.ntp_offset.at_start") as Int) From 7b3bc898972857138542c6830ca907b88c9eb5ca Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 13 Jun 2024 14:22:32 +0200 Subject: [PATCH 010/110] RUM-4591 Count events missed due to absence of an active view --- ...UMSessionEndedMetricIntegrationTests.swift | 26 ++++++++++ .../Tests/Datadog/Mocks/RUMFeatureMocks.swift | 1 + .../Sources/RUMMonitor/RUMCommand.swift | 21 ++++++++ .../RUMMonitor/Scopes/RUMSessionScope.swift | 7 ++- .../SDKMetrics/SessionEndedMetric.swift | 49 ++++++++++++++++++- .../SessionEndedMetricController.swift | 8 +++ DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift | 1 + .../SessionEndedMetricControllerTests.swift | 6 +++ .../SDKMetrics/SessionEndedMetricTests.swift | 30 ++++++++++++ 9 files changed, 145 insertions(+), 4 deletions(-) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index 5e5315e360..80f8bcdf81 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -234,6 +234,32 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { XCTAssertEqual(metric.attributes?.ntpOffset.atStart, offsetAtStart.toInt64Milliseconds) XCTAssertEqual(metric.attributes?.ntpOffset.atEnd, offsetAtEnd.toInt64Milliseconds) } + + func testTrackingNoViewEventsCount() throws { + let expectedCount: Int = .mockRandom(min: 1, max: 5) + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = RUMMonitor.shared(in: core) + monitor.startView(key: "key", name: "View") + monitor.stopView(key: "key") // no active view + + // When + (0.. Date: Thu, 13 Jun 2024 14:24:36 +0200 Subject: [PATCH 011/110] RUM-4591 Cleanup --- .../Sources/SDKMetrics/SessionEndedMetricController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index eaa7d24e53..dcb2f78b25 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -62,7 +62,7 @@ internal final class SessionEndedMetricController { func track(sdkErrorKind: String, in sessionID: RUMUUID?) { updateMetric(for: sessionID) { $0?.track(sdkErrorKind: sdkErrorKind) } } - + /// Tracks an event missed due to absence of an active view. /// - Parameters: /// - missedEventType: the type of an event that was missed From 84526260f6a56c1c7952fe2ae5a77ec82975123d Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Fri, 14 Jun 2024 11:28:48 +0200 Subject: [PATCH 012/110] RUM-4883 Tabbar Icon Default Tint Color --- .../Resources/Storyboards/Tabbars.storyboard | 9 +- .../SRSnapshotTests/SRSnapshotTests.swift | 10 +- .../NodeRecorders/UITabBarRecorder.swift | 106 +++++++++++++++++- 3 files changed, 111 insertions(+), 14 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Tabbars.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Tabbars.storyboard index 3ae05572e0..7b2fefb8a4 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Tabbars.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Tabbars.storyboard @@ -2,7 +2,6 @@ - @@ -316,16 +315,16 @@ - + - + - + - + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index f70310ffbe..e88a1e1065 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -316,12 +316,12 @@ final class SRSnapshotTests: SnapshotTestCase { DDAssertSnapshotTest( newImage: image, snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(privacyMode)-privacy"), - record: recordingMode + record: true ) } // - Embedded Tab Bar - show(fixture: .embeddedTabbar) + /*show(fixture: .embeddedTabbar) try forPrivacyModes([.allow, .mask]) { privacyMode in let image = try takeSnapshot(with: privacyMode) @@ -329,7 +329,7 @@ final class SRSnapshotTests: SnapshotTestCase { DDAssertSnapshotTest( newImage: image, snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(fileNamePrefix)-\(privacyMode)-privacy"), - record: recordingMode + record: true ) } @@ -342,9 +342,9 @@ final class SRSnapshotTests: SnapshotTestCase { DDAssertSnapshotTest( newImage: image, snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(fileNamePrefix)-\(privacyMode)-privacy"), - record: recordingMode + record: true ) - } + }*/ } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index 1d49a33ba6..eae234db4d 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -7,13 +7,91 @@ #if os(iOS) import UIKit -internal struct UITabBarRecorder: NodeRecorder { +internal class UITabBarRecorder: NodeRecorder { let identifier = UUID() + + private var currentlyProcessedTabbar: UITabBar? = nil + + // Some notes + // + // 1) Comparison options + // Since the image comparision is costly + on the main thread, + // a slightly better way might be to store/cache in the recorder + // each item's image the first time we traverse the tab bar. + // This way, we don't have to regenerate the hash (srIdentifier) each time we record + // the tabbar view hierarchy. + // Following this option, we can also store an alternative hash, + // but faster than the md5 used for srIdentifier. + // + // 2) Image data comparision + // We could also do the comparision using a JPG representation instead of a PNG comparision, + // which might be a bit faster. + // Tabbar icons are typically small and transparent, + // which probably makes PNG comparison more suited. + // On another note, given the usual size the tab bar icons, + // PNG comparison might be not that costly compared to JPG. + // + // It appears that none of these methods are ideal. + // A benchmark could be useful to determine which one is the "less worse". + + private lazy var subtreeViewRecorder: ViewTreeRecorder = { + ViewTreeRecorder( + nodeRecorders: [ + UIImageViewRecorder( + tintColorProvider: { imageView in + guard let imageViewImage = imageView.image else { return nil } + //print("UIImageViewRecorder -- currentlyProcessedTabbar:", self.currentlyProcessedTabbar ?? "nil") + guard let tabBar = self.currentlyProcessedTabbar else { return imageView.tintColor } + + // Access the selected item in the tabbar. + // Important note: our hypothesis is that each item uses a different image. + + let currentItemInSelectedState = tabBar.items?.first { + //$0.image?.dd.srIdentifier == imageView.image?.dd.srIdentifier + let itemImage = $0.image + let itemSelectedImage = $0.selectedImage + //return itemImage?.pngData() == imageViewImage.pngData() + let sameImage = itemSelectedImage?.pngData() == imageViewImage.pngData() + return sameImage + } + + // If item not selected, we return the unselectedItemTintColor, + // or the default gray color. + if currentItemInSelectedState == nil || tabBar.selectedItem != currentItemInSelectedState { + let unselectedColor = tabBar.unselectedItemTintColor ?? .lightGray.withAlphaComponent(0.5) + return tabBar.unselectedItemTintColor ?? .systemGray.withAlphaComponent(0.5) + } + + // Otherwise we return the tabbar tint color, + // or the default blue color. + let selectedColor = tabBar.tintColor ?? UIColor.systemBlue + return tabBar.tintColor ?? UIColor.systemBlue + }/*, + shouldRecordImagePredicate: {_ in + return true + }*/ + )/*, + UILabelRecorder()*/ + ] + ) + }() + + /*init() { + self.subtreeViewRecorder = { + + }() + }*/ + func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { guard let tabBar = view as? UITabBar else { return nil } + //print("isVisible:", attributes.isVisible) + currentlyProcessedTabbar = tabBar + //print("semantics -- currentlyProcessedTabbar:", currentlyProcessedTabbar ?? "nil") + + let subtreeRecordingResults = subtreeViewRecorder.record(tabBar, in: context) let builder = UITabBarWireframesBuilder( wireframeRect: inferOccupiedFrame(of: tabBar, in: context), wireframeID: context.ids.nodeID(view: tabBar, nodeRecorder: self), @@ -21,7 +99,23 @@ internal struct UITabBarRecorder: NodeRecorder { color: inferColor(of: tabBar) ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - return SpecificElement(subtreeStrategy: .record, nodes: [node]) + + let allNodes = subtreeRecordingResults.nodes + [node] + print("allNodes:", allNodes.count) + + for node in allNodes { + //print("node:", node.viewAttributes.frame) + } + //print(allNodes) + return SpecificElement(subtreeStrategy: .record, nodes: allNodes) + + /*if let subtreeRecorder { + print("subtree nodes:", subtreeRecorder.nodes.count) + return SpecificElement(subtreeStrategy: .ignore, nodes: [node] + subtreeRecorder.nodes) + } else { + return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) + }*/ + } private func inferOccupiedFrame(of tabBar: UITabBar, in context: ViewTreeRecordingContext) -> CGRect { @@ -30,6 +124,7 @@ internal struct UITabBarRecorder: NodeRecorder { let subviewFrame = subview.convert(subview.bounds, to: context.coordinateSpace) occupiedFrame = occupiedFrame.union(subviewFrame) } + print("Calculated occupied frame: \(occupiedFrame)") return occupiedFrame } @@ -62,12 +157,15 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { let color: CGColor func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { + print("Building wireframes for UITabBar with rect: \(wireframeRect)") + print("alpha:", attributes.alpha) + return [ builder.createShapeWireframe( id: wireframeID, frame: wireframeRect, - borderColor: UIColor.gray.cgColor, - borderWidth: 1, + borderColor: UIColor.lightGray.withAlphaComponent(0.5).cgColor, + borderWidth: 0.5, backgroundColor: color, cornerRadius: attributes.layerCornerRadius, opacity: attributes.alpha From e3d991267292bfe7d75fab24fbb1956d8e62d31e Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 14 Jun 2024 14:57:02 +0200 Subject: [PATCH 013/110] RUM-4591 CR feedback - change SEM to be reference type to avoid memory footprint on struct mutations --- .../SDKMetrics/SessionEndedMetric.swift | 57 +++++++++++++------ .../SDKMetrics/SessionEndedMetricTests.swift | 38 ++++++------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 14368bbaf0..989adf5f5e 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -23,7 +23,11 @@ internal enum SessionEndedMetricError: Error, CustomStringConvertible { } /// Tracks the state of RUM session and exports attributes for "RUM Session Ended" telemetry. -internal struct SessionEndedMetric { +/// +/// It is modeled as a reference type and contains mutable state. The thread safety for its mutations is +/// achieved by design: only `SessionEndedMetricController` interacts with this class and does +/// it through critical section each time. +internal class SessionEndedMetric { /// Definition of fields in "RUM Session Ended" telemetry, following the "RUM Session Ended" telemetry spec. internal enum Constants { /// The name of this metric, included in telemetry log. @@ -55,7 +59,7 @@ internal struct SessionEndedMetric { private let precondition: RUMSessionPrecondition? /// Tracks view information for certain `view.id`. - private struct TrackedViewInfo { + private class TrackedViewInfo { /// The view URL as reported in RUM data. let viewURL: String /// The type of instrumentation that started this view. @@ -67,6 +71,20 @@ internal struct SessionEndedMetric { var durationNs: Int64 /// If any of view updates to this `view.id` had `session.has_replay == true`. var hasReplay: Bool + + init( + viewURL: String, + instrumentationType: ViewInstrumentationType?, + startMs: Int64, + durationNs: Int64, + hasReplay: Bool + ) { + self.viewURL = viewURL + self.instrumentationType = instrumentationType + self.startMs = startMs + self.durationNs = durationNs + self.hasReplay = hasReplay + } } /// Stores information about tracked views, referencing them by their `view.id`. @@ -127,22 +145,27 @@ internal struct SessionEndedMetric { /// - view: the view event to track /// - instrumentationType: the type of instrumentation used to start this view (only the first value for each `view.id` is tracked; succeeding values /// will be ignored so it is okay to pass value on first call and then follow with `nil` for next updates of given `view.id`) - mutating func track(view: RUMViewEvent, instrumentationType: ViewInstrumentationType?) throws { + func track(view: RUMViewEvent, instrumentationType: ViewInstrumentationType?) throws { guard view.session.id == sessionID.toRUMDataFormat else { throw SessionEndedMetricError.trackingViewInForeignSession(viewURL: view.view.url, sessionID: sessionID) } - var info = trackedViews[view.view.id] ?? TrackedViewInfo( - viewURL: view.view.url, - instrumentationType: instrumentationType, - startMs: view.date, - durationNs: view.view.timeSpent, - hasReplay: view.session.hasReplay ?? false - ) + let info: TrackedViewInfo - info.durationNs = view.view.timeSpent - info.hasReplay = info.hasReplay || (view.session.hasReplay ?? false) - trackedViews[view.view.id] = info + if let existingInfo = trackedViews[view.view.id] { + info = existingInfo + info.durationNs = view.view.timeSpent + info.hasReplay = info.hasReplay || (view.session.hasReplay ?? false) + } else { + info = TrackedViewInfo( + viewURL: view.view.url, + instrumentationType: instrumentationType, + startMs: view.date, + durationNs: view.view.timeSpent, + hasReplay: view.session.hasReplay ?? false + ) + trackedViews[view.view.id] = info + } if firstTrackedView == nil { firstTrackedView = info @@ -151,7 +174,8 @@ internal struct SessionEndedMetric { } /// Tracks the kind of SDK error that occurred during the session. - mutating func track(sdkErrorKind: String) { + /// - Parameter sdkErrorKind: the kind of SDK error + func track(sdkErrorKind: String) { if let count = trackedSDKErrors[sdkErrorKind] { trackedSDKErrors[sdkErrorKind] = count + 1 } else { @@ -160,7 +184,8 @@ internal struct SessionEndedMetric { } /// Tracks an event missed due to absence of an active view. - mutating func track(missedEventType: MissedEventType) { + /// - Parameter missedEventType: the type of an event that was missed + func track(missedEventType: MissedEventType) { if let count = missedEvents[missedEventType] { missedEvents[missedEventType] = count + 1 } else { @@ -169,7 +194,7 @@ internal struct SessionEndedMetric { } /// Signals that the session was stopped with `stopSession()` API. - mutating func trackWasStopped() { + func trackWasStopped() { wasStopped = true } diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index e90f1c1b96..d109d40259 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -117,7 +117,7 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromSingleView() throws { // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) let view: RUMViewEvent = .mockRandomWith(sessionID: sessionID) // When @@ -131,7 +131,7 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromMultipleViews() throws { // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) let view1: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms, viewTimeSpent: 10.s2ns) let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) let view3: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms + 20.s2ms, viewTimeSpent: 50.s2ns) @@ -149,7 +149,7 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromOverlappingViews() throws { // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) let view1: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms, viewTimeSpent: 10.s2ns) let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 15.s2ms, viewTimeSpent: 20.s2ns) // starts in the middle of `view1` @@ -165,7 +165,7 @@ class SessionEndedMetricTests: XCTestCase { func testDurationIsAlwaysComputedFromTheFirstAndLastView() throws { // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) let firstView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms, viewTimeSpent: 10.s2ns) let lastView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) @@ -182,7 +182,7 @@ class SessionEndedMetricTests: XCTestCase { func testWhenComputingDuration_itIgnoresViewsFromDifferentSession() throws { // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When XCTAssertThrowsError(try metric.track(view: .mockRandom(), instrumentationType: nil)) @@ -198,7 +198,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionThatWasStopped() throws { // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When metric.trackWasStopped() @@ -245,7 +245,7 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil) } @@ -261,7 +261,7 @@ class SessionEndedMetricTests: XCTestCase { let viewID2: String = .mockRandom(otherThan: [viewID1]) // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try (0..<5).forEach { _ in // repeat few times @@ -281,7 +281,7 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs = backgroundViewIDs.union(otherViewIDs) // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { viewID in @@ -301,7 +301,7 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs = appLaunchViewIDs.union(otherViewIDs) // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { viewID in @@ -322,7 +322,7 @@ class SessionEndedMetricTests: XCTestCase { let unknownViewsCount: Int = .mockRandom(min: 1, max: 10) // Given - var metric = SessionEndedMetric.with(sessionID: sessionID) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try (0.. Date: Fri, 14 Jun 2024 18:32:19 +0200 Subject: [PATCH 014/110] RUM-4883 Finish + update snapshot tests --- .../SRSnapshotTests/SRSnapshotTests.swift | 4 +- .../testTabBars()-allow-privacy.png.json | 2 +- ...rs()-embeddedtabbar-allow-privacy.png.json | 2 +- ...ars()-embeddedtabbar-mask-privacy.png.json | 2 +- ...unselectedtintcolor-allow-privacy.png.json | 2 +- ...runselectedtintcolor-mask-privacy.png.json | 2 +- .../testTabBars()-mask-privacy.png.json | 2 +- .../NodeRecorders/UITabBarRecorder.swift | 107 ++++++------------ 8 files changed, 45 insertions(+), 78 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index e88a1e1065..e32d1176d7 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -321,7 +321,7 @@ final class SRSnapshotTests: SnapshotTestCase { } // - Embedded Tab Bar - /*show(fixture: .embeddedTabbar) + show(fixture: .embeddedTabbar) try forPrivacyModes([.allow, .mask]) { privacyMode in let image = try takeSnapshot(with: privacyMode) @@ -344,7 +344,7 @@ final class SRSnapshotTests: SnapshotTestCase { snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(fileNamePrefix)-\(privacyMode)-privacy"), record: true ) - }*/ + } } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json index aa73579e1b..68d3ce1abd 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"dea5628a7b223b82fcd7657a3b3baa002ac94b55"} \ No newline at end of file +{"hash":"964e6204a1bce78fb09e2794a40bbab30bc4a34c"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json index b8717a222d..97b21559cb 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"2824aae9011f3840aaff17eafc0255278c0fa510"} \ No newline at end of file +{"hash":"0c9a33804f16daa6889413e011e0cfb318f1880b"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json index a8c2288e7d..4e88eeb4ea 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"199a3f4b6c042a007e541fc0cbc6d603b7960d37"} \ No newline at end of file +{"hash":"10711d5599bdc5eb74b976e5f45d3c5f14d878bd"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json index ccd7b8d6f4..702d6cacce 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"a422a17789f21f52612bf86f8753aea330a865c0"} \ No newline at end of file +{"hash":"cc6aa28b47e5a305809e246475630e416f900c2c"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json index 94fa3dc04a..2884bae170 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"1bc1eabd753b248991d26f471ebd69b13ad599cf"} \ No newline at end of file +{"hash":"9d91581c014c31063416c7a392107859439307b3"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json index dd79383ae6..50567c584e 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"919fe524658055240b988869fcb5c6196ae719ff"} \ No newline at end of file +{"hash":"26aa1c124c13a842b4be63be2fd81680aac7b3a5"} \ No newline at end of file diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index eae234db4d..20385a3420 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -7,115 +7,70 @@ #if os(iOS) import UIKit -internal class UITabBarRecorder: NodeRecorder { +internal final class UITabBarRecorder: NodeRecorder { let identifier = UUID() private var currentlyProcessedTabbar: UITabBar? = nil - - // Some notes - // - // 1) Comparison options - // Since the image comparision is costly + on the main thread, - // a slightly better way might be to store/cache in the recorder - // each item's image the first time we traverse the tab bar. - // This way, we don't have to regenerate the hash (srIdentifier) each time we record - // the tabbar view hierarchy. - // Following this option, we can also store an alternative hash, - // but faster than the md5 used for srIdentifier. - // - // 2) Image data comparision - // We could also do the comparision using a JPG representation instead of a PNG comparision, - // which might be a bit faster. - // Tabbar icons are typically small and transparent, - // which probably makes PNG comparison more suited. - // On another note, given the usual size the tab bar icons, - // PNG comparison might be not that costly compared to JPG. - // - // It appears that none of these methods are ideal. - // A benchmark could be useful to determine which one is the "less worse". - private lazy var subtreeViewRecorder: ViewTreeRecorder = { ViewTreeRecorder( nodeRecorders: [ UIImageViewRecorder( tintColorProvider: { imageView in guard let imageViewImage = imageView.image else { return nil } - //print("UIImageViewRecorder -- currentlyProcessedTabbar:", self.currentlyProcessedTabbar ?? "nil") guard let tabBar = self.currentlyProcessedTabbar else { return imageView.tintColor } - // Access the selected item in the tabbar. - // Important note: our hypothesis is that each item uses a different image. - + + // Retrieve the tab bar item containing the imageView. let currentItemInSelectedState = tabBar.items?.first { - //$0.image?.dd.srIdentifier == imageView.image?.dd.srIdentifier - let itemImage = $0.image let itemSelectedImage = $0.selectedImage - //return itemImage?.pngData() == imageViewImage.pngData() - let sameImage = itemSelectedImage?.pngData() == imageViewImage.pngData() - return sameImage + + // Important note when comparing the different tab bar items' icons: + // our hypothesis is that each item uses a different image. + return itemSelectedImage?.uniqueDescription == imageViewImage.uniqueDescription } - // If item not selected, we return the unselectedItemTintColor, - // or the default gray color. + // If the item is not selected, + // return the unselectedItemTintColor, + // or the default gray color if not set. if currentItemInSelectedState == nil || tabBar.selectedItem != currentItemInSelectedState { let unselectedColor = tabBar.unselectedItemTintColor ?? .lightGray.withAlphaComponent(0.5) return tabBar.unselectedItemTintColor ?? .systemGray.withAlphaComponent(0.5) } - // Otherwise we return the tabbar tint color, - // or the default blue color. + // Otherwise, return the tab bar tint color, + // or the default blue color if not set. let selectedColor = tabBar.tintColor ?? UIColor.systemBlue return tabBar.tintColor ?? UIColor.systemBlue - }/*, - shouldRecordImagePredicate: {_ in - return true - }*/ - )/*, - UILabelRecorder()*/ + } + ), + UILabelRecorder(), + // This is for recording the badge view + UIViewRecorder() ] ) }() - /*init() { - self.subtreeViewRecorder = { - - }() - }*/ - func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { guard let tabBar = view as? UITabBar else { return nil } - //print("isVisible:", attributes.isVisible) currentlyProcessedTabbar = tabBar - //print("semantics -- currentlyProcessedTabbar:", currentlyProcessedTabbar ?? "nil") - let subtreeRecordingResults = subtreeViewRecorder.record(tabBar, in: context) let builder = UITabBarWireframesBuilder( wireframeRect: inferOccupiedFrame(of: tabBar, in: context), wireframeID: context.ids.nodeID(view: tabBar, nodeRecorder: self), attributes: attributes, color: inferColor(of: tabBar) ) - let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - let allNodes = subtreeRecordingResults.nodes + [node] - print("allNodes:", allNodes.count) - - for node in allNodes { - //print("node:", node.viewAttributes.frame) - } - //print(allNodes) - return SpecificElement(subtreeStrategy: .record, nodes: allNodes) + let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - /*if let subtreeRecorder { - print("subtree nodes:", subtreeRecorder.nodes.count) - return SpecificElement(subtreeStrategy: .ignore, nodes: [node] + subtreeRecorder.nodes) - } else { - return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) - }*/ + let subtreeRecordingResults = subtreeViewRecorder.record(tabBar, in: context) + let allNodes = [node] + subtreeRecordingResults.nodes + let resources = subtreeRecordingResults.resources + return SpecificElement(subtreeStrategy: .ignore, nodes: allNodes, resources: resources) } private func inferOccupiedFrame(of tabBar: UITabBar, in context: ViewTreeRecordingContext) -> CGRect { @@ -124,7 +79,6 @@ internal class UITabBarRecorder: NodeRecorder { let subviewFrame = subview.convert(subview.bounds, to: context.coordinateSpace) occupiedFrame = occupiedFrame.union(subviewFrame) } - print("Calculated occupied frame: \(occupiedFrame)") return occupiedFrame } @@ -157,8 +111,6 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { let color: CGColor func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { - print("Building wireframes for UITabBar with rect: \(wireframeRect)") - print("alpha:", attributes.alpha) return [ builder.createShapeWireframe( @@ -174,3 +126,18 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { } } #endif + +fileprivate extension UIImage { + var uniqueDescription: String? { + // Some images may not have an associated CGImage, + // e.g., vector-based images (PDF, SVG), CIImage. + // In the case of tab bar icons, + // it is likely they have an associated CGImage. + guard let cgImage = self.cgImage else { return nil } + // Combine properties to create an unique ID. + // Note: it is unlikely but not impossible for two different images to have the same ID. + // This could occur if two images have identical properties and pixel structures. + // In many use cases, such as tab bar icons in an app, the risk of collision is acceptable. + return "\(cgImage.width)x\(cgImage.height)-\(cgImage.bitsPerComponent)x\(cgImage.bitsPerPixel)-\(cgImage.bytesPerRow)-\(cgImage.bitmapInfo)" + } +} From 3f7c67ef07e92ca3c1968f10d356a2c62de7ce75 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Fri, 14 Jun 2024 18:40:09 +0200 Subject: [PATCH 015/110] RUM-4883 Fix --- .../SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index e32d1176d7..f70310ffbe 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -316,7 +316,7 @@ final class SRSnapshotTests: SnapshotTestCase { DDAssertSnapshotTest( newImage: image, snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(privacyMode)-privacy"), - record: true + record: recordingMode ) } @@ -329,7 +329,7 @@ final class SRSnapshotTests: SnapshotTestCase { DDAssertSnapshotTest( newImage: image, snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(fileNamePrefix)-\(privacyMode)-privacy"), - record: true + record: recordingMode ) } @@ -342,7 +342,7 @@ final class SRSnapshotTests: SnapshotTestCase { DDAssertSnapshotTest( newImage: image, snapshotLocation: .folder(named: snapshotsFolderPath, fileNameSuffix: "-\(fileNamePrefix)-\(privacyMode)-privacy"), - record: true + record: recordingMode ) } } From 03e8745fc0d56a99a4d59004ce756c9fb59050d8 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Mon, 17 Jun 2024 10:22:16 +0200 Subject: [PATCH 016/110] RUM-4883 Update snapshots with new CI device iPhone 15 iOS 17.5 --- .../_snapshots_/pointers/testTabBars()-allow-privacy.png.json | 2 +- .../testTabBars()-embeddedtabbar-allow-privacy.png.json | 2 +- .../pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json | 2 +- ...s()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json | 2 +- ...rs()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json | 2 +- .../_snapshots_/pointers/testTabBars()-mask-privacy.png.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json index 68d3ce1abd..d1f2ed15e9 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"964e6204a1bce78fb09e2794a40bbab30bc4a34c"} \ No newline at end of file +{"hash":"1da8a1b6695189e577d1fafa8163d23265dadcca"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json index 97b21559cb..16062b567e 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"0c9a33804f16daa6889413e011e0cfb318f1880b"} \ No newline at end of file +{"hash":"d3ee8deeae90af694b8d7df8992ea6a2750ca1de"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json index 4e88eeb4ea..58cbc5602e 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"10711d5599bdc5eb74b976e5f45d3c5f14d878bd"} \ No newline at end of file +{"hash":"bc44daa2a90fb6280f6ba9287c0bb5fdd80f8d8d"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json index 702d6cacce..f1f1eda2b9 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"cc6aa28b47e5a305809e246475630e416f900c2c"} \ No newline at end of file +{"hash":"8ff9b990e4dffb0bce6e3314cffb15beb24fe874"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json index 2884bae170..69500d48bf 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"9d91581c014c31063416c7a392107859439307b3"} \ No newline at end of file +{"hash":"6c19300b2a3b20699ea9ead38b280d948127a894"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json index 50567c584e..a7ebeb0555 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"26aa1c124c13a842b4be63be2fd81680aac7b3a5"} \ No newline at end of file +{"hash":"a265a30f128d8d4bea8d7238812a2f9cbc53a64d"} \ No newline at end of file From ad6e3599eaa0d6a49fd0006a7599513b6858519d Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Mon, 17 Jun 2024 10:47:37 +0200 Subject: [PATCH 017/110] RUM-4883 Fix syntax --- .../NodeRecorders/UITabBarRecorder.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index 20385a3420..e63dcebc6c 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -16,10 +16,13 @@ internal final class UITabBarRecorder: NodeRecorder { nodeRecorders: [ UIImageViewRecorder( tintColorProvider: { imageView in - guard let imageViewImage = imageView.image else { return nil } - guard let tabBar = self.currentlyProcessedTabbar else { return imageView.tintColor } + guard let imageViewImage = imageView.image else { + return nil + } + guard let tabBar = self.currentlyProcessedTabbar else { + return imageView.tintColor + } - // Retrieve the tab bar item containing the imageView. let currentItemInSelectedState = tabBar.items?.first { let itemSelectedImage = $0.selectedImage @@ -111,7 +114,6 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { let color: CGColor func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { - return [ builder.createShapeWireframe( id: wireframeID, @@ -133,7 +135,9 @@ fileprivate extension UIImage { // e.g., vector-based images (PDF, SVG), CIImage. // In the case of tab bar icons, // it is likely they have an associated CGImage. - guard let cgImage = self.cgImage else { return nil } + guard let cgImage = self.cgImage else { + return nil + } // Combine properties to create an unique ID. // Note: it is unlikely but not impossible for two different images to have the same ID. // This could occur if two images have identical properties and pixel structures. From c8e64d4352e0151f7c7fb5f30871e82bd8ddd729 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Mon, 17 Jun 2024 11:02:51 +0200 Subject: [PATCH 018/110] RUM-4883 Fix tests --- .../ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift index 3f0b84f277..ed8410e9a4 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift @@ -20,7 +20,7 @@ class UITabBarRecorderTests: XCTestCase { // Then let semantics = try XCTUnwrap(recorder.semantics(of: tabBar, with: viewAttributes, in: .mockAny())) XCTAssertTrue(semantics is SpecificElement) - XCTAssertEqual(semantics.subtreeStrategy, .record) + XCTAssertEqual(semantics.subtreeStrategy, .ignore) XCTAssertTrue(semantics.nodes.first?.wireframesBuilder is UITabBarWireframesBuilder) } From 241888f5afcaefe789e4b70da9c5935219ed8352 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Mon, 17 Jun 2024 11:35:46 +0200 Subject: [PATCH 019/110] RUM-4883 Remove tabbar from image use case snapshot tests --- .../Resources/Storyboards/Images.storyboard | 36 +++++-------------- .../ImagesViewControllers.swift | 3 -- .../testImages()-allow-privacy.png.json | 2 +- .../testImages()-mask-privacy.png.json | 2 +- 4 files changed, 10 insertions(+), 33 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard index 11f5e78166..497e2bfebf 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard @@ -1,9 +1,9 @@ - + - + @@ -156,23 +156,8 @@ - - - - - - - - - - - - - - - - + @@ -203,18 +188,15 @@ - - - - + @@ -224,7 +206,6 @@ - @@ -237,7 +218,6 @@ - @@ -246,16 +226,16 @@ - + - + - + - + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/ViewControllers/ImagesViewControllers.swift b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/ViewControllers/ImagesViewControllers.swift index 9cb390ac89..330a6c492a 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/ViewControllers/ImagesViewControllers.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/ViewControllers/ImagesViewControllers.swift @@ -10,7 +10,6 @@ internal class ImagesViewController: UIViewController { @IBOutlet weak var customButton: UIButton! @IBOutlet weak var customImageView: UIImageView! @IBOutlet weak var contentImageView: UIImageView! - @IBOutlet weak var tabBar: UITabBar! @IBOutlet weak var navigationBar: UINavigationBar! override func viewDidLoad() { @@ -22,8 +21,6 @@ internal class ImagesViewController: UIViewController { let color = UIColor(white: 0, alpha: 0.05) customButton.setBackgroundImage(UIImage(color: color), for: .normal) - tabBar.backgroundImage = UIImage(color: color) - tabBar.selectedItem = tabBar.items?.first navigationBar.setBackgroundImage(UIImage(color: color), for: .default) let image = UIImage(named: "dd_logo", in: .module, with: nil) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-allow-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-allow-privacy.png.json index 288960b69a..b18a3d9365 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-allow-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-allow-privacy.png.json @@ -1 +1 @@ -{"hash":"f3722bae1de59f237e6df310c8f9e433c25a2fbf"} \ No newline at end of file +{"hash":"9d85a526862ec0fe1ffd1f098048aeb53520fda0"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-mask-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-mask-privacy.png.json index bca5494fe7..1e5aafceea 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-mask-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages()-mask-privacy.png.json @@ -1 +1 @@ -{"hash":"7a54c4867cd6cf497b8542a0e66d2a63ae6786ea"} \ No newline at end of file +{"hash":"f9ec38cae01176a6c866c3210931a383cabfd441"} \ No newline at end of file From 153948a6001edb5ac6901b3a22e52a7311adbf9c Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 17 Jun 2024 13:58:23 +0200 Subject: [PATCH 020/110] RUM-4591 Make `endMetric()` thread safe --- .../Sources/SDKMetrics/SessionEndedMetricController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index dcb2f78b25..797e93ac44 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -80,11 +80,11 @@ internal final class SessionEndedMetricController { /// Ends the metric for a given session, sending it to telemetry and removing it from pending metrics. /// - Parameter sessionID: The ID of the session to end the metric for. func endMetric(sessionID: RUMUUID, with context: DatadogContext) { - guard let metric = metricsBySessionID[sessionID] else { - return - } - telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes(with: context)) _metricsBySessionID.mutate { metrics in + guard let metric = metrics[sessionID] else { + return + } + telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes(with: context)) metrics[sessionID] = nil pendingSessionIDs.removeAll(where: { $0 == sessionID }) // O(n), but "ending the metric" is very rare event } From 0358d1b43fcc41031d216d3a39240d8a7f6efe63 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Mon, 10 Jun 2024 11:40:41 +0200 Subject: [PATCH 021/110] RUM-3463 feat(watchdog-termination): track app state and detect watchdog terminations --- Datadog/Datadog.xcodeproj/project.pbxproj | 82 +++++ .../Mocks/CrashReportingFeatureMocks.swift | 2 + .../Sources/CrashReportingFeature.swift | 4 + .../Integrations/CrashReportSender.swift | 34 ++ .../Sources/Context/AppState.swift | 4 +- .../Sources/Context/DeviceInfo.swift | 43 ++- DatadogInternal/Sources/Context/Sysctl.swift | 52 ++- .../MessageBus/FeatureMessageReceiver.swift | 2 + .../Models/CrashReporting/LaunchReport.swift | 31 ++ DatadogRUM/Sources/Feature/RUMDataStore.swift | 5 +- DatadogRUM/Sources/Feature/RUMFeature.swift | 23 +- .../Instrumentation/RUMInstrumentation.swift | 9 +- .../WatchdogTerminationAppState.swift | 57 ++++ .../WatchdogTerminationAppStateManager.swift | 140 ++++++++ .../WatchdogTerminationChecker.swift | 123 +++++++ .../WatchdogTerminationMonitor.swift | 89 +++++ .../WatchdogTerminationReporter.swift | 22 ++ .../Integrations/LaunchReportReceiver.swift | 40 +++ .../RUMInstrumentationTests.swift | 21 +- ...chdogTerminationAppStateManagerTests.swift | 76 +++++ .../WatchdogTerminationCheckerTests.swift | 320 ++++++++++++++++++ .../WatchdogTerminationMocks.swift | 103 ++++++ .../WatchdogTerminationMonitorTests.swift | 103 ++++++ TestUtilities/Mocks/DatadogContextMock.swift | 18 +- 24 files changed, 1378 insertions(+), 25 deletions(-) create mode 100644 DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift create mode 100644 DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift create mode 100644 DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift create mode 100644 DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift create mode 100644 DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift create mode 100644 DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift create mode 100644 DatadogRUM/Sources/Integrations/LaunchReportReceiver.swift create mode 100644 DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift create mode 100644 DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift create mode 100644 DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift create mode 100644 DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 6c60348a5f..f160ae0a02 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 1434A4642B7F73170072E3BB /* OpenTelemetryApi.xcframework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1434A4662B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; 1434A4672B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; + 3C0CB3452C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */; }; + 3C0CB3462C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */; }; 3C0D5DD72A543B3B00446CF9 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DD62A543B3B00446CF9 /* Event.swift */; }; 3C0D5DD82A543B3B00446CF9 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DD62A543B3B00446CF9 /* Event.swift */; }; 3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */; }; @@ -38,7 +40,11 @@ 3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; }; 3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; }; 3C33E4072BEE35A8003B2988 /* RUMContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */; }; + 3C3EF2B02C1AEBAB009E9E57 /* LaunchReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */; }; + 3C3EF2B12C1AEBAB009E9E57 /* LaunchReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */; }; 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; + 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; + 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; 3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; 3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; 3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; }; @@ -95,8 +101,24 @@ 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */; }; 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3CE11A1229F7BE0900202522 /* DatadogWebViewTracking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3CEC57732C16FD0B0042B5F2 /* WatchdogTerminationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */; }; + 3CEC57742C16FD0C0042B5F2 /* WatchdogTerminationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */; }; + 3CEC57772C16FDD70042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */; }; + 3CEC57782C16FDD80042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */; }; 3CF673362B4807490016CE17 /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF673352B4807490016CE17 /* OTelSpanTests.swift */; }; 3CF673372B4807490016CE17 /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF673352B4807490016CE17 /* OTelSpanTests.swift */; }; + 3CFF4F8B2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */; }; + 3CFF4F8C2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */; }; + 3CFF4F912C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */; }; + 3CFF4F922C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */; }; + 3CFF4F942C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */; }; + 3CFF4F952C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */; }; + 3CFF4F972C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */; }; + 3CFF4F982C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */; }; + 3CFF4F9A2C0DBE4C006F191D /* LaunchReportReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F992C0DBE4C006F191D /* LaunchReportReceiver.swift */; }; + 3CFF4F9B2C0DBE4C006F191D /* LaunchReportReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F992C0DBE4C006F191D /* LaunchReportReceiver.swift */; }; + 3CFF4FA42C0E0FE8006F191D /* WatchdogTerminationCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */; }; + 3CFF4FA52C0E0FE9006F191D /* WatchdogTerminationCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */; }; 3CFF5D492B555F4F00FC483A /* OTelTracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */; }; 3CFF5D4A2B555F4F00FC483A /* OTelTracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */; }; 49274906288048B500ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; @@ -2063,6 +2085,7 @@ /* Begin PBXFileReference section */ 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugOTelTracingViewController.swift; sourceTree = ""; }; + 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationReporter.swift; sourceTree = ""; }; 3C0D5DD62A543B3B00446CF9 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 3C0D5DDC2A543D5D00446CF9 /* EventGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGenerator.swift; sourceTree = ""; }; 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventGeneratorTests.swift; sourceTree = ""; }; @@ -2075,6 +2098,8 @@ 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLink.swift; sourceTree = ""; }; 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLinkTests.swift; sourceTree = ""; }; 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextMocks.swift; sourceTree = ""; }; + 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchReport.swift; sourceTree = ""; }; + 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitorTests.swift; sourceTree = ""; }; 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+Datadog.swift"; sourceTree = ""; }; 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+DatadogTests.swift"; sourceTree = ""; }; 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = ""; }; @@ -2101,7 +2126,15 @@ 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDTests.swift; sourceTree = ""; }; 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogWebViewTracking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogWebViewTrackingTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMocks.swift; sourceTree = ""; }; + 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationAppStateManagerTests.swift; sourceTree = ""; }; 3CF673352B4807490016CE17 /* OTelSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanTests.swift; sourceTree = ""; }; + 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationAppState.swift; sourceTree = ""; }; + 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationAppStateManager.swift; sourceTree = ""; }; + 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationChecker.swift; sourceTree = ""; }; + 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitor.swift; sourceTree = ""; }; + 3CFF4F992C0DBE4C006F191D /* LaunchReportReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchReportReceiver.swift; sourceTree = ""; }; + 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationCheckerTests.swift; sourceTree = ""; }; 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelTracerProvider.swift; sourceTree = ""; }; 49274903288048AA00ECD49B /* InternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalProxyTests.swift; sourceTree = ""; }; 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMInternalProxyTests.swift; sourceTree = ""; }; @@ -3360,6 +3393,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */ = { + isa = PBXGroup; + children = ( + 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */, + 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */, + 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */, + 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */, + 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */, + ); + path = WatchdogTerminations; + sourceTree = ""; + }; 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = { isa = PBXGroup; children = ( @@ -3413,6 +3458,17 @@ path = ../DatadogWebViewTracking/Tests; sourceTree = ""; }; + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */ = { + isa = PBXGroup; + children = ( + 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */, + 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */, + 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */, + 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */, + ); + path = WatchdogTerminations; + sourceTree = ""; + }; 61020C272757AD63005EEAEA /* BackgroundEvents */ = { isa = PBXGroup; children = ( @@ -4644,6 +4700,7 @@ D236BE2729520FED00676E67 /* CrashReportReceiver.swift */, D215ED6A29D2E1080046B721 /* ErrorMessageReceiver.swift */, D214DAA729E54CB4004D0AE8 /* TelemetryReceiver.swift */, + 3CFF4F992C0DBE4C006F191D /* LaunchReportReceiver.swift */, 61DCC84D2C071DCD00CB59E5 /* TelemetryInterceptor.swift */, ); path = Integrations; @@ -4714,6 +4771,7 @@ 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */, 6167E6F52B81E94C00C3CA2D /* DDThread.swift */, 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */, + 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */, ); path = CrashReporting; sourceTree = ""; @@ -4757,6 +4815,7 @@ 6157FA5C252767B3009A8A3B /* Resources */, 9E06058F26EF904200F5F935 /* LongTasks */, 6167E6D12B7F8B1300C3CA2D /* AppHangs */, + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */, ); path = Instrumentation; sourceTree = ""; @@ -5419,6 +5478,7 @@ 6141014C251A577D00E3C2D9 /* Actions */, 613F23EF252B1287006CD2D7 /* Resources */, 6167E6D82B80047900C3CA2D /* AppHangs */, + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */, 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */, ); path = Instrumentation; @@ -8577,6 +8637,7 @@ D2F8235329915E12003C7E99 /* DatadogSite.swift in Sources */, D2D3199A29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */, 6128F56A2BA2237300D35B08 /* DataStore.swift in Sources */, + 3C3EF2B02C1AEBAB009E9E57 /* LaunchReport.swift in Sources */, 6167E7002B81EF7500C3CA2D /* BacktraceReportingFeature.swift in Sources */, D2EBEE2729BA160F00B15732 /* B3HTTPHeadersWriter.swift in Sources */, D23039E2298D5236001A1FA3 /* UserInfo.swift in Sources */, @@ -8621,6 +8682,7 @@ D253EE972B988CA90010B589 /* ViewCache.swift in Sources */, D23F8E5A29DDCD28001CFAE8 /* RUMResourceScope.swift in Sources */, D23F8E5C29DDCD28001CFAE8 /* RUMApplicationScope.swift in Sources */, + 3CFF4F982C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, D23F8E5D29DDCD28001CFAE8 /* SwiftUIViewModifier.swift in Sources */, D23F8E5E29DDCD28001CFAE8 /* VitalInfo.swift in Sources */, D23F8E5F29DDCD28001CFAE8 /* UIApplicationSwizzler.swift in Sources */, @@ -8630,6 +8692,7 @@ D23F8E6329DDCD28001CFAE8 /* RUMDataModels.swift in Sources */, 61C713AB2A3B790B00FA735A /* Monitor.swift in Sources */, D23F8E6429DDCD28001CFAE8 /* SwiftUIViewHandler.swift in Sources */, + 3CFF4F922C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, D23F8E6529DDCD28001CFAE8 /* RUMFeature.swift in Sources */, D23F8E6629DDCD28001CFAE8 /* RUMDebugging.swift in Sources */, D23F8E6729DDCD28001CFAE8 /* RUMUUID.swift in Sources */, @@ -8643,6 +8706,7 @@ 49D8C0B82AC5D2160075E427 /* RUM+Internal.swift in Sources */, D23F8E6E29DDCD28001CFAE8 /* RUMViewsHandler.swift in Sources */, 61C713BA2A3C935C00FA735A /* RUM.swift in Sources */, + 3C0CB3462C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */, D23F8E6F29DDCD28001CFAE8 /* RequestBuilder.swift in Sources */, D224430529E9588500274EC7 /* TelemetryReceiver.swift in Sources */, D23F8E7029DDCD28001CFAE8 /* URLSessionRUMResourcesHandler.swift in Sources */, @@ -8675,6 +8739,7 @@ D23F8E8329DDCD28001CFAE8 /* RUMUser.swift in Sources */, D23F8E8429DDCD28001CFAE8 /* UIKitRUMUserActionsPredicate.swift in Sources */, D23F8E8529DDCD28001CFAE8 /* SwiftUIExtensions.swift in Sources */, + 3CFF4F952C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */, D23F8E8629DDCD28001CFAE8 /* RUMDataModelsMapping.swift in Sources */, D23F8E8729DDCD28001CFAE8 /* RUMInstrumentation.swift in Sources */, D23F8E8829DDCD28001CFAE8 /* VitalCPUReader.swift in Sources */, @@ -8684,8 +8749,10 @@ D23F8E8C29DDCD28001CFAE8 /* RUMBaggageKeys.swift in Sources */, 6174D6212C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, D23F8E8D29DDCD28001CFAE8 /* VitalRefreshRateReader.swift in Sources */, + 3CFF4F8C2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, D23F8E8E29DDCD28001CFAE8 /* UIKitRUMUserActionsHandler.swift in Sources */, D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */, + 3CFF4F9B2C0DBE4C006F191D /* LaunchReportReceiver.swift in Sources */, 61DCC84F2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8706,16 +8773,19 @@ D23F8EA629DDCD38001CFAE8 /* RUMDeviceInfoTests.swift in Sources */, D23F8EA829DDCD38001CFAE8 /* RUMResourceScopeTests.swift in Sources */, D23F8EA929DDCD38001CFAE8 /* RUMOperatingSystemInfoTests.swift in Sources */, + 3CFF4FA52C0E0FE9006F191D /* WatchdogTerminationCheckerTests.swift in Sources */, D23F8EAB29DDCD38001CFAE8 /* RUMDataModelMocks.swift in Sources */, D23F8EAC29DDCD38001CFAE8 /* RUMDataModelsMappingTests.swift in Sources */, D23F8EAD29DDCD38001CFAE8 /* RUMEventBuilderTests.swift in Sources */, 61CE2E602BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */, + 3CEC57782C16FDD80042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */, D23F8EAE29DDCD38001CFAE8 /* DDTAssertValidRUMUUID.swift in Sources */, D23F8EAF29DDCD38001CFAE8 /* RUMScopeTests.swift in Sources */, D23F8EB029DDCD38001CFAE8 /* SessionReplayDependencyTests.swift in Sources */, 61C713B72A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, D23F8EB129DDCD38001CFAE8 /* RUMViewScopeTests.swift in Sources */, D224431029E977A100274EC7 /* TelemetryReceiverTests.swift in Sources */, + 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D23F8EB229DDCD38001CFAE8 /* ValuePublisherTests.swift in Sources */, 6174D61B2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, @@ -8732,6 +8802,7 @@ D23F8EBE29DDCD38001CFAE8 /* WebViewEventReceiverTests.swift in Sources */, D23F8EBF29DDCD38001CFAE8 /* URLSessionRUMResourcesHandlerTests.swift in Sources */, D23F8EC029DDCD38001CFAE8 /* RUMEventSanitizerTests.swift in Sources */, + 3CEC57742C16FD0C0042B5F2 /* WatchdogTerminationMocks.swift in Sources */, D253EE9C2B98B37C0010B589 /* ViewCacheTests.swift in Sources */, 6176C1732ABDBA2E00131A70 /* MonitorTests.swift in Sources */, D23F8EC129DDCD38001CFAE8 /* RUMEventsMapperTests.swift in Sources */, @@ -8941,6 +9012,7 @@ D253EE962B988CA90010B589 /* ViewCache.swift in Sources */, D29A9F8429DD85BB005C54A4 /* RUMResourceScope.swift in Sources */, D29A9F7329DD85BB005C54A4 /* RUMApplicationScope.swift in Sources */, + 3CFF4F972C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, D29A9F6A29DD85BB005C54A4 /* SwiftUIViewModifier.swift in Sources */, D29A9F6429DD85BB005C54A4 /* VitalInfo.swift in Sources */, D29A9F6D29DD85BB005C54A4 /* UIApplicationSwizzler.swift in Sources */, @@ -8950,6 +9022,7 @@ D29A9F7B29DD85BB005C54A4 /* RUMDataModels.swift in Sources */, 61C713AA2A3B790B00FA735A /* Monitor.swift in Sources */, D29A9F8529DD85BB005C54A4 /* SwiftUIViewHandler.swift in Sources */, + 3CFF4F912C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, D29A9F7429DD85BB005C54A4 /* RUMFeature.swift in Sources */, D29A9F7729DD85BB005C54A4 /* RUMDebugging.swift in Sources */, D29A9F6E29DD85BB005C54A4 /* RUMUUID.swift in Sources */, @@ -8963,6 +9036,7 @@ 49D8C0B72AC5D2160075E427 /* RUM+Internal.swift in Sources */, D29A9F7629DD85BB005C54A4 /* RUMViewsHandler.swift in Sources */, 61C713B92A3C935C00FA735A /* RUM.swift in Sources */, + 3C0CB3452C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */, D29A9F7929DD85BB005C54A4 /* RequestBuilder.swift in Sources */, D224430429E9588100274EC7 /* TelemetryReceiver.swift in Sources */, D29A9F5729DD85BB005C54A4 /* URLSessionRUMResourcesHandler.swift in Sources */, @@ -8995,6 +9069,7 @@ D29A9F6629DD85BB005C54A4 /* RUMUser.swift in Sources */, D29A9F8229DD85BB005C54A4 /* UIKitRUMUserActionsPredicate.swift in Sources */, D29A9F8E29DD8665005C54A4 /* SwiftUIExtensions.swift in Sources */, + 3CFF4F942C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */, D29A9F7829DD85BB005C54A4 /* RUMDataModelsMapping.swift in Sources */, D29A9F6F29DD85BB005C54A4 /* RUMInstrumentation.swift in Sources */, D29A9F7A29DD85BB005C54A4 /* VitalCPUReader.swift in Sources */, @@ -9004,8 +9079,10 @@ D29A9F8329DD85BB005C54A4 /* RUMBaggageKeys.swift in Sources */, 6174D6202C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, D29A9F8929DD85BB005C54A4 /* VitalRefreshRateReader.swift in Sources */, + 3CFF4F8B2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, D29A9F6929DD85BB005C54A4 /* UIKitRUMUserActionsHandler.swift in Sources */, D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */, + 3CFF4F9A2C0DBE4C006F191D /* LaunchReportReceiver.swift in Sources */, 61DCC84E2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -9026,16 +9103,19 @@ D29A9FA329DDB483005C54A4 /* RUMDeviceInfoTests.swift in Sources */, D29A9FBC29DDB483005C54A4 /* RUMResourceScopeTests.swift in Sources */, D29A9FB029DDB483005C54A4 /* RUMOperatingSystemInfoTests.swift in Sources */, + 3CFF4FA42C0E0FE8006F191D /* WatchdogTerminationCheckerTests.swift in Sources */, D29A9FC629DDBA8A005C54A4 /* RUMDataModelMocks.swift in Sources */, D29A9FD529DDC624005C54A4 /* RUMDataModelsMappingTests.swift in Sources */, D29A9FBE29DDB483005C54A4 /* RUMEventBuilderTests.swift in Sources */, 61CE2E5F2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */, + 3CEC57772C16FDD70042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */, D29A9FCC29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift in Sources */, D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */, D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */, 61C713B62A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, D29A9FB829DDB483005C54A4 /* RUMViewScopeTests.swift in Sources */, D224430F29E9779F00274EC7 /* TelemetryReceiverTests.swift in Sources */, + 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */, 6174D61A2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, @@ -9052,6 +9132,7 @@ D29A9FA429DDB483005C54A4 /* WebViewEventReceiverTests.swift in Sources */, D29A9F9A29DDB483005C54A4 /* URLSessionRUMResourcesHandlerTests.swift in Sources */, D29A9FA229DDB483005C54A4 /* RUMEventSanitizerTests.swift in Sources */, + 3CEC57732C16FD0B0042B5F2 /* WatchdogTerminationMocks.swift in Sources */, D253EE9B2B98B37B0010B589 /* ViewCacheTests.swift in Sources */, 6176C1722ABDBA2E00131A70 /* MonitorTests.swift in Sources */, D29A9FB929DDB483005C54A4 /* RUMEventsMapperTests.swift in Sources */, @@ -9525,6 +9606,7 @@ D2F8235429915E12003C7E99 /* DatadogSite.swift in Sources */, D2D3199B29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */, 6128F56B2BA2237300D35B08 /* DataStore.swift in Sources */, + 3C3EF2B12C1AEBAB009E9E57 /* LaunchReport.swift in Sources */, 6167E7012B81EF7500C3CA2D /* BacktraceReportingFeature.swift in Sources */, D2EBEE3529BA161100B15732 /* B3HTTPHeadersWriter.swift in Sources */, D2DA2376298D57AA00C6C7E6 /* UserInfo.swift in Sources */, diff --git a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 97566f0494..864d6377a5 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -93,6 +93,8 @@ class CrashReportSenderMock: CrashReportSender { } var didSendCrashReport: (() -> Void)? + + func send(launch: DatadogInternal.LaunchReport) {} } class RUMCrashReceiverMock: FeatureMessageReceiver { diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift index fe99801329..28865bc645 100644 --- a/DatadogCrashReporting/Sources/CrashReportingFeature.swift +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -59,6 +59,8 @@ internal final class CrashReportingFeature: DatadogFeature { self.plugin.readPendingCrashReport { [weak self] crashReport in guard let self = self, let availableCrashReport = crashReport else { DD.logger.debug("No pending Crash found") + // TODO: RUM-4911 enable after `WatchdogTerminationReporter` is implemented. + // self?.sender.send(launch: .init(didCrash: false)) return false } @@ -71,6 +73,8 @@ internal final class CrashReportingFeature: DatadogFeature { } self.sender.send(report: availableCrashReport, with: crashContext) + // TODO: RUM-4911 enable after `WatchdogTerminationReporter` is implemented. + // self.sender.send(launch: .init(didCrash: true)) return true } } diff --git a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift index 0052cec46b..8322942119 100644 --- a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift +++ b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift @@ -14,6 +14,12 @@ internal protocol CrashReportSender { /// - report: The crash report. /// - context: The crash context func send(report: DDCrashReport, with context: CrashContext) + + /// Send the launch report and context to integrations. + /// + /// - Parameters: + /// - launch: The launch report. + func send(launch: LaunchReport) } /// An object for sending crash reports on the Core message-bus. @@ -66,4 +72,32 @@ internal struct MessageBusSender: CrashReportSender { } ) } + + /// Send the launch report and context to integrations. + /// + /// - Parameters: + /// - launch: The launch report. + func send(launch: DatadogInternal.LaunchReport) { + guard let core = core else { + return + } + + // We use baggage message to pass the launch report instead updating the global context + // because under the hood, some integrations start certain features based on the launch report (e.g. `WatchdogTerminationMonitor`). + // If we update the global context, the integrations will keep starting the features on every update which is not desired. + core.send( + message: .baggage( + key: LaunchReport.messageKey, + value: launch + ), + else: { + DD.logger.warn( + """ + In order to use Crash Reporting, RUM or Logging feature must be enabled. + Make sure `RUM` or `Logs` are enabled when initializing Datadog SDK. + """ + ) + } + ) + } } diff --git a/DatadogInternal/Sources/Context/AppState.swift b/DatadogInternal/Sources/Context/AppState.swift index f3a57a0236..12b4c4b5d6 100644 --- a/DatadogInternal/Sources/Context/AppState.swift +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -15,13 +15,15 @@ public enum AppState: Codable, PassthroughAnyCodable { case inactive /// The app is running in the background. case background + /// The app is terminated. + case terminated /// If the app is running in the foreground - no matter if receiving events or not (i.e. being interrupted because of transitioning from background). public var isRunningInForeground: Bool { switch self { case .active, .inactive: return true - case .background: + case .background, .terminated: return false } } diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift index 99b2f885b5..46583079a7 100644 --- a/DatadogInternal/Sources/Context/DeviceInfo.swift +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -31,13 +31,29 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { /// The architecture of the device public let architecture: String + /// The device is a simulator + public let isSimulator: Bool + + /// The vendor identifier of the device. + public let vendorId: String? + + /// Returns `true` if the debugger is attached. + public let isDebugging: Bool + + /// Returns system boot time since epoch. + public let systemBootTime: TimeInterval + public init( name: String, model: String, osName: String, osVersion: String, osBuildNumber: String?, - architecture: String + architecture: String, + isSimulator: Bool, + vendorId: String?, + isDebugging: Bool, + systemBootTime: TimeInterval ) { self.brand = "Apple" self.name = name @@ -46,6 +62,10 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { self.osVersion = osVersion self.osBuildNumber = osBuildNumber self.architecture = architecture + self.isSimulator = isSimulator + self.vendorId = vendorId + self.isDebugging = isDebugging + self.systemBootTime = systemBootTime } } @@ -62,17 +82,20 @@ extension DeviceInfo { /// - device: The `UIDevice` description. public init( processInfo: ProcessInfo = .processInfo, - device: UIDevice = .current + device: UIDevice = .current, + sysctl: SysctlProviding = Sysctl() ) { var architecture = "unknown" if let archInfo = NXGetLocalArchInfo()?.pointee { architecture = String(utf8String: archInfo.name) ?? "unknown" } - let build = try? Sysctl.osVersion() + let build = try? sysctl.osBuild() + let isDebugging = try? sysctl.isDebugging() + let systemBootTime = try? sysctl.systemBootTime() #if !targetEnvironment(simulator) - let model = try? Sysctl.model() + let model = try? sysctl.model() // Real iOS device self.init( name: device.model, @@ -80,7 +103,11 @@ extension DeviceInfo { osName: device.systemName, osVersion: device.systemVersion, osBuildNumber: build, - architecture: architecture + architecture: architecture, + isSimulator: false, + vendorId: device.identifierForVendor?.uuidString, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate ) #else let model = processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? device.model @@ -91,7 +118,11 @@ extension DeviceInfo { osName: device.systemName, osVersion: device.systemVersion, osBuildNumber: build, - architecture: architecture + architecture: architecture, + isSimulator: true, + vendorId: device.identifierForVendor?.uuidString, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate ) #endif } diff --git a/DatadogInternal/Sources/Context/Sysctl.swift b/DatadogInternal/Sources/Context/Sysctl.swift index 292c7309a6..37612cb7dd 100644 --- a/DatadogInternal/Sources/Context/Sysctl.swift +++ b/DatadogInternal/Sources/Context/Sysctl.swift @@ -15,15 +15,38 @@ import Foundation +/// A `SysctlProviding` implementation that uses `Darwin.sysctl` to access system information. +public protocol SysctlProviding { + /// Returns model of the device. + func model() throws -> String + + /// Returns operating system version. + /// - Returns: Operating system version. + func osBuild() throws -> String + + /// Returns system boot time since epoch. + /// It stays same across app restarts and only changes on the operating system reboot. + /// - Returns: System boot time. + func systemBootTime() throws -> TimeInterval + + /// Returns `true` if the app is being debugged. + /// - Returns: `true` if the app is being debugged. + func isDebugging() throws -> Bool +} + /// A "static"-only namespace around a series of functions that operate on buffers returned from the `Darwin.sysctl` function -internal struct Sysctl { +public struct Sysctl: SysctlProviding { /// Possible errors. enum Error: Swift.Error { case unknown case malformedUTF8 + case malformedData case posixError(POSIXErrorCode) } + public init() { + } + /// Access the raw data for an array of sysctl identifiers. private static func data(for keys: [Int32]) throws -> [Int8] { return try keys.withUnsafeBufferPointer { keysPointer throws -> [Int8] in @@ -63,7 +86,7 @@ internal struct Sysctl { /// e.g. "MacPro4,1" or "iPhone8,1" /// NOTE: this is *corrected* on iOS devices to fetch hw.machine - static func model() throws -> String { + public func model() throws -> String { #if os(iOS) && !arch(x86_64) && !arch(i386) // iOS device && not Simulator return try Sysctl.string(for: [CTL_HW, HW_MACHINE]) #else @@ -71,8 +94,31 @@ internal struct Sysctl { #endif } + /// Returns the operating system build as a human-readable string. /// e.g. "15D21" or "13D20" - static func osVersion() throws -> String { + public func osBuild() throws -> String { try Sysctl.string(for: [CTL_KERN, KERN_OSVERSION]) } + + /// Returns the system uptime in seconds. + public func systemBootTime() throws -> TimeInterval { + let bootTime = try Sysctl.data(for: [CTL_KERN, KERN_BOOTTIME]) + let uptime = bootTime.withUnsafeBufferPointer { buffer -> timeval? in + buffer.baseAddress?.withMemoryRebound(to: timeval.self, capacity: 1) { $0.pointee } + } + guard let uptime = uptime else { + throw Error.malformedData + } + return TimeInterval(uptime.tv_sec) + } + + /// Returns `true` if the debugger is attached to the current process. + /// https://developer.apple.com/library/archive/qa/qa1361/_index.html + public func isDebugging() throws -> Bool { + var info = kinfo_proc() + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + var size = MemoryLayout.stride + let junk = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) + return (info.kp_proc.p_flag & P_TRACED) != 0 + } } diff --git a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift index 9e95de1bf2..89558283bd 100644 --- a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift +++ b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift @@ -41,6 +41,8 @@ public struct NOPFeatureMessageReceiver: FeatureMessageReceiver { } } +/// A receiver that combines multiple receivers. It will loop though receivers and stop on the first that is able to +/// consume the given message. public struct CombinedFeatureMessageReceiver: FeatureMessageReceiver { let receivers: [FeatureMessageReceiver] diff --git a/DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift b/DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift new file mode 100644 index 0000000000..e3efc95ee8 --- /dev/null +++ b/DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift @@ -0,0 +1,31 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// Launch report format supported by Datadog SDK. +public struct LaunchReport: Codable, PassthroughAnyCodable { + /// The key used to encode/decode the `LaunchReport` while sending across features. + public static let messageKey = "launch-report" + + /// Returns `true` if the previous session crashed. + public let didCrash: Bool + + /// Creates a new `LaunchReport`. + /// - Parameter didCrash: `true` if the previous session crashed. + public init(didCrash: Bool) { + self.didCrash = didCrash + } +} + +extension LaunchReport: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + LaunchReport + - didCrash: \(didCrash) + """ + } +} diff --git a/DatadogRUM/Sources/Feature/RUMDataStore.swift b/DatadogRUM/Sources/Feature/RUMDataStore.swift index dd3996d195..acd2798463 100644 --- a/DatadogRUM/Sources/Feature/RUMDataStore.swift +++ b/DatadogRUM/Sources/Feature/RUMDataStore.swift @@ -30,6 +30,7 @@ internal struct RUMDataStore { /// References pending App Hang information. /// If found during app start it is considered a fatal hang in previous process. case fatalAppHangKey = "fatal-app-hang" + case watchdogAppStateKey = "watchdog-app-state" } /// Encodes values in RUM data store. @@ -45,7 +46,7 @@ internal struct RUMDataStore { let data = try RUMDataStore.encoder.encode(value) featureScope.dataStore.setValue(data, forKey: key.rawValue, version: version) } catch let error { - DD.logger.error("Failed to encode \(type(of: value)) in RUM Data Store") + DD.logger.error("Failed to encode \(type(of: value)) in RUM Data Store", error: error) featureScope.telemetry.error("Failed to encode \(type(of: value)) in RUM Data Store", error: error) } } @@ -64,7 +65,7 @@ internal struct RUMDataStore { let value = try RUMDataStore.decoder.decode(V.self, from: data) callback(value) } catch let error { - DD.logger.error("Failed to decode \(V.self) from RUM Data Store") + DD.logger.error("Failed to decode \(V.self) from RUM Data Store", error: error) featureScope.telemetry.error("Failed to decode \(V.self) from RUM Data Store", error: error) callback(nil) } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index ead9764c04..69916c4d60 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import UIKit internal final class RUMFeature: DatadogRemoteFeature { static let name = "rum" @@ -82,6 +83,20 @@ internal final class RUMFeature: DatadogRemoteFeature { dateProvider: configuration.dateProvider ) + let appStateManager = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: configuration.processID + ) + let watchdogTermination = WatchdogTerminationMonitor( + appStateManager: appStateManager, + checker: .init( + appStateManager: appStateManager, + deviceInfo: .init() + ), + reporter: WatchdogTerminationReporter(), + telemetry: featureScope.telemetry + ) + self.instrumentation = RUMInstrumentation( featureScope: featureScope, uiKitRUMViewsPredicate: configuration.uiKitViewsPredicate, @@ -92,7 +107,8 @@ internal final class RUMFeature: DatadogRemoteFeature { dateProvider: configuration.dateProvider, backtraceReporter: core.backtraceReporter, fatalErrorContext: dependencies.fatalErrorContext, - processID: configuration.processID + processID: configuration.processID, + watchdogTermination: watchdogTermination ) self.requestBuilder = RequestBuilder( customIntakeURL: configuration.customEndpoint, @@ -134,7 +150,9 @@ internal final class RUMFeature: DatadogRemoteFeature { } }(), eventsMapper: eventsMapper - ) + ), + LaunchReportReceiver(featureScope: featureScope, watchdogTermination: watchdogTermination), + appStateManager ) // Forward instrumentation calls to monitor: @@ -165,6 +183,7 @@ extension RUMFeature: Flushable { /// **blocks the caller thread** func flush() { instrumentation.appHangs?.flush() + instrumentation.watchdogTermination?.flush() } } diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 13fc7b0bf4..1eeb5de698 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import UIKit /// Bundles RUM instrumentation components. internal final class RUMInstrumentation: RUMCommandPublisher { @@ -36,6 +37,9 @@ internal final class RUMInstrumentation: RUMCommandPublisher { /// Instruments App Hangs. It is `nil` if hangs monitoring is not enabled. let appHangs: AppHangsMonitor? + /// Instruments Watchdog Terminations. + let watchdogTermination: WatchdogTerminationMonitor? + // MARK: - Initialization init( @@ -48,7 +52,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { dateProvider: DateProvider, backtraceReporter: BacktraceReporting, fatalErrorContext: FatalErrorContextNotifying, - processID: UUID + processID: UUID, + watchdogTermination: WatchdogTerminationMonitor ) { // Always create views handler (we can't know if it will be used by SwiftUI instrumentation) // and only swizzle `UIViewController` if UIKit instrumentation is configured: @@ -119,6 +124,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { self.uiApplicationSwizzler = uiApplicationSwizzler self.longTasks = longTasks self.appHangs = appHangs + self.watchdogTermination = watchdogTermination // Enable configured instrumentations: self.viewControllerSwizzler?.swizzle() @@ -133,6 +139,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { uiApplicationSwizzler?.unswizzle() longTasks?.stop() appHangs?.stop() + watchdogTermination?.stop() } func publish(to subscriber: RUMCommandSubscriber) { diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift new file mode 100644 index 0000000000..998fb7c291 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift @@ -0,0 +1,57 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +#if canImport(UIKit) +import UIKit +#endif + +/// Represents the app state observed during application lifecycle events such as application start, resume and termination. +/// This state is used to detect Watchdog Terminations. +internal struct WatchdogTerminationAppState: Codable { + /// The Application version provided by the `Bundle`. + let appVersion: String + + /// The Operating System version. + let osVersion: String + + /// Last time the system was booted. + let systemBootTime: TimeInterval + + /// Returns true, if the app is running with a debug configuration. + let isDebugging: Bool + + /// Returns true, if the app was terminated normally. + var wasTerminated: Bool + + /// Returns true, if the app was in the foreground. + var isActive: Bool + + /// The vendor identifier of the device. + /// This value can change when installing test builds using Xcode or when installing an app on a device using ad-hoc distribution. + let vendorId: String? + + /// The process identifier of the app. This value stays the same during SDK start and stop but the app stays in memory. + let processId: UUID +} + +extension WatchdogTerminationAppState: CustomDebugStringConvertible { + var debugDescription: String { + return """ + WatchdogTerminationAppState + - appVersion: \(appVersion) + - osVersion: \(osVersion) + - systemBootTime: \(systemBootTime) + - isDebugging: \(isDebugging) + - wasTerminated: \(wasTerminated) + - isActive: \(isActive) + - vendorId: \(vendorId ?? "nil") + - processId: \(processId) + """ + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift new file mode 100644 index 0000000000..a6f554d4ea --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift @@ -0,0 +1,140 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Manages the app state changes observed during application lifecycle events such as application start, resume and termination. +internal final class WatchdogTerminationAppStateManager { + let featureScope: FeatureScope + + /// The last app state observed during application lifecycle events. + @ReadWriteLock + var lastAppState: AppState? + + /// The status of the app state manager indicating if it is active or not. + /// When it is active, it listens to the app state changes and updates the app state in the data store. + @ReadWriteLock + var isActive: Bool + + /// The process identifier of the app whose state is being monitored. + let processId: UUID + + init(featureScope: FeatureScope, processId: UUID) { + self.featureScope = featureScope + self.isActive = false + self.processId = processId + } + + /// Starts the app state monitoring. Depending on the app state changes, it updates the app state in the data store. + /// For example, when the app goes to the background, the app state is updated with `isActive = false`.` + func start() throws { + DD.logger.debug("Start app state monitoring") + isActive = true + try storeCurrentAppState() + } + + /// Stops the app state monitoring. + func stop() throws { + DD.logger.debug("Stop app state monitoring") + isActive = false + } + + /// Deletes the app state from the data store. + func deleteAppState() { + DD.logger.debug("Deleting app state from data store") + featureScope.rumDataStore.removeValue(forKey: .watchdogAppStateKey) + } + + /// Updates the app state in the data store with the given block. + /// - Parameter block: The block to update the app state. + private func updateAppState(block: @escaping (inout WatchdogTerminationAppState?) -> Void) { + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + var appState = appState + block(&appState) + DD.logger.debug("Updating app state in data store") + self.featureScope.rumDataStore.setValue(appState, forKey: .watchdogAppStateKey) + } + } + + /// Builds the current app state and stores it in the data store. + private func storeCurrentAppState() throws { + try currentAppState { [self] appState in + featureScope.rumDataStore.setValue(appState, forKey: .watchdogAppStateKey) + } + } + + /// Reads the app state from the data store asynchronously. + /// - Parameter completion: The completion block called with the app state. + func readAppState(completion: @escaping (WatchdogTerminationAppState?) -> Void) { + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (state: WatchdogTerminationAppState?) in + DD.logger.debug("Reading app state from data store.") + completion(state) + } + } + + /// Builds the current app state asynchronously. + /// - Parameter completion: The completion block called with the app state. + func currentAppState(completion: @escaping (WatchdogTerminationAppState) -> Void) throws { + featureScope.context { context in + let state: WatchdogTerminationAppState = .init( + appVersion: context.version, + osVersion: context.device.osVersion, + systemBootTime: context.device.systemBootTime, + isDebugging: context.device.isDebugging, + wasTerminated: false, + isActive: true, + vendorId: context.device.vendorId, + processId: self.processId + ) + completion(state) + } + } +} + +extension WatchdogTerminationAppStateManager: FeatureMessageReceiver { + /// Receives the feature message and updates the app state based on the context message. + /// It relies on `ApplicationStatePublisher` context message to update the app state. + /// Other messages are ignored. + /// - Parameters: + /// - message: The feature message. + /// - core: The core instance. + /// - Returns: Always `false`, because it doesn't block the message propagation. + func receive(message: DatadogInternal.FeatureMessage, from core: any DatadogInternal.DatadogCoreProtocol) -> Bool { + guard isActive else { + return false + } + + switch message { + case .baggage, .webview, .telemetry: + break + case .context(let context): + let state = context.applicationStateHistory.currentSnapshot.state + + // the message received on multiple times whenever there is change in context + // but it may not be the application state, hence we guard against the same state + guard state != lastAppState else { + return false + } + switch state { + case .active: + updateAppState { state in + state?.isActive = true + } + case .inactive, .background: + updateAppState { state in + state?.isActive = false + } + case .terminated: + updateAppState { state in + state?.wasTerminated = true + } + } + lastAppState = state + } + return false + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift new file mode 100644 index 0000000000..10d4178e54 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift @@ -0,0 +1,123 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +#if canImport(UIKit) +import UIKit +#endif + +/// Checks if the app was terminated by Watchdog using heuristics. +/// It uses the app state information from the last app session and the current app session +/// to determine if the app was terminated by Watchdog. +internal final class WatchdogTerminationChecker { + let appStateManager: WatchdogTerminationAppStateManager + let deviceInfo: DeviceInfo + + init( + appStateManager: WatchdogTerminationAppStateManager, + deviceInfo: DeviceInfo + ) { + self.appStateManager = appStateManager + self.deviceInfo = deviceInfo + } + + /// Checks if the app was terminated by Watchdog. + /// - Parameters: + /// - launch: The launch report containing information about the app launch. + /// - completion: The completion block called with the result. + func isWatchdogTermination(launch: LaunchReport, completion: @escaping (Bool) -> Void) throws { + do { + try appStateManager.currentAppState { current in + self.appStateManager.readAppState { [weak self] previous in + guard let self = self else { + completion(false) + return + } + completion(self.isWatchdogTermination(launch: launch, from: previous, to: current)) + } + } + } catch let error { + DD.logger.error("Failed to check if Watchdog Termination occurred", error: error) + completion(false) + throw error + } + } + + /// Checks if the app was terminated by Watchdog. + /// - Parameters: + /// - launch: The launch report containing information about the app launch. + /// - previous: The previous app state stored in the data store from the last app session. + /// - current: The current app state of the app. + func isWatchdogTermination( + launch: LaunchReport, + from previous: WatchdogTerminationAppState?, + to current: WatchdogTerminationAppState + ) -> Bool { + DD.logger.debug(launch.debugDescription) + DD.logger.debug(previous.debugDescription) + DD.logger.debug(current.debugDescription) + + guard let previous = previous else { + return false + } + + // Watchdog Termination detection doesn't work on simulators. + guard deviceInfo.isSimulator == false else { + return false + } + + // When the app is running in debug mode, we can't reliably tell if it was a Watchdog Termination or not. + guard previous.isDebugging == false else { + return false + } + + // Is the app version different than the last time the app was opened? + guard previous.appVersion == current.appVersion else { + return false + } + + // Is there a crash from the last time the app was opened? + guard launch.didCrash == false else { + return false + } + + // Did we receive a termination call the last time the app was opened? + guard previous.wasTerminated == false else { + return false + } + + // Is the OS version different than the last time the app was opened? + guard previous.osVersion == current.osVersion else { + return false + } + + // Was the system rebooted since the last time the app was opened? + guard previous.systemBootTime == current.systemBootTime else { + return false + } + + // This value can change when installing test builds using Xcode or when installing an app + // on a device using ad-hoc distribution. + guard previous.vendorId == current.vendorId else { + return false + } + + // This is likely same process but another check due to stop & start of the SDK + guard previous.processId != current.processId else { + return false + } + + // Was the app in foreground/active ? + // If the app was in background we can't reliably tell if it was a Watchdog Termination or not. + guard previous.isActive else { + return false + } + + return true + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift new file mode 100644 index 0000000000..1124308d3e --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift @@ -0,0 +1,89 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Monitors the Watchdog Termination events and reports them to Datadog. +internal final class WatchdogTerminationMonitor { + enum ErrorMessages { + static let failedToCheckWatchdogTermination = "Failed to check if Watchdog Termination occurred" + static let failedToStartAppState = "Failed to start Watchdog Termination App State Manager" + static let failedToStopAppState = "Failed to stop Watchdog Termination App State Manager" + static let detectedWatchdogTermination = "Based on heuristics, previous app session was terminated by Watchdog" + static let detectedNonWatchdogTermination = "Previous app session was not terminated by Watchdog" + } + + let checker: WatchdogTerminationChecker + let appStateManager: WatchdogTerminationAppStateManager + let telemetry: Telemetry + let reporter: WatchdogTerminationReporting + + init( + appStateManager: WatchdogTerminationAppStateManager, + checker: WatchdogTerminationChecker, + reporter: WatchdogTerminationReporting, + telemetry: Telemetry + ) { + self.checker = checker + self.appStateManager = appStateManager + self.telemetry = telemetry + self.reporter = reporter + } + + /// Starts the Watchdog Termination Monitor. + /// - Parameter launchReport: The launch report containing information about the app launch (if available). + func start(launchReport: LaunchReport?) { + if let launchReport = launchReport { + sendWatchTerminationIfFound(launch: launchReport) + } + + do { + try appStateManager.start() + } catch let error { + DD.logger.error(ErrorMessages.failedToStartAppState, error: error) + telemetry.error(ErrorMessages.failedToStartAppState, error: error) + } + } + + /// Checks if the app was terminated by Watchdog and sends the Watchdog Termination event to Datadog. + /// - Parameter launch: The launch report containing information about the app launch. + private func sendWatchTerminationIfFound(launch: LaunchReport) { + do { + try checker.isWatchdogTermination(launch: launch) { isWatchdogTermination in + if isWatchdogTermination { + DD.logger.debug(ErrorMessages.detectedWatchdogTermination) + self.reporter.send() + } else { + DD.logger.debug(ErrorMessages.detectedNonWatchdogTermination) + } + } + } catch let error { + DD.logger.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) + telemetry.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) + } + } + + /// Stops the Watchdog Termination Monitor. + func stop() { + do { + try appStateManager.stop() + } catch { + DD.logger.error(ErrorMessages.failedToStopAppState, error: error) + telemetry.error(ErrorMessages.failedToStopAppState, error: error) + } + } +} + +extension WatchdogTerminationMonitor: Flushable { + /// Flushes the Watchdog Termination Monitor. It stops the monitor and deletes the app state. + /// - Note: This method must be called manually only or in the tests. + /// This will reset the app state and the monitor will not able to detect Watchdog Termination due to absence of the previous app state. + func flush() { + stop() + appStateManager.deleteAppState() + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift new file mode 100644 index 0000000000..3f92c194b7 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift @@ -0,0 +1,22 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Reports Watchdog Termination events to Datadog. +internal protocol WatchdogTerminationReporting { + /// Sends the Watchdog Termination event to Datadog. + func send() +} + +/// Default implementation of `WatchdogTerminationReporting`. +internal final class WatchdogTerminationReporter: WatchdogTerminationReporting { + /// Sends the Watchdog Termination event to Datadog. + func send() { + DD.logger.error("TODO: WatchdogTerminationReporter.report()") + } +} diff --git a/DatadogRUM/Sources/Integrations/LaunchReportReceiver.swift b/DatadogRUM/Sources/Integrations/LaunchReportReceiver.swift new file mode 100644 index 0000000000..1ca64a7d09 --- /dev/null +++ b/DatadogRUM/Sources/Integrations/LaunchReportReceiver.swift @@ -0,0 +1,40 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Receives `LaunchReport` from CrashReporter and starts the Watchdog Termination Monitor. +internal struct LaunchReportReceiver: FeatureMessageReceiver { + let featureScope: FeatureScope + let watchdogTermination: WatchdogTerminationMonitor? + + init( + featureScope: FeatureScope, + watchdogTermination: WatchdogTerminationMonitor? + ) { + self.featureScope = featureScope + self.watchdogTermination = watchdogTermination + } + + /// Receives `LaunchReport` from CrashReporter and starts the Watchdog Termination Monitor. + /// - Parameters: + /// - message: The message containing `LaunchReport`. + /// - core: The `DatadogCore` instance. + /// - Returns: `true` if the message was successfully received, `false` otherwise. + func receive(message: DatadogInternal.FeatureMessage, from core: any DatadogInternal.DatadogCoreProtocol) -> Bool { + do { + guard let launch: LaunchReport? = try message.baggage(forKey: LaunchReport.messageKey) else { + return false + } + watchdogTermination?.start(launchReport: launch) + return false + } catch { + featureScope.telemetry.error("Fails to decode LaunchReport in RUM", error: error) + } + return false + } +} diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 9cb84d1fb9..d3c23d286f 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -24,7 +24,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -49,7 +50,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -71,7 +73,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -96,7 +99,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -117,7 +121,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -138,7 +143,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -159,7 +165,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) let subscriber = RUMCommandSubscriberMock() diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift new file mode 100644 index 0000000000..fdddb44c7a --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift @@ -0,0 +1,76 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationAppStateManagerTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + var sut: WatchdogTerminationAppStateManager! + var featureScope: FeatureScopeMock! + var context: DatadogContext = .mockWith(applicationStateHistory: .mockAppInBackground()) + // swiftlint:enable implicitly_unwrapped_optional + + override func setUpWithError() throws { + try super.setUpWithError() + + featureScope = FeatureScopeMock() + sut = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: .init() + ) + } + + func testAppStart_SetsIsActive() throws { + try sut.start() + + let isActiveExpectation = expectation(description: "isActive is set to true") + + // app state changes + context.applicationStateHistory.append(.init(state: .active, date: .init())) + _ = sut.receive(message: .context(context), from: NOPDatadogCore()) + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertTrue(appState?.isActive == true) + isActiveExpectation.fulfill() + } + wait(for: [isActiveExpectation], timeout: 1) + + let isBackgroundedExpectation = expectation(description: "isActive is set to false") + + // app state changes again + context.applicationStateHistory.append(.init(state: .background, date: .init())) + _ = sut.receive(message: .context(context), from: NOPDatadogCore()) + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertTrue(appState?.isActive == false) + isBackgroundedExpectation.fulfill() + } + + wait(for: [isBackgroundedExpectation], timeout: 1) + } + + func testDeleteAppState() throws { + try sut.start() + + let isActiveExpectation = expectation(description: "isActive is set") + context.applicationStateHistory.append(.init(state: .active, date: .init())) + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertNotNil(appState) + isActiveExpectation.fulfill() + } + wait(for: [isActiveExpectation], timeout: 1) + + let deleteExpectation = expectation(description: "isActive is set to false") + sut.deleteAppState() + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertNil(appState) + deleteExpectation.fulfill() + } + + wait(for: [deleteExpectation], timeout: 1) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift new file mode 100644 index 0000000000..1107195eaa --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift @@ -0,0 +1,320 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationCheckerTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + var sut: WatchdogTerminationChecker! + // swiftlint:enable implicitly_unwrapped_optional + + func testNoPreviousState_NoWatchdogTermination() throws { + given(isSimulator: .mockRandom()) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), from: nil, to: .mockRandom())) + } + + func testIsSimulatorBuild_NoWatchdogTermination() throws { + given(isSimulator: true) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), from: .mockRandom(), to: .mockRandom())) + } + + func testIsDebugging_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: true, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), from: previous, to: .mockRandom())) + } + + func testDifferentAppVersions_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.1", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), from: previous, to: current)) + } + + func testApplicationDidCrash_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: true), from: previous, to: current)) + } + + func testApplicationWasTerminated_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: true, + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + func testDifferentOSVersions_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.1", + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + func testDifferentBootTimes_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 2.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + func testDifferentVendorId_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "bar", + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + func testSDKWasStoppedAndStarted_NoWatchdogTermination() throws { + given(isSimulator: false) + + let pid = UUID() + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: pid + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: pid + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + func testApplicationWasInBackground_NoWatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: false, + vendorId: "foo", + processId: .mockAny() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: .mockAny() + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + func testApplicationWasInForeground_WatchdogTermination() throws { + given(isSimulator: false) + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: true, + vendorId: "foo", + processId: UUID() + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: true, + vendorId: "foo", + processId: UUID() + ) + + XCTAssertTrue(sut.isWatchdogTermination(launch: .init(didCrash: false), from: previous, to: current)) + } + + // MARK: Helpers + + func given(isSimulator: Bool) { + let deviceInfo: DeviceInfo = .init( + name: .mockAny(), + model: .mockAny(), + osName: .mockAny(), + osVersion: .mockAny(), + osBuildNumber: .mockAny(), + architecture: .mockAny(), + isSimulator: isSimulator, + vendorId: .mockAny(), + isDebugging: .mockAny(), + systemBootTime: .mockAny() + ) + + sut = WatchdogTerminationChecker( + appStateManager: .mockRandom(), + deviceInfo: deviceInfo + ) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift new file mode 100644 index 0000000000..4c4e8a8345 --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift @@ -0,0 +1,103 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +extension WatchdogTerminationAppState: RandomMockable, AnyMockable { + public static func mockAny() -> DatadogRUM.WatchdogTerminationAppState { + return .init( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: .mockAny(), + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny() + ) + } + + public static func mockRandom() -> WatchdogTerminationAppState { + return .init( + appVersion: .mockRandom(), + osVersion: .mockRandom(), + systemBootTime: .mockRandom(), + isDebugging: .mockRandom(), + wasTerminated: .mockRandom(), + isActive: .mockRandom(), + vendorId: .mockRandom(), + processId: .mockAny() + ) + } +} + +class WatchdogTerminationReporterMock: WatchdogTerminationReporting { + var didSend: XCTestExpectation + + init(didSend: XCTestExpectation) { + self.didSend = didSend + } + + func send() { + didSend.fulfill() + } +} + +extension WatchdogTerminationReporter: RandomMockable { + public static func mockRandom() -> Self { + return .init() + } +} + +extension WatchdogTerminationChecker: RandomMockable { + public static func mockRandom() -> WatchdogTerminationChecker { + return .init( + appStateManager: .mockRandom(), + deviceInfo: .mockRandom() + ) + } +} + +extension WatchdogTerminationAppStateManager: RandomMockable { + public static func mockRandom() -> WatchdogTerminationAppStateManager { + return .init( + featureScope: FeatureScopeMock(), + processId: .mockRandom() + ) + } +} + +extension Sysctl: RandomMockable { + public static func mockRandom() -> DatadogInternal.Sysctl { + return .init() + } +} + +extension RUMDataStore: RandomMockable { + public static func mockRandom() -> DatadogRUM.RUMDataStore { + return .init(featureScope: FeatureScopeMock()) + } +} + +extension WatchdogTerminationMonitor: RandomMockable { + public static func mockRandom() -> WatchdogTerminationMonitor { + return .init( + appStateManager: .mockRandom(), + checker: .mockRandom(), + reporter: WatchdogTerminationReporter.mockRandom(), + telemetry: NOPTelemetry() + ) + } +} + +extension LaunchReport: RandomMockable { + public static func mockRandom() -> DatadogInternal.LaunchReport { + return .init(didCrash: .mockRandom()) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift new file mode 100644 index 0000000000..c968573e07 --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift @@ -0,0 +1,103 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationMonitorTests: XCTestCase { + let featureScope = FeatureScopeMock() + // swiftlint:disable implicitly_unwrapped_optional + private var sut: WatchdogTerminationMonitor! + private var core: PassthroughCoreMock! + // swiftlint:enable implicitly_unwrapped_optional + + func testApplicationWasInForeground_WatchdogTermination() throws { + let didSend = self.expectation(description: "Watchdog termination was reported") + + // app starts + given( + isSimulator: false, + isDebugging: false, + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + vendorId: "foo", + processId: UUID(), + didSend: didSend + ) + core.context.applicationStateHistory.append(.init(state: .active, date: .init())) + + // saves the current state + sut.start(launchReport: nil) + + // watchdog termination happens here which causes app launch + given( + isSimulator: false, + isDebugging: false, + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + vendorId: "foo", + processId: UUID(), + didSend: didSend + ) + core.context.applicationStateHistory.append(.init(state: .active, date: .init())) + sut.start(launchReport: .init(didCrash: false)) + + waitForExpectations(timeout: 1) + } + + // MARK: Helpers + + func given( + isSimulator: Bool, + isDebugging: Bool, + appVersion: String, + osVersion: String, + systemBootTime: TimeInterval, + vendorId: String?, + processId: UUID, + didSend: XCTestExpectation + ) { + let deviceInfo: DeviceInfo = .init( + name: .mockAny(), + model: .mockAny(), + osName: .mockAny(), + osVersion: .mockAny(), + osBuildNumber: .mockAny(), + architecture: .mockAny(), + isSimulator: isSimulator, + vendorId: vendorId, + isDebugging: false, + systemBootTime: systemBootTime + ) + + featureScope.contextMock.version = appVersion + + let appStateManager = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: processId + ) + + let checker = WatchdogTerminationChecker(appStateManager: appStateManager, deviceInfo: deviceInfo) + + let reporter = WatchdogTerminationReporterMock(didSend: didSend) + + sut = WatchdogTerminationMonitor( + appStateManager: appStateManager, + checker: checker, + reporter: reporter, + telemetry: featureScope.telemetryMock + ) + + core = PassthroughCoreMock( + context: .mockWith(applicationStateHistory: .mockAppInBackground()), + messageReceiver: appStateManager + ) + } +} diff --git a/TestUtilities/Mocks/DatadogContextMock.swift b/TestUtilities/Mocks/DatadogContextMock.swift index a396203f1b..ec7be8fef3 100644 --- a/TestUtilities/Mocks/DatadogContextMock.swift +++ b/TestUtilities/Mocks/DatadogContextMock.swift @@ -133,7 +133,11 @@ extension DeviceInfo { osName: String = "iOS", osVersion: String = "15.4.1", osBuildNumber: String = "13D20", - architecture: String = "arm64e" + architecture: String = "arm64e", + isSimulator: Bool = true, + vendorId: String? = "xyz", + isDebugging: Bool = false, + systemBootTime: TimeInterval = Date.timeIntervalSinceReferenceDate ) -> DeviceInfo { return .init( name: name, @@ -141,7 +145,11 @@ extension DeviceInfo { osName: osName, osVersion: osVersion, osBuildNumber: osBuildNumber, - architecture: architecture + architecture: architecture, + isSimulator: isSimulator, + vendorId: vendorId, + isDebugging: isDebugging, + systemBootTime: systemBootTime ) } @@ -152,7 +160,11 @@ extension DeviceInfo { osName: .mockRandom(), osVersion: .mockRandom(), osBuildNumber: .mockRandom(), - architecture: .mockRandom() + architecture: .mockRandom(), + isSimulator: .mockRandom(), + vendorId: .mockRandom(), + isDebugging: .mockRandom(), + systemBootTime: .mockRandom() ) } } From 89b28e62e1aa853991255c682c079b6e4dc72dbf Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Wed, 5 Jun 2024 16:55:03 +0200 Subject: [PATCH 022/110] RUM-817 Add ObjcExceptionHandler rethrow abstraction --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 ----- DatadogCore/Private/ObjcExceptionHandler.m | 2 +- .../Private/include/ObjcExceptionHandler.h | 4 +- DatadogCore/Sources/Core/DatadogCore.swift | 9 ++++ .../Sources/Core/Storage/Files/File.swift | 9 ++-- DatadogCore/Sources/Datadog.swift | 2 + DatadogCore/Sources/Utils/Globals.swift | 14 ------ .../Datadog/Mocks/DatadogPrivateMocks.swift | 20 --------- .../ObjcExceptionHandlerTests.swift | 6 +-- DatadogInternal/Sources/Utils/DDError.swift | 45 +++++++++++++++++++ .../Recorder/RecordingCoordinator.swift | 1 + 11 files changed, 65 insertions(+), 59 deletions(-) delete mode 100644 DatadogCore/Sources/Utils/Globals.swift delete mode 100644 DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 6c60348a5f..d82ada0411 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -531,8 +531,6 @@ 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */; }; 61BBD19724ED50040023E65F /* DatadogConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BBD19624ED50040023E65F /* DatadogConfigurationTests.swift */; }; 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */; }; - 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */; }; - 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638424361E9200C4D4E6 /* Globals.swift */; }; 61C4534A2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C453492C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift */; }; 61C4534B2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C453492C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift */; }; 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89524509BF600DA608C /* TracerTests.swift */; }; @@ -1396,7 +1394,6 @@ D2CB6E3627C50EAE00A62B57 /* ObjcAppLaunchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */; }; D2CB6E3C27C50EAE00A62B57 /* Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD702589FAFD007E8BB7 /* Retrying.swift */; }; D2CB6E4327C50EAE00A62B57 /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; }; - D2CB6E4D27C50EAE00A62B57 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638424361E9200C4D4E6 /* Globals.swift */; }; D2CB6E5527C50EAE00A62B57 /* KronosNSTimer+ClosureKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */; }; D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E792E2577B0F900DFCC17 /* Reader.swift */; }; D2CB6E6927C50EAE00A62B57 /* KronosDNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */; }; @@ -1430,7 +1427,6 @@ D2CB6EF427C520D400A62B57 /* FileWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C292423990D00786299 /* FileWriterTests.swift */; }; D2CB6EFE27C520D400A62B57 /* RUMMonitorConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B954124BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift */; }; D2CB6F0027C520D400A62B57 /* RUMSessionMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */; }; - D2CB6F0127C520D400A62B57 /* DatadogPrivateMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */; }; D2CB6F0427C520D400A62B57 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; D2CB6F0927C520D400A62B57 /* RUMDataModels+objcTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */; }; D2CB6F0C27C520D400A62B57 /* KronosNTPPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */; }; @@ -2537,8 +2533,6 @@ 61C2C20824C0C75500C0321C /* RUMSessionScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionScopeTests.swift; sourceTree = ""; }; 61C2C21124C5951400C0321C /* RUMViewScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewScope.swift; sourceTree = ""; }; 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionHandlerTests.swift; sourceTree = ""; }; - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogPrivateMocks.swift; sourceTree = ""; }; - 61C3638424361E9200C4D4E6 /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMock.swift; sourceTree = ""; }; 61C3E63624BF191F008053F2 /* RUMScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMScope.swift; sourceTree = ""; }; 61C3E63824BF19B4008053F2 /* RUMContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMContext.swift; sourceTree = ""; }; @@ -4100,7 +4094,6 @@ 61133BB72423979B00786299 /* Utils */ = { isa = PBXGroup; children = ( - 61C3638424361E9200C4D4E6 /* Globals.swift */, 6139CD702589FAFD007E8BB7 /* Retrying.swift */, 61DA8CAE28620C760074A606 /* Cryptography.swift */, ); @@ -4243,7 +4236,6 @@ D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */, D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */, 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */, - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */, D20605BB28757BFB0047275C /* KronosClockMock.swift */, 61F1A61B2498AD2C00075390 /* SystemFrameworks */, ); @@ -7940,7 +7932,6 @@ 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */, D2FB125D292FBB56005B13F8 /* Datadog+Internal.swift in Sources */, D2A7840F29A53B2F003B03BB /* Directory.swift in Sources */, - 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */, D20605CB2875A83F0047275C /* ContextValueReader.swift in Sources */, 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */, D20605A92874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */, @@ -8024,7 +8015,6 @@ 6179DB562B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */, 3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */, 6136CB4A2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */, - 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, 6147989C2A459E2B0095CB02 /* DDTrace+apiTests.m in Sources */, D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */, @@ -9189,7 +9179,6 @@ D29294E1291D5ED500F8EFF9 /* ApplicationVersionPublisher.swift in Sources */, D20605A7287476230047275C /* ServerOffsetPublisher.swift in Sources */, D21C26C628A3B49C005DD405 /* FeatureStorage.swift in Sources */, - D2CB6E4D27C50EAE00A62B57 /* Globals.swift in Sources */, D2CB6E5527C50EAE00A62B57 /* KronosNSTimer+ClosureKit.swift in Sources */, D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */, D2CB6E6927C50EAE00A62B57 /* KronosDNSResolver.swift in Sources */, @@ -9261,7 +9250,6 @@ D2CB6EFE27C520D400A62B57 /* RUMMonitorConfigurationTests.swift in Sources */, D2CB6F0027C520D400A62B57 /* RUMSessionMatcher.swift in Sources */, A728ADB12934EB0C00397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */, - D2CB6F0127C520D400A62B57 /* DatadogPrivateMocks.swift in Sources */, 6167E6DE2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */, D26C49B02886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, D24C9C7229A7D57A002057CF /* DirectoriesMock.swift in Sources */, diff --git a/DatadogCore/Private/ObjcExceptionHandler.m b/DatadogCore/Private/ObjcExceptionHandler.m index e246b7358c..d287fdcf2b 100644 --- a/DatadogCore/Private/ObjcExceptionHandler.m +++ b/DatadogCore/Private/ObjcExceptionHandler.m @@ -9,7 +9,7 @@ @implementation __dd_private_ObjcExceptionHandler -- (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error { ++ (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error { @try { tryBlock(); return YES; diff --git a/DatadogCore/Private/include/ObjcExceptionHandler.h b/DatadogCore/Private/include/ObjcExceptionHandler.h index bfe9c68972..ed1c57e981 100644 --- a/DatadogCore/Private/include/ObjcExceptionHandler.h +++ b/DatadogCore/Private/include/ObjcExceptionHandler.h @@ -10,8 +10,8 @@ NS_ASSUME_NONNULL_BEGIN @interface __dd_private_ObjcExceptionHandler : NSObject -- (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error - NS_SWIFT_NAME(rethrowToSwift(tryBlock:)); ++ (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error + NS_SWIFT_NAME(rethrow(_:)); @end diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 266035e300..cbe2fd8849 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -485,3 +485,12 @@ extension DatadogCore: Flushable { } } } + +#if SPM_BUILD +import DatadogPrivate +#endif + +internal let registerObjcExceptionHandlerOnce: () -> Void = { + ObjcException.rethrow = __dd_private_ObjcExceptionHandler.rethrow + return {} +}() diff --git a/DatadogCore/Sources/Core/Storage/Files/File.swift b/DatadogCore/Sources/Core/Storage/Files/File.swift index 0dce3b8f2a..c3770674c6 100644 --- a/DatadogCore/Sources/Core/Storage/Files/File.swift +++ b/DatadogCore/Sources/Core/Storage/Files/File.swift @@ -5,10 +5,7 @@ */ import Foundation - -#if SPM_BUILD -import DatadogPrivate -#endif +import DatadogInternal /// Provides convenient interface for reading metadata and appending data to the file. internal protocol WritableFile { @@ -90,11 +87,11 @@ internal struct File: WritableFile, ReadableFile { private func legacyAppend(_ data: Data, to fileHandle: FileHandle) throws { defer { - try? objcExceptionHandler.rethrowToSwift { + try? objc_rethrow { fileHandle.closeFile() } } - try objcExceptionHandler.rethrowToSwift { + try objc_rethrow { fileHandle.seekToEndOfFile() fileHandle.write(data) } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 1b06cbf415..05c4461ce3 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -493,6 +493,8 @@ public enum Datadog { verbosityLevel: { Datadog.verbosityLevel } ) + registerObjcExceptionHandlerOnce() + return core } diff --git a/DatadogCore/Sources/Utils/Globals.swift b/DatadogCore/Sources/Utils/Globals.swift deleted file mode 100644 index baeb96d304..0000000000 --- a/DatadogCore/Sources/Utils/Globals.swift +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import Foundation - -#if SPM_BUILD -import DatadogPrivate -#endif - -/// Exception handler rethrowing `NSExceptions` to Swift `NSError`. -internal var objcExceptionHandler = __dd_private_ObjcExceptionHandler() diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift deleted file mode 100644 index 2ba25d2d9a..0000000000 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import Foundation -import DatadogCore - -class ObjcExceptionHandlerMock: __dd_private_ObjcExceptionHandler { - let error: Error - - init(throwingError: Error) { - self.error = throwingError - } - - override func rethrowToSwift(tryBlock: @escaping () -> Void) throws { - throw error - } -} diff --git a/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift b/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift index 0d61436c82..b74454275c 100644 --- a/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift +++ b/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift @@ -8,11 +8,9 @@ import XCTest import DatadogCore class ObjcExceptionHandlerTests: XCTestCase { - private let exceptionHandler = __dd_private_ObjcExceptionHandler() - func testGivenNonThrowingCode_itDoesNotThrow() throws { var counter = 0 - try exceptionHandler.rethrowToSwift { counter += 1 } + try __dd_private_ObjcExceptionHandler.rethrow { counter += 1 } XCTAssertEqual(counter, 1) } @@ -23,7 +21,7 @@ class ObjcExceptionHandlerTests: XCTestCase { userInfo: ["user-info": "some"] ) - XCTAssertThrowsError(try exceptionHandler.rethrowToSwift { nsException.raise() }) { error in + XCTAssertThrowsError(try __dd_private_ObjcExceptionHandler.rethrow { nsException.raise() }) { error in XCTAssertEqual((error as NSError).domain, "name") XCTAssertEqual((error as NSError).code, 0) XCTAssertEqual((error as NSError).userInfo as? [String: String], ["user-info": "some"]) diff --git a/DatadogInternal/Sources/Utils/DDError.swift b/DatadogInternal/Sources/Utils/DDError.swift index 1d4b35ffd1..58459bd285 100644 --- a/DatadogInternal/Sources/Utils/DDError.swift +++ b/DatadogInternal/Sources/Utils/DDError.swift @@ -90,3 +90,48 @@ public struct InternalError: Error, CustomStringConvertible { self.description = description } } + +public struct ObjcException: Error { + /// A closure to catch Objective-C runtime exception and rethrow as `Swift.Error`. + /// + /// - Important: Does nothing by default, it must be set to an Objective-C interopable function. + /// + /// - Warning: As stated in [Objective-C Automatic Reference Counting (ARC)](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#exceptions), + /// in Objective-C, ARC is not exception-safe and does not perform releases which would occur at the end of a + /// full-expression if that full-expression throws an exception. Therefore, ARC-generated code leaks by default + /// on exceptions. + public static var rethrow: ((() -> Void) throws -> Void) = { $0() } + + /// The underlying `NSError` describing the `NSException` + /// thrown by Objective-C runtime. + public let error: Error +} + +/// Rethrow Objective-C runtime exception as `Swift.Error`. +/// +/// - Warning: As stated in [Objective-C Automatic Reference Counting (ARC)](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#exceptions), +/// in Objective-C, ARC is not exception-safe and does not perform releases which would occur at the end of a +/// full-expression if that full-expression throws an exception. Therefore, ARC-generated code leaks by default +/// on exceptions. +/// - throws: `ObjcException` if an exception was raised by the Objective-C runtime. +@discardableResult +public func objc_rethrow(_ block: () throws -> T) throws -> T { + var value: T! //swiftlint:disable:this implicitly_unwrapped_optional + var swiftError: Error? + do { + try ObjcException.rethrow { + do { + value = try block() + } catch { + swiftError = error + } + } + } catch { + // wrap the underlying objc runtime exception in + // a `ObjcException` for easier matching during + // escalation. + throw ObjcException(error: error) + } + + return try swiftError.map { throw $0 } ?? value +} diff --git a/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift b/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift index e2abfa1a0b..7e82f8baa2 100644 --- a/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift +++ b/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift @@ -72,6 +72,7 @@ internal class RecordingCoordinator { viewID: viewID, viewServerTimeOffset: rumContext.viewServerTimeOffset ) + recorder.captureNextRecord(recorderContext) } } From 52acf662016f3e1bacd61694ef4c5c5fc3cb2b3c Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 6 Jun 2024 15:26:18 +0200 Subject: [PATCH 023/110] RUM-817 catch objc runtime exception at recording --- .../Utils/SnapshotTestCase.swift | 3 +- .../Feature/SessionReplayFeature.swift | 4 +- .../Sources/Recorder/Recorder.swift | 56 +++--------- .../Recorder/RecordingCoordinator.swift | 40 ++++++++- .../Tests/Recorder/RecorderTests.swift | 70 ++------------- .../Recorder/RecordingCoordinatorTests.swift | 85 ++++++++++++++++--- 6 files changed, 137 insertions(+), 121 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift index e20b816e3c..054175c2b7 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift @@ -55,7 +55,6 @@ internal class SnapshotTestCase: XCTestCase { let recorder = try Recorder( snapshotProcessor: snapshotProcessor, resourceProcessor: resourceProcessor, - telemetry: TelemetryMock(), additionalNodeRecorders: [] ) @@ -74,7 +73,7 @@ internal class SnapshotTestCase: XCTestCase { } // Capture next record with mock RUM Context - recorder.captureNextRecord( + try recorder.captureNextRecord( .init(privacy: privacyLevel, applicationID: "", sessionID: "", viewID: "", viewServerTimeOffset: 0) ) diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index ab66080437..9ac2eb9048 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -39,7 +39,6 @@ internal class SessionReplayFeature: SessionReplayConfiguration, DatadogRemoteFe let recorder = try Recorder( snapshotProcessor: snapshotProcessor, resourceProcessor: resourceProcessor, - telemetry: core.telemetry, additionalNodeRecorders: configuration._additionalNodeRecorders ) let scheduler = MainThreadScheduler(interval: 0.1) @@ -59,7 +58,8 @@ internal class SessionReplayFeature: SessionReplayConfiguration, DatadogRemoteFe rumContextObserver: contextReceiver, srContextPublisher: SRContextPublisher(core: core), recorder: recorder, - sampler: Sampler(samplingRate: configuration.debugSDK ? 100 : configuration.replaySampleRate) + sampler: Sampler(samplingRate: configuration.debugSDK ? 100 : configuration.replaySampleRate), + telemetry: core.telemetry ) self.requestBuilder = SegmentRequestBuilder( customUploadURL: configuration.customEndpoint, diff --git a/DatadogSessionReplay/Sources/Recorder/Recorder.swift b/DatadogSessionReplay/Sources/Recorder/Recorder.swift index a216f7cf3b..9d85b6e3c0 100644 --- a/DatadogSessionReplay/Sources/Recorder/Recorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/Recorder.swift @@ -10,7 +10,7 @@ import DatadogInternal /// A type managing Session Replay recording. internal protocol Recording { - func captureNextRecord(_ recorderContext: Recorder.Context) + func captureNextRecord(_ recorderContext: Recorder.Context) throws } /// The main engine and the heart beat of Session Replay. @@ -62,15 +62,10 @@ public class Recorder: Recording { private let snapshotProcessor: SnapshotProcessing /// Processes resources on a background thread. private let resourceProcessor: ResourceProcessing - /// Sends telemetry through sdk core. - private let telemetry: Telemetry - /// The sampling rate for internal telemetry of method call. - private let methodCallTelemetrySamplingRate: Float convenience init( snapshotProcessor: SnapshotProcessing, resourceProcessor: ResourceProcessing, - telemetry: Telemetry, additionalNodeRecorders: [NodeRecorder] ) throws { let windowObserver = KeyWindowObserver() @@ -87,8 +82,7 @@ public class Recorder: Recording { viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: touchSnapshotProducer, snapshotProcessor: snapshotProcessor, - resourceProcessor: resourceProcessor, - telemetry: telemetry + resourceProcessor: resourceProcessor ) } @@ -97,17 +91,13 @@ public class Recorder: Recording { viewTreeSnapshotProducer: ViewTreeSnapshotProducer, touchSnapshotProducer: TouchSnapshotProducer, snapshotProcessor: SnapshotProcessing, - resourceProcessor: ResourceProcessing, - telemetry: Telemetry, - methodCallTelemetrySamplingRate: Float = 0.1 + resourceProcessor: ResourceProcessing ) { self.uiApplicationSwizzler = uiApplicationSwizzler self.viewTreeSnapshotProducer = viewTreeSnapshotProducer self.touchSnapshotProducer = touchSnapshotProducer self.snapshotProcessor = snapshotProcessor self.resourceProcessor = resourceProcessor - self.telemetry = telemetry - self.methodCallTelemetrySamplingRate = methodCallTelemetrySamplingRate uiApplicationSwizzler.swizzle() } @@ -119,37 +109,19 @@ public class Recorder: Recording { /// Initiates the capture of a next record. /// **Note**: This is called on the main thread. - func captureNextRecord(_ recorderContext: Context) { - let methodCalledTrace = telemetry.startMethodCalled( - operationName: MethodCallConstants.captureRecordOperationName, - callerClass: MethodCallConstants.className, - samplingRate: methodCallTelemetrySamplingRate // Effectively 3% * 0.1% = 0.003% of calls - ) - var isSuccessful = true - do { - guard let viewTreeSnapshot = try viewTreeSnapshotProducer.takeSnapshot(with: recorderContext) else { - // There is nothing visible yet (i.e. the key window is not yet ready). - return - } - let touchSnapshot = touchSnapshotProducer.takeSnapshot(context: recorderContext) - snapshotProcessor.process(viewTreeSnapshot: viewTreeSnapshot, touchSnapshot: touchSnapshot) - - resourceProcessor.process( - resources: viewTreeSnapshot.resources, - context: .init(recorderContext.applicationID) - ) - } catch let error { - isSuccessful = false - telemetry.error("[SR] Failed to take snapshot", error: DDError(error: error)) + func captureNextRecord(_ recorderContext: Context) throws { + guard let viewTreeSnapshot = try viewTreeSnapshotProducer.takeSnapshot(with: recorderContext) else { + // There is nothing visible yet (i.e. the key window is not yet ready). + return } - telemetry.stopMethodCalled(methodCalledTrace, isSuccessful: isSuccessful) - } - private enum MethodCallConstants { - static let captureRecordOperationName = "Capture Record" - static let className = { - String(reflecting: Recorder.self) - }() + let touchSnapshot = touchSnapshotProducer.takeSnapshot(context: recorderContext) + snapshotProcessor.process(viewTreeSnapshot: viewTreeSnapshot, touchSnapshot: touchSnapshot) + + resourceProcessor.process( + resources: viewTreeSnapshot.resources, + context: .init(recorderContext.applicationID) + ) } } #endif diff --git a/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift b/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift index 7e82f8baa2..47ca64e4bf 100644 --- a/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift +++ b/DatadogSessionReplay/Sources/Recorder/RecordingCoordinator.swift @@ -20,19 +20,28 @@ internal class RecordingCoordinator { private var currentRUMContext: RUMContext? = nil private var isSampled = false + /// Sends telemetry through sdk core. + private let telemetry: Telemetry + /// The sampling rate for internal telemetry of method call. + private let methodCallTelemetrySamplingRate: Float + init( scheduler: Scheduler, privacy: PrivacyLevel, rumContextObserver: RUMContextObserver, srContextPublisher: SRContextPublisher, recorder: Recording, - sampler: Sampler + sampler: Sampler, + telemetry: Telemetry, + methodCallTelemetrySamplingRate: Float = 0.1 ) { self.recorder = recorder self.scheduler = scheduler self.sampler = sampler self.privacy = privacy self.srContextPublisher = srContextPublisher + self.telemetry = telemetry + self.methodCallTelemetrySamplingRate = methodCallTelemetrySamplingRate srContextPublisher.setHasReplay(false) @@ -65,6 +74,7 @@ internal class RecordingCoordinator { let viewID = rumContext.viewID else { return } + let recorderContext = Recorder.Context( privacy: privacy, applicationID: rumContext.applicationID, @@ -73,7 +83,33 @@ internal class RecordingCoordinator { viewServerTimeOffset: rumContext.viewServerTimeOffset ) - recorder.captureNextRecord(recorderContext) + let methodCalledTrace = telemetry.startMethodCalled( + operationName: MethodCallConstants.captureRecordOperationName, + callerClass: MethodCallConstants.className, + samplingRate: methodCallTelemetrySamplingRate // Effectively 3% * 0.1% = 0.003% of calls + ) + + var isSuccessful = false + do { + try objc_rethrow { try recorder.captureNextRecord(recorderContext) } + isSuccessful = true + } catch let objc as ObjcException { + telemetry.error("[SR] Failed to take snapshot due to Objective-C runtime exception", error: objc.error) + // An Objective-C runtime exception is a severe issue that will leak if + // the framework is not built with `-fobjc-arc-exceptions` option. + // We recover from the exception and stop the scheduler as a measure of + // caution. The scheduler could start again at a next RUM context change. + scheduler.stop() + } catch { + telemetry.error("[SR] Failed to take snapshot", error: error) + } + + telemetry.stopMethodCalled(methodCalledTrace, isSuccessful: isSuccessful) + } + + private enum MethodCallConstants { + static let captureRecordOperationName = "Capture Record" + static let className = "Recorder" } } #endif diff --git a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift index 8b690de9d3..d4091a1f74 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift @@ -11,7 +11,7 @@ import XCTest @testable import TestUtilities class RecorderTests: XCTestCase { - func testAfterCapturingSnapshot_itIsPassesToProcessor() { + func testAfterCapturingSnapshot_itIsPassesToProcessor() throws { let mockViewTreeSnapshots: [ViewTreeSnapshot] = .mockRandom(count: 1) let mockTouchSnapshots: [TouchSnapshot] = .mockRandom(count: 1) let snapshotProcessor = SnapshotProcessorSpy() @@ -23,13 +23,12 @@ class RecorderTests: XCTestCase { viewTreeSnapshotProducer: ViewTreeSnapshotProducerMock(succeedingSnapshots: mockViewTreeSnapshots), touchSnapshotProducer: TouchSnapshotProducerMock(succeedingSnapshots: mockTouchSnapshots), snapshotProcessor: snapshotProcessor, - resourceProcessor: resourceProcessor, - telemetry: TelemetryMock() + resourceProcessor: resourceProcessor ) let recorderContext = Recorder.Context.mockRandom() // When - recorder.captureNextRecord(recorderContext) + try recorder.captureNextRecord(recorderContext) // Then DDAssertReflectionEqual(snapshotProcessor.processedSnapshots.map { $0.viewTreeSnapshot }, mockViewTreeSnapshots) @@ -38,7 +37,7 @@ class RecorderTests: XCTestCase { DDAssertReflectionEqual(resourceProcessor.processedResources.map { $0.context }, mockViewTreeSnapshots.map { _ in EnrichedResource.Context(recorderContext.applicationID) }) } - func testWhenCapturingSnapshots_itUsesDefaultRecorderContext() { + func testWhenCapturingSnapshots_itUsesDefaultRecorderContext() throws { let recorderContext: Recorder.Context = .mockRandom() let viewTreeSnapshotProducer = ViewTreeSnapshotProducerSpy() let touchSnapshotProducer = TouchSnapshotProducerMock() @@ -49,68 +48,16 @@ class RecorderTests: XCTestCase { viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: touchSnapshotProducer, snapshotProcessor: SnapshotProcessorSpy(), - resourceProcessor: ResourceProcessorSpy(), - telemetry: TelemetryMock() + resourceProcessor: ResourceProcessorSpy() ) // When - recorder.captureNextRecord(recorderContext) + try recorder.captureNextRecord(recorderContext) // Then XCTAssertEqual(viewTreeSnapshotProducer.succeedingContexts.count, 1) XCTAssertEqual(viewTreeSnapshotProducer.succeedingContexts[0], recorderContext) } - func testWhenCapturingSnapshotFails_itSendsErrorTelemetry() { - let telemetry = TelemetryMock() - let viewTreeSnapshotProducer = ViewTreeSnapshotProducerMock( - succeedingErrors: [ErrorMock("snapshot creation error")] - ) - - // Given - let recorder = Recorder( - uiApplicationSwizzler: .mockAny(), - viewTreeSnapshotProducer: viewTreeSnapshotProducer, - touchSnapshotProducer: TouchSnapshotProducerMock(), - snapshotProcessor: SnapshotProcessorSpy(), - resourceProcessor: ResourceProcessorSpy(), - telemetry: telemetry, - methodCallTelemetrySamplingRate: 0 - ) - - // When - recorder.captureNextRecord(.mockRandom()) - - // Then - XCTAssertEqual( - telemetry.description, - """ - Telemetry logs: - - [error] [SR] Failed to take snapshot - snapshot creation error, kind: ErrorMock, stack: snapshot creation error - """ - ) - } - - func testWhenCapturingSnapshot_itSendsMethodCalledTelemetry() throws { - // Given - let telemetry = TelemetryMock() - let recorder = Recorder( - uiApplicationSwizzler: .mockAny(), - viewTreeSnapshotProducer: ViewTreeSnapshotProducerMock(succeedingSnapshots: .mockRandom()), - touchSnapshotProducer: TouchSnapshotProducerMock(), - snapshotProcessor: SnapshotProcessorSpy(), - resourceProcessor: ResourceProcessorSpy(), - telemetry: telemetry, - methodCallTelemetrySamplingRate: 100 - ) - - // When - recorder.captureNextRecord(.mockRandom()) - - // Then - let metric = try XCTUnwrap(telemetry.messages.last?.asMetric) - XCTAssertEqual(metric.name, "Method Called") - } - func testWhenCapturingSnapshots_itUsesAdditionalNodeRecorders() throws { let recorderContext: Recorder.Context = .mockRandom() let additionalNodeRecorder = SessionReplayNodeRecorderMock() @@ -127,11 +74,10 @@ class RecorderTests: XCTestCase { viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: touchSnapshotProducer, snapshotProcessor: SnapshotProcessorSpy(), - resourceProcessor: ResourceProcessorSpy(), - telemetry: TelemetryMock() + resourceProcessor: ResourceProcessorSpy() ) // When - recorder.captureNextRecord(recorderContext) + try recorder.captureNextRecord(recorderContext) // Then let queryContext = try XCTUnwrap(additionalNodeRecorder.queryContexts.first) diff --git a/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift b/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift index b6cb63abc5..5668d75745 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift @@ -6,7 +6,7 @@ #if os(iOS) import XCTest -import DatadogInternal +@testable import DatadogInternal @_spi(Internal) @testable import DatadogSessionReplay @testable import TestUtilities @@ -39,11 +39,10 @@ class RecordingCoordinatorTests: XCTestCase { func test_whenNotSampled_itStopsScheduler_andShouldNotRecord() { // Given - prepareRecordingCoordinator(sampler: Sampler(samplingRate: 0)) + prepareRecordingCoordinator(sampler: .mockRejectAll()) // When - let rumContext = RUMContext.mockRandom() - rumContextObserver.notify(rumContext: rumContext) + rumContextObserver.notify(rumContext: .mockRandom()) // Then XCTAssertFalse(scheduler.isRunning) @@ -54,7 +53,7 @@ class RecordingCoordinatorTests: XCTestCase { func test_whenSampled_itStartsScheduler_andShouldRecord() { // Given let privacy = PrivacyLevel.mockRandom() - prepareRecordingCoordinator(sampler: Sampler(samplingRate: 100), privacy: privacy) + prepareRecordingCoordinator(privacy: privacy) // When let rumContext = RUMContext.mockRandom() @@ -95,7 +94,7 @@ class RecordingCoordinatorTests: XCTestCase { func test_whenRUMContextWithoutViewID_itStartsScheduler_andShouldNotRecord() { // Given - prepareRecordingCoordinator(sampler: Sampler(samplingRate: 100)) + prepareRecordingCoordinator() // When let rumContext = RUMContext.mockWith(viewID: nil) @@ -107,14 +106,78 @@ class RecordingCoordinatorTests: XCTestCase { XCTAssertEqual(recordingMock.captureNextRecordCallsCount, 0) } - private func prepareRecordingCoordinator(sampler: Sampler, privacy: PrivacyLevel = .allow) { + func test_whenCapturingSnapshotFails_itSendsErrorTelemetry() { + let telemetry = TelemetryMock() + + // Given + recordingMock.captureNextRecordClosure = { _ in + throw ErrorMock("snapshot creation error") + } + + prepareRecordingCoordinator(telemetry: telemetry) + + // When + rumContextObserver.notify(rumContext: .mockRandom()) + + // Then + let error = telemetry.messages.firstError() + XCTAssertEqual(error?.message, "[SR] Failed to take snapshot - snapshot creation error") + XCTAssertEqual(error?.kind, "ErrorMock") + XCTAssertEqual(error?.stack, "snapshot creation error") + } + + func test_whenCapturingSnapshotFails_withObjCRuntimeException_itSendsErrorTelemetry() { + let telemetry = TelemetryMock() + + // Given + recordingMock.captureNextRecordClosure = { _ in + throw ObjcException(error: ErrorMock("snapshot creation error")) + } + + prepareRecordingCoordinator(telemetry: telemetry) + + // When + rumContextObserver.notify(rumContext: .mockRandom()) + + // Then + let error = telemetry.messages.firstError() + XCTAssertEqual(error?.message, "[SR] Failed to take snapshot due to Objective-C runtime exception - snapshot creation error") + XCTAssertEqual(error?.kind, "ErrorMock") + XCTAssertEqual(error?.stack, "snapshot creation error") + XCTAssertFalse(scheduler.isRunning) + } + + func test_whenCapturingSnapshot_itSendsMethodCalledTelemetry() throws { + // Given + let telemetry = TelemetryMock() + prepareRecordingCoordinator( + telemetry: telemetry, + methodCallTelemetrySamplingRate: 100 + ) + + // When + rumContextObserver.notify(rumContext: .mockRandom()) + + // Then + let metric = try XCTUnwrap(telemetry.messages.last?.asMetric) + XCTAssertEqual(metric.name, "Method Called") + } + + private func prepareRecordingCoordinator( + sampler: Sampler = .mockKeepAll(), + privacy: PrivacyLevel = .allow, + telemetry: Telemetry = NOPTelemetry(), + methodCallTelemetrySamplingRate: Float = 0 + ) { recordingCoordinator = RecordingCoordinator( scheduler: scheduler, privacy: privacy, rumContextObserver: rumContextObserver, srContextPublisher: contextPublisher, recorder: recordingMock, - sampler: sampler + sampler: sampler, + telemetry: telemetry, + methodCallTelemetrySamplingRate: methodCallTelemetrySamplingRate ) } } @@ -128,13 +191,13 @@ final class RecordingMock: Recording { } var captureNextRecordReceivedRecorderContext: Recorder.Context? var captureNextRecordReceivedInvocations: [Recorder.Context] = [] - var captureNextRecordClosure: ((Recorder.Context) -> Void)? + var captureNextRecordClosure: ((Recorder.Context) throws -> Void)? - func captureNextRecord(_ recorderContext: Recorder.Context) { + func captureNextRecord(_ recorderContext: Recorder.Context) throws { captureNextRecordCallsCount += 1 captureNextRecordReceivedRecorderContext = recorderContext captureNextRecordReceivedInvocations.append(recorderContext) - captureNextRecordClosure?(recorderContext) + try captureNextRecordClosure?(recorderContext) } } #endif From 3580385286fd2f18dac5a9d10c1d8d351d6b1a6f Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Mon, 17 Jun 2024 14:58:59 +0200 Subject: [PATCH 024/110] RUM-4883 Address comments --- .../Recorder/Utilities/SystemColors.swift | 8 ++++++++ .../NodeRecorders/UITabBarRecorder.swift | 16 +++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift index 6cf9417a72..d4aa7cb560 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift @@ -84,6 +84,14 @@ internal enum SystemColors { return UIColor.systemGreen.cgColor } + static var systemGray: UIColor { + return UIColor.systemGray + } + + static var systemBlue: UIColor { + return UIColor.systemBlue + } + static var placeholderText: CGColor { if #available(iOS 13.0, *) { return UIColor.placeholderText.cgColor diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index e63dcebc6c..6d38659daf 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -36,14 +36,12 @@ internal final class UITabBarRecorder: NodeRecorder { // return the unselectedItemTintColor, // or the default gray color if not set. if currentItemInSelectedState == nil || tabBar.selectedItem != currentItemInSelectedState { - let unselectedColor = tabBar.unselectedItemTintColor ?? .lightGray.withAlphaComponent(0.5) - return tabBar.unselectedItemTintColor ?? .systemGray.withAlphaComponent(0.5) + return tabBar.unselectedItemTintColor ?? SystemColors.systemGray.withAlphaComponent(0.5) } // Otherwise, return the tab bar tint color, // or the default blue color if not set. - let selectedColor = tabBar.tintColor ?? UIColor.systemBlue - return tabBar.tintColor ?? UIColor.systemBlue + return tabBar.tintColor ?? SystemColors.systemBlue } ), UILabelRecorder(), @@ -130,11 +128,15 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { #endif fileprivate extension UIImage { + /// Returns a unique description of the image. + /// It is calculated from `CGImage` properties, + /// Favors performance over acurracy (collisions are unlikely, but possible). + /// May return `nil` if the image has no associated `CGImage`. var uniqueDescription: String? { - // Some images may not have an associated CGImage, - // e.g., vector-based images (PDF, SVG), CIImage. + // Some images may not have an associated `CGImage`, + // e.g., vector-based images (PDF, SVG), `CIImage`. // In the case of tab bar icons, - // it is likely they have an associated CGImage. + // it is likely they have an associated `CGImage`. guard let cgImage = self.cgImage else { return nil } From c9be6980ded4bfa996b7563b4a74c1269af1a2f0 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 17 Jun 2024 11:17:20 +0200 Subject: [PATCH 025/110] Add #fileID and #line to ObjcException --- DatadogInternal/Sources/Utils/DDError.swift | 8 ++++++-- .../Tests/Recorder/RecordingCoordinatorTests.swift | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/DatadogInternal/Sources/Utils/DDError.swift b/DatadogInternal/Sources/Utils/DDError.swift index 58459bd285..ca40d8a63f 100644 --- a/DatadogInternal/Sources/Utils/DDError.swift +++ b/DatadogInternal/Sources/Utils/DDError.swift @@ -105,6 +105,10 @@ public struct ObjcException: Error { /// The underlying `NSError` describing the `NSException` /// thrown by Objective-C runtime. public let error: Error + /// The source file in which the exception was raised. + public let file: String + /// The line number on which the exception was raised. + public let line: Int } /// Rethrow Objective-C runtime exception as `Swift.Error`. @@ -115,7 +119,7 @@ public struct ObjcException: Error { /// on exceptions. /// - throws: `ObjcException` if an exception was raised by the Objective-C runtime. @discardableResult -public func objc_rethrow(_ block: () throws -> T) throws -> T { +public func objc_rethrow(_ block: () throws -> T, file: String = #fileID, line: Int = #line) throws -> T { var value: T! //swiftlint:disable:this implicitly_unwrapped_optional var swiftError: Error? do { @@ -130,7 +134,7 @@ public func objc_rethrow(_ block: () throws -> T) throws -> T { // wrap the underlying objc runtime exception in // a `ObjcException` for easier matching during // escalation. - throw ObjcException(error: error) + throw ObjcException(error: error, file: file, line: line) } return try swiftError.map { throw $0 } ?? value diff --git a/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift b/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift index 5668d75745..579db2f644 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecordingCoordinatorTests.swift @@ -131,7 +131,7 @@ class RecordingCoordinatorTests: XCTestCase { // Given recordingMock.captureNextRecordClosure = { _ in - throw ObjcException(error: ErrorMock("snapshot creation error")) + throw ObjcException(error: ErrorMock("snapshot creation error"), file: "File.swift", line: 0) } prepareRecordingCoordinator(telemetry: telemetry) From 5605d647740d3929f8871a9387b09b0a2adfc0ef Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 17 Jun 2024 11:38:14 +0200 Subject: [PATCH 026/110] Register ObjcExceptionHandler before SDK init --- DatadogCore/Sources/Datadog.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 05c4461ce3..5e3a487ce1 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -401,6 +401,8 @@ public enum Datadog { throw ProgrammerError(description: "The '\(instanceName)' instance of SDK is already initialized.") } + registerObjcExceptionHandlerOnce() + let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) if debug { consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) @@ -493,8 +495,6 @@ public enum Datadog { verbosityLevel: { Datadog.verbosityLevel } ) - registerObjcExceptionHandlerOnce() - return core } From 310fd8c873e0d6ac92930957a5f9013dbffaec73 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 17 Jun 2024 14:52:01 +0200 Subject: [PATCH 027/110] Add coverage for objc_rethrow --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 +++ .../Tests/Utils/ObjcExceptionTests.swift | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 DatadogInternal/Tests/Utils/ObjcExceptionTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index d82ada0411..948bcf875b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -1104,6 +1104,8 @@ D27D81C62A5D415200281CC2 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; }; D27D81C72A5D415200281CC2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; D27D81C82A5D41F400281CC2 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + D284C7402C2059F3005142CC /* ObjcExceptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */; }; + D284C7412C2059F3005142CC /* ObjcExceptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */; }; D286626E2A43487500852CE3 /* Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D286626D2A43487500852CE3 /* Datadog.swift */; }; D286626F2A43487500852CE3 /* Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D286626D2A43487500852CE3 /* Datadog.swift */; }; D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */; }; @@ -2873,6 +2875,7 @@ D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = ""; }; D27CBD992BB5DBBB00C766AA /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; + D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionTests.swift; sourceTree = ""; }; D286626D2A43487500852CE3 /* Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Datadog.swift; sourceTree = ""; }; D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSpanTests.swift; sourceTree = ""; }; D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingURLSessionHandlerTests.swift; sourceTree = ""; }; @@ -6194,6 +6197,7 @@ children = ( 613C6B912768FF3100870CBF /* SamplerTests.swift */, 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, + D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */, ); path = Utils; sourceTree = ""; @@ -9565,6 +9569,7 @@ D270CDE02B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, D2DA23A1298D58F400C6C7E6 /* ReadWriteLockTests.swift in Sources */, D2160CD829C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */, + D284C7402C2059F3005142CC /* ObjcExceptionTests.swift in Sources */, D2C5D5282B83FD5300B63F36 /* WebViewMessageTests.swift in Sources */, D20731CD29A52E8700ECBF94 /* SamplerTests.swift in Sources */, D2DA23A6298D58F400C6C7E6 /* AnyCoderTests.swift in Sources */, @@ -9614,6 +9619,7 @@ D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, D2DA23B5298D59DC00C6C7E6 /* ReadWriteLockTests.swift in Sources */, D2160CD929C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */, + D284C7412C2059F3005142CC /* ObjcExceptionTests.swift in Sources */, D2C5D5292B83FD5400B63F36 /* WebViewMessageTests.swift in Sources */, D20731CE29A52E8700ECBF94 /* SamplerTests.swift in Sources */, D2160CEA29C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */, diff --git a/DatadogInternal/Tests/Utils/ObjcExceptionTests.swift b/DatadogInternal/Tests/Utils/ObjcExceptionTests.swift new file mode 100644 index 0000000000..8c2c9e3a0e --- /dev/null +++ b/DatadogInternal/Tests/Utils/ObjcExceptionTests.swift @@ -0,0 +1,45 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities + +@testable import DatadogInternal + +class ObjcExceptionTests: XCTestCase { + func testWrappedObjcException() { + // Given + ObjcException.rethrow = { _ in throw ErrorMock("objc exception") } + defer { ObjcException.rethrow = { $0() } } + + do { + #sourceLocation(file: "File.swift", line: 1) + try objc_rethrow {} + #sourceLocation() + XCTFail("objc_rethrow should throw an error") + } catch let exception as ObjcException { + let error = exception.error as? ErrorMock + XCTAssertEqual(error?.description, "objc exception") + XCTAssertEqual(exception.file, "\(moduleName())/File.swift") + XCTAssertEqual(exception.line, 1) + } catch { + XCTFail("error should be of type ObjcException") + } + } + + func testRethrowSwiftError() { + do { + try objc_rethrow { throw ErrorMock("swift error") } + XCTFail("objc_rethrow should throw an error") + } catch let error as ErrorMock { + XCTAssertEqual(error.description, "swift error") + } catch is ObjcException { + XCTFail("error should not be of type ObjcException") + } catch { + XCTFail("error should be of type ErrorMock") + } + } +} From a01658979b62d6086d3c01543070f623a51f9be8 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Mon, 17 Jun 2024 17:52:48 +0200 Subject: [PATCH 028/110] RUM-3463 feat(watchdog-termination): fix linter --- DatadogInternal/Sources/Context/Sysctl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogInternal/Sources/Context/Sysctl.swift b/DatadogInternal/Sources/Context/Sysctl.swift index 37612cb7dd..2ab0b7d3f7 100644 --- a/DatadogInternal/Sources/Context/Sysctl.swift +++ b/DatadogInternal/Sources/Context/Sysctl.swift @@ -118,7 +118,7 @@ public struct Sysctl: SysctlProviding { var info = kinfo_proc() var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] var size = MemoryLayout.stride - let junk = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) + _ = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) return (info.kp_proc.p_flag & P_TRACED) != 0 } } From 2f609cefef17c6696456b856c4585ba3dec2a508 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 13 Jun 2024 18:14:00 +0200 Subject: [PATCH 029/110] RUM-4079 Migrate lint, test and ui-test to GitLab --- .gitlab-ci.yml | 110 +++++------ .../xcschemes/DatadogCore iOS.xcscheme | 58 ------ .../xcschemes/DatadogCore tvOS.xcscheme | 45 ----- .../xcschemes/DatadogTrace iOS.xcscheme | 2 +- .../xcschemes/DatadogTrace tvOS.xcscheme | 2 +- Gemfile | 2 +- Gemfile.lock | 52 +++-- .../project.pbxproj | 12 +- .../xcschemes/IntegrationScenarios.xcscheme | 10 +- .../xcschemes/Runner iOS.xcscheme | 10 +- .../xctestplans/CrashReporting.xctestplan | 34 ++++ ...gCrashReportingIntegrationTests.xctestplan | 122 ------------ .../DatadogIntegrationTests.xctestplan | 178 ------------------ .../xctestplans/Default.xctestplan | 44 +++++ .../NetworkInstrumentation.xctestplan | 54 ++++++ IntegrationTests/xctestplans/RUM.xctestplan | 41 ++++ Makefile | 177 ++++++++++++++--- tools/clean.sh | 20 ++ tools/env_check.sh | 70 +++++++ tools/license/check-license.sh | 5 +- tools/lint/patch_if_swiftlint_0.42.0.sh | 19 -- tools/lint/run-linter.sh | 2 +- tools/repo-setup/Base.ci.xcconfig.src | 17 ++ tools/repo-setup/Base.dev.xcconfig.src | 7 + tools/repo-setup/repo-setup.sh | 41 ++++ tools/rum-models-generator/run.py | 4 +- tools/runner-setup.sh | 43 +++++ tools/test.sh | 32 ++++ tools/ui-test.sh | 56 ++++++ tools/utils/argparse.sh | 121 ++++++++++++ tools/utils/echo_color.sh | 62 ++++++ xcconfigs/Base.xcconfig | 11 +- 32 files changed, 913 insertions(+), 550 deletions(-) create mode 100644 IntegrationTests/xctestplans/CrashReporting.xctestplan delete mode 100644 IntegrationTests/xctestplans/DatadogCrashReportingIntegrationTests.xctestplan delete mode 100644 IntegrationTests/xctestplans/DatadogIntegrationTests.xctestplan create mode 100644 IntegrationTests/xctestplans/Default.xctestplan create mode 100644 IntegrationTests/xctestplans/NetworkInstrumentation.xctestplan create mode 100644 IntegrationTests/xctestplans/RUM.xctestplan create mode 100755 tools/clean.sh create mode 100755 tools/env_check.sh delete mode 100755 tools/lint/patch_if_swiftlint_0.42.0.sh create mode 100644 tools/repo-setup/Base.ci.xcconfig.src create mode 100644 tools/repo-setup/Base.dev.xcconfig.src create mode 100755 tools/repo-setup/repo-setup.sh create mode 100755 tools/runner-setup.sh create mode 100755 tools/test.sh create mode 100755 tools/ui-test.sh create mode 100755 tools/utils/argparse.sh create mode 100755 tools/utils/echo_color.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddf3917437..eb7dfe6a1e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,86 +1,74 @@ stages: - - info + - pre - lint - test + - ui-test -ENV info: - stage: info +ENV check: + stage: pre tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:sonoma + - specific:true script: - - system_profiler SPSoftwareDataType # system info - - xcodebuild -version - - xcode-select -p # default Xcode - - ls /Applications/ | grep Xcode # other Xcodes - - xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore iOS" -showdestinations -quiet # installed iOS destinations - - xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdestinations -quiet # installed tvOS destinations - - xcbeautify --version - - swiftlint --version - - carthage version - - gh --version - - brew -v - - bundler --version - - python3 -V + - ./tools/runner-setup.sh --ios # temporary, waiting for AMI + - make env-check Lint: stage: lint tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:sonoma + - specific:true script: - - ./tools/lint/run-linter.sh - - ./tools/license/check-license.sh + - ./tools/runner-setup.sh --ios # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make lint license-check + - make rum-models-verify sr-models-verify -SDK unit tests (iOS): +Unit Tests (iOS): stage: test tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:sonoma + - specific:true variables: - TEST_WORKSPACE: "Datadog.xcworkspace" - TEST_DESTINATION: "platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.0.1" + OS: "latest" + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" script: - - make dependencies-gitlab - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogCoreTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogInternalTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogLogsTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogTraceTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogRUMTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogWebViewTrackingTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogSessionReplay iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCrashReporting iOS" test | xcbeautify + - ./tools/runner-setup.sh --ios # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make test-ios-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" -SDK unit tests (tvOS): +Unit Tests (tvOS): stage: test tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:sonoma + - specific:true variables: - TEST_WORKSPACE: "Datadog.xcworkspace" - TEST_DESTINATION: "platform=tvOS Simulator,name=Apple TV,OS=17.0" + OS: "latest" + PLATFORM: "tvOS Simulator" + DEVICE: "Apple TV" script: - - make dependencies-gitlab - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogCoreTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogInternalTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogLogsTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogTraceTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogRUMTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCrashReporting tvOS" test | xcbeautify + - ./tools/runner-setup.sh --tvos # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make test-tvos-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" -SDK integration tests (iOS): - stage: test +UI Tests: + stage: ui-test tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:sonoma + - specific:true variables: - TEST_WORKSPACE: "IntegrationTests/IntegrationTests.xcworkspace" - TEST_DESTINATION: "platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.0.1" + OS: "latest" + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" + parallel: + matrix: + - TEST_PLAN: + - Default + - RUM + - CrashReporting + - NetworkInstrumentation script: - - make dependencies-gitlab - - make prepare-integration-tests - # Before running crash reporting tests, disable Apple Crash Reporter so it doesn't capture the crash causing tests hang on " quit unexpectedly" prompt: - - launchctl unload -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist - - ./tools/config/generate-http-server-mock-config.sh - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "IntegrationScenarios" -testPlan DatadogIntegrationTests test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "IntegrationScenarios" -testPlan DatadogCrashReportingIntegrationTests test | xcbeautify + - ./tools/runner-setup.sh --ios # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make ui-test TEST_PLAN="$TEST_PLAN" OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index d2e0f0741f..8eea0f6703 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -197,64 +197,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Identifier = "OTelSpanTests/testSetActive_givenParentSpan()"> diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme index 5bd2dcf496..a3a85bc4c8 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme @@ -39,7 +39,7 @@ + Identifier = "OTelSpanTests/testSetActive_givenParentSpan()"> diff --git a/Gemfile b/Gemfile index 04643c8ee4..1aebea35a7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'cocoapods', '1.12.1' \ No newline at end of file +gem 'cocoapods', '1.15.2' diff --git a/Gemfile.lock b/Gemfile.lock index c7d98e8fa9..ab35897f6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,27 +1,35 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (6.1.7.7) + activesupport (7.1.3.4) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) + base64 (0.2.0) + bigdecimal (3.1.8) claide (1.1.0) - cocoapods (1.12.1) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.12.1) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -33,8 +41,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.12.1) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -45,7 +53,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -54,40 +62,44 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) + drb (2.2.1) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.15.5) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-linux-gnu) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) - json (2.6.3) - minitest (5.22.3) + json (2.7.2) + minitest (5.23.1) molinillo (0.8.0) + mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) + nkf (0.2.0) public_suffix (4.0.7) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.2.9) + strscan ruby-macho (2.5.1) strscan (3.1.0) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.22.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) - zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 @@ -96,7 +108,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - cocoapods (= 1.12.1) + cocoapods (= 1.15.2) BUNDLED WITH 2.3.25 diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj index 58125a0ff9..d807d82934 100644 --- a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj +++ b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj @@ -211,6 +211,8 @@ 6193DCCD251B6201009B8011 /* RUMTASScreen1ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTASScreen1ViewController.swift; sourceTree = ""; }; 6193DCE0251B692C009B8011 /* RUMTASTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTASTableViewController.swift; sourceTree = ""; }; 6193DCE7251B9AB1009B8011 /* RUMTASCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTASCollectionViewController.swift; sourceTree = ""; }; + 61A6F7792C208700005B74D5 /* RUM.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = RUM.xctestplan; sourceTree = ""; }; + 61A6F77A2C20891C005B74D5 /* NetworkInstrumentation.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = NetworkInstrumentation.xctestplan; sourceTree = ""; }; 61B6811E25F0EA860015B4AF /* CrashReportingWithLoggingScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingWithLoggingScenarioTests.swift; sourceTree = ""; }; 61B6815D25F135890015B4AF /* CrashReportingWithRUMScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingWithRUMScenarioTests.swift; sourceTree = ""; }; 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendLogsFixtureViewController.swift; sourceTree = ""; }; @@ -259,8 +261,8 @@ D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSwiftUIScenarioTests.swift; sourceTree = ""; }; D29495F429967780003518CD /* http-server-mock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "http-server-mock"; path = "../instrumented-tests/http-server-mock"; sourceTree = ""; }; D2A401D029925A2D00B230A3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D2AEFF1A29925BEC00A28997 /* DatadogCrashReportingIntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatadogCrashReportingIntegrationTests.xctestplan; sourceTree = ""; }; - D2AEFF1B29925BEC00A28997 /* DatadogIntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatadogIntegrationTests.xctestplan; sourceTree = ""; }; + D2AEFF1A29925BEC00A28997 /* CrashReporting.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CrashReporting.xctestplan; sourceTree = ""; }; + D2AEFF1B29925BEC00A28997 /* Default.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Default.xctestplan; sourceTree = ""; }; D2F5BB35271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMSwiftUIInstrumentationScenario.storyboard; sourceTree = ""; }; D2F5BB372718331800BDE2A4 /* SwiftUIRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIRootViewController.swift; sourceTree = ""; }; D2FCA7482B4D829F0014DC87 /* CSPictureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSPictureViewController.swift; sourceTree = ""; }; @@ -779,8 +781,10 @@ D2AEFF1929925BEC00A28997 /* xctestplans */ = { isa = PBXGroup; children = ( - D2AEFF1A29925BEC00A28997 /* DatadogCrashReportingIntegrationTests.xctestplan */, - D2AEFF1B29925BEC00A28997 /* DatadogIntegrationTests.xctestplan */, + D2AEFF1B29925BEC00A28997 /* Default.xctestplan */, + 61A6F7792C208700005B74D5 /* RUM.xctestplan */, + D2AEFF1A29925BEC00A28997 /* CrashReporting.xctestplan */, + 61A6F77A2C20891C005B74D5 /* NetworkInstrumentation.xctestplan */, ); path = xctestplans; sourceTree = ""; diff --git a/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationScenarios.xcscheme b/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationScenarios.xcscheme index 13298c2e10..91786a7ad1 100644 --- a/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationScenarios.xcscheme +++ b/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationScenarios.xcscheme @@ -177,11 +177,17 @@ + reference = "container:xctestplans/CrashReporting.xctestplan"> + + + + diff --git a/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/Runner iOS.xcscheme b/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/Runner iOS.xcscheme index 92fda4d0b2..0ffef52a44 100644 --- a/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/Runner iOS.xcscheme +++ b/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/Runner iOS.xcscheme @@ -56,11 +56,17 @@ + reference = "container:xctestplans/CrashReporting.xctestplan"> + + + + diff --git a/IntegrationTests/xctestplans/CrashReporting.xctestplan b/IntegrationTests/xctestplans/CrashReporting.xctestplan new file mode 100644 index 0000000000..c95c6ce9e3 --- /dev/null +++ b/IntegrationTests/xctestplans/CrashReporting.xctestplan @@ -0,0 +1,34 @@ +{ + "configurations" : [ + { + "id" : "15325B2B-2C4A-4B53-88F7-B6C1E3B6BE98", + "name" : "Configuration", + "options" : { + "mainThreadCheckerEnabled" : false, + "uiTestingScreenshotsLifetime" : "keepNever", + "userAttachmentLifetime" : "keepNever" + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + }, + "testTargets" : [ + { + "selectedTests" : [ + "CrashReportingWithLoggingScenarioTests\/testCrashReportingCollectOrSendWithLoggingScenario()", + "CrashReportingWithRUMScenarioTests\/testCrashReportingCollectOrSendWithRUMScenario()" + ], + "target" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + } + ], + "version" : 1 +} diff --git a/IntegrationTests/xctestplans/DatadogCrashReportingIntegrationTests.xctestplan b/IntegrationTests/xctestplans/DatadogCrashReportingIntegrationTests.xctestplan deleted file mode 100644 index 72c9962ee8..0000000000 --- a/IntegrationTests/xctestplans/DatadogCrashReportingIntegrationTests.xctestplan +++ /dev/null @@ -1,122 +0,0 @@ -{ - "configurations" : [ - { - "id" : "15325B2B-2C4A-4B53-88F7-B6C1E3B6BE98", - "name" : "Default", - "options" : { - "mainThreadCheckerEnabled" : false, - "uiTestingScreenshotsLifetime" : "keepNever", - "userAttachmentLifetime" : "keepNever" - } - } - ], - "defaultOptions" : { - "codeCoverage" : { - "targets" : [ - { - "containerPath" : "container:Datadog.xcodeproj", - "identifier" : "61133B81242393DE00786299", - "name" : "Datadog" - }, - { - "containerPath" : "container:Datadog.xcodeproj", - "identifier" : "61B7885325C180CB002675B5", - "name" : "DatadogCrashReporting" - } - ] - }, - "environmentVariableEntries" : [ - { - "key" : "DD_DISABLE_CRASH_HANDLER", - "value" : "1" - }, - { - "key" : "DD_TEST_RUNNER", - "value" : "$(DD_TEST_RUNNER)" - }, - { - "key" : "DD_API_KEY", - "value" : "$(DD_SDK_SWIFT_TESTING_APIKEY)" - }, - { - "key" : "DD_ENV", - "value" : "$(DD_SDK_SWIFT_TESTING_ENV)" - }, - { - "key" : "DD_SERVICE", - "value" : "$(DD_SDK_SWIFT_TESTING_SERVICE)" - }, - { - "key" : "DD_DISABLE_SDKIOS_INTEGRATION", - "value" : "1" - }, - { - "key" : "DD_DISABLE_HEADERS_INJECTION", - "value" : "1" - }, - { - "key" : "DD_ENABLE_RECORD_PAYLOAD", - "value" : "1" - }, - { - "key" : "SRCROOT", - "value" : "$(SRCROOT)" - }, - { - "key" : "BITRISE_SOURCE_DIR", - "value" : "$(BITRISE_SOURCE_DIR)" - }, - { - "key" : "BITRISE_TRIGGERED_WORKFLOW_ID", - "value" : "$(BITRISE_TRIGGERED_WORKFLOW_ID)" - }, - { - "key" : "BITRISE_BUILD_SLUG", - "value" : "$(BITRISE_BUILD_SLUG)" - }, - { - "key" : "BITRISE_BUILD_NUMBER", - "value" : "$(BITRISE_BUILD_NUMBER)" - }, - { - "key" : "BITRISE_BUILD_URL", - "value" : "$(BITRISE_BUILD_URL)" - }, - { - "key" : "DD_APPLICATION_KEY", - "value" : "$(DD_SDK_SWIFT_TESTING_APPLICATION_KEY)" - }, - { - "key" : "DD_CIVISIBILITY_GIT_UPLOAD_ENABLED", - "value" : "1" - }, - { - "key" : "DD_CIVISIBILITY_ITR_ENABLED", - "value" : "0" - }, - { - "key" : "DD_CIVISIBILITY_EXCLUDED_BRANCHES", - "value" : "develop,release\/*,hotfix\/*" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:IntegrationTests.xcodeproj", - "identifier" : "61441C2924616F1D003D8BB8", - "name" : "IntegrationScenarios" - } - }, - "testTargets" : [ - { - "selectedTests" : [ - "CrashReportingWithLoggingScenarioTests\/testCrashReportingCollectOrSendWithLoggingScenario()", - "CrashReportingWithRUMScenarioTests\/testCrashReportingCollectOrSendWithRUMScenario()" - ], - "target" : { - "containerPath" : "container:IntegrationTests.xcodeproj", - "identifier" : "61441C2924616F1D003D8BB8", - "name" : "IntegrationScenarios" - } - } - ], - "version" : 1 -} diff --git a/IntegrationTests/xctestplans/DatadogIntegrationTests.xctestplan b/IntegrationTests/xctestplans/DatadogIntegrationTests.xctestplan deleted file mode 100644 index 601e5afcde..0000000000 --- a/IntegrationTests/xctestplans/DatadogIntegrationTests.xctestplan +++ /dev/null @@ -1,178 +0,0 @@ -{ - "configurations" : [ - { - "id" : "D87CA41D-8EBB-4809-AC70-E3B8317FAAC7", - "name" : "TSAN", - "options" : { - "environmentVariableEntries" : [ - { - "key" : "DD_TEST_RUNNER", - "value" : "$(DD_TEST_RUNNER)" - }, - { - "key" : "DD_API_KEY", - "value" : "$(DD_SDK_SWIFT_TESTING_APIKEY)" - }, - { - "key" : "DD_ENV", - "value" : "$(DD_SDK_SWIFT_TESTING_ENV)" - }, - { - "key" : "DD_SERVICE", - "value" : "$(DD_SDK_SWIFT_TESTING_SERVICE)" - }, - { - "key" : "DD_DISABLE_SDKIOS_INTEGRATION", - "value" : "1" - }, - { - "key" : "DD_DISABLE_HEADERS_INJECTION", - "value" : "1" - }, - { - "key" : "DD_ENABLE_RECORD_PAYLOAD", - "value" : "1" - }, - { - "key" : "SRCROOT", - "value" : "$(SRCROOT)" - }, - { - "key" : "BITRISE_SOURCE_DIR", - "value" : "$(BITRISE_SOURCE_DIR)" - }, - { - "key" : "BITRISE_TRIGGERED_WORKFLOW_ID", - "value" : "$(BITRISE_TRIGGERED_WORKFLOW_ID)" - }, - { - "key" : "BITRISE_BUILD_SLUG", - "value" : "$(BITRISE_BUILD_SLUG)" - }, - { - "key" : "BITRISE_BUILD_NUMBER", - "value" : "$(BITRISE_BUILD_NUMBER)" - }, - { - "key" : "BITRISE_BUILD_URL", - "value" : "$(BITRISE_BUILD_URL)" - }, - { - "key" : "DD_ENABLE_STDOUT_INSTRUMENTATION", - "value" : "1" - }, - { - "key" : "DD_ENABLE_STDERR_INSTRUMENTATION", - "value" : "1" - } - ], - "threadSanitizerEnabled" : true - } - } - ], - "defaultOptions" : { - "codeCoverage" : { - "targets" : [ - { - "containerPath" : "container:Datadog.xcodeproj", - "identifier" : "61133B81242393DE00786299", - "name" : "Datadog" - }, - { - "containerPath" : "container:Datadog.xcodeproj", - "identifier" : "61133BEF242397DA00786299", - "name" : "DatadogObjc" - } - ] - }, - "environmentVariableEntries" : [ - { - "key" : "DD_TEST_RUNNER", - "value" : "$(DD_TEST_RUNNER)" - }, - { - "key" : "DD_API_KEY", - "value" : "$(DD_SDK_SWIFT_TESTING_APIKEY)" - }, - { - "key" : "DD_ENV", - "value" : "$(DD_SDK_SWIFT_TESTING_ENV)" - }, - { - "key" : "DD_SERVICE", - "value" : "$(DD_SDK_SWIFT_TESTING_SERVICE)" - }, - { - "key" : "DD_DISABLE_SDKIOS_INTEGRATION", - "value" : "1" - }, - { - "key" : "DD_DISABLE_HEADERS_INJECTION", - "value" : "1" - }, - { - "key" : "DD_ENABLE_RECORD_PAYLOAD", - "value" : "1" - }, - { - "key" : "SRCROOT", - "value" : "$(SRCROOT)" - }, - { - "key" : "BITRISE_SOURCE_DIR", - "value" : "$(BITRISE_SOURCE_DIR)" - }, - { - "key" : "BITRISE_TRIGGERED_WORKFLOW_ID", - "value" : "$(BITRISE_TRIGGERED_WORKFLOW_ID)" - }, - { - "key" : "BITRISE_BUILD_SLUG", - "value" : "$(BITRISE_BUILD_SLUG)" - }, - { - "key" : "BITRISE_BUILD_NUMBER", - "value" : "$(BITRISE_BUILD_NUMBER)" - }, - { - "key" : "BITRISE_BUILD_URL", - "value" : "$(BITRISE_BUILD_URL)" - }, - { - "key" : "DD_APPLICATION_KEY", - "value" : "$(DD_SDK_SWIFT_TESTING_APPLICATION_KEY)" - }, - { - "key" : "DD_CIVISIBILITY_GIT_UPLOAD_ENABLED", - "value" : "1" - }, - { - "key" : "DD_CIVISIBILITY_ITR_ENABLED", - "value" : "0" - }, - { - "key" : "DD_CIVISIBILITY_EXCLUDED_BRANCHES", - "value" : "develop,release\/*,hotfix\/*" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:IntegrationTests.xcodeproj", - "identifier" : "61441C2924616F1D003D8BB8", - "name" : "IntegrationScenarios" - } - }, - "testTargets" : [ - { - "skippedTests" : [ - "CrashReportingWithLoggingScenarioTests", - "CrashReportingWithRUMScenarioTests" - ], - "target" : { - "containerPath" : "container:IntegrationTests.xcodeproj", - "identifier" : "61441C2924616F1D003D8BB8", - "name" : "IntegrationScenarios" - } - } - ], - "version" : 1 -} diff --git a/IntegrationTests/xctestplans/Default.xctestplan b/IntegrationTests/xctestplans/Default.xctestplan new file mode 100644 index 0000000000..42c64564b4 --- /dev/null +++ b/IntegrationTests/xctestplans/Default.xctestplan @@ -0,0 +1,44 @@ +{ + "configurations" : [ + { + "id" : "D87CA41D-8EBB-4809-AC70-E3B8317FAAC7", + "name" : "Configuration", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + }, + "testTargets" : [ + { + "skippedTests" : [ + "CrashReportingWithLoggingScenarioTests", + "CrashReportingWithRUMScenarioTests", + "IntegrationTests", + "RUMManualInstrumentationScenarioTests", + "RUMMobileVitalsScenarioTests", + "RUMModalViewsScenarioTests", + "RUMNavigationControllerScenarioTests", + "RUMResourcesScenarioTests", + "RUMScrubbingScenarioTests", + "RUMStopSessionScenarioTests", + "RUMSwiftUIScenarioTests", + "RUMTabBarControllerScenarioTests", + "RUMTapActionScenarioTests", + "TracingURLSessionScenarioTests" + ], + "target" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + } + ], + "version" : 1 +} diff --git a/IntegrationTests/xctestplans/NetworkInstrumentation.xctestplan b/IntegrationTests/xctestplans/NetworkInstrumentation.xctestplan new file mode 100644 index 0000000000..8a0f25e900 --- /dev/null +++ b/IntegrationTests/xctestplans/NetworkInstrumentation.xctestplan @@ -0,0 +1,54 @@ +{ + "configurations" : [ + { + "id" : "51FFC535-B198-42D9-B7EE-3927A757F6C4", + "name" : "Configuration", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + }, + "testTargets" : [ + { + "selectedTests" : [ + "RUMResourcesScenarioTests\/testRUMNSURLSessionResourcesScenario_composition()", + "RUMResourcesScenarioTests\/testRUMNSURLSessionResourcesScenario_delegateUsingFeatureFirstPartyHosts()", + "RUMResourcesScenarioTests\/testRUMNSURLSessionResourcesScenario_delegateWithAdditionalFirstyPartyHosts()", + "RUMResourcesScenarioTests\/testRUMNSURLSessionResourcesScenario_inheritance()", + "RUMResourcesScenarioTests\/testRUMNSURLSessionResourcesScenario_legacyWithAdditionalFirstyPartyHosts()", + "RUMResourcesScenarioTests\/testRUMNSURLSessionResourcesScenario_legacyWithFeatureFirstPartyHosts()", + "RUMResourcesScenarioTests\/testRUMURLSessionResourcesScenario_composition()", + "RUMResourcesScenarioTests\/testRUMURLSessionResourcesScenario_delegateUsingFeatureFirstPartyHosts()", + "RUMResourcesScenarioTests\/testRUMURLSessionResourcesScenario_delegateWithAdditionalFirstyPartyHosts()", + "RUMResourcesScenarioTests\/testRUMURLSessionResourcesScenario_inheritance()", + "RUMResourcesScenarioTests\/testRUMURLSessionResourcesScenario_legacyWithAdditionalFirstyPartyHosts()", + "RUMResourcesScenarioTests\/testRUMURLSessionResourcesScenario_legacyWithFeatureFirstPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingNSURLSessionScenario_composition()", + "TracingURLSessionScenarioTests\/testTracingNSURLSessionScenario_delegateUsingFeatureFirstPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingNSURLSessionScenario_delegateWithAdditionalFirstyPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingNSURLSessionScenario_inheritance()", + "TracingURLSessionScenarioTests\/testTracingNSURLSessionScenario_legacyWithAdditionalFirstyPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingNSURLSessionScenario_legacyWithFeatureFirstPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingURLSessionScenario_composition()", + "TracingURLSessionScenarioTests\/testTracingURLSessionScenario_delegateUsingFeatureFirstPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingURLSessionScenario_delegateWithAdditionalFirstyPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingURLSessionScenario_directWithGlobalFirstPartyHosts()", + "TracingURLSessionScenarioTests\/testTracingURLSessionScenario_inheritance()", + "TracingURLSessionScenarioTests\/testTracingURLSessionScenario_legacyWithAdditionalFirstyPartyHosts()" + ], + "target" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + } + ], + "version" : 1 +} diff --git a/IntegrationTests/xctestplans/RUM.xctestplan b/IntegrationTests/xctestplans/RUM.xctestplan new file mode 100644 index 0000000000..1a0f7b795f --- /dev/null +++ b/IntegrationTests/xctestplans/RUM.xctestplan @@ -0,0 +1,41 @@ +{ + "configurations" : [ + { + "id" : "09CDD920-D143-447E-B9FD-F49185968B98", + "name" : "Configuration", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + }, + "testTargets" : [ + { + "selectedTests" : [ + "RUMManualInstrumentationScenarioTests\/testRUMManualInstrumentationScenario()", + "RUMMobileVitalsScenarioTests\/testRUMMobileVitalsScenario()", + "RUMMobileVitalsScenarioTests\/testRUMShortTimeSpentCPUScenario()", + "RUMModalViewsScenarioTests\/testRUMModalViewsScenario()", + "RUMModalViewsScenarioTests\/testRUMUntrackedModalViewsScenario()", + "RUMNavigationControllerScenarioTests\/testRUMNavigationControllerScenario()", + "RUMScrubbingScenarioTests\/testRUMScrubbingScenario()", + "RUMStopSessionScenarioTests\/testRUMStopSessionScenario()", + "RUMSwiftUIScenarioTests\/testSwiftUIScenario()", + "RUMTabBarControllerScenarioTests\/testRUMTabBarScenario()", + "RUMTapActionScenarioTests\/testRUMTapActionScenario()" + ], + "target" : { + "containerPath" : "container:IntegrationTests.xcodeproj", + "identifier" : "61441C2924616F1D003D8BB8", + "name" : "IntegrationScenarios" + } + } + ], + "version" : 1 +} diff --git a/Makefile b/Makefile index f2b2243240..7fd1ce9538 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,10 @@ all: dependencies templates +.PHONY: env-check repo-setup clean templates \ + lint license-check \ + test test-ios test-ios-all test-tvos test-tvos-all \ + ui-test ui-test-all ui-test-podinstall \ + models-generate rum-models-generate sr-models-generate models-verify rum-models-verify sr-models-verify \ + define DD_SDK_TESTING_XCCONFIG_CI DD_TEST_RUNNER=1\n @@ -56,15 +62,108 @@ endif endif -# Prepare project on GitLab CI (this will replace `make dependencies` once we're fully on GitLab). -dependencies-gitlab: - @echo "📝 Source xcconfigs..." - @echo $$DD_SDK_BASE_XCCONFIG > xcconfigs/Base.local.xcconfig; - @echo $$DD_SDK_BASE_XCCONFIG_CI >> xcconfigs/Base.local.xcconfig; - # We use Xcode 15 on GitLab, so overwrite deployment target in all projects to avoid build errors: - @echo "IPHONEOS_DEPLOYMENT_TARGET=12.0\n" >> xcconfigs/Base.local.xcconfig; - @echo "⚙️ Carthage bootstrap..." - @carthage bootstrap --platform iOS,tvOS --use-xcframeworks +# Default ENV for setting up the repo +DEFAULT_ENV := dev + +env-check: + @$(ECHO_TITLE) "make env-check" + ./tools/env_check.sh + +repo-setup: + @:$(eval ENV ?= $(DEFAULT_ENV)) + @$(ECHO_TITLE) "make repo-setup ENV='$(ENV)'" + ./tools/repo-setup/repo-setup.sh --env "$(ENV)" + +clean: + @$(ECHO_TITLE) "make clean" + ./tools/clean.sh + +lint: + @$(ECHO_TITLE) "make lint" + ./tools/lint/run-linter.sh + +license-check: + @$(ECHO_TITLE) "make license-check" + ./tools/license/check-license.sh + +# Test env for running iOS tests in local: +DEFAULT_IOS_OS := latest +DEFAULT_IOS_PLATFORM := iOS Simulator +DEFAULT_IOS_DEVICE := iPhone 15 Pro + +# Test env for running tvOS tests in local: +DEFAULT_TVOS_OS := latest +DEFAULT_TVOS_PLATFORM := tvOS Simulator +DEFAULT_TVOS_DEVICE := Apple TV + +# Run unit tests for specified SCHEME +test: + @$(call require_param,SCHEME) + @$(call require_param,OS) + @$(call require_param,PLATFORM) + @$(call require_param,DEVICE) + @:$(eval OS ?= $(DEFAULT_IOS_OS)) + @:$(eval PLATFORM ?= $(DEFAULT_IOS_PLATFORM)) + @:$(eval DEVICE ?= $(DEFAULT_IOS_DEVICE)) + @$(ECHO_TITLE) "make test SCHEME='$(SCHEME)' OS='$(OS)' PLATFORM='$(PLATFORM)' DEVICE='$(DEVICE)'" + ./tools/test.sh --scheme "$(SCHEME)" --os "$(OS)" --platform "$(PLATFORM)" --device "$(DEVICE)" + +# Run unit tests for specified SCHEME using iOS Simulator +test-ios: + @$(call require_param,SCHEME) + @:$(eval OS ?= $(DEFAULT_IOS_OS)) + @:$(eval PLATFORM ?= $(DEFAULT_IOS_PLATFORM)) + @:$(eval DEVICE ?= $(DEFAULT_IOS_DEVICE)) + @$(MAKE) test SCHEME="$(SCHEME)" OS="$(OS)" PLATFORM="$(PLATFORM)" DEVICE="$(DEVICE)" + +# Run unit tests for all iOS schemes +test-ios-all: + @$(MAKE) test-ios SCHEME="DatadogCore iOS" + @$(MAKE) test-ios SCHEME="DatadogInternal iOS" + @$(MAKE) test-ios SCHEME="DatadogRUM iOS" + @$(MAKE) test-ios SCHEME="DatadogSessionReplay iOS" + @$(MAKE) test-ios SCHEME="DatadogLogs iOS" + @$(MAKE) test-ios SCHEME="DatadogTrace iOS" + @$(MAKE) test-ios SCHEME="DatadogCrashReporting iOS" + @$(MAKE) test-ios SCHEME="DatadogWebViewTracking iOS" + +# Run unit tests for specified SCHEME using tvOS Simulator +test-tvos: + @$(call require_param,SCHEME) + @:$(eval OS ?= $(DEFAULT_TVOS_OS)) + @:$(eval PLATFORM ?= $(DEFAULT_TVOS_PLATFORM)) + @:$(eval DEVICE ?= $(DEFAULT_TVOS_DEVICE)) + @$(MAKE) test SCHEME="$(SCHEME)" OS="$(OS)" PLATFORM="$(PLATFORM)" DEVICE="$(DEVICE)" + +# Run unit tests for all tvOS schemes +test-tvos-all: + @$(MAKE) test-tvos SCHEME="DatadogCore tvOS" + @$(MAKE) test-tvos SCHEME="DatadogInternal tvOS" + @$(MAKE) test-tvos SCHEME="DatadogRUM tvOS" + @$(MAKE) test-tvos SCHEME="DatadogLogs tvOS" + @$(MAKE) test-tvos SCHEME="DatadogTrace tvOS" + @$(MAKE) test-tvos SCHEME="DatadogCrashReporting tvOS" + +# Run UI tests for specified TEST_PLAN +ui-test: + @$(call require_param,TEST_PLAN) + @:$(eval OS ?= $(DEFAULT_IOS_OS)) + @:$(eval PLATFORM ?= $(DEFAULT_IOS_PLATFORM)) + @:$(eval DEVICE ?= $(DEFAULT_IOS_DEVICE)) + @$(ECHO_TITLE) "make ui-test TEST_PLAN='$(TEST_PLAN)' OS='$(OS)' PLATFORM='$(PLATFORM)' DEVICE='$(DEVICE)'" + ./tools/ui-test.sh --test-plan "$(TEST_PLAN)" --os "$(OS)" --platform "$(PLATFORM)" --device "$(DEVICE)" + +# Run UI tests for all test plans +ui-test-all: + @$(MAKE) ui-test TEST_PLAN="Default" + @$(MAKE) ui-test TEST_PLAN="RUM" + @$(MAKE) ui-test TEST_PLAN="CrashReporting" + @$(MAKE) ui-test TEST_PLAN="NetworkInstrumentation" + +# Update UI test project with latest SDK +ui-test-podinstall: + @$(ECHO_TITLE) "make ui-test-podinstall" + cd IntegrationTests/ && bundle exec pod install xcodeproj-session-replay: @echo "⚙️ Generating 'DatadogSessionReplay.xcodeproj'..." @@ -83,9 +182,8 @@ open-sr-snapshot-tests: @open --env DD_TEST_UTILITIES_ENABLED ./DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcworkspace templates: - @echo "⚙️ Installing Xcode templates..." - ./tools/xcode-templates/install-xcode-templates.sh - @echo "OK 👌" + @$(ECHO_TITLE) "make templates" + ./tools/xcode-templates/install-xcode-templates.sh # Tests if current branch ships a valid SPM package. test-spm: @@ -103,31 +201,36 @@ test-cocoapods: test-xcframeworks: @cd dependency-manager-tests/xcframeworks && $(MAKE) -# Generate RUM data models from rum-events-format JSON Schemas -# - run with `git_ref=` argument to generate models for given schema commit or branch name (default is 'master'). +# Generate data models from https://github.com/DataDog/rum-events-format +models-generate: + @$(call require_param,PRODUCT) # 'rum' or 'sr' + @$(call require_param,GIT_REF) + @$(ECHO_TITLE) "make models-generate PRODUCT='$(PRODUCT)' GIT_REF='$(GIT_REF)'" + ./tools/rum-models-generator/run.py generate $(PRODUCT) --git_ref=$(GIT_REF) + +# Validate data models against https://github.com/DataDog/rum-events-format +models-verify: + @$(call require_param,PRODUCT) # 'rum' or 'sr' + @$(ECHO_TITLE) "make models-verify PRODUCT='$(PRODUCT)'" + ./tools/rum-models-generator/run.py verify $(PRODUCT) + +# Generate RUM data models rum-models-generate: - @echo "⚙️ Generating RUM models..." - ./tools/rum-models-generator/run.py generate rum --git_ref=$(if $(git_ref),$(git_ref),master) - @echo "OK 👌" + @:$(eval GIT_REF ?= master) + @$(MAKE) models-generate PRODUCT="rum" GIT_REF="$(GIT_REF)" -# Verify if RUM data models follow rum-events-format JSON Schemas +# Validate RUM data models rum-models-verify: - @echo "🧪 Verifying RUM models..." - ./tools/rum-models-generator/run.py verify rum - @echo "OK 👌" + @$(MAKE) models-verify PRODUCT="rum" -# Generate Session Replay data models from rum-events-format JSON Schemas -# - run with `git_ref=` argument to generate models for given schema commit or branch name (default is 'master'). +# Generate SR data models sr-models-generate: - @echo "⚙️ Generating Session Replay models..." - ./tools/rum-models-generator/run.py generate sr --git_ref=$(if $(git_ref),$(git_ref),master) - @echo "OK 👌" + @:$(eval GIT_REF ?= master) + @$(MAKE) models-generate PRODUCT="sr" GIT_REF="$(GIT_REF)" -# Verify if Session Replay data models follow rum-events-format JSON Schemas +# Validate SR data models sr-models-verify: - @echo "🧪 Verifying Session Replay models..." - ./tools/rum-models-generator/run.py verify sr - @echo "OK 👌" + @$(MAKE) models-verify PRODUCT="sr" sr-push-snapshots: @echo "🎬 ↗️ Pushing SR snapshots to remote repo..." @@ -188,3 +291,17 @@ bump: e2e-upload: ./tools/code-sign.sh -- $(MAKE) -C E2ETests + +# Helpers + +ECHO_TITLE=./tools/utils/echo_color.sh --title +ECHO_ERROR=./tools/utils/echo_color.sh --err +ECHO_WARNING=./tools/utils/echo_color.sh --warn +ECHO_SUCCESS=./tools/utils/echo_color.sh --succ + +define require_param + if [ -z "$${$(1)}" ]; then \ + $(ECHO_ERROR) "Error:" "$(1) parameter is required but not provided."; \ + exit 1; \ + fi +endef diff --git a/tools/clean.sh b/tools/clean.sh new file mode 100755 index 0000000000..a75229c5d4 --- /dev/null +++ b/tools/clean.sh @@ -0,0 +1,20 @@ +#!/bin/zsh + +# Usage: +# $ ./tools/clean.sh + +source ./tools/utils/echo_color.sh + +echo_warn "Cleaning" "~/Library/Developer/Xcode/DerivedData/" +rm -rf ~/Library/Developer/Xcode/DerivedData/* + +echo_warn "Cleaning" "./Carthage/" +rm -rf ./Carthage/Build/* +rm -rf ./Carthage/Checkouts/* + +echo_warn "Cleaning" "./IntegrationTests/Pods/" +rm -rf ./IntegrationTests/Pods/* + +echo_warn "Cleaning" "local xcconfigs" +rm -vf ./xcconfigs/Base.ci.local.xcconfig +rm -vf ./xcconfigs/Base.dev.local.xcconfig diff --git a/tools/env_check.sh b/tools/env_check.sh new file mode 100755 index 0000000000..8c362680a9 --- /dev/null +++ b/tools/env_check.sh @@ -0,0 +1,70 @@ +#!/bin/zsh + +# Usage: +# ./tools/env_check.sh +# Prints environment information and checks if required tools are installed. + +source ./tools/utils/echo_color.sh + +check_if_installed() { + if ! command -v $1 >/dev/null 2>&1; then + echo_err "Error" "$1 is not installed but it is required for development. Install it and try again." + exit 1 + fi +} + +echo_succ "System info:" +system_profiler SPSoftwareDataType + +echo "" +echo_succ "Active Xcode:" +check_if_installed xcodebuild +xcode-select -p +xcodebuild -version + +echo "" +echo_succ "Other Xcodes:" +ls /Applications/ | grep Xcode + +echo "" +echo_succ "xcbeautify:" +check_if_installed xcbeautify +xcbeautify --version + +echo "" +echo_succ "swiftlint:" +check_if_installed swiftlint +swiftlint --version + +echo "" +echo_succ "carthage:" +check_if_installed carthage +carthage version + +echo "" +echo_succ "gh:" +check_if_installed gh +gh --version + +echo "" +echo_succ "bundler:" +check_if_installed bundler +bundler --version + +echo "" +echo_succ "python3:" +check_if_installed python3 +python3 -V + +echo "" +echo_succ "Installed iOS runtimes:" +xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore iOS" -showdestinations -quiet | grep platform + +echo "" +echo_succ "Installed tvOS runtimes:" +xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdestinations -quiet | grep platform + +if command -v brew >/dev/null 2>&1; then + echo_succ "brew:" + brew -v +fi \ No newline at end of file diff --git a/tools/license/check-license.sh b/tools/license/check-license.sh index 2af7774d2a..f0b53ff6c0 100755 --- a/tools/license/check-license.sh +++ b/tools/license/check-license.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/zsh if [ ! -f "Package.swift" ]; then echo "\`check-license.sh\` must be run in repository root folder: \`./tools/license/check-license.sh\`"; exit 1 @@ -19,7 +19,8 @@ function files { -not -path "./tools/rum-models-generator/rum-events-format/*" \ -not -path "*/tools/distribution/venv/*" \ -not -path "*/tools/ci/venv/*" \ - -not -path "./instrumented-tests/DatadogSDKTesting.xcframework/*" \ + -not -path "*.xcframework/*" \ + -not -path "*.xcarchive/*" \ -not -name "OTSpan.swift" \ -not -name "OTFormat.swift" \ -not -name "OTTracer.swift" \ diff --git a/tools/lint/patch_if_swiftlint_0.42.0.sh b/tools/lint/patch_if_swiftlint_0.42.0.sh deleted file mode 100755 index eace92377f..0000000000 --- a/tools/lint/patch_if_swiftlint_0.42.0.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Patches `sources.swiftlint.yml` and `tests.swiftlint.yml` configs for swiftlint `0.42.0`. -# We need to run this patch on Bitrise, as their brew-core mirror doesn't include swiftlint `0.43.1` -# on some agent versions (notably: `Agent version: 1.20.0` considers `0.42.0` as the latest version). -# -# REF: we could eventually switch to the official brew source, but this is discouraged in -# https://discuss.bitrise.io/t/how-to-change-brew-core-from-mirror-to-official/16033 - -SWIFTLINT_VERSION=$(swiftlint version) - -if [ $SWIFTLINT_VERSION = "0.42.0" ]; then - echo "⚙️ Found swiftlint '0.42.0', applying the patch." - # Replace "../../" with "" - sed -i '' 's/..\/..\///g' tools/lint/sources.swiftlint.yml - sed -i '' 's/..\/..\///g' tools/lint/tests.swiftlint.yml -else - echo "⚙️ Using swiftlint '${SWIFTLINT_VERSION}', no need to patch." -fi diff --git a/tools/lint/run-linter.sh b/tools/lint/run-linter.sh index b4293ba125..5cca9bc276 100755 --- a/tools/lint/run-linter.sh +++ b/tools/lint/run-linter.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/zsh if [ ! -f "Package.swift" ]; then echo "\`run-linter.sh\` must be run in repository root folder: \`./tools/lint/run-linter.sh\`"; exit 1 diff --git a/tools/repo-setup/Base.ci.xcconfig.src b/tools/repo-setup/Base.ci.xcconfig.src new file mode 100644 index 0000000000..a1f93610b4 --- /dev/null +++ b/tools/repo-setup/Base.ci.xcconfig.src @@ -0,0 +1,17 @@ +// Base.ci.local.xcconfig for CI env, sourced from `make repo-setup` +// - It is git-ignored, so not present for git clones in dependency managers. +// - It is also sourced for DEV env, but certain settings can be overwritten (disabled) for the +// convenience of local development. + +// Active compilation conditions that are only enabled on the local machine: +// - DD_SDK_COMPILED_FOR_TESTING: This condition ensures the SDK code is compiled specifically for testing purposes. +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DD_SDK_COMPILED_FOR_TESTING + +// Build only the active architecture to optimize build time +ONLY_ACTIVE_ARCH = YES + +// Treat all warnings as errors to prevent unresolved warnings +SWIFT_TREAT_WARNINGS_AS_ERRORS = YES + +// Indicates the build is running on a CI. This value is injected into the `Info.plist` of some targets. +IS_CI = true diff --git a/tools/repo-setup/Base.dev.xcconfig.src b/tools/repo-setup/Base.dev.xcconfig.src new file mode 100644 index 0000000000..3563b7ba43 --- /dev/null +++ b/tools/repo-setup/Base.dev.xcconfig.src @@ -0,0 +1,7 @@ +// Base.dev.local.xcconfig for DEV env (all contributors), sourced from `make repo-setup` +// - It is git-ignored, so not present for git clones in dependency managers. +// - It is applied on top of Base.ci.local.xcconfig (for CI) and can be used to disable CI-specific +// settings for the convenience of local development. + +IS_CI = false +SWIFT_TREAT_WARNINGS_AS_ERRORS = NO diff --git a/tools/repo-setup/repo-setup.sh b/tools/repo-setup/repo-setup.sh new file mode 100755 index 0000000000..009b6fb4e6 --- /dev/null +++ b/tools/repo-setup/repo-setup.sh @@ -0,0 +1,41 @@ +#!/bin/zsh + +# Usage: +# $ ./tools/repo-setup/repo-setup.sh -h +# Prepares the repository for development and testing in given ENV. + +# Options: +# --env: Specifies the environment for preparation. Use 'dev' for local development and 'ci' for CI. + +source ./tools/utils/argparse.sh +source ./tools/utils/echo_color.sh + +set_description "Prepares the repository for development and testing in given ENV." +define_arg "env" "" "Specifies the environment for preparation. Use 'dev' for local development and 'ci' for CI." "string" "true" + +check_for_help "$@" +parse_args "$@" + +ENV_DEV="dev" +ENV_CI="ci" + +if [[ "$env" != "$ENV_CI" && "$env" != "$ENV_DEV" ]]; then + echo_err "Error: env variable must be 'ci' or 'dev'." + exit 1 +fi + +set -eo pipefail + +# Materialize CI xcconfig: +cp -vi "./tools/repo-setup/Base.ci.xcconfig.src" ./xcconfigs/Base.ci.local.xcconfig + +# Materialize DEV xcconfig: +if [[ "$env" == "$ENV_DEV" ]]; then + cp -vi "./tools/repo-setup/Base.dev.xcconfig.src" ./xcconfigs/Base.dev.local.xcconfig +fi + +bundle install +carthage bootstrap --platform iOS,tvOS --use-xcframeworks + +echo_succ "Using OpenTelemetryApi version: $(cat ./Carthage/Build/.OpenTelemetryApi.version | grep 'commitish' | awk -F'"' '{print $4}')" +echo_succ "Using PLCrashReporter version: $(cat ./Carthage/Build/.plcrashreporter.version | grep 'commitish' | awk -F'"' '{print $4}')" diff --git a/tools/rum-models-generator/run.py b/tools/rum-models-generator/run.py index bb5b4dd7b6..30d25026fa 100755 --- a/tools/rum-models-generator/run.py +++ b/tools/rum-models-generator/run.py @@ -15,7 +15,7 @@ import subprocess from dataclasses import dataclass -SCHEMAS_REPO_SSH = 'git@github.com:DataDog/rum-events-format.git' +SCHEMAS_REPO = 'https://github.com/DataDog/rum-events-format.git' # JSON Schema paths (relative to cwd) RUM_SCHEMA_PATH = '/rum-events-format/rum-events-format.json' @@ -104,7 +104,7 @@ def clone_schemas_repo(git_ref: str): """ print(f'⚙️ Cloning `rum-events-format` repository at "{git_ref}"...') shell_output('rm -rf rum-events-format') - shell_output(f'git clone {SCHEMAS_REPO_SSH}') + shell_output(f'git clone {SCHEMAS_REPO}') shell_output(f'cd rum-events-format && git fetch origin {git_ref} && git checkout FETCH_HEAD') sha = shell_output(f'cd rum-events-format && git rev-parse HEAD') return sha diff --git a/tools/runner-setup.sh b/tools/runner-setup.sh new file mode 100755 index 0000000000..284561fbf9 --- /dev/null +++ b/tools/runner-setup.sh @@ -0,0 +1,43 @@ +#!/bin/zsh + +# Usage: +# $ ./tools/runner-setup.sh -h +# This script is for TEMPORARY. It supplements missing components on the runner. It will be removed once all configurations are integrated into the AMI. + +# Options: +# --ios: Flag that prepares the runner instance for iOS testing. Disabled by default. +# --tvos: Flag that prepares the runner instance for tvOS testing. Disabled by default. + +source ./tools/utils/echo_color.sh +source ./tools/utils/argparse.sh + +set_description "This script is for TEMPORARY. It supplements missing components on the runner. It will be removed once all configurations are integrated into the AMI." +define_arg "ios" "false" "Flag that prepares the runner instance for iOS testing. Disabled by default." "store_true" +define_arg "tvos" "false" "Flag that prepares the runner instance for tvOS testing. Disabled by default." "store_true" + +check_for_help "$@" +parse_args "$@" + +set -eo pipefail + +xcodebuild -version + +if [ "$ios" = "true" ]; then + echo "Check current runner for any iPhone Simulator runtime supporting OS 17.x." + if ! xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore iOS" -showdestinations -quiet | grep -q 'platform:iOS Simulator.*OS:17'; then + echo_warn "Found no iOS Simulator runtime supporting OS 17.x. Installing..." + # xcodebuild -downloadPlatform iOS -quiet | xcbeautify + else + echo_succ "Found some iOS Simulator runtime supporting OS 17.x. Skipping..." + fi +fi + +if [ "$tvos" = "true" ]; then + echo "Check current runner for any tvOS Simulator runtime supporting OS 17.x." + if ! xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdestinations -quiet | grep -q 'platform:tvOS Simulator.*OS:17'; then + echo_warn "Found no tvOS Simulator runtime supporting OS 17.x. Installing..." + # xcodebuild -downloadPlatform tvOS -quiet | xcbeautify + else + echo_succ "Found some tvOS Simulator runtime supporting OS 17.x. Skipping..." + fi +fi diff --git a/tools/test.sh b/tools/test.sh new file mode 100755 index 0000000000..3732a8fae2 --- /dev/null +++ b/tools/test.sh @@ -0,0 +1,32 @@ +#!/bin/zsh + +# Usage: +# $ ./tools/test.sh -h +# Executes unit tests for a specified --scheme, using the provided --os, --platform, and --device. + +# Options: +# --device: Specifies the simulator device for running tests, e.g. 'iPhone 15 Pro' +# --scheme: Identifies the test scheme to execute +# --platform: Defines the type of simulator platform for the tests, e.g. 'iOS Simulator' +# --os: Sets the operating system version for the tests, e.g. '17.5' + +source ./tools/utils/argparse.sh + +set_description "Executes unit tests for a specified --scheme, using the provided --os, --platform, and --device." +define_arg "scheme" "" "Identifies the test scheme to execute" "string" "true" +define_arg "os" "" "Sets the operating system version for the tests, e.g. '17.5'" "string" "true" +define_arg "platform" "" "Defines the type of simulator platform for the tests, e.g. 'iOS Simulator'" "string" "true" +define_arg "device" "" "Specifies the simulator device for running tests, e.g. 'iPhone 15 Pro'" "string" "true" + +check_for_help "$@" +parse_args "$@" + +WORKSPACE="Datadog.xcworkspace" +DESTINATION="platform=$platform,name=$device,OS=$os" +SCHEME=$scheme + +set -x +set -eo pipefail + +xcodebuild -version +xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" test | xcbeautify diff --git a/tools/ui-test.sh b/tools/ui-test.sh new file mode 100755 index 0000000000..dfdc2b877c --- /dev/null +++ b/tools/ui-test.sh @@ -0,0 +1,56 @@ +#!/bin/zsh + +# Usage: +# $ ./tools/ui-test.sh -h +# Executes UI tests for a specified --test-plan using the provided --os, --platform, and --device. + +# Options: +# --device: Specifies the simulator device for running tests, e.g. 'iPhone 15 Pro' +# --platform: Defines the type of simulator platform for the tests, e.g. 'iOS Simulator' +# --os: Sets the operating system version for the tests, e.g. '17.5' +# --test-plan: Identifies the test plan to run + +source ./tools/utils/echo_color.sh +source ./tools/utils/argparse.sh + +set_description "Executes UI tests for a specified --test-plan using the provided --os, --platform, and --device." +define_arg "test-plan" "" "Identifies the test plan to run" "string" "true" +define_arg "os" "" "Sets the operating system version for the tests, e.g. '17.5'" "string" "true" +define_arg "platform" "" "Defines the type of simulator platform for the tests, e.g. 'iOS Simulator'" "string" "true" +define_arg "device" "" "Specifies the simulator device for running tests, e.g. 'iPhone 15 Pro'" "string" "true" + +check_for_help "$@" +parse_args "$@" + +disable_apple_crash_reporter() { + launchctl unload -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist + echo_warn "Disabling Apple Crash Reporter before running UI tests." + echo "This action disables the system prompt ('Runner quit unexpectedly') if an app crashes in the simulator, which" + echo "is expected behavior when running UI tests for 'DatadogCrashReporting'." +} + +enable_apple_crash_reporter() { + launchctl load -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist + echo_succ "Apple Crash Reporter has been re-enabled after the UI tests." +} + +set -x +set -eo pipefail + +DIR=$(pwd) +cd IntegrationTests/ && bundle exec pod install +cd "$DIR" + +WORKSPACE="IntegrationTests/IntegrationTests.xcworkspace" +DESTINATION="platform=$platform,name=$device,OS=$os" +SCHEME="IntegrationScenarios" +TEST_PLAN="$test_plan" + +./tools/config/generate-http-server-mock-config.sh + +xcodebuild -version + +disable_apple_crash_reporter +trap enable_apple_crash_reporter EXIT INT + +xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" -testPlan "$TEST_PLAN" test | xcbeautify diff --git a/tools/utils/argparse.sh b/tools/utils/argparse.sh new file mode 100755 index 0000000000..5bd29143a9 --- /dev/null +++ b/tools/utils/argparse.sh @@ -0,0 +1,121 @@ +#!/bin/zsh + +# argparse.sh contains zsh functions that streamlines parsing of +# command-line arguments in zsh scripts + +# Example: +# define_arg "username" "" "Username for login" "string" "true" +# parse_args "$@" +# +# echo "Welcome, $username!" +# +# # Usage: +# # ./example.sh --username Alice + +# Author: Yaacov Zamir +# License: MIT License. +# https://github.com/yaacov/argparse-sh/ + +# Modified by ncreated to make this script compatible with macOS zsh +# See: https://github.com/ncreated/argparse-zsh + +# Declare an associative array for argument properties +declare -A ARG_PROPERTIES + +# Variable for the script description +SCRIPT_DESCRIPTION="" + +# Function to display an error message and exit +# Usage: display_error "Error message" +display_error() { + echo -e "Error: $1\n" + show_help + exit 1 +} + +# Function to set the script description +# Usage: set_description "Description text" +set_description() { + SCRIPT_DESCRIPTION="$1" +} + +# Escape argument name +escape() { + echo "$1" | tr '-' '_' # replace '-' with '_' +} + +# Function to define a command-line argument +# Usage: define_arg "arg_name" ["default"] ["help text"] ["action"] ["required"] +define_arg() { + local arg_name=$1 + ARG_PROPERTIES["$arg_name,default"]=${2:-""} # Default value + ARG_PROPERTIES["$arg_name,help"]=${3:-""} # Help text + ARG_PROPERTIES["$arg_name,action"]=${4:-"string"} # Action, default is "string" + ARG_PROPERTIES["$arg_name,required"]=${5:-"false"} # Required flag, default is "false" +} + +# Function to parse command-line arguments +# Usage: parse_args "$@" +parse_args() { + while [[ $# -gt 0 ]]; do + key="$1" + key="${key#--}" # Remove the '--' prefix + + if [[ -n "${ARG_PROPERTIES["$key,help"]}" ]]; then + if [[ "${ARG_PROPERTIES["$key,action"]}" == "store_true" ]]; then + escaped_key=$(escape $key) + export "$escaped_key"="true" + shift # past the flag argument + else + [[ -z "$2" || "$2" == --* ]] && display_error "Missing value for argument --$key" + escaped_key=$(escape $key) + export "$escaped_key"="$2" + shift # past argument + shift # past value + fi + else + display_error "Unknown option: $key" + fi + done + + # Check for required arguments + for arg in "${(k)ARG_PROPERTIES[@]}"; do + arg_name="${arg%%,*}" # Extract argument name (drop suffix) + arg_name="${arg_name#\"}" # Extract argument name (drop prefix) + escaped_arg_name=$(escape $arg_name) + [[ "${ARG_PROPERTIES["$arg_name,required"]}" == "true" && -z "${(P)escaped_arg_name}" ]] && display_error "Missing required argument --$arg_name" + done + + # Set defaults for any unset arguments + for arg in "${(k)ARG_PROPERTIES[@]}"; do + arg_name="${arg%%,*}" # Extract argument name (drop suffix) + arg_name="${arg_name#\"}" # Extract argument name (drop prefix) + escaped_arg_name=$(escape $arg_name) + [[ -z "${(P)escaped_arg_name}" ]] && export "$escaped_arg_name"="${ARG_PROPERTIES["$arg_name,default"]}" + done +} + +# Function to display help +# Usage: show_help +show_help() { + [[ -n "$SCRIPT_DESCRIPTION" ]] && echo -e "$SCRIPT_DESCRIPTION\n" + + echo "Options:" + for arg in "${(k)ARG_PROPERTIES[@]}"; do + arg_name="${arg%%,*}" # Extract argument name (drop suffix) + arg_name="${arg_name#\"}" # Extract argument name (drop prefix) + arg_prop=${arg##*,} # Extract argument property (drop prefix) + arg_prop="${arg_prop%%\"}" # Extract argument property (drop suffix) + [[ "$arg_prop" == "help" ]] && { + [[ "${ARG_PROPERTIES["$arg_name,action"]}" != "store_true" ]] && echo " --$arg_name: ${ARG_PROPERTIES[$arg]}" || echo " --$arg_name: ${ARG_PROPERTIES[$arg]}" + } + done +} + +# Function to check for help option +# Usage: check_for_help "$@" +check_for_help() { + for arg in "$@"; do + [[ $arg == "-h" || $arg == "--help" ]] && { show_help; exit 0; } + done +} diff --git a/tools/utils/echo_color.sh b/tools/utils/echo_color.sh new file mode 100755 index 0000000000..ed4f5d260e --- /dev/null +++ b/tools/utils/echo_color.sh @@ -0,0 +1,62 @@ +#!/bin/zsh + +# Usage: +# ./tools/utils/echo_color.sh [OPTION] "message" [additional_message] +# This script renders colored output for different types of log messages. +# It supports error, warning, success messages, and titles, each with distinctive coloring. + +# Options: +# --err Display the message as an error in red. +# --warn Display the message as a warning in yellow. +# --succ Display the message as a success in green. +# --title Display the message in a purple title box. + +# Arguments: +# "message" Mandatory first argument; content varies based on the chosen option. +# [additional_message] Optional second argument for --err, --warn, and --succ, not used with --title. + +RED="\e[31m" +YELLOW="\e[33m" +GREEN="\e[32m" +RESET="\e[0m" +BOLD="\e[1m" +PURPLE="\e[35m" + +echo_err() { + echo "${RED}$1${RESET} $2" +} + +echo_warn() { + echo "${YELLOW}$1${RESET} $2" +} + +echo_succ() { + echo "${GREEN}$1${RESET} $2" +} + +echo_title() { + echo "" + local len=$((${#1}+2)) + local separator=$(printf '.%.0s' {1..$len}) + echo -e " ${PURPLE}${separator}${RESET}" + echo -e "${PURPLE}┌$(printf '%*s' $len | tr ' ' '─')┐${RESET}" + echo -e "${PURPLE}│ ${BOLD}$1${RESET}${PURPLE} │${RESET}" + echo -e "${PURPLE}└$(printf '%*s' $len | tr ' ' '─')┘${RESET}" +} + +case "$1" in + --err) + echo_err "$2" "$3" + ;; + --warn) + echo_warn "$2" "$3" + ;; + --succ) + echo_succ "$2" "$3" + ;; + --title) + echo_title "$2" + ;; + *) + ;; +esac diff --git a/xcconfigs/Base.xcconfig b/xcconfigs/Base.xcconfig index 6907ed93c2..ad4f975149 100644 --- a/xcconfigs/Base.xcconfig +++ b/xcconfigs/Base.xcconfig @@ -1,5 +1,5 @@ // Base configuration file for all targets. -// Note: all configuration here will be applied to `Datadog*.framework` produced by Carthage. +// Note: all configuration here will be applied to artifacts produced by Carthage. DD_SWIFT_SDK_PRODUCT_NAME=DatadogCore DD_OBJC_SDK_PRODUCT_NAME=DatadogObjc @@ -16,4 +16,13 @@ MACOSX_DEPLOYMENT_TARGET=12.6 SWIFT_VERSION=5.9 // Include internal base config (git-ignored, so excluded from Carthage build) +// TODO: RUM-4079 Remove once when we're fully on GitLab #include? "Base.local.xcconfig" + +// Apply git-ignored overrides for CI environment (if exists) +// NOTE: This won't exist when SDK is built from source by dependency managers +#include? "Base.ci.local.xcconfig" + +// Apply git-ignored overrides for all DEV environments (if exists) +// NOTE: This won't exist on CI and when SDK is built from source by dependency managers +#include? "Base.dev.local.xcconfig" From df44f45afdfd04b00f1ebb58d3bfc3f8e3082016 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 18 Jun 2024 12:49:11 +0200 Subject: [PATCH 030/110] RUM-4079 Configure GL workflow --- .gitlab-ci.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eb7dfe6a1e..0575e1b779 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,20 +4,29 @@ stages: - test - ui-test -ENV check: - stage: pre +# Workflow configuration for all jobs in this file +# Ref.: https://docs.gitlab.com/ee/ci/yaml/workflow.html +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == 'merge_request_event' + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_BRANCH == 'develop' || $CI_COMMIT_BRANCH == 'master' + +# Tags applied to all jobs +# Ref.: https://docs.gitlab.com/ee/ci/yaml/ +default: tags: - macos:sonoma - specific:true + +ENV check: + stage: pre script: - ./tools/runner-setup.sh --ios # temporary, waiting for AMI - make env-check Lint: stage: lint - tags: - - macos:sonoma - - specific:true script: - ./tools/runner-setup.sh --ios # temporary, waiting for AMI - make clean repo-setup ENV=ci @@ -26,9 +35,6 @@ Lint: Unit Tests (iOS): stage: test - tags: - - macos:sonoma - - specific:true variables: OS: "latest" PLATFORM: "iOS Simulator" @@ -40,9 +46,6 @@ Unit Tests (iOS): Unit Tests (tvOS): stage: test - tags: - - macos:sonoma - - specific:true variables: OS: "latest" PLATFORM: "tvOS Simulator" @@ -54,9 +57,6 @@ Unit Tests (tvOS): UI Tests: stage: ui-test - tags: - - macos:sonoma - - specific:true variables: OS: "latest" PLATFORM: "iOS Simulator" From 13c5df8110fb81c5da17431358161ac04638a246 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 18 Jun 2024 13:35:33 +0200 Subject: [PATCH 031/110] RUM-4079 Remove tests, lint and ui-tests from Bitrise CI --- bitrise.yml | 163 ---------------------------------------------------- 1 file changed, 163 deletions(-) diff --git a/bitrise.yml b/bitrise.yml index c9c1872cbb..2a686017f0 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -11,17 +11,13 @@ workflows: This workflow is triggered on starting new PR or pushing new changes to existing PRs. By default, it doesn't run any test phases, but this behaviour is overwritten in `choose_workflows.py` when: - one or more `DD_OVERWRITE_RUN_(phase)_TESTS` ENVs are passed to the current CI job: - - DD_OVERRIDE_RUN_UNIT_TESTS='1' to run unit tests phase for the main SDK - DD_OVERRIDE_RUN_SR_UNIT_TESTS='1' to run unit tests phase for Session Replay product - - DD_OVERRIDE_RUN_INTEGRATION_TESTS='1' to run integration tests phase - DD_OVERRIDE_RUN_SMOKE_TESTS='1' to run smoke tests phase - DD_OVERRIDE_RUN_TOOLS_TESTS='1' to run tools tests phase - a phase is selected on the checklist in the PR description, - the PR changes a file which matches phase filter (e.g. changing a file in `Sources/*` will trigger unit tests phase) envs: - - DD_RUN_UNIT_TESTS: '0' - DD_RUN_SR_UNIT_TESTS: '0' - - DD_RUN_INTEGRATION_TESTS: '0' - DD_RUN_SMOKE_TESTS: '0' - DD_RUN_TOOLS_TESTS: '0' after_run: @@ -33,9 +29,7 @@ workflows: description: |- This workflow is triggered for each new commit pushed to `develop` or `master` branch. envs: - - DD_RUN_UNIT_TESTS: '1' - DD_RUN_SR_UNIT_TESTS: '1' - - DD_RUN_INTEGRATION_TESTS: '1' - DD_RUN_SMOKE_TESTS: '0' - DD_RUN_TOOLS_TESTS: '0' after_run: @@ -49,9 +43,7 @@ workflows: description: |- This workflow is triggered every night. envs: - - DD_RUN_UNIT_TESTS: '0' - DD_RUN_SR_UNIT_TESTS: '0' - - DD_RUN_INTEGRATION_TESTS: '0' - DD_RUN_SMOKE_TESTS: '1' - DD_RUN_TOOLS_TESTS: '1' after_run: @@ -75,9 +67,7 @@ workflows: description: |- This workflow is triggered on pushing a new release tag. envs: - - DD_RUN_UNIT_TESTS: '1' - DD_RUN_SR_UNIT_TESTS: '1' - - DD_RUN_INTEGRATION_TESTS: '1' - DD_RUN_SMOKE_TESTS: '1' - DD_RUN_TOOLS_TESTS: '0' after_run: @@ -141,119 +131,15 @@ workflows: # by modifying `DD_RUN_*` ENV variables with `envman` (ref.: https://github.com/bitrise-io/envman). venv/bin/python3 choose_workflows.py after_run: - - run_linter - run_unit_tests - - run_integration_tests - run_smoke_tests - run_tools_tests - run_linter: - description: |- - Runs swiftlint and license check for all source and test files. - steps: - - script: - title: Patch linter configuration for swiftlint 0.42.0 - inputs: - - content: |- - #!/usr/bin/env bash - set -e - ./tools/lint/patch_if_swiftlint_0.42.0.sh - - swiftlint@0.8.0: - title: Lint Sources/* - inputs: - - strict: 'yes' - - lint_config_file: "$BITRISE_SOURCE_DIR/tools/lint/sources.swiftlint.yml" - - linting_path: "$BITRISE_SOURCE_DIR" - - reporter: emoji - - swiftlint@0.8.0: - title: Lint Tests/* - inputs: - - strict: 'yes' - - linting_path: "$BITRISE_SOURCE_DIR" - - lint_config_file: "$BITRISE_SOURCE_DIR/tools/lint/tests.swiftlint.yml" - - reporter: emoji - - script: - title: Check license headers - is_always_run: true - inputs: - - content: |- - #!/usr/bin/env bash - set -e - ./tools/license/check-license.sh - run_unit_tests: description: |- - Runs unit tests for SDK on iOS Simulator. - Runs unit tests for SDK on tvOS Simulator. Selectively runs: - - main SDK tests when 'DD_RUN_UNIT_TESTS' is '1' - or Session Replay tests when when 'DD_RUN_SR_UNIT_TESTS' is '1' steps: - - script: - title: Verify RUM data models - run_if: '{{enveq "DD_RUN_UNIT_TESTS" "1"}}' - inputs: - - content: |- - #!/usr/bin/env zsh - set -e - make rum-models-verify ci=${CI} - - script: - title: Verify SR data models - run_if: '{{enveq "DD_RUN_SR_UNIT_TESTS" "1"}}' - inputs: - - content: |- - #!/usr/bin/env zsh - set -e - make sr-models-verify ci=${CI} - - xcode-test: - title: Run unit tests for Datadog - iOS Simulator - run_if: '{{enveq "DD_RUN_UNIT_TESTS" "1"}}' - inputs: - - scheme: DatadogCore iOS - - destination: platform=iOS Simulator,name=iPhone 11,OS=latest - - is_clean_build: 'yes' - - generate_code_coverage_files: 'yes' - - project_path: Datadog.xcworkspace - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Datadog-ios-unit-tests.html" - - xcode-test: - title: Run unit tests for DatadogCrashReporting - iOS Simulator - run_if: '{{enveq "DD_RUN_UNIT_TESTS" "1"}}' - inputs: - - scheme: DatadogCrashReporting iOS - - destination: platform=iOS Simulator,name=iPhone 11,OS=latest - - generate_code_coverage_files: 'yes' - - project_path: Datadog.xcworkspace - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/DatadogCrashReporting-ios-unit-tests.html" - - xcode-test: - title: Run unit tests for Datadog - tvOS Simulator - run_if: '{{enveq "DD_RUN_UNIT_TESTS" "1"}}' - inputs: - - scheme: DatadogCore tvOS - - destination: platform=tvOS Simulator,name=Apple TV,OS=latest - - is_clean_build: 'yes' - - generate_code_coverage_files: 'yes' - - project_path: Datadog.xcworkspace - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Datadog-tvos-unit-tests.html" - - xcode-test: - title: Run unit tests for DatadogCrashReporting - tvOS Simulator - run_if: '{{enveq "DD_RUN_UNIT_TESTS" "1"}}' - inputs: - - scheme: DatadogCrashReporting tvOS - - destination: platform=tvOS Simulator,name=Apple TV,OS=latest - - generate_code_coverage_files: 'yes' - - project_path: Datadog.xcworkspace - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/DatadogCrashReporting-tvos-unit-tests.html" - - xcode-test: - title: Run unit tests for Session Replay - iOS Simulator - run_if: '{{enveq "DD_RUN_SR_UNIT_TESTS" "1"}}' - inputs: - - scheme: DatadogSessionReplay iOS - - destination: platform=iOS Simulator,name=iPhone 11,OS=latest - - should_build_before_test: 'no' - - is_clean_build: 'no' - - generate_code_coverage_files: 'yes' - - project_path: Datadog.xcworkspace - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/DatadogSessionReplay-ios-unit-tests.html" - script: title: Pull Session Replay snapshots run_if: '{{enveq "DD_RUN_SR_UNIT_TESTS" "1"}}' @@ -285,55 +171,6 @@ workflows: - project_path: DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcworkspace - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/DatadogSessionReplay-snapshot-tests.html" - run_integration_tests: - description: |- - Build benchmarks for SDK on iOS Simulator. - Runs integration tests from Datadog.xcworkspace. - Only ran if 'DD_RUN_INTEGRATION_TESTS' is '1'. - steps: - - script: - title: Prepare Integration Tests - run_if: '{{enveq "DD_RUN_INTEGRATION_TESTS" "1"}}' - inputs: - - content: |- - #!/usr/bin/env zsh - set -e - make prepare-integration-tests - ./tools/config/generate-http-server-mock-config.sh - - xcode-test: - title: Run integration tests for RUM, Logging, Tracing and SR (on iOS Simulator) - run_if: '{{enveq "DD_RUN_INTEGRATION_TESTS" "1"}}' - inputs: - - scheme: IntegrationScenarios - - destination: platform=iOS Simulator,name=iPhone 11,OS=latest - - should_build_before_test: 'no' - - is_clean_build: 'no' - - generate_code_coverage_files: 'yes' - - project_path: IntegrationTests/IntegrationTests.xcworkspace - - xcodebuild_options: -testPlan DatadogIntegrationTests - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/DatadogIntegration-tests.html" - - script: - title: Disable Apple Crash Reporter - run_if: '{{enveq "DD_RUN_INTEGRATION_TESTS" "1"}}' - inputs: - - content: |- - #!/usr/bin/env zsh - # We suspect Apple Crash Reporter causing flakiness in our CR integration tests. - # Disabling it makes the system prompt ("Example iOS quit unexpectedly") not appear. - launchctl unload -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist - - xcode-test: - title: Run integration tests for Crash Reporting (on iOS Simulator) - run_if: '{{enveq "DD_RUN_INTEGRATION_TESTS" "1"}}' - inputs: - - scheme: IntegrationScenarios - - destination: platform=iOS Simulator,name=iPhone 11,OS=latest - - should_build_before_test: 'no' - - is_clean_build: 'no' - - generate_code_coverage_files: 'yes' - - project_path: IntegrationTests/IntegrationTests.xcworkspace - - xcodebuild_options: -testPlan DatadogCrashReportingIntegrationTests - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/DatadogCrashReportingIntegration-tests.html" - run_smoke_tests: description: |- Uses supported dependency managers to fetch, install and link the SDK From a49e510c782296775ed08ff979bc0712ff161d44 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 18 Jun 2024 13:36:34 +0200 Subject: [PATCH 032/110] RUM-4079 Update PR template --- .github/PULL_REQUEST_TEMPLATE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5bed1f3ac..6c88142429 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,8 +12,6 @@ A brief description of implementation details of this PR. - [ ] Add CHANGELOG entry for user facing changes ### Custom CI job configuration (optional) -- [ ] Run unit tests for Core, RUM, Trace, Logs, CR and WVT - [ ] Run unit tests for Session Replay -- [ ] Run integration tests - [ ] Run smoke tests - [ ] Run tests for `tools/` From 7f66da30ae6d7ad194c2847d2e8a224d612c486d Mon Sep 17 00:00:00 2001 From: Nikita Ogorodnikov Date: Tue, 18 Jun 2024 10:39:50 +0200 Subject: [PATCH 033/110] Allow disabling app hang monitoring in ObjC API --- CHANGELOG.md | 3 +++ .../Tests/DatadogObjc/DDRUMConfigurationTests.swift | 8 +++++++- DatadogObjc/Sources/RUM/RUM+objc.swift | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 283ebe244d..eadb4d480d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [IMPROVEMENT] Allow disabling app hang monitoring in ObjC API. See [#1908][] + # 2.13.0 / 13-06-2024 - [IMPROVEMENT] Bump `IPHONEOS_DEPLOYMENT_TARGET` and `TVOS_DEPLOYMENT_TARGET` from 11 to 12. See [#1891][] @@ -684,6 +686,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1835]: https://github.com/DataDog/dd-sdk-ios/pull/1835 [#1886]: https://github.com/DataDog/dd-sdk-ios/pull/1886 [#1898]: https://github.com/DataDog/dd-sdk-ios/pull/1898 +[#1908]: https://github.com/DataDog/dd-sdk-ios/pull/1908 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift index 9cb9201af7..6468bbe483 100644 --- a/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift @@ -107,12 +107,18 @@ class DDRUMConfigurationTests: XCTestCase { } func testAppHangThreshold() { - let random: TimeInterval = .mockRandom() + let random: TimeInterval = .mockRandom(min: 0.01, max: .greatestFiniteMagnitude) objc.appHangThreshold = random XCTAssertEqual(objc.appHangThreshold, random) XCTAssertEqual(swift.appHangThreshold, random) } + func testAppHangThresholdDisable() { + objc.appHangThreshold = 0 + XCTAssertEqual(objc.appHangThreshold, 0) + XCTAssertEqual(swift.appHangThreshold, nil) + } + func testVitalsUpdateFrequency() { objc.vitalsUpdateFrequency = .frequent XCTAssertEqual(swift.vitalsUpdateFrequency, .frequent) diff --git a/DatadogObjc/Sources/RUM/RUM+objc.swift b/DatadogObjc/Sources/RUM/RUM+objc.swift index 6f2e8cd113..3410fe3b93 100644 --- a/DatadogObjc/Sources/RUM/RUM+objc.swift +++ b/DatadogObjc/Sources/RUM/RUM+objc.swift @@ -378,7 +378,7 @@ public class DDRUMConfiguration: NSObject { } @objc public var appHangThreshold: TimeInterval { - set { swiftConfig.appHangThreshold = newValue } + set { swiftConfig.appHangThreshold = newValue == 0 ? nil : newValue } get { swiftConfig.appHangThreshold ?? 0 } } From 8419d0bf9f28cb896d78e1ad0ab2c882179177fe Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Tue, 18 Jun 2024 16:00:40 +0200 Subject: [PATCH 034/110] RUM-4911 feat(watchdog-termination): setup trackWatchdogTermination configuration --- Datadog/Example/ExampleAppDelegate.swift | 4 +- .../Sources/CrashReportingFeature.swift | 7 ++- DatadogRUM/Sources/Feature/RUMFeature.swift | 46 ++++++++++++------- .../Instrumentation/RUMInstrumentation.swift | 2 +- .../WatchdogTerminationMonitor.swift | 14 +++--- DatadogRUM/Sources/RUMConfiguration.swift | 10 ++++ .../WatchdogTerminationMocks.swift | 4 +- .../WatchdogTerminationMonitorTests.swift | 4 +- 8 files changed, 57 insertions(+), 34 deletions(-) diff --git a/Datadog/Example/ExampleAppDelegate.swift b/Datadog/Example/ExampleAppDelegate.swift index 6113abc415..e471c90ab8 100644 --- a/Datadog/Example/ExampleAppDelegate.swift +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -77,8 +77,10 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { resourceAttributesProvider: { req, resp, data, err in print("⭐️ [Attributes Provider] data: \(String(describing: data))") return [:] - }), + } + ), trackBackgroundEvents: true, + trackWatchdogTermination: true, customEndpoint: Environment.readCustomRUMURL(), telemetrySampleRate: 100 ) diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift index 28865bc645..20848d3e0d 100644 --- a/DatadogCrashReporting/Sources/CrashReportingFeature.swift +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -59,8 +59,7 @@ internal final class CrashReportingFeature: DatadogFeature { self.plugin.readPendingCrashReport { [weak self] crashReport in guard let self = self, let availableCrashReport = crashReport else { DD.logger.debug("No pending Crash found") - // TODO: RUM-4911 enable after `WatchdogTerminationReporter` is implemented. - // self?.sender.send(launch: .init(didCrash: false)) + self?.sender.send(launch: .init(didCrash: false)) return false } @@ -69,12 +68,12 @@ internal final class CrashReportingFeature: DatadogFeature { guard let crashContext = availableCrashReport.context.flatMap({ self.decode(crashContextData: $0) }) else { // `CrashContext` is malformed and and cannot be read. Return `true` to let the crash reporter // purge this crash report as we are not able to process it respectively. + self.sender.send(launch: .init(didCrash: true)) return true } self.sender.send(report: availableCrashReport, with: crashContext) - // TODO: RUM-4911 enable after `WatchdogTerminationReporter` is implemented. - // self.sender.send(launch: .init(didCrash: true)) + self.sender.send(launch: .init(didCrash: true)) return true } } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index 69916c4d60..ed064e82c6 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -37,6 +37,27 @@ internal final class RUMFeature: DatadogRemoteFeature { let featureScope = core.scope(for: RUMFeature.self) let sessionEndedMetric = SessionEndedMetricController(telemetry: core.telemetry) + + var watchdogTermination: WatchdogTerminationMonitor? + var watchdogTerminationAppStateManager: WatchdogTerminationAppStateManager? + if configuration.trackWatchdogTermination { + let appStateManager = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: configuration.processID + ) + let monitor = WatchdogTerminationMonitor( + appStateManager: appStateManager, + checker: .init( + appStateManager: appStateManager, + deviceInfo: .init() + ), + feature: featureScope, + reporter: WatchdogTerminationReporter() + ) + watchdogTerminationAppStateManager = appStateManager + watchdogTermination = monitor + } + let dependencies = RUMScopeDependencies( featureScope: featureScope, rumApplicationID: configuration.applicationID, @@ -83,20 +104,6 @@ internal final class RUMFeature: DatadogRemoteFeature { dateProvider: configuration.dateProvider ) - let appStateManager = WatchdogTerminationAppStateManager( - featureScope: featureScope, - processId: configuration.processID - ) - let watchdogTermination = WatchdogTerminationMonitor( - appStateManager: appStateManager, - checker: .init( - appStateManager: appStateManager, - deviceInfo: .init() - ), - reporter: WatchdogTerminationReporter(), - telemetry: featureScope.telemetry - ) - self.instrumentation = RUMInstrumentation( featureScope: featureScope, uiKitRUMViewsPredicate: configuration.uiKitViewsPredicate, @@ -115,7 +122,7 @@ internal final class RUMFeature: DatadogRemoteFeature { eventsFilter: RUMViewEventsFilter(), telemetry: core.telemetry ) - self.messageReceiver = CombinedFeatureMessageReceiver( + var messageReceivers: [FeatureMessageReceiver] = [ TelemetryInterceptor(sessionEndedMetric: sessionEndedMetric), TelemetryReceiver( featureScope: featureScope, @@ -152,8 +159,13 @@ internal final class RUMFeature: DatadogRemoteFeature { eventsMapper: eventsMapper ), LaunchReportReceiver(featureScope: featureScope, watchdogTermination: watchdogTermination), - appStateManager - ) + ] + + if let watchdogTerminationAppStateManager = watchdogTerminationAppStateManager { + messageReceivers.append(watchdogTerminationAppStateManager) + } + + self.messageReceiver = CombinedFeatureMessageReceiver(messageReceivers) // Forward instrumentation calls to monitor: instrumentation.publish(to: monitor) diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 1eeb5de698..8b86fb09c6 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -53,7 +53,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { backtraceReporter: BacktraceReporting, fatalErrorContext: FatalErrorContextNotifying, processID: UUID, - watchdogTermination: WatchdogTerminationMonitor + watchdogTermination: WatchdogTerminationMonitor? ) { // Always create views handler (we can't know if it will be used by SwiftUI instrumentation) // and only swizzle `UIViewController` if UIKit instrumentation is configured: diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift index 1124308d3e..cd9bc8a08a 100644 --- a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift @@ -19,18 +19,18 @@ internal final class WatchdogTerminationMonitor { let checker: WatchdogTerminationChecker let appStateManager: WatchdogTerminationAppStateManager - let telemetry: Telemetry + let feature: FeatureScope let reporter: WatchdogTerminationReporting init( appStateManager: WatchdogTerminationAppStateManager, checker: WatchdogTerminationChecker, - reporter: WatchdogTerminationReporting, - telemetry: Telemetry + feature: FeatureScope, + reporter: WatchdogTerminationReporting ) { self.checker = checker self.appStateManager = appStateManager - self.telemetry = telemetry + self.feature = feature self.reporter = reporter } @@ -45,7 +45,7 @@ internal final class WatchdogTerminationMonitor { try appStateManager.start() } catch let error { DD.logger.error(ErrorMessages.failedToStartAppState, error: error) - telemetry.error(ErrorMessages.failedToStartAppState, error: error) + feature.telemetry.error(ErrorMessages.failedToStartAppState, error: error) } } @@ -63,7 +63,7 @@ internal final class WatchdogTerminationMonitor { } } catch let error { DD.logger.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) - telemetry.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) + feature.telemetry.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) } } @@ -73,7 +73,7 @@ internal final class WatchdogTerminationMonitor { try appStateManager.stop() } catch { DD.logger.error(ErrorMessages.failedToStopAppState, error: error) - telemetry.error(ErrorMessages.failedToStopAppState, error: error) + feature.telemetry.error(ErrorMessages.failedToStopAppState, error: error) } } } diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index ffff00b710..ed427f048c 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -111,6 +111,13 @@ extension RUM { /// Default: `false`. public var trackBackgroundEvents: Bool + /// Determines whether the SDK should track application termination by the watchdog. + /// + /// Read more about watchdog terminations at https://developer.apple.com/documentation/xcode/addressing-watchdog-terminations + /// + /// Default: `false`. + public var trackWatchdogTermination: Bool + /// Enables RUM long tasks tracking with the given threshold (in seconds). /// /// Any operation on the main thread that exceeds this threshold will be reported as a RUM long task. @@ -339,6 +346,7 @@ extension RUM.Configuration { /// - trackBackgroundEvents: Determines whether RUM events should be tracked when no view is active. Default: `false`. /// - longTaskThreshold: The threshold for RUM long tasks tracking (in seconds). Default: `0.1`. /// - appHangThreshold: The threshold for App Hangs monitoring (in seconds). Default: `nil`. + /// - trackWatchdogTermination: Determines whether the SDK should track application termination by the watchdog. Default: `false`. /// - vitalsUpdateFrequency: The preferred frequency for collecting RUM vitals. Default: `.average`. /// - viewEventMapper: Custom mapper for RUM view events. Default: `nil`. /// - resourceEventMapper: Custom mapper for RUM resource events. Default: `nil`. @@ -358,6 +366,7 @@ extension RUM.Configuration { trackBackgroundEvents: Bool = false, longTaskThreshold: TimeInterval? = 0.1, appHangThreshold: TimeInterval? = nil, + trackWatchdogTermination: Bool = false, vitalsUpdateFrequency: VitalsFrequency? = .average, viewEventMapper: RUM.ViewEventMapper? = nil, resourceEventMapper: RUM.ResourceEventMapper? = nil, @@ -386,6 +395,7 @@ extension RUM.Configuration { self.onSessionStart = onSessionStart self.customEndpoint = customEndpoint self.telemetrySampleRate = telemetrySampleRate + self.trackWatchdogTermination = trackWatchdogTermination } } diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift index 4c4e8a8345..2f7a440fce 100644 --- a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift @@ -90,8 +90,8 @@ extension WatchdogTerminationMonitor: RandomMockable { return .init( appStateManager: .mockRandom(), checker: .mockRandom(), - reporter: WatchdogTerminationReporter.mockRandom(), - telemetry: NOPTelemetry() + feature: FeatureScopeMock(), + reporter: WatchdogTerminationReporter.mockRandom() ) } } diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift index c968573e07..6b9d9c4fc1 100644 --- a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift @@ -91,8 +91,8 @@ final class WatchdogTerminationMonitorTests: XCTestCase { sut = WatchdogTerminationMonitor( appStateManager: appStateManager, checker: checker, - reporter: reporter, - telemetry: featureScope.telemetryMock + feature: FeatureScopeMock(), + reporter: reporter ) core = PassthroughCoreMock( From 3598ab54e2d8d88cb751fe1ce4f9a95c7fc0d73a Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Tue, 18 Jun 2024 16:30:58 +0200 Subject: [PATCH 035/110] RUM-4883 Address comment --- .../NodeRecorders/UITabBarRecorder.swift | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index 6d38659daf..3f3f3844fc 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -10,18 +10,34 @@ import UIKit internal final class UITabBarRecorder: NodeRecorder { let identifier = UUID() - private var currentlyProcessedTabbar: UITabBar? = nil - private lazy var subtreeViewRecorder: ViewTreeRecorder = { - ViewTreeRecorder( + func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { + guard let tabBar = view as? UITabBar else { + return nil + } + + let builder = UITabBarWireframesBuilder( + wireframeRect: inferOccupiedFrame(of: tabBar, in: context), + wireframeID: context.ids.nodeID(view: tabBar, nodeRecorder: self), + attributes: attributes, + color: inferColor(of: tabBar) + ) + + let node = Node(viewAttributes: attributes, wireframesBuilder: builder) + let subtreeRecordingResults = recordSubtree(of: tabBar, in: context) + let allNodes = [node] + subtreeRecordingResults.nodes + let resources = subtreeRecordingResults.resources + + return SpecificElement(subtreeStrategy: .ignore, nodes: allNodes, resources: resources) + } + + private func recordSubtree(of tabBar: UITabBar, in context: ViewTreeRecordingContext) -> RecordingResult { + let subtreeViewRecorder = ViewTreeRecorder( nodeRecorders: [ UIImageViewRecorder( tintColorProvider: { imageView in guard let imageViewImage = imageView.image else { return nil } - guard let tabBar = self.currentlyProcessedTabbar else { - return imageView.tintColor - } // Retrieve the tab bar item containing the imageView. let currentItemInSelectedState = tabBar.items?.first { @@ -49,29 +65,8 @@ internal final class UITabBarRecorder: NodeRecorder { UIViewRecorder() ] ) - }() - func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { - guard let tabBar = view as? UITabBar else { - return nil - } - - currentlyProcessedTabbar = tabBar - - let builder = UITabBarWireframesBuilder( - wireframeRect: inferOccupiedFrame(of: tabBar, in: context), - wireframeID: context.ids.nodeID(view: tabBar, nodeRecorder: self), - attributes: attributes, - color: inferColor(of: tabBar) - ) - - let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - - let subtreeRecordingResults = subtreeViewRecorder.record(tabBar, in: context) - let allNodes = [node] + subtreeRecordingResults.nodes - let resources = subtreeRecordingResults.resources - - return SpecificElement(subtreeStrategy: .ignore, nodes: allNodes, resources: resources) + return subtreeViewRecorder.record(tabBar, in: context) } private func inferOccupiedFrame(of tabBar: UITabBar, in context: ViewTreeRecordingContext) -> CGRect { From f832ba11a86dce47b88ffe718a5d7a3535ea029f Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 09:06:46 +0200 Subject: [PATCH 036/110] RUM-4079 Adjust condition for running key CI jobs --- .gitlab-ci.yml | 23 +++++++++++++---------- Makefile | 5 ----- tools/env_check.sh | 2 +- tools/test.sh | 5 +++-- tools/ui-test.sh | 6 +++--- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0575e1b779..4ff2973e30 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,21 +4,21 @@ stages: - test - ui-test -# Workflow configuration for all jobs in this file -# Ref.: https://docs.gitlab.com/ee/ci/yaml/workflow.html -workflow: - rules: - - if: $CI_PIPELINE_SOURCE == 'merge_request_event' - - if: $CI_COMMIT_TAG - - if: $CI_COMMIT_BRANCH == 'develop' || $CI_COMMIT_BRANCH == 'master' - -# Tags applied to all jobs -# Ref.: https://docs.gitlab.com/ee/ci/yaml/ +# Default configuration for all jobs: default: tags: - macos:sonoma - specific:true +# Utility job that conditions when to add inheriting jobs to the pipeline: +.run:on-sdk-changes: + rules: + - changes: + - "Datadog*/**/*" + - "IntegrationTests/**/*" + - "TestUtilities/**/*" + - "*.yml" + ENV check: stage: pre script: @@ -35,6 +35,7 @@ Lint: Unit Tests (iOS): stage: test + extends: .run:on-sdk-changes variables: OS: "latest" PLATFORM: "iOS Simulator" @@ -46,6 +47,7 @@ Unit Tests (iOS): Unit Tests (tvOS): stage: test + extends: .run:on-sdk-changes variables: OS: "latest" PLATFORM: "tvOS Simulator" @@ -57,6 +59,7 @@ Unit Tests (tvOS): UI Tests: stage: ui-test + extends: .run:on-sdk-changes variables: OS: "latest" PLATFORM: "iOS Simulator" diff --git a/Makefile b/Makefile index 7fd1ce9538..a9adf1edb6 100644 --- a/Makefile +++ b/Makefile @@ -170,11 +170,6 @@ xcodeproj-session-replay: @cd DatadogSessionReplay/ && swift package generate-xcodeproj @echo "OK 👌" -prepare-integration-tests: - @echo "⚙️ Prepare Integration Tests ..." - @cd IntegrationTests/ && pod install - @echo "OK 👌" - open-sr-snapshot-tests: @echo "⚙️ Opening SRSnapshotTests with DD_TEST_UTILITIES_ENABLED ..." @pgrep -q Xcode && killall Xcode && echo "- Xcode killed" || echo "- Xcode not running" diff --git a/tools/env_check.sh b/tools/env_check.sh index 8c362680a9..f8383382c9 100755 --- a/tools/env_check.sh +++ b/tools/env_check.sh @@ -67,4 +67,4 @@ xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdest if command -v brew >/dev/null 2>&1; then echo_succ "brew:" brew -v -fi \ No newline at end of file +fi diff --git a/tools/test.sh b/tools/test.sh index 3732a8fae2..a8188703d3 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -28,5 +28,6 @@ SCHEME=$scheme set -x set -eo pipefail -xcodebuild -version -xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" test | xcbeautify +# xcodebuild -version +# xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" test | xcbeautify +echo "xcodebuild -workspace '$WORKSPACE' -destination '$DESTINATION' -scheme '$SCHEME' test | xcbeautify" diff --git a/tools/ui-test.sh b/tools/ui-test.sh index dfdc2b877c..f7dfd31829 100755 --- a/tools/ui-test.sh +++ b/tools/ui-test.sh @@ -48,9 +48,9 @@ TEST_PLAN="$test_plan" ./tools/config/generate-http-server-mock-config.sh -xcodebuild -version - disable_apple_crash_reporter trap enable_apple_crash_reporter EXIT INT -xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" -testPlan "$TEST_PLAN" test | xcbeautify +# xcodebuild -version +# xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" -testPlan "$TEST_PLAN" test | xcbeautify +echo "xcodebuild -workspace '$WORKSPACE' -destination '$DESTINATION' -scheme '$SCHEME' -testPlan '$TEST_PLAN' test | xcbeautify" From 90e4b77cc0dccd1698adb9ecb4bfcef6441443bb Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 09:10:55 +0200 Subject: [PATCH 037/110] RUM-4079 Update default `make` command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a9adf1edb6..2cd633653e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: dependencies templates +all: env-check repo-setup templates .PHONY: env-check repo-setup clean templates \ lint license-check \ test test-ios test-ios-all test-tvos test-tvos-all \ From 410d9a2a172257e59bc0d08708486bae96628cdd Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 09:34:22 +0200 Subject: [PATCH 038/110] RUM-4079 Adjust conditions for running key CI jobs --- .gitlab-ci.yml | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4ff2973e30..b7b8532917 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,16 +4,27 @@ stages: - test - ui-test -# Default configuration for all jobs: +variables: + MAIN_BRANCH: "master" + DEVELOP_BRANCH: "develop" + default: tags: - macos:sonoma - specific:true -# Utility job that conditions when to add inheriting jobs to the pipeline: +# ┌───────────────┐ +# │ Utility jobs: │ +# └───────────────┘ + +.run:always-on-default-branches: + rules: + - if: '$CI_COMMIT_BRANCH == $MAIN_BRANCH || $CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + when: always + .run:on-sdk-changes: rules: - - changes: + - changes: # on changes to these files for other branches - "Datadog*/**/*" - "IntegrationTests/**/*" - "TestUtilities/**/*" @@ -25,8 +36,15 @@ ENV check: - ./tools/runner-setup.sh --ios # temporary, waiting for AMI - make env-check +# ┌──────────────────────────┐ +# │ SDK changes integration: │ +# └──────────────────────────┘ + Lint: stage: lint + rules: + - !reference [.run:always-on-default-branches, rules] + - !reference [.run:on-sdk-changes, rules] script: - ./tools/runner-setup.sh --ios # temporary, waiting for AMI - make clean repo-setup ENV=ci @@ -35,7 +53,9 @@ Lint: Unit Tests (iOS): stage: test - extends: .run:on-sdk-changes + rules: + - !reference [.run:always-on-default-branches, rules] + - !reference [.run:on-sdk-changes, rules] variables: OS: "latest" PLATFORM: "iOS Simulator" @@ -47,7 +67,9 @@ Unit Tests (iOS): Unit Tests (tvOS): stage: test - extends: .run:on-sdk-changes + rules: + - !reference [.run:always-on-default-branches, rules] + - !reference [.run:on-sdk-changes, rules] variables: OS: "latest" PLATFORM: "tvOS Simulator" @@ -59,7 +81,9 @@ Unit Tests (tvOS): UI Tests: stage: ui-test - extends: .run:on-sdk-changes + rules: + - !reference [.run:always-on-default-branches, rules] + - !reference [.run:on-sdk-changes, rules] variables: OS: "latest" PLATFORM: "iOS Simulator" From 2f2901aa43926c327a75b7d23b5520ee49ff30f6 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 10:04:58 +0200 Subject: [PATCH 039/110] RUM-4079 Cleanup --- .gitlab-ci.yml | 2 +- tools/test.sh | 5 ++--- tools/ui-test.sh | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b7b8532917..17cbcb486a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,7 +24,7 @@ default: .run:on-sdk-changes: rules: - - changes: # on changes to these files for other branches + - changes: - "Datadog*/**/*" - "IntegrationTests/**/*" - "TestUtilities/**/*" diff --git a/tools/test.sh b/tools/test.sh index a8188703d3..3732a8fae2 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -28,6 +28,5 @@ SCHEME=$scheme set -x set -eo pipefail -# xcodebuild -version -# xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" test | xcbeautify -echo "xcodebuild -workspace '$WORKSPACE' -destination '$DESTINATION' -scheme '$SCHEME' test | xcbeautify" +xcodebuild -version +xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" test | xcbeautify diff --git a/tools/ui-test.sh b/tools/ui-test.sh index f7dfd31829..6e1870965d 100755 --- a/tools/ui-test.sh +++ b/tools/ui-test.sh @@ -51,6 +51,5 @@ TEST_PLAN="$test_plan" disable_apple_crash_reporter trap enable_apple_crash_reporter EXIT INT -# xcodebuild -version -# xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" -testPlan "$TEST_PLAN" test | xcbeautify -echo "xcodebuild -workspace '$WORKSPACE' -destination '$DESTINATION' -scheme '$SCHEME' -testPlan '$TEST_PLAN' test | xcbeautify" +xcodebuild -version +xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" -testPlan "$TEST_PLAN" test | xcbeautify From 694a1359faafd0fc1c95d1cba19077b22b5795bf Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 11:25:11 +0200 Subject: [PATCH 040/110] RUM-4079 Update `LICENSE-3rdparty.csv` --- LICENSE-3rdparty.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index fdefe83c9d..fe0e1257da 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -9,3 +9,4 @@ import (tools),https://github.com/apple/swift-argument-parser,Apache-2.0,(c) 202 import (tools),https://github.com/krzysztofzablocki/Difference.git,MIT,Copyright (c) 2017 Krzysztof Zablocki import (tools),https://github.com/pointfreeco/swift-snapshot-testing,MIT,Copyright (c) 2019 Point-Free, Inc. import (tools),https://github.com/ncreated/Framing,MIT,Copyright (c) 2016 Maciek Grzybowski +import (tools),https://github.com/ncreated/argparse-zsh,MIT,Copyright (c) 2023 Yaacov Zamir and 2024 Maciek Grzybowski (fork) From d7ad33706c10f8c51b7a68b19d81fb29eeb9fed9 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Wed, 19 Jun 2024 11:28:41 +0200 Subject: [PATCH 041/110] RUM-4911 feat(watchdog-termination): ability to find most recently updated file in a dir --- .../Core/Storage/Files/Directory.swift | 59 ++++++- .../Sources/Core/Storage/Files/File.swift | 17 +- .../Persistence/Files/DirectoryTests.swift | 150 ++++++++++++++++++ .../Core/Persistence/Files/FileTests.swift | 22 +++ 4 files changed, 246 insertions(+), 2 deletions(-) diff --git a/DatadogCore/Sources/Core/Storage/Files/Directory.swift b/DatadogCore/Sources/Core/Storage/Files/Directory.swift index 76fdfd239b..1e8712adc3 100644 --- a/DatadogCore/Sources/Core/Storage/Files/Directory.swift +++ b/DatadogCore/Sources/Core/Storage/Files/Directory.swift @@ -7,8 +7,15 @@ import Foundation import DatadogInternal +/// Provides interfaces for accessing common properties and operations for a directory. +internal protocol DirectoryProtocol: FileProtocol { + /// Returns list of subdirectories in the directory. + /// - Returns: list of subdirectories. + func subdirectories() throws -> [Directory] +} + /// An abstraction over file system directory where SDK stores its files. -internal struct Directory { +internal struct Directory: DirectoryProtocol { let url: URL /// Creates subdirectory with given path under system caches directory. @@ -21,6 +28,56 @@ internal struct Directory { self.url = url } + func modifiedAt() throws -> Date? { + try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date + } + + /// Returns list of subdirectories using system APIs. + /// - Returns: list of subdirectories. + func subdirectories() throws -> [Directory] { + try FileManager.default + .contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey, .canonicalPathKey]) + .filter { url in + var isDirectory = ObjCBool(false) + FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return isDirectory.boolValue + } + .map { url in Directory(url: url) } + } + + /// Recursively goes through subdirectories and finds the most recent modified file before given date. + /// This includes files in subdirectories, files in this directory and itself. + /// - Parameter before: The date to compare the last modification date of files. + /// - Returns: The latest modified file or `nil` if no files were modified before given date. + func mostRecentModifiedFile(before: Date) throws -> FileProtocol? { + let mostRecentModifiedInSubdirectories = try subdirectories() + .compactMap { directory in + try directory.mostRecentModifiedFile(before: before) + } + .max { file1, file2 in + guard let modifiedAt1 = try file1.modifiedAt(), let modifiedAt2 = try file2.modifiedAt() else { + return false + } + return modifiedAt1 < modifiedAt2 + } + + let files = try self.files() + + return try ([self, mostRecentModifiedInSubdirectories].compactMap { $0 } + files) + .filter { + guard let modifiedAt = try $0.modifiedAt() else { + return false + } + return modifiedAt < before + } + .max { file1, file2 in + guard let modifiedAt1 = try file1.modifiedAt(), let modifiedAt2 = try file2.modifiedAt() else { + return false + } + return modifiedAt1 < modifiedAt2 + } + } + /// Creates subdirectory with given path by creating intermediate directories if needed. /// If directory already exists at given `path` it will be used, without being altered. func createSubdirectory(path: String) throws -> Directory { diff --git a/DatadogCore/Sources/Core/Storage/Files/File.swift b/DatadogCore/Sources/Core/Storage/Files/File.swift index c3770674c6..e3498d498e 100644 --- a/DatadogCore/Sources/Core/Storage/Files/File.swift +++ b/DatadogCore/Sources/Core/Storage/Files/File.swift @@ -7,6 +7,17 @@ import Foundation import DatadogInternal +/// Provides interfaces for accessing common properties and operations for a file. +internal protocol FileProtocol { + /// URL of the file on the disk. + var url: URL { get } + + /// Returns the date when the file was last modified. Returns `nil` if the file does not exist. + /// If the file is created and never modified, the creation date is returned. + /// - Returns: The date when the file was last modified. + func modifiedAt() throws -> Date? +} + /// Provides convenient interface for reading metadata and appending data to the file. internal protocol WritableFile { /// Name of this file. @@ -37,7 +48,7 @@ private enum FileError: Error { /// An immutable `struct` designed to provide optimized and thread safe interface for file manipulation. /// It doesn't own the file, which means the file presence is not guaranteed - the file can be deleted by OS at any time (e.g. due to memory pressure). -internal struct File: WritableFile, ReadableFile { +internal struct File: WritableFile, ReadableFile, FileProtocol { let url: URL let name: String @@ -46,6 +57,10 @@ internal struct File: WritableFile, ReadableFile { self.name = url.lastPathComponent } + func modifiedAt() throws -> Date? { + try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date + } + /// Appends given data at the end of this file. func append(data: Data) throws { let fileHandle = try FileHandle(forWritingTo: url) diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift index 31f7f4e843..3d83cd5340 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift @@ -171,6 +171,156 @@ class DirectoryTests: XCTestCase { XCTAssertNoThrow(try destinationDirectory.file(named: "f3")) } + func testModifiedAt() throws { + // when directory is created + let before = Date.timeIntervalSinceReferenceDate - 1 + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + let creationDate = try directory.modifiedAt() + let after = Date.timeIntervalSinceReferenceDate + 1 + + XCTAssertNotNil(creationDate) + XCTAssertGreaterThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, before) + XCTAssertLessThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, after) + + // when directory is updated + let beforeModification = Date.timeIntervalSinceReferenceDate + _ = try directory.createFile(named: "file") + let modificationDate = try directory.modifiedAt() + let afterModification = Date.timeIntervalSinceReferenceDate + + XCTAssertNotNil(modificationDate) + XCTAssertGreaterThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, beforeModification) + XCTAssertLessThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, afterModification) + } + + func testLatestModifiedFile_whenDirectoryEmpty_returnsSelf() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(directory.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsFiles_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let file1 = try directory.createFile(named: "file1") + let file2 = try directory.createFile(named: "file2") + let file3 = try directory.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file3.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsSubdirectories_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let subdirectory1 = try directory.createSubdirectory(path: "subdirectory1") + let subdirectory2 = try directory.createSubdirectory(path: "subdirectory2") + let subdirectory3 = try directory.createSubdirectory(path: "subdirectory3") + + let file1 = try subdirectory1.createFile(named: "file1") + let file2 = try subdirectory2.createFile(named: "file2") + let file3 = try subdirectory3.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file3.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsFilesAndSubdirectories_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let subdirectory1 = try directory.createSubdirectory(path: "subdirectory1") + let subdirectory2 = try directory.createSubdirectory(path: "subdirectory2") + let subdirectory3 = try directory.createSubdirectory(path: "subdirectory3") + + let file1 = try subdirectory1.createFile(named: "file1") + let file2 = try subdirectory2.createFile(named: "file2") + let file3 = try subdirectory3.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file3.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsFilesAndFileIsDeleted_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let file1 = try directory.createFile(named: "file1") + let file2 = try directory.createFile(named: "file2") + let file3 = try directory.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + try file2.delete() + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(directory.url, modifiedFile?.url) + } + + func testLatestModifiedFile_givenBeforeDate_returnsLatestModifiedFileBeforeGivenDate() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let file1 = try directory.createFile(named: "file1") + let file2 = try directory.createFile(named: "file2") + let file3 = try directory.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + let beforeDate = Date() + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: beforeDate) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file2.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsSubdirectoriesAndFiles_givenBeforeDate_returnsLatestModifiedFileBeforeGivenDate() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let subdirectory1 = try directory.createSubdirectory(path: "subdirectory1") + let subdirectory2 = try directory.createSubdirectory(path: "subdirectory2") + let subdirectory3 = try directory.createSubdirectory(path: "subdirectory3") + + let file1 = try subdirectory1.createFile(named: "file1") + let file2 = try subdirectory2.createFile(named: "file2") + let file3 = try subdirectory3.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + let beforeDate = Date() + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: beforeDate) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file2.url, modifiedFile?.url) + } + // MARK: - Helpers private func uniqueSubdirectoryName() -> String { diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift index e4a1ce4efd..c97b1d10b5 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift @@ -88,4 +88,26 @@ class FileTests: XCTestCase { XCTAssertEqual((error as NSError).localizedDescription, "The file “file” doesn’t exist.") } } + + func testModifiedAt() throws { + // when file is created + let before = Date.timeIntervalSinceReferenceDate + let file = try directory.createFile(named: "file") + let creationDate = try file.modifiedAt() + let after = Date.timeIntervalSinceReferenceDate + + XCTAssertNotNil(creationDate) + XCTAssertGreaterThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, before) + XCTAssertLessThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, after) + + // when file is modified + let beforeModification = Date.timeIntervalSinceReferenceDate + try file.append(data: .mock(ofSize: 5)) + let modificationDate = try file.modifiedAt() + let afterModification = Date.timeIntervalSinceReferenceDate + + XCTAssertNotNil(modificationDate) + XCTAssertGreaterThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, beforeModification) + XCTAssertLessThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, afterModification) + } } From 053f19aadc5e33faaee1514a1a48dfbf9764e873 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 16:18:25 +0200 Subject: [PATCH 042/110] RUM-4079 CR feedback - fix conditional trigger by using `changes:path:compare_to:` - `set -e` in all scripts - add `--os` to `runner-setup.sh` - use `xctrace list devices` to list available simulators --- .gitlab-ci.yml | 48 ++++++++++++++++++---------------- tools/clean.sh | 1 + tools/env_check.sh | 10 ++++--- tools/repo-setup/repo-setup.sh | 3 +-- tools/runner-setup.sh | 25 +++++++++--------- tools/test.sh | 2 +- tools/ui-test.sh | 2 +- 7 files changed, 48 insertions(+), 43 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 17cbcb486a..c6e380ad1f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,23 +17,26 @@ default: # │ Utility jobs: │ # └───────────────┘ -.run:always-on-default-branches: +# Trigger jobs on 'develop' and 'master' branches +.run:when-develop-or-master: rules: - - if: '$CI_COMMIT_BRANCH == $MAIN_BRANCH || $CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH || $CI_COMMIT_BRANCH == $MAIN_BRANCH' when: always -.run:on-sdk-changes: +# Trigger jobs on SDK code changes, comparing against 'develop' branch +.run:if-sdk-modified: rules: - changes: - - "Datadog*/**/*" - - "IntegrationTests/**/*" - - "TestUtilities/**/*" - - "*.yml" + paths: + - "Datadog*/**/*" + - "IntegrationTests/**/*" + - "TestUtilities/**/*" + - "*" # match any file in the root directory + compare_to: 'develop' # cannot use variable due to: https://gitlab.com/gitlab-org/gitlab/-/issues/369916 ENV check: stage: pre script: - - ./tools/runner-setup.sh --ios # temporary, waiting for AMI - make env-check # ┌──────────────────────────┐ @@ -43,10 +46,9 @@ ENV check: Lint: stage: lint rules: - - !reference [.run:always-on-default-branches, rules] - - !reference [.run:on-sdk-changes, rules] + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] script: - - ./tools/runner-setup.sh --ios # temporary, waiting for AMI - make clean repo-setup ENV=ci - make lint license-check - make rum-models-verify sr-models-verify @@ -54,38 +56,38 @@ Lint: Unit Tests (iOS): stage: test rules: - - !reference [.run:always-on-default-branches, rules] - - !reference [.run:on-sdk-changes, rules] + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] variables: - OS: "latest" + OS: "17.4" PLATFORM: "iOS Simulator" DEVICE: "iPhone 15 Pro" script: - - ./tools/runner-setup.sh --ios # temporary, waiting for AMI + - ./tools/runner-setup.sh --ios --os "$OS" # temporary, waiting for AMI - make clean repo-setup ENV=ci - make test-ios-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" Unit Tests (tvOS): stage: test rules: - - !reference [.run:always-on-default-branches, rules] - - !reference [.run:on-sdk-changes, rules] + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] variables: - OS: "latest" + OS: "17.4" PLATFORM: "tvOS Simulator" DEVICE: "Apple TV" script: - - ./tools/runner-setup.sh --tvos # temporary, waiting for AMI + - ./tools/runner-setup.sh --tvos --os "$OS" # temporary, waiting for AMI - make clean repo-setup ENV=ci - make test-tvos-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" UI Tests: stage: ui-test rules: - - !reference [.run:always-on-default-branches, rules] - - !reference [.run:on-sdk-changes, rules] + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] variables: - OS: "latest" + OS: "17.4" PLATFORM: "iOS Simulator" DEVICE: "iPhone 15 Pro" parallel: @@ -96,6 +98,6 @@ UI Tests: - CrashReporting - NetworkInstrumentation script: - - ./tools/runner-setup.sh --ios # temporary, waiting for AMI + - ./tools/runner-setup.sh --ios --os "$OS" # temporary, waiting for AMI - make clean repo-setup ENV=ci - make ui-test TEST_PLAN="$TEST_PLAN" OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" diff --git a/tools/clean.sh b/tools/clean.sh index a75229c5d4..a533d31d27 100755 --- a/tools/clean.sh +++ b/tools/clean.sh @@ -3,6 +3,7 @@ # Usage: # $ ./tools/clean.sh +set -e source ./tools/utils/echo_color.sh echo_warn "Cleaning" "~/Library/Developer/Xcode/DerivedData/" diff --git a/tools/env_check.sh b/tools/env_check.sh index f8383382c9..a4532d54d5 100755 --- a/tools/env_check.sh +++ b/tools/env_check.sh @@ -4,6 +4,7 @@ # ./tools/env_check.sh # Prints environment information and checks if required tools are installed. +set -e source ./tools/utils/echo_color.sh check_if_installed() { @@ -57,14 +58,15 @@ check_if_installed python3 python3 -V echo "" -echo_succ "Installed iOS runtimes:" -xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore iOS" -showdestinations -quiet | grep platform +echo_succ "Available iOS Simulators:" +xctrace list devices | grep "iPhone.*Simulator" echo "" -echo_succ "Installed tvOS runtimes:" -xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdestinations -quiet | grep platform +echo_succ "Available tvOS Simulators:" +xctrace list devices | grep "Apple TV.*Simulator" if command -v brew >/dev/null 2>&1; then + echo "" echo_succ "brew:" brew -v fi diff --git a/tools/repo-setup/repo-setup.sh b/tools/repo-setup/repo-setup.sh index 009b6fb4e6..ff15233a2d 100755 --- a/tools/repo-setup/repo-setup.sh +++ b/tools/repo-setup/repo-setup.sh @@ -7,6 +7,7 @@ # Options: # --env: Specifies the environment for preparation. Use 'dev' for local development and 'ci' for CI. +set -eo pipefail source ./tools/utils/argparse.sh source ./tools/utils/echo_color.sh @@ -24,8 +25,6 @@ if [[ "$env" != "$ENV_CI" && "$env" != "$ENV_DEV" ]]; then exit 1 fi -set -eo pipefail - # Materialize CI xcconfig: cp -vi "./tools/repo-setup/Base.ci.xcconfig.src" ./xcconfigs/Base.ci.local.xcconfig diff --git a/tools/runner-setup.sh b/tools/runner-setup.sh index 284561fbf9..61bafb150f 100755 --- a/tools/runner-setup.sh +++ b/tools/runner-setup.sh @@ -7,37 +7,38 @@ # Options: # --ios: Flag that prepares the runner instance for iOS testing. Disabled by default. # --tvos: Flag that prepares the runner instance for tvOS testing. Disabled by default. +# --os: Sets the expected OS version for installed simulators. Default: '17.4'. +set -eo pipefail source ./tools/utils/echo_color.sh source ./tools/utils/argparse.sh set_description "This script is for TEMPORARY. It supplements missing components on the runner. It will be removed once all configurations are integrated into the AMI." define_arg "ios" "false" "Flag that prepares the runner instance for iOS testing. Disabled by default." "store_true" define_arg "tvos" "false" "Flag that prepares the runner instance for tvOS testing. Disabled by default." "store_true" +define_arg "os" "17.4" "Sets the expected OS version for installed simulators. Default: '17.4'." "string" "false" check_for_help "$@" parse_args "$@" -set -eo pipefail - xcodebuild -version if [ "$ios" = "true" ]; then - echo "Check current runner for any iPhone Simulator runtime supporting OS 17.x." - if ! xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore iOS" -showdestinations -quiet | grep -q 'platform:iOS Simulator.*OS:17'; then - echo_warn "Found no iOS Simulator runtime supporting OS 17.x. Installing..." - # xcodebuild -downloadPlatform iOS -quiet | xcbeautify + echo "Check current runner for any iPhone Simulator runtime supporting OS '$os'." + if ! xctrace list devices | grep "iPhone.*Simulator ($os)"; then + echo_warn "Found no iOS Simulator runtime supporting OS '$os'. Installing..." + xcodebuild -downloadPlatform iOS -quiet | xcbeautify else - echo_succ "Found some iOS Simulator runtime supporting OS 17.x. Skipping..." + echo_succ "Found some iOS Simulator runtime supporting OS '$os'. Skipping..." fi fi if [ "$tvos" = "true" ]; then - echo "Check current runner for any tvOS Simulator runtime supporting OS 17.x." - if ! xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdestinations -quiet | grep -q 'platform:tvOS Simulator.*OS:17'; then - echo_warn "Found no tvOS Simulator runtime supporting OS 17.x. Installing..." - # xcodebuild -downloadPlatform tvOS -quiet | xcbeautify + echo "Check current runner for any tvOS Simulator runtime supporting OS '$os'." + if ! xctrace list devices | grep "Apple TV.*Simulator ($os)"; then + echo_warn "Found no tvOS Simulator runtime supporting OS '$os'. Installing..." + xcodebuild -downloadPlatform tvOS -quiet | xcbeautify else - echo_succ "Found some tvOS Simulator runtime supporting OS 17.x. Skipping..." + echo_succ "Found some tvOS Simulator runtime supporting OS '$os'. Skipping..." fi fi diff --git a/tools/test.sh b/tools/test.sh index 3732a8fae2..3d9aa917bf 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -10,6 +10,7 @@ # --platform: Defines the type of simulator platform for the tests, e.g. 'iOS Simulator' # --os: Sets the operating system version for the tests, e.g. '17.5' +set -eo pipefail source ./tools/utils/argparse.sh set_description "Executes unit tests for a specified --scheme, using the provided --os, --platform, and --device." @@ -26,7 +27,6 @@ DESTINATION="platform=$platform,name=$device,OS=$os" SCHEME=$scheme set -x -set -eo pipefail xcodebuild -version xcodebuild -workspace "$WORKSPACE" -destination "$DESTINATION" -scheme "$SCHEME" test | xcbeautify diff --git a/tools/ui-test.sh b/tools/ui-test.sh index 6e1870965d..b2abbed603 100755 --- a/tools/ui-test.sh +++ b/tools/ui-test.sh @@ -10,6 +10,7 @@ # --os: Sets the operating system version for the tests, e.g. '17.5' # --test-plan: Identifies the test plan to run +set -eo pipefail source ./tools/utils/echo_color.sh source ./tools/utils/argparse.sh @@ -35,7 +36,6 @@ enable_apple_crash_reporter() { } set -x -set -eo pipefail DIR=$(pwd) cd IntegrationTests/ && bundle exec pod install From 33586e91b084fd55b74c2f1ddda2f7596c774c5e Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 19 Jun 2024 16:28:24 +0200 Subject: [PATCH 043/110] RUM-4079 Handle errors in `make clean` --- tools/clean.sh | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tools/clean.sh b/tools/clean.sh index a533d31d27..0bcba2b407 100755 --- a/tools/clean.sh +++ b/tools/clean.sh @@ -6,16 +6,21 @@ set -e source ./tools/utils/echo_color.sh -echo_warn "Cleaning" "~/Library/Developer/Xcode/DerivedData/" -rm -rf ~/Library/Developer/Xcode/DerivedData/* +clean_dir() { + local dir="$1" + if [ -d "$dir" ] && [ "$(ls -A "$dir")" ]; then + echo_warn "Removing contents:" "'$dir'" + rm -rf "$dir"/* + else + echo_warn "Nothing to clean:" "'$dir' does not exist or it is already empty" + fi +} -echo_warn "Cleaning" "./Carthage/" -rm -rf ./Carthage/Build/* -rm -rf ./Carthage/Checkouts/* +clean_dir ~/Library/Developer/Xcode/DerivedData +clean_dir ./Carthage/Build +clean_dir ./Carthage/Checkouts +clean_dir ./IntegrationTests/Pods -echo_warn "Cleaning" "./IntegrationTests/Pods/" -rm -rf ./IntegrationTests/Pods/* - -echo_warn "Cleaning" "local xcconfigs" +echo_warn "Cleaning local xcconfigs" rm -vf ./xcconfigs/Base.ci.local.xcconfig rm -vf ./xcconfigs/Base.dev.local.xcconfig From 4fa8c61edacd2da159db8392a585e0e4003acce5 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 20 Jun 2024 09:24:09 +0200 Subject: [PATCH 044/110] RUM-4911 feat(watchdog-termination): fix objc API and config name --- Datadog/Example/ExampleAppDelegate.swift | 2 +- DatadogObjc/Sources/RUM/RUM+objc.swift | 5 +++++ DatadogRUM/Sources/Feature/RUMFeature.swift | 2 +- DatadogRUM/Sources/RUMConfiguration.swift | 8 ++++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Datadog/Example/ExampleAppDelegate.swift b/Datadog/Example/ExampleAppDelegate.swift index e471c90ab8..6046fd33a1 100644 --- a/Datadog/Example/ExampleAppDelegate.swift +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -80,7 +80,7 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { } ), trackBackgroundEvents: true, - trackWatchdogTermination: true, + trackWatchdogTerminations: true, customEndpoint: Environment.readCustomRUMURL(), telemetrySampleRate: 100 ) diff --git a/DatadogObjc/Sources/RUM/RUM+objc.swift b/DatadogObjc/Sources/RUM/RUM+objc.swift index 6f2e8cd113..14da000084 100644 --- a/DatadogObjc/Sources/RUM/RUM+objc.swift +++ b/DatadogObjc/Sources/RUM/RUM+objc.swift @@ -372,6 +372,11 @@ public class DDRUMConfiguration: NSObject { get { swiftConfig.trackBackgroundEvents } } + @objc public var trackWatchdogTerminations: Bool { + set { swiftConfig.trackWatchdogTerminations = newValue } + get { swiftConfig.trackWatchdogTerminations } + } + @objc public var longTaskThreshold: TimeInterval { set { swiftConfig.longTaskThreshold = newValue } get { swiftConfig.longTaskThreshold ?? 0 } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index ed064e82c6..79a91d44f9 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -40,7 +40,7 @@ internal final class RUMFeature: DatadogRemoteFeature { var watchdogTermination: WatchdogTerminationMonitor? var watchdogTerminationAppStateManager: WatchdogTerminationAppStateManager? - if configuration.trackWatchdogTermination { + if configuration.trackWatchdogTerminations { let appStateManager = WatchdogTerminationAppStateManager( featureScope: featureScope, processId: configuration.processID diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index ed427f048c..50f132ed73 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -116,7 +116,7 @@ extension RUM { /// Read more about watchdog terminations at https://developer.apple.com/documentation/xcode/addressing-watchdog-terminations /// /// Default: `false`. - public var trackWatchdogTermination: Bool + public var trackWatchdogTerminations: Bool /// Enables RUM long tasks tracking with the given threshold (in seconds). /// @@ -346,7 +346,7 @@ extension RUM.Configuration { /// - trackBackgroundEvents: Determines whether RUM events should be tracked when no view is active. Default: `false`. /// - longTaskThreshold: The threshold for RUM long tasks tracking (in seconds). Default: `0.1`. /// - appHangThreshold: The threshold for App Hangs monitoring (in seconds). Default: `nil`. - /// - trackWatchdogTermination: Determines whether the SDK should track application termination by the watchdog. Default: `false`. + /// - trackWatchdogTerminations: Determines whether the SDK should track application termination by the watchdog. Default: `false`. /// - vitalsUpdateFrequency: The preferred frequency for collecting RUM vitals. Default: `.average`. /// - viewEventMapper: Custom mapper for RUM view events. Default: `nil`. /// - resourceEventMapper: Custom mapper for RUM resource events. Default: `nil`. @@ -366,7 +366,7 @@ extension RUM.Configuration { trackBackgroundEvents: Bool = false, longTaskThreshold: TimeInterval? = 0.1, appHangThreshold: TimeInterval? = nil, - trackWatchdogTermination: Bool = false, + trackWatchdogTerminations: Bool = false, vitalsUpdateFrequency: VitalsFrequency? = .average, viewEventMapper: RUM.ViewEventMapper? = nil, resourceEventMapper: RUM.ResourceEventMapper? = nil, @@ -395,7 +395,7 @@ extension RUM.Configuration { self.onSessionStart = onSessionStart self.customEndpoint = customEndpoint self.telemetrySampleRate = telemetrySampleRate - self.trackWatchdogTermination = trackWatchdogTermination + self.trackWatchdogTerminations = trackWatchdogTerminations } } From 8c648ba9b7399d5730c91aaf1458e23aa3b61288 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 20 Jun 2024 10:54:22 +0200 Subject: [PATCH 045/110] chore: fix SPM build and add scripts to check locally --- .../NodeRecorders/UITabBarRecorder.swift | 2 +- .../Sources/ObjC/WebViewTracking+objc.swift | 2 +- Makefile | 10 ++++ tools/spm.sh | 47 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100755 tools/spm.sh diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index 3f3f3844fc..a80414ca5f 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -120,7 +120,6 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { ] } } -#endif fileprivate extension UIImage { /// Returns a unique description of the image. @@ -142,3 +141,4 @@ fileprivate extension UIImage { return "\(cgImage.width)x\(cgImage.height)-\(cgImage.bitsPerComponent)x\(cgImage.bitsPerPixel)-\(cgImage.bytesPerRow)-\(cgImage.bitmapInfo)" } } +#endif diff --git a/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift b/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift index 8af8c2374a..286d2b9a0f 100644 --- a/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift +++ b/DatadogWebViewTracking/Sources/ObjC/WebViewTracking+objc.swift @@ -11,7 +11,6 @@ import DatadogInternal #warning("Datadog WebView Tracking does not support tvOS") #else import WebKit -#endif @objc(DDWebViewTracking) @_spi(objc) @@ -55,3 +54,4 @@ public final class objc_WebViewTracking: NSObject { WebViewTracking.disable(webView: webView) } } +#endif diff --git a/Makefile b/Makefile index 2cd633653e..66e4bb1adc 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,16 @@ DEFAULT_TVOS_OS := latest DEFAULT_TVOS_PLATFORM := tvOS Simulator DEFAULT_TVOS_DEVICE := Apple TV +build-spm: + @$(call require_param,PLATFORM) + @:$(eval PLATFORM ?= iOS) + ./tools/spm.sh --platform $(PLATFORM) + +build-spm-all: + ./tools/spm.sh --platform iOS + ./tools/spm.sh --platform tvOS + ./tools/spm.sh --platform visionOS + # Run unit tests for specified SCHEME test: @$(call require_param,SCHEME) diff --git a/tools/spm.sh b/tools/spm.sh new file mode 100755 index 0000000000..33cc122d54 --- /dev/null +++ b/tools/spm.sh @@ -0,0 +1,47 @@ +#!/bin/zsh + +# Usage: +# $ ./tools/spm.sh -h +# Builds the SDK only using Package.swift. + +set -eo pipefail +source ./tools/utils/echo_color.sh +source ./tools/utils/argparse.sh + +define_arg "platform" "" "Defines the type of simulator platform for the tests, e.g. 'iOS', 'tvOS, 'visionOS'" "string" "true" + +check_for_help "$@" +parse_args "$@" + +WORKSPACE="Datadog.xcworkspace" +WORKSPACE_RENAMED="Datadog.xcworkspace.old" +SCHEME="Datadog-Package" + +set -x + +rename_workspace() { + if [ ! -d "$WORKSPACE" ]; then + echo_warn "Workspace $WORKSPACE does not exist" + return 0 + fi + + echo_warn "Renaming workspace to $WORKSPACE_RENAMED" + mv "$WORKSPACE" "$WORKSPACE_RENAMED" +} + +restore_workspace() { + if [ ! -d "$WORKSPACE_RENAMED" ]; then + echo_warn "Workspace $WORKSPACE_RENAMED does not exist" + return 0 + fi + + echo_warn "Restoring workspace to $WORKSPACE" + mv "$WORKSPACE_RENAMED" "$WORKSPACE" +} + +rename_workspace +echo "Building SDK for platform $platform" +xcodebuild build -scheme $SCHEME -destination generic/platform="$platform" | xcbeautify + +# try to restore the files even if the script fails +trap restore_workspace EXIT INT From e1db92269216f78fd068672ae02cc83c6221c704 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 20 Jun 2024 11:04:31 +0200 Subject: [PATCH 046/110] trap before error --- tools/spm.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/spm.sh b/tools/spm.sh index 33cc122d54..c27bb69e68 100755 --- a/tools/spm.sh +++ b/tools/spm.sh @@ -40,8 +40,8 @@ restore_workspace() { } rename_workspace -echo "Building SDK for platform $platform" -xcodebuild build -scheme $SCHEME -destination generic/platform="$platform" | xcbeautify - # try to restore the files even if the script fails trap restore_workspace EXIT INT + +echo "Building SDK for platform $platform" +xcodebuild build -scheme $SCHEME -destination generic/platform="$platform" | xcbeautify \ No newline at end of file From 57855ac0df4e39c9037f62f6e811a6f03c79bc32 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Fri, 21 Jun 2024 11:58:22 +0200 Subject: [PATCH 047/110] RUM-4964 --- .../Resources/Storyboards/Images.storyboard | 18 - .../Storyboards/NavigationBars.storyboard | 455 ++++++++++++++++-- .../Resources/Storyboards/Tabbars.storyboard | 20 +- .../ImagesViewControllers.swift | 3 - .../NavigationBarControllers.swift | 39 ++ .../ViewControllers/TabbarControllers.swift | 6 +- .../SRSnapshotTests/SRSnapshotTests.swift | 49 +- .../testImages()-allow-privacy.png.json | 2 +- .../testImages()-mask-privacy.png.json | 2 +- ...lack+nontranslucent-allow-privacy.png.json | 2 +- ...black+nontranslucent-mask-privacy.png.json | 2 +- ...arblack+translucent-allow-privacy.png.json | 2 +- ...barblack+translucent-mask-privacy.png.json | 2 +- ...ent+backgroundcolor-allow-privacy.png.json | 2 +- ...cent+backgroundcolor-mask-privacy.png.json | 2 +- ...translucent+bartint-allow-privacy.png.json | 2 +- ...ntranslucent+bartint-mask-privacy.png.json | 2 +- ...ault+nontranslucent-allow-privacy.png.json | 2 +- ...fault+nontranslucent-mask-privacy.png.json | 2 +- ...ent+backgroundcolor-allow-privacy.png.json | 2 +- ...cent+backgroundcolor-mask-privacy.png.json | 2 +- ...translucent+bartint-allow-privacy.png.json | 2 +- ...+translucent+bartint-mask-privacy.png.json | 2 +- ...default+translucent-allow-privacy.png.json | 2 +- ...rdefault+translucent-mask-privacy.png.json | 2 +- ...rs()-navigationbars-allow-privacy.png.json | 2 +- ...nbars-itemTintColor-allow-privacy.png.json | 1 + ...onbars-itemTintColor-mask-privacy.png.json | 1 + ...ars()-navigationbars-mask-privacy.png.json | 2 +- .../testTabBars()-allow-privacy.png.json | 2 +- ...rs()-embeddedtabbar-allow-privacy.png.json | 2 +- ...ars()-embeddedtabbar-mask-privacy.png.json | 2 +- ...unselectedtintcolor-allow-privacy.png.json | 2 +- ...runselectedtintcolor-mask-privacy.png.json | 2 +- .../testTabBars()-mask-privacy.png.json | 2 +- .../UINavigationBarRecorder.swift | 11 +- .../NodeRecorders/UITabBarRecorder.swift | 4 +- 37 files changed, 535 insertions(+), 124 deletions(-) create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/ViewControllers/NavigationBarControllers.swift create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-navigationbars-itemTintColor-allow-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-navigationbars-itemTintColor-mask-privacy.png.json diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard index 497e2bfebf..17c11237d6 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard @@ -156,18 +156,6 @@ - - - - - - - - - - - -