Skip to content

Commit

Permalink
RUM-3461 Test data and conditions behind App Hang uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
ncreated committed Apr 5, 2024
1 parent 52141f0 commit 0243a15
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,13 @@ internal final class FatalAppHangsHandler {
// There is an oportunity for covering these cases through massive code reuse between fatal hangs and crashes through `FatalErrorBuilder`.
// TODO: RUM-3840 Track fatal App Hangs if there is no active RUM view

let realErrorDate = fatalHang.hang.startDate.addingTimeInterval(fatalHang.serverTimeOffset)
let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset)

let builder = FatalErrorBuilder(
context: context,
error: .hang,
errorDate: fatalHang.hang.startDate,
errorDate: realErrorDate,
errorType: AppHangsMonitor.Constants.appHangErrorType,
errorMessage: AppHangsMonitor.Constants.appHangErrorMessage,
errorStack: fatalHang.hang.backtraceResult.stack,
Expand All @@ -104,8 +107,6 @@ internal final class FatalAppHangsHandler {
)
let error = builder.createRUMError(with: fatalHang.lastRUMView)
let view = builder.updateRUMViewWithError(fatalHang.lastRUMView)
let realErrorDate = fatalHang.hang.startDate.addingTimeInterval(fatalHang.serverTimeOffset)
let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset)

if realDateNow.timeIntervalSince(realErrorDate) < FatalErrorBuilder.Constants.viewEventAvailabilityThreshold {
DD.logger.debug("Sending fatal App hang as RUM error with issuing RUM view update")
Expand Down
236 changes: 228 additions & 8 deletions DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,95 @@ class AppHangsMonitorTests: XCTestCase {
XCTAssertEqual(dd.logger.debugMessages, ["No pending App Hang found"])
}

func testGivenPendingHangSavedInOneProcess_whenStartedInDiffferentProcess_itSendsFatalHang() throws {
let sessionState: RUMSessionState = .mockRandom()
// MARK: - Fatal App Hangs - Testing Conditional Uploads

func testGivenPendingHangStartedLessThan4HoursAgo_whenStartedInAnotherProcess_itSendsBothRUMErrorAndRUMViewEvent() throws {
let currentDate: Date = .mockDecember15th2019At10AMUTC()
let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours))
let view: RUMViewEvent = .mockRandom()
let hang: AppHang = .mockRandom()
let hang: AppHang = .mockWith(startDate: hangDate)

// Given
featureScope.contextMock.trackingConsent = .granted
monitor.start()
fatalErrorContext.view = view
watchdogThread.delegate?.hangStarted(hang)
monitor.stop()

// When
featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session

let monitor = AppHangsMonitor(
featureScope: featureScope,
watchdogThread: watchdogThread,
fatalErrorContext: fatalErrorContext,
processID: UUID(), // different process
dateProvider: DateProviderMock(now: currentDate)
)
monitor.start()
defer { monitor.stop() }

// Then
XCTAssertEqual(
dd.logger.debugMessages,
[
"No pending App Hang found", // from hanged process
"Sending fatal App hang as RUM error with issuing RUM view update", // from next process
]
)

XCTAssertEqual(featureScope.eventsWritten.count, 2, "It must send both RUM error and RUM view")
XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1)
XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1)
}

func testGivenPendingHangStartedMoreThan4HoursAgo_whenStartedInAnotherProcess_itSendsOnlyRUMError() throws {
let currentDate: Date = .mockDecember15th2019At10AMUTC()
let hangDate: Date = currentDate.secondsAgo(.random(in: 4.hours..<24.hours))
let view: RUMViewEvent = .mockRandom()
let hang: AppHang = .mockWith(startDate: hangDate)

// Given
featureScope.contextMock.trackingConsent = .granted
monitor.start()
fatalErrorContext.sessionState = sessionState
fatalErrorContext.view = view
watchdogThread.delegate?.hangStarted(hang)
monitor.stop()

// When
featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session

let monitor = AppHangsMonitor(
featureScope: featureScope,
watchdogThread: watchdogThread,
fatalErrorContext: fatalErrorContext,
processID: UUID(), // different process
dateProvider: DateProviderMock(now: currentDate)
)
monitor.start()
defer { monitor.stop() }

// Then
XCTAssertEqual(
dd.logger.debugMessages,
[
"No pending App Hang found", // from hanged process
"Sending fatal App hang as RUM error without updating RUM view", // from next process
]
)

XCTAssertEqual(featureScope.eventsWritten.count, 1, "It must send only RUM error")
XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1)
}

func testGivenPendingHangStartedWithPendingOrNotGrantedConsent_whenStartedInAnotherProcess_itSendsNoEvent() throws {
let consent: TrackingConsent = .mockRandom(otherThan: TrackingConsent.granted)
let view: RUMViewEvent = .mockRandom()
let hang: AppHang = .mockRandom()

// Given
featureScope.contextMock.trackingConsent = consent
monitor.start()
fatalErrorContext.view = view
watchdogThread.delegate?.hangStarted(hang)
monitor.stop()
Expand All @@ -186,12 +266,152 @@ class AppHangsMonitorTests: XCTestCase {
XCTAssertEqual(
dd.logger.debugMessages,
[
"No pending App Hang found",
"Sending fatal App hang as RUM error without updating RUM view",
"No pending App Hang found", // from hanged process
"Skipped sending fatal App Hang as it was recorded with \(consent) consent", // from next process
]
)

// TODO: RUM-3461
// Assert on collected RUM error and RUM view update, similar to how we test it for crash reports
XCTAssertEqual(featureScope.eventsWritten.count, 0, "It must send no event")
}

// MARK: - Fatal App Hangs - Testing Uploaded Data

func testWhenSendingRUMViewEvent_itIsLinkedToPreviousRUMSessionAndIncludesErrorInformation() throws {
let currentDate: Date = .mockDecember15th2019At10AMUTC()
let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours))
let serverTimeOffset: TimeInterval = .mockRandom()
let lastView: RUMViewEvent = .mockRandom()
let hang: AppHang = .mockWith(startDate: hangDate)

// Given
featureScope.contextMock.trackingConsent = .granted
featureScope.contextMock.serverTimeOffset = serverTimeOffset
monitor.start()
fatalErrorContext.view = lastView
watchdogThread.delegate?.hangStarted(hang)
monitor.stop()

// When
featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session

let monitor = AppHangsMonitor(
featureScope: featureScope,
watchdogThread: watchdogThread,
fatalErrorContext: fatalErrorContext,
processID: UUID(), // different process
dateProvider: DateProviderMock(now: currentDate)
)
monitor.start()
defer { monitor.stop() }

// Then
let viewEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).first)
XCTAssertEqual(viewEvent.application.id, lastView.application.id)
XCTAssertEqual(viewEvent.session.id, lastView.session.id)
XCTAssertEqual(viewEvent.view.id, lastView.view.id)
XCTAssertEqual(viewEvent.dd.documentVersion, lastView.dd.documentVersion + 1, "It must increment document version")
XCTAssertEqual(viewEvent.view.error.count, lastView.view.error.count + 1, "It must count the hang error")
XCTAssertEqual(viewEvent.view.crash?.count, 1, "It must count crash")
XCTAssertEqual(viewEvent.view.isActive, false, "The view must be marked as inactive.")

XCTAssertEqual(viewEvent.service, lastView.service)
XCTAssertEqual(viewEvent.version, lastView.version)
XCTAssertEqual(viewEvent.buildVersion, lastView.buildVersion)
XCTAssertEqual(viewEvent.view.name, lastView.view.name)
XCTAssertEqual(viewEvent.view.url, lastView.view.url)
XCTAssertEqual(viewEvent.view.resource.count, lastView.view.resource.count)
XCTAssertEqual(viewEvent.view.action.count, lastView.view.action.count)
XCTAssertEqual(
viewEvent.date,
hangDate.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds - 1,
"It must be issued at hang date corrected by recorded offset and shifted back by 1ms"
)
XCTAssertEqual(viewEvent.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan")
DDAssertReflectionEqual(viewEvent.device, lastView.device)
DDAssertReflectionEqual(viewEvent.os, lastView.os)
DDAssertJSONEqual(viewEvent.connectivity, lastView.connectivity)
DDAssertJSONEqual(viewEvent.usr, lastView.usr)
}

func testWhenSendingRUMErrorEvent_itIsLinkedToPreviousRUMSessionAndIncludesErrorInformation() throws {
let currentDate: Date = .mockDecember15th2019At10AMUTC()
let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours))
let serverTimeOffset: TimeInterval = .mockRandom()
let lastView: RUMViewEvent = .mockRandom()
let hangBacktrace: BacktraceReport = .mockWith(
stack: """
0: stack-trace line 0
1: stack-trace line 1
2: stack-trace line 2
""",
threads: [
.init(name: "Thread 0", stack: "thread 0 stack", crashed: true, state: nil),
.init(name: "Thread 1", stack: "thread 1 stack", crashed: false, state: nil),
.init(name: "Thread 2", stack: "thread 2 stack", crashed: false, state: nil),
],
binaryImages: [
.init(libraryName: "library1", uuid: "uuid1", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad1", maxAddress: "0xMax1"),
.init(libraryName: "library2", uuid: "uuid2", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad2", maxAddress: "0xMax2"),
.init(libraryName: "library3", uuid: "uuid3", architecture: "arch", isSystemLibrary: false, loadAddress: "0xLoad3", maxAddress: "0xMax3"),
],
wasTruncated: .random()
)
let hang: AppHang = .mockWith(startDate: hangDate, backtraceResult: .succeeded(hangBacktrace))

// Given
featureScope.contextMock.trackingConsent = .granted
featureScope.contextMock.serverTimeOffset = serverTimeOffset
monitor.start()
fatalErrorContext.view = lastView
watchdogThread.delegate?.hangStarted(hang)
monitor.stop()

// When
featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session

let monitor = AppHangsMonitor(
featureScope: featureScope,
watchdogThread: watchdogThread,
fatalErrorContext: fatalErrorContext,
processID: UUID(), // different process
dateProvider: DateProviderMock(now: currentDate)
)
monitor.start()
defer { monitor.stop() }

// Then
let errorEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self).first)
XCTAssertEqual(errorEvent.application.id, lastView.application.id)
XCTAssertEqual(errorEvent.session.id, lastView.session.id)
XCTAssertEqual(errorEvent.view.id, lastView.view.id)
XCTAssertEqual(errorEvent.error.category, .appHang)
XCTAssertEqual(errorEvent.error.isCrash, true, "Fatal hang must be marked as crash")
XCTAssertEqual(errorEvent.view.name, lastView.view.name, "It must include view attributes")
XCTAssertEqual(errorEvent.view.referrer, lastView.view.referrer, "It must include view attributes")
XCTAssertEqual(errorEvent.view.url, lastView.view.url, "It must include view attributes")
DDAssertJSONEqual(
AnyEncodable(errorEvent.context?.contextInfo),
AnyEncodable(lastView.context?.contextInfo),
"It must include the user context from the last view"
)
XCTAssertEqual(
errorEvent.date,
hangDate.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds,
"It must include error date corrected by recorded server time offset"
)
XCTAssertEqual(errorEvent.error.type, AppHangsMonitor.Constants.appHangErrorType)
XCTAssertEqual(errorEvent.error.message, AppHangsMonitor.Constants.appHangErrorMessage)
XCTAssertEqual(
errorEvent.error.stack,
"""
0: stack-trace line 0
1: stack-trace line 1
2: stack-trace line 2
"""
)
XCTAssertEqual(errorEvent.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan")
DDAssertJSONEqual(errorEvent.error.threads, hangBacktrace.threads.toRUMDataFormat)
DDAssertJSONEqual(errorEvent.error.binaryImages, hangBacktrace.binaryImages.toRUMDataFormat)
XCTAssertEqual(errorEvent.error.wasTruncated, hangBacktrace.wasTruncated)
}
}
2 changes: 1 addition & 1 deletion DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ extension RUMViewEvent: RandomMockable {
pageStates: nil,
replayStats: nil,
session: .init(
plan: [.plan1, .plan2].randomElement()!,
plan: .plan1,
sessionPrecondition: .mockRandom()
)
),
Expand Down

0 comments on commit 0243a15

Please sign in to comment.