{"id":13465320,"url":"https://github.com/kafejo/Tracker-Aggregator","last_synced_at":"2025-03-25T16:31:34.888Z","repository":{"id":61350245,"uuid":"101397907","full_name":"kafejo/Tracker-Aggregator","owner":"kafejo","description":"An abstraction layer for analytics in your app to keep your tracking code clean and reusable.","archived":false,"fork":false,"pushed_at":"2022-10-13T12:54:56.000Z","size":243,"stargazers_count":22,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-07-31T15:01:25.928Z","etag":null,"topics":["analytic-adapters","analytics","analytics-tracking","swift","tracker-aggregator"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kafejo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-08-25T11:34:07.000Z","updated_at":"2024-04-29T13:26:04.000Z","dependencies_parsed_at":"2022-10-15T16:34:32.414Z","dependency_job_id":null,"html_url":"https://github.com/kafejo/Tracker-Aggregator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kafejo%2FTracker-Aggregator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kafejo%2FTracker-Aggregator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kafejo%2FTracker-Aggregator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kafejo%2FTracker-Aggregator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kafejo","download_url":"https://codeload.github.com/kafejo/Tracker-Aggregator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222088535,"owners_count":16928976,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analytic-adapters","analytics","analytics-tracking","swift","tracker-aggregator"],"created_at":"2024-07-31T15:00:27.136Z","updated_at":"2024-10-29T17:30:41.239Z","avatar_url":"https://github.com/kafejo.png","language":"Swift","funding_links":[],"categories":["Libs","Data and Storage"],"sub_categories":["Analytics"],"readme":"\u003cimg src=\"https://github.com/kafejo/Tracker-Aggregator/blob/master/Assets/logo-text@2x.png\" width=\"302\" /\u003e\n\nAn abstraction layer for analytics in your app to help you organise your analytics code in your app. Think of it as a local [Rudderstack](https://www.rudderstack.com) or [Segment](https://segment.com).\n\n\u003cimg src=\"https://github.com/kafejo/Tracker-Aggregator/blob/master/Assets/graph@2x.png\" width=\"555\"/\u003e\n\n```swift\nEvents.App.Opened(appName: \"My App\").trigger()\n// Based on your rules it will be sent to your adapters (e.g. Intercom, Firebase, Mixpanel, …)\n```\n\n# Why?\nIn 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.\n\n## Features\n#### 👉 Support both Events and Properties\nMany tracking tools are able to track events as well as properties (usually called User properties or User attributes). \n\n#### 🕺 Easily plug-in new analytics of your choice\nAdding new analytic tool is a matter of implementing three simple methods.\n\n#### 🏃 Asynchronous\nStartup 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.\n\n#### ✍️ Configurable\nYou 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.\n\n#### 👮 Conventional\nEvent 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. \nExamples:\n* App: Opened\n* App: Closed\n* Song: Tapped - More Info\n* Artist: Viewed \n\n\n# How does it work?\nDefine 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). \n\n## Quick start\n\n### Define your events\n\n```swift\n// MyEvents.swift\nstruct Events {\n    struct App {\n        static let name = \"App\"\n        \n        struct Opened: TrackableEvent {\n            let appName: String\n\n            let identifier = EventIdentifier(object: name, action: \"Opened\")\n\n            var metadata: [String: Any] {\n                return [\n                    \"name\": appName\n                ]\n            }\n        }\n    }\n}\n```\n\n### Create adapters\n\n```swift\nimport Mixpanel\n\nclass MixpanelAdapter: AnalyticsAdapter {\n\n    private let token: String\n\n    init(token: String) {\n        self.token = token\n    }\n \n    // This is called when we start tracking\n    func configure() {\n        _ = Mixpanel.sharedInstance(withToken: token)\n    }\n\n    // This is called when the adapter receives an event\n    func track(event: TrackableEvent) {\n        // Track event in Mixpanel\n        Mixpanel.sharedInstance()?.track(event.identifier.stringValue, properties: event.metadata)\n    }\n\n    // This is called when the adapter receives a property\n    func track(property: TrackableProperty) {\n        // Track property in Mixpanel\n        Mixpanel.sharedInstance()?.people.set(property.identifier, to: property.trackedValue)\n    }\n}\n```\n\n### Start tracking\n\n```swift\n// AppDelegate.swift\nGlobalTracker.startTracking(adapters: [MixpanelAdapter(token: \"abcd\")])\n```\n\nthen somewhere in your code\n\n```swift\nEvents.App.Opened(appName: \"My App\").trigger()\n```\n\nTrigger 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.\n\n# More Details \n\n## Defining events\n\nTo define an event you need a struct that conforms to `TrackableEvent` protocol. \n\n```swift\nstruct AppOpen: TrackableEvent {\n    let timestamp: Date\n    \n    let identifier: EventIdentifier = EventIdentifier(object: \"App\", action: \"Open\")\n    var metadata: [String : Any] { \n        return [\"timestamp\": dateFormatter.string(from: timestamp)] \n    }\n}\n```\n\nThe structure is up to you but I like to do nest it based on the objects / screens. e.g.\n\n```swift\nstruct Events {\n    struct User {\n        static let name = \"User\"\n        \n        struct LoggedIn: TrackableEvent {\n            let identifier: EventIdentifier(object: name, action: \"Logged In\")\n        }\n        \n        struct LoggedOut: TrackableEvent {\n            let identifier: EventIdentifier(object: name, action: \"Logged Out\")\n        }\n        \n        struct ProfileUpdated: TrackableEvent {\n            let newName: String\n            let identifier: EventIdentifier(object: name, action: \"Profile Updated\")\n            var metadata: [String: Any] {\n                return [\"new_name\": newName]\n            }\n        }\n    }\n}\n```\n\nThis allow for nice structure (and autocompletion) when triggering events.\n\n```swift\nEvents.User.LoggedIn().trigger()\nEvents.User.ProfileUpdated(newName: \"Alfred\").trigger()\n```\n\n## Track property\n\nSimilarly to event tracking, we define a struct that represents our property by conforming it to `TrackableProperty` protocol\n\n```swift\nstruct Email: TrackableProperty {\n    let identifier: String = \"email\"                          // Name of the property\n    let value: String                                         // Our value, whatever type we need. Enums are handy.\n\n    var trackedValue: TrackableValueType? { return value }     // How to convert our value to TrackableValueType\n                                                             // String and Int are trackable by default\n}\n```\n\nand now we track it by calling update on it.\n\n```swift\nEmail(value: \"test@test.com\").update()\n```\n\n### Binding Event to Property\n\nYou can also bind an event to property by implementing the `func generateUpdateEvents() -\u003e [TrackableEvent]` function. This function is called to generate events after the property is updated.\n\n```swift\nstruct EmailChanged: TrackableEvent {\n    let newEmail: String\n    \n    let identifier: EventIdentifier = EventIdentifier(object: \"User\", action: \"Changed\", label: \"Email\")\n    var metadata: [String : Any] { \n        return [\"new_email\": newEmail] \n    }\n}\n\nstruct Email: TrackableProperty {\n    let identifier: String = \"email\"                          // Name of the property\n    let value: String                                         // Our value, whatever type we need. Enums are handy.\n\n    var trackedValue: TrackableValueType { return value }     // How to convert our value to TrackableValueType\n                                                              // String and Int are trackable by default\n    func generateUpdateEvents() -\u003e [TrackableEvent] {\n        let emailChanged = EmailChanged(newEmail: value)\n        return [emailChanged]\n    }\n}\n```\n\nIn that way you can just update a email property and the Tracker Aggregator will send the EmailChanged event for you automatically.\n\n## Plug-in analytics tool\n\nWe create simple classes that encapsulate each analytic tool logic by conforming them to `AnalyticsAdapter` protocol. \n\n```swift\nimport Mixpanel\n\nclass MixpanelAdapter: AnalyticsAdapter {\n\n    private let token: String\n\n    // Optional name for nice logs. By default the class name is used (e.g. `MixpanelAdapter`)\n    var name: String { \"Mixpanel\" }\n    \n    init(token: String) {\n        self.token = token\n    }\n \n    // This method is when the tracking is about to start\n    func configure() {\n        _ = Mixpanel.sharedInstance(withToken: token)\n    }\n\n    // This is called when the adapter receives an event to track\n    func track(event: TrackableEvent) {\n        // Track event in Mixpanel\n        Mixpanel.sharedInstance()?.track(event.identifier.stringValue, properties: event.metadata)\n    }\n\n    // This is called when the adapter receives a property to track\n    func track(property: TrackableProperty) {\n        // Track property in Mixpanel\n        Mixpanel.sharedInstance()?.people.set(property.identifier, to: property.trackedValue)\n    }\n}\n```\n\nIn your AppDelegate, for example, you can now plug-in your adapters to `GlobalTracker` by calling `startTracking(adapters:)`.\n\n```swift\nGlobalTracker.startTracking(adapters: [MixpanelAdapter(token: \"abcd\")])\n```\n\n## Rules\nYou 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.\n\n```swift\nclass MixpanelAdapter: AnalyticsAdapter {\n    \n    // Set event tracking rule to prohibit AppOpen event from being tracked\n    // That means function track(event:) for this tracker wont be called for AppOpen event\n    let eventTrackingRule: EventTrackingRule? = EventTrackingRule(.prohibit, types: [AppOpen.self])\n\n    private let token: String\n\n    init(token: String) {\n        self.token = token\n    }\n\n    func configure() {\n        _ = Mixpanel.sharedInstance(withToken: token)\n    }\n\n    func track(event: TrackableEvent) {\n        // Track event in Mixpanel\n        Mixpanel.sharedInstance()?.track(event.identifier.stringValue, properties: event.metadata)\n    }\n\n    func track(property: TrackableProperty) {\n        // Track property in Mixpanel\n        Mixpanel.sharedInstance()?.people.set(property.identifier, to: property.trackedValue)\n    }\n}\n```\n\nThere are two types of rules `.allow` or `.prohibit`. \n\n### Allow\nThe adapter only receives events included in the allowed types.\n\n```swift\nlet eventTrackingRule: EventTrackingRule? = EventTrackingRule(.allow, types: \n    [\n        Events.App.Open.self,\n        Events.User.LogIn.self\n    ]\n)\n```\nIn this example the tracker will not receive any other event than the 2 defined in the rule.\n\n### Prohibit\nThe adapter receives all events beside the events included in the prohibited types.\n\n```swift\nlet eventTrackingRule: EventTrackingRule? = EventTrackingRule(.allow, types: \n    [\n        Events.App.Open.self,\n        Events.User.LogIn.self\n    ]\n)\n```\nIn this example the tracker will receive all events but the 2 defined in the rule.\n\nThe same mechanism works for properies, just define `propertyTrackingRule` in your adapter.\n\n## Exceptions\n\nIn 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\n\n```swift\nfunc track(property: TrackableProperty) {\n\n    switch property {\n    case is Property.Email:\n        let attrs = ICMUserAttributes()\n        attrs.email = property.trackedValue?.stringValue\n        Intercom.updateUser(attrs)\n    case let property as Property.PushNotificationToken:\n        Intercom.setDeviceToken(property.value)\n    default:\n        let attrs = ICMUserAttributes()\n        attrs.customAttributes = [property.identifier: property.trackedValue ?? \"\"]\n        Intercom.updateUser(attrs)\n    }\n}\n```\n\n# Logging\n\nThere are 3 logging levels - `.none`, `.info`, `.verbose`. \n```swift\nGlobalTracker.loggingLevel = .info // default is .none\n```\n\n## Info \n\nThe info prints only the name of the event and where it was sent\n```\n-[Intercom]: EVENT TRIGGERED - 'Event Detail: Viewed'\n```\n\n## Verbose\nVerbose prints also the metadata.\n\n```\n-[Intercom]: EVENT TRIGGERED - 'Event Detail: Viewed' \n \u003e event_id: 21421\n \u003e state: rendering\n```\n\n## Custom Logger\nBy default things are just `print()`ed. If you use custom logging system you can easily integrate it by setting your own log callback.\n\n```swift\nGlobalTracker.log { message in\n    SwiftyBeaver.log.info(message) // Log with your favourite logging system\n}\n```\n\n# Installation\nJust copy `TrackerAggregator.swift` to your project.\n\n# License\nDeveloped and maintained by Ales Kocur (ales@spurrapp.com).\nTracker Aggregator is under MIT license. See the LICENSE file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkafejo%2FTracker-Aggregator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkafejo%2FTracker-Aggregator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkafejo%2FTracker-Aggregator/lists"}