Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dokun1-ltk committed May 3, 2024
1 parent 15be46c commit 77b92f7
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift

name: Build and Test

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: macos-14

steps:
- uses: actions/checkout@v3
- name: Switch Xcode Version
run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer
- name: Build
run: swift build -v
- name: Run tests
run: swift test -v
40 changes: 40 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "LTKAnalytics-iOS",
platforms: [
.macOS(
.v14
),
.iOS(
.v16
),
.tvOS(
.v16
),
.watchOS(
.v9
),
.visionOS(
.v1
)
],
products: [
.library(
name: "LTKAnalytics",
targets: ["LTKAnalytics"]
),
],
targets: [
.target(
name: "LTKAnalytics"
),
.testTarget(
name: "LTKAnalyticsTests",
dependencies: ["LTKAnalytics"]
),
]
)
59 changes: 59 additions & 0 deletions Sources/LTKAnalytics/Private/EventBus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// EventBus.swift
//
//
// Created by David Okun on 5/3/24.
//

import Foundation
import os

internal actor EventBus {
private let logger = os.Logger(
subsystem: "LTKAnalytics",
category: "EventBus"
)
private var queue = [AnalyticsEvent]()

init() {
logger.trace(
#function
)
}

internal func addEventToQueue(
_ event: AnalyticsEvent
) async throws {
// TODO: do this in a better way
let existingEvents = queue.filter { $0.id == event.id}
if existingEvents.count > 0 {
throw AnalyticsError.duplicateEvent(id: event.id)
}
if await checkForPotentialRapidFire(event) {
await dispatchEvent(event)
logger.critical("potential duplicate event fired: \(event.loggingDescription)")
// throw error of potential duplicate, but this is more of a warning
throw AnalyticsError.potentialRapidFire(type: event.type)
}
await dispatchEvent(event)
}

private func dispatchEvent(_ event: AnalyticsEvent) async {
queue.append(event)
// TODO: actually send events to a stream where they are processed and sent off
}

private func checkForPotentialRapidFire(_ event: AnalyticsEvent) async -> Bool {
let existingEvents = queue.filter { existingEvent in
existingEvent.type == event.type &&
abs(existingEvent.timestamp.timeIntervalSince(event.timestamp)) <= 0.25
}
return existingEvents.count > 0
}

deinit {
logger.trace(
#function
)
}
}
33 changes: 33 additions & 0 deletions Sources/LTKAnalytics/Public/AnalyticsAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

import Foundation
import os

public actor LTKAnalytics {
private let logger = os.Logger(subsystem: "LTKAnalytics", category: "PublicAPI")
private let eventBus = EventBus()

public init() {
logger.trace(#function)
}

@discardableResult
public func recordEvent(_ type: EventType, attributes: [String: String]? = nil) async throws -> UUID {
let recordedEvent = AnalyticsEvent(type: type, attributes: attributes)
do {
try await eventBus.addEventToQueue(recordedEvent)
return recordedEvent.id
} catch AnalyticsError.duplicateEvent(let id) {
logger.warning("received duplicate event: \(id, privacy: .public)")
throw AnalyticsError.duplicateEvent(id: id)
} catch AnalyticsError.potentialRapidFire(let type) {
logger.critical("potential duplicate event fired: \(type.description)")
return recordedEvent.id
}
}

deinit {
logger.trace(#function)
}
}
17 changes: 17 additions & 0 deletions Sources/LTKAnalytics/Public/AnalyticsError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// AnalyticsError.swift
//
//
// Created by David Okun on 5/3/24.
//

import Foundation

public enum AnalyticsError: Error {
case duplicateEvent(
id: UUID
)
case potentialRapidFire(
type: EventType
)
}
33 changes: 33 additions & 0 deletions Sources/LTKAnalytics/Public/EventType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// File.swift
//
//
// Created by David Okun on 5/3/24.
//

import Foundation

public enum EventType: Equatable {
case applicationBecameActive
case applicationFinishedLoading
case loadedHomeFeed
case postImpression(id: String)
case profileImpression(id: String)
}

extension EventType: CustomStringConvertible {
public var description: String {
switch self {
case .applicationBecameActive:
return "Application Became Active"
case .applicationFinishedLoading:
return "Application Finished Loading"
case .loadedHomeFeed:
return "Loaded Home Feed"
case .postImpression(let id):
return "Post Impression (ID: \(id))"
case .profileImpression(let id):
return "Profile Impression (ID: \(id))"
}
}
}
35 changes: 35 additions & 0 deletions Sources/LTKAnalytics/Public/Events.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// AnalyticsEvent.swift
//
//
// Created by David Okun on 5/3/24.
//

import Foundation

public struct AnalyticsEvent: Identifiable {
internal let type: EventType
internal let attributes: [String: String]?
internal let timestamp: Date
public let id: UUID

init(
type: EventType,
attributes: [String : String]? = nil
) {
self.id = UUID()
self.timestamp = Date()
self.type = type
self.attributes = attributes
}

public var loggingDescription: String {
"""
ID: \(id.uuidString)
type: \(type.description)
timestamp: \(timestamp.ISO8601Format())
attributes: \(String(describing: attributes))
"""
}
}
39 changes: 39 additions & 0 deletions Tests/LTKAnalyticsTests/PublicAPITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import XCTest
@testable import LTKAnalytics

final class PublicAPITests: XCTestCase {
private var publicAPI: LTKAnalytics?

override func setUp() async throws {
publicAPI = LTKAnalytics()
}

func testExample() async throws {
do {
let eventID = try await publicAPI?.recordEvent(.loadedHomeFeed)
XCTAssertNotNil(eventID)
} catch {
XCTFail(error.localizedDescription)
}
}

func testThatTwoSeparateEventsHaveSeparateIDs() async throws {
do {
let first = try await publicAPI?.recordEvent(.loadedHomeFeed)
let second = try await publicAPI?.recordEvent(.postImpression(id: "8675309"))
XCTAssertNotEqual(first, second, "two events of different types fired should have different IDs")
} catch {
XCTFail(error.localizedDescription)
}
}

func testThatTwoIdenticalEventsHaveSeparateIDs() async throws {
do {
let first = try await publicAPI?.recordEvent(.loadedHomeFeed)
let second = try await publicAPI?.recordEvent(.loadedHomeFeed)
XCTAssertNotEqual(first, second, "two events of the same type fired should have different IDs")
} catch {
XCTFail(error.localizedDescription)
}
}
}

0 comments on commit 77b92f7

Please sign in to comment.