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

RUMM-830 Add consent provider #328

Merged
merged 2 commits into from
Dec 7, 2020
Merged
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
8 changes: 8 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
6114FE0F257667D40084E372 /* ConsentAwareDataWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6114FE0E257667D40084E372 /* ConsentAwareDataWriter.swift */; };
6114FE1625766B310084E372 /* TrackingConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6114FE1525766B310084E372 /* TrackingConsent.swift */; };
6114FE23257671F00084E372 /* ConsentAwareDataWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6114FE22257671F00084E372 /* ConsentAwareDataWriterTests.swift */; };
6114FE2F257687310084E372 /* ConsentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6114FE2E257687300084E372 /* ConsentProvider.swift */; };
6114FE3B25768AA90084E372 /* ConsentProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6114FE3A25768AA90084E372 /* ConsentProviderTests.swift */; };
61163C37252DDD60007DD5BF /* RUMMVSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61163C36252DDD60007DD5BF /* RUMMVSViewController.swift */; };
61163C3E252E0015007DD5BF /* RUMMVSModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61163C3D252E0015007DD5BF /* RUMMVSModalViewController.swift */; };
61163C4A252E03D6007DD5BF /* RUMModalViewsScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61163C49252E03D6007DD5BF /* RUMModalViewsScenarioTests.swift */; };
Expand Down Expand Up @@ -485,6 +487,8 @@
6114FE0E257667D40084E372 /* ConsentAwareDataWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentAwareDataWriter.swift; sourceTree = "<group>"; };
6114FE1525766B310084E372 /* TrackingConsent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingConsent.swift; sourceTree = "<group>"; };
6114FE22257671F00084E372 /* ConsentAwareDataWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentAwareDataWriterTests.swift; sourceTree = "<group>"; };
6114FE2E257687300084E372 /* ConsentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentProvider.swift; sourceTree = "<group>"; };
6114FE3A25768AA90084E372 /* ConsentProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentProviderTests.swift; sourceTree = "<group>"; };
61163C36252DDD60007DD5BF /* RUMMVSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMVSViewController.swift; sourceTree = "<group>"; };
61163C3D252E0015007DD5BF /* RUMMVSModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMVSModalViewController.swift; sourceTree = "<group>"; };
61163C49252E03D6007DD5BF /* RUMModalViewsScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMModalViewsScenarioTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1250,6 +1254,7 @@
isa = PBXGroup;
children = (
6114FE1525766B310084E372 /* TrackingConsent.swift */,
6114FE2E257687300084E372 /* ConsentProvider.swift */,
6114FE0E257667D40084E372 /* ConsentAwareDataWriter.swift */,
);
path = Privacy;
Expand All @@ -1259,6 +1264,7 @@
isa = PBXGroup;
children = (
6114FE22257671F00084E372 /* ConsentAwareDataWriterTests.swift */,
6114FE3A25768AA90084E372 /* ConsentProviderTests.swift */,
);
path = Privacy;
sourceTree = "<group>";
Expand Down Expand Up @@ -2514,6 +2520,7 @@
9E58E8E124615C75008E5063 /* JSONEncoder.swift in Sources */,
61133BCA2423979B00786299 /* EncodableValue.swift in Sources */,
61BBD19524ED4E9E0023E65F /* FeaturesConfiguration.swift in Sources */,
6114FE2F257687310084E372 /* ConsentProvider.swift in Sources */,
61F3CDA52511190E00C816E5 /* UIViewControllerSwizzler.swift in Sources */,
6156CB9024DDA8BE008CB2B2 /* RUMCurrentContext.swift in Sources */,
614B0A4F24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift in Sources */,
Expand Down Expand Up @@ -2713,6 +2720,7 @@
61133C652423990D00786299 /* LogBuilderTests.swift in Sources */,
61F8CC092469295500FE2908 /* DatadogConfigurationBuilderTests.swift in Sources */,
61F1A623249B811200075390 /* Encoding.swift in Sources */,
6114FE3B25768AA90084E372 /* ConsentProviderTests.swift in Sources */,
61133C642423990D00786299 /* LoggerTests.swift in Sources */,
617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */,
61786F7724FCDE05009E6BAB /* RUMDebuggingTests.swift in Sources */,
Expand Down
5 changes: 4 additions & 1 deletion Sources/Datadog/Core/Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ internal struct FeatureStorage {
dateProvider: commonDependencies.dateProvider
)

// TODO: RUMM-833 Share consent provider among all features
let consentProvider = ConsentProvider(initialConsent: .granted)

let consentAwareDataWriter = ConsentAwareDataWriter(
initialConsent: .granted, // TODO: RUMM-830 Inject `ConsentProvider`
consentProvider: consentProvider,
queue: readWriteQueue,
unauthorizedFileWriter: FileWriter(
dataFormat: dataFormat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import Foundation

/// File writer which writes data to different folders depending on the tracking consent value.
internal class ConsentAwareDataWriter: FileWriterType {
internal class ConsentAwareDataWriter: FileWriterType, ConsentSubscriber {
/// Queue used to synchronize reads and writes for the feature.
/// TODO: RUMM-777 will be used synchronize `activeFileWriter` swaps on consent change.
/// TODO: RUMM-832 will be used to synchornize data migration with reads and writes.
internal let queue: DispatchQueue
/// File writer writting unauthorized data when consent is `.pending`.
private let unauthorizedFileWriter: FileWriterType
Expand All @@ -20,23 +20,61 @@ internal class ConsentAwareDataWriter: FileWriterType {
private var activeFileWriter: FileWriterType?

init(
initialConsent: TrackingConsent,
consentProvider: ConsentProvider,
queue: DispatchQueue,
unauthorizedFileWriter: FileWriterType,
authorizedFileWriter: FileWriterType
) {
self.queue = queue
self.unauthorizedFileWriter = unauthorizedFileWriter
self.authorizedFileWriter = authorizedFileWriter
self.activeFileWriter = resolveActiveFileWriter(
for: consentProvider.currentValue,
unauthorizedWriter: unauthorizedFileWriter,
authorizedWriter: authorizedFileWriter
)

switch initialConsent {
case .granted: self.activeFileWriter = authorizedFileWriter
case .notGranted: self.activeFileWriter = nil
case .pending: self.activeFileWriter = unauthorizedFileWriter
}
consentProvider.subscribe(consentSubscriber: self)
}

// MARK: - FileWriterType

func write<T>(value: T) where T: Encodable {
activeFileWriter?.write(value: value)
synchronized {
activeFileWriter?.write(value: value)
}
}

// MARK: - ConsentSubscriber

func consentChanged(from oldValue: TrackingConsent, to newValue: TrackingConsent) {
synchronized {
activeFileWriter = resolveActiveFileWriter(
for: newValue,
unauthorizedWriter: unauthorizedFileWriter,
authorizedWriter: authorizedFileWriter
)
}
}

// MARK: - Private

private func synchronized(block: () -> Void) {
objc_sync_enter(self)
block()
objc_sync_exit(self)
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is totally fine 👍 i'm just wondering why we didn't use self.queue?

Copy link
Member Author

@ncreated ncreated Dec 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because in this PR the FileWriter is still async. It dispatches writes asynchronously to this queue. Making self.queue.async {} in this PR would lead to doubling the async call to queue (queue.async { queue.async { /* write */ } }).

But I'm aware of this and this was changed in #330 where queue is used instead of objc_sync. In #330 the FileWriter is changed to be synchronous, which provides simpler threading model.

}
}

// TODO: RUMM-831 Move writer resolution to `DataProcessorFactory`
private func resolveActiveFileWriter(
for consent: TrackingConsent,
unauthorizedWriter: FileWriterType,
authorizedWriter: FileWriterType
) -> FileWriterType? {
switch consent {
case .granted: return authorizedWriter
case .notGranted: return nil
case .pending: return unauthorizedWriter
}
}
51 changes: 51 additions & 0 deletions Sources/Datadog/Core/Persistence/Privacy/ConsentProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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

internal protocol ConsentSubscriber: class {
func consentChanged(from oldValue: TrackingConsent, to newValue: TrackingConsent)
}

/// Provides the current `TrackingConsent` value and notifies all subscribers on its change.
internal class ConsentProvider {
private let queue = DispatchQueue(
label: "com.datadoghq.tracking-consent",
target: .global(qos: .userInteractive)
)
private var subscribers: [ConsentSubscriber] = []

init(initialConsent: TrackingConsent) {
self.unsafeCurrentValue = initialConsent
}

// MARK: - Consent Value

/// Unsychronized consent value. Use `self.currentValue` setter & getter.
private var unsafeCurrentValue: TrackingConsent

/// The current value of`TrackingConsent`.
private(set) var currentValue: TrackingConsent {
get { queue.sync { unsafeCurrentValue } }
set { queue.async { self.unsafeCurrentValue = newValue } }
}

/// Sets the new value of `TrackingConsent` and notifies all subscribers.
func changeConsent(to newValue: TrackingConsent) {
let oldValue = currentValue
currentValue = newValue

subscribers.forEach { subscriber in
subscriber.consentChanged(from: oldValue, to: newValue)
}
}

// MARK: - Managing Subscribers

func subscribe(consentSubscriber: ConsentSubscriber) {
subscribers.append(consentSubscriber)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ private class FileWriterMock: FileWriterType {

class ConsentAwareDataWriterTests: XCTestCase {
private let queue = DispatchQueue(label: "dd-tests-write", target: .global(qos: .utility))
private let unauthorizedWriter = FileWriterMock()
private let authorizedWriter = FileWriterMock()

override func setUp() {
super.setUp()
Expand All @@ -28,60 +30,155 @@ class ConsentAwareDataWriterTests: XCTestCase {
super.tearDown()
}

func testWhenInitializedWithConsentGranted_thenItWritesDataToAuthorizedFolder() {
let unauthorizedWriter = FileWriterMock()
let authorizedWriter = FileWriterMock()
// MARK: - Testing Initial Consent

func testWhenInitializedWithConsentGranted_thenItWritesDataToAuthorizedFolder() {
// When
let writer = ConsentAwareDataWriter(
initialConsent: .granted,
consentProvider: ConsentProvider(initialConsent: .granted),
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

// Then
writer.write(value: "abc")
writer.write(value: "authorized data")

waitForAsyncWrite(on: queue)
XCTAssertNil(unauthorizedWriter.dataWritten)
XCTAssertEqual(authorizedWriter.dataWritten as? String, "abc")
XCTAssertEqual(authorizedWriter.dataWritten as? String, "authorized data")
}

func testWhenInitializedWithConsentPending_thenItWritesDataToUnauthorizedFolder() {
let unauthorizedWriter = FileWriterMock()
let authorizedWriter = FileWriterMock()

// When
let writer = ConsentAwareDataWriter(
initialConsent: .pending,
consentProvider: ConsentProvider(initialConsent: .pending),
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

// Then
writer.write(value: "abc")
writer.write(value: "unauthorized data")

waitForAsyncWrite(on: queue)
XCTAssertNil(authorizedWriter.dataWritten)
XCTAssertEqual(unauthorizedWriter.dataWritten as? String, "abc")
XCTAssertEqual(unauthorizedWriter.dataWritten as? String, "unauthorized data")
}

func testWhenInitializedWithConsentNotGranted_thenItDoesNotWriteDataToAnyFolder() {
let unauthorizedWriter = FileWriterMock()
let authorizedWriter = FileWriterMock()
// When
let writer = ConsentAwareDataWriter(
consentProvider: ConsentProvider(initialConsent: .notGranted),
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

// Then
writer.write(value: "rejected data")

waitForAsyncWrite(on: queue)
XCTAssertNil(unauthorizedWriter.dataWritten)
XCTAssertNil(authorizedWriter.dataWritten)
}

// MARK: - Testing Consent Changes

func testWhenConsentChangesToGranted_thenItStartsWrittingDataToAuthorizedFolder() {
let initialConsent: TrackingConsent = [.pending, .notGranted].randomElement()!
let consentProvider = ConsentProvider(initialConsent: initialConsent)
let writer = ConsentAwareDataWriter(
consentProvider: consentProvider,
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

// When
consentProvider.changeConsent(to: .granted)

// Then
writer.write(value: "authorized data")

waitForAsyncWrite(on: queue)
XCTAssertNil(unauthorizedWriter.dataWritten)
XCTAssertEqual(authorizedWriter.dataWritten as? String, "authorized data")
}

func testWhenConsentChangesPending_thenItStartsWrittingDataToUnauthorizedFolder() {
let initialConsent: TrackingConsent = [.granted, .notGranted].randomElement()!
let consentProvider = ConsentProvider(initialConsent: initialConsent)
let writer = ConsentAwareDataWriter(
consentProvider: consentProvider,
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

// When
consentProvider.changeConsent(to: .pending)

// Then
writer.write(value: "unauthorized data")

waitForAsyncWrite(on: queue)
XCTAssertEqual(unauthorizedWriter.dataWritten as? String, "unauthorized data")
XCTAssertNil(authorizedWriter.dataWritten)
}

func testWhenConsentChangesToNotGranted_thenItStopsWrittingDataToAnyFolder() {
let initialConsent: TrackingConsent = [.granted, .pending].randomElement()!
let consentProvider = ConsentProvider(initialConsent: initialConsent)
let writer = ConsentAwareDataWriter(
initialConsent: .notGranted,
consentProvider: consentProvider,
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

// When
consentProvider.changeConsent(to: .notGranted)

// Then
writer.write(value: "abc")
writer.write(value: "rejected data")

waitForAsyncWrite(on: queue)
XCTAssertNil(unauthorizedWriter.dataWritten)
XCTAssertNil(authorizedWriter.dataWritten)
}

// MARK: - Thread Safety

func testChangingConsentAndCallingWriterFromDifferentThreadsShouldNotCrash() {
func randomConsent() -> TrackingConsent {
return [.granted, .pending, .notGranted].randomElement()!
}

let consentProvider = ConsentProvider(initialConsent: randomConsent())
let writer = ConsentAwareDataWriter(
consentProvider: consentProvider,
queue: queue,
unauthorizedFileWriter: unauthorizedWriter,
authorizedFileWriter: authorizedWriter
)

DispatchQueue.concurrentPerform(iterations: 10_000) { iteration in
if iteration % 2 == 0 {
consentProvider.changeConsent(to: randomConsent())
} else {
writer.write(value: "data \(iteration)")
}
}

waitForAsyncWrite(on: queue)
XCTAssertNotNil(unauthorizedWriter.dataWritten, "There should be some unauthorized data written.")
XCTAssertNotNil(authorizedWriter.dataWritten, "There should be some authorized data written.")
}

// MARK: - Helpers

private func waitForAsyncWrite(on queue: DispatchQueue) {
queue.sync {}
}
}
Loading