Replies: 1 comment 9 replies
-
Hi, I think that one of the options is to do something like that func makeReducer() -> Reducer<AppState, AppAction, AppEnvironment> {
Reducer.combine(
analyticsReducer,
appReducer.debugActions()
)
} where analyticsReducer is Reducer<
AppState,
AppAction,
AppEnvironment
> { state, action, environment in
let appState = state
let appAction = appAction
environment.backgroundQueue.schedule {
switch appAction {
case .someAction:
environment.analyticsClient.trackEvent(
AnalyticEvents.SomeEvent(parameter: .init(appState.someParameter))
...
)
}
}
return .none
} You can handle analytics in a reducer without extracting it to a background queue, but it probably does not modify state and you may need some custom logic for extracting actions (maybe I'll share some code for it later), so I think there is no need to handle it on your appLogicQueue (probably the mainQueue) Also, I use https://github.com/hectr/swift-event-tracker for generalising tracking for every analytics service I need. AnalyticsClientExample: public struct AnalyticsClient {
public init(
trackEvent: @escaping (AnalyticEvent) -> Void,
setUserProperty: @escaping (UserProperty) -> Void
) {
self.trackEvent = trackEvent
self.setUserProperty = setUserProperty
}
public var trackEvent: (AnalyticEvent) -> Void
public var setUserProperty: (UserProperty) -> Void
} AnalyticEvent public protocol AnalyticEvent {
var name: String { get }
var metadata: [String: String] { get }
}
extension AnalyticEvent {
public func eraseToAnyEvent() -> AnyAnalyticEvent {
AnyAnalyticEvent(self)
}
}
public enum AnalyticEvents {
public enum Parameters {}
}
public struct AnyAnalyticEvent: AnalyticEvent, Codable {
public init(_ event: AnalyticEvent) {
self.init(name: event.name, metadata: event.metadata)
}
public init(name: String, metadata: [String: String]) {
self.name = name
self.metadata = metadata
}
public let name: String
public let metadata: [String: String]
}
// Example for string convertible number
extension AnalyticEvents.Parameters {
public struct Number<Value: Numeric>: Codable {
public let value: Value
public init(_ value: Value) {
self.value = value
}
private struct DecodingError_Unimplemented: Error {
let localizedDescription = "Decoder for \(Value.self) is not implemented"
}
public init(from decoder: Decoder) throws {
throw DecodingError_Unimplemented()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("\(value)")
}
}
}
extension AnalyticEvents.Parameters.Number: ExpressibleByIntegerLiteral
where Value: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Value.IntegerLiteralType) {
self.init(.init(integerLiteral: value))
}
}
extension AnalyticEvents.Parameters.Number: LosslessStringConvertible, CustomStringConvertible
where Value: LosslessStringConvertible {
public var description: String { value.description }
public init?(_ description: String) {
guard let value = Value(description) else { return nil }
self.init(value)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let stringValue = try container.decode(String.self)
guard let value = Value(stringValue.trimmingCharacters(in: ["\""]))
else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Expected String containing \(Value.self) value"
)
}
self.value = value
}
}
extension AnalyticEvents.Parameters.Number: ExpressibleByFloatLiteral
where Value: ExpressibleByFloatLiteral {
public init(floatLiteral value: Value.FloatLiteralType) {
self.init(.init(floatLiteral: value))
}
} UserProperty public protocol UserProperty {
var name: String { get }
var value: String { get }
}
extension UserProperty {
public func eraseToAnyUserProperty() -> AnyUserProperty {
AnyUserProperty(self)
}
}
public enum UserProperties {}
public struct AnyUserProperty: UserProperty, Codable {
public init(_ userProperty: UserProperty) {
self.init(name: userProperty.name, value: userProperty.value)
}
public init(name: String, value: String) {
self.name = name
self.value = value
}
public let name: String
public let value: String
} I convert encoded JSON for each event to Dictionary for metadata, but maybe it's better just to create metadata fields in your events if you need custom event parameter names (I use CodingKeys enum, but it requires the same amount of work, plus runtime computations for such conversions). Event name is taken from the type. extension AnalyticEvent {
public static var name: String { String(describing: self).separatingCamelCase(using: " ") }
public var name: String { Self.name }
public var metadata: [String: String] { self.asStringDictionary() ?? [:] }
} |
Beta Was this translation helpful? Give feedback.
-
Having gone through all the examples and isowords one of the more practical questions at hand is what would be the most elegant and practical way to implement somethings like logging/analytics. One approach would be to wrap the app reducer with a higher order reducer however I would like to hear what others have tried and would advice.
Beta Was this translation helpful? Give feedback.
All reactions