Skip to content

Commit fcb4d29

Browse files
committed
Adding location event reporter, location event store, and tests
1 parent 63b656b commit fcb4d29

8 files changed

Lines changed: 392 additions & 24 deletions

IFTTT SDK.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
DE25265623D8C49D0019C9CB /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE25265523D8C49D0019C9CB /* Analytics.swift */; };
6161
DE260F6B26CAFC20004191D1 /* SynchronizationSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE260F6A26CAFC20004191D1 /* SynchronizationSchedulerTests.swift */; };
6262
DE2906D3242BF66E00CC2825 /* Connection+Parsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2906D2242BF66E00CC2825 /* Connection+Parsing.swift */; };
63+
DE2AE45C2721DD9E00C4794A /* LocationEventReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2AE45B2721DD9E00C4794A /* LocationEventReporterTests.swift */; };
64+
DE2AE45E2721EBFE00C4794A /* LocationEventStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2AE45D2721EBFD00C4794A /* LocationEventStoreTests.swift */; };
6365
DE2F524A2429404200EF986A /* Connection+Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2F52492429404200EF986A /* Connection+Location.swift */; };
6466
DE2F524C242940AD00EF986A /* CLCircularRegion+Parsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2F524B242940AD00EF986A /* CLCircularRegion+Parsing.swift */; };
6567
DE3074A723DB506D00A3C71F /* AnalyticsNetworkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3074A623DB506D00A3C71F /* AnalyticsNetworkController.swift */; };
@@ -237,6 +239,8 @@
237239
DE25265523D8C49D0019C9CB /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = "<group>"; };
238240
DE260F6A26CAFC20004191D1 /* SynchronizationSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizationSchedulerTests.swift; sourceTree = "<group>"; };
239241
DE2906D2242BF66E00CC2825 /* Connection+Parsing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+Parsing.swift"; sourceTree = "<group>"; };
242+
DE2AE45B2721DD9E00C4794A /* LocationEventReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEventReporterTests.swift; sourceTree = "<group>"; };
243+
DE2AE45D2721EBFD00C4794A /* LocationEventStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEventStoreTests.swift; sourceTree = "<group>"; };
240244
DE2F52492429404200EF986A /* Connection+Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Connection+Location.swift"; sourceTree = "<group>"; };
241245
DE2F524B242940AD00EF986A /* CLCircularRegion+Parsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLCircularRegion+Parsing.swift"; sourceTree = "<group>"; };
242246
DE3074A623DB506D00A3C71F /* AnalyticsNetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsNetworkController.swift; sourceTree = "<group>"; };
@@ -463,6 +467,8 @@
463467
DEC29EE7258419FC00BF56EE /* Info.plist */,
464468
DEF4A4962587BA1A00735E98 /* ArrayHelpersTests.swift */,
465469
DE260F6A26CAFC20004191D1 /* SynchronizationSchedulerTests.swift */,
470+
DE2AE45B2721DD9E00C4794A /* LocationEventReporterTests.swift */,
471+
DE2AE45D2721EBFD00C4794A /* LocationEventStoreTests.swift */,
466472
);
467473
path = SDKHostAppTests;
468474
sourceTree = "<group>";
@@ -881,11 +887,13 @@
881887
buildActionMask = 2147483647;
882888
files = (
883889
DEC29F0425841A0800BF56EE /* CLRegion+Parsing_spec.swift in Sources */,
890+
DE2AE45C2721DD9E00C4794A /* LocationEventReporterTests.swift in Sources */,
884891
DEC29F0325841A0800BF56EE /* RegionEventsRegistryTests.swift in Sources */,
885892
DEC29F0825841A0800BF56EE /* LocationServiceTests.swift in Sources */,
886893
DEC29F0725841A0800BF56EE /* ConnectionsRegistryTests.swift in Sources */,
887894
DEC29F0125841A0800BF56EE /* String_EmailDataDetectorTests.swift in Sources */,
888895
DEC29F0525841A0800BF56EE /* RegionsMonitorTests.swift in Sources */,
896+
DE2AE45E2721EBFE00C4794A /* LocationEventStoreTests.swift in Sources */,
889897
DEC29F0625841A0800BF56EE /* Connection_ParsingTests.swift in Sources */,
890898
DE260F6B26CAFC20004191D1 /* SynchronizationSchedulerTests.swift in Sources */,
891899
DEF4A4972587BA1A00735E98 /* ArrayHelpersTests.swift in Sources */,

IFTTT SDK/ConnectButtonController+Public.swift

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,222 @@
77

88
import UIKit
99

10+
struct LocationEventStore {
11+
enum EventState: String {
12+
case recorded, uploadStart, uploadSuccess, uploadError
13+
}
14+
15+
struct RecordedEvent {
16+
let state: EventState
17+
let date: Date
18+
19+
init(
20+
state: EventState,
21+
date: Date
22+
) {
23+
self.state = state
24+
self.date = date
25+
}
26+
27+
init?(dictionary: [String: Any]) {
28+
guard let stateRawValue = dictionary["state"] as? String,
29+
let dateRawValue = dictionary["date"] as? TimeInterval,
30+
let state = EventState(rawValue: stateRawValue) else {
31+
return nil
32+
}
33+
self.state = state
34+
self.date = Date(timeIntervalSinceReferenceDate: dateRawValue)
35+
}
36+
37+
var dictionary: [String: Any] {
38+
return [
39+
"state": state.rawValue,
40+
"date": date.timeIntervalSinceReferenceDate
41+
]
42+
}
43+
}
44+
45+
private var eventMap: [String: RecordedEvent]? {
46+
get {
47+
guard let dictionary = UserDefaults.standard.dictionary(forKey: "com.ifttt.locationEventReporter.map") else { return nil }
48+
return dictionary.compactMapValues { value -> RecordedEvent? in
49+
guard let dictionary = value as? [String: Any] else { return nil }
50+
return .init(dictionary: dictionary)
51+
}
52+
}
53+
set {
54+
let mappedDictionary = newValue?.compactMapValues { $0.dictionary }
55+
UserDefaults.standard.set(mappedDictionary, forKey: "com.ifttt.locationEventReporter.map")
56+
}
57+
}
58+
59+
init() {
60+
initializeEventMapIfNecessary()
61+
}
62+
63+
private mutating func initializeEventMapIfNecessary() {
64+
if eventMap == nil {
65+
eventMap = .init()
66+
}
67+
}
68+
69+
subscript(key: String) -> RecordedEvent? {
70+
return eventMap?[key]
71+
}
72+
73+
private mutating func updateRecordedEvent(
74+
_ event: RegionEvent,
75+
state: EventState,
76+
date: Date
77+
) {
78+
initializeEventMapIfNecessary()
79+
var _eventMap = eventMap
80+
_eventMap?[event.recordId.uuidString] = .init(state: state, date: date)
81+
self.eventMap = _eventMap
82+
}
83+
84+
mutating func trackRecordedEvent(_ event: RegionEvent, at date: Date) {
85+
updateRecordedEvent(
86+
event,
87+
state: .recorded,
88+
date: date
89+
)
90+
}
91+
92+
mutating func trackEventUploadStart(_ event: RegionEvent, at date: Date) {
93+
updateRecordedEvent(
94+
event,
95+
state: .uploadStart,
96+
date: date
97+
)
98+
}
99+
100+
mutating func trackEventSuccessfulUpload(_ event: RegionEvent, at date: Date) {
101+
initializeEventMapIfNecessary()
102+
if eventMap?[event.recordId.uuidString] != nil {
103+
var _eventMap = eventMap
104+
_eventMap?[event.recordId.uuidString] = nil
105+
self.eventMap = _eventMap
106+
}
107+
}
108+
109+
mutating func trackEventFailedUpload(_ event: RegionEvent, error: EventUploadError, at date: Date) {
110+
initializeEventMapIfNecessary()
111+
var _eventMap = eventMap
112+
switch error {
113+
case .crossedSanityThreshold:
114+
_eventMap?[event.recordId.uuidString] = nil
115+
case .networkError:
116+
_eventMap?[event.recordId.uuidString] = .init(state: .uploadError, date: date)
117+
}
118+
self.eventMap = _eventMap
119+
}
120+
121+
func delay(for event: RegionEvent, against date: Date) -> TimeInterval {
122+
var delay: TimeInterval = -1
123+
if let record = eventMap?[event.recordId.uuidString] {
124+
delay = date.timeIntervalSince(record.date)
125+
}
126+
return delay
127+
}
128+
129+
mutating func reset() {
130+
eventMap = nil
131+
}
132+
}
133+
134+
final class LocationEventReporter {
135+
private var eventStore: LocationEventStore
136+
137+
var closure: LocationEventsClosure?
138+
139+
init(eventStore: LocationEventStore) {
140+
self.eventStore = eventStore
141+
}
142+
143+
func recordRegionEvent(_ event: RegionEvent, at date: Date = .init()) {
144+
eventStore.trackRecordedEvent(event, at: date)
145+
closure?([.reported(event: event)])
146+
}
147+
148+
func regionEventsStartUpload(_ events: [RegionEvent]) {
149+
process(
150+
events,
151+
state: .uploadStart,
152+
error: nil
153+
)
154+
}
155+
156+
func regionEventsSuccessfulUpload(_ events: [RegionEvent]) {
157+
process(
158+
events,
159+
state: .uploadSuccess,
160+
error: nil
161+
)
162+
}
163+
164+
func regionEventsErrorUpload(_ events: [RegionEvent], error: EventUploadError) {
165+
process(
166+
events,
167+
state: .uploadError,
168+
error: error
169+
)
170+
}
171+
172+
private func process(_ events: [RegionEvent], state: LocationEventStore.EventState, error: EventUploadError?) {
173+
let date = Date()
174+
var locationEvents: [LocationEvent]
175+
switch state {
176+
case .recorded:
177+
locationEvents = events.map { event -> LocationEvent in
178+
eventStore.trackRecordedEvent(event, at: date)
179+
return .reported(event: event)
180+
}
181+
case .uploadStart:
182+
locationEvents = events.map { event -> LocationEvent in
183+
let delay = eventStore.delay(for: event, against: date)
184+
eventStore.trackRecordedEvent(event, at: date)
185+
return LocationEvent.uploadAttempted(event: event, delay: delay)
186+
}
187+
case .uploadSuccess:
188+
locationEvents = events.map { event -> LocationEvent in
189+
let delay = eventStore.delay(for: event, against: date)
190+
eventStore.trackRecordedEvent(event, at: date)
191+
return LocationEvent.uploadSuccessful(event: event, delay: delay)
192+
}
193+
case .uploadError:
194+
guard let error = error else {
195+
fatalError("Expecting error to not be nil for this case here")
196+
}
197+
locationEvents = events.map { event -> LocationEvent in
198+
let delay = eventStore.delay(for: event, against: date)
199+
eventStore.trackEventSuccessfulUpload(event, at: date)
200+
return LocationEvent.uploadFailed(event: event, error: error, delay: delay)
201+
}
202+
}
203+
closure?(locationEvents)
204+
}
205+
}
206+
207+
public enum LocationEventKind: String {
208+
case entry = "entry"
209+
case exit = "exit"
210+
}
211+
212+
public enum LocationEvent {
213+
case reported(event: RegionEvent)
214+
case uploadAttempted(event: RegionEvent, delay: TimeInterval) // The delay between reporting the event and an attempted upload. This is in seconds.
215+
case uploadSuccessful(event: RegionEvent, delay: TimeInterval) // The delay between attempting the event upload and successfully completing the upload. This is in seconds.
216+
case uploadFailed(event: RegionEvent, error: EventUploadError, delay: TimeInterval) // The delay between attempting the event upload and error completing the upload. This is in seconds.
217+
}
218+
219+
public enum EventUploadError: Error {
220+
case crossedSanityThreshold
221+
case networkError
222+
}
223+
224+
public typealias LocationEventsClosure = ([LocationEvent]) -> Void
225+
10226
/// Describes options to initialize the SDK with
11227
public struct InitializerOptions {
12228

@@ -164,4 +380,8 @@ extension ConnectButtonController {
164380
ConnectionsSynchronizer.shared.setDeveloperBackgroundProcessClosures(launchHandler: launchHandler,
165381
expirationHandler: expirationHandler)
166382
}
383+
384+
public static func setLocationEventReportedClosure(_ closure: LocationEventsClosure?) {
385+
ConnectionsSynchronizer.shared.setLocationEventReportedClosure(closure: closure)
386+
}
167387
}

IFTTT SDK/ConnectionsSynchronizer.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ final class ConnectionsSynchronizer {
7676
private let subscribers: [SynchronizationSubscriber]
7777
private var scheduler: SynchronizationScheduler
7878
private let permissionsRequestor: PermissionsRequestor
79+
private let locationEventReporter: LocationEventReporter
7980

8081
private var state: RunState = .unknown
8182

@@ -99,12 +100,16 @@ final class ConnectionsSynchronizer {
99100
let regionsMonitor = RegionsMonitor(allowsBackgroundLocationUpdates: Bundle.main.backgroundLocationEnabled)
100101
let locationSessionManager = RegionEventsSessionManager(networkController: .init(urlSession: .regionEventsURLSession),
101102
regionEventsRegistry: regionEventsRegistry)
103+
let locationEventReporter = LocationEventReporter(eventStore: .init())
102104

103-
let location = LocationService(regionsMonitor: regionsMonitor,
104-
regionEventsRegistry: regionEventsRegistry,
105-
connectionsRegistry: connectionsRegistry,
106-
sessionManager: locationSessionManager,
107-
eventPublisher: eventPublisher)
105+
let location = LocationService(
106+
regionsMonitor: regionsMonitor,
107+
regionEventsRegistry: regionEventsRegistry,
108+
connectionsRegistry: connectionsRegistry,
109+
sessionManager: locationSessionManager,
110+
eventPublisher: eventPublisher,
111+
eventReporter: locationEventReporter
112+
)
108113

109114
let connectionsMonitor = ConnectionsMonitor(connectionsRegistry: connectionsRegistry)
110115
let nativeServicesCoordinator = NativeServicesCoordinator(locationService: location,
@@ -121,6 +126,7 @@ final class ConnectionsSynchronizer {
121126
self.location = location
122127
self.connectionsMonitor = connectionsMonitor
123128
self.permissionsRequestor = permissionsRequestor
129+
self.locationEventReporter = locationEventReporter
124130

125131
let manager = SynchronizationManager(subscribers: subscribers)
126132
self.scheduler = SynchronizationScheduler(manager: manager,
@@ -259,6 +265,10 @@ final class ConnectionsSynchronizer {
259265
scheduler.developerBackgroundProcessLaunchClosure = launchHandler
260266
scheduler.developerBackgroundProcessExpirationClosure = expirationHandler
261267
}
268+
269+
func setLocationEventReportedClosure(closure: LocationEventsClosure?) {
270+
locationEventReporter.closure = closure
271+
}
262272
}
263273

264274
/// Handles coordination of native services with a set of connections

0 commit comments

Comments
 (0)