Skip to content

Commit b1ed208

Browse files
authored
Storage hardening; Add error handling capabilities. (#163)
* Storage hardening; Expose monitoring capabilities. * Updated header * Trying to fix CI. * Disabled logger tests; Will remove in the future. * fix package.resolved * Fix package resolved. * And again ... * Account for lack of URLProtocol in Linux * Refactored ErrorHandling to be more global applicable. * Fixed xcodeproj * Fixed merge issues. * Removed accidental commit of example changes. * Renamed `hardStop` to `fatal` as it's more descriptive. * Fixed up callpoints. * Fixed watchOS tests.
1 parent c2c4398 commit b1ed208

File tree

18 files changed

+548
-108
lines changed

18 files changed

+548
-108
lines changed

Segment.xcodeproj/project.pbxproj

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
465879BA2686560C00180335 /* watchOSDelegation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465879B82686560C00180335 /* watchOSDelegation.swift */; };
3535
465879BB2686560C00180335 /* watchOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465879B92686560C00180335 /* watchOSLifecycleMonitor.swift */; };
3636
4663C729267A799100ADDD1A /* QueueTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4663C728267A799100ADDD1A /* QueueTimer.swift */; };
37+
466EC2CE28FB7D5D001B384E /* OutputFileStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466EC2CD28FB7D5D001B384E /* OutputFileStream.swift */; };
38+
4697A238290341EF00FAE14F /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4697A237290341EF00FAE14F /* Errors.swift */; };
3739
46A018C225E5857D00F9CCD8 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A018C125E5857D00F9CCD8 /* Context.swift */; };
3840
46A018D425E6C9C200F9CCD8 /* LinuxUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A018D325E6C9C200F9CCD8 /* LinuxUtils.swift */; };
3941
46A018DA25E97FDF00F9CCD8 /* AppleUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A018D925E97FDF00F9CCD8 /* AppleUtils.swift */; };
@@ -55,15 +57,13 @@
5557
96259F8626CF1D45008AE301 /* ConsoleTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96259F8526CF1D45008AE301 /* ConsoleTarget.swift */; };
5658
96469AF82706225900AC5772 /* SystemTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96469AF72706225900AC5772 /* SystemTarget.swift */; };
5759
966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40ED259A7311008EB0B6 /* HTTPClient.swift */; };
58-
967C40DA258D472C008EB0B6 /* SegmentLog_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40D9258D472C008EB0B6 /* SegmentLog_Tests.swift */; };
5960
967C40E3258D4DAF008EB0B6 /* Metrics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40E2258D4DAF008EB0B6 /* Metrics_Tests.swift */; };
6061
9692724E25A4E5B7009B5298 /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9692724D25A4E5B7009B5298 /* Startup.swift */; };
6162
9692726825A583A6009B5298 /* SegmentDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9692726725A583A6009B5298 /* SegmentDestination.swift */; };
6263
96A9624E2810C6B80011DE54 /* macOSLifecycleEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A9624D2810C6B80011DE54 /* macOSLifecycleEvents.swift */; };
6364
96A9668927BC137F00078F8B /* iOSLifecycle_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A9668827BC137F00078F8B /* iOSLifecycle_Tests.swift */; };
6465
96C33A9C25880A5E00F3D538 /* SegmentLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33A9B25880A5E00F3D538 /* SegmentLog.swift */; };
6566
96C33AB1258961F500F3D538 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33AB0258961F500F3D538 /* Settings.swift */; };
66-
96C95B16271DE22700C3EB9A /* LogTarget_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C95B15271DE22600C3EB9A /* LogTarget_Tests.swift */; };
6767
96DBF37B26F39B5500724B0B /* Timeline_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DBF37A26F39B5500724B0B /* Timeline_Tests.swift */; };
6868
A31A16262576B6F200C9CDDF /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31A16252576B6F200C9CDDF /* Timeline.swift */; };
6969
A31A162F2576B73F00C9CDDF /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31A162E2576B73F00C9CDDF /* State.swift */; };
@@ -125,6 +125,8 @@
125125
465879B82686560C00180335 /* watchOSDelegation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = watchOSDelegation.swift; sourceTree = "<group>"; };
126126
465879B92686560C00180335 /* watchOSLifecycleMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = watchOSLifecycleMonitor.swift; sourceTree = "<group>"; };
127127
4663C728267A799100ADDD1A /* QueueTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTimer.swift; sourceTree = "<group>"; };
128+
466EC2CD28FB7D5D001B384E /* OutputFileStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutputFileStream.swift; sourceTree = "<group>"; };
129+
4697A237290341EF00FAE14F /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = "<group>"; };
128130
46A018C125E5857D00F9CCD8 /* Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
129131
46A018D325E6C9C200F9CCD8 /* LinuxUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinuxUtils.swift; sourceTree = "<group>"; };
130132
46A018D925E97FDF00F9CCD8 /* AppleUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleUtils.swift; sourceTree = "<group>"; };
@@ -356,6 +358,7 @@
356358
A3AEE1492580208E002386EB /* iso8601.swift */,
357359
46FE4CDF25A53FAD003A7362 /* Storage.swift */,
358360
4621080B2605332D00EBC4A8 /* KeyPath.swift */,
361+
466EC2CD28FB7D5D001B384E /* OutputFileStream.swift */,
359362
46022770261F7A4800A9E913 /* Atomic.swift */,
360363
46E382E62654429A00BA2502 /* Utils.swift */,
361364
4663C728267A799100ADDD1A /* QueueTimer.swift */,
@@ -435,6 +438,7 @@
435438
9620862B2575C0C800314F8D /* Events.swift */,
436439
A31A16C225794BEF00C9CDDF /* Plugins.swift */,
437440
96C33AB0258961F500F3D538 /* Settings.swift */,
441+
4697A237290341EF00FAE14F /* Errors.swift */,
438442
9692724D25A4E5B7009B5298 /* Startup.swift */,
439443
A31A162E2576B73F00C9CDDF /* State.swift */,
440444
A31A16252576B6F200C9CDDF /* Timeline.swift */,
@@ -507,7 +511,7 @@
507511
attributes = {
508512
LastSwiftMigration = 9999;
509513
LastSwiftUpdateCheck = 1220;
510-
LastUpgradeCheck = 1310;
514+
LastUpgradeCheck = 1340;
511515
};
512516
buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Segment" */;
513517
compatibilityVersion = "Xcode 3.2";
@@ -570,13 +574,15 @@
570574
9692726825A583A6009B5298 /* SegmentDestination.swift in Sources */,
571575
4602276C261E7BF900A9E913 /* iOSDelegation.swift in Sources */,
572576
46A018D425E6C9C200F9CCD8 /* LinuxUtils.swift in Sources */,
577+
466EC2CE28FB7D5D001B384E /* OutputFileStream.swift in Sources */,
573578
96C33A9C25880A5E00F3D538 /* SegmentLog.swift in Sources */,
574579
46FE4C9725A3F35E003A7362 /* macOSLifecycleMonitor.swift in Sources */,
575580
9620862C2575C0C800314F8D /* Events.swift in Sources */,
576581
A3AEE1882581A8F1002386EB /* Deprecations.swift in Sources */,
577582
966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */,
578583
A31A16CA25794D9700C9CDDF /* Plugins.swift in Sources */,
579584
46A018C225E5857D00F9CCD8 /* Context.swift in Sources */,
585+
4697A238290341EF00FAE14F /* Errors.swift in Sources */,
580586
96208650257AA83E00314F8D /* iOSLifecycleMonitor.swift in Sources */,
581587
46031D65266E7C10009BA540 /* StartupQueue.swift in Sources */,
582588
465879BB2686560C00180335 /* watchOSLifecycleMonitor.swift in Sources */,
@@ -596,11 +602,9 @@
596602
46210811260538BE00EBC4A8 /* KeyPath_Tests.swift in Sources */,
597603
967C40E3258D4DAF008EB0B6 /* Metrics_Tests.swift in Sources */,
598604
96A9668927BC137F00078F8B /* iOSLifecycle_Tests.swift in Sources */,
599-
96C95B16271DE22700C3EB9A /* LogTarget_Tests.swift in Sources */,
600605
A31A16512576C47400C9CDDF /* JSON_Tests.swift in Sources */,
601606
46FE4D1D25A7A850003A7362 /* Storage_Tests.swift in Sources */,
602607
46FE4CFB25A6C671003A7362 /* TestUtilities.swift in Sources */,
603-
967C40DA258D472C008EB0B6 /* SegmentLog_Tests.swift in Sources */,
604608
);
605609
runOnlyForDeploymentPostprocessing = 0;
606610
};
@@ -792,7 +796,7 @@
792796
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG";
793797
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
794798
SWIFT_VERSION = 5.0;
795-
TVOS_DEPLOYMENT_TARGET = 11.0;
799+
TVOS_DEPLOYMENT_TARGET = 12.0;
796800
USE_HEADERMAP = NO;
797801
WATCHOS_DEPLOYMENT_TARGET = 7.1;
798802
};
@@ -865,7 +869,7 @@
865869
SWIFT_COMPILATION_MODE = wholemodule;
866870
SWIFT_OPTIMIZATION_LEVEL = "-O";
867871
SWIFT_VERSION = 5.0;
868-
TVOS_DEPLOYMENT_TARGET = 11.0;
872+
TVOS_DEPLOYMENT_TARGET = 12.0;
869873
USE_HEADERMAP = NO;
870874
WATCHOS_DEPLOYMENT_TARGET = 7.1;
871875
};

Segment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 0 additions & 16 deletions
This file was deleted.

Segment.xcodeproj/xcshareddata/xcschemes/Segment-Package.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1310"
3+
LastUpgradeVersion = "1340"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Sources/Segment/Analytics.swift

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import Sovran
1111
// MARK: - Base Setup
1212

1313
public class Analytics {
14-
internal var configuration: Configuration
14+
internal var configuration: Configuration {
15+
get {
16+
// we're absolutely certain we will have a config
17+
let system: System = store.currentState()!
18+
return system.configuration
19+
}
20+
}
1521
internal var store: Store
1622
internal var storage: Storage
1723

@@ -28,8 +34,6 @@ public class Analytics {
2834
/// - Parameters:
2935
/// - configuration: The configuration to use
3036
public init(configuration: Configuration) {
31-
self.configuration = configuration
32-
3337
store = Store()
3438
storage = Storage(store: self.store, writeKey: configuration.values.writeKey)
3539
timeline = Timeline()
@@ -38,11 +42,14 @@ public class Analytics {
3842
store.provide(state: System.defaultState(configuration: configuration, from: storage))
3943
store.provide(state: UserInfo.defaultState(from: storage))
4044

45+
storage.analytics = self
46+
4147
// Get everything running
4248
platformStartup()
4349
}
4450

4551
internal func process<E: RawEvent>(incomingEvent: E) {
52+
guard enabled == true else { return }
4653
let event = incomingEvent.applyRawEventData(store: store)
4754
_ = timeline.process(incomingEvent: event)
4855
}
@@ -51,6 +58,7 @@ public class Analytics {
5158
/// - Parameters:
5259
/// - event: An event conforming to RawEvent that will be processed.
5360
public func process(event: RawEvent) {
61+
guard enabled == true else { return }
5462
switch event {
5563
case let e as TrackEvent:
5664
timeline.process(incomingEvent: e)
@@ -71,6 +79,20 @@ public class Analytics {
7179
// MARK: - System Modifiers
7280

7381
extension Analytics {
82+
/// Enable/Disable analytics capture
83+
public var enabled: Bool {
84+
get {
85+
if let system: System = store.currentState() {
86+
return system.enabled
87+
}
88+
// we don't have state if we get here, so assume we're not enabled.
89+
return false
90+
}
91+
set(value) {
92+
store.dispatch(action: System.ToggleEnabledAction(enabled: value))
93+
}
94+
}
95+
7496
/// Returns the anonymousId currently in use.
7597
public var anonymousId: String {
7698
if let userInfo: UserInfo = store.currentState() {
@@ -87,6 +109,32 @@ extension Analytics {
87109
return nil
88110
}
89111

112+
/// Adjusts the flush interval post configuration.
113+
public var flushInterval: TimeInterval {
114+
get {
115+
configuration.values.flushInterval
116+
}
117+
set(value) {
118+
if let state: System = store.currentState() {
119+
let config = state.configuration.flushInterval(value)
120+
store.dispatch(action: System.UpdateConfigurationAction(configuration: config))
121+
}
122+
}
123+
}
124+
125+
/// Adjusts the flush-at count post configuration.
126+
public var flushAt: Int {
127+
get {
128+
configuration.values.flushAt
129+
}
130+
set(value) {
131+
if let state: System = store.currentState() {
132+
let config = state.configuration.flushAt(value)
133+
store.dispatch(action: System.UpdateConfigurationAction(configuration: config))
134+
}
135+
}
136+
}
137+
90138
/// Returns the traits that were specified in the last identify call.
91139
public func traits<T: Codable>() -> T? {
92140
if let userInfo: UserInfo = store.currentState() {
@@ -184,6 +232,20 @@ extension Analytics {
184232
return storage.read(Storage.Constants.events)
185233
}
186234

235+
/// Purge all pending event upload files.
236+
public func purgeStorage() {
237+
if let files = pendingUploads {
238+
for file in files {
239+
purgeStorage(fileURL: file)
240+
}
241+
}
242+
}
243+
244+
/// Purge a single event upload file.
245+
public func purgeStorage(fileURL: URL) {
246+
try? FileManager.default.removeItem(at: fileURL)
247+
}
248+
187249
/// Wait until the Analytics object has completed startup.
188250
/// This method is primarily useful for command line utilities where
189251
/// it's desirable to wait until the system is up and running

Sources/Segment/Configuration.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class Configuration {
3030
var autoAddSegmentDestination: Bool = true
3131
var apiHost: String = HTTPClient.getDefaultAPIHost()
3232
var cdnHost: String = HTTPClient.getDefaultCDNHost()
33+
var errorHandler: ((Error) -> Void)?
3334
}
3435
internal var values: Values
3536

@@ -95,5 +96,16 @@ public extension Configuration {
9596
values.cdnHost = value
9697
return self
9798
}
99+
100+
@discardableResult
101+
func errorHandler(_ value: @escaping (Error) -> Void) -> Configuration {
102+
values.errorHandler = value
103+
return self
104+
}
98105
}
99106

107+
extension Analytics {
108+
func configuration<T>(valueFor: () -> T) -> T {
109+
return valueFor()
110+
}
111+
}

Sources/Segment/Errors.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//
2+
// Errors.swift
3+
//
4+
//
5+
// Created by Brandon Sneed on 10/20/22.
6+
//
7+
8+
import Foundation
9+
10+
public enum AnalyticsError: Error {
11+
case storageUnableToCreate(String)
12+
case storageUnableToWrite(String)
13+
case storageUnableToRename(String)
14+
case storageUnableToOpen(String)
15+
case storageInvalid(String)
16+
case storageUnknown(Error)
17+
18+
case networkUnexpectedHTTPCode(Int)
19+
case networkServerLimited(Int)
20+
case networkServerRejected(Int)
21+
case networkUnknown(Error)
22+
case networkInvalidData
23+
24+
case jsonUnableToSerialize(Error)
25+
case jsonUnableToDeserialize(Error)
26+
case jsonUnknown(Error)
27+
28+
case pluginError(Error)
29+
}
30+
31+
extension Analytics {
32+
/// Tries to convert known error types to AnalyticsError.
33+
static internal func translate(error: Error) -> Error {
34+
if let e = error as? OutputFileStream.OutputStreamError {
35+
switch e {
36+
case .invalidPath(let path):
37+
return AnalyticsError.storageInvalid(path)
38+
case .unableToCreate(let path):
39+
return AnalyticsError.storageUnableToCreate(path)
40+
case .unableToOpen(let path):
41+
return AnalyticsError.storageUnableToOpen(path)
42+
case .unableToWrite(let path):
43+
return AnalyticsError.storageUnableToWrite(path)
44+
}
45+
}
46+
47+
if let e = error as? JSON.JSONError {
48+
switch e {
49+
case .incorrectType:
50+
return AnalyticsError.jsonUnableToDeserialize(e)
51+
case .nonJSONType:
52+
return AnalyticsError.jsonUnableToDeserialize(e)
53+
case .unknown:
54+
return AnalyticsError.jsonUnknown(e)
55+
}
56+
}
57+
return error
58+
}
59+
60+
/// Reports an internal error to the user-defined error handler.
61+
public func reportInternalError(_ error: Error, fatal: Bool = false) {
62+
let translatedError = Self.translate(error: error)
63+
configuration.values.errorHandler?(translatedError)
64+
Self.segmentLog(message: "An internal error occurred: \(translatedError)", kind: .error)
65+
if fatal {
66+
exceptionFailure("A critical error occurred: \(translatedError)")
67+
}
68+
}
69+
70+
static public func reportInternalError(_ error: Error, fatal: Bool = false) {
71+
// we don't have an instance of analytics to call to get our error handler,
72+
// but we can at least report the message to the console.
73+
let translatedError = Self.translate(error: error)
74+
Self.segmentLog(message: "An internal error occurred: \(translatedError)", kind: .error)
75+
if fatal {
76+
exceptionFailure("A critical error occurred: \(translatedError)")
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)