Skip to content

Commit

Permalink
[WEB-863] Guard Segment Events Behind AppTrackingTransparency (#1799)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottkicks committed Mar 14, 2023
1 parent 2fb519b commit 3e59797
Show file tree
Hide file tree
Showing 15 changed files with 170 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Kickstarter-iOS/AppDelegateViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi
.ksr_delay(.seconds(1), on: AppEnvironment.current.scheduler)
.map { _ -> ATTrackingAuthorizationStatus in
guard featureConsentManagementDialogEnabled() else { return .notDetermined }
return AppTrackingTransparency.authorizationStatus()
return AppEnvironment.current.appTrackingTransparency.authorizationStatus()
}
}

Expand Down
6 changes: 6 additions & 0 deletions Kickstarter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,8 @@
59D1E6261D1865AC00896A4C /* DashboardVideoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D1E6241D1865AC00896A4C /* DashboardVideoCell.swift */; };
59D1E6581D1866F800896A4C /* DashboardVideoCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D1E6571D1866F800896A4C /* DashboardVideoCellViewModel.swift */; };
59E877381DC9419700BCD1F7 /* Newsletter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59E877371DC9419700BCD1F7 /* Newsletter.swift */; };
6008633F29BF750700B87B39 /* MockAppTrackingTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6008633E29BF750700B87B39 /* MockAppTrackingTransparency.swift */; };
6008634129BF8B5100B87B39 /* MockAppTrackingTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6008633E29BF750700B87B39 /* MockAppTrackingTransparency.swift */; };
6018626629A9194600EA2842 /* AppTrackingTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6018626529A9194600EA2842 /* AppTrackingTransparency.swift */; };
6018626829A91B8C00EA2842 /* FacebookCAPIEventName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6018626729A91B8C00EA2842 /* FacebookCAPIEventName.swift */; };
606754BD28CF91D60033CD5E /* FacebookCore in Frameworks */ = {isa = PBXBuildFile; productRef = 606754BC28CF91D60033CD5E /* FacebookCore */; };
Expand Down Expand Up @@ -2070,6 +2072,7 @@
59D1E6241D1865AC00896A4C /* DashboardVideoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVideoCell.swift; sourceTree = "<group>"; };
59D1E6571D1866F800896A4C /* DashboardVideoCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVideoCellViewModel.swift; sourceTree = "<group>"; };
59E877371DC9419700BCD1F7 /* Newsletter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Newsletter.swift; sourceTree = "<group>"; };
6008633E29BF750700B87B39 /* MockAppTrackingTransparency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAppTrackingTransparency.swift; sourceTree = "<group>"; };
6018626529A9194600EA2842 /* AppTrackingTransparency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingTransparency.swift; sourceTree = "<group>"; };
6018626729A91B8C00EA2842 /* FacebookCAPIEventName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookCAPIEventName.swift; sourceTree = "<group>"; };
6067BCE7293E48140036ABB1 /* FacebookResetPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookResetPasswordViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6484,6 +6487,7 @@
A7ED1F441E831BA200BFFA01 /* DispatchTimeInterval-Extensions.swift */,
77C9122A23C5079200F3D2C9 /* MockApplePayCapable.swift */,
D033E2C122A05B4800464E43 /* MockApplication.swift */,
6008633E29BF750700B87B39 /* MockAppTrackingTransparency.swift */,
A7ED1F451E831BA200BFFA01 /* MockBundle.swift */,
774D98DB23B1520D00FC81C2 /* MockOptimizelyClient.swift */,
1611EF6823B275700051CDCC /* MockUUID.swift */,
Expand Down Expand Up @@ -8049,6 +8053,7 @@
A7ED1F2B1E830FDC00BFFA01 /* IsValidEmailTests.swift in Sources */,
A7ED1FEB1E831C5C00BFFA01 /* DashboardReferrersCellViewModelTests.swift in Sources */,
37C7B81723187BAC00C78278 /* ShippingRuleCellViewModelTests.swift in Sources */,
6008633F29BF750700B87B39 /* MockAppTrackingTransparency.swift in Sources */,
37FDAFAD2273BA4B00662CC8 /* UIStackView+Tests.swift in Sources */,
8A13D16E24985411007E2C0B /* PledgeExpandableHeaderRewardCellViewModelTests.swift in Sources */,
D04AACA7218BB72100CF713E /* DiscoveryProjectCategoryViewModelTests.swift in Sources */,
Expand Down Expand Up @@ -8586,6 +8591,7 @@
buildActionMask = 2147483647;
files = (
1611EF6023ABD3D90051CDCC /* MockOptimizelyResult.swift in Sources */,
6008634129BF8B5100B87B39 /* MockAppTrackingTransparency.swift in Sources */,
A7ED20631E83256700BFFA01 /* HelpWebViewModelTests.swift in Sources */,
8AD7952F239EBDF600998C79 /* MockTrackingClient.swift in Sources */,
47732FFC26824E5000E84915 /* OptimizelyFeatureFlagToolsViewControllerTests.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions Library/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ public struct AppEnvironment: AppEnvironmentType {
apiDelayInterval: DispatchTimeInterval = AppEnvironment.current.apiDelayInterval,
applePayCapabilities: ApplePayCapabilitiesType = AppEnvironment.current.applePayCapabilities,
application: UIApplicationType = UIApplication.shared,
appTrackingTransparency: AppTrackingTransparencyType = AppEnvironment.current
.appTrackingTransparency,
assetImageGeneratorType: AssetImageGeneratorType.Type = AppEnvironment.current.assetImageGeneratorType,
cache: KSCache = AppEnvironment.current.cache,
calendar: Calendar = AppEnvironment.current.calendar,
Expand Down Expand Up @@ -162,6 +164,7 @@ public struct AppEnvironment: AppEnvironmentType {
apiDelayInterval: apiDelayInterval,
applePayCapabilities: applePayCapabilities,
application: application,
appTrackingTransparency: appTrackingTransparency,
assetImageGeneratorType: assetImageGeneratorType,
cache: cache,
calendar: calendar,
Expand Down Expand Up @@ -198,6 +201,7 @@ public struct AppEnvironment: AppEnvironmentType {
apiDelayInterval: DispatchTimeInterval = AppEnvironment.current.apiDelayInterval,
applePayCapabilities: ApplePayCapabilitiesType = AppEnvironment.current.applePayCapabilities,
application: UIApplicationType = UIApplication.shared,
appTrackingTransparency: AppTrackingTransparencyType = AppEnvironment.current.appTrackingTransparency,
assetImageGeneratorType: AssetImageGeneratorType.Type = AppEnvironment.current.assetImageGeneratorType,
cache: KSCache = AppEnvironment.current.cache,
calendar: Calendar = AppEnvironment.current.calendar,
Expand Down Expand Up @@ -230,6 +234,7 @@ public struct AppEnvironment: AppEnvironmentType {
apiDelayInterval: apiDelayInterval,
applePayCapabilities: applePayCapabilities,
application: application,
appTrackingTransparency: appTrackingTransparency,
assetImageGeneratorType: assetImageGeneratorType,
cache: cache,
calendar: calendar,
Expand Down
5 changes: 5 additions & 0 deletions Library/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public struct Environment {
/// The app instance
public let application: UIApplicationType

/// A type that provides methods that return the user's tracking consent preference and advertising identifier
public let appTrackingTransparency: AppTrackingTransparencyType

/// A type that exposes how to extract a still image from an AVAsset.
public let assetImageGeneratorType: AssetImageGeneratorType.Type

Expand Down Expand Up @@ -111,6 +114,7 @@ public struct Environment {
apiDelayInterval: DispatchTimeInterval = .seconds(0),
applePayCapabilities: ApplePayCapabilitiesType = ApplePayCapabilities(),
application: UIApplicationType = UIApplication.shared,
appTrackingTransparency: AppTrackingTransparencyType = AppTrackingTransparency(),
assetImageGeneratorType: AssetImageGeneratorType.Type = AVAssetImageGenerator.self,
cache: KSCache = KSCache(),
calendar: Calendar = .current,
Expand Down Expand Up @@ -142,6 +146,7 @@ public struct Environment {
self.apiDelayInterval = apiDelayInterval
self.applePayCapabilities = applePayCapabilities
self.application = application
self.appTrackingTransparency = appTrackingTransparency
self.assetImageGeneratorType = assetImageGeneratorType
self.cache = cache
self.calendar = calendar
Expand Down
14 changes: 14 additions & 0 deletions Library/TestHelpers/MockAppTrackingTransparency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Library

struct MockAppTrackingTransparency: AppTrackingTransparencyType {
public var authStatusStub: ATTrackingAuthorizationStatus =
.authorized // defaulting to .authorized so existing tests will still pass

func authorizationStatus() -> ATTrackingAuthorizationStatus {
return self.authStatusStub
}

func advertisingIdentifier() -> String? {
return "advertisingIdentifier"
}
}
7 changes: 6 additions & 1 deletion Library/TestHelpers/TestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ internal class TestCase: XCTestCase {
internal static let interval = DispatchTimeInterval.milliseconds(1)

internal let apiService = MockService()
internal let appTrackingTransparency = MockAppTrackingTransparency()
internal let cache = KSCache()
internal let config = Config.config
internal let cookieStorage = MockCookieStorage()
internal let coreTelephonyNetworkInfo = MockCoreTelephonyNetworkInfo()
internal let dateType = MockDate.self
internal let mainBundle = MockBundle()
internal let optimizelyClient = MockOptimizelyClient()
internal var optimizelyClient = MockOptimizelyClient()
internal let reachability = MutableProperty(Reachability.wifi)
internal let scheduler = TestScheduler(startDate: MockDate().date)
internal let segmentTrackingClient = MockTrackingClient()
Expand All @@ -32,11 +33,15 @@ internal class TestCase: XCTestCase {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "GMT")!

self.optimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.consentManagementDialogEnabled.rawValue: true]

AppEnvironment.pushEnvironment(
apiService: self.apiService,
apiDelayInterval: .seconds(0),
applePayCapabilities: MockApplePayCapabilities(),
application: UIApplication.shared,
appTrackingTransparency: self.appTrackingTransparency,
assetImageGeneratorType: AVAssetImageGenerator.self,
cache: self.cache,
calendar: calendar,
Expand Down
3 changes: 3 additions & 0 deletions Library/TestHelpers/XCTestCase+AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ extension XCTestCase {
apiDelayInterval: DispatchTimeInterval = AppEnvironment.current.apiDelayInterval,
applePayCapabilities: ApplePayCapabilitiesType = AppEnvironment.current.applePayCapabilities,
application: UIApplicationType = UIApplication.shared,
appTrackingTransparency: AppTrackingTransparencyType = AppEnvironment.current
.appTrackingTransparency,
assetImageGeneratorType: AssetImageGeneratorType.Type = AppEnvironment.current.assetImageGeneratorType,
cache: KSCache = AppEnvironment.current.cache,
calendar: Calendar = AppEnvironment.current.calendar,
Expand Down Expand Up @@ -49,6 +51,7 @@ extension XCTestCase {
apiDelayInterval: apiDelayInterval,
applePayCapabilities: applePayCapabilities,
application: application,
appTrackingTransparency: appTrackingTransparency,
assetImageGeneratorType: assetImageGeneratorType,
cache: cache,
calendar: calendar,
Expand Down
13 changes: 10 additions & 3 deletions Library/Tracking/AppTrackingTransparency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ import AdSupport
import AppTrackingTransparency
import Foundation

public struct AppTrackingTransparency {
public static func authorizationStatus() -> ATTrackingAuthorizationStatus {
public protocol AppTrackingTransparencyType {
func authorizationStatus() -> ATTrackingAuthorizationStatus
func advertisingIdentifier() -> String?
}

public struct AppTrackingTransparency: AppTrackingTransparencyType {
public init() {}

public func authorizationStatus() -> ATTrackingAuthorizationStatus {
var authorizationStatus: ATTrackingAuthorizationStatus = .notDetermined

ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
Expand All @@ -24,7 +31,7 @@ public struct AppTrackingTransparency {
return authorizationStatus
}

public static func advertisingIdentifier() -> String? {
public func advertisingIdentifier() -> String? {
guard self.authorizationStatus() == .authorized else { return nil }

return ASIdentifierManager.shared().advertisingIdentifier.uuidString
Expand Down
3 changes: 3 additions & 0 deletions Library/Tracking/KSRAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,9 @@ public final class KSRAnalytics {
properties: [String: Any] = [:],
refTag: String? = nil
) {
guard featureConsentManagementDialogEnabled(),
AppEnvironment.current.appTrackingTransparency.authorizationStatus() == .authorized else { return }

let props = self.sessionProperties(refTag: refTag)
.withAllValuesFrom(userProperties(for: self.loggedInUser))
.withAllValuesFrom(properties)
Expand Down
106 changes: 106 additions & 0 deletions Library/Tracking/KSRAnalyticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class KSRAnalyticsTests: TestCase {

func testSessionProperties_OptimizelyClient() {
let optimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.consentManagementDialogEnabled.rawValue: true]
|> \.allKnownExperiments .~ [
OptimizelyExperiment.Key.nativeProjectCards.rawValue
]
Expand Down Expand Up @@ -1610,6 +1611,111 @@ final class KSRAnalyticsTests: TestCase {
XCTAssertEqual("Documentary", segmentClient.properties.last?["discover_subcategory_name"] as? String)
}

func testEventsCalledAsExpectedWhenAppTrackingConsentAuthorized_WhenFeatureFlagEnabled() {
let segmentClient = MockTrackingClient()
let ksrAnalytics = KSRAnalytics(segmentClient: segmentClient)
var mockAppTrackingTransparency = MockAppTrackingTransparency()
let optimizelyClient = MockOptimizelyClient()
|> \.features .~ [
OptimizelyFeature.consentManagementDialogEnabled.rawValue: true
]

mockAppTrackingTransparency.authStatusStub = .authorized

withEnvironment(
appTrackingTransparency: mockAppTrackingTransparency,
optimizelyClient: optimizelyClient
) {
ksrAnalytics.trackProjectViewed(Project.template, sectionContext: .overview)
ksrAnalytics.trackTabBarClicked(tabBarItemLabel: .discovery, previousTabBarItemLabel: .search)
ksrAnalytics.trackDiscovery(params: .defaults)
ksrAnalytics.trackExploreButtonClicked()

XCTAssertEqual(["Page Viewed", "CTA Clicked", "Page Viewed", "CTA Clicked"], segmentClient.events)
}
}

func testNoEventsCalledWhenAppTrackingConsentDenied_WhenFeatureFlagEnabled() {
let segmentClient = MockTrackingClient()
let ksrAnalytics = KSRAnalytics(segmentClient: segmentClient)
var mockAppTrackingTransparency = MockAppTrackingTransparency()
let optimizelyClient = MockOptimizelyClient()
|> \.features .~ [
OptimizelyFeature.consentManagementDialogEnabled.rawValue: true
]

mockAppTrackingTransparency.authStatusStub = .denied

withEnvironment(
appTrackingTransparency: mockAppTrackingTransparency,
optimizelyClient: optimizelyClient
) {
ksrAnalytics.trackProjectViewed(Project.template, sectionContext: .overview)
ksrAnalytics.trackTabBarClicked(tabBarItemLabel: .discovery, previousTabBarItemLabel: .search)
ksrAnalytics.trackDiscovery(params: .defaults)
ksrAnalytics.trackExploreButtonClicked()

XCTAssert(
segmentClient.properties.isEmpty,
"No events tracked by segment client"
)
}
}

func testNoEventsCalledWhenAppTrackingConsentNotDetermined_WhenFeatureFlagEnabled() {
let segmentClient = MockTrackingClient()
let ksrAnalytics = KSRAnalytics(segmentClient: segmentClient)
var mockAppTrackingTransparency = MockAppTrackingTransparency()
let optimizelyClient = MockOptimizelyClient()
|> \.features .~ [
OptimizelyFeature.consentManagementDialogEnabled.rawValue: true
]

mockAppTrackingTransparency.authStatusStub = .notDetermined

withEnvironment(
appTrackingTransparency: mockAppTrackingTransparency,
optimizelyClient: optimizelyClient
) {
ksrAnalytics.trackProjectViewed(Project.template, sectionContext: .overview)
ksrAnalytics.trackTabBarClicked(tabBarItemLabel: .discovery, previousTabBarItemLabel: .search)
ksrAnalytics.trackDiscovery(params: .defaults)
ksrAnalytics.trackExploreButtonClicked()

XCTAssert(
segmentClient.properties.isEmpty,
"No events tracked by segment client"
)
}
}

func testNoEventsCalledWhenAppTrackingConsentRestricted() {
let segmentClient = MockTrackingClient()
let ksrAnalytics = KSRAnalytics(segmentClient: segmentClient)
var mockAppTrackingTransparency = MockAppTrackingTransparency()
let optimizelyClient = MockOptimizelyClient()
|> \.features .~ [
OptimizelyFeature.consentManagementDialogEnabled.rawValue: true
]

mockAppTrackingTransparency.authStatusStub = .restricted

withEnvironment(
appTrackingTransparency: mockAppTrackingTransparency,
optimizelyClient: optimizelyClient
) {
ksrAnalytics.trackProjectViewed(Project.template, sectionContext: .overview)
ksrAnalytics.trackTabBarClicked(tabBarItemLabel: .discovery, previousTabBarItemLabel: .search)
ksrAnalytics.trackDiscovery(params: .defaults)
ksrAnalytics.trackExploreButtonClicked()

XCTAssert(
segmentClient.properties.isEmpty,
"No events tracked by segment client"
)
}
}

func testTrackProjectViewedEvent() {
let segmentClient = MockTrackingClient()
let ksrAnalytics = KSRAnalytics(segmentClient: segmentClient)
Expand Down
3 changes: 2 additions & 1 deletion Library/ViewModels/PledgePaymentMethodsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,8 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT
let (project, _) = projectSignal

guard featureFacebookConversionsAPIEnabled(), project.sendMetaCapiEvents == true,
let externalId = AppTrackingTransparency.advertisingIdentifier() else { return }
let externalId = AppEnvironment.current.appTrackingTransparency.advertisingIdentifier()
else { return }

_ = AppEnvironment
.current
Expand Down
3 changes: 3 additions & 0 deletions Library/ViewModels/PledgeViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,7 @@ final class PledgeViewModelTests: TestCase {
)
)
let mockOptimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.consentManagementDialogEnabled.rawValue: true]
|> \.experiments
.~ [
OptimizelyExperiment.Key.nativeRiskMessaging.rawValue:
Expand Down Expand Up @@ -2339,6 +2340,7 @@ final class PledgeViewModelTests: TestCase {
)
)
let mockOptimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.consentManagementDialogEnabled.rawValue: true]
|> \.experiments
.~ [
OptimizelyExperiment.Key.nativeRiskMessaging.rawValue:
Expand Down Expand Up @@ -6821,6 +6823,7 @@ final class PledgeViewModelTests: TestCase {

func testTrackingEvents_PledgeConfirmButtonClicked() {
let mockOptimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.consentManagementDialogEnabled.rawValue: true]
|> \.experiments
.~ [
OptimizelyExperiment.Key.nativeRiskMessaging.rawValue:
Expand Down
3 changes: 2 additions & 1 deletion Library/ViewModels/ProjectPageViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,8 @@ public final class ProjectPageViewModel: ProjectPageViewModelType, ProjectPageVi
let (project, _) = projectAndRefTag

guard featureFacebookConversionsAPIEnabled(), project.sendMetaCapiEvents,
let externalId = AppTrackingTransparency.advertisingIdentifier() else { return }
let externalId = AppEnvironment.current.appTrackingTransparency.advertisingIdentifier()
else { return }

_ = AppEnvironment
.current
Expand Down
3 changes: 2 additions & 1 deletion Library/ViewModels/RewardsCollectionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ public final class RewardsCollectionViewModel: RewardsCollectionViewModelType,
let (project, _) = projectAndRefTag

guard featureFacebookConversionsAPIEnabled(), project.sendMetaCapiEvents,
let externalId = AppTrackingTransparency.advertisingIdentifier() else { return }
let externalId = AppEnvironment.current.appTrackingTransparency.advertisingIdentifier()
else { return }

_ = AppEnvironment
.current
Expand Down
3 changes: 2 additions & 1 deletion Library/ViewModels/ThanksViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ public final class ThanksViewModel: ThanksViewModelType, ThanksViewModelInputs,
}

guard featureFacebookConversionsAPIEnabled(), project.sendMetaCapiEvents,
let externalId = AppTrackingTransparency.advertisingIdentifier() else { return }
let externalId = AppEnvironment.current.appTrackingTransparency.advertisingIdentifier()
else { return }

_ = AppEnvironment
.current
Expand Down

0 comments on commit 3e59797

Please sign in to comment.