diff --git a/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj b/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj index 4220055b..f03b7a12 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj +++ b/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 965DC1232669947F00DDF9C7 /* FirebaseDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */; }; 965DC1262671656C00DDF9C7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 965DC1252671656C00DDF9C7 /* GoogleService-Info.plist */; }; 9697C1F52679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png in Resources */ = {isa = PBXBuildFile; fileRef = 9697C1F42679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png */; }; + 96D8F16F26EFFA09007F8B28 /* ExampleDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96D8F16E26EFFA09007F8B28 /* ExampleDestination.swift */; }; BA384C9826824F3700AFEA1B /* AppsFlyerLib in Frameworks */ = {isa = PBXBuildFile; productRef = BA384C9726824F3700AFEA1B /* AppsFlyerLib */; }; BA384C9A2682973300AFEA1B /* AppsFlyerDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA384C992682973300AFEA1B /* AppsFlyerDestination.swift */; }; /* End PBXBuildFile section */ @@ -48,6 +49,7 @@ 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseDestination.swift; sourceTree = ""; }; 965DC1252671656C00DDF9C7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 9697C1F42679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Segment_Logo_Avatar_Grey-1024.png"; sourceTree = ""; }; + 96D8F16E26EFFA09007F8B28 /* ExampleDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleDestination.swift; sourceTree = ""; }; BA384C992682973300AFEA1B /* AppsFlyerDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppsFlyerDestination.swift; sourceTree = ""; }; BA384C9D2686609000AFEA1B /* DestinationsExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DestinationsExample.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ @@ -119,6 +121,7 @@ BA384C992682973300AFEA1B /* AppsFlyerDestination.swift */, 469F7B24266013320038E773 /* AdjustDestination.swift */, 965DC0F92668077400DDF9C7 /* AmplitudeSession.swift */, + 96D8F16E26EFFA09007F8B28 /* ExampleDestination.swift */, 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */, 469F7B1F266012CB0038E773 /* FlurryDestination.swift */, 965DC0F82668077400DDF9C7 /* MixpanelDestination.swift */, @@ -226,6 +229,7 @@ BA384C9A2682973300AFEA1B /* AppsFlyerDestination.swift in Sources */, 469F7B20266012CB0038E773 /* FlurryDestination.swift in Sources */, 469F7B0C266011690038E773 /* ViewController.swift in Sources */, + 96D8F16F26EFFA09007F8B28 /* ExampleDestination.swift in Sources */, 965DC0FA2668077400DDF9C7 /* MixpanelDestination.swift in Sources */, 965DC0FB2668077400DDF9C7 /* AmplitudeSession.swift in Sources */, 469F7B08266011690038E773 /* AppDelegate.swift in Sources */, @@ -455,32 +459,32 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/adjust/ios_sdk.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.29.2; + kind = exactVersion; + version = 4.29.6; }; }; 965DC0FC2668079400DDF9C7 /* XCRemoteSwiftPackageReference "mixpanel-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "git@github.com:mixpanel/mixpanel-swift.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.9.3; + kind = exactVersion; + version = 2.9.3; }; }; 965DC11F2669942800DDF9C7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 8.1.0; + kind = exactVersion; + version = 8.1.0; }; }; BA384C9626824F3700AFEA1B /* XCRemoteSwiftPackageReference "AppsFlyerFramework" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/AppsFlyerSDK/AppsFlyerFramework"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 6.3.2; + kind = exactVersion; + version = 6.3.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/destination_plugins/ExampleDestination.swift b/Examples/destination_plugins/ExampleDestination.swift new file mode 100644 index 00000000..1e4b9bc4 --- /dev/null +++ b/Examples/destination_plugins/ExampleDestination.swift @@ -0,0 +1,148 @@ +// +// ExampleDestination.swift +// ExampleDestination +// +// Created by Cody Garvin on 9/13/21. +// + +// NOTE: You can see this plugin in use in the DestinationsExample application. +// +// This plugin is NOT SUPPORTED by Segment. It is here merely as an example, +// and for your convenience should you find it useful. +// + +// MIT License +// +// Copyright (c) 2021 Segment +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import Segment +//import ExampleModule // TODO: Import partner SDK module here + +/** + An implementation of the Example Analytics device mode destination as a plugin. + */ + +class ExampleDestination: DestinationPlugin { + let timeline = Timeline() + let type = PluginType.destination + // TODO: Fill this out with your settings key that matches your destination in the Segment App + let key = "Example" + var analytics: Analytics? = nil + + private var exampleSettings: ExampleSettings? + + func update(settings: Settings, type: UpdateType) { + // Skip if you have a singleton and don't want to keep updating via settings. + guard type == .initial else { return } + + // Grab the settings and assign them for potential later usage. + // Note: Since integrationSettings is generic, strongly type the variable. + guard let tempSettings: ExampleSettings = settings.integrationSettings(forPlugin: self) else { return } + exampleSettings = tempSettings + + // TODO: initialize partner SDK here + } + + func identify(event: IdentifyEvent) -> IdentifyEvent? { + + if let _ = event.traits?.dictionaryValue { + // TODO: Do something with traits if they exist + } + + // TODO: Do something with userId & traits in partner SDK + + return event + } + + func track(event: TrackEvent) -> TrackEvent? { + + var returnEvent = event + + // !!!: Sample of how to convert property keys + if let mappedProperties = try? event.properties?.mapTransform(ExampleDestination.eventNameMap, + valueTransform: ExampleDestination.eventValueConversion) { + returnEvent.properties = mappedProperties + } + + // TODO: Do something with event & properties in partner SDK from returnEvent + + return returnEvent + } + + func screen(event: ScreenEvent) -> ScreenEvent? { + + if let _ = event.properties?.dictionaryValue { + // TODO: Do something with properties if they exist + } + + // TODO: Do something with name, category & properties in partner SDK + + return event + } + + func group(event: GroupEvent) -> GroupEvent? { + + if let _ = event.traits?.dictionaryValue { + // TODO: Do something with traits if they exist + } + + // TODO: Do something with groupId & traits in partner SDK + + return event + } + + func alias(event: AliasEvent) -> AliasEvent? { + + // TODO: Do something with previousId & userId in partner SDK + + return event + } + + func reset() { + // TODO: Do something with resetting partner SDK + } +} + +// Example of what settings may look like. +private struct ExampleSettings: Codable { + let apiKey: String + let configB: Int? + let configC: Bool? +} + +// Rules for converting keys and values to the proper formats that bridge +// from Segment to the Partner SDK. These are only examples. +private extension ExampleDestination { + + static var eventNameMap = ["ADD_TO_CART": "Product Added", + "PRODUCT_TAPPED": "Product Tapped"] + + static var eventValueConversion: ((_ key: String, _ value: Any) -> Any) = { (key, value) in + if let valueString = value as? String { + return valueString + .replacingOccurrences(of: "-", with: "_") + .replacingOccurrences(of: " ", with: "_") + } else { + return value + } + } +} diff --git a/Examples/destination_plugins/FlurryDestination.swift b/Examples/destination_plugins/FlurryDestination.swift index 09bff30a..26f3a840 100644 --- a/Examples/destination_plugins/FlurryDestination.swift +++ b/Examples/destination_plugins/FlurryDestination.swift @@ -39,7 +39,7 @@ import Segment import FlurryAnalytics /** - An implmentation of the Flurry Analytics device mode destination as a plugin. + An implementation of the Flurry Analytics device mode destination as a plugin. */ private struct FlurrySettings: Codable { diff --git a/Examples/destination_plugins/MixpanelDestination.swift b/Examples/destination_plugins/MixpanelDestination.swift index b1722cc8..abb5ad50 100644 --- a/Examples/destination_plugins/MixpanelDestination.swift +++ b/Examples/destination_plugins/MixpanelDestination.swift @@ -35,17 +35,17 @@ // SOFTWARE. import Foundation -import Mixpanel import Segment +import Mixpanel class MixpanelDestination: DestinationPlugin, RemoteNotifications { let timeline = Timeline() let type = PluginType.destination let key = "Mixpanel" - var analytics: Analytics? + var analytics: Analytics? = nil private var mixpanel: MixpanelInstance? = nil - private var settings: [String: Any]? = nil + private var mixpanelSettings: MixpanelSettings? = nil func update(settings: Settings, type: UpdateType) { // we've already set up this singleton SDK, can't do it again, so skip. @@ -55,20 +55,23 @@ class MixpanelDestination: DestinationPlugin, RemoteNotifications { mixpanel?.flush() // TODO: Update the proper types - if let mixPanelSettings = settings.integrationSettings(forKey: key), - let token = mixPanelSettings["token"] as? String { - self.settings = mixPanelSettings - mixpanel = Mixpanel.initialize(token: token) - - // Check for EU endpoint - if let euEndPointEnabled = self.settings?["enableEuropeanUnionEndpoint"] as? Bool { - if euEndPointEnabled { - mixpanel?.serverURL = "api-eu.mixpanel.com" - } - } - } else { + guard let tempSettings: MixpanelSettings = settings.integrationSettings(forPlugin: self) else { mixpanel = nil analytics?.log(message: "Could not load Mixpanel settings") + return + } + + mixpanelSettings = tempSettings + + // Initialize mixpanel + if let token = mixpanelSettings?.token { + mixpanel = Mixpanel.initialize(token: token) + } + + // Change the endpoint if euro one is set + if let euEndpointEnabled = mixpanelSettings?.enableEuropeanUnionEndpoint, + euEndpointEnabled { + mixpanel?.serverURL = "api-eu.mixpanel.com" } } @@ -79,48 +82,46 @@ class MixpanelDestination: DestinationPlugin, RemoteNotifications { analytics?.log(message: "Mixpanel identify \(eventUserID)") } - let keyMap = ["$first_name": "firstName", - "$last_name": "lastName", - "$created": "createdAt", - "$last_seen": "lastSeen", - "$email": "email", - "$name": "name", - "$username": "username", - "$phone": "phone"] - - guard let traits = event.traits?.dictionaryValue as? Properties else { + guard let traits = try? event.traits?.dictionaryValue?.mapTransform(MixpanelDestination.keyMap, + valueTransform: nil) as? Properties else { return event } if setAllTraitsByDefault() { - let mappedTraits = mapTraits(traits, keyMap: keyMap) // Register the mapped traits - mixpanel?.registerSuperProperties(mappedTraits) - analytics?.log(message: "Mixpanel registerSuperProperties \(mappedTraits)") + mixpanel?.registerSuperProperties(traits) + analytics?.log(message: "Mixpanel registerSuperProperties \(traits)") // Mixpanel also has a people API that works separately so we set hte traits for it as well. if peopleEnabled() { - mixpanel?.people.set(properties: mappedTraits) - analytics?.log(message: "Mixpanel people set \(mappedTraits)") + mixpanel?.people.set(properties: traits) + analytics?.log(message: "Mixpanel people set \(traits)") } } - if let superProperties = settings?["superProperties"] as? [String] { - var superPropertyTraits = [String: MixpanelType]() + if let superProperties = mixpanelSettings?.superProperties { + var superPropertyTraits = [String: Any]() for superProperty in superProperties { superPropertyTraits[superProperty] = traits[superProperty] } - let mappedSuperProperties = mapTraits(superPropertyTraits, keyMap: keyMap) + guard let mappedSuperProperties = try? superPropertyTraits.mapTransform(MixpanelDestination.keyMap, + valueTransform: nil) as? [String: MixpanelType] else { + return event + } + mixpanel?.registerSuperProperties(mappedSuperProperties) analytics?.log(message: "Mixpanel registerSuperProperties \(mappedSuperProperties)") - if peopleEnabled(), let peopleProperties = settings?["peopleProperties"] as? [String] { - var peoplePropertyTraits = [String: MixpanelType]() + if peopleEnabled(), let peopleProperties = mixpanelSettings?.peopleProperties { + var peoplePropertyTraits = [String: Any]() for peopleProperty in peopleProperties { peoplePropertyTraits[peopleProperty] = traits[peopleProperty] } - let mappedPeopleProperties = mapTraits(peoplePropertyTraits, keyMap: keyMap) + guard let mappedPeopleProperties = try? peoplePropertyTraits.mapTransform(MixpanelDestination.keyMap, + valueTransform: nil) as? [String: MixpanelType] else { + return event + } mixpanel?.people.set(properties: mappedPeopleProperties) analytics?.log(message: "Mixpanel people set \(mappedSuperProperties)") } @@ -135,7 +136,7 @@ class MixpanelDestination: DestinationPlugin, RemoteNotifications { } func screen(event: ScreenEvent) -> ScreenEvent? { - if settings?["consolidatedPageCalls"] as? Bool ?? false, + if mixpanelSettings?.consolidatedPageCalls ?? false, var payloadProps = event.properties?.dictionaryValue { let eventName = "Loaded a Screen" @@ -144,7 +145,7 @@ class MixpanelDestination: DestinationPlugin, RemoteNotifications { } mixpanelTrack(eventName, properties: payloadProps) analytics?.log(message: "Mixpanel track \(eventName) properties \(payloadProps)") - } else if settings?["trackAllPages"] as? Bool ?? false { + } else if mixpanelSettings?.trackAllPages ?? false { var finalEventName = "Viewed Screen" if let eventName = event.name { @@ -153,11 +154,11 @@ class MixpanelDestination: DestinationPlugin, RemoteNotifications { mixpanelTrack(finalEventName, properties: event.properties?.dictionaryValue) analytics?.log(message: "Mixpanel track \(finalEventName) properties \(String(describing: event.properties?.dictionaryValue))") - } else if settings?["trackNamedPages"] as? Bool ?? false, let eventName = event.name { + } else if mixpanelSettings?.trackNamedPages ?? false, let eventName = event.name { let finalEventName = "Viewed \(eventName) Screen" mixpanelTrack(finalEventName, properties: event.properties?.dictionaryValue) analytics?.log(message: "Mixpanel track \(finalEventName) properties \(String(describing: event.properties?.dictionaryValue))") - } else if settings?["trackCategorizedPages"] as? Bool ?? false, let category = event.category { + } else if mixpanelSettings?.trackCategorizedPages ?? false, let category = event.category { let finalEventName = "Viewed \(category) Screen" mixpanelTrack(finalEventName, properties: event.properties?.dictionaryValue) analytics?.log(message: "Mixpanel track \(finalEventName) properties \(String(describing: event.properties?.dictionaryValue))") @@ -169,7 +170,7 @@ class MixpanelDestination: DestinationPlugin, RemoteNotifications { func group(event: GroupEvent) -> GroupEvent? { guard let groupID = event.groupId, !groupID.isEmpty, - let groupIdentifierProperties = settings?["groupIdentifierTraits"] as? [String] else { + let groupIdentifierProperties = mixpanelSettings?.groupIdentifierTraits else { return event } @@ -272,7 +273,7 @@ extension MixpanelDestination { private func eventShouldIncrement(event: String) -> Bool { var shouldIncrement = false - if let propertyIncrements = settings?["eventIncrements"] as? [String] { + if let propertyIncrements = mixpanelSettings?.eventIncrements { for increment in propertyIncrements { if event.lowercased() == increment.lowercased() { shouldIncrement = true @@ -285,7 +286,7 @@ extension MixpanelDestination { } private func incrementProperties(_ properties: [String: Any]) { - if let propertyIncrements = settings?["propIncrements"] as? [String] { + if let propertyIncrements = mixpanelSettings?.propIncrements { for propString in propertyIncrements { for property in properties.keys { if propString.lowercased() == property.lowercased(), @@ -300,7 +301,7 @@ extension MixpanelDestination { private func peopleEnabled() -> Bool { var enabled = false - if let peopleEnabled = settings?["people"] as? Bool { + if let peopleEnabled = mixpanelSettings?.people { enabled = peopleEnabled } @@ -309,21 +310,38 @@ extension MixpanelDestination { private func setAllTraitsByDefault() -> Bool { var traitsByDefault = false - if let setAllTraitsByDefault = settings?["setAllTraitsByDefault"] as? Bool { + if let setAllTraitsByDefault = mixpanelSettings?.setAllTraitsByDefault { traitsByDefault = setAllTraitsByDefault } return traitsByDefault } +} + +private struct MixpanelSettings: Codable { + let token: String + let enableEuropeanUnionEndpoint: Bool + let consolidatedPageCalls: Bool + let trackAllPages: Bool + let trackNamedPages: Bool + let trackCategorizedPages: Bool + let people: Bool + let setAllTraitsByDefault: Bool + let superProperties: [String]? + let peopleProperties: [String]? + let groupIdentifierTraits: [String]? + let eventIncrements: [String]? + let propIncrements: [String]? +} + +private extension MixpanelDestination { - private func mapTraits(_ traits: [String: MixpanelType], keyMap: [String: MixpanelType]) -> Properties { - var returnMap = traits - for (key, value) in traits { - if keyMap.keys.contains(key), let newKey = keyMap[key] as? String { - returnMap.removeValue(forKey: key) - returnMap[newKey] = value - } - } - return returnMap - } + static let keyMap = ["$first_name": "firstName", + "$last_name": "lastName", + "$created": "createdAt", + "$last_seen": "lastSeen", + "$email": "email", + "$name": "name", + "$username": "username", + "$phone": "phone"] } diff --git a/Segment.xcodeproj/project.pbxproj b/Segment.xcodeproj/project.pbxproj index 13308e01..2804259f 100644 --- a/Segment.xcodeproj/project.pbxproj +++ b/Segment.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 96C33A9C25880A5E00F3D538 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33A9B25880A5E00F3D538 /* Logger.swift */; }; 96C33AAC25892D6D00F3D538 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33AAB25892D6D00F3D538 /* Metrics.swift */; }; 96C33AB1258961F500F3D538 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33AB0258961F500F3D538 /* Settings.swift */; }; + 96DBF37B26F39B5500724B0B /* Timeline_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DBF37A26F39B5500724B0B /* Timeline_Tests.swift */; }; A31A16262576B6F200C9CDDF /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31A16252576B6F200C9CDDF /* Timeline.swift */; }; A31A162F2576B73F00C9CDDF /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31A162E2576B73F00C9CDDF /* State.swift */; }; A31A16342576B7AF00C9CDDF /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31A16332576B7AF00C9CDDF /* Types.swift */; }; @@ -143,6 +144,7 @@ 9620862B2575C0C800314F8D /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = ""; }; 962086482579CCC200314F8D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLifecycleMonitor.swift; sourceTree = ""; }; + 9679DD6226EFF00800A6933C /* ExampleDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleDestination.swift; sourceTree = ""; }; 967C40D9258D472C008EB0B6 /* Logger_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger_Tests.swift; sourceTree = ""; }; 967C40E2258D4DAF008EB0B6 /* Metrics_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metrics_Tests.swift; sourceTree = ""; }; 967C40ED259A7311008EB0B6 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; @@ -151,6 +153,7 @@ 96C33A9B25880A5E00F3D538 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 96C33AAB25892D6D00F3D538 /* Metrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metrics.swift; sourceTree = ""; }; 96C33AB0258961F500F3D538 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + 96DBF37A26F39B5500724B0B /* Timeline_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline_Tests.swift; sourceTree = ""; }; A31A16252576B6F200C9CDDF /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; A31A162E2576B73F00C9CDDF /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; A31A16332576B7AF00C9CDDF /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; @@ -239,12 +242,13 @@ 46D98E3C26D6FEF300E7A86A /* destination_plugins */ = { isa = PBXGroup; children = ( - 46D98E3D26D6FEF300E7A86A /* FlurryDestination.swift */, 46D98E3E26D6FEF300E7A86A /* AdjustDestination.swift */, - 46D98E3F26D6FEF300E7A86A /* MixpanelDestination.swift */, + 46D98E4226D6FEF300E7A86A /* AmplitudeSession.swift */, 46D98E4026D6FEF300E7A86A /* AppsFlyerDestination.swift */, + 9679DD6226EFF00800A6933C /* ExampleDestination.swift */, 46D98E4126D6FEF300E7A86A /* FirebaseDestination.swift */, - 46D98E4226D6FEF300E7A86A /* AmplitudeSession.swift */, + 46D98E3D26D6FEF300E7A86A /* FlurryDestination.swift */, + 46D98E3F26D6FEF300E7A86A /* MixpanelDestination.swift */, ); name = destination_plugins; path = Examples/destination_plugins; @@ -348,16 +352,17 @@ OBJ_11 /* Segment-Tests */ = { isa = PBXGroup; children = ( - 4621082D2609206D00EBC4A8 /* Support */, - OBJ_13 /* XCTestManifests.swift */, - A31A16502576C47400C9CDDF /* JSON_Tests.swift */, - 967C40D9258D472C008EB0B6 /* Logger_Tests.swift */, - 46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */, - 967C40E2258D4DAF008EB0B6 /* Metrics_Tests.swift */, OBJ_12 /* Analytics_Tests.swift */, 4658175325BA4C20006B2809 /* HTTPClient_Tests.swift */, + A31A16502576C47400C9CDDF /* JSON_Tests.swift */, 46210810260538BE00EBC4A8 /* KeyPath_Tests.swift */, + 967C40D9258D472C008EB0B6 /* Logger_Tests.swift */, + 967C40E2258D4DAF008EB0B6 /* Metrics_Tests.swift */, 46F7485F26C720F60042798E /* ObjC_Tests.swift */, + 46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */, + 96DBF37A26F39B5500724B0B /* Timeline_Tests.swift */, + OBJ_13 /* XCTestManifests.swift */, + 4621082D2609206D00EBC4A8 /* Support */, ); name = "Segment-Tests"; path = "Tests/Segment-Tests"; @@ -553,6 +558,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + 96DBF37B26F39B5500724B0B /* Timeline_Tests.swift in Sources */, OBJ_30 /* Analytics_Tests.swift in Sources */, 46F7486026C720F60042798E /* ObjC_Tests.swift in Sources */, OBJ_31 /* XCTestManifests.swift in Sources */, diff --git a/Sources/Segment/Timeline.swift b/Sources/Segment/Timeline.swift index 0adaf092..42b82894 100644 --- a/Sources/Segment/Timeline.swift +++ b/Sources/Segment/Timeline.swift @@ -72,7 +72,13 @@ internal class Mediator { plugins.forEach { (plugin) in if let r = result { - result = plugin.execute(event: r) + // 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) + } } } diff --git a/Sources/Segment/Utilities/JSON.swift b/Sources/Segment/Utilities/JSON.swift index 1160bd77..a924e6df 100644 --- a/Sources/Segment/Utilities/JSON.swift +++ b/Sources/Segment/Utilities/JSON.swift @@ -285,7 +285,7 @@ extension JSON { // MARK: - Helpers extension Dictionary where Key == String, Value == Any { - internal func mapTransform(_ keys: [String: String], valueTransform: ((_ key: Key, _ value: Value) -> Any)? = nil) throws -> [Key: Value] { + public func mapTransform(_ keys: [String: String], valueTransform: ((_ key: Key, _ value: Value) -> Any)? = nil) throws -> [Key: Value] { let mapped = Dictionary(uniqueKeysWithValues: self.map { key, value -> (Key, Value) in var newKey = key var newValue = value diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index ec943bc0..57c3741f 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -57,6 +57,7 @@ final class Analytics_Tests: XCTestCase { let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination { expectation.fulfill() + return true } var settings = Settings(writeKey: "test") @@ -87,6 +88,7 @@ final class Analytics_Tests: XCTestCase { let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination { expectation.fulfill() + return true } let configuration = Configuration(writeKey: "test") @@ -304,7 +306,8 @@ final class Analytics_Tests: XCTestCase { } func testFlush() { - let analytics = Analytics(configuration: Configuration(writeKey: "test")) + // Use a specific writekey to this test so we do not collide with other cached items. + let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_do_not_reuse_this_writekey")) waitUntilStarted(analytics: analytics) diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index 6d231516..f8cd58b4 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -73,9 +73,9 @@ class MyDestination: DestinationPlugin { let type: PluginType let key: String var analytics: Analytics? - let trackCompletion: (() -> Void)? + let trackCompletion: (() -> Bool)? - init(trackCompletion: (() -> Void)? = nil) { + init(trackCompletion: (() -> Bool)? = nil) { self.key = "MyDestination" self.type = .destination self.timeline = Timeline() @@ -87,10 +87,13 @@ class MyDestination: DestinationPlugin { } func track(event: TrackEvent) -> TrackEvent? { + var returnEvent: TrackEvent? = event if let completion = trackCompletion { - completion() + if !completion() { + returnEvent = nil + } } - return event + return returnEvent } } diff --git a/Tests/Segment-Tests/Timeline_Tests.swift b/Tests/Segment-Tests/Timeline_Tests.swift new file mode 100644 index 00000000..9dd7656d --- /dev/null +++ b/Tests/Segment-Tests/Timeline_Tests.swift @@ -0,0 +1,123 @@ +// +// Timeline_Tests.swift +// Timeline_Tests +// +// Created by Cody Garvin on 9/16/21. +// + +import XCTest +@testable import Segment + +class Timeline_Tests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testBaseEventCreation() { + let expectation = XCTestExpectation(description: "First") + + let firstDestination = MyDestination { + expectation.fulfill() + 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) + + waitUntilStarted(analytics: analytics) + + analytics.track(name: "Booya") + + wait(for: [expectation], timeout: 1.0) + } + + func testTwoBaseEventCreation() { + let expectation = XCTestExpectation(description: "First") + let expectationTrack2 = XCTestExpectation(description: "Second") + + let firstDestination = MyDestination { + expectation.fulfill() + return true + } + let secondDestination = MyDestination { + expectationTrack2.fulfill() + 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 + newIntegrations[secondDestination.key] = true + settings.integrations = try! JSON(newIntegrations) + } + let configuration = Configuration(writeKey: "test") + configuration.defaultSettings(settings) + let analytics = Analytics(configuration: configuration) + + analytics.add(plugin: firstDestination) + analytics.add(plugin: secondDestination) + + waitUntilStarted(analytics: analytics) + + analytics.track(name: "Booya") + + wait(for: [expectation, expectationTrack2], timeout: 1.0) + } + + func testTwoBaseEventCreationFirstFail() { + let expectation = XCTestExpectation(description: "First") + let expectationTrack2 = XCTestExpectation(description: "Second") + + let firstDestination = MyDestination { + expectation.fulfill() + return false + } + let secondDestination = MyDestination { + expectationTrack2.fulfill() + 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 + newIntegrations[secondDestination.key] = true + settings.integrations = try! JSON(newIntegrations) + } + let configuration = Configuration(writeKey: "test") + configuration.defaultSettings(settings) + let analytics = Analytics(configuration: configuration) + + analytics.add(plugin: firstDestination) + analytics.add(plugin: secondDestination) + + waitUntilStarted(analytics: analytics) + + analytics.track(name: "Booya") + + wait(for: [expectation, expectationTrack2], timeout: 1.0) + } + +}