Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow manually tracked resources to detect first party hosts #837

Merged
merged 5 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Changes

* [IMPROVEMENT] Allow manually tracked resources in RUM Sessions to detect first party hosts.

# 1.11.0-beta2 / 05-04-2022

### Changes
Expand Down
19 changes: 13 additions & 6 deletions Sources/Datadog/Core/FeaturesConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ internal struct FeaturesConfiguration {
let instrumentation: Instrumentation?
let backgroundEventTrackingEnabled: Bool
let onSessionStart: RUMSessionListener?
let firstPartyHosts: Set<String>
}

struct URLSessionAutoInstrumentation {
Expand Down Expand Up @@ -184,6 +185,14 @@ extension FeaturesConfiguration {
)
}

var sanitizedHosts: Set<String> = []
if let firstPartyHosts = configuration.firstPartyHosts {
sanitizedHosts = hostsSanitizer.sanitized(
hosts: firstPartyHosts,
warningMessage: "The first party host configured for Datadog SDK is not valid"
)
}

if configuration.rumEnabled {
let instrumentation = RUM.Instrumentation(
uiKitRUMViewsPredicate: configuration.rumUIKitViewsPredicate,
Expand All @@ -207,7 +216,8 @@ extension FeaturesConfiguration {
longTaskEventMapper: configuration.rumLongTaskEventMapper,
instrumentation: instrumentation,
backgroundEventTrackingEnabled: configuration.rumBackgroundEventTrackingEnabled,
onSessionStart: configuration.rumSessionsListener
onSessionStart: configuration.rumSessionsListener,
firstPartyHosts: sanitizedHosts
)
} else {
let error = ProgrammerError(
Expand All @@ -220,13 +230,10 @@ extension FeaturesConfiguration {
}
}

if let firstPartyHosts = configuration.firstPartyHosts {
if configuration.firstPartyHosts != nil {
if configuration.tracingEnabled || configuration.rumEnabled {
urlSessionAutoInstrumentation = URLSessionAutoInstrumentation(
userDefinedFirstPartyHosts: hostsSanitizer.sanitized(
hosts: firstPartyHosts,
warningMessage: "The first party host configured for Datadog SDK is not valid"
),
userDefinedFirstPartyHosts: sanitizedHosts,
sdkInternalURLs: [
logsEndpoint.url,
tracesEndpoint.url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ internal class URLSessionRUMResourcesHandler: URLSessionInterceptionHandler, RUM
url: url,
httpMethod: RUMMethod(httpMethod: interception.request.httpMethod),
kind: RUMResourceType(request: interception.request),
isFirstPartyRequest: interception.isFirstPartyRequest,
spanContext: interception.spanContext.flatMap { spanContext in
.init(
traceID: String(spanContext.traceID.rawValue),
Expand Down
2 changes: 0 additions & 2 deletions Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,6 @@ internal struct RUMStartResourceCommand: RUMResourceCommand {
let httpMethod: RUMMethod
/// A type of the Resource if it's possible to determine on start (when the response MIME is not yet known).
let kind: RUMResourceType?
/// Whether or not the resource url targets a first party host, if that information is available.
let isFirstPartyRequest: Bool?
/// Span context passed to the RUM backend in order to generate the APM span for underlying resource.
let spanContext: RUMSpanContext?
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal class RUMResourceScope: RUMScope {
/// The HTTP method used to load this Resource.
private var resourceHTTPMethod: RUMMethod
/// Whether or not the Resource is provided by a first party host, if that information is available.
private let isFirstPartyResource: Bool?
private let isFirstPartyResource: Bool
/// The Resource kind captured when starting the `URLRequest`.
/// It may be `nil` if it's not possible to predict the kind from resource and the response MIME type is needed.
private var resourceKindBasedOnRequest: RUMResourceType?
Expand All @@ -54,7 +54,6 @@ internal class RUMResourceScope: RUMScope {
dateCorrection: DateCorrection,
url: String,
httpMethod: RUMMethod,
isFirstPartyResource: Bool?,
resourceKindBasedOnRequest: RUMResourceType?,
spanContext: RUMSpanContext?,
onResourceEventSent: @escaping () -> Void,
Expand All @@ -69,7 +68,7 @@ internal class RUMResourceScope: RUMScope {
self.resourceLoadingStartTime = startTime
self.dateCorrection = dateCorrection
self.resourceHTTPMethod = httpMethod
self.isFirstPartyResource = isFirstPartyResource
self.isFirstPartyResource = dependencies.firstPartyURLsFilter.isFirstParty(string: url)
self.resourceKindBasedOnRequest = resourceKindBasedOnRequest
self.spanContext = spanContext
self.onResourceEventSent = onResourceEventSent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal struct RUMScopeDependencies {
let applicationVersion: String
let sdkVersion: String
let source: String
let firstPartyURLsFilter: FirstPartyURLsFilter
let eventBuilder: RUMEventBuilder
let eventOutput: RUMEventOutput
let rumUUIDGenerator: RUMUUIDGenerator
Expand Down Expand Up @@ -61,6 +62,7 @@ internal extension RUMScopeDependencies {
applicationVersion: rumFeature.configuration.common.applicationVersion,
sdkVersion: rumFeature.configuration.common.sdkVersion,
source: rumFeature.configuration.common.source,
firstPartyURLsFilter: FirstPartyURLsFilter(hosts: rumFeature.configuration.firstPartyHosts),
eventBuilder: RUMEventBuilder(
eventsMapper: rumFeature.eventsMapper
),
Expand Down
1 change: 0 additions & 1 deletion Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider {
dateCorrection: dateCorrection,
url: command.url,
httpMethod: command.httpMethod,
isFirstPartyResource: command.isFirstPartyRequest,
resourceKindBasedOnRequest: command.kind,
spanContext: command.spanContext,
onResourceEventSent: { [weak self] in
Expand Down
3 changes: 0 additions & 3 deletions Sources/Datadog/RUMMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,6 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber {
url: request.url?.absoluteString ?? "unknown_url",
httpMethod: RUMMethod(httpMethod: request.httpMethod),
kind: RUMResourceType(request: request),
isFirstPartyRequest: nil,
spanContext: nil
)
)
Expand All @@ -368,7 +367,6 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber {
url: url.absoluteString,
httpMethod: .get,
kind: nil,
isFirstPartyRequest: nil,
spanContext: nil
)
)
Expand All @@ -388,7 +386,6 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber {
url: urlString,
httpMethod: httpMethod,
kind: nil,
isFirstPartyRequest: nil,
spanContext: nil
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ internal struct FirstPartyURLsFilter {
}
return host.range(of: regex, options: .regularExpression) != nil
}

// Returns `true` if given `String` can be parsed as a URL and matches the first
// party hosts defined by the user; `false` otherwise
func isFirstParty(string: String) -> Bool {
guard let url = URL(string: string),
let regex = self.regex,
let host = url.host else {
return false
}
return host.range(of: regex, options: .regularExpression) != nil
}
}
6 changes: 4 additions & 2 deletions Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ extension FeaturesConfiguration.RUM {
longTaskEventMapper: RUMLongTaskEventMapper? = nil,
instrumentation: FeaturesConfiguration.RUM.Instrumentation? = nil,
backgroundEventTrackingEnabled: Bool = false,
onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListener()
onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListener(),
firstPartyHosts: Set<String> = []
) -> Self {
return .init(
common: common,
Expand All @@ -285,7 +286,8 @@ extension FeaturesConfiguration.RUM {
longTaskEventMapper: longTaskEventMapper,
instrumentation: instrumentation,
backgroundEventTrackingEnabled: backgroundEventTrackingEnabled,
onSessionStart: onSessionStart
onSessionStart: onSessionStart,
firstPartyHosts: firstPartyHosts
)
}
}
Expand Down
6 changes: 4 additions & 2 deletions Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,6 @@ extension RUMStartResourceCommand: AnyMockable, RandomMockable {
url: url,
httpMethod: httpMethod,
kind: kind,
isFirstPartyRequest: isFirstPartyRequest,
spanContext: spanContext
)
}
Expand Down Expand Up @@ -664,6 +663,7 @@ extension RUMScopeDependencies {
applicationVersion: String = .mockAny(),
sdkVersion: String = .mockAny(),
source: String = "ios",
firstPartyURLsFilter: FirstPartyURLsFilter = FirstPartyURLsFilter(hosts: []),
eventBuilder: RUMEventBuilder = RUMEventBuilder(eventsMapper: .mockNoOp()),
eventOutput: RUMEventOutput = RUMEventOutputMock(),
rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(),
Expand All @@ -686,6 +686,7 @@ extension RUMScopeDependencies {
applicationVersion: applicationVersion,
sdkVersion: sdkVersion,
source: source,
firstPartyURLsFilter: firstPartyURLsFilter,
eventBuilder: eventBuilder,
eventOutput: eventOutput,
rumUUIDGenerator: rumUUIDGenerator,
Expand Down Expand Up @@ -714,6 +715,7 @@ extension RUMScopeDependencies {
applicationVersion: String? = nil,
sdkVersion: String? = nil,
source: String? = nil,
firstPartyUrls: Set<String>? = nil,
eventBuilder: RUMEventBuilder? = nil,
eventOutput: RUMEventOutput? = nil,
rumUUIDGenerator: RUMUUIDGenerator? = nil,
Expand All @@ -736,6 +738,7 @@ extension RUMScopeDependencies {
applicationVersion: applicationVersion ?? self.applicationVersion,
sdkVersion: sdkVersion ?? self.sdkVersion,
source: source ?? self.source,
firstPartyURLsFilter: firstPartyUrls != nil ? FirstPartyURLsFilter(hosts: firstPartyUrls!) : self.firstPartyURLsFilter,
fuzzybinary marked this conversation as resolved.
Show resolved Hide resolved
eventBuilder: eventBuilder ?? self.eventBuilder,
eventOutput: eventOutput ?? self.eventOutput,
rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator,
Expand Down Expand Up @@ -868,7 +871,6 @@ extension RUMResourceScope {
dateCorrection: dateCorrection,
url: url,
httpMethod: httpMethod,
isFirstPartyResource: isFirstPartyResource,
resourceKindBasedOnRequest: resourceKindBasedOnRequest,
spanContext: spanContext,
onResourceEventSent: onResourceEventSent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,40 +45,6 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase {
XCTAssertNil(resourceStartCommand.spanContext)
}

func testGivenTaskInterceptionForFirstPartyHost_whenInterceptionStarts_itStartsRUMResourceForFirstPartyHost() throws {
let receiveCommand = expectation(description: "Receive RUM command")
commandSubscriber.onCommandReceived = { _ in receiveCommand.fulfill() }

// Given
let taskInterception = TaskInterception(request: .mockAny(), isFirstParty: true)

// When
handler.notify_taskInterceptionStarted(interception: taskInterception)

// Then
waitForExpectations(timeout: 0.5, handler: nil)

let resourceStartCommand = try XCTUnwrap(commandSubscriber.lastReceivedCommand as? RUMStartResourceCommand)
XCTAssertTrue(resourceStartCommand.isFirstPartyRequest!)
}

func testGivenTaskInterceptionForThirdPartyHost_whenInterceptionStarts_itStartsRUMResourceForThirdPartyHost() throws {
let receiveCommand = expectation(description: "Receive RUM command")
commandSubscriber.onCommandReceived = { _ in receiveCommand.fulfill() }

// Given
let taskInterception = TaskInterception(request: .mockAny(), isFirstParty: false)

// When
handler.notify_taskInterceptionStarted(interception: taskInterception)

// Then
waitForExpectations(timeout: 0.5, handler: nil)

let resourceStartCommand = try XCTUnwrap(commandSubscriber.lastReceivedCommand as? RUMStartResourceCommand)
XCTAssertFalse(resourceStartCommand.isFirstPartyRequest!)
}

func testGivenTaskInterceptionWithSpanContext_whenInterceptionStarts_itStartsRUMResource() throws {
let receiveCommand = expectation(description: "Receive RUM command")
commandSubscriber.onCommandReceived = { _ in receiveCommand.fulfill() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class RUMResourceScopeTests: XCTestCase {
private let randomServiceName: String = .mockRandom()
private lazy var dependencies: RUMScopeDependencies = .mockWith(
serviceName: randomServiceName,
firstPartyURLsFilter: FirstPartyURLsFilter(hosts: ["firstparty.com"]),
eventOutput: output
)
private let context = RUMContext.mockWith(
Expand Down Expand Up @@ -53,7 +54,6 @@ class RUMResourceScopeTests: XCTestCase {
dateCorrection: .zero,
url: "https://foo.com/resource/1",
httpMethod: .post,
isFirstPartyResource: nil,
resourceKindBasedOnRequest: nil,
spanContext: .init(traceID: "100", spanID: "200")
)
Expand Down Expand Up @@ -560,7 +560,7 @@ class RUMResourceScopeTests: XCTestCase {
attributes: [:],
startTime: currentTime,
dateCorrection: .zero,
url: "https://foo.com/resource/1",
url: "https://firstparty.com/resource/1",
httpMethod: .post,
isFirstPartyResource: true,
resourceKindBasedOnRequest: nil,
Expand All @@ -581,7 +581,7 @@ class RUMResourceScopeTests: XCTestCase {
let providerType = try XCTUnwrap(event.resource.provider?.type)
let providerDomain = try XCTUnwrap(event.resource.provider?.domain)
XCTAssertEqual(providerType, .firstParty)
XCTAssertEqual(providerDomain, "foo.com")
XCTAssertEqual(providerDomain, "firstparty.com")
}

func testGivenStartedThirdartyResource_whenResourceLoadingEnds_itSendsResourceEventWithoutResourceProvider() throws {
Expand Down Expand Up @@ -627,7 +627,7 @@ class RUMResourceScopeTests: XCTestCase {
attributes: [:],
startTime: currentTime,
dateCorrection: .zero,
url: "https://foo.com/resource/1",
url: "https://firstparty.com/resource/1",
httpMethod: .post,
isFirstPartyResource: true
)
Expand All @@ -646,7 +646,7 @@ class RUMResourceScopeTests: XCTestCase {
let providerType = try XCTUnwrap(event.error.resource?.provider?.type)
let providerDomain = try XCTUnwrap(event.error.resource?.provider?.domain)
XCTAssertEqual(providerType, .firstParty)
XCTAssertEqual(providerDomain, "foo.com")
XCTAssertEqual(providerDomain, "firstparty.com")
XCTAssertEqual(event.error.sourceType, .ios)
}

Expand Down
52 changes: 52 additions & 0 deletions Tests/DatadogTests/Datadog/RUMMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ class RUMMonitorTests: XCTestCase {
let resourceEvent = session.viewVisits[0].resourceEvents[0]
XCTAssertEqual(resourceEvent.resource.url, url.absoluteString)
XCTAssertEqual(resourceEvent.resource.statusCode, 200)
XCTAssertNil(resourceEvent.resource.provider?.type)
}

func testStartingView_thenLoadingResourceWithURLString() throws {
Expand All @@ -284,6 +285,57 @@ class RUMMonitorTests: XCTestCase {
XCTAssertEqual(resourceEvent.resource.statusCode, 333)
XCTAssertEqual(resourceEvent.resource.type, .beacon)
XCTAssertEqual(resourceEvent.resource.method, .post)
XCTAssertNil(resourceEvent.resource.provider?.type)
}

func testLoadingResourceWithURL_thenMarksFirstPartyURLs() throws {
RUMFeature.instance = .mockByRecordingRUMEventMatchers(
directories: temporaryFeatureDirectories,
configuration: .mockWith(
// .mockRandom always uses foo.com
firstPartyHosts: ["foo.com"]
)
)
defer { RUMFeature.instance?.deinitialize() }

let monitor = try createTestableRUMMonitor()
setGlobalAttributes(of: monitor)

let url: URL = .mockRandom()
monitor.startView(viewController: mockView)
monitor.startResourceLoading(resourceKey: "/resource/1", url: url)
monitor.stopResourceLoading(resourceKey: "/resource/1", response: .mockWith(statusCode: 200, mimeType: "image/png"))

let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 4)
verifyGlobalAttributes(in: rumEventMatchers)

let session = try XCTUnwrap(try RUMSessionMatcher.groupMatchersBySessions(rumEventMatchers).first)
let resourceEvent = session.viewVisits[0].resourceEvents[0]
XCTAssertEqual(resourceEvent.resource.provider?.type, RUMResourceEvent.Resource.Provider.ProviderType.firstParty)
}

func testLoadingResourceWithURLString_thenMarksFirstPartyURLs() throws {
RUMFeature.instance = .mockByRecordingRUMEventMatchers(
directories: temporaryFeatureDirectories,
configuration: .mockWith(
firstPartyHosts: ["foo.com"]
)
)
defer { RUMFeature.instance?.deinitialize() }

let monitor = try createTestableRUMMonitor()
setGlobalAttributes(of: monitor)

monitor.startView(viewController: mockView)
monitor.startResourceLoading(resourceKey: "/resource/1", httpMethod: .post, urlString: "http://www.foo.com/some/url/string", attributes: [:])
monitor.stopResourceLoading(resourceKey: "/resource/1", statusCode: 333, kind: .beacon)

let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 4)
verifyGlobalAttributes(in: rumEventMatchers)

let session = try XCTUnwrap(try RUMSessionMatcher.groupMatchersBySessions(rumEventMatchers).first)
let resourceEvent = session.viewVisits[0].resourceEvents[0]
XCTAssertEqual(resourceEvent.resource.provider?.type, RUMResourceEvent.Resource.Provider.ProviderType.firstParty)
}

func testStartingView_thenTappingButton() throws {
Expand Down