diff --git a/Makefile b/Makefile index 1ec47f01..42f4721a 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ functional-test-coverage: javadoc: (./code/gradlew -p code/$(EXTENSION-LIBRARY-FOLDER-NAME) javadocJar) + (./code/gradlew -p code/$(EXTENSION-LIBRARY-FOLDER-NAME) dokkaJavadoc) assemble-phone: (./code/gradlew -p code/$(EXTENSION-LIBRARY-FOLDER-NAME) assemblePhone) @@ -55,4 +56,4 @@ ci-publish-staging: assemble-phone-release (./code/gradlew -p code/$(EXTENSION-LIBRARY-FOLDER-NAME) publishReleasePublicationToSonatypeRepository) ci-publish: assemble-phone-release - (./code/gradlew -p code/$(EXTENSION-LIBRARY-FOLDER-NAME) publishReleasePublicationToSonatypeRepository -Prelease) \ No newline at end of file + (./code/gradlew -p code/$(EXTENSION-LIBRARY-FOLDER-NAME) publishReleasePublicationToSonatypeRepository -Prelease) diff --git a/code/gradle.properties b/code/gradle.properties index 5d7ed040..5cdddcfb 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -16,7 +16,7 @@ org.gradle.caching=true android.useAndroidX=true moduleName=optimize -moduleVersion=3.0.2 +moduleVersion=3.1.0 #Maven artifact mavenRepoName=AdobeMobileOptimizeSdk @@ -24,6 +24,6 @@ mavenRepoDescription=Adobe Experience Platform Optimize extension for the Adobe mavenUploadDryRunFlag=false # production versions for production build -mavenCoreVersion=3.0.0 +mavenCoreVersion=3.2.0 mavenEdgeVersion=3.0.0 functionalTestEdgeVersion=3.0.0 diff --git a/code/optimize/build.gradle.kts b/code/optimize/build.gradle.kts index 63ea812b..4b7c2b84 100644 --- a/code/optimize/build.gradle.kts +++ b/code/optimize/build.gradle.kts @@ -18,6 +18,7 @@ val mavenEdgeVersion: String by project aepLibrary { namespace = "com.adobe.marketing.mobile.optimize" + enableDokkaDoc = true enableSpotless = true enableCheckStyle = true diff --git a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java index b41580d7..3d4e6f7a 100644 --- a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java +++ b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java @@ -76,6 +76,39 @@ public void testExtensionVersion() { Assert.assertEquals(OptimizeTestConstants.EXTENSION_VERSION, Optimize.extensionVersion()); } + @Test + public void testUpdatePropositions_timeoutError() throws Exception { + // Setup + final String decisionScopeName = "decisionScope"; + Map configData = new HashMap<>(); + configData.put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + updateConfiguration(configData); + + // Action + Optimize.updatePropositions( + Collections.singletonList(new DecisionScope(decisionScopeName)), + null, + null, + new AdobeCallbackWithOptimizeError>() { + @Override + public void fail(AEPOptimizeError error) { + Assert.fail(OptimizeConstants.ErrorData.Timeout.DETAIL); + Assert.assertEquals( + OptimizeConstants.ErrorData.Timeout.STATUS, error.getStatus()); + Assert.assertEquals( + OptimizeConstants.ErrorData.Timeout.TITLE, error.getTitle()); + Assert.assertEquals( + OptimizeConstants.ErrorData.Timeout.DETAIL, error.getDetail()); + } + + @Override + public void call( + Map decisionScopePropositionMap) { + Assert.assertNull(decisionScopePropositionMap); + } + }); + } + // 2a @Test public void testUpdatePropositions_validDecisionScope() throws InterruptedException { @@ -819,8 +852,11 @@ public void call( OptimizeTestConstants.EventSource.RESPONSE_CONTENT); Assert.assertNotNull(optimizeResponseEventsList); - Assert.assertEquals(1, optimizeResponseEventsList.size()); - Assert.assertNull(optimizeResponseEventsList.get(0).getEventData().get("responseerror")); + + // 1 additional event is being sent from handleUpdatePropositions() to provide callback for + // updatePropositons() + Assert.assertEquals(2, optimizeResponseEventsList.size()); + Assert.assertEquals(1, propositionMap.size()); OptimizeProposition optimizeProposition = propositionMap.get(decisionScope); Assert.assertNotNull(optimizeProposition); @@ -1208,6 +1244,7 @@ public void call( OptimizeTestConstants.EventSource.RESPONSE_CONTENT); Assert.assertNotNull(optimizeResponseEventsList); + Assert.assertEquals(1, optimizeResponseEventsList.size()); Assert.assertNull(optimizeResponseEventsList.get(0).getEventData().get("responseerror")); Assert.assertEquals(1, propositionMap.size()); diff --git a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java index cf191803..b3a5ca74 100644 --- a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java +++ b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java @@ -13,7 +13,7 @@ public class OptimizeTestConstants { - static final String EXTENSION_VERSION = "3.0.2"; + static final String EXTENSION_VERSION = "3.1.0"; public static final String LOG_TAG = "OptimizeTest"; static final String CONFIG_DATA_STORE = "AdobeMobile_ConfigState"; diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt new file mode 100644 index 00000000..5a092da4 --- /dev/null +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt @@ -0,0 +1,122 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.optimize + +import com.adobe.marketing.mobile.AdobeError + +/** + * AEPOptimizeError class is used to create AEPOptimizeError from error details received from Experience Edge. + * @param type The type of error that occurred. + * @param status The HTTP status code of the error. + * @param title The title of the error. + * @param detail The details of the error. + * @param report The report of the error. + * @param adobeError The corresponding AdobeError. + */ + +data class AEPOptimizeError( + val type: String? = "", + val status: Int? = 0, + val title: String? = "", + val detail: String? = "", + var report: Map?, + var adobeError: AdobeError? +) { + + fun toEventData(): Map = mapOf( + TYPE to type, + STATUS to status, + TITLE to title, + DETAIL to detail, + REPORT to report, + ADOBE_ERROR to adobeError?.toEventData() + ) + + companion object { + const val TYPE = "type" + const val STATUS = "status" + const val TITLE = "title" + const val DETAIL = "detail" + const val REPORT = "report" + const val ADOBE_ERROR = "adobeError" + const val ERROR_NAME = "errorName" + const val ERROR_CODE = "errorCode" + + private val serverErrors = listOf( + OptimizeConstants.HTTPResponseCodes.tooManyRequests, + OptimizeConstants.HTTPResponseCodes.internalServerError, + OptimizeConstants.HTTPResponseCodes.serviceUnavailable + ) + + private val networkErrors = listOf( + OptimizeConstants.HTTPResponseCodes.badGateway, + OptimizeConstants.HTTPResponseCodes.gatewayTimeout + ) + + fun AdobeError.toEventData(): Map = mapOf( + ERROR_NAME to errorName, + ERROR_CODE to errorCode, + ) + + @JvmStatic + fun toAEPOptimizeError(data: Map): AEPOptimizeError { + return AEPOptimizeError( + type = data[TYPE] as? String ?: "", + status = data[STATUS] as? Int ?: 0, + title = data[TITLE] as? String ?: "", + detail = data[DETAIL] as? String ?: "", + report = data[REPORT] as? Map, + adobeError = toAdobeError(data[ADOBE_ERROR] as Map) + ) + } + + @JvmStatic + fun toAdobeError(data: Map): AdobeError { + return getAdobeErrorFromStatus(data[STATUS] as Int?) + } + + fun getTimeoutError(): AEPOptimizeError { + return AEPOptimizeError( + null, + OptimizeConstants.ErrorData.Timeout.STATUS, + OptimizeConstants.ErrorData.Timeout.TITLE, + OptimizeConstants.ErrorData.Timeout.DETAIL, + null, + AdobeError.CALLBACK_TIMEOUT + ) + } + + fun getUnexpectedError(): AEPOptimizeError { + return AEPOptimizeError( + null, + null, + OptimizeConstants.ErrorData.Unexpected.TITLE, + OptimizeConstants.ErrorData.Unexpected.DETAIL, + null, + AdobeError.UNEXPECTED_ERROR + ) + } + + private fun getAdobeErrorFromStatus(status: Int?): AdobeError = when { + status == OptimizeConstants.HTTPResponseCodes.clientTimeout -> AdobeError.CALLBACK_TIMEOUT + serverErrors.contains(status) -> AdobeError.SERVER_ERROR + networkErrors.contains(status) -> AdobeError.NETWORK_ERROR + else -> AdobeError.UNEXPECTED_ERROR + } + } + + init { + if (adobeError == null) { + adobeError = getAdobeErrorFromStatus(status) + } + } +} diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AdobeCallbackWithOptimizeError.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AdobeCallbackWithOptimizeError.java new file mode 100644 index 00000000..f13c5d8a --- /dev/null +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AdobeCallbackWithOptimizeError.java @@ -0,0 +1,18 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.optimize; + +import com.adobe.marketing.mobile.AdobeCallback; + +public interface AdobeCallbackWithOptimizeError extends AdobeCallback { + void fail(final AEPOptimizeError error); +} diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java index a7092b6c..5033e55c 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java @@ -62,12 +62,43 @@ public static void updatePropositions( @NonNull final List decisionScopes, @Nullable final Map xdm, @Nullable final Map data) { + + updatePropositions(decisionScopes, xdm, data, null); + } + + /** + * This API dispatches an Event for the Edge network extension to fetch decision propositions, + * for the provided decision scopes list, from the decisioning services enabled in the + * Experience Edge network. + * + *

The returned decision propositions are cached in-memory in the Optimize SDK extension and + * can be retrieved using {@link #getPropositions(List, AdobeCallback)} API. + * + * @param decisionScopes {@code List} containing scopes for which offers need to + * be updated. + * @param xdm {@code Map} containing additional XDM-formatted data to be sent in + * the personalization query request. + * @param data {@code Map} containing additional free-form data to be sent in + * the personalization query request. + * @param callback {@code AdobeCallback>} which will be + * invoked when decision propositions are received from the Edge network. + */ + public static void updatePropositions( + @NonNull final List decisionScopes, + @Nullable final Map xdm, + @Nullable final Map data, + @Nullable final AdobeCallback> callback) { + if (OptimizeUtils.isNullOrEmpty(decisionScopes)) { Log.warning( OptimizeConstants.LOG_TAG, SELF_TAG, "Cannot update propositions, provided list of decision scopes is null or" + " empty."); + + AEPOptimizeError aepOptimizeError = AEPOptimizeError.Companion.getUnexpectedError(); + failWithOptimizeError(callback, aepOptimizeError); + return; } @@ -115,7 +146,82 @@ public static void updatePropositions( .setEventData(eventData) .build(); - MobileCore.dispatchEvent(event); + MobileCore.dispatchEventWithResponseCallback( + event, + OptimizeConstants.EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT, + new AdobeCallbackWithError() { + @Override + public void fail(final AdobeError adobeError) { + AEPOptimizeError aepOptimizeError; + if (adobeError == AdobeError.CALLBACK_TIMEOUT) { + aepOptimizeError = AEPOptimizeError.Companion.getTimeoutError(); + } else { + aepOptimizeError = AEPOptimizeError.Companion.getUnexpectedError(); + } + failWithOptimizeError(callback, aepOptimizeError); + } + + @Override + public void call(final Event event) { + try { + final Map eventData = event.getEventData(); + if (OptimizeUtils.isNullOrEmpty(eventData)) { + + AEPOptimizeError aepOptimizeError = + AEPOptimizeError.Companion.getUnexpectedError(); + failWithOptimizeError(callback, aepOptimizeError); + return; + } + + if (eventData.containsKey( + OptimizeConstants.EventDataKeys.RESPONSE_ERROR)) { + Object error = + eventData.get( + OptimizeConstants.EventDataKeys.RESPONSE_ERROR); + if (error instanceof Map) { + failWithOptimizeError( + callback, + AEPOptimizeError.toAEPOptimizeError( + (Map) error)); + } + } + + if (!eventData.containsKey( + OptimizeConstants.EventDataKeys.PROPOSITIONS)) { + return; + } + + final List> propositionsList; + propositionsList = + DataReader.getTypedListOfMap( + Object.class, + eventData, + OptimizeConstants.EventDataKeys.PROPOSITIONS); + final Map propositionsMap = + new HashMap<>(); + if (propositionsList != null) { + for (final Map propositionData : propositionsList) { + final OptimizeProposition optimizeProposition = + OptimizeProposition.fromEventData(propositionData); + if (optimizeProposition != null + && !OptimizeUtils.isNullOrEmpty( + optimizeProposition.getScope())) { + final DecisionScope scope = + new DecisionScope(optimizeProposition.getScope()); + propositionsMap.put(scope, optimizeProposition); + } + } + } + + if (callback != null) { + callback.call(propositionsMap); + } + } catch (DataReaderException e) { + failWithOptimizeError( + callback, AEPOptimizeError.Companion.getUnexpectedError()); + } + } + }); } /** @@ -327,4 +433,17 @@ private static void failWithError(final AdobeCallback callback, final AdobeEr callbackWithError.fail(error); } } + + protected static void failWithOptimizeError( + final AdobeCallback callback, final AEPOptimizeError error) { + + final AdobeCallbackWithOptimizeError callbackWithError = + callback instanceof AdobeCallbackWithOptimizeError + ? (AdobeCallbackWithOptimizeError) callback + : null; + + if (callbackWithError != null) { + callbackWithError.fail(error); + } + } } diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java index 02314b73..4633cf71 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java @@ -13,12 +13,12 @@ class OptimizeConstants { static final String LOG_TAG = "Optimize"; - static final String EXTENSION_VERSION = "3.0.2"; + static final String EXTENSION_VERSION = "3.1.0"; static final String EXTENSION_NAME = "com.adobe.optimize"; static final String FRIENDLY_NAME = "Optimize"; static final long DEFAULT_RESPONSE_CALLBACK_TIMEOUT = 500L; static final long GET_RESPONSE_CALLBACK_TIMEOUT = 10000L; - static final long EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT = 5000L; + static final long EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT = 10000L; static final String ACTIVITY_ID = "activityId"; static final String XDM_ACTIVITY_ID = "xdm:activityId"; @@ -29,6 +29,7 @@ class OptimizeConstants { static final String XDM_NAME = "xdm:name"; static final String ERROR_UNKNOWN = "unknown"; + static final Integer UNKNOWN_STATUS = 0; private OptimizeConstants() {} @@ -99,6 +100,9 @@ static final class Edge { static final class ErrorKeys { static final String TYPE = "type"; static final String DETAIL = "detail"; + static final String STATUS = "status"; + static final String TITLE = "title"; + static final String REPORT = "report"; private ErrorKeys() {} } @@ -181,4 +185,38 @@ static final class JsonValues { private JsonValues() {} } + + static final class ErrorData { + static final class Timeout { + static final Integer STATUS = 408; + static final String TITLE = "Request Timeout"; + static final String DETAIL = "Update/Get proposition request resulted in a timeout."; + + private Timeout() {} + } + + static final class Unexpected { + static final String TITLE = "Unexpected Error"; + static final String DETAIL = "An unexpected error occurred."; + + private Unexpected() {} + } + + private ErrorData() {} + } + + static final class HTTPResponseCodes { + static final int success = 200; + static final int noContent = 204; + static final int multiStatus = 207; + static final int invalidRequest = 400; + static final int clientTimeout = 408; + static final int tooManyRequests = 429; + static final int internalServerError = 500; + static final int badGateway = 502; + static final int serviceUnavailable = 503; + static final int gatewayTimeout = 504; + + private HTTPResponseCodes() {} + } } diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java index 50537c5e..c7457206 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java @@ -93,6 +93,19 @@ public boolean doWork(final Event event) { OptimizeConstants.JsonValues.SCHEMA_OFFER_IMAGE, OptimizeConstants.JsonValues.SCHEMA_OFFER_TEXT); + // List containing recoverable network error codes being retried by Edge Network Service + private static final List recoverableNetworkErrorCodes = + Arrays.asList( + OptimizeConstants.HTTPResponseCodes.clientTimeout, + OptimizeConstants.HTTPResponseCodes.tooManyRequests, + OptimizeConstants.HTTPResponseCodes.badGateway, + OptimizeConstants.HTTPResponseCodes.serviceUnavailable, + OptimizeConstants.HTTPResponseCodes.gatewayTimeout); + + // Map containing the update event IDs and corresponding errors as received from Edge SDK + private static final Map updateRequestEventIdsErrors = + new ConcurrentHashMap<>(); + /** * Constructor for {@code OptimizeExtension}. * @@ -380,17 +393,59 @@ public void fail(final AdobeError error) { updateRequestEventIdsInProgress.remove(edgeEvent.getUniqueIdentifier()); propositionsInProgress.clear(); + AEPOptimizeError aepOptimizeError; + if (error == AdobeError.CALLBACK_TIMEOUT) { + aepOptimizeError = AEPOptimizeError.Companion.getTimeoutError(); + } else { + aepOptimizeError = AEPOptimizeError.Companion.getUnexpectedError(); + } + + getApi().dispatch( + createResponseEventWithError(event, aepOptimizeError)); + eventsDispatcher.resume(); } @Override - public void call(final Event event) { - final String requestEventId = OptimizeUtils.getRequestEventId(event); + public void call(final Event callbackEvent) { + final String requestEventId = + OptimizeUtils.getRequestEventId(callbackEvent); if (OptimizeUtils.isNullOrEmpty(requestEventId)) { fail(AdobeError.UNEXPECTED_ERROR); return; } + final Map responseEventData = new HashMap<>(); + AEPOptimizeError aepOptimizeError = + updateRequestEventIdsErrors.get(requestEventId); + if (aepOptimizeError != null) { + responseEventData.put( + OptimizeConstants.EventDataKeys.RESPONSE_ERROR, + aepOptimizeError.toEventData()); + } + + final List> propositionsList = new ArrayList<>(); + + for (Map.Entry entry : + propositionsInProgress.entrySet()) { + OptimizeProposition optimizeProposition = entry.getValue(); + propositionsList.add(optimizeProposition.toEventData()); + } + + responseEventData.put( + OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); + + final Event responseEvent = + new Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.RESPONSE_CONTENT) + .setEventData(responseEventData) + .inResponseToEvent(event) + .build(); + + getApi().dispatch(responseEvent); + final Event updateCompleteEvent = new Event.Builder( OptimizeConstants.EventNames @@ -601,34 +656,94 @@ void handleEdgeResponse(@NonNull final Event event) { * @param event incoming {@link Event} object to be processed. */ void handleEdgeErrorResponse(@NonNull final Event event) { - if (OptimizeUtils.isNullOrEmpty(event.getEventData())) { - Log.debug( + try { + final Map eventData = event.getEventData(); + final String requestEventId = OptimizeUtils.getRequestEventId(event); + + if (!OptimizeUtils.isEdgeErrorResponseContent(event) + || OptimizeUtils.isNullOrEmpty(requestEventId) + || !updateRequestEventIdsInProgress.containsKey(requestEventId)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleEdgeResponse - Ignoring Edge event, either handle type is not edge" + + " error response content, or the response isn't intended for this" + + " extension."); + return; + } + + if (OptimizeUtils.isNullOrEmpty(event.getEventData())) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleEdgeErrorResponse - Ignoring the Edge error response event, either" + + " event is null or event data is null/ empty."); + return; + } + + final String errorType = + DataReader.optString( + eventData, + OptimizeConstants.Edge.ErrorKeys.TYPE, + OptimizeConstants.ERROR_UNKNOWN); + final int errorStatus = + DataReader.optInt( + eventData, + OptimizeConstants.Edge.ErrorKeys.STATUS, + OptimizeConstants.UNKNOWN_STATUS); + final String errorTitle = + DataReader.optString( + eventData, + OptimizeConstants.Edge.ErrorKeys.TITLE, + OptimizeConstants.ERROR_UNKNOWN); + final String errorDetail = + DataReader.optString( + eventData, + OptimizeConstants.Edge.ErrorKeys.DETAIL, + OptimizeConstants.ERROR_UNKNOWN); + final Map errorReport = + DataReader.optTypedMap( + Object.class, + eventData, + OptimizeConstants.Edge.ErrorKeys.REPORT, + new HashMap<>()); + + Log.warning( OptimizeConstants.LOG_TAG, SELF_TAG, - "handleEdgeErrorResponse - Ignoring the Edge error response event, either event" - + " is null or event data is null/ empty."); - return; + "handleEdgeErrorResponse - Decisioning Service error! Error type: (%s),\n" + + "title: (%s),\n" + + "detail: (%s),\n" + + "status: (%s),\n" + + "report: (%s)", + errorType, + errorTitle, + errorDetail, + errorStatus, + errorReport); + + // Check if the errorStatus is in the list of recoverable error codes + if (recoverableNetworkErrorCodes.contains(errorStatus)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "Recoverable error encountered: Status %d", + errorStatus); + return; + } else { + AEPOptimizeError aepOptimizeError = + new AEPOptimizeError( + errorType, errorStatus, errorTitle, errorDetail, errorReport, null); + updateRequestEventIdsErrors.put(requestEventId, aepOptimizeError); + } + } catch (final Exception e) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleEdgeResponse - Cannot process the Edge Error Response event" + + " due to an exception (%s)!", + e.getLocalizedMessage()); } - final Map eventData = event.getEventData(); - - final String errorType = - DataReader.optString( - eventData, - OptimizeConstants.Edge.ErrorKeys.TYPE, - OptimizeConstants.ERROR_UNKNOWN); - final String errorDetail = - DataReader.optString( - eventData, - OptimizeConstants.Edge.ErrorKeys.DETAIL, - OptimizeConstants.ERROR_UNKNOWN); - - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeErrorResponse - Decisioning Service error! Error type: (%s), detail:" - + " (%s)", - errorType, - errorDetail); } /** @@ -859,6 +974,19 @@ private Event createResponseEventWithError(final Event event, final AdobeError e .build(); } + private Event createResponseEventWithError(final Event event, final AEPOptimizeError error) { + final Map eventData = new HashMap<>(); + eventData.put(OptimizeConstants.EventDataKeys.RESPONSE_ERROR, error); + + return new Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.RESPONSE_CONTENT) + .setEventData(eventData) + .inResponseToEvent(event) + .build(); + } + @VisibleForTesting Map getCachedPropositions() { return cachedPropositions; diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java index 189aac1d..505e0214 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java @@ -139,6 +139,20 @@ static boolean isPersonalizationDecisionsResponse(final Event event) { event.getSource()); } + /** + * Checks whether the given event is a edge error response content response returned from the + * Edge network. + * + * @param event instance of {@link Event} + * @return {@code boolean} containing true if event is a edge error response content event, + * false otherwise. + */ + static boolean isEdgeErrorResponseContent(final Event event) { + return OptimizeConstants.EventType.EDGE.equalsIgnoreCase(event.getType()) + && OptimizeConstants.EventSource.ERROR_RESPONSE_CONTENT.equalsIgnoreCase( + event.getSource()); + } + /** * Checks whether the given event is an Optimize request content event for retrieving cached * propositions. diff --git a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/AEPOptimizeErrorTest.kt b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/AEPOptimizeErrorTest.kt new file mode 100644 index 00000000..b7a004bf --- /dev/null +++ b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/AEPOptimizeErrorTest.kt @@ -0,0 +1,105 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.optimize + +import com.adobe.marketing.mobile.AdobeError +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +class AEPOptimizeErrorTest { + + @Test + fun `test toEventData without AdobeError`() { + // Arrange + val reportData: Map = mapOf("key1" to "value1", "key2" to "value2") + val aepOptimizeError = AEPOptimizeError( + type = "SomeErrorType", + status = 404, + title = "Not Found", + detail = "The requested resource could not be found", + report = reportData, + adobeError = null + ) + + // Act + val eventData = aepOptimizeError.toEventData() + + // Assert + assertEquals("SomeErrorType", eventData[AEPOptimizeError.TYPE]) + assertEquals(404, eventData[AEPOptimizeError.STATUS]) + assertEquals("Not Found", eventData[AEPOptimizeError.TITLE]) + assertEquals( + "The requested resource could not be found", + eventData[AEPOptimizeError.DETAIL] + ) + assertEquals(reportData, eventData[AEPOptimizeError.REPORT]) + assertNotNull(eventData[AEPOptimizeError.ADOBE_ERROR]) + } + + @Test + fun `test toEventData with AdobeError`() { + // Arrange + val adobeError = AdobeError.UNEXPECTED_ERROR + val aepOptimizeError = AEPOptimizeError( + type = "SomeErrorType", + status = 500, + title = "Internal Server Error", + detail = "An unexpected error occurred", + report = null, + adobeError = adobeError + ) + + // Act + val eventData = aepOptimizeError.toEventData() + + // Assert + assertEquals("SomeErrorType", eventData[AEPOptimizeError.TYPE]) + assertEquals(500, eventData[AEPOptimizeError.STATUS]) + assertEquals("Internal Server Error", eventData[AEPOptimizeError.TITLE]) + assertEquals("An unexpected error occurred", eventData[AEPOptimizeError.DETAIL]) + assertNull(eventData[AEPOptimizeError.REPORT]) + assertNotNull(eventData[AEPOptimizeError.ADOBE_ERROR]) + + val adobeErrorData = eventData[AEPOptimizeError.ADOBE_ERROR] as Map + assertEquals(adobeError.errorName, adobeErrorData[AEPOptimizeError.ERROR_NAME]) + assertEquals(adobeError.errorCode, adobeErrorData[AEPOptimizeError.ERROR_CODE]) + } + + @Test + fun `test toAEPOptimizeError`() { + // Arrange + val data: Map = mapOf( + AEPOptimizeError.TYPE to "SomeErrorType", + AEPOptimizeError.STATUS to 400, + AEPOptimizeError.TITLE to "Bad Request", + AEPOptimizeError.DETAIL to "Invalid request", + AEPOptimizeError.REPORT to null, + AEPOptimizeError.ADOBE_ERROR to mapOf( + AEPOptimizeError.ERROR_NAME to AdobeError.UNEXPECTED_ERROR.errorName, + AEPOptimizeError.ERROR_CODE to AdobeError.UNEXPECTED_ERROR.errorCode + ) + ) + + // Act + val aepOptimizeError = AEPOptimizeError.toAEPOptimizeError(data) + + // Assert + assertEquals("SomeErrorType", aepOptimizeError.type) + assertEquals(400, aepOptimizeError.status) + assertEquals("Bad Request", aepOptimizeError.title) + assertEquals("Invalid request", aepOptimizeError.detail) + assertNull(aepOptimizeError.report) + assertEquals(AdobeError.UNEXPECTED_ERROR, aepOptimizeError.adobeError) + } +} diff --git a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java index 02e07481..6d3bc72a 100644 --- a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java +++ b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java @@ -1269,6 +1269,17 @@ public void testHandleEdgeErrorResponse() throws Exception { .getResource( "json/EVENT_DATA_EDGE_ERROR_RESPONSE.json"), HashMap.class); + + extension.setUpdateRequestEventIdsInProgress( + "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", + new ArrayList() { + { + add( + new DecisionScope( + "eydhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==")); + } + }); + final Event testEvent = new Event.Builder( "AEP Error Response", diff --git a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java index 0baea913..a880b206 100644 --- a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java +++ b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java @@ -82,8 +82,17 @@ public void testUpdatePropositions_validDecisionScope() { Optimize.updatePropositions(scopes, null, null); // verify + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - mobileCoreMockedStatic.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); + final ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(AdobeCallbackWithError.class); + mobileCoreMockedStatic.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + ArgumentMatchers.anyLong(), + callbackCaptor.capture())); + final Event event = eventCaptor.getValue(); Assert.assertNotNull(event); @@ -108,6 +117,108 @@ public void testUpdatePropositions_validDecisionScope() { } } + @Test + public void testUpdatePropositionsWithCallback_validDecisionScope() throws Exception { + try (MockedStatic mobileCoreMockedStatic = + Mockito.mockStatic(MobileCore.class); + MockedStatic base64MockedStatic = Mockito.mockStatic(Base64.class)) { + // setup + base64MockedStatic + .when( + () -> + Base64.decode( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt())) + .thenAnswer( + (Answer) + invocation -> + java.util.Base64.getDecoder() + .decode((String) invocation.getArguments()[0])); + + // test + final List scopes = new ArrayList<>(); + scopes.add( + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==")); + + Optimize.updatePropositions( + scopes, + null, + null, + new AdobeCallbackWithError>() { + @Override + public void fail(AdobeError adobeError) { + responseError = adobeError; + } + + @Override + public void call(Map propositionsMap) { + responseMap = propositionsMap; + } + }); + + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(AdobeCallbackWithError.class); + + mobileCoreMockedStatic.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + ArgumentMatchers.anyLong(), + callbackCaptor.capture())); + + final Event event = eventCaptor.getValue(); + final AdobeCallbackWithError callbackWithError = callbackCaptor.getValue(); + + Assert.assertNotNull(event); + Assert.assertEquals("com.adobe.eventType.optimize", event.getType()); + Assert.assertEquals("com.adobe.eventSource.requestContent", event.getSource()); + + final Map eventData = event.getEventData(); + Assert.assertEquals("updatepropositions", eventData.get("requesttype")); + + final List> scopesList = + (List>) eventData.get("decisionscopes"); + Assert.assertEquals(1, scopesList.size()); + + final Map scopeData = scopesList.get(0); + Assert.assertNotNull(scopeData); + Assert.assertEquals(1, scopeData.size()); + Assert.assertEquals( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", + scopeData.get("name")); + + // verify callback response + final Map propositionData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource("json/PROPOSITION_VALID.json"), + HashMap.class); + final OptimizeProposition optimizeProposition = + OptimizeProposition.fromEventData(propositionData); + Assert.assertNotNull(optimizeProposition); + + final List> propositionsList = new ArrayList<>(); + propositionsList.add(optimizeProposition.toEventData()); + + final Map responseEventData = new HashMap<>(); + responseEventData.put("propositions", propositionsList); + final Event responseEvent = + new Event.Builder( + "Optimize Response", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.responseContent") + .setEventData(responseEventData) + .build(); + callbackWithError.call(responseEvent); + + Assert.assertNull(responseError); + } + } + @Test public void testUpdatePropositions_validDecisionScopeWithXDMAndData() { try (MockedStatic mobileCoreMockedStatic = @@ -145,9 +256,16 @@ public void testUpdatePropositions_validDecisionScopeWithXDMAndData() { } }); - // verify final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - mobileCoreMockedStatic.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); + final ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(AdobeCallbackWithError.class); + mobileCoreMockedStatic.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + ArgumentMatchers.anyLong(), + callbackCaptor.capture())); + final Event event = eventCaptor.getValue(); Assert.assertNotNull(event); @@ -207,9 +325,16 @@ public void testUpdatePropositions_multipleValidDecisionScopes() { Optimize.updatePropositions(scopes, null, null); - // verify final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - mobileCoreMockedStatic.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); + final ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(AdobeCallbackWithError.class); + mobileCoreMockedStatic.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + ArgumentMatchers.anyLong(), + callbackCaptor.capture())); + final Event event = eventCaptor.getValue(); Assert.assertNotNull(event); diff --git a/code/testapp/build.gradle.kts b/code/testapp/build.gradle.kts index de122c32..cc6b2bc9 100644 --- a/code/testapp/build.gradle.kts +++ b/code/testapp/build.gradle.kts @@ -54,7 +54,7 @@ android { dependencies { implementation(project(":optimize")) - implementation("com.adobe.marketing.mobile:core:3.0.0") + implementation("com.adobe.marketing.mobile:core:3.2.0") implementation("com.adobe.marketing.mobile:edge:3.0.0") implementation("com.adobe.marketing.mobile:edgeidentity:3.0.0") implementation("com.adobe.marketing.mobile:assurance:3.0.0") diff --git a/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt b/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt index dfbabc7a..a7832f5e 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt @@ -11,10 +11,13 @@ */ package com.adobe.marketing.optimizeapp.viewmodels +import android.util.Log import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import com.adobe.marketing.mobile.AdobeCallbackWithError import com.adobe.marketing.mobile.AdobeError +import com.adobe.marketing.mobile.optimize.AEPOptimizeError +import com.adobe.marketing.mobile.optimize.AdobeCallbackWithOptimizeError import com.adobe.marketing.mobile.optimize.DecisionScope import com.adobe.marketing.mobile.optimize.Optimize import com.adobe.marketing.mobile.optimize.OptimizeProposition @@ -94,7 +97,16 @@ class MainViewModel: ViewModel() { */ fun updatePropositions(decisionScopes: List , xdm: Map , data: Map) { optimizePropositionStateMap.clear() - Optimize.updatePropositions(decisionScopes, xdm, data) + Optimize.updatePropositions(decisionScopes, xdm, data, object: AdobeCallbackWithOptimizeError>{ + override fun call(propositions: Map?) { + Log.i("Optimize Test App","Propositions updated successfully.") + } + + override fun fail(error: AEPOptimizeError?) { + Log.i("Optimize Test App","Error in updating Propositions:: ${error?.title ?: "Undefined"}.") + } + + }) } /**