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

Simplify Analytics.MultiTracker 👁 #229

Merged
merged 1 commit into from
Apr 6, 2021
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
59 changes: 6 additions & 53 deletions Sources/Analytics/Trackers/Analytics+MultiTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,20 @@ import Foundation

public extension Analytics {

/// An error produced by `MultiTracker` instances.
enum MultiTrackerError: Swift.Error {

/// A tracker with the same id already registered.
case duplicateTracker(AnalyticsTracker.ID)

/// A tracker with the given id isn't registered.
case inexistentTracker(AnalyticsTracker.ID)
}

/// An analytics tracker that forwards analytics events to multiple trackers, while not doing any tracking on its
/// own.
final class MultiTracker<State, Action, ParameterKey: AnalyticsParameterKey>: AnalyticsTracker {

/// The registered sub trackers (read only).
public var trackers: [AnyAnalyticsTracker<State, Action, ParameterKey>] { return _trackers.value }

/// The registered sub trackers.
private let _trackers: Atomic<[AnyAnalyticsTracker<State, Action, ParameterKey>]>
public let trackers: [AnyAnalyticsTracker<State, Action, ParameterKey>]

/// Creates an analytics multi tracker instance.
public init() {
self._trackers = Atomic<[AnyAnalyticsTracker<State, Action, ParameterKey>]>([])
}
/// - Parameter trackers: The analytics trackers to register.
public init(trackers: [AnyAnalyticsTracker<State, Action, ParameterKey>]) {

// MARK: - Sub-Tracker Management
assert(!trackers.isEmpty, "🙅‍♂️ Trackers shouldn't be empty, since it renders this tracker useless!")

/// Registers a sub tracker, and starts sending any new analytics events to it. This method is thread safe.
///
/// - Parameter tracker: The analytics tracker to register.
/// - Throws: An `Analytics.MultiTrackerError.duplicateTracker` error if a tracker with the same `id` is
/// already registered.
public func register<T: AnalyticsTracker>(_ tracker: T) throws
where T.State == State, T.Action == Action, T.ParameterKey == ParameterKey {
precondition(tracker.id != id, "🙅‍♂️ Can't register a tracker with the same `id` as `self`!")

try _trackers.modify {
guard $0.contains(where: { $0.id == tracker.id }) == false else {
throw MultiTrackerError.duplicateTracker(tracker.id)
}
$0.append(AnyAnalyticsTracker(tracker))
}
}

/// Unregisters a sub tracker, preventing any new analytics events from being sent to it. This method is thread
/// safe.
///
/// - Parameter tracker: The analytics tracker to unregister.
/// - Throws: An `Analytics.MultiTrackerError.inexistentTracker` error if a tracker with the same `id` isn't
/// registered.
public func unregister<T: AnalyticsTracker>(_ tracker: T) throws
where T.State == State, T.Action == Action, T.ParameterKey == ParameterKey {
try _trackers.modify {
guard $0.contains(where: { $0.id == tracker.id }) else {
throw MultiTrackerError.inexistentTracker(tracker.id)
}
$0 = $0.filter { $0.id != tracker.id }
}
self.trackers = trackers
}

// MARK: - Tracking
Expand All @@ -68,11 +24,8 @@ public extension Analytics {
///
/// - Parameter event: The event to track.
public func track(_ event: Analytics.Event<State, Action, ParameterKey>) {
let currentTrackers = _trackers.value

guard currentTrackers.isEmpty == false else { return }

currentTrackers.forEach { $0.track(event) }
trackers.forEach { $0.track(event) }
}
}
}
12 changes: 2 additions & 10 deletions Sources/Analytics/Trackers/AnalyticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,13 @@ public protocol AnalyticsTracker: AnyObject {
/// An analytics event.
typealias Event = Analytics.Event<State, Action, ParameterKey>

/// A type representing a tracker's identifier.
typealias ID = String

/// The identifier of the tracker. The default is the tracker's type name.
var id: ID { get }

/// Tracks an analytics event.
///
/// - Parameter event: The event to track.
func track(_ event: Event)
}

extension AnalyticsTracker {
public extension AnalyticsTracker {

public var id: ID {
return "\(type(of: self))"
}
func eraseToAnyAnalyticsTracker() -> Analytics.AnyAnalyticsTracker<State, Action, ParameterKey> { .init(self) }
}
11 changes: 5 additions & 6 deletions Sources/Analytics/Trackers/AnyAnalyticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ extension Analytics {
/// A type-erased analytics tracker.
public final class AnyAnalyticsTracker<State, Action, ParameterKey: AnalyticsParameterKey>: AnalyticsTracker {

/// The type-erased tracker's wrapped instance id.
public let id: ID
/// The type-erased tracker's wrapped instance.
public let _wrapped: AnyObject

/// The type-erased tracker's wrapped instance `track` method, stored as a closure.
private let _track: (Analytics.Event<State, Action, ParameterKey>) -> Void
Expand All @@ -17,15 +17,14 @@ extension Analytics {
/// - tracker: The analytics tracker instance to wrap.
public init<T: AnalyticsTracker>(_ tracker: T)
where T.State == State, T.Action == Action, T.ParameterKey == ParameterKey {
id = tracker.id

_wrapped = tracker
_track = tracker.track
}

/// Tracks an analytics event via the wrapped tracker.
///
/// - Parameter event: The analytics event.
public func track(_ event: Analytics.Event<State, Action, ParameterKey>) {
_track(event)
}
public func track(_ event: Analytics.Event<State, Action, ParameterKey>) { _track(event) }
}
}
16 changes: 2 additions & 14 deletions Tests/AlicerceTests/Analytics/MockAnalyticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,9 @@ final class MockAnalyticsTracker<S, A, PK: AnalyticsParameterKey>: AnalyticsTrac
typealias Action = A
typealias ParameterKey = PK

var mockID: ID?

var trackInvokedClosure: ((Event) -> Void)?

let defaultID: ID

var id: ID { return mockID ?? defaultID }

// MARK: - Lifecycle

public init(id: ID = "MockSubTracker") {
self.defaultID = id
}
init() {}

func track(_ event: Event) {
trackInvokedClosure?(event)
}
func track(_ event: Event) { trackInvokedClosure?(event) }
}
122 changes: 9 additions & 113 deletions Tests/AlicerceTests/Analytics/MultiTrackerTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,118 +22,18 @@ final class MultiTrackerTestCase: XCTestCase {
typealias MultiTracker = Analytics.MultiTracker<MockState, MockAction, MockParameterKey>
typealias MockSubTracker = MockAnalyticsTracker<MockState, MockAction, MockParameterKey>

private var tracker: MultiTracker!

override func setUp() {
super.setUp()

tracker = MultiTracker()
}

override func tearDown() {
tracker = nil

super.tearDown()
}

// register

func testRegister_WithUniqueIDs_ShouldSucceed() {

let subTracker1 = MockSubTracker(id: "1")
let subTracker2 = MockSubTracker(id: "2")

do {
try tracker.register(subTracker1)
XCTAssertEqual(tracker.trackers.count, 1)
try tracker.register(subTracker2)
XCTAssertEqual(tracker.trackers.count, 2)
} catch {
return XCTFail("unexpected error \(error)!")
}
}

func testRegister_WithDuplicateIDs_ShouldFail() {

let subTracker = MockSubTracker()

do {
try tracker.register(subTracker)
XCTAssertEqual(tracker.trackers.count, 1)
} catch {
return XCTFail("unexpected error \(error)!")
}

do {
try tracker.register(subTracker)
} catch Analytics.MultiTrackerError.duplicateTracker(let id) {
XCTAssertEqual(id, subTracker.id)
} catch {
return XCTFail("unexpected error \(error)!")
}
}

// unregister

func testUnregister_WithExistingID_ShouldSucceed() {

let subTracker = MockSubTracker()

do {
try tracker.register(subTracker)
XCTAssertEqual(tracker.trackers.count, 1)
try tracker.unregister(subTracker)
XCTAssertEqual(tracker.trackers.count, 0)
} catch {
return XCTFail("unexpected error \(error)!")
}
}

func testUnregister_WithNonExistingIDs_ShouldFail() {

let subTracker = MockSubTracker()

do {
XCTAssertEqual(tracker.trackers.count, 0)
try tracker.unregister(subTracker)
} catch Analytics.MultiTrackerError.inexistentTracker(let id) {
XCTAssertEqual(id, subTracker.id)
} catch {
return XCTFail("unexpected error \(error)!")
}
}

// track

func testTrack_WithUniqueIDs_ShouldSucceed() {

let subTracker1 = MockSubTracker(id: "1")
let subTracker2 = MockSubTracker(id: "2")
func testTrack_WithActionEvent_ShouldCallTrackOnAlltrackers() {

do {
try tracker.register(subTracker1)
XCTAssertEqual(tracker.trackers.count, 1)
try tracker.register(subTracker2)
XCTAssertEqual(tracker.trackers.count, 2)
} catch {
return XCTFail("unexpected error \(error)!")
}
}

func testTrack_WithRegisteredtrackersAndActionEvent_ShouldCallTrackOnAlltrackers() {
let trackExpectation = self.expectation(description: "track")
trackExpectation.expectedFulfillmentCount = 2
defer { waitForExpectations(timeout: 1) }

let subTracker1 = MockSubTracker(id: "1")
let subTracker2 = MockSubTracker(id: "2")
let subTracker1 = MockSubTracker()
let subTracker2 = MockSubTracker()

do {
try tracker.register(subTracker1)
try tracker.register(subTracker2)
} catch {
return XCTFail("unexpected error \(error)!")
}
let tracker = MultiTracker(trackers: [subTracker1, subTracker2].map { $0.eraseToAnyAnalyticsTracker() })

let event = MultiTracker.Event.action(.🔨, [.language : "🇵🇹", .date : Date()])

Expand All @@ -150,20 +50,16 @@ final class MultiTrackerTestCase: XCTestCase {
tracker.track(event)
}

func testTrack_WithRegisteredtrackersAndStateEvent_ShouldCallTrackOnAlltrackers() {
func testTrack_WithStateEvent_ShouldCallTrackOnAlltrackers() {

let trackExpectation = self.expectation(description: "track")
trackExpectation.expectedFulfillmentCount = 2
defer { waitForExpectations(timeout: 1) }

let subTracker1 = MockSubTracker(id: "1")
let subTracker2 = MockSubTracker(id: "2")
let subTracker1 = MockSubTracker()
let subTracker2 = MockSubTracker()

do {
try tracker.register(subTracker1)
try tracker.register(subTracker2)
} catch {
return XCTFail("unexpected error \(error)!")
}
let tracker = MultiTracker(trackers: [subTracker1, subTracker2].map { $0.eraseToAnyAnalyticsTracker() })

let event = MultiTracker.Event.state(.screen(name: "🖼"), [.language : "🇵🇹", .date : Date()])

Expand Down