Skip to content

Commit c6c1528

Browse files
Merge pull request #2318 from DataDog/mariedm/rum-9035-action-tracking-telemetry
RUM-9035 Report Telemetry for Action count and instrumentation type Co-authored-by: mariedm <[email protected]>
2 parents 0eb2621 + 175bd1b commit c6c1528

File tree

12 files changed

+176
-31
lines changed

12 files changed

+176
-31
lines changed

DatadogCore/Sources/Datadog.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ extension DatadogCore {
570570
) throws {
571571
let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug)
572572
if debug {
573-
consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn)
573+
consolePrint("⚠️ Overriding verbosity, upload frequency, and sample rates due to \(LaunchArguments.Debug) launch argument", .warn)
574574
Datadog.verbosityLevel = .debug
575575
}
576576

DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class DDRUMConfigurationTests: XCTestCase {
131131
func testEventMappers() {
132132
let swiftViewEvent: RUMViewEvent = .mockRandom()
133133
let swiftResourceEvent: RUMResourceEvent = .mockRandom()
134-
let swiftActionEvent: RUMActionEvent = .mockRandom()
134+
let swiftActionEvent: RUMActionEvent = .mockAny()
135135
let swiftErrorEvent: RUMErrorEvent = .mockRandom()
136136
let swiftLongTaskEvent: RUMLongTaskEvent = .mockRandom()
137137

DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class RUMDataModels_objcTests: XCTestCase {
5757
let expectedUserInfoAttributes: [String: Any] = mockRandomAttributes()
5858

5959
// Given
60-
var swiftAction: RUMActionEvent = .mockRandom()
60+
var swiftAction: RUMActionEvent = .mockAny()
6161
swiftAction.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes
6262
swiftAction.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes
6363

DatadogRUM/Sources/Feature/RUMFeature.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal final class RUMFeature: DatadogRemoteFeature {
4444
let featureScope = core.scope(for: RUMFeature.self)
4545
let sessionEndedMetric = SessionEndedMetricController(
4646
telemetry: core.telemetry,
47-
sampleRate: configuration.sessionEndedSampleRate
47+
sampleRate: configuration.debugSDK ? 100 : configuration.sessionEndedSampleRate
4848
)
4949
let tnsPredicateType = configuration.networkSettledResourcePredicate.metricPredicateType
5050
let invPredicateType = configuration.nextViewActionPredicate?.metricPredicateType ?? .disabled
@@ -128,7 +128,7 @@ internal final class RUMFeature: DatadogRemoteFeature {
128128
viewEndedMetricFactory: {
129129
let viewEndedController = ViewEndedController(
130130
telemetry: featureScope.telemetry,
131-
sampleRate: configuration.viewEndedSampleRate
131+
sampleRate: configuration.debugSDK ? 100 : configuration.viewEndedSampleRate
132132
)
133133
viewEndedController.add(metric: ViewEndedMetric(tnsConfigPredicate: tnsPredicateType, invConfigPredicate: invPredicateType))
134134

@@ -259,8 +259,8 @@ internal final class RUMFeature: DatadogRemoteFeature {
259259
appHangThreshold: configuration.appHangThreshold?.toInt64Milliseconds,
260260
invTimeThresholdMs: (configuration.nextViewActionPredicate as? TimeBasedINVActionPredicate)?.maxTimeToNextView.toInt64Milliseconds,
261261
mobileVitalsUpdatePeriod: configuration.vitalsUpdateFrequency?.timeInterval.toInt64Milliseconds,
262-
sessionSampleRate: Int64(withNoOverflow: configuration.sessionSampleRate),
263-
telemetrySampleRate: Int64(withNoOverflow: configuration.telemetrySampleRate),
262+
sessionSampleRate: Int64(withNoOverflow: configuration.debugSDK ? 100 : configuration.sessionSampleRate),
263+
telemetrySampleRate: Int64(withNoOverflow: configuration.debugSDK ? 100 : configuration.telemetrySampleRate),
264264
tnsTimeThresholdMs: (configuration.networkSettledResourcePredicate as? TimeBasedTNSResourcePredicate)?.threshold.toInt64Milliseconds,
265265
traceSampleRate: configuration.urlSessionTracking?.firstPartyHostsTracing.map { Int64(withNoOverflow: $0.sampleRate) },
266266
trackBackgroundEvents: configuration.trackBackgroundEvents,

DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider {
206206
}
207207

208208
if didCreateInitialSessionCount > 0 { // Sanity check
209-
// We assume this is not an initial session in the app (such is started with `RUMSDKInitCommand`:
210-
dependencies.telemetry.error("Starting NEW session on due to \(type(of: command)), but initial sesison never existed")
209+
// This is a non-initial session (initial sessions are created via `RUMSDKInitCommand`)
210+
dependencies.telemetry.debug("Starting new session triggered by \(type(of: command)). Previous session was stopped for the following reason: \(startPrecondition?.rawValue ?? "unknown")")
211211
}
212212

213213
let resumingViewScope = command is RUMStartViewCommand ? nil : lastActiveView

DatadogRUM/Sources/RUMMonitor/Scopes/RUMUserActionScope.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider {
203203

204204
if let event = dependencies.eventBuilder.build(from: actionEvent) {
205205
writer.write(value: event)
206+
207+
// Track action in session ended metric
208+
dependencies.sessionEndedMetric.track(
209+
action: event,
210+
instrumentationType: instrumentation,
211+
in: self.context.sessionID
212+
)
213+
206214
onActionEventSent(event)
207215

208216
if let activeViewID = self.context.activeViewID {

DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ internal class SessionEndedMetric {
8686
/// Info about the last tracked view.
8787
private var lastTrackedView: TrackedViewInfo?
8888

89+
/// Stores information about tracked actions, referencing them by their instrumentation type.
90+
private var trackedActions: [String: Int] = [:]
91+
8992
/// Tracks the number of SDK errors by their kind.
9093
private var trackedSDKErrors: [String: Int] = [:]
9194

@@ -167,24 +170,28 @@ internal class SessionEndedMetric {
167170
lastTrackedView = info
168171
}
169172

173+
/// Tracks information about an action that occurred during the session.
174+
/// - Parameters:
175+
/// - action: the action event to track
176+
/// - instrumentationType: the type of instrumentation used to start this action
177+
func track(action: RUMActionEvent, instrumentationType: InstrumentationType) {
178+
guard action.session.id == sessionID.toRUMDataFormat else {
179+
return
180+
}
181+
182+
trackedActions[instrumentationType.metricKey, default: 0] += 1
183+
}
184+
170185
/// Tracks the kind of SDK error that occurred during the session.
171186
/// - Parameter sdkErrorKind: the kind of SDK error
172187
func track(sdkErrorKind: String) {
173-
if let count = trackedSDKErrors[sdkErrorKind] {
174-
trackedSDKErrors[sdkErrorKind] = count + 1
175-
} else {
176-
trackedSDKErrors[sdkErrorKind] = 1
177-
}
188+
trackedSDKErrors[sdkErrorKind, default: 0] += 1
178189
}
179190

180191
/// Tracks an event missed due to absence of an active view.
181192
/// - Parameter missedEventType: the type of an event that was missed
182193
func track(missedEventType: MissedEventType) {
183-
if let count = missedEvents[missedEventType] {
184-
missedEvents[missedEventType] = count + 1
185-
} else {
186-
missedEvents[missedEventType] = 1
187-
}
194+
missedEvents[missedEventType, default: 0] += 1
188195
}
189196

190197
/// Signals that the session was stopped with `stopSession()` API.
@@ -274,6 +281,20 @@ internal class SessionEndedMetric {
274281

275282
let viewsCount: ViewsCount
276283

284+
struct ActionsCount: Encodable {
285+
/// The number of distinct actions sent during this session.
286+
let total: Int
287+
/// The map of action instrumentation types to the number of actions tracked with each instrumentation.
288+
let byInstrumentation: [String: Int]
289+
290+
enum CodingKeys: String, CodingKey {
291+
case total
292+
case byInstrumentation = "by_instrumentation"
293+
}
294+
}
295+
296+
let actionsCount: ActionsCount
297+
277298
struct SDKErrorsCount: Encodable {
278299
/// The total number of SDK errors that occurred during the session, excluding any effects from telemetry limits
279300
/// such as duplicate filtering or maximum caps.
@@ -340,8 +361,8 @@ internal class SessionEndedMetric {
340361

341362
/// Information about the upload quality during the session.
342363
/// The upload quality is splitting between upload track name.
343-
/// Tracks upload quality during the session, aggregating them by track name.
344-
/// Each track reports its own upload quality metrics.
364+
/// Tracks upload quality during the session, aggregating them by track name.
365+
/// Each track reports its own upload quality metrics.
345366
let uploadQuality: [String: UploadQuality]
346367

347368
enum CodingKeys: String, CodingKey {
@@ -351,6 +372,7 @@ internal class SessionEndedMetric {
351372
case wasStopped = "was_stopped"
352373
case hasBackgroundEventsTrackingEnabled = "has_background_events_tracking_enabled"
353374
case viewsCount = "views_count"
375+
case actionsCount = "actions_count"
354376
case sdkErrorsCount = "sdk_errors_count"
355377
case ntpOffset = "ntp_offset"
356378
case noViewEventsCount = "no_view_events_count"
@@ -378,9 +400,10 @@ internal class SessionEndedMetric {
378400
var byInstrumentationViewsCount: [String: Int] = [:]
379401
trackedViews.values.forEach {
380402
if let instrumentationType = $0.instrumentationType {
381-
byInstrumentationViewsCount[instrumentationType.metricKey] = (byInstrumentationViewsCount[instrumentationType.metricKey] ?? 0) + 1
403+
byInstrumentationViewsCount[instrumentationType.metricKey, default: 0] += 1
382404
}
383405
}
406+
let totalActionsCount = trackedActions.values.reduce(0, +)
384407
let withHasReplayCount = trackedViews.values.reduce(0, { acc, next in acc + (next.hasReplay ? 1 : 0) })
385408

386409
// Compute SDK errors count
@@ -408,6 +431,10 @@ internal class SessionEndedMetric {
408431
byInstrumentation: byInstrumentationViewsCount,
409432
withHasReplay: withHasReplayCount
410433
),
434+
actionsCount: .init(
435+
total: totalActionsCount,
436+
byInstrumentation: trackedActions
437+
),
411438
sdkErrorsCount: .init(
412439
total: totalSDKErrors,
413440
byKind: top5SDKErrorsByKind

DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ internal final class SessionEndedMetricController {
6565
updateMetric(for: sessionID) { try $0?.track(view: view, instrumentationType: instrumentationType) }
6666
}
6767

68+
/// Tracks the action event that occurred during the session.
69+
/// - Parameters:
70+
/// - action: the action event to track
71+
/// - instrumentationType: the type of instrumentation used to start this action
72+
/// - sessionID: session ID to track this action in (pass `nil` to track it for the last started session)
73+
func track(
74+
action: RUMActionEvent,
75+
instrumentationType: InstrumentationType,
76+
in sessionID: RUMUUID?
77+
) {
78+
updateMetric(for: sessionID) { $0?.track(action: action, instrumentationType: instrumentationType) }
79+
}
80+
6881
/// Tracks the kind of SDK error that occurred during the session.
6982
/// - Parameters:
7083
/// - sdkErrorKind: the kind of SDK error to track

DatadogRUM/Tests/RUMEvent/RUMEventSanitizerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import DatadogInternal
1111
class RUMEventSanitizerTests: XCTestCase {
1212
private let viewEvent: RUMViewEvent = .mockRandom()
1313
private let resourceEvent: RUMResourceEvent = .mockRandom()
14-
private let actionEvent: RUMActionEvent = .mockRandom()
14+
private let actionEvent: RUMActionEvent = .mockAny()
1515
private let errorEvent: RUMErrorEvent = .mockRandom()
1616
private let longTaskEvent: RUMLongTaskEvent = .mockRandom()
1717

DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class SessionEndedMetricTests: XCTestCase {
2727
XCTAssertEqual(rse.viewsCount.total, 0)
2828
XCTAssertEqual(rse.viewsCount.background, 0)
2929
XCTAssertEqual(rse.viewsCount.applicationLaunch, 0)
30+
XCTAssertEqual(rse.actionsCount.total, 0)
3031
XCTAssertEqual(rse.sdkErrorsCount.total, 0)
3132
XCTAssertEqual(rse.sdkErrorsCount.byKind, [:])
3233
}
@@ -407,6 +408,96 @@ class SessionEndedMetricTests: XCTestCase {
407408
XCTAssertEqual(rse.viewsCount.total, 0)
408409
}
409410

411+
// MARK: - Actions Count
412+
413+
func testReportingTotalActionsCount() throws {
414+
let actionCount: Int = .mockRandom(min: 1, max: 10)
415+
416+
// Given
417+
let metric = SessionEndedMetric.with(sessionID: sessionID)
418+
419+
// When
420+
(0..<actionCount).forEach { _ in
421+
metric.track(
422+
action: .mockWith(sessionID: sessionID.rawValue),
423+
instrumentationType: .manual
424+
)
425+
}
426+
let attributes = metric.asMetricAttributes()
427+
428+
// Then
429+
let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes)
430+
XCTAssertEqual(rse.actionsCount.total, actionCount)
431+
}
432+
433+
func testReportingActionsCountByInstrumentationType() throws {
434+
let manualActionsCount: Int = .mockRandom(min: 1, max: 10)
435+
let swiftuiActionsCount: Int = .mockRandom(min: 1, max: 10)
436+
let uikitPredicateActionsCount: Int = .mockRandom(min: 1, max: 10)
437+
let swiftuiAutomaticPredicateActionsCount: Int = .mockRandom(min: 1, max: 10)
438+
439+
// Given
440+
let metric = SessionEndedMetric.with(sessionID: sessionID)
441+
442+
// When
443+
(0..<manualActionsCount).forEach { _ in
444+
metric.track(
445+
action: .mockWith(sessionID: sessionID.rawValue),
446+
instrumentationType: .manual
447+
)
448+
}
449+
(0..<swiftuiActionsCount).forEach { _ in
450+
metric.track(
451+
action: .mockWith(sessionID: sessionID.rawValue),
452+
instrumentationType: .swiftui
453+
)
454+
}
455+
(0..<uikitPredicateActionsCount).forEach { _ in
456+
metric.track(
457+
action: .mockWith(sessionID: sessionID.rawValue),
458+
instrumentationType: .uikit
459+
)
460+
}
461+
(0..<swiftuiAutomaticPredicateActionsCount).forEach { _ in
462+
metric.track(
463+
action: .mockWith(sessionID: sessionID.rawValue),
464+
instrumentationType: .swiftuiAutomatic
465+
)
466+
}
467+
let attributes = metric.asMetricAttributes()
468+
469+
// Then
470+
let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes)
471+
XCTAssertEqual(
472+
rse.actionsCount.total,
473+
manualActionsCount + swiftuiActionsCount + uikitPredicateActionsCount + swiftuiAutomaticPredicateActionsCount
474+
)
475+
XCTAssertEqual(
476+
rse.actionsCount.byInstrumentation,
477+
[
478+
"manual": manualActionsCount,
479+
"swiftui": swiftuiActionsCount,
480+
"uikit": uikitPredicateActionsCount,
481+
"swiftuiAutomatic": swiftuiAutomaticPredicateActionsCount
482+
]
483+
)
484+
}
485+
486+
func testWhenReportingActionsCount_itIgnoresActionsFromDifferentSession() throws {
487+
// Given
488+
let metric = SessionEndedMetric.with(sessionID: sessionID)
489+
490+
// When
491+
metric.track(action: .mockAny(), instrumentationType: .manual)
492+
metric.track(action: .mockAny(), instrumentationType: .manual)
493+
let attributes = metric.asMetricAttributes()
494+
495+
// Then
496+
let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes)
497+
XCTAssertEqual(rse.actionsCount.total, 0)
498+
XCTAssertEqual(rse.actionsCount.byInstrumentation, [:])
499+
}
500+
410501
// MARK: - SDK Errors Count
411502

412503
func testReportingTotalSDKErrorsCount() throws {

DatadogRUM/Tests/Scrubbing/RUMEventsMapperTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ class RUMEventsMapperTests: XCTestCase {
2020
let originalResourceEvent: RUMResourceEvent = .mockRandom()
2121
let modifiedResourceEvent: RUMResourceEvent = .mockRandom()
2222

23-
let originalActionEvent: RUMActionEvent = .mockRandom()
24-
let modifiedActionEvent: RUMActionEvent = .mockRandom()
23+
let originalActionEvent: RUMActionEvent = .mockAny()
24+
let modifiedActionEvent: RUMActionEvent = .mockAny()
2525

2626
let originalLongTaskEvent: RUMLongTaskEvent = .mockRandom()
2727
let modifiedLongTaskEvent: RUMLongTaskEvent = .mockRandom()
@@ -68,7 +68,7 @@ class RUMEventsMapperTests: XCTestCase {
6868
func testGivenMappersEnabled_whenDroppingEvents_itReturnsNil() {
6969
let originalErrorEvent: RUMErrorEvent = .mockRandom()
7070
let originalResourceEvent: RUMResourceEvent = .mockRandom()
71-
let originalActionEvent: RUMActionEvent = .mockRandom()
71+
let originalActionEvent: RUMActionEvent = .mockAny()
7272
let originalLongTaskEvent: RUMLongTaskEvent = .mockRandom()
7373

7474
// Given
@@ -108,7 +108,7 @@ class RUMEventsMapperTests: XCTestCase {
108108
let originalViewEvent: RUMViewEvent = .mockRandom()
109109
let originalErrorEvent: RUMErrorEvent = .mockRandom()
110110
let originalResourceEvent: RUMResourceEvent = .mockRandom()
111-
let originalActionEvent: RUMActionEvent = .mockRandom()
111+
let originalActionEvent: RUMActionEvent = .mockAny()
112112
let originalLongTaskEvent: RUMLongTaskEvent = .mockRandom()
113113

114114
// Given

TestUtilities/Sources/Mocks/DatadogInternal/RUMDataModelMocks.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public func randomRUMEvent() -> RUMDataModel {
4141
// swiftlint:disable opening_brace
4242
return oneOf([
4343
{ RUMViewEvent.mockRandom() },
44-
{ RUMActionEvent.mockRandom() },
44+
{ RUMActionEvent.mockAny() },
4545
{ RUMResourceEvent.mockRandom() },
4646
{ RUMErrorEvent.mockRandom() },
4747
{ RUMLongTaskEvent.mockRandom() },
@@ -369,8 +369,14 @@ extension RUMActionEvent.DD.Configuration: RandomMockable {
369369
}
370370
}
371371

372-
extension RUMActionEvent: RandomMockable {
373-
public static func mockRandom() -> RUMActionEvent {
372+
extension RUMActionEvent: AnyMockable {
373+
public static func mockAny() -> RUMActionEvent {
374+
.mockWith()
375+
}
376+
377+
public static func mockWith(
378+
sessionID: UUID = .mockRandom()
379+
) -> RUMActionEvent {
374380
return RUMActionEvent(
375381
dd: .init(
376382
action: .init(
@@ -414,7 +420,7 @@ extension RUMActionEvent: RandomMockable {
414420
service: .mockRandom(),
415421
session: .init(
416422
hasReplay: nil,
417-
id: .mockRandom(),
423+
id: sessionID.uuidString.lowercased(),
418424
type: .user
419425
),
420426
source: .ios,

0 commit comments

Comments
 (0)