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

Initial commit #1

Closed
wants to merge 3 commits into from
Closed
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
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"]
),
]
)
83 changes: 83 additions & 0 deletions Sources/LTKAnalytics/Private/EventBus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// 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
)
}
}
58 changes: 58 additions & 0 deletions Sources/LTKAnalytics/Public/AnalyticsAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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
)
}
41 changes: 41 additions & 0 deletions Sources/LTKAnalytics/Public/EventType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// EventType.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))"
}
}
}
37 changes: 37 additions & 0 deletions Sources/LTKAnalytics/Public/Events.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// 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
))
"""
}
}
67 changes: 67 additions & 0 deletions Tests/LTKAnalyticsTests/PublicAPITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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
)
}
}
}
Loading