diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt index 2c40b00..ced525f 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt @@ -221,7 +221,7 @@ class Coordinator( } } } catch (error: SPError) { - throw LoadMessagesException(causedBy = error) + throw LoadMessagesException(cause = error) } storeLegislationConsent(userData = userData) persistState() @@ -1007,7 +1007,7 @@ class Coordinator( IOS14, SPCampaignType.Unknown -> {} } } catch (error: SPError) { - throw ReportActionException(causedBy = error, actionType = action.type, campaignType = action.campaignType) + throw ReportActionException(cause = error, actionType = action.type, campaignType = action.campaignType) } finally { storeLegislationConsent(userData = userData) persistState() @@ -1044,7 +1044,7 @@ class Coordinator( legIntCategories = legIntCategories )) } catch (error: SPError) { - throw PostCustomConsentGDPRException(causedBy = error) + throw PostCustomConsentGDPRException(cause = error) } } @@ -1065,7 +1065,7 @@ class Coordinator( legIntCategories = legIntCategories )) } catch (error: SPError) { - throw DeleteCustomConsentGDPRException(causedBy = error) + throw DeleteCustomConsentGDPRException(cause = error) } } diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt index b929aa8..88c2424 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt @@ -3,9 +3,15 @@ package com.sourcepoint.mobile_core.models open class SPError( val code: String = "sp_metric_generic_mobile-core_error", val description: String = "Something went wrong in the Mobile Core", - val causedBy: SPError? = null, + override val cause: Throwable? = null, open val campaignType: SPCampaignType? = null -): Exception(description) +): Throwable(description) { + companion object { + fun castToSPError(error: Throwable): SPError = + error as? SPError ?: + SPError(cause = error, description = error.message ?: "Something went wrong in the Mobile Core") + } +} open class SPUnknownNetworkError(path: String): SPError( code = "sp_metric_unknown_network_error_${path}", @@ -52,27 +58,55 @@ open class InvalidPropertyNameError(propertyName: String): SPError( open class ReportActionException( actionType: SPActionType, campaignType: SPCampaignType?, - causedBy: SPError + cause: SPError ): SPError( code = "sp_metric_report_action_exception_${campaignType?.name}_${actionType.name}", - description = "Unable to report ${actionType.name} action for campaign ${campaignType?.name} due to ${causedBy.description}", - causedBy = causedBy + description = "Unable to report ${actionType.name} action for campaign ${campaignType?.name} due to ${cause.description}", + cause = cause ) -open class LoadMessagesException(causedBy: SPError): SPError( +open class LoadMessagesException(cause: SPError): SPError( code = "sp_metric_load_messages", - description = "Unable to loadMessages due to ${causedBy.description}", - causedBy = causedBy + description = "Unable to loadMessages due to ${cause.description}", + cause = cause ) -open class PostCustomConsentGDPRException(causedBy: SPError): SPError( +open class PostCustomConsentGDPRException(cause: SPError): SPError( code = "sp_metric_post_custom_consent_gdpr", - description = "Unable to post CustomConsentGDPR due to ${causedBy.description}", - causedBy = causedBy + description = "Unable to post CustomConsentGDPR due to ${cause.description}", + cause = cause ) -open class DeleteCustomConsentGDPRException(causedBy: SPError): SPError( +open class DeleteCustomConsentGDPRException(cause: SPError): SPError( code = "sp_metric_delete_custom_consent_gdpr", - description = "Unable to delete CustomConsentGDPR due to ${causedBy.description}", - causedBy = causedBy + description = "Unable to delete CustomConsentGDPR due to ${cause.description}", + cause = cause ) + +open class InvalidRequestAPIError(cause: Throwable, endpoint: InvalidAPICode): SPError ( + code = "sp_metric_invalid_response_api${endpoint.type}", + description = "The SDK got an unexpected response from ${endpoint.name}", + cause = castToSPError(cause) +) + +enum class InvalidAPICode(val type: String) { + META_DATA("_meta-data"), + CONSENT_STATUS("_consent-status"), + PV_DATA("_pv-data"), + MESSAGES("_messages"), + ERROR_METRICS("_error-metrics"), + CCPA_ACTION("_CCPA-action"), + GDPR_ACTION("_GDPR-action"), + USNAT_ACTION("_USNAT-action"), + GLOBALCMP_ACTION("_GLOBALCMP-action"), + PREFERENCES_ACTION("_PREFERENCES-action"), + IDFA_STATUS( "_IDFA-status"), + CHOICE_ALL("_choice-all"), + CUSTOM_CONSENT("custom-consent-GDPR"), + DELETE_CUSTOM_CONSENT("_delete-custom-consent-GDPR"), + CCPA_PRIVACY_MANAGER("_CCPA-privacy-manager"), + GDPR_PRIVACY_MANAGER("_GDPR-privacy-manager"), + CCPA_MESSAGE("_CCPA-message"), + GDPR_MESSAGE("_GDPR-message"), + EMPTY("") +} diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt index e1bc891..4a93cd7 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt @@ -3,8 +3,12 @@ package com.sourcepoint.mobile_core.network import com.sourcepoint.core.BuildConfig import com.sourcepoint.mobile_core.DeviceInformation import com.sourcepoint.mobile_core.models.InvalidChoiceAllParamsError +import com.sourcepoint.mobile_core.models.InvalidAPICode +import com.sourcepoint.mobile_core.models.InvalidAPICode.* +import com.sourcepoint.mobile_core.models.InvalidRequestAPIError import com.sourcepoint.mobile_core.models.SPActionType import com.sourcepoint.mobile_core.models.SPCampaignType +import com.sourcepoint.mobile_core.models.SPClientTimeout import com.sourcepoint.mobile_core.models.SPError import com.sourcepoint.mobile_core.models.SPIDFAStatus import com.sourcepoint.mobile_core.models.SPNetworkError @@ -199,68 +203,80 @@ class SourcepointClient( private val baseWrapperUrl = "https://cdn.privacy-mgmt.com/" - override suspend fun getMetaData(campaigns: MetaDataRequest.Campaigns): MetaDataResponse = http.get( - URLBuilder(baseWrapperUrl).apply { - path("wrapper", "v2", "meta-data") - withParams( - MetaDataRequest( - accountId = accountId, - propertyId = propertyId, - metadata = campaigns - ) - ) - }.build() - ).bodyOr(::reportErrorAndThrow) + override suspend fun getMetaData(campaigns: MetaDataRequest.Campaigns): MetaDataResponse = + executeAPIRequest(META_DATA) { + http.get( + URLBuilder(baseWrapperUrl).apply { + path("wrapper", "v2", "meta-data") + withParams( + MetaDataRequest( + accountId = accountId, + propertyId = propertyId, + metadata = campaigns + ) + ) + }.build() + ).bodyOr(::reportErrorAndThrow) + } override suspend fun postPvData(request: PvDataRequest): PvDataResponse = - http.post(URLBuilder(baseWrapperUrl).apply { - path("wrapper", "v2", "pv-data") - withParams(DefaultRequest()) + executeAPIRequest(PV_DATA) { + http.post(URLBuilder(baseWrapperUrl).apply { + path("wrapper", "v2", "pv-data") + withParams(DefaultRequest()) }.build()) { contentType(ContentType.Application.Json) setBody(request) }.bodyOr(::reportErrorAndThrow) + } override suspend fun getConsentStatus(authId: String?, metadata: ConsentStatusRequest.MetaData): ConsentStatusResponse = - http.get(URLBuilder(baseWrapperUrl).apply { - path("wrapper", "v2", "consent-status") - withParams( - ConsentStatusRequest( - propertyId = propertyId, - authId = authId, - metadata = metadata - ) - ) - }.build() - ).bodyOr(::reportErrorAndThrow) + executeAPIRequest(CONSENT_STATUS) { + http.get( + URLBuilder(baseWrapperUrl).apply { + path("wrapper", "v2", "consent-status") + withParams( + ConsentStatusRequest( + propertyId = propertyId, + authId = authId, + metadata = metadata + ) + ) + }.build() + ).bodyOr(::reportErrorAndThrow) + } @Throws(SPNetworkError::class, SPUnableToParseBodyError::class, CancellationException::class, HttpRequestTimeoutException::class) private suspend inline fun genericPostChoiceAction( actionType: SPActionType, + endpoint: InvalidAPICode, fromCampaign: String, request: ChoiceRequest - ): ChoiceResponse = http.post(URLBuilder(baseWrapperUrl).apply { - path("wrapper", "v2", "choice", fromCampaign, actionType.type.toString()) - withParams(DefaultRequest()) - }.build()) { - contentType(ContentType.Application.Json) - setBody(request) - }.bodyOr(::reportErrorAndThrow) + ): ChoiceResponse = + executeAPIRequest(endpoint) { + http.post(URLBuilder(baseWrapperUrl).apply { + path("wrapper", "v2", "choice", fromCampaign, actionType.type.toString()) + withParams(DefaultRequest()) + }.build()) { + contentType(ContentType.Application.Json) + setBody(request) + }.bodyOr(::reportErrorAndThrow) + } override suspend fun postChoiceGDPRAction(actionType: SPActionType, request: GDPRChoiceRequest): GDPRChoiceResponse = - genericPostChoiceAction(actionType, "gdpr", request) + genericPostChoiceAction(actionType, GDPR_ACTION, "gdpr", request) override suspend fun postChoiceUSNatAction(actionType: SPActionType, request: USNatChoiceRequest): USNatChoiceResponse = - genericPostChoiceAction(actionType, "usnat", request) + genericPostChoiceAction(actionType, USNAT_ACTION, "usnat", request) override suspend fun postChoiceGlobalCmpAction(actionType: SPActionType, request: GlobalCmpChoiceRequest): GlobalCmpChoiceResponse = - genericPostChoiceAction(actionType, "global-cmp", request) + genericPostChoiceAction(actionType, GLOBALCMP_ACTION, "global-cmp", request) override suspend fun postChoicePreferencesAction(actionType: SPActionType, request: PreferencesChoiceRequest): PreferencesChoiceResponse = - genericPostChoiceAction(actionType, "preferences", request) + genericPostChoiceAction(actionType, PREFERENCES_ACTION, "preferences", request) override suspend fun postChoiceCCPAAction(actionType: SPActionType, request: CCPAChoiceRequest): CCPAChoiceResponse = - genericPostChoiceAction(actionType, "ccpa", request) + genericPostChoiceAction(actionType, CCPA_ACTION, "ccpa", request) override suspend fun getChoiceAll( actionType: SPActionType, @@ -271,17 +287,21 @@ class SourcepointClient( SPActionType.RejectAll -> { "reject-all" } else -> throw InvalidChoiceAllParamsError() } - return http.get(URLBuilder(baseWrapperUrl).apply { - path("wrapper", "v2", "choice", choicePath) - withParams(ChoiceAllRequest(accountId, propertyId, campaigns)) - }.build()).bodyOr(::reportErrorAndThrow) + return executeAPIRequest(CHOICE_ALL) { + return@executeAPIRequest http.get(URLBuilder(baseWrapperUrl).apply { + path("wrapper", "v2", "choice", choicePath) + withParams(ChoiceAllRequest(accountId, propertyId, campaigns)) + }.build()).bodyOr(::reportErrorAndThrow) + } } override suspend fun getMessages(request: MessagesRequest): MessagesResponse = - http.get(URLBuilder(baseWrapperUrl).apply { - path("wrapper", "v2", "messages") - withParams(request) - }.build()).bodyOr(::reportErrorAndThrow) + executeAPIRequest(MESSAGES) { + http.get(URLBuilder(baseWrapperUrl).apply { + path("wrapper", "v2", "messages") + withParams(request) + }.build()).bodyOr(::reportErrorAndThrow) + } override suspend fun postReportIdfaStatus( propertyId: Int?, @@ -293,26 +313,28 @@ class SourcepointClient( iosVersion: String, partitionUUID: String? ) { - http.post(URLBuilder(baseWrapperUrl).apply { - path("wrapper", "metrics", "v1", "apple-tracking") - withParams(DefaultRequest()) - }.build()) { - contentType(ContentType.Application.Json) - setBody( - IDFAStatusReportRequest( - accountId = accountId, - propertyId = propertyId, - uuid = uuid, - uuidType = uuidType, - requestUUID = requestUUID, - iosVersion = iosVersion, - appleTracking = IDFAStatusReportRequest.AppleTrackingPayload( - appleChoice = idfaStatus, - appleMsgId = messageId, - messagePartitionUUID = partitionUUID + executeAPIRequest(IDFA_STATUS) { + http.post(URLBuilder(baseWrapperUrl).apply { + path("wrapper", "metrics", "v1", "apple-tracking") + withParams(DefaultRequest()) + }.build()) { + contentType(ContentType.Application.Json) + setBody( + IDFAStatusReportRequest( + accountId = accountId, + propertyId = propertyId, + uuid = uuid, + uuidType = uuidType, + requestUUID = requestUUID, + iosVersion = iosVersion, + appleTracking = IDFAStatusReportRequest.AppleTrackingPayload( + appleChoice = idfaStatus, + appleMsgId = messageId, + messagePartitionUUID = partitionUUID + ) ) ) - ) + } } } @@ -323,6 +345,7 @@ class SourcepointClient( categories: List, legIntCategories: List ): GDPRConsent = + executeAPIRequest(CUSTOM_CONSENT) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "tcfv2", "v1", "gdpr", "custom-consent") withParams(DefaultRequest()) @@ -335,8 +358,10 @@ class SourcepointClient( vendors = vendors, categories = categories, legIntCategories = legIntCategories - )) + ) + ) }.bodyOr(::reportErrorAndThrow) + } override suspend fun deleteCustomConsentGDPR( consentUUID: String, @@ -345,23 +370,26 @@ class SourcepointClient( categories: List, legIntCategories: List ): GDPRConsent = - http.delete(URLBuilder(baseWrapperUrl).apply { - path("consent", "tcfv2", "consent", "v3", "custom", propertyId.toString()) - withParams(DeleteCustomConsentRequest(consentUUID)) - }.build()) { - contentType(ContentType.Application.Json) - setBody( - CustomConsentRequest( - consentUUID = consentUUID, - propertyId = propertyId, - vendors = vendors, - categories = categories, - legIntCategories = legIntCategories - )) - }.bodyOr(::reportErrorAndThrow) + executeAPIRequest(DELETE_CUSTOM_CONSENT) { + http.delete(URLBuilder(baseWrapperUrl).apply { + path("consent", "tcfv2", "consent", "v3", "custom", propertyId.toString()) + withParams(DeleteCustomConsentRequest(consentUUID)) + }.build()) { + contentType(ContentType.Application.Json) + setBody( + CustomConsentRequest( + consentUUID = consentUUID, + propertyId = propertyId, + vendors = vendors, + categories = categories, + legIntCategories = legIntCategories + ) + ) + }.bodyOr(::reportErrorAndThrow) + } override suspend fun errorMetrics(error: SPError) { - try { + executeAPIRequest(ERROR_METRICS) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "metrics", "v1", "custom-metrics") withParams(DefaultRequest()) @@ -379,7 +407,7 @@ class SourcepointClient( ) ) } - } catch (_: Exception) {} + } } private suspend fun reportErrorAndThrow(error: SPError): SPError { @@ -388,6 +416,14 @@ class SourcepointClient( } } +private suspend fun executeAPIRequest(endpoint: InvalidAPICode, command: suspend () -> T): T = + try { command() } catch (error: Throwable) { + when (error) { + is SPUnableToParseBodyError, is SPClientTimeout, is SPNetworkError -> throw error + else -> throw InvalidRequestAPIError(cause = error, endpoint = endpoint) + } + } + @Throws(SPUnableToParseBodyError::class, CancellationException::class, HttpRequestTimeoutException::class) internal suspend inline fun HttpResponse.bodyOr(loggingFunction: KSuspendFunction1): T = try {