From df48ac93d9b673b9ddf3d2c6f0d32922841cf3af Mon Sep 17 00:00:00 2001 From: Xiaowei Zhu <33129495+zhu-xiaowei@users.noreply.github.com> Date: Mon, 25 Mar 2024 23:17:44 +0800 Subject: [PATCH] feat: support init SDK with custom configuration (#59) --- Package.swift | 3 +- README.md | 62 +++++--- .../AWSClickstreamPlugin+Configure.swift | 87 ++++++++++- .../Clickstream/AWSClickstreamPlugin.swift | 6 +- .../Clickstream/ClickstreamAnalytics.swift | 27 +++- Sources/Clickstream/ClickstreamObjc.swift | 7 +- .../AWSClickstreamPluginConfiguration.swift | 36 ++--- .../Clickstream/ClickstreamContext.swift | 97 ++++++++++--- Sources/Clickstream/Network/NetRequest.swift | 2 +- .../Support/Extension/String+HashCode.swift | 6 + .../Clickstream/AnalyticsClientTest.swift | 11 +- .../AutoRecordEventClientTest.swift | 12 +- .../Clickstream/EventRecorderTest.swift | 12 +- .../Clickstream/SessionClientTests.swift | 30 +++- .../ClickstreamAnalyticsTest.swift | 23 ++- .../ClickstreamPluginTestBase.swift | 12 +- .../ClickstreamPluginConfigurationTest.swift | 36 +---- Tests/ClickstreamTests/IntegrationTest.swift | 23 ++- Tests/ClickstreamTests/SDKInitialTest.swift | 137 ++++++++++++++++++ .../amplifyconfiguration.json | 15 ++ 20 files changed, 489 insertions(+), 155 deletions(-) create mode 100644 Tests/ClickstreamTests/SDKInitialTest.swift create mode 100644 Tests/ClickstreamTests/amplifyconfiguration.json diff --git a/Package.swift b/Package.swift index a06a859..5142b53 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,8 @@ let package = Package( ), .testTarget( name: "ClickstreamTests", - dependencies: ["Clickstream", .product(name: "Swifter", package: "swifter")] + dependencies: ["Clickstream", .product(name: "Swifter", package: "swifter")], + resources: [.process("amplifyconfiguration.json")] ) ] ) diff --git a/README.md b/README.md index 783ee54..218e057 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The Clickstream SDK supports iOS 13+. Clickstream requires Xcode 13.4 or higher to build. -**1.Add Package** +### 1.Add Package We use **Swift Package Manager** to distribute Clickstream Swift SDK, open your project in Xcode and select **File > Add Pckages**. @@ -30,7 +30,7 @@ Enter the Clickstream Library for Swift GitHub repo URL (`https://github.com/aws ![](images/add_package_url.png) -**2.Parameter configuration** +### 2.Parameter configuration Downlod your `amplifyconfiguration.json` file from your Clickstream solution control plane, and paste it to your project root folder: @@ -62,15 +62,15 @@ Your `appId` and `endpoint` are already set up in it, here's an explanation of e - **autoFlushEventsInterval**: event sending interval, the default is `10s` - **isTrackAppExceptionEvents**: whether auto track exception event in app, default is `false` -**3.Initialize the SDK** +### 3.Initialize the SDK Once you have configured the parameters, you need to initialize it in your app delegate's `application(_:didFinishLaunchingWithOptions:)` lifecycle method: +#### 3.1 Initialize the SDK with default configuration ```swift import Clickstream ... func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. do { try ClickstreamAnalytics.initSDK() } catch { @@ -80,6 +80,34 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau } ``` +#### 3.2 Initialize the SDK with global attributes and custom configuration + +```swift +import Clickstream +... +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + do { + let configuration = ClickstreamConfiguration() + .withAppId("your appId") + .withEndpoint("https://example.com/collect") + .withLogEvents(true) + .withInitialGlobalAttributes([ + "_traffic_source_name": "Summer promotion", + "_traffic_source_medium": "Search engine" + ]) + try ClickstreamAnalytics.initSDK(configuration) + } catch { + assertionFailure("Fail to initialize ClickstreamAnalytics: \(error)") + } + return true +} +``` + +By default, we will use the configurations in `amplifyconfiguration.json` file. If you add a custom configuration, the added configuration items will override the default values. + +You can also add all the configuration parameters you need in the `initSDK` method without using the `amplifyconfiguration.json` file. + +#### 3.3 SwiftUI configuration If your project is developed with SwiftUI, you need to create an application delegate and attach it to your `App` through `UIApplicationDelegateAdaptor`. ```swift @@ -94,24 +122,24 @@ struct YourApp: App { } ``` -You also need to disable swzzling by setting `configuration.isTrackScreenViewEvents = false`, see the next configuration steps. +Clickstream Swift SDK depends on method swizzling to automatically record screen views. SwiftUI apps must [record screen view events manually](#record-screen-view-events-manually), and disable automatic tracking of screen view events by setting `configuration.withTrackScreenViewEvents(false)` when the SDK is initialized. -**4.Configure the SDK** +### 4. Update Configuration ```swift import Clickstream -// config the sdk after initialize. +// configure the sdk after initialize. do { - var configuration = try ClickstreamAnalytics.getClickstreamConfiguration() - configuration.appId = "appId" - configuration.endpoint = "https://example.com/collect" - configuration.authCookie = "your authentication cookie" - configuration.sessionTimeoutDuration = 1800000 - configuration.isTrackScreenViewEvents = true - configuration.isTrackUserEngagementEvents = true - configuration.isLogEvents = true - configuration.isCompressEvents = true + let configuration = try ClickstreamAnalytics.getClickstreamConfiguration() + configuration.withAppId("your appId") + .withEndpoint("https://example.com/collect") + .withLogEvents(true) + .withCompressEvents(true) + .withSessionTimeoutDuration(1800000) + .withTrackAppExceptionEvents(true) + .withTrackScreenViewEvents(true) + .withTrackUserEngagementEvents(true) } catch { print("Failed to config ClickstreamAnalytics: \(error)") } @@ -136,7 +164,7 @@ let attributes: ClickstreamAttribute = [ ] ClickstreamAnalytics.recordEvent("testEvent", attributes) -// for record an event directly +// for record an event with event name ClickstreamAnalytics.recordEvent("button_click") ``` diff --git a/Sources/Clickstream/AWSClickstreamPlugin+Configure.swift b/Sources/Clickstream/AWSClickstreamPlugin+Configure.swift index 1d3b7d0..c153e66 100644 --- a/Sources/Clickstream/AWSClickstreamPlugin+Configure.swift +++ b/Sources/Clickstream/AWSClickstreamPlugin+Configure.swift @@ -26,15 +26,10 @@ extension AWSClickstreamPlugin { } /// Configure AWSClickstreamPlugin programatically using AWSClickstreamConfiguration - func configure(using configuration: AWSClickstreamConfiguration) throws { - let contextConfiguration = ClickstreamContextConfiguration(appId: configuration.appId, - endpoint: configuration.endpoint, - sendEventsInterval: configuration.sendEventsInterval, - isTrackAppExceptionEvents: - configuration.isTrackAppExceptionEvents, - isCompressEvents: configuration.isCompressEvents) - clickstream = try ClickstreamContext(with: contextConfiguration) + func configure(using amplifyConfigure: AWSClickstreamConfiguration) throws { + try mergeConfiguration(amplifyConfigure: amplifyConfigure) + clickstream = try ClickstreamContext(with: configuration) let sessionClient = SessionClient(clickstream: clickstream) clickstream.sessionClient = sessionClient let eventRecorder = try EventRecorder(clickstream: clickstream) @@ -61,6 +56,7 @@ extension AWSClickstreamPlugin { autoFlushEventsTimer: autoFlushEventsTimer, networkMonitor: networkMonitor ) + initGlobalAttributes() log.debug("initialize Clickstream SDK successful") sessionClient.handleAppStart() } @@ -82,4 +78,79 @@ extension AWSClickstreamPlugin { ) ) } + + /// Internal method to merge the configurations + func mergeConfiguration(amplifyConfigure: AWSClickstreamConfiguration) throws { + let defaultConfiguration = ClickstreamConfiguration.getDefaultConfiguration() + if (configuration.appId.isNilOrEmpty() && amplifyConfigure.appId.isEmpty) || + (configuration.endpoint.isNilOrEmpty() && amplifyConfigure.endpoint.isEmpty) + { + throw ConfigurationError.unableToDecode( + "Configuration Error: `appId` and `endpoint` are required", """ + Ensure they are correctly set in `amplifyconfiguration.json`\ + or provided during SDK initialization with `initSDK()` + """ + ) + } + + if configuration.appId.isNilOrEmpty() { + defaultConfiguration.appId = amplifyConfigure.appId + } else { + defaultConfiguration.appId = configuration.appId + } + if configuration.endpoint.isNilOrEmpty() { + defaultConfiguration.endpoint = amplifyConfigure.endpoint + } else { + defaultConfiguration.endpoint = configuration.endpoint + } + if configuration.sendEventsInterval > 0 { + defaultConfiguration.sendEventsInterval = configuration.sendEventsInterval + } else if amplifyConfigure.sendEventsInterval > 0 { + defaultConfiguration.sendEventsInterval = amplifyConfigure.sendEventsInterval + } + if configuration.isTrackAppExceptionEvents != nil { + defaultConfiguration.isTrackAppExceptionEvents = configuration.isTrackAppExceptionEvents + } else if amplifyConfigure.isTrackAppExceptionEvents != nil { + defaultConfiguration.isTrackAppExceptionEvents = amplifyConfigure.isTrackAppExceptionEvents + } + if configuration.isCompressEvents != nil { + defaultConfiguration.isCompressEvents = configuration.isCompressEvents + } else if amplifyConfigure.isCompressEvents != nil { + defaultConfiguration.isCompressEvents = amplifyConfigure.isCompressEvents + } + + mergeDefaultConfiguration(defaultConfiguration) + configuration = defaultConfiguration + } + + /// Internal method to merge the default configurations + func mergeDefaultConfiguration(_ defaultConfiguration: ClickstreamConfiguration) { + if let isTrackScreenViewEvents = configuration.isTrackScreenViewEvents { + defaultConfiguration.isTrackScreenViewEvents = isTrackScreenViewEvents + } + if let isTrackUserEngagementEvents = configuration.isTrackUserEngagementEvents { + defaultConfiguration.isTrackUserEngagementEvents = isTrackUserEngagementEvents + } + if let isLogEvents = configuration.isLogEvents { + defaultConfiguration.isLogEvents = isLogEvents + } + if configuration.sessionTimeoutDuration > 0 { + defaultConfiguration.sessionTimeoutDuration = configuration.sessionTimeoutDuration + } + if configuration.authCookie != nil { + defaultConfiguration.authCookie = configuration.authCookie + } + if configuration.globalAttributes != nil { + defaultConfiguration.globalAttributes = configuration.globalAttributes + } + } + + /// Internal method to add global attributes + func initGlobalAttributes() { + if let globalAttributes = configuration.globalAttributes { + for (key, value) in globalAttributes { + analyticsClient.addGlobalAttribute(value, forKey: key) + } + } + } } diff --git a/Sources/Clickstream/AWSClickstreamPlugin.swift b/Sources/Clickstream/AWSClickstreamPlugin.swift index 8eb3272..d9e8cb7 100644 --- a/Sources/Clickstream/AWSClickstreamPlugin.swift +++ b/Sources/Clickstream/AWSClickstreamPlugin.swift @@ -28,6 +28,10 @@ final class AWSClickstreamPlugin: AnalyticsCategoryPlugin { "awsClickstreamPlugin" } + var configuration: ClickstreamConfiguration + /// Instantiates an instance of the AWSClickstreamPlugin - init() {} + init(_ configuration: ClickstreamConfiguration? = nil) { + self.configuration = configuration ?? ClickstreamConfiguration() + } } diff --git a/Sources/Clickstream/ClickstreamAnalytics.swift b/Sources/Clickstream/ClickstreamAnalytics.swift index 4a53c40..03e17eb 100644 --- a/Sources/Clickstream/ClickstreamAnalytics.swift +++ b/Sources/Clickstream/ClickstreamAnalytics.swift @@ -6,13 +6,14 @@ // import Amplify +import Foundation /// ClickstreamAnalytics api for swift public enum ClickstreamAnalytics { /// Init ClickstreamAnalytics - public static func initSDK() throws { - try Amplify.add(plugin: AWSClickstreamPlugin()) - try Amplify.configure() + public static func initSDK(_ configuration: ClickstreamConfiguration? = nil) throws { + try Amplify.add(plugin: AWSClickstreamPlugin(configuration)) + try Amplify.configure(getAmplifyConfigurationSafely()) } /// Use this method to record event @@ -70,7 +71,7 @@ public enum ClickstreamAnalytics { /// Get Clickstream configuration, please config it after initialize sdk /// - Returns: ClickstreamContextConfiguration to modify the configuration of clickstream sdk - public static func getClickstreamConfiguration() throws -> ClickstreamContextConfiguration { + public static func getClickstreamConfiguration() throws -> ClickstreamConfiguration { let plugin = try Amplify.Analytics.getPlugin(for: "awsClickstreamPlugin") // swiftlint:disable force_cast return (plugin as! AWSClickstreamPlugin).getEscapeHatch().configuration @@ -89,6 +90,22 @@ public enum ClickstreamAnalytics { Amplify.Analytics.enable() } + static func getAmplifyConfigurationSafely(_ bundle: Bundle = Bundle.main) throws -> AmplifyConfiguration { + guard let path = bundle.path(forResource: "amplifyconfiguration", ofType: "json") else { + log.debug("Could not load default `amplifyconfiguration.json` file") + let plugins: [String: JSONValue] = [ + "awsClickstreamPlugin": [ + "appId": JSONValue.string(""), + "endpoint": JSONValue.string("") + ] + ] + let analyticsConfiguration = AnalyticsCategoryConfiguration(plugins: plugins) + return AmplifyConfiguration(analytics: analyticsConfiguration) + } + let url = URL(fileURLWithPath: path) + return try AmplifyConfiguration(configurationFile: url) + } + /// ClickstreamAnalytics preset events public enum EventName { public static let SCREEN_VIEW = "_screen_view" @@ -133,3 +150,5 @@ public enum ClickstreamAnalytics { public static let ITEM_CATEGORY5 = "item_category5" } } + +extension ClickstreamAnalytics: ClickstreamLogger {} diff --git a/Sources/Clickstream/ClickstreamObjc.swift b/Sources/Clickstream/ClickstreamObjc.swift index bfdfdbc..d00b6fb 100644 --- a/Sources/Clickstream/ClickstreamObjc.swift +++ b/Sources/Clickstream/ClickstreamObjc.swift @@ -20,6 +20,11 @@ import Foundation try ClickstreamAnalytics.initSDK() } + /// Init the Clickstream sdk + public static func initSDK(_ configuration: ClickstreamConfiguration) throws { + try ClickstreamAnalytics.initSDK(configuration) + } + /// Use this method to record event /// - Parameter eventName: the event name public static func recordEvent(_ eventName: String) { @@ -76,7 +81,7 @@ import Foundation /// Get Clickstream configuration, please config it after initialize sdk /// - Returns: ClickstreamContextConfiguration to modify the configuration of clickstream sdk - public static func getClickstreamConfiguration() throws -> ClickstreamContextConfiguration { + public static func getClickstreamConfiguration() throws -> ClickstreamConfiguration { try ClickstreamAnalytics.getClickstreamConfiguration() } diff --git a/Sources/Clickstream/Configuration/AWSClickstreamPluginConfiguration.swift b/Sources/Clickstream/Configuration/AWSClickstreamPluginConfiguration.swift index 9d4de87..c79fff4 100644 --- a/Sources/Clickstream/Configuration/AWSClickstreamPluginConfiguration.swift +++ b/Sources/Clickstream/Configuration/AWSClickstreamPluginConfiguration.swift @@ -14,15 +14,11 @@ struct AWSClickstreamConfiguration { static let isTrackAppExceptionKey = "isTrackAppExceptionEvents" static let isCompressEventsKey = "isCompressEvents" - static let defaultSendEventsInterval = 10_000 - static let defaulTrackAppException = true - static let defaulCompressEvents = true - let appId: String let endpoint: String let sendEventsInterval: Int - let isTrackAppExceptionEvents: Bool - let isCompressEvents: Bool + let isTrackAppExceptionEvents: Bool! + let isCompressEvents: Bool! init(_ configuration: JSONValue) throws { guard case let .object(configObject) = configuration else { @@ -48,8 +44,8 @@ struct AWSClickstreamConfiguration { init(appId: String, endpoint: String, sendEventsInterval: Int, - isTrackAppExceptionEvents: Bool, - isCompressEvents: Bool) + isTrackAppExceptionEvents: Bool!, + isCompressEvents: Bool!) { self.appId = appId self.endpoint = endpoint @@ -73,13 +69,6 @@ struct AWSClickstreamConfiguration { ) } - if appIdValue.isEmpty { - throw PluginError.pluginConfigurationError( - "appId is specified but is empty", - "appId should not be empty" - ) - } - return appIdValue } @@ -98,19 +87,12 @@ struct AWSClickstreamConfiguration { ) } - if endpointValue.isEmpty { - throw PluginError.pluginConfigurationError( - "endpoint is specified but is empty", - "endpoint should not be empty" - ) - } - return endpointValue } private static func getSendEventsInterval(_ configuration: [String: JSONValue]) throws -> Int { guard let sendEventsInterval = configuration[sendEventsIntervalKey] else { - return AWSClickstreamConfiguration.defaultSendEventsInterval + return 0 } guard case let .number(sendEventsIntervalValue) = sendEventsInterval else { @@ -130,9 +112,9 @@ struct AWSClickstreamConfiguration { return Int(sendEventsIntervalValue) } - private static func getIsTrackAppExceptionEvents(_ configuration: [String: JSONValue]) throws -> Bool { + private static func getIsTrackAppExceptionEvents(_ configuration: [String: JSONValue]) throws -> Bool! { guard let isTrackAppException = configuration[isTrackAppExceptionKey] else { - return AWSClickstreamConfiguration.defaulTrackAppException + return nil } guard case let .boolean(isTrackAppExceptionValue) = isTrackAppException else { @@ -145,9 +127,9 @@ struct AWSClickstreamConfiguration { return isTrackAppExceptionValue } - private static func getIsCompressEvents(_ configuration: [String: JSONValue]) throws -> Bool { + private static func getIsCompressEvents(_ configuration: [String: JSONValue]) throws -> Bool! { guard let isCompressEvents = configuration[isCompressEventsKey] else { - return AWSClickstreamConfiguration.defaulCompressEvents + return nil } guard case let .boolean(isCompressEventsValue) = isCompressEvents else { diff --git a/Sources/Clickstream/Dependency/Clickstream/ClickstreamContext.swift b/Sources/Clickstream/Dependency/Clickstream/ClickstreamContext.swift index 0766c8f..094d5f5 100644 --- a/Sources/Clickstream/Dependency/Clickstream/ClickstreamContext.swift +++ b/Sources/Clickstream/Dependency/Clickstream/ClickstreamContext.swift @@ -34,46 +34,95 @@ extension UserDefaults: UserDefaultsBehaviour { // MARK: - ClickstreamContext /// The configuration object for clickstream, modify the params after sdk initialize -@objcMembers public class ClickstreamContextConfiguration: NSObject { +@objcMembers public class ClickstreamConfiguration: NSObject { /// The clickstream appId - public var appId: String + var appId: String! /// The clickstream endpoint - public var endpoint: String + var endpoint: String! /// Time interval after which the events are automatically submitted to server - private let sendEventsInterval: Int + var sendEventsInterval: Int = 0 /// Whether to track app exception events automatically - var isTrackAppExceptionEvents: Bool + var isTrackAppExceptionEvents: Bool! /// Whether to track app scren view events automatically - public var isTrackScreenViewEvents: Bool - public var isTrackUserEngagementEvents: Bool + var isTrackScreenViewEvents: Bool! + /// Whether to track app user engagement events automatically + var isTrackUserEngagementEvents: Bool! /// Whether to compress events when send to server - public var isCompressEvents: Bool + var isCompressEvents: Bool! /// Whether to log events json in console when debug - public var isLogEvents: Bool + var isLogEvents: Bool! /// The auth cookie for request - public var authCookie: String? + var authCookie: String? /// The session timeout calculated the duration from last app in background, defalut is 1800000ms - public var sessionTimeoutDuration: Int64 - - init(appId: String, - endpoint: String, - sendEventsInterval: Int, - isTrackAppExceptionEvents: Bool = true, - isTrackScreenViewEvents: Bool = true, - isTrackUserEngagementEvents: Bool = true, - isCompressEvents: Bool = true, - isLogEvents: Bool = false, - sessionTimeoutDuration: Int64 = 1_800_000) - { + var sessionTimeoutDuration: Int64 = 0 + /// The global attributes when initialize the SDK + var globalAttributes: ClickstreamAttribute? + + static func getDefaultConfiguration() -> ClickstreamConfiguration { + let configuration = ClickstreamConfiguration() + configuration.sendEventsInterval = 10_000 + configuration.isTrackAppExceptionEvents = false + configuration.isTrackScreenViewEvents = true + configuration.isTrackUserEngagementEvents = true + configuration.isCompressEvents = true + configuration.isLogEvents = false + configuration.sessionTimeoutDuration = 1_800_000 + return configuration + } + + public func withAppId(_ appId: String) -> ClickstreamConfiguration { self.appId = appId + return self + } + + public func withEndpoint(_ endpoint: String) -> ClickstreamConfiguration { self.endpoint = endpoint + return self + } + + public func withSendEventInterval(_ sendEventsInterval: Int) -> ClickstreamConfiguration { self.sendEventsInterval = sendEventsInterval + return self + } + + public func withTrackAppExceptionEvents(_ isTrackAppExceptionEvents: Bool) -> ClickstreamConfiguration { self.isTrackAppExceptionEvents = isTrackAppExceptionEvents + return self + } + + public func withTrackScreenViewEvents(_ isTrackScreenViewEvents: Bool) -> ClickstreamConfiguration { self.isTrackScreenViewEvents = isTrackScreenViewEvents + return self + } + + public func withTrackUserEngagementEvents(_ isTrackUserEngagementEvents: Bool) -> ClickstreamConfiguration { self.isTrackUserEngagementEvents = isTrackUserEngagementEvents + return self + } + + public func withCompressEvents(_ isCompressEvents: Bool) -> ClickstreamConfiguration { self.isCompressEvents = isCompressEvents + return self + } + + public func withLogEvents(_ isLogEvents: Bool) -> ClickstreamConfiguration { self.isLogEvents = isLogEvents + return self + } + + public func withAuthCookie(_ authCookie: String) -> ClickstreamConfiguration { + self.authCookie = authCookie + return self + } + + public func withSessionTimeoutDuration(_ sessionTimeoutDuration: Int64) -> ClickstreamConfiguration { self.sessionTimeoutDuration = sessionTimeoutDuration + return self + } + + public func withInitialGlobalAttributes(_ globalAttributes: ClickstreamAttribute) -> ClickstreamConfiguration { + self.globalAttributes = globalAttributes + return self } } @@ -86,12 +135,12 @@ class ClickstreamContext { var analyticsClient: AnalyticsClientBehaviour! var networkMonitor: NetworkMonitor! let systemInfo: SystemInfo - var configuration: ClickstreamContextConfiguration + var configuration: ClickstreamConfiguration var userUniqueId: String let storage: ClickstreamContextStorage var isEnable: Bool - init(with configuration: ClickstreamContextConfiguration, + init(with configuration: ClickstreamConfiguration, userDefaults: UserDefaultsBehaviour = UserDefaults.standard) throws { self.storage = ClickstreamContextStorage(userDefaults: userDefaults) diff --git a/Sources/Clickstream/Network/NetRequest.swift b/Sources/Clickstream/Network/NetRequest.swift index 1183b57..a3d55f1 100644 --- a/Sources/Clickstream/Network/NetRequest.swift +++ b/Sources/Clickstream/Network/NetRequest.swift @@ -11,7 +11,7 @@ import Foundation enum NetRequest { static func uploadEventWithURLSession( eventsJson: String, - configuration: ClickstreamContextConfiguration, + configuration: ClickstreamConfiguration, bundleSequenceId: Int) -> Bool { var requestData = eventsJson diff --git a/Sources/Clickstream/Support/Extension/String+HashCode.swift b/Sources/Clickstream/Support/Extension/String+HashCode.swift index f831857..933ef1e 100644 --- a/Sources/Clickstream/Support/Extension/String+HashCode.swift +++ b/Sources/Clickstream/Support/Extension/String+HashCode.swift @@ -18,3 +18,9 @@ extension String { return "" } } + +extension String? { + func isNilOrEmpty() -> Bool { + self?.isEmpty ?? true + } +} diff --git a/Tests/ClickstreamTests/Clickstream/AnalyticsClientTest.swift b/Tests/ClickstreamTests/Clickstream/AnalyticsClientTest.swift index f09ec5a..a53fab6 100644 --- a/Tests/ClickstreamTests/Clickstream/AnalyticsClientTest.swift +++ b/Tests/ClickstreamTests/Clickstream/AnalyticsClientTest.swift @@ -18,11 +18,12 @@ class AnalyticsClientTest: XCTestCase { override func setUp() async throws { UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) - let contextConfiguration = ClickstreamContextConfiguration(appId: testAppId, - endpoint: testEndpoint, - sendEventsInterval: 10_000, - isTrackAppExceptionEvents: false, - isCompressEvents: false) + let contextConfiguration = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId(testAppId) + .withEndpoint(testEndpoint) + .withSendEventInterval(10_000) + .withTrackAppExceptionEvents(false) + .withCompressEvents(false) clickstream = try ClickstreamContext(with: contextConfiguration) let sessionClient = SessionClient(clickstream: clickstream) clickstream.sessionClient = sessionClient diff --git a/Tests/ClickstreamTests/Clickstream/AutoRecordEventClientTest.swift b/Tests/ClickstreamTests/Clickstream/AutoRecordEventClientTest.swift index 47490c3..4d9f178 100644 --- a/Tests/ClickstreamTests/Clickstream/AutoRecordEventClientTest.swift +++ b/Tests/ClickstreamTests/Clickstream/AutoRecordEventClientTest.swift @@ -22,11 +22,13 @@ class AutoRecordEventClientTest: XCTestCase { UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) let mockNetworkMonitor = MockNetworkMonitor() activityTracker = MockActivityTracker() - let contextConfiguration = ClickstreamContextConfiguration(appId: testAppId, - endpoint: testEndpoint, - sendEventsInterval: 10_000, - isTrackAppExceptionEvents: true, - isCompressEvents: false) + + let contextConfiguration = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId(testAppId) + .withEndpoint(testEndpoint) + .withSendEventInterval(10_000) + .withTrackAppExceptionEvents(true) + .withCompressEvents(false) clickstream = try ClickstreamContext(with: contextConfiguration) clickstream.networkMonitor = mockNetworkMonitor sessionClient = SessionClient(activityTracker: activityTracker, clickstream: clickstream) diff --git a/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift b/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift index f1ebdfd..69e6d7e 100644 --- a/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift +++ b/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift @@ -41,11 +41,13 @@ class EventRecorderTest: XCTestCase { } try! server.start() let appId = testAppId + String(describing: Date().millisecondsSince1970) - let contextConfiguration = ClickstreamContextConfiguration(appId: appId, - endpoint: testSuccessEndpoint, - sendEventsInterval: 10_000, - isTrackAppExceptionEvents: false, - isCompressEvents: false) + + let contextConfiguration = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId(appId) + .withEndpoint(testSuccessEndpoint) + .withSendEventInterval(10_000) + .withTrackAppExceptionEvents(false) + .withCompressEvents(false) contextConfiguration.isLogEvents = false clickstream = try ClickstreamContext(with: contextConfiguration) clickstreamEvent = ClickstreamEvent(eventType: "testEvent", diff --git a/Tests/ClickstreamTests/Clickstream/SessionClientTests.swift b/Tests/ClickstreamTests/Clickstream/SessionClientTests.swift index 64c1fd6..288cc0d 100644 --- a/Tests/ClickstreamTests/Clickstream/SessionClientTests.swift +++ b/Tests/ClickstreamTests/Clickstream/SessionClientTests.swift @@ -22,11 +22,13 @@ class SessionClientTests: XCTestCase { UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) activityTracker = MockActivityTracker() mockNetworkMonitor = MockNetworkMonitor() - let contextConfiguration = ClickstreamContextConfiguration(appId: testAppId, - endpoint: testEndpoint, - sendEventsInterval: 10_000, - isTrackAppExceptionEvents: false, - isCompressEvents: false) + + let contextConfiguration = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId(testAppId) + .withEndpoint(testEndpoint) + .withSendEventInterval(10_000) + .withTrackAppExceptionEvents(false) + .withCompressEvents(false) clickstream = try ClickstreamContext(with: contextConfiguration) clickstream.networkMonitor = mockNetworkMonitor sessionClient = SessionClient(activityTracker: activityTracker, clickstream: clickstream) @@ -268,4 +270,22 @@ class SessionClientTests: XCTestCase { let events = eventRecorder.savedEvents XCTAssertEqual(0, events.count) } + + func testActivityTrackerApplicationState() { + let state1 = ApplicationState.Resolver.resolve(currentState: ApplicationState.terminated, + event: ActivityEvent.applicationWillMoveToForeground) + XCTAssertEqual(ApplicationState.terminated, state1) + + let state2 = ApplicationState.Resolver.resolve(currentState: ApplicationState.runningInBackground, + event: ActivityEvent.applicationWillTerminate) + XCTAssertEqual(ApplicationState.terminated, state2) + + let state3 = ApplicationState.Resolver.resolve(currentState: ApplicationState.runningInBackground, + event: ActivityEvent.applicationDidMoveToBackground) + XCTAssertEqual(ApplicationState.runningInBackground, state3) + + let state4 = ApplicationState.Resolver.resolve(currentState: ApplicationState.runningInForeground, + event: ActivityEvent.applicationWillMoveToForeground) + XCTAssertEqual(ApplicationState.runningInForeground, state4) + } } diff --git a/Tests/ClickstreamTests/ClickstreamAnalyticsTest.swift b/Tests/ClickstreamTests/ClickstreamAnalyticsTest.swift index 1c2820c..444444a 100644 --- a/Tests/ClickstreamTests/ClickstreamAnalyticsTest.swift +++ b/Tests/ClickstreamTests/ClickstreamAnalyticsTest.swift @@ -5,32 +5,47 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@testable import Amplify import Clickstream import XCTest class ClickstreamAnalyticsTest: XCTestCase { + override func setUp() async throws { + await Amplify.reset() + } + + override func tearDown() async throws { + await Amplify.reset() + } + func testThrowMissingConfigureFileWhenInitSDK() throws { do { try ClickstreamAnalytics.initSDK() XCTFail("Should have thrown a invalidAmplifyConfigurationFile error, if no configuration file is specified") } catch { - guard case ConfigurationError.invalidAmplifyConfigurationFile = error else { + guard case ConfigurationError.unableToDecode = error else { XCTFail("Should have thrown a invalidAmplifyConfigurationFile error") return } } } - func testThrowMissingConfigureFileWhenInitSDKForObjc() throws { + func testInitSDKForObjc() throws { do { try ClickstreamObjc.initSDK() XCTFail("Should have thrown a invalidAmplifyConfigurationFile error, if no configuration file is specified") } catch { - guard case ConfigurationError.invalidAmplifyConfigurationFile = error else { + guard case ConfigurationError.unableToDecode = error else { XCTFail("Should have thrown a invalidAmplifyConfigurationFile error") return } } } + + func testInitSDKWithConfigurationForObjc() throws { + let config = ClickstreamConfiguration() + .withAppId("testAppId") + .withEndpoint("http://example.com/collect") + try ClickstreamObjc.initSDK(config) + } } diff --git a/Tests/ClickstreamTests/ClickstreamPluginTestBase.swift b/Tests/ClickstreamTests/ClickstreamPluginTestBase.swift index 842169d..6b018e0 100644 --- a/Tests/ClickstreamTests/ClickstreamPluginTestBase.swift +++ b/Tests/ClickstreamTests/ClickstreamPluginTestBase.swift @@ -19,11 +19,13 @@ class ClickstreamPluginTestBase: XCTestCase { override func setUp() async throws { mockNetworkMonitor = MockNetworkMonitor() analyticsPlugin = AWSClickstreamPlugin() - let contextConfiguration = ClickstreamContextConfiguration(appId: testAppId, - endpoint: testEndpoint, - sendEventsInterval: 10_000, - isTrackAppExceptionEvents: false, - isCompressEvents: false) + + let contextConfiguration = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId(testAppId) + .withEndpoint(testEndpoint) + .withSendEventInterval(10_000) + .withTrackAppExceptionEvents(false) + .withCompressEvents(false) clickstream = try ClickstreamContext(with: contextConfiguration) let sessionClient = SessionClient(clickstream: clickstream) diff --git a/Tests/ClickstreamTests/Configuration/ClickstreamPluginConfigurationTest.swift b/Tests/ClickstreamTests/Configuration/ClickstreamPluginConfigurationTest.swift index ad89c20..813a16f 100644 --- a/Tests/ClickstreamTests/Configuration/ClickstreamPluginConfigurationTest.swift +++ b/Tests/ClickstreamTests/Configuration/ClickstreamPluginConfigurationTest.swift @@ -29,9 +29,9 @@ class ClickstreamPluginConfigurationTest: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.endpoint, testEndpoint) - XCTAssertEqual(config.sendEventsInterval, AWSClickstreamConfiguration.defaultSendEventsInterval) - XCTAssertEqual(config.isTrackAppExceptionEvents, AWSClickstreamConfiguration.defaulTrackAppException) - XCTAssertEqual(config.isCompressEvents, AWSClickstreamConfiguration.defaulCompressEvents) + XCTAssertEqual(config.sendEventsInterval, 0) + XCTAssertNil(config.isTrackAppExceptionEvents) + XCTAssertNil(config.isCompressEvents) } catch { XCTFail("Failed to instantiate clicstream plugin configuration") } @@ -62,16 +62,6 @@ class ClickstreamPluginConfigurationTest: XCTestCase { assertInitPluginError(errString: errString, configJson: configJson) } - func testConfigureWithEmptyStringAppId() { - let configJson = JSONValue( - dictionaryLiteral: - (AWSClickstreamConfiguration.appIdKey, ""), - (AWSClickstreamConfiguration.endpointKey, endpoint) - ) - let errString = "appId is specified but is empty" - assertInitPluginError(errString: errString, configJson: configJson) - } - func testConfigureWithNoEndpoint() { let configJson = JSONValue( dictionaryLiteral: @@ -91,16 +81,6 @@ class ClickstreamPluginConfigurationTest: XCTestCase { assertInitPluginError(errString: errString, configJson: configJson) } - func testConfigureWithEmptyStringEndpoint() { - let configJson = JSONValue( - dictionaryLiteral: - (AWSClickstreamConfiguration.appIdKey, appId), - (AWSClickstreamConfiguration.endpointKey, "") - ) - let errString = "endpoint is specified but is empty" - assertInitPluginError(errString: errString, configJson: configJson) - } - func testConfigureSuccessWithCustomSendEventsInterval() { let configJson = JSONValue( dictionaryLiteral: @@ -114,8 +94,8 @@ class ClickstreamPluginConfigurationTest: XCTestCase { XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.endpoint, testEndpoint) XCTAssertEqual(config.sendEventsInterval, testSendEventsInterval) - XCTAssertEqual(config.isTrackAppExceptionEvents, AWSClickstreamConfiguration.defaulTrackAppException) - XCTAssertEqual(config.isCompressEvents, AWSClickstreamConfiguration.defaulCompressEvents) + XCTAssertNil(config.isTrackAppExceptionEvents) + XCTAssertNil(config.isCompressEvents) } catch { XCTFail("Failed to instantiate clicstream plugin configuration") } @@ -155,9 +135,9 @@ class ClickstreamPluginConfigurationTest: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.endpoint, testEndpoint) - XCTAssertEqual(config.sendEventsInterval, AWSClickstreamConfiguration.defaultSendEventsInterval) + XCTAssertEqual(config.sendEventsInterval, 0) XCTAssertEqual(config.isTrackAppExceptionEvents, false) - XCTAssertEqual(config.isCompressEvents, AWSClickstreamConfiguration.defaulCompressEvents) + XCTAssertNil(config.isCompressEvents) } catch { XCTFail("Failed to instantiate clicstream plugin configuration") } @@ -186,7 +166,7 @@ class ClickstreamPluginConfigurationTest: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.endpoint, testEndpoint) - XCTAssertEqual(config.sendEventsInterval, AWSClickstreamConfiguration.defaultSendEventsInterval) + XCTAssertEqual(config.sendEventsInterval, 0) XCTAssertEqual(config.isCompressEvents, false) } catch { XCTFail("Failed to instantiate clicstream plugin configuration") diff --git a/Tests/ClickstreamTests/IntegrationTest.swift b/Tests/ClickstreamTests/IntegrationTest.swift index 7e6adc3..fd2797b 100644 --- a/Tests/ClickstreamTests/IntegrationTest.swift +++ b/Tests/ClickstreamTests/IntegrationTest.swift @@ -29,23 +29,18 @@ class IntegrationTest: XCTestCase { HttpResponse.badRequest(.text("request fail")) } try! server.start() - analyticsPlugin = AWSClickstreamPlugin() - let appId = JSONValue(stringLiteral: "testAppId" + String(describing: Date().millisecondsSince1970)) await Amplify.reset() - let plugins: [String: JSONValue] = [ - "awsClickstreamPlugin": [ - "appId": appId, - "endpoint": "http://localhost:8080/collect", - "isCompressEvents": false, - "autoFlushEventsInterval": 10_000, - "isTrackAppExceptionEvents": false - ] - ] - let analyticsConfiguration = AnalyticsCategoryConfiguration(plugins: plugins) - let config = AmplifyConfiguration(analytics: analyticsConfiguration) + let appId = "testAppId" + String(describing: Date().millisecondsSince1970) + let configure = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId(appId) + .withEndpoint("http://localhost:8080/collect") + .withCompressEvents(false) + .withTrackAppExceptionEvents(false) + .withSendEventInterval(10_000) + analyticsPlugin = AWSClickstreamPlugin(configure) do { try Amplify.add(plugin: analyticsPlugin) - try Amplify.configure(config) + try Amplify.configure(ClickstreamAnalytics.getAmplifyConfigurationSafely()) analyticsClient = analyticsPlugin!.analyticsClient as? AnalyticsClient eventRecorder = analyticsClient.eventRecorder as? EventRecorder } catch { diff --git a/Tests/ClickstreamTests/SDKInitialTest.swift b/Tests/ClickstreamTests/SDKInitialTest.swift new file mode 100644 index 0000000..b8694d8 --- /dev/null +++ b/Tests/ClickstreamTests/SDKInitialTest.swift @@ -0,0 +1,137 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +@testable import Clickstream +import Foundation +import XCTest + +class SDKInitialTest: XCTestCase { + override func setUp() async throws { + await Amplify.reset() + } + + override func tearDown() async throws { + await Amplify.reset() + } + + func testInitSDKWithoutAnyConfiguration() throws { + XCTAssertThrowsError(try ClickstreamAnalytics.initSDK()) { error in + XCTAssertTrue(error is ConfigurationError) + } + } + + func testInitSDKWithOnlyAmplifyJSONFile() throws { + let amplifyConfigure = try ClickstreamAnalytics.getAmplifyConfigurationSafely(Bundle.module) + try Amplify.add(plugin: AWSClickstreamPlugin()) + try Amplify.configure(amplifyConfigure) + } + + func testInitSDKWithAllConfiguration() throws { + let configure = ClickstreamConfiguration() + .withAppId("testAppId") + .withEndpoint("https://example.com/collect") + .withLogEvents(true) + .withCompressEvents(false) + .withSessionTimeoutDuration(100_000) + .withSendEventInterval(15_000) + .withTrackAppExceptionEvents(true) + .withTrackScreenViewEvents(false) + .withTrackUserEngagementEvents(false) + .withAuthCookie("testAuthCookie") + .withInitialGlobalAttributes([ + "channel": "AppStore", + "level": 5.1, + "class": 5, + "isOpenNotification": true + ]) + try ClickstreamAnalytics.initSDK(configure) + } + + func testInitSDKOverrideAllAmplifyConfiguration() throws { + let configure = ClickstreamConfiguration() + .withAppId("testAppId1") + .withEndpoint("https://example.com/collect1") + .withLogEvents(true) + .withCompressEvents(false) + .withSessionTimeoutDuration(100_000) + .withSendEventInterval(15_000) + .withTrackAppExceptionEvents(true) + .withTrackScreenViewEvents(false) + .withTrackUserEngagementEvents(false) + .withAuthCookie("testAuthCookie") + let amplifyConfigure = try ClickstreamAnalytics.getAmplifyConfigurationSafely(Bundle.module) + try Amplify.add(plugin: AWSClickstreamPlugin(configure)) + try Amplify.configure(amplifyConfigure) + let resultConfig = try ClickstreamAnalytics.getClickstreamConfiguration() + XCTAssertEqual("testAppId1", resultConfig.appId) + XCTAssertEqual("https://example.com/collect1", resultConfig.endpoint) + XCTAssertEqual(true, resultConfig.isLogEvents) + XCTAssertEqual(false, resultConfig.isCompressEvents) + XCTAssertEqual(true, resultConfig.isTrackAppExceptionEvents) + } + + func testInitSDKOverrideSomeAmplifyConfiguration() throws { + let configure = ClickstreamConfiguration() + .withLogEvents(true) + .withCompressEvents(false) + .withSessionTimeoutDuration(100_000) + .withSendEventInterval(15_000) + .withTrackScreenViewEvents(false) + .withTrackUserEngagementEvents(false) + .withAuthCookie("testAuthCookie") + let amplifyConfigure = try ClickstreamAnalytics.getAmplifyConfigurationSafely(Bundle.module) + try Amplify.add(plugin: AWSClickstreamPlugin(configure)) + try Amplify.configure(amplifyConfigure) + let resultConfig = try ClickstreamAnalytics.getClickstreamConfiguration() + XCTAssertEqual("testAppId", resultConfig.appId) + XCTAssertEqual("http://example.com/collect", resultConfig.endpoint) + XCTAssertEqual(true, resultConfig.isLogEvents) + XCTAssertEqual(false, resultConfig.isCompressEvents) + XCTAssertEqual(100_000, resultConfig.sessionTimeoutDuration) + XCTAssertEqual(15_000, resultConfig.sendEventsInterval) + XCTAssertEqual(false, resultConfig.isTrackScreenViewEvents) + XCTAssertEqual(false, resultConfig.isTrackUserEngagementEvents) + XCTAssertEqual("testAuthCookie", resultConfig.authCookie) + } + + func testRecordEventWithGlobalAttribute() throws { + let configure = ClickstreamConfiguration.getDefaultConfiguration() + .withAppId("testAppId") + .withEndpoint("https://example.com/collect") + .withInitialGlobalAttributes([ + "channel": "AppStore", + "Score": 90.1, + "class": 5, + "isOpenNotification": true + ]) + try ClickstreamAnalytics.initSDK(configure) + ClickstreamAnalytics.recordEvent("testEvent") + Thread.sleep(forTimeInterval: 0.1) + let plugin = try Amplify.Analytics.getPlugin(for: "awsClickstreamPlugin") + let analyticsClient = (plugin as! AWSClickstreamPlugin).analyticsClient as! AnalyticsClient + let eventRecorder = analyticsClient.eventRecorder as! EventRecorder + let testEvent = try getEventForName("testEvent", eventRecorder: eventRecorder) + let eventAttribute = testEvent["attributes"] as! [String: Any] + XCTAssertEqual("AppStore", eventAttribute["channel"] as! String) + XCTAssertEqual(90.1, eventAttribute["Score"] as! Double) + XCTAssertEqual(5, eventAttribute["class"] as! Int) + XCTAssertEqual(true, eventAttribute["isOpenNotification"] as! Bool) + } + + private func getEventForName(_ name: String, eventRecorder: EventRecorder) throws -> [String: Any] { + var testEvent: [String: Any] = JsonObject() + let eventArray = try eventRecorder.getBatchEvent().eventsJson.jsonArray() + for event in eventArray { + if event["event_type"] as! String == name { + testEvent = event + break + } + } + return testEvent + } +} diff --git a/Tests/ClickstreamTests/amplifyconfiguration.json b/Tests/ClickstreamTests/amplifyconfiguration.json new file mode 100644 index 0000000..2bf315e --- /dev/null +++ b/Tests/ClickstreamTests/amplifyconfiguration.json @@ -0,0 +1,15 @@ +{ + "UserAgent": "aws-solution/clickstream", + "Version": "1.0", + "analytics": { + "plugins": { + "awsClickstreamPlugin": { + "appId": "testAppId", + "endpoint": "http://example.com/collect", + "isCompressEvents": true, + "autoFlushEventsInterval": 10000, + "isTrackAppExceptionEvents": false + } + } + } +}