-
Notifications
You must be signed in to change notification settings - Fork 96
/
Copy pathTimeline.swift
319 lines (272 loc) · 10.5 KB
/
Timeline.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
//
// Timeline.swift
// Segment
//
// Created by Brandon Sneed on 12/1/20.
//
import Foundation
import Sovran
// MARK: - Main Timeline
public class Timeline {
internal let plugins: [PluginType: Mediator]
public init() {
self.plugins = [
.before: Mediator(),
.enrichment: Mediator(),
.destination: Mediator(),
.after: Mediator(),
.utility: Mediator()
]
}
@discardableResult
internal func process<E: RawEvent>(incomingEvent: E) -> E? {
// apply .before and .enrichment types first ...
let beforeResult = applyPlugins(type: .before, event: incomingEvent)
// .enrichment here is akin to source middleware in the old analytics-ios.
var enrichmentResult = applyPlugins(type: .enrichment, event: beforeResult)
if let enrichments = enrichmentResult?.enrichments {
for closure in enrichments {
if let result = closure(enrichmentResult) as? E {
enrichmentResult = result
} else {
Analytics.reportInternalError(AnalyticsError.enrichmentError("The given enrichment attempted to change the event type!"))
}
}
}
// once the event enters a destination, we don't want
// to know about changes that happen there. those changes
// are to only be received by the destination.
_ = applyPlugins(type: .destination, event: enrichmentResult)
// apply .after plugins ...
let afterResult = applyPlugins(type: .after, event: enrichmentResult)
return afterResult
}
// helper method used by DestinationPlugins and Timeline
internal func applyPlugins<E: RawEvent>(type: PluginType, event: E?) -> E? {
var result: E? = event
if let mediator = plugins[type], let e = event {
result = mediator.execute(event: e)
}
return result
}
}
internal class Mediator {
internal func add(plugin: Plugin) {
plugins.append(plugin)
Telemetry.shared.increment(metric: Telemetry.INTEGRATION_METRIC) {
(_ it: inout [String: String]) in
it["message"] = "added"
if let plugin = plugin as? DestinationPlugin, !plugin.key.isEmpty {
it["plugin"] = "\(plugin.type)-\(plugin.key)"
} else {
it["plugin"] = "\(plugin.type)-\(String(describing: type(of: plugin)))"
}
}
}
internal func remove(plugin: Plugin) {
plugins.removeAll { (storedPlugin) -> Bool in
Telemetry.shared.increment(metric: Telemetry.INTEGRATION_METRIC) {
(_ it: inout [String: String]) in
it["message"] = "removed"
if let plugin = plugin as? DestinationPlugin, !plugin.key.isEmpty {
it["plugin"] = "\(plugin.type)-\(plugin.key)"
} else {
it["plugin"] = "\(plugin.type)-\(String(describing: type(of: plugin)))"
} }
return plugin === storedPlugin
}
}
internal var plugins = [Plugin]()
internal func execute<T: RawEvent>(event: T) -> T? {
var result: T? = event
plugins.forEach { (plugin) in
if let r = result {
// Drop the event return because we don't care about the
// final result.
if plugin is DestinationPlugin {
_ = plugin.execute(event: r)
} else {
result = plugin.execute(event: r)
}
Telemetry.shared.increment(metric: Telemetry.INTEGRATION_METRIC) {
(_ it: inout [String: String]) in
it["message"] = "event-\(r.type ?? "unknown")"
if let plugin = plugin as? DestinationPlugin, !plugin.key.isEmpty {
it["plugin"] = "\(plugin.type)-\(plugin.key)"
} else {
it["plugin"] = "\(plugin.type)-\(String(describing: type(of: plugin)))"
} }
}
}
return result
}
}
// MARK: - Plugin Support
extension Timeline {
internal func apply(_ closure: (Plugin) -> Void) {
for type in PluginType.allCases {
if let mediator = plugins[type] {
mediator.plugins.forEach { (plugin) in
closure(plugin)
if let destPlugin = plugin as? DestinationPlugin {
destPlugin.apply(closure: closure)
}
}
}
}
}
internal func add(plugin: Plugin) {
if let mediator = plugins[plugin.type] {
mediator.add(plugin: plugin)
}
}
internal func remove(plugin: Plugin) {
// remove all plugins with this name in every category
for type in PluginType.allCases {
if let mediator = plugins[type] {
let toRemove = mediator.plugins.filter { (storedPlugin) -> Bool in
return plugin === storedPlugin
}
toRemove.forEach { (plugin) in
plugin.shutdown()
mediator.remove(plugin: plugin)
}
}
}
}
internal func find<T: Plugin>(pluginType: T.Type) -> T? {
var found = [Plugin]()
for type in PluginType.allCases {
if let mediator = plugins[type] {
found.append(contentsOf: mediator.plugins.filter { (plugin) -> Bool in
return plugin is T
})
}
}
return found.first as? T
}
internal func findAll<T: Plugin>(pluginType: T.Type) -> [T]? {
var found = [Plugin]()
for type in PluginType.allCases {
if let mediator = plugins[type] {
found.append(contentsOf: mediator.plugins.filter { (plugin) -> Bool in
return plugin is T
})
}
}
return found as? [T]
}
internal func find(key: String) -> DestinationPlugin? {
var found = [Plugin]()
if let mediator = plugins[.destination] {
found.append(contentsOf: mediator.plugins.filter{ plugin in
guard let p = plugin as? DestinationPlugin else { return false }
return p.key == key
})
}
return found.first as? DestinationPlugin
}
}
// MARK: - Plugin Timeline Execution
extension Plugin {
public func execute<T: RawEvent>(event: T?) -> T? {
// do nothing.
return event
}
public func update(settings: Settings, type: UpdateType) {
// do nothing by default, user can override.
}
public func shutdown() {
// do nothing by default, user can override.
}
}
extension EventPlugin {
public func execute<T: RawEvent>(event: T?) -> T? {
var result: T? = event
switch result {
case let r as IdentifyEvent:
result = self.identify(event: r) as? T
case let r as TrackEvent:
result = self.track(event: r) as? T
case let r as ScreenEvent:
result = self.screen(event: r) as? T
case let r as AliasEvent:
result = self.alias(event: r) as? T
case let r as GroupEvent:
result = self.group(event: r) as? T
default:
break
}
return result
}
// Default implementations that forward the event. This gives plugin
// implementors the chance to interject on an event.
public func identify(event: IdentifyEvent) -> IdentifyEvent? {
return event
}
public func track(event: TrackEvent) -> TrackEvent? {
return event
}
public func screen(event: ScreenEvent) -> ScreenEvent? {
return event
}
public func group(event: GroupEvent) -> GroupEvent? {
return event
}
public func alias(event: AliasEvent) -> AliasEvent? {
return event
}
public func flush() { }
public func reset() { }
}
// MARK: - Destination Timeline
extension DestinationPlugin {
public func execute<T: RawEvent>(event: T?) -> T? {
var result: T? = event
if let r = result {
result = self.process(incomingEvent: r)
}
return result
}
internal func isDestinationEnabled(event: RawEvent) -> Bool {
var customerDisabled = false
if let disabled: Bool = event.integrations?.value(forKeyPath: KeyPath(self.key)), disabled == false {
customerDisabled = true
}
var hasSettings = false
if let settings = analytics?.settings() {
hasSettings = settings.hasIntegrationSettings(forPlugin: self)
}
return (hasSettings == true && customerDisabled == false)
}
internal func process<E: RawEvent>(incomingEvent: E) -> E? {
// This will process plugins (think destination middleware) that are tied
// to this destination.
var result: E? = nil
if isDestinationEnabled(event: incomingEvent) {
// apply .before and .enrichment types first ...
let beforeResult = timeline.applyPlugins(type: .before, event: incomingEvent)
let enrichmentResult = timeline.applyPlugins(type: .enrichment, event: beforeResult)
// now we execute any overrides we may have made. basically, the idea is to take an
// incoming event, like identify, and map it to whatever is appropriate for this destination.
var destinationResult: E? = nil
switch enrichmentResult {
case let e as IdentifyEvent:
destinationResult = identify(event: e) as? E
case let e as TrackEvent:
destinationResult = track(event: e) as? E
case let e as ScreenEvent:
destinationResult = screen(event: e) as? E
case let e as GroupEvent:
destinationResult = group(event: e) as? E
case let e as AliasEvent:
destinationResult = alias(event: e) as? E
default:
break
}
// apply .after plugins ...
result = timeline.applyPlugins(type: .after, event: destinationResult)
}
return result
}
}