From b54736ed08a8330e79b4f3356d32470a3422c65d Mon Sep 17 00:00:00 2001 From: Soner Yuksel Date: Fri, 22 Dec 2023 13:29:17 -0500 Subject: [PATCH] Error cases is handled and proper ping referral code is added for these cases --- .../Growth/URP/AdAttributionReportData.swift | 34 ++++++++++------ Sources/Growth/URP/AttributionManager.swift | 39 +++++++++++-------- Sources/Growth/URP/UrpService.swift | 4 +- Sources/Growth/URP/UserReferralProgram.swift | 10 ++--- .../Welcome/WelcomeViewController.swift | 34 +++++++++++++--- 5 files changed, 80 insertions(+), 41 deletions(-) diff --git a/Sources/Growth/URP/AdAttributionReportData.swift b/Sources/Growth/URP/AdAttributionReportData.swift index 7759c88f351..acefbb36943 100644 --- a/Sources/Growth/URP/AdAttributionReportData.swift +++ b/Sources/Growth/URP/AdAttributionReportData.swift @@ -6,28 +6,38 @@ import os.log import Foundation -public enum SerializationError: Error { +enum SerializationError: Error { case missing(String) case invalid(String, Any) } +public enum SearchAdError: Error { + case invalidCampaignTokenData + case invalidGroupsReportData + case failedCampaignTokenFetch + case failedCampaignTokenLookup + case missingReportsKeywordParameter + case failedReportsKeywordLookup + case successfulCampaignFailedKeywordLookup(AdAttributionData) +} + public struct AdAttributionData { // A value of true returns if a user clicks an Apple Search Ads impression up to 30 days before your app download. // If the API can’t find a matching attribution record, the attribution value is false. - public let attribution: Bool + let attribution: Bool // The identifier of the organization that owns the campaign. // organizationId is the same as your account in the Apple Search Ads UI. - public let organizationId: Int? + let organizationId: Int? // The type of conversion is either Download or Redownload. - public let conversionType: String? + let conversionType: String? // The unique identifier for the campaign. - public let campaignId: Int + let campaignId: Int // The country or region for the campaign. - public let countryOrRegion: String? + let countryOrRegion: String? // The ad group if for the campaign which will be used for feature link - public let adGroupId: Int? + let adGroupId: Int? // The keyword id for the campaign which will be used for feature link - public let keywordId: Int? + let keywordId: Int? init(attribution: Bool, organizationId: Int? = nil, @@ -83,20 +93,20 @@ public struct AdAttributionData { } public struct AdGroupReportData { - public struct KeywordReportResponse: Codable { + struct KeywordReportResponse: Codable { let row: [KeywordRow] } - public struct KeywordRow: Codable { + struct KeywordRow: Codable { let metadata: KeywordMetadata } - public struct KeywordMetadata: Codable { + struct KeywordMetadata: Codable { let keyword: String let keywordId: Int } - public let productKeyword: String + let productKeyword: String init(data: Data, keywordId: Int) throws { do { diff --git a/Sources/Growth/URP/AttributionManager.swift b/Sources/Growth/URP/AttributionManager.swift index 49254837282..3a50e0f1985 100644 --- a/Sources/Growth/URP/AttributionManager.swift +++ b/Sources/Growth/URP/AttributionManager.swift @@ -8,14 +8,15 @@ import Preferences import Combine import Shared -public class AttributionManager { - public enum FeatureLinkageType { - case undefined, vpn, playlist - } - - public enum FeatureLinkageError: Error { - case executionTimeout - } +public enum FeatureLinkageType { + case undefined, vpn, playlist +} + +public enum FeatureLinkageError: Error { + case executionTimeout(AdAttributionData) +} + +public class AttributionManager { private let dau: DAU private let urp: UserReferralProgram @@ -44,14 +45,13 @@ public class AttributionManager { @MainActor public func handleSearchAdsInstallAttribution() async throws { do { let attributionData = try await urp.adCampaignLookup() - let refCode = generateReferralCode(attributionData: attributionData) - setupReferralCodeAndPingServer(refCode: refCode) + generateReferralCodeAndPingServer(with: attributionData) } catch { throw error } } - @MainActor public func handleAdsReportingFeatureLinkage() async throws -> String { + @MainActor public func handleAdsReportingFeatureLinkage() async throws -> (keyword: String, attributionData: AdAttributionData) { // This function should run multiple tasks first adCampaignLookup // and adReportsKeywordLookup depending on adCampaignLookup result. // There is maximum threshold of 5 sec for all the tasks to be completed @@ -61,13 +61,14 @@ public class AttributionManager { let start = DispatchTime.now() // Start time for time tracking do { - let attributionData = try await urp.adCampaignLookup() + // Ad campaign Lookup should be performed with no retry as first step + let attributionData = try await urp.adCampaignLookup(isRetryEnabled: false) let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000 let remainingTime = 5.0 - elapsedTime guard remainingTime > 0 else { - throw FeatureLinkageError.executionTimeout + throw FeatureLinkageError.executionTimeout(attributionData) } let task2Timeout = DispatchTime.now() + .seconds(Int(remainingTime)) @@ -78,16 +79,15 @@ public class AttributionManager { let keyword = try await self.urp.adReportsKeywordLookup(attributionData: attributionData) continuation.resume(returning: keyword) } catch { - continuation.resume(throwing: error) + continuation.resume(throwing: SearchAdError.successfulCampaignFailedKeywordLookup(attributionData)) } } DispatchQueue.global().asyncAfter(deadline: task2Timeout) { - continuation.resume(throwing: FeatureLinkageError.executionTimeout) + continuation.resume(throwing: FeatureLinkageError.executionTimeout(attributionData)) } } - - return keywordResult + return (keywordResult, attributionData) } catch { throw error } @@ -104,6 +104,11 @@ public class AttributionManager { dau.sendPingToServer() } + public func generateReferralCodeAndPingServer(with attributionData: AdAttributionData) { + let refCode = generateReferralCode(attributionData: attributionData) + setupReferralCodeAndPingServer(refCode: refCode) + } + private func performProgramReferralLookup(refCode: String?, completion: @escaping (URL?) -> Void) { urp.referralLookup(refCode: refCode) { referralCode, offerUrl in Preferences.URP.referralLookupOutstanding.value = false diff --git a/Sources/Growth/URP/UrpService.swift b/Sources/Growth/URP/UrpService.swift index ecc249fef56..8dfda9c9eae 100644 --- a/Sources/Growth/URP/UrpService.swift +++ b/Sources/Growth/URP/UrpService.swift @@ -99,7 +99,7 @@ struct UrpService { return adAttributionData } - throw SerializationError.invalid("Invalid Data type from response", "") + throw SearchAdError.invalidCampaignTokenData } catch { throw error } @@ -123,7 +123,7 @@ struct UrpService { return adGroupsReportData.productKeyword } - throw SerializationError.invalid("Invalid Data type from response", "") + throw SearchAdError.invalidGroupsReportData } catch { throw error } diff --git a/Sources/Growth/URP/UserReferralProgram.swift b/Sources/Growth/URP/UserReferralProgram.swift index c4427c90fe0..b388fba9c0b 100644 --- a/Sources/Growth/URP/UserReferralProgram.swift +++ b/Sources/Growth/URP/UserReferralProgram.swift @@ -122,20 +122,20 @@ public class UserReferralProgram { do { return try await service.adCampaignTokenLookupQueue(adAttributionToken: adAttributionToken) - } catch { Logger.module.info("Could not retrieve ad campaign attibution from ad services") - throw error + throw SearchAdError.invalidCampaignTokenData } } catch { Logger.module.info("Couldnt fetch attribute tokens with error: \(error)") - throw error + throw SearchAdError.failedCampaignTokenFetch } } @MainActor func adReportsKeywordLookup(attributionData: AdAttributionData) async throws -> String { guard let adGroupId = attributionData.adGroupId, let keywordId = attributionData.keywordId else { - throw SerializationError.invalid("adGroupId or keywordId is nil", "") + Logger.module.info("Could not retrieve ad campaign attibution from ad services") + throw SearchAdError.missingReportsKeywordParameter } do { @@ -146,7 +146,7 @@ public class UserReferralProgram { } catch { Logger.module.info("Could not retrieve ad groups reports using ad services") - throw error + throw SearchAdError.failedReportsKeywordLookup } } diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index cb34ad02b98..4267014794b 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -389,17 +389,27 @@ public class WelcomeViewController: UIViewController { Task { @MainActor in do { // Handle API calls and send linkage type -// let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() + let featureType = try await controller.attributionManager.handleAdsReportingFeatureLinkage() // controller.attributionManager.adFeatureLinkage = featureType! controller.calloutView.isLoading = false self?.close() + } catch FeatureLinkageError.executionTimeout(let attributionData) { + // Time out occurred while executing ad reports lookup + // Ad Campaign Lookup is successful so dau server should be pinged + // attribution data referral code + await self?.pingServerWithGeneratedReferralCode( + using: attributionData, controller: controller) + } catch SearchAdError.successfulCampaignFailedKeywordLookup(let attributionData) { + // Error occurred while executing ad reports lookup + // Ad Campaign Lookup is successful so dau server should be pinged + // attribution data referral code + await self?.pingServerWithGeneratedReferralCode( + using: attributionData, controller: controller) } catch { - // Sending default organic install code for dau + // Error occurred before getting successful + // attributuion data, generic code should be pinged controller.attributionManager.setupReferralCodeAndPingServer() - - controller.calloutView.isLoading = false - self?.close() } } } @@ -413,6 +423,20 @@ public class WelcomeViewController: UIViewController { Preferences.Onboarding.p3aOnboardingShown.value = true } } + + private func pingServerWithGeneratedReferralCode(using attributionData: AdAttributionData, controller: WelcomeViewController) async { + Task { + await withCheckedContinuation { continuation in + DispatchQueue.global().async { + controller.attributionManager.generateReferralCodeAndPingServer(with: attributionData) + continuation.resume() + } + } + } + + controller.calloutView.isLoading = false + close() + } private func onSetDefaultBrowser() { guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {