diff --git a/Examples/destination_plugins/AmplitudeSession.swift b/Examples/destination_plugins/AmplitudeSession.swift index 36322248..1c10ac90 100644 --- a/Examples/destination_plugins/AmplitudeSession.swift +++ b/Examples/destination_plugins/AmplitudeSession.swift @@ -53,7 +53,7 @@ class AmplitudeSession: EventPlugin, iOSLifecycle { private let fireTime = TimeInterval(300) func update(settings: Settings, type: UpdateType) { - if settings.isDestinationEnabled(key: key) { + if settings.hasIntegrationSettings(key: key) { active = true } else { active = false diff --git a/Segment.xcodeproj/project.pbxproj b/Segment.xcodeproj/project.pbxproj index be9a39e5..6e2f92f6 100644 --- a/Segment.xcodeproj/project.pbxproj +++ b/Segment.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ attributes = { LastSwiftMigration = 9999; LastSwiftUpdateCheck = 1220; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1310; }; buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Segment" */; compatibilityVersion = "Xcode 3.2"; diff --git a/Segment.xcodeproj/xcshareddata/xcschemes/Segment-Package.xcscheme b/Segment.xcodeproj/xcshareddata/xcschemes/Segment-Package.xcscheme index e4ef2220..9911830e 100644 --- a/Segment.xcodeproj/xcshareddata/xcschemes/Segment-Package.xcscheme +++ b/Segment.xcodeproj/xcshareddata/xcschemes/Segment-Package.xcscheme @@ -1,6 +1,6 @@ () -> T? { if let userInfo: UserInfo = store.currentState() { return userInfo.traits?.codableValue() @@ -86,6 +95,8 @@ extension Analytics { return nil } + /// Tells this instance of Analytics to flush any queued events up to Segment.com. This command will also + /// be sent to each plugin present in the system. public func flush() { apply { plugin in if let p = plugin as? EventPlugin { @@ -94,6 +105,8 @@ extension Analytics { } } + /// Resets this instance of Analytics to a clean slate. Traits, UserID's, anonymousId, etc are all cleared or reset. This + /// command will also be sent to each plugin present in the system. public func reset() { store.dispatch(action: UserInfo.ResetAction()) apply { plugin in @@ -103,6 +116,22 @@ extension Analytics { } } + /// Retrieve the version of this library in use. + /// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format. + public func version() -> String { + return Analytics.version() + } + + /// Retrieve the version of this library in use. + /// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format. + public static func version() -> String { + return __segment_version + } +} + +extension Analytics { + /// Manually retrieve the settings that were supplied from Segment.com. + /// - Returns: A Settings object containing integration settings, tracking plan, etc. public func settings() -> Settings? { var settings: Settings? if let system: System = store.currentState() { @@ -111,11 +140,12 @@ extension Analytics { return settings } - public func version() -> String { - return Analytics.version() - } - - public static func version() -> String { - return __segment_version + /// Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all. + /// This will allow the destination to be processed in the same way within this library. + /// - Parameters: + /// - plugin: The destination plugin to enable. + public func manuallyEnableDestination(plugin: DestinationPlugin) { + self.store.dispatch(action: System.AddDestinationToSettingsAction(key: plugin.key)) } + } diff --git a/Sources/Segment/Plugins.swift b/Sources/Segment/Plugins.swift index 421a8331..16ad34c8 100644 --- a/Sources/Segment/Plugins.swift +++ b/Sources/Segment/Plugins.swift @@ -140,11 +140,6 @@ extension Analytics { public func add(plugin: Plugin) -> Plugin { plugin.configure(analytics: self) timeline.add(plugin: plugin) - if !(plugin is SegmentDestination), let destPlugin = plugin as? DestinationPlugin { - // need to maintain the list of integrations to inject into payload - store.dispatch(action: System.AddIntegrationAction(key: destPlugin.key)) - } - return plugin } @@ -155,9 +150,6 @@ extension Analytics { */ public func remove(plugin: Plugin) { timeline.remove(plugin: plugin) - if !(plugin is SegmentDestination), let destPlugin = plugin as? DestinationPlugin { - store.dispatch(action: System.RemoveIntegrationAction(key: destPlugin.key)) - } } public func find(pluginType: T.Type) -> T? { diff --git a/Sources/Segment/Plugins/SegmentDestination.swift b/Sources/Segment/Plugins/SegmentDestination.swift index fa11e2ff..f2685be1 100644 --- a/Sources/Segment/Plugins/SegmentDestination.swift +++ b/Sources/Segment/Plugins/SegmentDestination.swift @@ -70,29 +70,13 @@ public class SegmentDestination: DestinationPlugin { } // MARK: - Event Handling Methods - public func identify(event: IdentifyEvent) -> IdentifyEvent? { - queueEvent(event: event) - return event - } - - public func track(event: TrackEvent) -> TrackEvent? { - queueEvent(event: event) - return event - } - - public func screen(event: ScreenEvent) -> ScreenEvent? { - queueEvent(event: event) - return event - } - - public func alias(event: AliasEvent) -> AliasEvent? { - queueEvent(event: event) - return event - } - - public func group(event: GroupEvent) -> GroupEvent? { - queueEvent(event: event) - return event + public func execute(event: T?) -> T? { + let result: T? = event + if let r = result { + let modified = configureCloudDestinations(event: r) + queueEvent(event: modified) + } + return result } // MARK: - Abstracted Lifecycle Methods @@ -156,6 +140,37 @@ public class SegmentDestination: DestinationPlugin { } } +// MARK: - Utility methods +extension SegmentDestination { + internal func configureCloudDestinations(event: T) -> T { + guard let integrationSettings = analytics?.settings() else { return event } + guard let plugins = analytics?.timeline.plugins[.destination]?.plugins as? [DestinationPlugin] else { return event } + guard let customerValues = event.integrations?.dictionaryValue else { return event } + + var merged = [String: Any]() + + // compare settings to loaded plugins + for plugin in plugins { + let hasSettings = integrationSettings.hasIntegrationSettings(forPlugin: plugin) + if hasSettings { + // we have a device mode plugin installed. + // tell segment not to send it via cloud mode. + merged[plugin.key] = false + } + } + + // apply customer values; the customer is always right! + for (key, value) in customerValues { + merged[key] = value + } + + var modified = event + modified.integrations = try? JSON(merged) + + return modified + } +} + // MARK: - Upload management extension SegmentDestination { diff --git a/Sources/Segment/Settings.swift b/Sources/Segment/Settings.swift index 72eca586..7e72f038 100644 --- a/Sources/Segment/Settings.swift +++ b/Sources/Segment/Settings.swift @@ -68,12 +68,13 @@ public struct Settings: Codable { return integrationSettings(forKey: plugin.key) } - public func isDestinationEnabled(key: String) -> Bool { + public func hasIntegrationSettings(forPlugin plugin: DestinationPlugin) -> Bool { + return hasIntegrationSettings(key: plugin.key) + } + + public func hasIntegrationSettings(key: String) -> Bool { guard let settings = integrations?.dictionaryValue else { return false } - if settings.keys.contains(key) { - return true - } - return false + return (settings[key] != nil) } } @@ -85,15 +86,7 @@ extension Settings: Equatable { } } -extension Analytics { - /// Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all. - /// This will allow the destination to be processed in the same way within this library. - /// - Parameters: - /// - plugin: The destination plugin to enable. - public func manuallyEnableDestination(plugin: DestinationPlugin) { - self.store.dispatch(action: System.AddIntegrationAction(key: plugin.key)) - } - +extension Analytics { internal func update(settings: Settings, type: UpdateType) { apply { (plugin) in // tell all top level plugins to update. diff --git a/Sources/Segment/State.swift b/Sources/Segment/State.swift index 3a71be49..cbf8c020 100644 --- a/Sources/Segment/State.swift +++ b/Sources/Segment/State.swift @@ -12,7 +12,6 @@ import Sovran struct System: State { let configuration: Configuration - let integrations: JSON? let settings: Settings? let running: Bool @@ -21,60 +20,34 @@ struct System: State { func reduce(state: System) -> System { let result = System(configuration: state.configuration, - integrations: state.integrations, settings: settings, running: state.running) return result } } - - struct AddIntegrationAction: Action { - let key: String + + struct ToggleRunningAction: Action { + let running: Bool func reduce(state: System) -> System { - // we need to set any destination plugins to false in the - // integrations payload. this prevents them from being sent - // by segment.com once an event reaches segment. - if var integrations = state.integrations?.dictionaryValue { - integrations[key] = false - if let jsonIntegrations = try? JSON(integrations) { - let result = System(configuration: state.configuration, - integrations: jsonIntegrations, - settings: state.settings, - running: state.running) - return result - } - } - return state + return System(configuration: state.configuration, + settings: state.settings, + running: running) } } - struct RemoveIntegrationAction: Action { + struct AddDestinationToSettingsAction: Action { let key: String func reduce(state: System) -> System { - if var integrations = state.integrations?.dictionaryValue { - integrations.removeValue(forKey: key) - if let jsonIntegrations = try? JSON(integrations) { - let result = System(configuration: state.configuration, - integrations: jsonIntegrations, - settings: state.settings, - running: state.running) - return result - } + var settings = state.settings + if var integrations = settings?.integrations?.dictionaryValue { + integrations[key] = true + settings?.integrations = try? JSON(integrations) } - return state - } - } - - struct ToggleRunningAction: Action { - let running: Bool - - func reduce(state: System) -> System { return System(configuration: state.configuration, - integrations: state.integrations, - settings: state.settings, - running: running) + settings: settings, + running: state.running) } } } @@ -139,8 +112,7 @@ extension System { settings = Settings(writeKey: configuration.values.writeKey, apiHost: HTTPClient.getDefaultAPIHost()) } } - let integrationDictionary = try! JSON([String: Any]()) - return System(configuration: configuration, integrations: integrationDictionary, settings: settings, running: false) + return System(configuration: configuration, settings: settings, running: false) } } diff --git a/Sources/Segment/Timeline.swift b/Sources/Segment/Timeline.swift index 42b82894..c82a7a80 100644 --- a/Sources/Segment/Timeline.swift +++ b/Sources/Segment/Timeline.swift @@ -210,6 +210,20 @@ extension DestinationPlugin { } 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(incomingEvent: E) -> E? { // This will process plugins (think destination middleware) that are tied @@ -217,11 +231,7 @@ extension DestinationPlugin { var result: E? = nil - // For destination plugins, we will always have some kind of `settings`, - // and if we don't, it means this destination hasn't been setup on app.segment.com, - // which in turn ALSO means that we shouldn't be sending events to it. - - if let enabled = analytics?.settings()?.isDestinationEnabled(key: self.key), enabled == true { + 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) diff --git a/Sources/Segment/Types.swift b/Sources/Segment/Types.swift index 35246e50..7724e410 100644 --- a/Sources/Segment/Types.swift +++ b/Sources/Segment/Types.swift @@ -277,14 +277,13 @@ extension RawEvent { internal func applyRawEventData(store: Store) -> Self { var result: Self = self - guard let system: System = store.currentState() else { return self } guard let userInfo: UserInfo = store.currentState() else { return self } result.anonymousId = userInfo.anonymousId result.userId = userInfo.userId result.messageId = UUID().uuidString result.timestamp = Date().iso8601() - result.integrations = system.integrations + result.integrations = try? JSON([String: Any]()) return result } diff --git a/Sources/Segment/Utilities/Storage.swift b/Sources/Segment/Utilities/Storage.swift index f581e0c2..02f8aca5 100644 --- a/Sources/Segment/Utilities/Storage.swift +++ b/Sources/Segment/Utilities/Storage.swift @@ -239,6 +239,8 @@ extension Storage { if fm.fileExists(atPath: storeFile.path) == false { start(file: storeFile) newFile = true + } else if fileHandle == nil { + fileHandle = try? FileHandle(forWritingTo: file) } // Verify file size isn't too large diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 57c3741f..ed4d093e 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -86,7 +86,7 @@ final class Analytics_Tests: XCTestCase { UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") let expectation = XCTestExpectation(description: "MyDestination Expectation") - let myDestination = MyDestination { + let myDestination = MyDestination(disabled: true) { expectation.fulfill() return true } diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index 5a65189d..0010465e 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -75,15 +75,21 @@ class MyDestination: DestinationPlugin { var analytics: Analytics? let trackCompletion: (() -> Bool)? - init(trackCompletion: (() -> Bool)? = nil) { + let disabled: Bool + + init(disabled: Bool = false, trackCompletion: (() -> Bool)? = nil) { self.key = "MyDestination" self.type = .destination self.timeline = Timeline() self.trackCompletion = trackCompletion + self.disabled = disabled } func update(settings: Settings, type: UpdateType) { - // + if disabled == false { + // add ourselves to the settings + analytics?.manuallyEnableDestination(plugin: self) + } } func track(event: TrackEvent) -> TrackEvent? { diff --git a/Tests/Segment-Tests/Timeline_Tests.swift b/Tests/Segment-Tests/Timeline_Tests.swift index 9dd7656d..04a2ea92 100644 --- a/Tests/Segment-Tests/Timeline_Tests.swift +++ b/Tests/Segment-Tests/Timeline_Tests.swift @@ -26,17 +26,7 @@ class Timeline_Tests: XCTestCase { return true } - // Do this to force enable the destination - var settings = Settings(writeKey: "test") - if let existing = settings.integrations?.dictionaryValue { - var newIntegrations = existing - newIntegrations[firstDestination.key] = true - settings.integrations = try! JSON(newIntegrations) - } let configuration = Configuration(writeKey: "test") - configuration.defaultSettings(settings) - - let analytics = Analytics(configuration: configuration) analytics.add(plugin: firstDestination)