From d2b4cce65215dc4970a4b00a3ba374e563d32057 Mon Sep 17 00:00:00 2001 From: Chris Leonavicius Date: Fri, 3 May 2024 13:57:50 -0700 Subject: [PATCH] feat: Dispatch operations to internal queue --- Amplitude-Swift.xcodeproj/project.pbxproj | 5 + .../project.pbxproj | 28 ++-- .../AmplitudeObjCExampleTests.m | 121 ++++++++++++---- .../TestAmplitude.swift | 135 ++++++++++++++++++ Sources/Amplitude/Amplitude.swift | 60 +++++--- .../NetworkConnectivityCheckerPlugin.swift | 8 +- Sources/Amplitude/Sessions.swift | 4 +- Sources/Amplitude/State.swift | 4 +- .../Amplitude/Utilities/EventPipeline.swift | 6 +- Sources/Amplitude/Utilities/HttpClient.swift | 28 ++-- .../AmplitudeTests/Amplitude+Extensions.swift | 23 +++ Tests/AmplitudeTests/AmplitudeIOSTests.swift | 16 ++- .../AmplitudeSessionTests.swift | 24 ++++ Tests/AmplitudeTests/AmplitudeTests.swift | 60 ++++++-- .../Storages/PersistentStorageTests.swift | 1 - .../Supports/TestUtilities.swift | 2 +- .../Utilities/IdentifyInterceptorTests.swift | 5 +- 17 files changed, 426 insertions(+), 104 deletions(-) create mode 100644 Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift create mode 100644 Tests/AmplitudeTests/Amplitude+Extensions.swift diff --git a/Amplitude-Swift.xcodeproj/project.pbxproj b/Amplitude-Swift.xcodeproj/project.pbxproj index fd6522dd..2a700cb0 100644 --- a/Amplitude-Swift.xcodeproj/project.pbxproj +++ b/Amplitude-Swift.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 3E281B8C2B967F19009D913B /* Diagonostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E281B8B2B967F19009D913B /* Diagonostics.swift */; }; 3E281B8E2B96833D009D913B /* DiagnosticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E281B8D2B96833D009D913B /* DiagnosticsTests.swift */; }; 3E281B912B9BCC14009D913B /* DispatchQueueHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E281B902B9BCC14009D913B /* DispatchQueueHolder.swift */; }; + 4E05BB942BE41AEB009DE475 /* Amplitude+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */; }; 4E2B646B2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */; }; 4E3871622BB34DBC002890AB /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B6DF481F2B5B45BE00B3E6AA /* PrivacyInfo.xcprivacy */; }; 8EDEC02B99EE2092B567A61D /* ObjCIngestionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC500EBDA8B813056E2DB /* ObjCIngestionMetadata.swift */; }; @@ -150,6 +151,7 @@ 3E281B8B2B967F19009D913B /* Diagonostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Diagonostics.swift; sourceTree = ""; }; 3E281B8D2B96833D009D913B /* DiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTests.swift; sourceTree = ""; }; 3E281B902B9BCC14009D913B /* DispatchQueueHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueHolder.swift; sourceTree = ""; }; + 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Amplitude+Extensions.swift"; sourceTree = ""; }; 4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitScreenViewsPluginTests.swift; sourceTree = ""; }; 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeSessionTests.swift; sourceTree = ""; }; 8EDEC1160D95DC3F0E48DDF7 /* ObjCPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCPlugin.swift; sourceTree = ""; }; @@ -466,6 +468,7 @@ 8EDECBC5925DC68913C7CB89 /* Migration */, BA1EC0F32A9F2FC700C2D547 /* DefaultTrackingOptionsTests.swift */, BA1EC0F52A9F63FD00C2D547 /* AmplitudeIOSTests.swift */, + 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */, ); name = Tests; path = Tests/AmplitudeTests; @@ -662,6 +665,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3E281B8F2B98EC92009D913B /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -713,6 +717,7 @@ OBJ_156 /* HttpClientTests.swift in Sources */, D01043612B6C5A8500F8173C /* SandboxHelperTests.swift in Sources */, OBJ_157 /* PersistentStorageResponseHandlerTests.swift in Sources */, + 4E05BB942BE41AEB009DE475 /* Amplitude+Extensions.swift in Sources */, OBJ_158 /* UrlExtensionTests.swift in Sources */, 8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */, BA1EC0F62A9F63FD00C2D547 /* AmplitudeIOSTests.swift in Sources */, diff --git a/Examples/AmplitudeObjCExample/AmplitudeObjCExample.xcodeproj/project.pbxproj b/Examples/AmplitudeObjCExample/AmplitudeObjCExample.xcodeproj/project.pbxproj index 927636ee..dffc2fd1 100644 --- a/Examples/AmplitudeObjCExample/AmplitudeObjCExample.xcodeproj/project.pbxproj +++ b/Examples/AmplitudeObjCExample/AmplitudeObjCExample.xcodeproj/project.pbxproj @@ -3,11 +3,12 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 63; objects = { /* Begin PBXBuildFile section */ - 4E890A202BAB82DF00B3F736 /* Amplitude_Swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E890A142BAB814A00B3F736 /* Amplitude_Swift.framework */; }; + 4E3ECB742BE5B96400FD5CC0 /* TestAmplitude.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3ECB732BE5B96400FD5CC0 /* TestAmplitude.swift */; }; + 4E890A202BAB82DF00B3F736 /* AmplitudeSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E890A142BAB814A00B3F736 /* AmplitudeSwift.framework */; }; BA2E1DA42AC1EA220074E74F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BA2E1DA32AC1EA220074E74F /* AppDelegate.m */; }; BA2E1DA72AC1EA220074E74F /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BA2E1DA62AC1EA220074E74F /* SceneDelegate.m */; }; BA2E1DAA2AC1EA220074E74F /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BA2E1DA92AC1EA220074E74F /* ViewController.m */; }; @@ -50,6 +51,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 4E3ECB732BE5B96400FD5CC0 /* TestAmplitude.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAmplitude.swift; sourceTree = ""; }; 4E890A0C2BAB814A00B3F736 /* Amplitude-Swift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "Amplitude-Swift.xcodeproj"; path = "../../Amplitude-Swift.xcodeproj"; sourceTree = ""; }; BA2E1D9F2AC1EA220074E74F /* AmplitudeObjCExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AmplitudeObjCExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; BA2E1DA22AC1EA220074E74F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -72,7 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4E890A202BAB82DF00B3F736 /* Amplitude_Swift.framework in Frameworks */, + 4E890A202BAB82DF00B3F736 /* AmplitudeSwift.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -89,7 +91,7 @@ 4E890A0D2BAB814A00B3F736 /* Products */ = { isa = PBXGroup; children = ( - 4E890A142BAB814A00B3F736 /* Amplitude_Swift.framework */, + 4E890A142BAB814A00B3F736 /* AmplitudeSwift.framework */, 4E890A162BAB814A00B3F736 /* Amplitude_SwiftTests.xctest */, ); name = Products; @@ -137,6 +139,7 @@ isa = PBXGroup; children = ( BA2E1DBE2AC1EA240074E74F /* AmplitudeObjCExampleTests.m */, + 4E3ECB732BE5B96400FD5CC0 /* TestAmplitude.swift */, ); path = AmplitudeObjCExampleTests; sourceTree = ""; @@ -205,12 +208,12 @@ }; BA2E1DB92AC1EA230074E74F = { CreatedOnToolsVersion = 14.2; - TestTargetID = BA2E1D9E2AC1EA220074E74F; + LastSwiftMigration = 1530; }; }; }; buildConfigurationList = BA2E1D9A2AC1EA220074E74F /* Build configuration list for PBXProject "AmplitudeObjCExample" */; - compatibilityVersion = "Xcode 14.0"; + compatibilityVersion = "Xcode 15.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -237,10 +240,9 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 4E890A142BAB814A00B3F736 /* Amplitude_Swift.framework */ = { + 4E890A142BAB814A00B3F736 /* AmplitudeSwift.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; - name = Amplitude_Swift.framework; path = AmplitudeSwift.framework; remoteRef = 4E890A132BAB814A00B3F736 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; @@ -292,6 +294,7 @@ buildActionMask = 2147483647; files = ( BA2E1DBF2AC1EA240074E74F /* AmplitudeObjCExampleTests.m in Sources */, + 4E3ECB742BE5B96400FD5CC0 /* TestAmplitude.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -495,7 +498,7 @@ BA2E1DD22AC1EA240074E74F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -504,15 +507,16 @@ PRODUCT_BUNDLE_IDENTIFIER = com.amplitude.AmplitudeObjCExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AmplitudeObjCExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AmplitudeObjCExample"; }; name = Debug; }; BA2E1DD32AC1EA240074E74F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -521,8 +525,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.amplitude.AmplitudeObjCExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AmplitudeObjCExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AmplitudeObjCExample"; }; name = Release; }; diff --git a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m index e0f9065b..c6fe2a24 100644 --- a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m +++ b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m @@ -1,3 +1,4 @@ +#import #import @import AmplitudeSwift; @@ -21,8 +22,14 @@ - (void)testTrack { @"prop-bool-array": @[@true, @false, @true], @"prop-object": @{@"nested-prop-1": @555, @"nested-prop-2": @"nested-string"} }; - [amplitude track:@"Event-A" eventProperties:eventProperties]; - + AMPBaseEvent *event = [[AMPBaseEvent alloc] initWithEventType:@"Event-A" + eventProperties:eventProperties]; + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"event tracked"]; + [amplitude track:event callback:^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation]]; + NSArray* eventsStrings = [amplitude.storage getEventsStrings]; NSArray* events = [self parseEvents:eventsStrings]; XCTAssertEqual(events.count, 1); @@ -40,8 +47,16 @@ - (void)testTrack_Options { @"prop-string": @"string-value", @"prop-int": @111 }; - [amplitude track:@"Event-A" eventProperties:eventProperties options:eventOptions]; - + AMPBaseEvent *event = [[AMPBaseEvent alloc] initWithEventType:@"Event-A" + eventProperties:eventProperties]; + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"event tracked"]; + [amplitude track:event + options:eventOptions + callback:^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation]]; + NSArray* eventsStrings = [amplitude.storage getEventsStrings]; NSArray* events = [self parseEvents:eventsStrings]; XCTAssertEqual(events.count, 1); @@ -62,8 +77,15 @@ - (void)testIdentify { [identify add:@"user-sum-prop" valueInt:7]; [identify remove:@"user-agg-prop" value: @"item1"]; [identify unset:@"user-deprecated-prop"]; - [amplitude identify:identify]; - + + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"identify tracked"]; + AMPEventOptions *options = [[AMPEventOptions alloc] init]; + options.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }; + [amplitude identify:identify options:options]; + [self waitForExpectations:@[expectation]]; + NSDictionary* expectedUserProperties = @{ @"$set": @{@"user-string-prop": @"string-value"}, @"$setOnce": @{@"user-int-prop": @111}, @@ -86,8 +108,15 @@ - (void)testIdentify_ClearAll { AMPIdentify* identify = [AMPIdentify new]; [identify clearAll]; - [amplitude identify:identify]; - + + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"identify tracked"]; + AMPEventOptions *options = [[AMPEventOptions alloc] init]; + options.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }; + [amplitude identify:identify options:options]; + [self waitForExpectations:@[expectation]]; + NSDictionary* expectedUserProperties = @{ @"$clearAll": @"-" }; @@ -101,14 +130,28 @@ - (void)testIdentify_ClearAll { - (void)testIdentify_InterceptedIdentifies { Amplitude* amplitude = [self getAmplitude:@"identify-interceptedIdentifies"]; - + AMPIdentify* identify1 = [AMPIdentify new]; [identify1 set:@"user-string-prop" value:@"string-value"]; - [amplitude identify:identify1]; - + + XCTestExpectation *expectation1 = [[XCTestExpectation alloc] initWithDescription:@"identify 1 tracked"]; + AMPEventOptions *options1 = [[AMPEventOptions alloc] init]; + options1.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation1 fulfill]; + }; + [amplitude identify:identify1 options:options1]; + AMPIdentify* identify2 = [AMPIdentify new]; [identify2 set:@"user-int-prop" value:@111]; - [amplitude identify:identify2]; + + XCTestExpectation *expectation2 = [[XCTestExpectation alloc] initWithDescription:@"identify 1 tracked"]; + AMPEventOptions *options2 = [[AMPEventOptions alloc] init]; + options2.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation2 fulfill]; + }; + [amplitude identify:identify2 options:options2]; + + [self waitForExpectations:@[expectation1, expectation2]]; NSDictionary* expectedUserProperties1 = @{ @"$set": @{@"user-string-prop": @"string-value"} @@ -138,8 +181,15 @@ - (void)testGroupIdentify { [identify add:@"user-sum-prop" valueInt:7]; [identify remove:@"user-agg-prop" value: @"item1"]; [identify unset:@"user-deprecated-prop"]; - [amplitude groupIdentify:@"type-1" groupName:@"name-1" identify:identify]; - + + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"group identify tracked"]; + AMPEventOptions *options = [[AMPEventOptions alloc] init]; + options.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }; + [amplitude groupIdentify:@"type-1" groupName:@"name-1" identify:identify options:options]; + [self waitForExpectations:@[expectation]]; + NSDictionary* expectedGroupProperties = @{ @"$set": @{@"user-string-prop": @"string-value"}, @"$setOnce": @{@"user-int-prop": @111}, @@ -160,10 +210,16 @@ - (void)testGroupIdentify { - (void)testSetGroup { Amplitude* amplitude = [self getAmplitude:@"setGroup"]; - - NSString* groupName = @"name-1"; - [amplitude setGroup:@"type-1" groupName:groupName]; - + + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"set group tracked"]; + AMPEventOptions *options = [[AMPEventOptions alloc] init]; + options.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }; + NSString *groupName = @"name-1"; + [amplitude setGroup:@"type-1" groupName:groupName options:options]; + [self waitForExpectations:@[expectation]]; + NSArray* eventsStrings = [amplitude.storage getEventsStrings]; NSArray* events = [self parseEvents:eventsStrings]; XCTAssertEqual(events.count, 1); @@ -174,10 +230,16 @@ - (void)testSetGroup { - (void)testSetGroup_Multiple { Amplitude* amplitude = [self getAmplitude:@"setGroup_Multiple"]; - + + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"set group tracked"]; + AMPEventOptions *options = [[AMPEventOptions alloc] init]; + options.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }; NSArray* groupNames = @[@"name-1", @"name-2"]; - [amplitude setGroup:@"type-1" groupNames:groupNames]; - + [amplitude setGroup:@"type-1" groupNames:groupNames options:options]; + [self waitForExpectations:@[expectation]]; + NSArray* eventsStrings = [amplitude.storage getEventsStrings]; NSArray* events = [self parseEvents:eventsStrings]; XCTAssertEqual(events.count, 1); @@ -195,11 +257,16 @@ - (void)testPlugin { return event; }]]; - [amplitude track:@"Event-A" eventProperties:@{ - @"prop-string": @"string-value", - @"prop-int": @111 - }]; - + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"group identify tracked"]; + AMPEventOptions *options = [[AMPEventOptions alloc] init]; + options.callback = ^(AMPBaseEvent *event, NSInteger code, NSString *message) { + [expectation fulfill]; + }; + [amplitude track:@"Event-A" + eventProperties: @{@"prop-string": @"string-value", @"prop-int": @111} + options:options]; + [self waitForExpectations:@[expectation]]; + NSDictionary* expectedEventProperties = @{ @"prop-string": @"string-value", @"prop-int": @111, @@ -259,7 +326,7 @@ - (Amplitude *)getAmplitude:(NSString *)instancePrefix { NSString* instanceName = [NSString stringWithFormat:@"%@-%f", instancePrefix, [[NSDate date] timeIntervalSince1970]]; AMPConfiguration* configuration = [AMPConfiguration initWithApiKey:@"API-KEY" instanceName:instanceName]; configuration.defaultTracking = AMPDefaultTrackingOptions.NONE; - Amplitude* amplitude = [Amplitude initWithConfiguration:configuration]; + Amplitude *amplitude = [[TestAmplitude alloc] initWithConfiguration:configuration]; return amplitude; } diff --git a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift new file mode 100644 index 00000000..4e540ea2 --- /dev/null +++ b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift @@ -0,0 +1,135 @@ +// +// TestAmplitude.swift +// AmplitudeObjCExampleTests +// +// Created by Chris Leonavicius on 5/3/24. +// + +@testable import AmplitudeSwift +import XCTest + +@objc +public class TestAmplitude: ObjCAmplitude { + + @objc + public override init(configuration: ObjCConfiguration) { + let config = configuration.configuration + let updatedConfig = Configuration(apiKey: config.apiKey, + flushQueueSize: config.flushQueueSize, + flushIntervalMillis: config.flushIntervalMillis, + instanceName: config.instanceName, + optOut: config.optOut, + storageProvider: TestStorage(), + identifyStorageProvider: TestStorage(), + logLevel: config.logLevel, + loggerProvider: config.loggerProvider, + minIdLength: config.minIdLength, + partnerId: config.partnerId, + callback: config.callback, + flushMaxRetries: config.flushMaxRetries, + useBatch: config.useBatch, + serverZone: config.serverZone, + serverUrl: "https://127.0.0.1", + plan: config.plan, + ingestionMetadata: config.ingestionMetadata, + trackingOptions: config.trackingOptions, + enableCoppaControl: config.enableCoppaControl, + flushEventsOnClose: config.flushEventsOnClose, + minTimeBetweenSessionsMillis: config.minTimeBetweenSessionsMillis, + defaultTracking: config.defaultTracking, + identifyBatchIntervalMillis: config.identifyBatchIntervalMillis, + migrateLegacyData: config.migrateLegacyData, + offline: NetworkConnectivityCheckerPlugin.Disabled) + + super.init(configuration: ObjCConfiguration(configuration: updatedConfig)) + } +} + +class TestStorage: Storage { + + private let eventsURL = URL(string: NSTemporaryDirectory()) + private var storage: [String: Any] = [:] + private var events: [BaseEvent] = [] + + func write(key: StorageKey, value: Any?) throws { + switch key { + case .EVENTS: + if let event = value as? BaseEvent { + events.append(event) + event.callback?(event, events.count, "") + } + default: + storage[key.rawValue] = value + } + } + + func read(key: StorageKey) -> T? { + switch key { + case .EVENTS: + return [eventsURL] as? T + default: + return storage[key.rawValue] as? T + } + } + + func getEventsString(eventBlock: URL) -> String? { + let eventsData = try? JSONEncoder().encode(events) + return eventsData.flatMap { String(data: $0, encoding: .utf8) } + } + + func remove(eventBlock: URL) { + // no-op + } + + func splitBlock(eventBlock: URL, events: [BaseEvent]) { + // no-op + } + + func rollover() { + // no-op + } + + func reset() { + storage.removeAll() + events.removeAll() + } + + func getResponseHandler( + configuration: Configuration, + eventPipeline: EventPipeline, + eventBlock: URL, + eventsString: String + ) -> ResponseHandler { + class TestResponseHandler: ResponseHandler { + + func handle(result: Result) { + // no-op + } + + func handleSuccessResponse(code: Int) { + // no-op + } + + func handleBadRequestResponse(data: [String: Any]) { + // no-op + } + + func handlePayloadTooLargeResponse(data: [String: Any]) { + // no-op + } + + func handleTooManyRequestsResponse(data: [String: Any]) { + // no-op + } + + func handleTimeoutResponse(data: [String: Any]) { + // no-op + } + + func handleFailedResponse(data: [String: Any]) { + // no-op + } + } + return TestResponseHandler() + } +} diff --git a/Sources/Amplitude/Amplitude.swift b/Sources/Amplitude/Amplitude.swift index fd0c0309..0df04c15 100644 --- a/Sources/Amplitude/Amplitude.swift +++ b/Sources/Amplitude/Amplitude.swift @@ -31,6 +31,8 @@ public class Amplitude { return self.configuration.loggerProvider }() + let trackingQueue = DispatchQueue(label: "com.amplitude.analytics", target: .global(qos: .utility)) + public init( configuration: Configuration ) { @@ -84,9 +86,7 @@ public class Amplitude { } @discardableResult - public func track(eventType: String, eventProperties: [String: Any]? = nil, options: EventOptions? = nil) - -> Amplitude - { + public func track(eventType: String, eventProperties: [String: Any]? = nil, options: EventOptions? = nil) -> Amplitude { let event = BaseEvent(eventType: eventType) event.eventProperties = eventProperties if let eventOptions = options { @@ -114,10 +114,10 @@ public class Amplitude { if let eventOptions = options { event.mergeEventOptions(eventOptions: eventOptions) if eventOptions.userId != nil { - _ = setUserId(userId: eventOptions.userId) + setUserId(userId: eventOptions.userId) } if eventOptions.deviceId != nil { - _ = setDeviceId(deviceId: eventOptions.deviceId) + setDeviceId(deviceId: eventOptions.deviceId) } } process(event: event) @@ -247,9 +247,11 @@ public class Amplitude { @discardableResult public func flush() -> Amplitude { - timeline.apply { plugin in - if let _plugin = plugin as? EventPlugin { - _plugin.flush() + trackingQueue.async { + self.timeline.apply { plugin in + if let _plugin = plugin as? EventPlugin { + _plugin.flush() + } } } return self @@ -283,10 +285,17 @@ public class Amplitude { @discardableResult public func setSessionId(timestamp: Int64) -> Amplitude { - let sessionEvents = sessions.assignEventId( - events: timestamp >= 0 ? sessions.startNewSession(timestamp: timestamp) : sessions.endCurrentSession() - ) - sessionEvents.forEach { e in timeline.processEvent(event: e) } + trackingQueue.async { [self] in + let sessionEvents: [BaseEvent] + if timestamp >= 0 { + sessionEvents = self.sessions.startNewSession(timestamp: timestamp) + } else { + sessionEvents = self.sessions.endCurrentSession() + } + self.sessions.assignEventId(events: sessionEvents).forEach { e in + self.timeline.processEvent(event: e) + } + } return self } @@ -299,8 +308,8 @@ public class Amplitude { @discardableResult public func reset() -> Amplitude { - _ = setUserId(userId: nil) - _ = setDeviceId(deviceId: nil) + setUserId(userId: nil) + setDeviceId(deviceId: nil) contextPlugin.initializeDeviceId() return self } @@ -314,26 +323,33 @@ public class Amplitude { logger?.log(message: "Skip event based on opt out configuration") return } - let events = sessions.processEvent(event: event, inForeground: inForeground) - events.forEach { e in timeline.processEvent(event: e) } + let inForeground = inForeground + trackingQueue.async { [self] in + let events = self.sessions.processEvent(event: event, inForeground: inForeground) + events.forEach { e in self.timeline.processEvent(event: e) } + } } func onEnterForeground(timestamp: Int64) { + inForeground = true let dummySessionStartEvent = BaseEvent( timestamp: timestamp, eventType: Constants.AMP_SESSION_START_EVENT ) - let events = sessions.processEvent(event: dummySessionStartEvent, inForeground: false) - // Set inForeground to true only after we have successfully started a new session if needed. - inForeground = true - events.forEach { e in timeline.processEvent(event: e) } + trackingQueue.async { [self] in + // set inForeground to false to represent state before event was fired + let events = self.sessions.processEvent(event: dummySessionStartEvent, inForeground: false) + events.forEach { e in self.timeline.processEvent(event: e) } + } } func onExitForeground(timestamp: Int64) { inForeground = false - sessions.lastEventTime = timestamp + trackingQueue.async { [self] in + self.sessions.lastEventTime = timestamp + } if configuration.flushEventsOnClose == true { - _ = self.flush() + flush() } } diff --git a/Sources/Amplitude/Plugins/NetworkConnectivityCheckerPlugin.swift b/Sources/Amplitude/Plugins/NetworkConnectivityCheckerPlugin.swift index d70fd19d..23dc7830 100644 --- a/Sources/Amplitude/Plugins/NetworkConnectivityCheckerPlugin.swift +++ b/Sources/Amplitude/Plugins/NetworkConnectivityCheckerPlugin.swift @@ -21,7 +21,7 @@ public struct NetworkPath { // Protocol for creating network paths protocol PathCreationProtocol { var networkPathPublisher: AnyPublisher? { get } - func start() + func start(queue: DispatchQueue) } // Implementation of PathCreationProtocol using NWPathMonitor @@ -30,12 +30,12 @@ final class PathCreation: PathCreationProtocol { private let subject = PassthroughSubject() private let monitor = NWPathMonitor() - func start() { + func start(queue: DispatchQueue) { monitor.pathUpdateHandler = subject.send networkPathPublisher = subject .map { NetworkPath(status: $0.status) } .eraseToAnyPublisher() - monitor.start(queue: .main) + monitor.start(queue: queue) } } @@ -53,7 +53,7 @@ open class NetworkConnectivityCheckerPlugin: BeforePlugin { super.setup(amplitude: amplitude) amplitude.logger?.debug(message: "Installing NetworkConnectivityCheckerPlugin, offline feature should be supported.") - pathCreation.start() + pathCreation.start(queue: amplitude.trackingQueue) pathUpdateCancellable = pathCreation.networkPathPublisher? .sink(receiveValue: { [weak self] networkPath in let isOffline = !(networkPath.status == .satisfied) diff --git a/Sources/Amplitude/Sessions.swift b/Sources/Amplitude/Sessions.swift index ad226d23..77fe9c84 100644 --- a/Sources/Amplitude/Sessions.swift +++ b/Sources/Amplitude/Sessions.swift @@ -4,7 +4,7 @@ public class Sessions { private let amplitude: Amplitude private var _sessionId: Int64 = -1 - var sessionId: Int64 { + private(set) var sessionId: Int64 { get { _sessionId } set { _sessionId = newValue @@ -17,7 +17,7 @@ public class Sessions { } private var _lastEventId: Int64 = 0 - var lastEventId: Int64 { + private(set) var lastEventId: Int64 { get { _lastEventId } set { _lastEventId = newValue diff --git a/Sources/Amplitude/State.swift b/Sources/Amplitude/State.swift index 1e3c0901..4b4a542c 100644 --- a/Sources/Amplitude/State.swift +++ b/Sources/Amplitude/State.swift @@ -8,7 +8,7 @@ import Foundation class State { - var userId: String? { + @Atomic var userId: String? { didSet { for plugin in plugins { plugin.onUserIdChanged(userId) @@ -16,7 +16,7 @@ class State { } } - var deviceId: String? { + @Atomic var deviceId: String? { didSet { for plugin in plugins { plugin.onDeviceIdChanged(deviceId) diff --git a/Sources/Amplitude/Utilities/EventPipeline.swift b/Sources/Amplitude/Utilities/EventPipeline.swift index ae20d88f..092bd76b 100644 --- a/Sources/Amplitude/Utilities/EventPipeline.swift +++ b/Sources/Amplitude/Utilities/EventPipeline.swift @@ -23,8 +23,10 @@ public class EventPipeline { init(amplitude: Amplitude) { self.amplitude = amplitude - self.httpClient = HttpClient(configuration: amplitude.configuration, diagnostics: amplitude.configuration.diagonostics) - self.flushTimer = QueueTimer(interval: getFlushInterval()) { [weak self] in + self.httpClient = HttpClient(configuration: amplitude.configuration, + diagnostics: amplitude.configuration.diagonostics, + callbackQueue: amplitude.trackingQueue) + self.flushTimer = QueueTimer(interval: getFlushInterval(), queue: amplitude.trackingQueue) { [weak self] in self?.flush() } } diff --git a/Sources/Amplitude/Utilities/HttpClient.swift b/Sources/Amplitude/Utilities/HttpClient.swift index f8299e1a..1e8e7cc4 100644 --- a/Sources/Amplitude/Utilities/HttpClient.swift +++ b/Sources/Amplitude/Utilities/HttpClient.swift @@ -9,8 +9,9 @@ import Foundation class HttpClient { let configuration: Configuration - internal let session: URLSession + let session: URLSession let diagnostics: Diagnostics + let callbackQueue: DispatchQueue private lazy var dateFormatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() @@ -18,9 +19,10 @@ class HttpClient { return formatter }() - init(configuration: Configuration, diagnostics: Diagnostics) { + init(configuration: Configuration, diagnostics: Diagnostics, callbackQueue: DispatchQueue? = nil) { self.configuration = configuration self.diagnostics = diagnostics + self.callbackQueue = callbackQueue ?? .global() let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.httpMaximumConnectionsPerHost = 2 @@ -35,18 +37,20 @@ class HttpClient { let request = try getRequest() let requestData = getRequestData(events: events) - sessionTask = session.uploadTask(with: request, from: requestData) { data, response, error in - if error != nil { - completion(.failure(error!)) - } else if let httpResponse = response as? HTTPURLResponse { - switch httpResponse.statusCode { - case 1..<300: - completion(.success(httpResponse.statusCode)) - default: - completion(.failure(Exception.httpError(code: httpResponse.statusCode, data: data))) + sessionTask = session.uploadTask(with: request, from: requestData) { [callbackQueue] data, response, error in + callbackQueue.async { + if error != nil { + completion(.failure(error!)) + } else if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 1..<300: + completion(.success(httpResponse.statusCode)) + default: + completion(.failure(Exception.httpError(code: httpResponse.statusCode, data: data))) + } } + backgroundTaskCompletion?() } - backgroundTaskCompletion?() } sessionTask!.resume() } catch { diff --git a/Tests/AmplitudeTests/Amplitude+Extensions.swift b/Tests/AmplitudeTests/Amplitude+Extensions.swift new file mode 100644 index 00000000..1349ff58 --- /dev/null +++ b/Tests/AmplitudeTests/Amplitude+Extensions.swift @@ -0,0 +1,23 @@ +// +// Amplitude+Extensions.swift +// Amplitude-SwiftTests +// +// Created by Chris Leonavicius on 5/2/24. +// + +@testable import AmplitudeSwift +import XCTest + +extension Amplitude { + + func waitForTrackingQueue() { + let waitForQueueExpectation = XCTestExpectation(description: "Wait for trackingQueue") + // Because trackingQueue is serial, this acts as a barrier in which any previous operations will + // have guaranteed to complete after this has run. + trackingQueue.async { + waitForQueueExpectation.fulfill() + } + + XCTWaiter().wait(for: [waitForQueueExpectation]) + } +} diff --git a/Tests/AmplitudeTests/AmplitudeIOSTests.swift b/Tests/AmplitudeTests/AmplitudeIOSTests.swift index 0a0d5f57..da24869f 100644 --- a/Tests/AmplitudeTests/AmplitudeIOSTests.swift +++ b/Tests/AmplitudeTests/AmplitudeIOSTests.swift @@ -26,9 +26,11 @@ final class AmplitudeIOSTests: XCTestCase { identifyStorageProvider: interceptStorageMem, defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) ) - _ = Amplitude(configuration: configuration) + let amplitude = Amplitude(configuration: configuration) NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil) + amplitude.waitForTrackingQueue() + let info = Bundle.main.infoDictionary let currentBuild = info?["CFBundleVersion"] ?? "" let currentVersion = info?["CFBundleShortVersionString"] ?? "" @@ -58,8 +60,9 @@ final class AmplitudeIOSTests: XCTestCase { try storageMem.write(key: StorageKey.LAST_EVENT_TIME, value: 123 as Int64) try storageMem.write(key: StorageKey.APP_BUILD, value: "abc") try storageMem.write(key: StorageKey.APP_VERSION, value: "xyz") - _ = Amplitude(configuration: configuration) + let amplitude = Amplitude(configuration: configuration) NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil) + amplitude.waitForTrackingQueue() let info = Bundle.main.infoDictionary let currentBuild = info?["CFBundleVersion"] ?? "" @@ -97,8 +100,9 @@ final class AmplitudeIOSTests: XCTestCase { try storageMem.write(key: StorageKey.LAST_EVENT_TIME, value: 123 as Int64) try storageMem.write(key: StorageKey.APP_BUILD, value: currentBuild) try storageMem.write(key: StorageKey.APP_VERSION, value: currentVersion) - _ = Amplitude(configuration: configuration) + let amplitude = Amplitude(configuration: configuration) NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) @@ -122,8 +126,9 @@ final class AmplitudeIOSTests: XCTestCase { let currentBuild = info?["CFBundleVersion"] ?? "" let currentVersion = info?["CFBundleShortVersionString"] ?? "" - _ = Amplitude(configuration: configuration) + let amplitude = Amplitude(configuration: configuration) NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) @@ -143,8 +148,9 @@ final class AmplitudeIOSTests: XCTestCase { defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) ) - _ = Amplitude(configuration: configuration) + let amplitude = Amplitude(configuration: configuration) NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) diff --git a/Tests/AmplitudeTests/AmplitudeSessionTests.swift b/Tests/AmplitudeTests/AmplitudeSessionTests.swift index 5235ea19..0084cbc8 100644 --- a/Tests/AmplitudeTests/AmplitudeSessionTests.swift +++ b/Tests/AmplitudeTests/AmplitudeSessionTests.swift @@ -33,6 +33,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1000, eventType: "test event 1")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1050, eventType: "test event 2")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -69,6 +70,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1000, eventType: "test event 1")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -123,6 +125,7 @@ final class AmplitudeSessionTests: XCTestCase { let eventType = "out of session event" amplitude.track(eventType: eventType, options: eventOptions) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1050, eventType: "test event")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events XCTAssertEqual(collectedEvents.count, 2) var event = collectedEvents[0] @@ -147,6 +150,8 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1050, eventType: "test event 1")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() + let collectedEvents = eventCollector.events XCTAssertEqual(collectedEvents.count, 3) @@ -183,6 +188,8 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.onEnterForeground(timestamp: 1050) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() + let collectedEvents = eventCollector.events XCTAssertEqual(collectedEvents.count, 3) @@ -219,6 +226,8 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.onEnterForeground(timestamp: 2000) amplitude.track(event: BaseEvent(userId: "user", timestamp: 3000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() + let collectedEvents = eventCollector.events XCTAssertEqual(collectedEvents.count, 5) @@ -267,6 +276,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1500, eventType: "test event 1")) amplitude.onExitForeground(timestamp: 2000) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2050, eventType: "test event 2")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -304,6 +314,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1500, eventType: "test event 1")) amplitude.onExitForeground(timestamp: 2000) amplitude.track(event: BaseEvent(userId: "user", timestamp: 3000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -343,6 +354,7 @@ final class AmplitudeSessionTests: XCTestCase { func testSessionDataShouldBePersisted() throws { let amplitude1 = Amplitude(configuration: configuration) amplitude1.onEnterForeground(timestamp: 1000) + amplitude1.waitForTrackingQueue() XCTAssertEqual(amplitude1.sessionId, 1000) XCTAssertEqual(amplitude1.sessions.sessionId, 1000) @@ -350,6 +362,7 @@ final class AmplitudeSessionTests: XCTestCase { XCTAssertEqual(amplitude1.sessions.lastEventId, 1) amplitude1.track(event: BaseEvent(userId: "user", timestamp: 1200, eventType: "test event 1")) + amplitude1.waitForTrackingQueue() XCTAssertEqual(amplitude1.sessionId, 1000) XCTAssertEqual(amplitude1.sessions.sessionId, 1000) @@ -357,6 +370,7 @@ final class AmplitudeSessionTests: XCTestCase { XCTAssertEqual(amplitude1.sessions.lastEventId, 2) let amplitude2 = Amplitude(configuration: configuration) + amplitude2.waitForTrackingQueue() XCTAssertEqual(amplitude2.sessionId, 1000) XCTAssertEqual(amplitude2.sessions.sessionId, 1000) @@ -376,6 +390,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1000, eventType: "test event 1")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1050, sessionId: 3000, eventType: "test event 2")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1100, eventType: "test event 3")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -418,6 +433,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1000, eventType: "test event 1")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1050, sessionId: -1, eventType: "test event 2")) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1100, eventType: "test event 3")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -460,6 +476,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 100, eventType: "test event 1")) amplitude.setSessionId(timestamp: 150) amplitude.track(event: BaseEvent(userId: "user", timestamp: 200, eventType: "test event 2")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -509,6 +526,7 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.track(event: BaseEvent(userId: "user", timestamp: 1050, eventType: "test event 1")) amplitude.setSessionId(timestamp: 1100) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() let collectedEvents = eventCollector.events @@ -555,12 +573,15 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.add(plugin: eventCollector) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1000, eventType: "test event 1")) + amplitude.waitForTrackingQueue() XCTAssertEqual(amplitude.sessionId, 1000) amplitude.setSessionId(timestamp: -1) + amplitude.waitForTrackingQueue() XCTAssertEqual(amplitude.sessionId, -1) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() XCTAssertEqual(amplitude.sessionId, 2000) let collectedEvents = eventCollector.events @@ -609,12 +630,15 @@ final class AmplitudeSessionTests: XCTestCase { amplitude.onEnterForeground(timestamp: 1000) amplitude.track(event: BaseEvent(userId: "user", timestamp: 1500, eventType: "test event 1")) + amplitude.waitForTrackingQueue() XCTAssertEqual(amplitude.sessionId, 1000) amplitude.setSessionId(timestamp: -1) + amplitude.waitForTrackingQueue() XCTAssertEqual(amplitude.sessionId, -1) amplitude.track(event: BaseEvent(userId: "user", timestamp: 2000, eventType: "test event 2")) + amplitude.waitForTrackingQueue() XCTAssertEqual(amplitude.sessionId, 2000) let collectedEvents = eventCollector.events diff --git a/Tests/AmplitudeTests/AmplitudeTests.swift b/Tests/AmplitudeTests/AmplitudeTests.swift index 94f71ab3..b42f14b3 100644 --- a/Tests/AmplitudeTests/AmplitudeTests.swift +++ b/Tests/AmplitudeTests/AmplitudeTests.swift @@ -72,6 +72,7 @@ final class AmplitudeTests: XCTestCase { let outputReader = OutputReaderPlugin() amplitude.add(plugin: outputReader) amplitude.track(event: BaseEvent(eventType: "testEvent")) + amplitude.waitForTrackingQueue() let lastEvent = outputReader.lastEvent XCTAssertEqual(lastEvent?.library, "\(Constants.SDK_LIBRARY)/\(Constants.SDK_VERSION)") @@ -109,6 +110,7 @@ final class AmplitudeTests: XCTestCase { amplitude.add(plugin: testPlugin) amplitude.track(event: BaseEvent(eventType: enrichedEventType)) amplitude.track(event: BaseEvent(eventType: "Other Event")) + amplitude.waitForTrackingQueue() let events = storage.events() XCTAssertEqual(events[0].eventType, enrichedEventType) @@ -179,6 +181,7 @@ final class AmplitudeTests: XCTestCase { amplitude.setUserId(userId: "test-user") amplitude.identify(identify: Identify().set(property: "key-1", value: "value-1")) amplitude.identify(identify: Identify().set(property: "key-2", value: "value-2")) + amplitude.waitForTrackingQueue() var intercepts = interceptStorageMem.events() var events = storageMem.events() @@ -186,6 +189,7 @@ final class AmplitudeTests: XCTestCase { XCTAssertEqual(events.count, 0) amplitude.flush() + amplitude.waitForTrackingQueue() intercepts = interceptStorageMem.events() events = storageMem.events() @@ -209,6 +213,7 @@ final class AmplitudeTests: XCTestCase { // send 2 $set only Identify's, should be intercepted amplitude.identify(identify: Identify().set(property: "key-1", value: "value-1")) amplitude.identify(identify: Identify().set(property: "key-2", value: "value-2")) + amplitude.waitForTrackingQueue() var intercepts = interceptStorageTest.events() var events = storageTest.events() @@ -217,6 +222,7 @@ final class AmplitudeTests: XCTestCase { // setGroup event should not be intercepted amplitude.setGroup(groupType: "group-type", groupName: "group-name") + amplitude.waitForTrackingQueue() intercepts = interceptStorageTest.events() XCTAssertEqual(intercepts.count, 0) @@ -306,6 +312,7 @@ final class AmplitudeTests: XCTestCase { userActivity.referrerURL = URL(string: "https://test-referrer.com") amplitude.track(event: DeepLinkOpenedEvent(activity: userActivity)) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) @@ -329,6 +336,7 @@ final class AmplitudeTests: XCTestCase { let amplitude = Amplitude(configuration: configuration) amplitude.track(event: DeepLinkOpenedEvent(url: URL(string: "https://test-app.com")!)) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) @@ -351,6 +359,7 @@ final class AmplitudeTests: XCTestCase { let amplitude = Amplitude(configuration: configuration) amplitude.track(event: DeepLinkOpenedEvent(url: NSURL(string: "https://test-app.com")!)) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) @@ -373,6 +382,7 @@ final class AmplitudeTests: XCTestCase { let amplitude = Amplitude(configuration: configuration) amplitude.track(event: ScreenViewedEvent(screenName: "main view")) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) @@ -395,6 +405,7 @@ final class AmplitudeTests: XCTestCase { let eventOptions = EventOptions(sessionId: -1) let eventType = "out of session event" amplitude.track(eventType: eventType, options: eventOptions) + amplitude.waitForTrackingQueue() let events = storageMem.events() XCTAssertEqual(events.count, 1) XCTAssertEqual(events[0].eventType, eventType) @@ -415,23 +426,23 @@ final class AmplitudeTests: XCTestCase { let oneHourEarlierTimestamp = timestamp - (1 * 60 * 60 * 1000) amplitude.setSessionId(timestamp: oneHourEarlierTimestamp) - @Sendable - func processStartSessionEvent() async { + // We process the session start event first. The session class will wait for 3 seconds before it processes + // the event + let processSessionStartEvent = Task.detached { amplitude.onEnterForeground(timestamp: timestamp) } - func processRegularEvent() async { + // Sleep for 1 second and process a regular event. This is to try the case where an event gets processed + // before the session start event + let processRegularEvent = Task.detached { + try await Task.sleep(nanoseconds: NSEC_PER_SEC) amplitude.track(eventType: "test_event") } - // We process the session start event first. The session class will wait for 3 seconds before it processes - // the event - async let task = processStartSessionEvent() - // Sleep for 1 second and process a regular event. This is to try the case where an event gets processed - // before the session start event - sleep(1) - await processRegularEvent() - await task + _ = await processRegularEvent.result + _ = await processSessionStartEvent.result + + amplitude.waitForTrackingQueue() // We want to make sure that a new session was started XCTAssertTrue(amplitude.getSessionId() > oneHourEarlierTimestamp) @@ -474,6 +485,7 @@ final class AmplitudeTests: XCTestCase { // track events to legacy storage legacyStorageAmplitude.identify(identify: Identify().set(property: "user-prop", value: true)) legacyStorageAmplitude.track(event: BaseEvent(eventType: "Legacy Storage Event")) + legacyStorageAmplitude.waitForTrackingQueue() guard let legacyEventFiles: [URL]? = legacyEventStorage.read(key: StorageKey.EVENTS) else { return } @@ -557,6 +569,7 @@ final class AmplitudeTests: XCTestCase { // track events to legacy storage legacyStorageAmplitude.identify(identify: Identify().set(property: "user-prop", value: true)) legacyStorageAmplitude.track(event: BaseEvent(eventType: "Legacy Storage Event")) + legacyStorageAmplitude.waitForTrackingQueue() guard let legacyEventFiles: [URL]? = legacyEventStorage.read(key: StorageKey.EVENTS) else { return } @@ -679,6 +692,31 @@ final class AmplitudeTests: XCTestCase { XCTAssertEqual(Amplitude(configuration: configuration).configuration.offline, false) } + func testConcurrentAccess() { + let amplitude = Amplitude(configuration: Configuration(apiKey: "test-api-key", + storageProvider: InMemoryStorage(), + defaultTracking: .init(sessions: true, + appLifecycles: true))) + let eventCollector = EventCollectorPlugin() + amplitude.add(plugin: eventCollector) + let sessionID = Int64(Date().timeIntervalSince1970 * 1000) + amplitude.setSessionId(timestamp: sessionID) + + DispatchQueue.concurrentPerform(iterations: 100) { i in + amplitude.onEnterForeground(timestamp: Int64(Date().timeIntervalSince1970 * 1000)) + amplitude.track(eventType: "Test Event \(i)") + amplitude.onExitForeground(timestamp: Int64(Date().timeIntervalSince1970 * 1000)) + } + + amplitude.waitForTrackingQueue() + + XCTAssertEqual(amplitude.getSessionId(), sessionID) + + var allEventIds = Set((0..<100).map { "Test Event \($0)" }) + allEventIds.insert("session_start") + XCTAssertEqual(allEventIds, Set(eventCollector.events.map(\.eventType))) + } + func getDictionary(_ props: [String: Any?]) -> NSDictionary { return NSDictionary(dictionary: props as [AnyHashable: Any]) } diff --git a/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift b/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift index 107a8f00..fea4b9f4 100644 --- a/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift +++ b/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift @@ -173,7 +173,6 @@ final class PersistentStorageTests: XCTestCase { let partial = "{\"event_type\":\"test1\",\"user_id\":\"159995596214061\",\"device_id\":\"9b935bb3cd75\"," let malformedContent = "\(event1.toString())\(PersistentStorage.DELMITER)\(partial)\(PersistentStorage.DELMITER)" writeContent(file: currentFile, content: malformedContent) - let rawFiles = try? FileManager.default.contentsOfDirectory(at: storeDirectory, includingPropertiesForKeys: nil) let eventFiles: [URL]? = persistentStorage.read(key: StorageKey.EVENTS) XCTAssertEqual(eventFiles?.count, 1) diff --git a/Tests/AmplitudeTests/Supports/TestUtilities.swift b/Tests/AmplitudeTests/Supports/TestUtilities.swift index 522f2fd7..08b4c918 100644 --- a/Tests/AmplitudeTests/Supports/TestUtilities.swift +++ b/Tests/AmplitudeTests/Supports/TestUtilities.swift @@ -306,7 +306,7 @@ final class MockPathCreation: PathCreationProtocol { var networkPathPublisher: AnyPublisher? private let subject = PassthroughSubject() - func start() { + func start(queue: DispatchQueue) { networkPathPublisher = subject.eraseToAnyPublisher() } diff --git a/Tests/AmplitudeTests/Utilities/IdentifyInterceptorTests.swift b/Tests/AmplitudeTests/Utilities/IdentifyInterceptorTests.swift index 3fe8f2c2..1288fc32 100644 --- a/Tests/AmplitudeTests/Utilities/IdentifyInterceptorTests.swift +++ b/Tests/AmplitudeTests/Utilities/IdentifyInterceptorTests.swift @@ -135,9 +135,8 @@ final class IdentifyInterceptorTests: XCTestCase { destination: ["key-1": nil, "key-2": "value-2", "key-3": nil, "key-4": nil], source: ["key-1": "value-1", "key-2": nil, "key-3": nil, "key-5": nil] ) - XCTAssertTrue(getDictionary(merged).isEqual( - to: ["key-1": "value-1", "key-2": "value-2", "key-3": nil, "key-4": nil, "key-5": nil]) - ) + XCTAssertEqual(getDictionary(merged), + ["key-1": "value-1", "key-2": "value-2", "key-3": nil, "key-4": nil, "key-5": nil] as NSDictionary) merged = interceptor.mergeUserProperties( destination: ["key-1": NSNull(), "key-2": "value-2", "key-3": NSNull(), "key-4": NSNull()],