Skip to content

Commit

Permalink
Merge pull request #328 from DataDog/ncreated/RUMM-830-add-consent-pr…
Browse files Browse the repository at this point in the history
…ovider

RUMM-830 Add consent provider
  • Loading branch information
ncreated authored Dec 7, 2020
2 parents 461e91c + 4800a91 commit 6471e79
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 26 deletions.
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)
}
}

// 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

0 comments on commit 6471e79

Please sign in to comment.