Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 2 additions & 10 deletions DatadogSessionReplay/Sources/Feature/RUMContextReceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,19 @@ internal protocol RUMContextObserver {
}

/// Receives RUM context from `DatadogCore` and notifies it through `RUMContextObserver` interface.
internal class RUMContextReceiver: FeatureMessageReceiver, RUMContextObserver {
internal final class RUMContextReceiver: BusMessageReceiver, RUMContextObserver {
/// Notifies new `RUMContext` or `nil` if current RUM session is not sampled.
private var onNew: ((RUMCoreContext?) -> Void)?
private var previous: RUMCoreContext?

// MARK: - FeatureMessageReceiver

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

func receive(message context: DatadogContext, from core: DatadogCoreProtocol) {
let new = context.additionalContext(ofType: RUMCoreContext.self)

// Notify only if it has changed:
if new != previous {
onNew?(new)
previous = new
}

return true
}

// MARK: - RUMContextObserver
Expand Down
11 changes: 8 additions & 3 deletions DatadogSessionReplay/Sources/Feature/RecordingComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import DatadogInternal
internal struct RecordingComponents {
let recordingCoordinator: any RecordingController
let messageReceiver: any FeatureMessageReceiver
let contextReceiver: RUMContextReceiver

init(
core: DatadogCoreProtocol,
Expand All @@ -30,10 +31,12 @@ internal struct RecordingComponents {

private init(
recordingCoordinator: any RecordingController,
messageReceiver: any FeatureMessageReceiver
messageReceiver: any FeatureMessageReceiver,
contextReceiver: RUMContextReceiver
) {
self.recordingCoordinator = recordingCoordinator
self.messageReceiver = messageReceiver
self.contextReceiver = contextReceiver
}

private static func viewTreeRecordingComponents(
Expand Down Expand Up @@ -90,7 +93,8 @@ internal struct RecordingComponents {

return .init(
recordingCoordinator: recordingCoordinator,
messageReceiver: contextReceiver
messageReceiver: NOPFeatureMessageReceiver(),
contextReceiver: contextReceiver
)
}

Expand Down Expand Up @@ -128,7 +132,8 @@ internal struct RecordingComponents {

return .init(
recordingCoordinator: recordingCoordinator,
messageReceiver: recordingCoordinator
messageReceiver: recordingCoordinator,
contextReceiver: RUMContextReceiver()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import DatadogInternal
internal class SessionReplayFeature: SessionReplayConfiguration, DatadogRemoteFeature {
let requestBuilder: FeatureRequestBuilder
let messageReceiver: FeatureMessageReceiver
let contextReceiver: RUMContextReceiver
let webViewRecordReceiver: WebViewRecordReceiver
let performanceOverride: PerformancePresetOverride?
let textAndInputPrivacyLevel: TextAndInputPrivacyLevel
let imagePrivacyLevel: ImagePrivacyLevel
Expand All @@ -32,16 +34,15 @@ internal class SessionReplayFeature: SessionReplayConfiguration, DatadogRemoteFe
configuration: configuration
)

self.contextReceiver = recordingComponents.contextReceiver
self.webViewRecordReceiver = WebViewRecordReceiver(
scope: core.scope(for: SessionReplayFeature.self)
)
self.requestBuilder = SegmentRequestBuilder(
customUploadURL: configuration.customEndpoint,
telemetry: core.telemetry
)
self.messageReceiver = CombinedFeatureMessageReceiver(
[
recordingComponents.messageReceiver,
WebViewRecordReceiver(scope: core.scope(for: SessionReplayFeature.self))
]
)
self.messageReceiver = recordingComponents.messageReceiver
self.performanceOverride = PerformancePresetOverride(
maxFileSize: SessionReplay.maxObjectSize,
maxObjectSize: SessionReplay.maxObjectSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import Foundation
import DatadogInternal

internal struct WebViewRecordReceiver: FeatureMessageReceiver {
internal final class WebViewRecordReceiver: BusMessageReceiver {
internal struct WebRecord: Encodable {
/// The RUM application ID of all records.
let applicationID: String
Expand All @@ -22,10 +22,13 @@ internal struct WebViewRecordReceiver: FeatureMessageReceiver {
/// Session Replay feature scope.
let scope: FeatureScope

func receive(message: DatadogInternal.FeatureMessage, from core: DatadogInternal.DatadogCoreProtocol) -> Bool {
guard case let .webview(.record(event, view)) = message else {
return false
}
init(scope: FeatureScope) {
self.scope = scope
}

func receive(message: WebViewRecordMessage, from core: DatadogCoreProtocol) {
let event = message.event
let view = message.view

scope.eventWriteContext { context, writer in
// Extract the `RUMContext` or `nil` if RUM session is not sampled:
Expand Down Expand Up @@ -53,7 +56,5 @@ internal struct WebViewRecordReceiver: FeatureMessageReceiver {

writer.write(value: record)
}

return true
}
}
5 changes: 5 additions & 0 deletions DatadogSessionReplay/Sources/SessionReplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ public enum SessionReplay {
try core.register(feature: resources)

let sessionReplay = try SessionReplayFeature(core: core, configuration: configuration)

// Subscribe typed-bus receivers before registration so initial context push is received:
core.messageBus.subscribe(receiver: sessionReplay.contextReceiver)
core.messageBus.subscribe(receiver: sessionReplay.webViewRecordReceiver)

try core.register(feature: sessionReplay)
core.set(
context: SessionReplayCoreContext.Configuration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ class RUMContextReceiverTests: XCTestCase {
}

// When
XCTAssert(
receiver.receive(message: .context(coreContext), from: core)
)
receiver.receive(message: coreContext, from: core)

// Then
XCTAssertEqual(rumContext?.applicationID, "app-id")
Expand Down Expand Up @@ -78,13 +76,8 @@ class RUMContextReceiverTests: XCTestCase {
context.flatMap { rumContexts.append($0) }
}
// When
XCTAssert(
receiver.receive(message: .context(coreContext1), from: core)
)

XCTAssert(
receiver.receive(message: .context(coreContext2), from: core)
)
receiver.receive(message: coreContext1, from: core)
receiver.receive(message: coreContext2, from: core)

// Then
XCTAssertEqual(rumContexts.count, 2)
Expand Down Expand Up @@ -130,13 +123,8 @@ class RUMContextReceiverTests: XCTestCase {
context.flatMap { rumContexts.append($0) }
}
// When
XCTAssert(
receiver.receive(message: .context(coreContext1), from: core)
)

XCTAssert(
receiver.receive(message: .context(coreContext2), from: core)
)
receiver.receive(message: coreContext1, from: core)
receiver.receive(message: coreContext2, from: core)

// Then
XCTAssertEqual(rumContexts.count, 1)
Expand Down Expand Up @@ -169,39 +157,29 @@ class RUMContextReceiverTests: XCTestCase {
}

// When
XCTAssert(
receiver.receive(message: .context(coreContext1), from: core)
)
receiver.receive(message: coreContext1, from: core)

XCTAssertEqual(rumContext?.applicationID, "app-id")
XCTAssertEqual(rumContext?.sessionID, "session-id")
XCTAssertEqual(rumContext?.viewID, "view-id")
XCTAssertEqual(rumContext?.viewServerTimeOffset, 123)

// When
XCTAssert(
receiver.receive(message: .context(coreContext2), from: core)
)
receiver.receive(message: coreContext2, from: core)
// Then
XCTAssertNil(rumContext)
}

func testWhenMessageIsNotContext_itReturnsFalse() throws {
func testWhenNoMessageIsReceived_observerIsNotCalled() throws {
// Given
let expectation = expectation(description: "observe not called")
expectation.isInverted = true
let core = PassthroughCoreMock()

receiver.observe(on: NoQueue()) { _ in
expectation.fulfill()
}

// When
XCTAssertFalse(
receiver.receive(message: .payload("value"), from: core)
)

// Then
// Then — no message delivered, observer must stay silent
waitForExpectations(timeout: 0.1)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class WebViewRecordReceiverTests: XCTestCase {

// When

let message = WebViewMessage.record(webRecordMock, WebViewMessage.View(id: browserViewID))
let result = receiver.receive(message: .webview(message), from: NOPDatadogCore())
let message = WebViewRecordMessage(event: webRecordMock, view: WebViewMessage.View(id: browserViewID))
receiver.receive(message: message, from: NOPDatadogCore())

// Then
let expectedWebSegmentWritten: [String: Any] = [
Expand All @@ -58,7 +58,6 @@ class WebViewRecordReceiverTests: XCTestCase {
]
]

XCTAssertTrue(result, "It must accept the message")
XCTAssertEqual(scope.eventsWritten.count, 1, "It must write web segment to core")
let actualWebEventWritten = try XCTUnwrap(scope.eventsWritten.first)
DDAssertJSONEqual(AnyCodable(actualWebEventWritten), AnyCodable(expectedWebSegmentWritten))
Expand All @@ -73,27 +72,12 @@ class WebViewRecordReceiverTests: XCTestCase {
let receiver = WebViewRecordReceiver(scope: scope)

// When
let record = WebViewMessage.record(mockRandomAttributes(), WebViewMessage.View(id: .mockRandom()))
let result = receiver.receive(message: .webview(record), from: NOPDatadogCore())
let message = WebViewRecordMessage(event: mockRandomAttributes(), view: WebViewMessage.View(id: .mockRandom()))
receiver.receive(message: message, from: NOPDatadogCore())

// Then
XCTAssertTrue(result, "It must accept the message")
XCTAssertTrue(scope.eventsWritten.isEmpty, "The event must be dropped")
}

func testWhenReceivingOtherMessage_itRejectsIt() throws {
let scope = FeatureScopeMock()

// Given
let receiver = WebViewRecordReceiver(scope: scope)

// When
let otherMessage: FeatureMessage = .payload(String.mockRandom())
let result = receiver.receive(message: otherMessage, from: NOPDatadogCore())

// Then
XCTAssertFalse(result, "It must reject messages addressed to other receivers")
}
}

#endif
14 changes: 8 additions & 6 deletions DatadogSessionReplay/Tests/SessionReplayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@ class SessionReplayTests: XCTestCase {
let textAndInputPrivacyLevel: TextAndInputPrivacyLevel = .mockRandom()
let imagePrivacyLevel: ImagePrivacyLevel = .mockRandom()
let touchPrivacyLevel: TouchPrivacyLevel = .mockRandom()
let messageReceiver = FeatureMessageReceiverMock()
let core = PassthroughCoreMock(messageReceiver: messageReceiver)
let telemetryReceiver = TelemetryReceiverMock()
let core = PassthroughCoreMock()
core.subscribe(receiver: telemetryReceiver)

// When
SessionReplay.enable(
Expand All @@ -232,7 +233,7 @@ class SessionReplayTests: XCTestCase {
)

// Then
let configuration = try XCTUnwrap(messageReceiver.messages.firstTelemetry?.asConfiguration)
let configuration = try XCTUnwrap(telemetryReceiver.messages.firstConfiguration())
XCTAssertEqual(configuration.sessionReplaySampleRate, sampleRate)
XCTAssertNil(configuration.defaultPrivacyLevel)
XCTAssertEqual(configuration.textAndInputPrivacyLevel, textAndInputPrivacyLevel.rawValue)
Expand All @@ -248,8 +249,9 @@ class SessionReplayTests: XCTestCase {
let imageLevel: ImagePrivacyLevel = .mockRandom()
let touchLevel: TouchPrivacyLevel = .mockRandom()
let startRecordingImmediately: Bool = .mockRandom()
let messageReceiver = FeatureMessageReceiverMock()
let core = PassthroughCoreMock(messageReceiver: messageReceiver)
let telemetryReceiver = TelemetryReceiverMock()
let core = PassthroughCoreMock()
core.subscribe(receiver: telemetryReceiver)

// When
SessionReplay.enable(
Expand All @@ -264,7 +266,7 @@ class SessionReplayTests: XCTestCase {
)

// Then
let configuration = try XCTUnwrap(messageReceiver.messages.firstTelemetry?.asConfiguration)
let configuration = try XCTUnwrap(telemetryReceiver.messages.firstConfiguration())
XCTAssertEqual(configuration.sessionReplaySampleRate, sampleRate)
XCTAssertNil(configuration.defaultPrivacyLevel)
XCTAssertEqual(configuration.textAndInputPrivacyLevel, textAndInputLevel.rawValue)
Expand Down
22 changes: 17 additions & 5 deletions DatadogWebViewTracking/Sources/MessageEmitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,35 @@ internal final class MessageEmitter: InternalExtension<WebViewTracking>.Abstract
return
}

core.send(message: .webview(message), else: {
guard case let .log(event) = message else {
return
}
core.messageBus.send(message: WebViewLogMessage(event: event), else: {
DD.logger.warn("A WebView log is lost because Logging is disabled in the SDK")
})
}

private func send(rum message: WebViewMessage, in core: DatadogCoreProtocol) {
core.send(message: .webview(message), else: {
DD.logger.warn("A WebView RUM event is lost because RUM is disabled in the SDK")
})
switch message {
case let .rum(event):
core.messageBus.send(message: WebViewRUMMessage(kind: .rum, event: event), else: {
DD.logger.warn("A WebView RUM event is lost because RUM is disabled in the SDK")
})
case let .telemetry(event):
core.messageBus.send(message: WebViewRUMMessage(kind: .telemetry, event: event), else: {
DD.logger.warn("A WebView RUM telemetry event is lost because RUM is disabled in the SDK")
})
default:
break
}
}

private func send(record event: WebViewMessage.Event, view: WebViewMessage.View, slotId: String?, in core: DatadogCoreProtocol) {
var event = event
// inject the slotId
event["slotId"] = slotId

core.send(message: .webview(.record(event, view)), else: {
core.messageBus.send(message: WebViewRecordMessage(event: event, view: view), else: {
DD.logger.warn("A WebView Replay record is lost because Session Replay is disabled in the SDK")
})
}
Expand Down
Loading