Analytics layer abstraction, abstract analytics reporters and collect domain-driven analytic events.

Overview

🐙 Tentacles

Current State: Work in Progress Documentation & Tests(100% completed, but needs refactoring and structuring) started but not done yet, implementation of first version is done. However, changes in public API possible before release.

Welcome to Tentacles

Tentacles are body parts that an animal uses to hold, grab or even feel things. That is what Tentacles are used for in terms of data collection in your application. It helps you to abstract analytics from specific providers, to structure your analytic events in a type-safe way and to collect meaningful domain-driven data with DomainActivity.

For further information, why abstracting a third party library make sense Benoit Pasquier wrote an article.

Features

  • Analytics layer abstraction
    • Analytics event reporting
    • Error reporting
    • Adding user attributes
  • Type-safety for events and no manual data converting between event layers
  • Domain-driven analytics with DomainActivity
  • Middleware to transform/ignore events for reporters

Analytics setup

Tentacles registers and manages AnalyticsReporter in a central entity. If we want to use a service like Firebase we need to create an implementation that conforms to AnalyticsReporting:

class FirebaseReporter: AnalyticsReporting {
    func setup() {
        FirebaseApp.configure()
    }
    func report(event: RawAnalyticsEvent) {
        Analytics.logEvent(event.name, parameters: event.attributes)
    }
    func addUserAttributes(_ attributes: AttributesValue) {
        attributes.forEach { (key, value) in
            Analytics.setUserProperty(value as? String, forName: key)
        }
    }
    func identify(with id: String) {
        Analytics.setUserID(id)
    }
    func logout() {
        Analytics.resetAnalyticsData()
    }
    func report(_ error: Error, filename: String, line: Int) {
    }
}

Registering reporters to Tentacles is easy:

let firebaseReporter = FirebaseReporter()
let tentacles = Tentacles()
tentacles.register(analyticsReporter: firebaseReporter)

In the case where we want to register a Middleware to affect events going to all of our reporters:

tentacles.register(.capitalisedAttributeKeys)

Or if we want to add a Middleware only affecting events for a specific reporter:

tentacles.register(analyticsReporter: firebaseReporter, middlewares: [.ignoreLifecycleEvents])

Defining Events & Using Analytics

Creating analytic events and attributes is easy and type safe. Defining Attributes:

struct UserContentSharingAttributes: TentaclesAttributes {
        enum Content: Encodable {
            case video
            case picture
            case story
        } 
        let content: Content
        let likedContent: Bool
        let commentedOnContent: Bool
    }
}

Adding your own AnalyticsEventCategory (Adding AnalyticsEventTrigger works the same way) :

enum MyAppAnalyticsEventCategory: String, AnalyticsEventCategory {
    case social
    var name: String {
        self.rawValue
    }
}

Defining AnalyticsEvent:

typealias UserContentSharing = AnalyticsEvent<UserContentSharingAttributes>
extension UserContentSharing {
    init(name: String = "userContentSharing",
         category: AnalyticsEventCategory = MyAppAnalyticsEventCategory.social,
         trigger: AnalyticsEventTrigger = TentaclesEventTrigger.clicked,
         otherAttributes: UserContentSharingAttributes) {
         self.init(category: category, trigger: trigger,
                   name: name, otherAttributes: otherAttributes)
    }
}
let userContentSharingAttributes = UserContentSharingAttributes(
content: .video, didUserLikeContent: true, didUserComment: false)
let userSharedContentEvent = UserContentSharing(otherAttributes: userContentSharingAttributes)
tentacles.track(userSharedContentEvent)

Defining and tracking a screen event:

typealias  AnalyticsScreenEvent = AnalyticsEvent<EmptyAttributes>
extension AnalyticsScreenEvent {
    init(name: String) {
        self.init(category: TentaclesEventCategory.screen,
                  trigger: TentaclesEventTrigger.screenDidAppear,
                  name: name,
                  otherAttributes: EmptyAttributes())
    }
}
let screenEvent = AnalyticsScreenEvent(name: "Home Screen")
tentacles.track(screenEvent)

Tracking an error is also possible:

tentacles.report(error)

Our Firebase analytics implementation does not support reporting errors, therefore this would not report anything. We would need to add a AnalyticsReporting implementation for a service like Crashlytics, it is the same process as described above for Firebase analytics.

In a case where no attributes need to be reported, EmptyAttributes must be used.

Domain driven analytics

When developing an app, it is important to understand its domain. Yes, we want to track if a user logs in or clicks on a specific button, but what we are particular interested is how are users interacting with DomainActivitys. DomainActivitys are the core functionalities that should bring the most value to your users and are specific to your app and its domain.

Tentacles offers a way to connect events that are related to the same DomainActivity. A session (identified by UUID) is a period devoted to a particular DomainActivity. The UUID identifying the session is automatically added and managed. This brings the advantage of further possibilities to analyse the data, as connections between the events can be derived. For example, as Tentacles tracks every status change of a DomainActivity with a timestamp it is easily possible to calculate the duration between when the DomainActivity started and completed.

Let's use Youtube as an example, one of their DomainActivitys a user can do on their platform is watching videos. The user experience of watching a video usually involves these steps:

graph LR
A(Open Video Page) --&gt; B(Start Video)
B --&gt; C(Pause Video)
B --&gt; D(Complete Video)
B --&gt; E(Cancel Video)
C --&gt; B

These steps are the possible status of a session related to a DomainActivity. When a DomainActivity is tracked with an DomainActivityAction, the status of the session is updated and an event forwarded. Status changes that are allowed:

graph LR
A(Open) --&gt; B(Start)
A --&gt; E
B --&gt; C(Pause)
C --&gt; B
B --&gt; D(Complete)
C --&gt; E
B --&gt; E(Cancel)

By reaching completed or canceled the session ends, and it gets deallocated. If a prohibited status update occurs, a non fatal error event is forwarded and the status is not updated. In cases where attributes are specific to a DomainActivity status, they can be added to DomainActivityAction. I.e. if a pause event needs the pausing point of the video, these attributes are then mapped to the derived analytics events.

Multiple sessions with different DomainActivitys can be managed. However, only one session for one particular DomainActivity. A DomainActivity is equal if name and attributes match, not considering additional attributes that can be added by DomainActivityAction.

Background & Foreground Applifecycle

When the app will resign, all active DomainActivity sessions are canceled and cached in memory in case the app enters foreground again. After app did become active again, all previous active sessions are reset and updated with a new identifier. For all previous active sessions, an open event is sent and then reset to the previous status that also triggers an event.

Defining & Tracking DomainActivitys

struct VideoWatchingAttributes: TentaclesAttributes {
    videoName: String
    language: String
    duration: Double  // in seconds
}

typealias VideoWatching = DomainActivity<VideoWatchingAttributes>

let attributes = VideoWatchingAttributes.Attributes(
    videoName: "Learning Swift", language: "English", duration: 3240)
let videoWatching = VideoWatching(name: "videoWatching", attributes: attributes)
let action = DomainActivityAction(status: .open, trigger: .clicked)
tracker.track(for: videoWatching, with: action)

There are convenient static functions to build an action, e.g.:

tracker.track(for: watchingVideo, with: .start())

Adding action specific attributes:

struct WatchingVideoCompletionAttributes: TentaclesAttributes {
    let secondsSkipped: Double
    let userCommented: Bool
}

let completionAttributes = WatchingVideoCompletionAttributes(
    secondsSkipped: 300, userCommented: false)
tracker.track(for: videoWatching, with: .complete(trigger: .automatically, attributes: completionAttributes))

Default attributes

CustomAttributes added via TentacleAttributes that share the same key as default attributes will overwrite default ones.

Attributes added to every event by default:

  • sessionId - A generated random uuid, to let you search events from the same session.

Attributes added to events derived from DomainActivity:

  • trigger, activity triggering the event, specified by the app
  • category - value: domainActivity
  • status - status of the DomainActivity session, possible values:
    • opened, started, paused, canceled, completed
  • domainActivitySessionId - A generated random uuid, to let you group events from the same DomainActivity session.
  • with every session status update a timestamp of the update is logged:
    • i.e. opened: 123456.00, started: 1234567.00, completed: 1234354.00,
    • if an update occurs more than once a count is added as suffix to the key:
      • i.e. started_1, started_2

Middleware

Middlewares are used to transform events and can be registered to a specific reporter or as a general Middleware to the AnalyticsRegister. If added to a specific reporter, only events reported to this reporter will be transformed. Use Cases:

  • Transforming Events
    • Editing existing attribute keys or values, i.e. capitalising the key or converting it in a different format.
    • Adding new attributes, i.e. calculate the active duration a user spent with a particular domain proposition.
  • Skipping events, i.e. skip all events for a category for a specific reporter.

Middlewares predefined:

  • calculateDomainActivityDuration - calculates the duration between two status changes for a DomainActivity.
  • skipEvent - skips events for a specific category or names
  • capitalisedAttributeKeys - capitalises keys of attributes
You might also like...
Classes-and-structures-in-swift - This source files show what is the difference between class and structure

This source files show what is the difference between class and structure You ca

Elegant library to manage the interactions between view and model in Swift
Elegant library to manage the interactions between view and model in Swift

An assistant to manage the interactions between view and model ModelAssistant is a mediator between the view and model. This framework is tailored to

Store and retrieve Codable objects to various persistence layers, in a couple lines of code!
Store and retrieve Codable objects to various persistence layers, in a couple lines of code!

tl;dr You love Swift's Codable protocol and use it everywhere, who doesn't! Here is an easy and very light way to store and retrieve Codable objects t

Disk is a powerful and simple file management library built with Apple's iOS Data Storage Guidelines in mind
Disk is a powerful and simple file management library built with Apple's iOS Data Storage Guidelines in mind

Disk is a powerful and simple file management library built with Apple's iOS Data Storage Guidelines in mind

KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

KeyPathKit Context Swift 4 has introduced a new type called KeyPath, with allows to access the properties of an object with a very nice syntax. For in

StorageManager - FileManager framework that handels Store, fetch, delete and update files in local storage
StorageManager - FileManager framework that handels Store, fetch, delete and update files in local storage

StorageManager - FileManager framework that handels Store, fetch, delete and update files in local storage. Requirements iOS 8.0+ / macOS 10.10+ / tvOS

Listens to changes in a PostgreSQL Database and via websockets.

realtime-swift Listens to changes in a PostgreSQL Database and via websockets. A Swift client for Supabase Realtime server. Usage Creating a Socket co

An Objective-C wrapper for RocksDB - A Persistent Key-Value Store for Flash and RAM Storage.

ObjectiveRocks ObjectiveRocks is an Objective-C wrapper of Facebook's RocksDB - A Persistent Key-Value Store for Flash and RAM Storage. Current RocksD

Simple, Strongly Typed UserDefaults for iOS, macOS and tvOS
Simple, Strongly Typed UserDefaults for iOS, macOS and tvOS

简体中文 DefaultsKit leverages Swift 4's powerful Codable capabilities to provide a Simple and Strongly Typed wrapper on top of UserDefaults. It uses less

Owner
Patrick
Developing iOS Applications in Swift.
Patrick
🛶Shallows is a generic abstraction layer over lightweight data storage and persistence.

Shallows Shallows is a generic abstraction layer over lightweight data storage and persistence. It provides a Storage<Key, Value> type, instances of w

Oleg Dreyman 620 Dec 3, 2022
Nora is a Firebase abstraction layer for FirebaseDatabase and FirebaseStorage

Nora is a Firebase abstraction layer for working with FirebaseDatabase and FirebaseStorage. Stop spending all that time cleaning up your view controll

Steven Deutsch 273 Oct 15, 2022
SwiftPublicSuffixList - Public Suffix List domain name checker in Swift

SwiftPublicSuffixList This library is a Swift implementation of the necessary co

Dave Poirier 0 Jan 31, 2022
A proof-of-concept WebURL domain renderer, using a port of Chromium's IDN spoof-checking logic to protect against confusable domains

WebURLSpoofChecking A proof-of-concept WebURL.Domain renderer which uses a port of Chromium's IDN spoof-checking logic (Overview, Implementation) to p

Karl 3 Aug 6, 2022
The hassle-free way to add Segment analytics to your Swift app (iOS/tvOS/watchOS/macOS/Linux).

Analytics-Swift The hassle-free way to add Segment analytics to your Swift app (iOS/tvOS/watchOS/macOS/Linux/iPadOS). Analytics helps you measure your

Segment 53 Dec 16, 2022
Facebook Analytics In-App Notifications Framework

Facebook In-App Notifications enables you to create rich and customizable in-app notifications and deliver them via push notifications, based on the a

Meta Archive 496 Nov 17, 2022
Server-driven SwiftUI - Maintain iOS apps without making app releases.

ServerDrivenSwiftUI Maintain ios apps without making app releases. Deploy changes to the server and users will receive changes within minutes. This pa

null 9 Dec 29, 2022
SQLite.swift - A type-safe, Swift-language layer over SQLite3.

SQLite.swift provides compile-time confidence in SQL statement syntax and intent.

Stephen Celis 8.7k Jan 3, 2023
A lightweight wrapper over UserDefaults/NSUserDefaults with an additional layer of AES-256 encryption

SecureDefaults for iOS, macOS Requirements • Usage • Installation • Contributing • Acknowledgments • Contributing • Author • License SecureDefaults is

Victor Peschenkov 216 Dec 22, 2022
Prephirences is a Swift library that provides useful protocols and convenience methods to manage application preferences, configurations and app-state. UserDefaults

Prephirences - Preϕrences Prephirences is a Swift library that provides useful protocols and convenience methods to manage application preferences, co

Eric Marchand 557 Nov 22, 2022