可组合的分析

一种可组合、解耦且可测试的方式,用于向任何 TCA 项目添加分析功能,而不会将分析代码和工作代码混在一起。

基本用法

Composable Analytics 提供了一个 AnalyticsReducer,它为解包和发送分析事件到项目中的 @Dependency(\\.analyticsClient) 提供了所有工作逻辑。默认情况下,analyticsClient 依赖项设置为 unimplemented,因此首先您应该将该依赖项添加到您的 store 中。

在您首次创建 Store 时,在应用程序的入口点,您可以在此处更新分析。此包提供了一个 .consoleLogger 客户端,您也可以添加自己的客户端。

Store(
  initialState: App.State(),
  reducer: App()
    .dependency(\.analyticsClient, AnalyticsClient.consoleLogger)
)

然后在应用程序中的任何 Reducer 中,您可以将 AnalyticsReducer 添加到 body 中。这是通过一个函数创建的,该函数接受 stateaction 并返回一个可选的 AnalyticsData

struct App: Reducer {
  struct State {
    var title: String
  }

  enum Action {
    case buttonTapped
  }

  var body: some ReducerOf<Self> {
    AnalyticsReducer { state, action in
      // state here is immutable so there is no way for your analytics to interfere with your app.
      switch action {
      case .buttonTapped:
        return  .event(name: "AppButtonTapped", properties: ["title": state.title])
      }
    }
  
    Reduce<State, Action> { state, action in
      // your normal app logic sits here unchanged
    }
  }
}

这使您的所有分析代码都脱离了工作代码,但仍然在一个易于查看和推理您的应用程序正在发送哪些分析的位置。

由于大多数分析可能是不带任何属性的事件,因此 AnalyticsData 可以用字符串字面量表示。因此,.event(name: "SomeName")"SomeName" 是等效的。

作为个人偏好,我倾向于在末尾使用 default: return nil。当您不希望发送分析时,AnalyticsReducer 会为任何 action/state 组合返回 nil。因此,将它们全部包装在 switch 语句末尾的 default case 中,而不是列出所有 action 并从每个 action 返回 nil,要方便得多。

状态更改分析

您现在可以从状态更改触发分析。

如果您的状态像...

struct State {
  var count: Int
}

现在,您可以通过向 reducer 添加 .analyticsOnChange,在计数更改时添加分析。

Reduce<State, Action> { state, action in
  // feature reducer 
}
.analyticsOnChange(of: \.count) { oldValue, newValue in
  return .event(
    name: "countChanged", 
    properties: [
      "from": "\(oldValue)",
      "to": "\(newValue)",
    ]
  )
}

自定义分析客户端

此包仅提供用于记录到控制台的分析客户端。可以作为 AnalyticsClient.consoleLogger 访问,但是您可以非常轻松地添加自己的自定义客户端。

例如,您可能希望将分析记录到 Firebase。在这种情况下,您可以通过扩展 AnalyticsClient 来添加自己的客户端...

import Firebase
import FirebaseCrashlytics
import ComposableAnalytics

public extension AnalyticsClient {
  static var firebaseClient: Self {
    return .init(
      sendAnalytics: { analyticsData in
        switch analyticsData {
        case let .event(name: name, properties: properties):
          Firebase.Analytics.logEvent(name, parameters: properties)

        case .userId(let id):
          Firebase.Analytics.setUserID(id)
          Crashlytics.crashlytics().setUserID(id)

        case let .userProperty(name: name, value: value):
          Firebase.Analytics.setUserProperty(value, forName: name)

        case .screen(name: let name):
          Firebase.Analytics.logEvent(AnalyticsEventScreenView, parameters: [
            AnalyticsParameterScreenName: name
          ])

        case .error(let error):
          Crashlytics.crashlytics().record(error: error)
        }
      }
    )
  }
}

这可能是您的 Firebase 实现。然后,您可以通过与您想要使用的任何其他客户端合并,将其添加到 store 中...

let analytics = AnalyticsClient.merge(
  // this merges multiple analytics clients into a single instance
  .consoleLogger,
  .firebaseClient
)

Store(
  initialState: App.State(),
  reducer: App()
    .dependency(\.analyticsClient, analytics)
)

测试

这符合 TCA 的测试方式。因为您的所有分析都是使用 Effects 发送的。此包提供了一个 expect 函数,可以用于轻松地告诉您的测试您在测试期间期望哪些分析...

import XCTest
import ComposableArchitecture
@testable import App

@MainActor
class AppTests: XCTestCase {
  func testButtonTap() async throws {
    let store = TestStore(
      initialState: App.State.init(title: "Hello, world!"),
      reducer: App()
    )

    store.dependencies.analyticsClient.expect(
      .event(name: "AppButtonTapped", properties: ["title": "Hello, world!"])
    )

    await store.send(.buttonTapped)
  }
}

这种期望是穷尽的。

如果期望分析但未收到,则测试将失败。如果您收到您不期望的分析,测试也会失败。

安装

您可以通过将 https://github.com/oliverfoggin/swift-composable-analytics 添加到项目的 SPM 包中,将 ComposableAnalytics 添加到您的项目中。