An abstraction layer for analytics in your app to keep your tracking code clean and reusable.
An abstraction layer for analytics in your app to help you organise your analytics code in your app. Think of it as a local Rudderstack or Segment.
Events.App.Opened(appName: "My App").trigger()
// Based on your rules it will be sent to your adapters (e.g. Intercom, Firebase, Mixpanel, …)
In case you use multiple analytic tools like Mixpanel, Intercom, Segment, Fabric (you name it…), you probably have the tracking code all over your project. Tracker-Aggregator is a simple interface for your project analytics that allows you to simply plug-in third party tools. This mechanism also allows you to easily migrate from one analytics tool to another.
Many tracking tools are able to track events as well as properties (usually called User properties or User attributes).
Adding new analytic tool is a matter of implementing three simple methods.
Startup time matters. Everything is dispatched to the background, even first setup, and it’s up to you when you call it. Every event or property update called before everything is ready is saved in a queue and triggered once the configuration is done.
You can define what you want to track for each analytics by setting rules. By default, everything is tracked. You can easily allow only certain events to be tracked for your AnalyticsTool1 and at the same time prohibit few events from the whole to be tracked by your AnalyticsTool2.
Event and property names are important for the data analytic to easily understand what they mean. Tracker Aggregator is using convention of Object: Action - Label where label is optional. That way you group your events into logical categories.
Examples:
Define your events and properties by creating structs and conforming them to TrackableEvent or TrackableProperty. Setup your analytic adapters and plug them into GlobalTracker (acts as a hub that forwards events and properies to plugged adapters).
// MyEvents.swift
struct Events {
struct App {
static let name = "App"
struct Opened: TrackableEvent {
let appName: String
let identifier = EventIdentifier(object: name, action: "Opened")
var metadata: [String: Any] {
return [
"name": appName
]
}
}
}
}
import Mixpanel
class MixpanelAdapter: AnalyticsAdapter {
private let token: String
init(token: String) {
self.token = token
}
// This is called when we start tracking
func configure() {
_ = Mixpanel.sharedInstance(withToken: token)
}
// This is called when the adapter receives an event
func track(event: TrackableEvent) {
// Track event in Mixpanel
Mixpanel.sharedInstance()?.track(event.identifier.stringValue, properties: event.metadata)
}
// This is called when the adapter receives a property
func track(property: TrackableProperty) {
// Track property in Mixpanel
Mixpanel.sharedInstance()?.people.set(property.identifier, to: property.trackedValue)
}
}
// AppDelegate.swift
GlobalTracker.startTracking(adapters: [MixpanelAdapter(token: "abcd")])
then somewhere in your code
Events.App.Opened(appName: "My App").trigger()
Trigger method just simply calls the event on GlobalTracker
that forwards it to all plugged analytics adapters if they support it (e.g. Intercom, Firebase, Mixpanel, …). Later you will learn how to setup adapter rules so they receive only some events or properties if needed.
To define an event you need a struct that conforms to TrackableEvent
protocol.
struct AppOpen: TrackableEvent {
let timestamp: Date
let identifier: EventIdentifier = EventIdentifier(object: "App", action: "Open")
var metadata: [String : Any] {
return ["timestamp": dateFormatter.string(from: timestamp)]
}
}
The structure is up to you but I like to do nest it based on the objects / screens. e.g.
struct Events {
struct User {
static let name = "User"
struct LoggedIn: TrackableEvent {
let identifier: EventIdentifier(object: name, action: "Logged In")
}
struct LoggedOut: TrackableEvent {
let identifier: EventIdentifier(object: name, action: "Logged Out")
}
struct ProfileUpdated: TrackableEvent {
let newName: String
let identifier: EventIdentifier(object: name, action: "Profile Updated")
var metadata: [String: Any] {
return ["new_name": newName]
}
}
}
}
This allow for nice structure (and autocompletion) when triggering events.
Events.User.LoggedIn().trigger()
Events.User.ProfileUpdated(newName: "Alfred").trigger()
Similarly to event tracking, we define a struct that represents our property by conforming it to TrackableProperty
protocol
struct Email: TrackableProperty {
let identifier: String = "email" // Name of the property
let value: String // Our value, whatever type we need. Enums are handy.
var trackedValue: TrackableValueType? { return value } // How to convert our value to TrackableValueType
// String and Int are trackable by default
}
and now we track it by calling update on it.
Email(value: "[email protected]").update()
You can also bind an event to property by implementing the func generateUpdateEvents() -> [TrackableEvent]
function. This function is called to generate events after the property is updated.
struct EmailChanged: TrackableEvent {
let newEmail: String
let identifier: EventIdentifier = EventIdentifier(object: "User", action: "Changed", label: "Email")
var metadata: [String : Any] {
return ["new_email": newEmail]
}
}
struct Email: TrackableProperty {
let identifier: String = "email" // Name of the property
let value: String // Our value, whatever type we need. Enums are handy.
var trackedValue: TrackableValueType { return value } // How to convert our value to TrackableValueType
// String and Int are trackable by default
func generateUpdateEvents() -> [TrackableEvent] {
let emailChanged = EmailChanged(newEmail: value)
return [emailChanged]
}
}
In that way you can just update a email property and the Tracker Aggregator will send the EmailChanged event for you automatically.
We create simple classes that encapsulate each analytic tool logic by conforming them to AnalyticsAdapter
protocol.
import Mixpanel
class MixpanelAdapter: AnalyticsAdapter {
private let token: String
// Optional name for nice logs. By default the class name is used (e.g. `MixpanelAdapter`)
var name: String { "Mixpanel" }
init(token: String) {
self.token = token
}
// This method is when the tracking is about to start
func configure() {
_ = Mixpanel.sharedInstance(withToken: token)
}
// This is called when the adapter receives an event to track
func track(event: TrackableEvent) {
// Track event in Mixpanel
Mixpanel.sharedInstance()?.track(event.identifier.stringValue, properties: event.metadata)
}
// This is called when the adapter receives a property to track
func track(property: TrackableProperty) {
// Track property in Mixpanel
Mixpanel.sharedInstance()?.people.set(property.identifier, to: property.trackedValue)
}
}
In your AppDelegate, for example, you can now plug-in your adapters to GlobalTracker
by calling startTracking(adapters:)
.
GlobalTracker.startTracking(adapters: [MixpanelAdapter(token: "abcd")])
You may want to allow only certain events or properties to be tracker by specific trackers. To do so, each tracker can specify EventTrackingRule
and PropertyTrackingRule
. In the rule you have to define either only allowed events/properties or prohibit only certain events/properties. Let’s prohibit Mixpanel
to track AppOpen
event.
class MixpanelAdapter: AnalyticsAdapter {
// Set event tracking rule to prohibit AppOpen event from being tracked
// That means function track(event:) for this tracker wont be called for AppOpen event
let eventTrackingRule: EventTrackingRule? = EventTrackingRule(.prohibit, types: [AppOpen.self])
private let token: String
init(token: String) {
self.token = token
}
func configure() {
_ = Mixpanel.sharedInstance(withToken: token)
}
func track(event: TrackableEvent) {
// Track event in Mixpanel
Mixpanel.sharedInstance()?.track(event.identifier.stringValue, properties: event.metadata)
}
func track(property: TrackableProperty) {
// Track property in Mixpanel
Mixpanel.sharedInstance()?.people.set(property.identifier, to: property.trackedValue)
}
}
There are two types of rules .allow
or .prohibit
.
The adapter only receives events included in the allowed types.
let eventTrackingRule: EventTrackingRule? = EventTrackingRule(.allow, types:
[
Events.App.Open.self,
Events.User.LogIn.self
]
)
In this example the tracker will not receive any other event than the 2 defined in the rule.
The adapter receives all events beside the events included in the prohibited types.
let eventTrackingRule: EventTrackingRule? = EventTrackingRule(.allow, types:
[
Events.App.Open.self,
Events.User.LogIn.self
]
)
In this example the tracker will receive all events but the 2 defined in the rule.
The same mechanism works for properies, just define propertyTrackingRule
in your adapter.
In case there’s a specific way how to track certain property, just use switch
or if
on the property and define required tracking code. For example here is custom handling of Intercom tracker properties
func track(property: TrackableProperty) {
switch property {
case is Property.Email:
let attrs = ICMUserAttributes()
attrs.email = property.trackedValue?.stringValue
Intercom.updateUser(attrs)
case let property as Property.PushNotificationToken:
Intercom.setDeviceToken(property.value)
default:
let attrs = ICMUserAttributes()
attrs.customAttributes = [property.identifier: property.trackedValue ?? ""]
Intercom.updateUser(attrs)
}
}
There are 3 logging levels - .none
, .info
, .verbose
.
GlobalTracker.loggingLevel = .info // default is .none
The info prints only the name of the event and where it was sent
-[Intercom]: EVENT TRIGGERED - 'Event Detail: Viewed'
Verbose prints also the metadata.
-[Intercom]: EVENT TRIGGERED - 'Event Detail: Viewed'
> event_id: 21421
> state: rendering
By default things are just print()
ed. If you use custom logging system you can easily integrate it by setting your own log callback.
GlobalTracker.log { message in
SwiftyBeaver.log.info(message) // Log with your favourite logging system
}
Just copy TrackerAggregator.swift
to your project.
Developed and maintained by Ales Kocur ([email protected]).
Tracker Aggregator is under MIT license. See the LICENSE file for more info.