Skip to content

Commit

Permalink
RUM-1660 Track "RUM Session Ended" attributes in RUM
Browse files Browse the repository at this point in the history
  • Loading branch information
ncreated committed May 29, 2024
1 parent da2ed96 commit bce9791
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 21 deletions.
20 changes: 20 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@
61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */; };
61DCC8472C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */; };
61DCC8482C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */; };
61DCC84A2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */; };
61DCC84B2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */; };
61DCC84E2C071DCD00CB59E5 /* TelemetryInterecptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC84D2C071DCD00CB59E5 /* TelemetryInterecptor.swift */; };
61DCC84F2C071DCD00CB59E5 /* TelemetryInterecptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC84D2C071DCD00CB59E5 /* TelemetryInterecptor.swift */; };
61E45BE724519A3700F2C652 /* JSONDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */; };
61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */; };
61E5333824B84EE2003D6C4E /* DebugRUMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5333724B84EE2003D6C4E /* DebugRUMViewController.swift */; };
Expand Down Expand Up @@ -2585,6 +2589,8 @@
61DB33B025DEDFC200F7EA71 /* CustomObjcViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomObjcViewController.h; sourceTree = "<group>"; };
61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomObjcViewController.m; sourceTree = "<group>"; };
61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndedMetricControllerTests.swift; sourceTree = "<group>"; };
61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionEndedMetricIntegrationTests.swift; sourceTree = "<group>"; };
61DCC84D2C071DCD00CB59E5 /* TelemetryInterecptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryInterecptor.swift; sourceTree = "<group>"; };
61DE333525C8278A008E3EC2 /* CrashReportingPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingPlugin.swift; sourceTree = "<group>"; };
61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceIDTests.swift; sourceTree = "<group>"; };
61E45BD12450F65B00F2C652 /* SpanEventBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanEventBuilderTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4629,6 +4635,7 @@
D236BE2729520FED00676E67 /* CrashReportReceiver.swift */,
D215ED6A29D2E1080046B721 /* ErrorMessageReceiver.swift */,
D214DAA729E54CB4004D0AE8 /* TelemetryReceiver.swift */,
61DCC84D2C071DCD00CB59E5 /* TelemetryInterecptor.swift */,
);
path = Integrations;
sourceTree = "<group>";
Expand Down Expand Up @@ -5251,6 +5258,14 @@
path = NTP;
sourceTree = "<group>";
};
61DCC84C2C05D4E500CB59E5 /* SDKMetrics */ = {
isa = PBXGroup;
children = (
61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */,
);
path = SDKMetrics;
sourceTree = "<group>";
};
61E45BD02450F64100F2C652 /* Span */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5309,6 +5324,7 @@
61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */,
6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */,
D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */,
61DCC84C2C05D4E500CB59E5 /* SDKMetrics */,
);
path = RUM;
sourceTree = "<group>";
Expand Down Expand Up @@ -8018,6 +8034,7 @@
D24C9C7129A7D57A002057CF /* DirectoriesMock.swift in Sources */,
D22743E329DEB90B001A7EF9 /* RUMDebuggingTests.swift in Sources */,
614798992A459B2E0095CB02 /* DDTraceConfigurationTests.swift in Sources */,
61DCC84A2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */,
61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */,
D2A1EE36287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift in Sources */,
61BBD19724ED50040023E65F /* DatadogConfigurationTests.swift in Sources */,
Expand Down Expand Up @@ -8648,6 +8665,7 @@
D23F8E8D29DDCD28001CFAE8 /* VitalRefreshRateReader.swift in Sources */,
D23F8E8E29DDCD28001CFAE8 /* UIKitRUMUserActionsHandler.swift in Sources */,
D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */,
61DCC84F2C071DCD00CB59E5 /* TelemetryInterecptor.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -8964,6 +8982,7 @@
D29A9F8929DD85BB005C54A4 /* VitalRefreshRateReader.swift in Sources */,
D29A9F6929DD85BB005C54A4 /* UIKitRUMUserActionsHandler.swift in Sources */,
D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */,
61DCC84E2C071DCD00CB59E5 /* TelemetryInterecptor.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -9202,6 +9221,7 @@
A7EA11622AB0CE6C00C73970 /* DDUIKitRUMActionsPredicateTests.swift in Sources */,
D2CB6EE527C520D400A62B57 /* DataUploadConditionsTests.swift in Sources */,
D2CB6EE627C520D400A62B57 /* DateFormattingTests.swift in Sources */,
61DCC84B2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */,
D2CB6EE727C520D400A62B57 /* FileTests.swift in Sources */,
610ABD4D2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */,
D2CB6EEA27C520D400A62B57 /* LogMatcher.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* 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 DatadogRUM

class RUMSessionEndedMetricIntegrationTests: XCTestCase {
private let dateProvider = DateProviderMock()
private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional
private var rumConfig: RUM.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional

override func setUp() {
super.setUp()
core = DatadogCoreProxy()
core.context = .mockWith(
launchTime: .mockWith(launchDate: dateProvider.now),
applicationStateHistory: .mockAppInForeground(since: dateProvider.now)
)
rumConfig = RUM.Configuration(applicationID: .mockAny())
rumConfig.telemetrySampleRate = 100
rumConfig.metricsTelemetrySampleRate = 100
rumConfig.dateProvider = dateProvider
}

override func tearDown() {
core.flushAndTearDown()
core = nil
rumConfig = nil
super.tearDown()
}

// MARK: - Conditions For Sending The Metric

func testWhenSessionEndsWithStopAPI() throws {
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
monitor.startView(key: "key", name: "View")

// When
monitor.stopSession()

// Then
let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes)
XCTAssertTrue(metricAttributes.wasStopped)
}

func testWhenSessionEndsDueToInactivityTimeout() throws {
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
monitor.startView(key: "key1", name: "View1")

// When
dateProvider.now += RUMSessionScope.Constants.sessionTimeoutDuration + 1.seconds
monitor.startView(key: "key2", name: "View2")

// Then
let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes)
XCTAssertFalse(metricAttributes.wasStopped)
}

func testWhenSessionReachesMaxDuration() throws {
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
monitor.startView(key: "key", name: "View")

// When
let deadline = dateProvider.now + RUMSessionScope.Constants.sessionMaxDuration * 1.5
while dateProvider.now < deadline {
monitor.addAction(type: .custom, name: "action")
dateProvider.now += RUMSessionScope.Constants.sessionTimeoutDuration - 1.seconds
}

// Then
let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes)
XCTAssertFalse(metricAttributes.wasStopped)
}

func testWhenSessionIsNotSampled_thenMetricIsNotSent() throws {
rumConfig.sessionSampleRate = 0
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
monitor.startView(key: "key", name: "View")

// When
monitor.stopSession()

// Then
let events = core.waitAndReturnEventsData(ofFeature: RUMFeature.name, timeout: .now() + 0.5)
XCTAssertTrue(events.isEmpty)
}

// MARK: - Reporting Session Attributes

func testReportingSessionID() throws {
var currentSessionID: String?
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
monitor.startView(key: "key", name: "View")
monitor.currentSessionID { currentSessionID = $0 }
monitor.stopView(key: "key")

// When
monitor.stopSession()

// Then
let metric = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent())
let expectedSessionID = try XCTUnwrap(currentSessionID)
XCTAssertEqual(metric.session?.id, expectedSessionID.lowercased())
}

func testTrackingSessionDuration() throws {
let startTime = dateProvider.now
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
dateProvider.now += 5.seconds
monitor.startView(key: "key1", name: "View1")
dateProvider.now += 5.seconds
monitor.startView(key: "key2", name: "View2")
dateProvider.now += 5.seconds
monitor.startView(key: "key3", name: "View3")
dateProvider.now += 5.seconds
monitor.stopView(key: "key3")

// When
monitor.stopSession()

// Then
let expectedDuration = dateProvider.now.timeIntervalSince(startTime)
let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes)
XCTAssertEqual(metricAttributes.duration, expectedDuration.toInt64Nanoseconds)
}

func testTrackingViewsCount() throws {
rumConfig.trackBackgroundEvents = true // enable tracking "Background" view
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
(0..<3).forEach { _ in
// Simulate app in foreground:
core.context = .mockWith(applicationStateHistory: .mockAppInForeground(since: dateProvider.now))

// Track 2 distinct views:
dateProvider.now += 5.seconds
monitor.startView(key: "key1", name: "View1")
dateProvider.now += 5.seconds
monitor.startView(key: "key2", name: "View2")
dateProvider.now += 5.seconds
monitor.stopView(key: "key2")

// Simulate app in background:
core.context = .mockWith(applicationStateHistory: .mockAppInBackground(since: dateProvider.now))

// Track resource without view:
dateProvider.now += 1.seconds
monitor.startResource(resourceKey: "resource", url: .mockAny())
dateProvider.now += 1.seconds
monitor.stopResource(resourceKey: "resource", response: .mockAny())
}

// When
monitor.stopSession()

// Then
let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes)
XCTAssertEqual(metricAttributes.viewsCount.total, 10)
XCTAssertEqual(metricAttributes.viewsCount.applicationLaunch, 1)
XCTAssertEqual(metricAttributes.viewsCount.background, 3)
}

func testTrackingSDKErrors() throws {
RUM.enable(with: rumConfig, in: core)

// Given
let monitor = RUMMonitor.shared(in: core)
monitor.startView(key: "key", name: "View")

core.flush()
(0..<9).forEach { _ in core.telemetry.error(id: "id1", message: .mockAny(), kind: "kind1", stack: .mockAny()) }
(0..<8).forEach { _ in core.telemetry.error(id: "id2", message: .mockAny(), kind: "kind2", stack: .mockAny()) }
(0..<7).forEach { _ in core.telemetry.error(id: "id3", message: .mockAny(), kind: "kind3", stack: .mockAny()) }
(0..<6).forEach { _ in core.telemetry.error(id: "id4", message: .mockAny(), kind: "kind4", stack: .mockAny()) }
(0..<5).forEach { _ in core.telemetry.error(id: "id5", message: .mockAny(), kind: "kind5", stack: .mockAny()) }
(0..<4).forEach { _ in core.telemetry.error(id: "id6", message: .mockAny(), kind: "kind6", stack: .mockAny()) }
core.flush()

// When
monitor.stopSession()

// Then
let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes)
XCTAssertEqual(metricAttributes.sdkErrorsCount.total, 39, "It should count all SDK errors")
XCTAssertEqual(
metricAttributes.sdkErrorsCount.byKind,
["kind1": 9, "kind2": 8, "kind3": 7, "kind4": 6, "kind5": 5],
"It should report TOP 5 error kinds"
)
}
}

// MARK: - Helpers

private extension DatadogCoreProxy {
func waitAndReturnSessionEndedMetricEvent() -> TelemetryDebugEvent? {
let events = waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self)
return events.first(where: { $0.telemetry.message == "[Mobile Metric] \(SessionEndedMetric.Constants.name)" })
}
}

private extension TelemetryDebugEvent {
var attributes: SessionEndedMetric.Attributes? {
return telemetry.telemetryInfo[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes
}
}

10 changes: 0 additions & 10 deletions DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,6 @@ extension UploadPerformanceMock {
}
}

extension BundleType: AnyMockable, RandomMockable {
public static func mockAny() -> BundleType {
return .iOSApp
}

public static func mockRandom() -> BundleType {
return [.iOSApp, .iOSAppExtension].randomElement()!
}
}

extension PerformancePreset: AnyMockable, RandomMockable {
public static func mockAny() -> Self {
PerformancePreset(batchSize: .medium, uploadFrequency: .average, bundleType: .iOSApp)
Expand Down
8 changes: 7 additions & 1 deletion DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import Foundation

/// Common fields in SDK metrics.
public enum SDKMetricFields {
/// Metric type key.
/// Metric type key. It expects `String` value.
public static let typeKey = "metric_type"

/// Key referencing the session ID (`String`) that the metric should be sent with. It expects `String` value.
///
/// When attached to metric attributes, the value of this key (session ID) will be used to replace
/// the ID of session that the metric was collected in. The key itself is dropped before the metric is sent.
public static let sessionIDOverrideKey = "session_id_override"
}
1 change: 1 addition & 0 deletions DatadogRUM/Sources/Feature/RUMFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ internal final class RUMFeature: DatadogRemoteFeature {
telemetry: core.telemetry
)
self.messageReceiver = CombinedFeatureMessageReceiver(
TelemetryInterecptor(sessionEndedMetric: sessionEndedMetric),
TelemetryReceiver(
featureScope: featureScope,
dateProvider: configuration.dateProvider,
Expand Down
31 changes: 31 additions & 0 deletions DatadogRUM/Sources/Integrations/TelemetryInterecptor.swift
Original file line number Diff line number Diff line change
@@ -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
import DatadogInternal

internal struct TelemetryInterecptor: FeatureMessageReceiver {
let sessionEndedMetric: SessionEndedMetricController

func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool {
guard case .telemetry(let telemetry) = message else {
return false
}

switch telemetry {
case .error(let id, let message, let kind, let stack):
interceptError(id: id, message: message, kind: kind, stack: stack)
default:
break
}

return false // do not consume, pass to next receivers
}

private func interceptError(id: String, message: String, kind: String, stack: String) {
sessionEndedMetric.latestMetric?.track(sdkErrorKind: kind)
}
}
Loading

0 comments on commit bce9791

Please sign in to comment.