Parchment-ios

该项目提供了一个记录器 (logger) 的实现,用于跟踪用户行为和系统行为。通过该实现,许多与日志相关的流程可以被标准化和隐藏。

这在以下情况下特别有用:

安装

如果您正在使用 Xcode 项目,您可以通过从 Xcode 指定此存储库来为此软件包添加依赖项。

如果您正在使用 Swift Package 项目,您可以通过将以下描述添加到 Package.swift 来为此软件包添加依赖项。

dependencies: [
    .product(name: "ParchmentCore", package: "Parchment"),
    // The following statements are optional
    .product(name: "Parchment", package: "Parchment"),
]

项目概览

ParchmentCore

它包含日志处理和事件日志定义的主要逻辑和定义。

Parchment

提供符合 ParchmentCore 提供的协议的标准实现。 如果您实现自己的缓冲区和调度器,则无需添加任何依赖项。

有关更多详细信息,请参见 自定义 部分。

eventgen

这是一个实验性 API,可从以自然语言编写的事件日志规范生成 Swift 代码。

有关更多详细信息,请参见 文档 部分。

使用方法

本节介绍该项目的基本用法。

定义日志事件

// with struct
struct Event: Loggable {
    public let eventName: String
    public let parameters: [String : Any]
}

// with enum
enum Event: Loggable {
  case impletion(screen: String)

  var eventName: String {
    ...
  }

  var parameters: [String : Any] {
    ...
  }
}

或者,有两种方法可以在不定义日志事件的情况下执行此操作。

包装日志服务

使用 LoggerComponent 包装现有日志记录器实现,例如 FirebaseAnalytics 和端点。

extension LoggerComponentID {
    static let analytics = LoggerComponentID("Analytics")
}

struct Analytics: LoggerComponent {
    static let id: LoggerComponentID = .analytics

    func send(_ event: Loggable) async -> Bool {
        let url = URL(string: "https://your-endpoint/...")!
        request.httpBody = convertBody(from: event)

        return await withCheckedContinuation { continuation in
            let task = URLSession.shared.dataTask(with: request) { data, response, error in

                if let error = error {
                    print(error)
                    continuation.resume(returning: false)
                    return
                }

                guard
                    let response = response as? HTTPURLResponse,
                    (200..<300).contains(response.statusCode)
                else {
                    continuation.resume(returning: false)
                    return
                }

                continuation.resume(returning: true)
            }
            task.resume()
        }
    }
}

发送事件

初始化 LoggerBundler 并使用它发送日志。

let analytics = Analytics()
let logger = LoggerBundler(components: [analytics])

await logger.send(
    TrackingEvent(eventName: "hoge", parameters: [:]),
    with: .init(policy: .immediately)
)

await logger.send(.impletion(screen: "Home"))

await logger.send([\.eventName: "tapButton", \.parameters: ["ButtonID": 1]])

更多信息

请参阅下面的 API 文档(WIP)。

自定义

本节介绍如何自定义记录器的行为。

创建一个符合 Mutation 协议的类型

Mutation 将一个日志转换为另一个日志。

如果您希望将参数添加到所有日志,这将非常有用。

要创建类型并将其设置为记录器,请按如下方式编写。

// An implementation similar to this can be found in Parchment

struct DeviceDataMutation: Mutation {
    private let deviceParams = [
        "Model": UIDevice.current.name,
        "OS": UIDevice.current.systemName,
        "OS Version": UIDevice.current.systemVersion
    ]

    public func transform(_ event: Loggable, id: LoggerComponentID) -> Loggable {
        let log: LoggableDictonary = [
            \.eventName: event.eventName,
            \.parameters: event.parameters.merging(deviceParams) { left, _ in left }
        ]
        return log
    }
}

logger.mutations.append(DeviceDataMutation())

扩展 LoggerComponentID

LoggerComponentID 是唯一标识记录器的 ID。

通过扩展 LoggerComponentID,可以如下所示控制日志的目标位置。

extension LoggerComponentID {
    static let firebase: Self = .init("firebase")
    static let myBadkend: Self = .init("myBadkend")
}

await logger.send(.tap, with: .init(scope: .exclude([.firebase, .myBadkend])))

await logger.send(.tap, with: .init(scope: .only([.myBadkend])))

创建一个符合 BufferedEventFlushScheduler 协议的类型

BufferedEventFlushScheduler 确定获取缓冲区中日志数据的时间。 要创建类型并将其设置为记录器,请按如下方式编写。

// An implementation similar to this can be found in Parchment
final class RegularlyPollingScheduler: BufferedEventFlushScheduler {
    public static let `default` = RegularlyPollingScheduler(timeInterval: 60)

    let timeInterval: TimeInterval

    var lastFlushedDate: Date = Date()

    private weak var timer: Timer?

    public init(
        timeInterval: TimeInterval,
    ) {
        self.timeInterval = timeInterval
    }

    public func schedule(with buffer: TrackingEventBufferAdapter) async -> AsyncThrowingStream<[BufferRecord], Error> {
        return AsyncThrowingStream { continuation in
            let timer = Timer(fire: .init(), interval: 1, repeats: true) { _ in
                Task { [weak self] in
                    await self?.tick(with: buffer) {
                        continuation.yield($0)
                    }
                }
            }
            RunLoop.main.add(timer, forMode: .common)
            self.timer = timer
        }
    }

    public func cancel() {
        timer?.invalidate()
    }

    private func tick(with buffer: TrackingEventBufferAdapter, didFlush: @escaping ([BufferRecord])->()) async {
        guard await buffer.count() > 0 else { return }

        let flush = {
            let records = await buffer.load()
            didFlush(records)
        }

        let timeSinceLastFlush = abs(self.lastFlushedDate.timeIntervalSinceNow)
        if self.timeInterval < timeSinceLastFlush {
            await flush()
            self.lastFlushedDate = Date()
            return
        }
    }
}

let logger = LoggerBundler(
    components: [...],
    buffer: TrackingEventBuffer = ...,
    loggingStrategy: BufferedEventFlushScheduler = RegularlyPollingScheduler.default
)

创建一个符合 TrackingEventBuffer 协议的类型

TrackingEventBuffer 是一个保存日志的缓冲区。

Parchment 定义了一个类 SQLiteBuffer,它使用 SQLite 来存储日志。

此实现可以替换为与 TrackingEventBuffer 兼容的类。