Skip to content

Commit

Permalink
RUMM-1064 AppStateListener added
Browse files Browse the repository at this point in the history
This will avoid unrealistically long spans in APM.
URLSessionTracingHandler will listen to app state changes
and will add foreground duration, background information to the span.

PR comments addressed.
  • Loading branch information
buranmert committed Mar 25, 2021
1 parent 45dd24b commit 0eb6772
Show file tree
Hide file tree
Showing 14 changed files with 449 additions and 28 deletions.
8 changes: 8 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@
9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */; };
9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; };
9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Private, ); }; };
9E989A4225F640D100235FC3 /* AppStateListenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E989A4125F640D100235FC3 /* AppStateListenerTests.swift */; };
9ED6A6B425F2901800CB2E29 /* AppStateListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED6A6B325F2901800CB2E29 /* AppStateListener.swift */; };
9EEA4871258B76A100EBDA9D /* Global+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEA4870258B76A100EBDA9D /* Global+objc.swift */; };
9EF963E82537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF963E72537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift */; };
9EFD112C24B32D29003A1A2B /* FirstPartyURLsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFD112B24B32D29003A1A2B /* FirstPartyURLsFilter.swift */; };
Expand Down Expand Up @@ -966,7 +968,9 @@
9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEncoderTests.swift; sourceTree = "<group>"; };
9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcExceptionHandler.m; sourceTree = "<group>"; };
9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcExceptionHandler.h; sourceTree = "<group>"; };
9E989A4125F640D100235FC3 /* AppStateListenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateListenerTests.swift; sourceTree = "<group>"; };
9E9EB37624468CE90002C80B /* Datadog.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Datadog.modulemap; sourceTree = "<group>"; };
9ED6A6B325F2901800CB2E29 /* AppStateListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateListener.swift; sourceTree = "<group>"; };
9EEA4870258B76A100EBDA9D /* Global+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Global+objc.swift"; sourceTree = "<group>"; };
9EF49F1624476FBD004F2CA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogIntegrationTests.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1212,6 +1216,7 @@
61133BA32423979B00786299 /* MobileDevice.swift */,
61133BA42423979B00786299 /* NetworkConnectionInfoProvider.swift */,
61133BA52423979B00786299 /* BatteryStatusProvider.swift */,
9ED6A6B325F2901800CB2E29 /* AppStateListener.swift */,
);
path = System;
sourceTree = "<group>";
Expand Down Expand Up @@ -2309,6 +2314,7 @@
613F23E2252B05D7006CD2D7 /* URLFiltering */,
61B03874252724AB00518F3C /* URLSessionInterceptorTests.swift */,
613F23E3252B062F006CD2D7 /* TaskInterceptionTests.swift */,
9E989A4125F640D100235FC3 /* AppStateListenerTests.swift */,
);
path = Interception;
sourceTree = "<group>";
Expand Down Expand Up @@ -3285,6 +3291,7 @@
61C3638524361E9200C4D4E6 /* Globals.swift in Sources */,
E1D202EA24C065CF00D1AF3A /* ActiveSpansPool.swift in Sources */,
61940C7C25668EC600A20043 /* URLSessionInterceptionHandler.swift in Sources */,
9ED6A6B425F2901800CB2E29 /* AppStateListener.swift in Sources */,
61F3CDA3251118FB00C816E5 /* UIKitRUMViewsHandler.swift in Sources */,
61C5A88824509A0C00DA608C /* Warnings.swift in Sources */,
619E16E92578E73E00B2516B /* DataMigrator.swift in Sources */,
Expand Down Expand Up @@ -3524,6 +3531,7 @@
9EF963E82537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift in Sources */,
61133C552423990D00786299 /* BatteryStatusProviderTests.swift in Sources */,
61F3CDAB25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift in Sources */,
9E989A4225F640D100235FC3 /* AppStateListenerTests.swift in Sources */,
617B954024BF4DB300E6F443 /* RUMApplicationScopeTests.swift in Sources */,
61F2724925C943C500D54BF8 /* CrashReporterTests.swift in Sources */,
6172472725D673D7007085B3 /* CrashContextTests.swift in Sources */,
Expand Down
138 changes: 138 additions & 0 deletions Sources/Datadog/Core/System/AppStateListener.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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 class UIKit.UIApplication

/// A data structure to represent recorded app states in a given period of time
internal struct AppStateHistory: Equatable {
/// Snapshot of the app state at `date`
struct Snapshot: Equatable {
let isActive: Bool
let date: Date
}

var initialState: Snapshot
var changes = [Snapshot]()
var finalDate: Date
var finalState: Snapshot {
return Snapshot(
isActive: (changes.last ?? initialState).isActive,
date: finalDate
)
}

/// Limits or extrapolates app state history to the given range
/// This is useful when you record between 0...3t but you are concerned of t...2t only
/// - Parameter range: if outside of `initialState` and `finalState`, it extrapolates; otherwise it limits
/// - Returns: a history instance spanning the given range
func take(between range: ClosedRange<Date>) -> AppStateHistory {
var taken = self
// move initial state to lowerBound
taken.initialState = Snapshot(
isActive: isActive(at: range.lowerBound),
date: range.lowerBound
)
// move final state to upperBound
taken.finalDate = range.upperBound
// filter changes outside of the range
taken.changes = taken.changes.filter { range.contains($0.date) }
return taken
}

var foregroundDuration: TimeInterval {
var duration: TimeInterval = 0.0
var lastActiveStartDate: Date?
let allEvents = [initialState] + changes + [finalState]
for event in allEvents {
if let startDate = lastActiveStartDate {
duration += event.date.timeIntervalSince(startDate)
}
if event.isActive {
lastActiveStartDate = event.date
} else {
lastActiveStartDate = nil
}
}
return duration
}

var didRunInBackground: Bool {
return !initialState.isActive || !finalState.isActive
}

private func isActive(at date: Date) -> Bool {
if date <= initialState.date {
// we assume there was no change before initial state
return initialState.isActive
} else if finalState.date <= date {
// and no change after final state
return finalState.isActive
}
var active = initialState
for change in changes {
if date < change.date {
break
}
active = change
}
return active.isActive
}
}

internal protocol AppStateListening: class {
var history: AppStateHistory { get }
}

internal class AppStateListener: AppStateListening {
typealias Snapshot = AppStateHistory.Snapshot

private let dateProvider: DateProvider
private let publisher: ValuePublisher<AppStateHistory>

var history: AppStateHistory {
var current = publisher.currentValue
current.finalDate = dateProvider.currentDate()
return current
}

private static var isAppActive: Bool {
return UIApplication.managedShared?.applicationState == .active
}

init(dateProvider: DateProvider) {
self.dateProvider = dateProvider
let currentState = Snapshot(
isActive: AppStateListener.isAppActive,
date: dateProvider.currentDate()
)
self.publisher = ValuePublisher(
initialValue: AppStateHistory(
initialState: currentState,
finalDate: currentState.date
)
)

let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
nc.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
}

@objc
private func appWillResignActive() {
let now = dateProvider.currentDate()
var value = publisher.currentValue
value.changes.append(Snapshot(isActive: false, date: now))
publisher.publishAsync(value)
}
@objc
private func appDidBecomeActive() {
let now = dateProvider.currentDate()
var value = publisher.currentValue
value.changes.append(Snapshot(isActive: true, date: now))
publisher.publishAsync(value)
}
}
3 changes: 2 additions & 1 deletion Sources/Datadog/Datadog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ public class Datadog {
if let urlSessionAutoInstrumentationConfiguration = configuration.urlSessionAutoInstrumentation {
urlSessionAutoInstrumentation = URLSessionAutoInstrumentation(
configuration: urlSessionAutoInstrumentationConfiguration,
dateProvider: dateProvider
dateProvider: dateProvider,
appStateListener: AppStateListener(dateProvider: dateProvider)
)
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/Datadog/Tracer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public struct DDTags {
///
/// Expects `String` value set for a tag.
public static let resource = "resource.name"
/// Internal tag. `Integer` value. Measures elapsed time at app's foreground state in nanoseconds.
/// (duration - foregroundDuration) gives you the elapsed time while the app wasn't active (probably at background)
internal static let foregroundDuration = "foreground_duration"
/// Internal tag. `Bool` value.
/// `true` if span was started or ended while the app was not active, `false` otherwise.
internal static let isBackground = "is_background"

/// Those keys used to encode information received from the user through `OpenTracingLogFields`, `OpenTracingTagKeys` or custom fields.
/// Supported by Datadog platform.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
import Foundation

internal class URLSessionTracingHandler: URLSessionInterceptionHandler {
/// Listening to app state changes and use it to report `foreground_duration`
let appStateListener: AppStateListening

init(appStateListener: AppStateListening) {
self.appStateListener = appStateListener
}

// MARK: - URLSessionInterceptionHandler

func notify_taskInterceptionStarted(interception: TaskInterception) {
Expand Down Expand Up @@ -68,6 +75,11 @@ internal class URLSessionTracingHandler: URLSessionInterceptionHandler {
}
}
}
let appStateHistory = appStateListener.history.take(
between: resourceMetrics.fetch.start...resourceMetrics.fetch.end
)
span.setTag(key: DDTags.foregroundDuration, value: appStateHistory.foregroundDuration.toNanoseconds)
span.setTag(key: DDTags.isBackground, value: appStateHistory.didRunInBackground)

span.finish(at: resourceMetrics.fetch.end)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ internal class TaskInterception {
/// or when the task was created through `URLSession.dataTask(with:url)` on some iOS13+.
private(set) var spanContext: DDSpanContext?

init(request: URLRequest, isFirstParty: Bool) {
init(
request: URLRequest,
isFirstParty: Bool
) {
self.identifier = UUID()
self.request = request
self.isFirstPartyRequest = isFirstParty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,24 @@ public class URLSessionInterceptor: URLSessionInterceptorType {

convenience init(
configuration: FeaturesConfiguration.URLSessionAutoInstrumentation,
dateProvider: DateProvider
dateProvider: DateProvider,
appStateListener: AppStateListening
) {
let handler: URLSessionInterceptionHandler

if configuration.instrumentRUM {
handler = URLSessionRUMResourcesHandler(dateProvider: dateProvider)
} else {
handler = URLSessionTracingHandler()
handler = URLSessionTracingHandler(appStateListener: appStateListener)
}

self.init(configuration: configuration, handler: handler)
self.init(configuration: configuration, handler: handler, appStateListener: appStateListener)
}

init(
configuration: FeaturesConfiguration.URLSessionAutoInstrumentation,
handler: URLSessionInterceptionHandler
handler: URLSessionInterceptionHandler,
appStateListener: AppStateListening
) {
self.defaultFirstPartyURLsFilter = FirstPartyURLsFilter(hosts: configuration.userDefinedFirstPartyHosts)
self.internalURLsFilter = InternalURLsFilter(urls: configuration.sdkInternalURLs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ internal class URLSessionAutoInstrumentation {

init?(
configuration: FeaturesConfiguration.URLSessionAutoInstrumentation,
dateProvider: DateProvider
dateProvider: DateProvider,
appStateListener: AppStateListening
) {
do {
self.interceptor = URLSessionInterceptor(configuration: configuration, dateProvider: dateProvider)
self.interceptor = URLSessionInterceptor(
configuration: configuration,
dateProvider: dateProvider,
appStateListener: appStateListener
)
self.swizzler = try URLSessionSwizzler()
} catch {
consolePrint(
Expand Down
6 changes: 6 additions & 0 deletions Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,12 @@ class CarrierInfoProviderMock: CarrierInfoProviderType, WrappedCarrierInfoProvid
}
}

extension AppStateListener {
static func mockAny() -> AppStateListener {
return AppStateListener(dateProvider: SystemDateProvider())
}
}

extension EncodableValue {
static func mockAny() -> EncodableValue {
return EncodableValue(String.mockAny())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@
import XCTest
@testable import Datadog

private class MockAppStateListener: AppStateListening {
let history = AppStateHistory(
initialState: .init(isActive: true, date: .mockDecember15th2019At10AMUTC()),
finalDate: .mockDecember15th2019At10AMUTC() + 10
)
}

class URLSessionTracingHandlerTests: XCTestCase {
private let spanOutput = SpanOutputMock()
private let logOutput = LogOutputMock()
private let handler = URLSessionTracingHandler()
private let handler = URLSessionTracingHandler(appStateListener: MockAppStateListener())

override func setUp() {
Global.sharedTracer = Tracer.mockWith(
Expand Down Expand Up @@ -95,7 +102,7 @@ class URLSessionTracingHandlerTests: XCTestCase {
XCTAssertEqual(span.tags[OTTags.httpUrl]?.encodable.value as? String, request.url!.absoluteString)
XCTAssertEqual(span.tags[OTTags.httpMethod]?.encodable.value as? String, "POST")
XCTAssertEqual(span.tags[OTTags.httpStatusCode]?.encodable.value as? Int, 200)
XCTAssertEqual(span.tags.count, 3)
XCTAssertEqual(span.tags.count, 5)

let log = logOutput.recordedLog
XCTAssertNil(log)
Expand Down Expand Up @@ -138,7 +145,7 @@ class URLSessionTracingHandlerTests: XCTestCase {
"Error Domain=domain Code=123 \"network error\" UserInfo={NSLocalizedDescription=network error}"
)
XCTAssertEqual(span.tags[DDTags.errorMessage]?.encodable.value as? String, "network error")
XCTAssertEqual(span.tags.count, 5)
XCTAssertEqual(span.tags.count, 7)

let log = try XCTUnwrap(logOutput.recordedLog, "It should send error log")
XCTAssertEqual(log.status, .error)
Expand Down Expand Up @@ -201,7 +208,7 @@ class URLSessionTracingHandlerTests: XCTestCase {
span.tags[DDTags.errorStack]?.encodable.value as? String,
"Error Domain=HTTPURLResponse Code=404 \"404 not found\" UserInfo={NSLocalizedDescription=404 not found}"
)
XCTAssertEqual(span.tags.count, 6)
XCTAssertEqual(span.tags.count, 8)

let log = try XCTUnwrap(logOutput.recordedLog, "It should send error log")
XCTAssertEqual(log.status, .error)
Expand Down Expand Up @@ -269,4 +276,26 @@ class URLSessionTracingHandlerTests: XCTestCase {
XCTAssertNil(spanOutput.recordedSpan)
XCTAssertNil(logOutput.recordedLog)
}

func testGivenAnyInterception_itAddsAppStateInformationToSpan() throws {
// Given
let interception = TaskInterception(request: .mockAny(), isFirstParty: true)
interception.register(completion: .mockAny())
interception.register(
metrics: .mockWith(
fetch: .init(
start: .mockDecember15th2019At10AMUTC(),
end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 10)
)
)
)

// When
handler.notify_taskInterceptionCompleted(interception: interception)

// Then
let recordedSpan = try XCTUnwrap(spanOutput.recordedSpan)
XCTAssertEqual(recordedSpan.tags[DDTags.foregroundDuration]?.encodable.value as? UInt64, 10_000_000_000)
XCTAssertEqual(recordedSpan.tags[DDTags.isBackground]?.encodable.value as? Bool, false)
}
}
Loading

0 comments on commit 0eb6772

Please sign in to comment.