diff --git a/FBAEMKit/FBAEMKitTests/FBAEM+Testing.h b/FBAEMKit/FBAEMKitTests/FBAEM+Testing.h index 0c04916385..b150076f90 100644 --- a/FBAEMKit/FBAEMKitTests/FBAEM+Testing.h +++ b/FBAEMKit/FBAEMKitTests/FBAEM+Testing.h @@ -66,7 +66,8 @@ NS_ASSUME_NONNULL_BEGIN ACSSharedSecret:(nullable NSString *)ACSSharedSecret ACSConfigID:(nullable NSString *)ACSConfigID businessID:(nullable NSString *)businessID - isTestMode:(BOOL)isTestMode; + isTestMode:(BOOL)isTestMode + hasSKAN:(BOOL)hasSKAN; - (nullable instancetype)initWithCampaignID:(NSString *)campaignID ACSToken:(NSString *)ACSToken @@ -82,7 +83,8 @@ NS_ASSUME_NONNULL_BEGIN priority:(NSInteger)priority conversionTimestamp:(nullable NSDate *)conversionTimestamp isAggregated:(BOOL)isAggregated - isTestMode:(BOOL)isTestMode; + isTestMode:(BOOL)isTestMode + hasSKAN:(BOOL)hasSKAN; - (nullable FBAEMConfiguration *)_findConfig:(nullable NSDictionary *> *)configs; @@ -134,6 +136,9 @@ NS_ASSUME_NONNULL_BEGIN parameters:(nullable NSDictionary *)parameters configs:(NSDictionary *> *)configs; ++ (BOOL)_isDoubleCounting:(FBAEMInvocation *)invocation + event:(NSString *)event; + + (void)_sendDebuggingRequest:(FBAEMInvocation *)invocation; + (NSDictionary *)_debuggingRequestParameters:(FBAEMInvocation *)invocation; diff --git a/FBAEMKit/FBAEMKitTests/FBAEMInvocationTests.swift b/FBAEMKit/FBAEMKitTests/FBAEMInvocationTests.swift index d10fcefb27..a5f9802eb5 100644 --- a/FBAEMKit/FBAEMKitTests/FBAEMInvocationTests.swift +++ b/FBAEMKit/FBAEMKitTests/FBAEMInvocationTests.swift @@ -37,6 +37,7 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len static let priority = "priority" static let conversionTimestamp = "conversion_timestamp" static let isAggregated = "is_aggregated" + static let hasSKAN = "has_skan" static let defaultCurrency = "default_currency" static let cutoffTime = "cutoff_time" static let validFrom = "valid_from" @@ -76,7 +77,8 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len priority: -1, conversionTimestamp: Date(timeIntervalSince1970: 1618383700), isAggregated: false, - isTestMode: false + isTestMode: false, + hasSKAN: false ) var config1: AEMConfiguration! // swiftlint:disable:this implicitly_unwrapped_optional = AEMConfiguration(json: [ @@ -215,6 +217,31 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len ) } + func testInvocationWithSKANInfoAppLinkData() throws { + let data = [ + "acs_token": "debuggingtoken", + "campaign_ids": "test_campaign_1234", + "advertiser_id": "test_advertiserid_coffee", + "has_skan": true + ] as [String: Any] + let invocation = try XCTUnwrap(AEMInvocation(appLinkData: data)) + + XCTAssertTrue( + invocation.hasSKAN, + "Invocation's hasSKAN is expected to be true when has_skan is true" + ) + XCTAssertEqual( + invocation.acsToken, + "debuggingtoken", + "Invocations's acsToken is not expected" + ) + XCTAssertEqual( + invocation.campaignID, + "test_campaign_1234", + "Invocations's campaignID is not expected" + ) + } + func testFindConfig() { var invocation: AEMInvocation? = self.validInvocation invocation?.reset() @@ -230,7 +257,8 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len acsSharedSecret: nil, acsConfigID: nil, businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false ) let config = invocation?._findConfig([Values.defaultMode: [config1, config2]]) XCTAssertEqual(invocation?.configID, 20000, "Should set the invocation with expected configID") @@ -294,7 +322,8 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len acsSharedSecret: nil, acsConfigID: nil, businessID: "test_advertiserid_123", - isTestMode: false + isTestMode: false, + hasSKAN: false ) let config = invocation?._findConfig([ Values.defaultMode: [configWithoutBusinessID], @@ -604,6 +633,12 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len invocation.isAggregated, "Should encode the expected isAggregated with the correct key" ) + let hasSKAN = coder.encodedObject[Keys.hasSKAN] as? NSNumber + XCTAssertEqual( + hasSKAN?.boolValue, + invocation.hasSKAN, + "Should encode the expected hasSKAN with the correct key" + ) } func testDecoding() { // swiftlint:disable:this function_body_length @@ -670,5 +705,10 @@ class FBAEMInvocationTests: XCTestCase { // swiftlint:disable:this type_body_len "decodeBoolForKey", "Should decode the expected type for the is_aggregated key" ) + XCTAssertEqual( + decoder.decodedObject[Keys.hasSKAN] as? String, + "decodeBoolForKey", + "Should decode the expected type for the has_skan key" + ) } } // swiftlint:disable:this file_length diff --git a/FBAEMKit/FBAEMKitTests/FBAEMReporterTests.swift b/FBAEMKit/FBAEMKitTests/FBAEMReporterTests.swift index d57c1994ce..8235ffdcca 100644 --- a/FBAEMKit/FBAEMKitTests/FBAEMReporterTests.swift +++ b/FBAEMKit/FBAEMKitTests/FBAEMReporterTests.swift @@ -193,14 +193,16 @@ class FBAEMReporterTests: XCTestCase { acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false ), let invocation2 = AEMInvocation( campaignID: "test_campaign_1234", acsToken: "test_token_1234567", acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false ) else { return XCTFail("Unwrapping Error") } invocation1.setConfigID(10000) @@ -413,7 +415,8 @@ class FBAEMReporterTests: XCTestCase { acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false ) else { return XCTFail("Unwrapping Error") } guard let config = AEMConfiguration(json: SampleAEMData.validConfigData3) @@ -709,6 +712,109 @@ class FBAEMReporterTests: XCTestCase { ) } + func testAttributedInvocationWithDoubleCounting() { + self.reporter.cutOff = false + self.reporter.reportingEvents = [Values.purchase] + let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() + + let configs = [ + Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]) + ] + + let attributedInvocation = AEMReporter._attributedInvocation( + [invocation], + event: Values.purchase, + currency: Values.USD, + value: 10, + parameters: ["value": "abcdefg"], + configs: configs + ) + XCTAssertNil( + attributedInvocation, + "Should not have invocation attributed with double counting" + ) + XCTAssertEqual( + invocation.recordedEvents, + [], + "Should not expect invocation's recorded events to be changed with double counting" + ) + XCTAssertEqual( + invocation.recordedValues, + [:], + "Should not expect invocation's recorded values to be changed with double counting" + ) + } + + func testAttributedInvocationWithoutDoubleCounting() { + self.reporter.cutOff = false + self.reporter.reportingEvents = [Values.purchase] + let invocation = SampleAEMInvocations.createGeneralInvocation1() + + let configs = [ + Values.defaultMode: NSMutableArray(array: [SampleAEMConfigurations.createConfigWithoutBusinessID()]) + ] + + let attributedInvocation = AEMReporter._attributedInvocation( + [invocation], + event: Values.purchase, + currency: Values.USD, + value: 10, + parameters: ["value": "abcdefg"], + configs: configs + ) + XCTAssertNotNil( + attributedInvocation, + "Should have invocation attributed without double counting" + ) + XCTAssertEqual( + invocation.recordedEvents, + [Values.purchase], + "Should expect invocation's recorded events to be changed with double counting" + ) + XCTAssertEqual( + invocation.recordedValues, + [Values.purchase: [Values.USD: 10]], + "Should expect invocation's recorded values to be changed with double counting" + ) + } + + func testIsDoubleCounting() { + self.reporter.cutOff = false + self.reporter.reportingEvents = ["fb_test"] + let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() + + XCTAssertTrue( + AEMReporter._isDoubleCounting(invocation, event: "fb_test"), + "Should expect double counting" + ) + XCTAssertFalse( + AEMReporter._isDoubleCounting(invocation, event: "test"), + "Should not expect double counting" + ) + } + + func testIsDoubleCountingWithCutOff() { + self.reporter.cutOff = true + self.reporter.reportingEvents = ["fb_test"] + let invocation = SampleAEMInvocations.createSKANOverlappedInvocation() + + XCTAssertFalse( + AEMReporter._isDoubleCounting(invocation, event: "fb_test"), + "Should not expect double counting with SKAN cutoff" + ) + } + + func testIsDoubleCountingWithoutSKANClick() { + self.reporter.cutOff = false + self.reporter.reportingEvents = ["fb_test"] + let invocation = SampleAEMInvocations.createGeneralInvocation1() + + XCTAssertFalse( + AEMReporter._isDoubleCounting(invocation, event: "fb_test"), + "Should not expect double counting without SKAN click" + ) + } + // MARK: - Helpers func removeReportFile() { diff --git a/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMData.swift b/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMData.swift index b1bff3440e..72e5c348f6 100644 --- a/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMData.swift +++ b/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMData.swift @@ -161,7 +161,8 @@ class SampleAEMData { // swiftlint:disable:this convenience_type acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: "test_advertiserid_123", - isTestMode: false + isTestMode: false, + hasSKAN: false )! // swiftlint:disable:this force_unwrapping static let invocationWithAdvertiserID2 = AEMInvocation( @@ -170,7 +171,8 @@ class SampleAEMData { // swiftlint:disable:this convenience_type acsSharedSecret: "test_shared_secret_124", acsConfigID: "test_config_id_124", businessID: "test_advertiserid_12346", - isTestMode: false + isTestMode: false, + hasSKAN: false )! // swiftlint:disable:this force_unwrapping static let invocationWithoutAdvertiserID = AEMInvocation( @@ -179,6 +181,7 @@ class SampleAEMData { // swiftlint:disable:this convenience_type acsSharedSecret: "test_shared_secret_123", acsConfigID: "test_config_id_333", businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false )! // swiftlint:disable:this force_unwrapping } diff --git a/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMInvocations.swift b/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMInvocations.swift index 07601c9b34..6836c9b22e 100644 --- a/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMInvocations.swift +++ b/FBAEMKit/FBAEMKitTests/Helpers/SampleAEMInvocations.swift @@ -27,7 +27,8 @@ class SampleAEMInvocations { // swiftlint:disable:this convenience_type acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false )! // swiftlint:disable:this force_unwrapping } @@ -38,7 +39,8 @@ class SampleAEMInvocations { // swiftlint:disable:this convenience_type acsSharedSecret: "test_shared_secret", acsConfigID: "test_config_id_123", businessID: nil, - isTestMode: false + isTestMode: false, + hasSKAN: false )! // swiftlint:disable:this force_unwrapping } @@ -49,7 +51,20 @@ class SampleAEMInvocations { // swiftlint:disable:this convenience_type acsSharedSecret: "debugging_shared_secret", acsConfigID: "debugging_config_id_123", businessID: nil, - isTestMode: true + isTestMode: true, + hasSKAN: false + )! // swiftlint:disable:this force_unwrapping + } + + static func createSKANOverlappedInvocation() -> AEMInvocation { + AEMInvocation( + campaignID: "debugging_campaign", + acsToken: "debugging_token", + acsSharedSecret: "debugging_shared_secret", + acsConfigID: "debugging_config_id_123", + businessID: nil, + isTestMode: false, + hasSKAN: true )! // swiftlint:disable:this force_unwrapping } } diff --git a/Sources/FBAEMKit/FBAEMInvocation.h b/Sources/FBAEMKit/FBAEMInvocation.h index 8fd5d590a1..c64d64f57a 100644 --- a/Sources/FBAEMKit/FBAEMInvocation.h +++ b/Sources/FBAEMKit/FBAEMInvocation.h @@ -41,6 +41,8 @@ NS_SWIFT_NAME(AEMInvocation) @property (nonatomic, readonly, assign) BOOL isTestMode; +@property (nonatomic, readonly, assign) BOOL hasSKAN; + @property (nonatomic, readonly, copy) NSDate *timestamp; @property (nonatomic, readonly, copy) NSString *configMode; diff --git a/Sources/FBAEMKit/FBAEMInvocation.m b/Sources/FBAEMKit/FBAEMInvocation.m index 3711d7b459..d38645a838 100644 --- a/Sources/FBAEMKit/FBAEMInvocation.m +++ b/Sources/FBAEMKit/FBAEMInvocation.m @@ -43,6 +43,7 @@ static NSString *const PRIORITY_KEY = @"priority"; static NSString *const CONVERSION_TIMESTAMP_KEY = @"conversion_timestamp"; static NSString *const IS_AGGREGATED_KEY = @"is_aggregated"; +static NSString *const HAS_SKAN_KEY = @"has_skan"; typedef NSString *const FBAEMInvocationConfigMode; @@ -64,7 +65,8 @@ + (nullable instancetype)invocationWithAppLinkData:(nullable NSDictionary BOOL isGeneralInvocationVisited = NO; FBAEMInvocation *attributedInvocation = nil; for (FBAEMInvocation *invocation in [invocations reverseObjectEnumerator]) { + if ([self _isDoubleCounting:invocation event:event]) { + break; + } if (!invocation.businessID && isGeneralInvocationVisited) { continue; } @@ -224,6 +227,18 @@ + (nullable FBAEMInvocation *)_attributedInvocation:(NSArray return attributedInvocation; } ++ (BOOL)_isDoubleCounting:(FBAEMInvocation *)invocation + event:(NSString *)event +{ + // We consider it as double counting if following conditions meet simultaneously + // 1. The field hasSKAN is true + // 2. The conversion happens before SKAdNetwork cutoff + // 3. The event is also being reported by SKAdNetwork + return invocation.hasSKAN + && ![_reporter shouldCutoff] + && [_reporter isReportingEvent:event]; +} + + (void)_appendAndSaveInvocation:(FBAEMInvocation *)invocation { [self dispatchOnQueue:g_serialQueue block:^() {