Skip to content

Commit

Permalink
Add support for user consent (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
waliid authored Sep 6, 2023
1 parent 4055dad commit b30a751
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 21 deletions.
16 changes: 15 additions & 1 deletion Demo/Sources/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
sourceKey: "39ae8f94-595c-4ca4-81f7-fb7748bd3f04",
appSiteName: "pillarbox-demo-apple"
)
try? Analytics.shared.start(with: configuration)
try? Analytics.shared.start(with: configuration, dataSource: self)
}
}

extension AppDelegate: AnalyticsDataSource {
var comScoreGlobals: ComScoreGlobals {
.init(consent: .unknown, labels: [
"demo_key": "demo_value"
])
}

var commandersActGlobals: CommandersActGlobals {
.init(consentServices: ["service1", "service2", "service3"], labels: [
"demo_key": "demo_value"
])
}
}
23 changes: 18 additions & 5 deletions Sources/Analytics/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,17 @@ public class Analytics {
PackageInfo.version
}

var comScoreGlobals: ComScoreGlobals? {
dataSource?.comScoreGlobals
}

private var configuration: Configuration?

private let comScoreService = ComScoreService()
private let commandersActService = CommandersActService()

private weak var dataSource: AnalyticsDataSource?

private init() {}

/// Starts analytics with the specified configuration.
Expand All @@ -64,15 +70,16 @@ public class Analytics {
/// delegate method implementation, otherwise the behavior is undefined.
///
/// The method throws if called more than once.
public func start(with configuration: Configuration) throws {
public func start(with configuration: Configuration, dataSource: AnalyticsDataSource? = nil) throws {
guard self.configuration == nil else {
throw AnalyticsError.alreadyStarted
}
self.configuration = configuration
self.dataSource = dataSource

UIViewController.setupViewControllerTracking()

comScoreService.start(with: configuration)
comScoreService.start(with: configuration, globals: dataSource?.comScoreGlobals)
commandersActService.start(with: configuration)
}

Expand All @@ -85,14 +92,20 @@ public class Analytics {
comScore comScorePageView: ComScorePageView,
commandersAct commandersActPageView: CommandersActPageView
) {
comScoreService.trackPageView(comScorePageView)
commandersActService.trackPageView(commandersActPageView)
comScoreService.trackPageView(
comScorePageView.merging(globals: dataSource?.comScoreGlobals)
)
commandersActService.trackPageView(
commandersActPageView.merging(globals: dataSource?.commandersActGlobals)
)
}

/// Sends an event.
///
/// - Parameter commandersAct: Commanders Act event data
public func sendEvent(commandersAct commandersActEvent: CommandersActEvent) {
commandersActService.sendEvent(commandersActEvent)
commandersActService.sendEvent(
commandersActEvent.merging(globals: dataSource?.commandersActGlobals)
)
}
}
15 changes: 15 additions & 0 deletions Sources/Analytics/AnalyticsDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Copyright (c) SRG SSR. All rights reserved.
//
// License information is available from the LICENSE file.
//

import Foundation

/// A protocol for analytics data sources.
public protocol AnalyticsDataSource: AnyObject {
/// comScore global labels.
var comScoreGlobals: ComScoreGlobals { get }
/// Commanders Act global labels.
var commandersActGlobals: CommandersActGlobals { get }
}
33 changes: 33 additions & 0 deletions Sources/Analytics/ComScore/ComScoreGlobals.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Copyright (c) SRG SSR. All rights reserved.
//
// License information is available from the LICENSE file.
//

import Foundation

/// An enum representing the user consent options for comScore.
public enum ComScoreConsent: String {
/// The user's consent status is unknown.
case unknown = ""

/// The user has accepted comScore analytics.
case accepted = "1"

/// The user has declined comScore analytics.
case declined = "0"
}

/// A struct representing global labels to send to comScore.
public struct ComScoreGlobals {
let labels: [String: String]

/// Creates comScore global labels.
///
/// - Parameters:
/// - consent: The user's consent status.
/// - labels: Additional information associated with the global labels.
public init(consent: ComScoreConsent, labels: [String: String]) {
self.labels = labels.merging(["cs_ucfr": consent.rawValue]) { _, new in new }
}
}
5 changes: 5 additions & 0 deletions Sources/Analytics/ComScore/ComScoreLabels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ public struct ComScoreLabels {
extract()
}

/// The value of `cs_ucfr` (user consent).
public var cs_ucfr: String? {
extract()
}

// MARK: Page view labels

/// The value of `c8` (page title).
Expand Down
10 changes: 8 additions & 2 deletions Sources/Analytics/ComScore/ComScorePageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ public struct ComScorePageView {

/// Creates a comScore page view.
///
/// Custom labels which might accidentally override official labels will be ignored.
///
/// - Parameters:
/// - name: The page name.
/// - labels: Additional information associated with the page view.
///
/// Custom labels which might accidentally override official labels will be ignored.
public init(name: String, labels: [String: String] = [:]) {
assert(!name.isBlank, "A name is required")
self.name = name
self.labels = labels
}

func merging(globals: ComScoreGlobals?) -> Self {
guard let globals else { return self }
let labels = labels.merging(globals.labels) { _, new in new }
return .init(name: name, labels: labels)
}
}
5 changes: 4 additions & 1 deletion Sources/Analytics/ComScore/ComScoreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct ComScoreService {
Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
}

func start(with configuration: Analytics.Configuration) {
func start(with configuration: Analytics.Configuration, globals: ComScoreGlobals?) {
let publisherConfiguration = SCORPublisherConfiguration { builder in
guard let builder else { return }
builder.publisherId = "6036016"
Expand All @@ -31,6 +31,9 @@ struct ComScoreService {
"mp_brand": configuration.vendor.rawValue,
"mp_v": applicationVersion
])
if let globals {
comScoreConfiguration.addStartLabels(globals.labels)
}
}
SCORAnalytics.start()
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/Analytics/ComScore/ComScoreTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ public final class ComScoreTracker: PlayerItemTracker {

private func updateMetadata(with metadata: Metadata) {
let builder = SCORStreamingContentMetadataBuilder()
builder.setCustomLabels(metadata.labels)
if let globals = Analytics.shared.comScoreGlobals {
builder.setCustomLabels(metadata.labels.merging(globals.labels) { _, new in new })
}
else {
builder.setCustomLabels(metadata.labels)
}
let contentMetadata = SCORStreamingContentMetadata(builder: builder)
streamingAnalytics.setMetadata(contentMetadata)
}
Expand Down
10 changes: 8 additions & 2 deletions Sources/Analytics/CommandersAct/CommandersActEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ public struct CommandersActEvent {

/// Creates a Commanders Act event.
///
/// Custom labels which might accidentally override official labels will be ignored.
///
/// - Parameters:
/// - name: The event name.
/// - labels: Additional information associated with the event.
///
/// Custom labels which might accidentally override official labels will be ignored.
public init(
name: String,
labels: [String: String] = [:]
Expand All @@ -26,4 +26,10 @@ public struct CommandersActEvent {
self.name = name
self.labels = labels
}

func merging(globals: CommandersActGlobals?) -> Self {
guard let globals else { return self }
let labels = labels.merging(globals.labels) { _, new in new }
return .init(name: name, labels: labels)
}
}
23 changes: 23 additions & 0 deletions Sources/Analytics/CommandersAct/CommandersActGlobals.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Copyright (c) SRG SSR. All rights reserved.
//
// License information is available from the LICENSE file.
//

import Foundation

/// A struct representing global labels to send to Commanders Act.
public struct CommandersActGlobals {
let labels: [String: String]

/// Creates Commanders Act global labels.
///
/// - Parameters:
/// - consentServices: The list of service allowed.
/// - labels: Additional information associated with the global labels.
public init(consentServices: [String], labels: [String: String]) {
self.labels = labels.merging([
"consent_services": consentServices.joined(separator: ",")
]) { _, new in new }
}
}
4 changes: 4 additions & 0 deletions Sources/Analytics/CommandersAct/CommandersActLabels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public struct CommandersActLabels: Decodable {
/// The value of `navigation_device`.
public let navigation_device: String?

/// The value of `consent_services`.
public let consent_services: String?

// MARK: Page view labels

/// The value of `navigation_property_type`.
Expand Down Expand Up @@ -127,6 +130,7 @@ private extension CommandersActLabels {
case app_library_version
case navigation_app_site_name
case navigation_device
case consent_services
case navigation_property_type
case navigation_bu_distributer
case navigation_level_0
Expand Down
10 changes: 8 additions & 2 deletions Sources/Analytics/CommandersAct/CommandersActPageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ public struct CommandersActPageView {

/// Creates a Commanders Act page view.
///
/// Custom labels which might accidentally override official labels will be ignored.
///
/// - Parameters:
/// - name: The page name.
/// - type: The page type (e.g. Article).
/// - labels: Additional information associated with the page view.
/// - levels: The page levels.
///
/// Custom labels which might accidentally override official labels will be ignored.
public init(name: String, type: String, levels: [String] = [], labels: [String: String] = [:]) {
assert(!name.isBlank, "A name is required")
assert(!type.isBlank, "A type is required")
Expand All @@ -30,4 +30,10 @@ public struct CommandersActPageView {
self.levels = levels
self.labels = labels
}

func merging(globals: CommandersActGlobals?) -> Self {
guard let globals else { return self }
let labels = labels.merging(globals.labels) { _, new in new }
return .init(name: name, type: type, levels: levels, labels: labels)
}
}
9 changes: 7 additions & 2 deletions Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private class ManualMockViewController: UIViewController, PageViewTracking {
}

final class ComScorePageViewTests: ComScoreTestCase {
func testLabels() {
func testGlobals() {
expectAtLeastHits(
.view { labels in
expect(labels.c2).to(equal("6036016"))
Expand All @@ -78,6 +78,7 @@ final class ComScorePageViewTests: ComScoreTestCase {
expect(labels.ns_st_mv).to(beNil())
expect(labels.mp_brand).to(equal("SRG"))
expect(labels.mp_v).notTo(beEmpty())
expect(labels.cs_ucfr).to(beEmpty())
}
) {
Analytics.shared.trackPageView(
Expand Down Expand Up @@ -112,10 +113,14 @@ final class ComScorePageViewTests: ComScoreTestCase {
expectAtLeastHits(
.view { labels in
expect(labels.c8).to(equal("name"))
expect(labels.cs_ucfr).to(beEmpty())
}
) {
Analytics.shared.trackPageView(
comScore: .init(name: "name", labels: ["c8": "overridden_title"]),
comScore: .init(name: "name", labels: [
"c8": "overridden_title",
"cs_ucfr": "42"
]),
commandersAct: .init(name: "name", type: "type")
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ final class ComScoreTrackerMetadataTests: ComScoreTestCase {
.play { labels in
expect(labels["meta_1"]).to(equal("custom-1"))
expect(labels["meta_2"]).to(equal(42))
expect(labels["cs_ucfr"]).to(beEmpty())
}
) {
player.play()
Expand Down
3 changes: 2 additions & 1 deletion Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ private struct AssetMetadataMock: AssetMetadata {}
// Thus, to test end events resulting from tracker deallocation we need to have another event sent within the same
// expectation first so that the end event is provided a listener identifier.
final class ComScoreTrackerTests: ComScoreTestCase {
func testMediaPlayerProperties() {
func testGlobals() {
let player = Player(item: .simple(
url: Stream.onDemand.url,
metadata: AssetMetadataMock(),
Expand All @@ -36,6 +36,7 @@ final class ComScoreTrackerTests: ComScoreTestCase {
.play { labels in
expect(labels.ns_st_mp).to(equal("Pillarbox"))
expect(labels.ns_st_mv).notTo(beEmpty())
expect(labels.cs_ucfr).to(beEmpty())
}
) {
player.play()
Expand Down
Loading

0 comments on commit b30a751

Please sign in to comment.