From 1f5367cf8d06be95dad8587e4fb389b46de4ae62 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Tue, 20 Aug 2024 18:06:48 +0530 Subject: [PATCH 01/39] Updated updateProposition public API to provide a callback to customer --- Podfile.lock | 36 ++++++++++---------- Sources/AEPOptimize/Optimize+PublicAPI.swift | 16 +++++++-- Sources/AEPOptimize/Optimize.swift | 11 +++++- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 58f6e1d..4c0376c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,25 +2,25 @@ PODS: - AEPAssurance (5.0.0): - AEPCore (< 6.0.0, >= 5.0.0) - AEPServices (< 6.0.0, >= 5.0.0) - - AEPCore (5.0.0): + - AEPCore (5.2.0): - AEPRulesEngine (< 6.0.0, >= 5.0.0) - - AEPServices (< 6.0.0, >= 5.0.0) - - AEPEdge (5.0.1): - - AEPCore (< 6.0.0, >= 5.0.0) + - AEPServices (< 6.0.0, >= 5.2.0) + - AEPEdge (5.0.2): + - AEPCore (< 6.0.0, >= 5.1.0) - AEPEdgeIdentity (< 6.0.0, >= 5.0.0) - AEPEdgeConsent (5.0.0): - AEPCore (< 6.0.0, >= 5.0.0) - AEPEdge (< 6.0.0, >= 5.0.0) - AEPEdgeIdentity (5.0.0): - AEPCore (< 6.0.0, >= 5.0.0) - - AEPIdentity (5.0.0): - - AEPCore (< 6.0.0, >= 5.0.0) - - AEPLifecycle (5.0.0): - - AEPCore (< 6.0.0, >= 5.0.0) + - AEPIdentity (5.2.0): + - AEPCore (< 6.0.0, >= 5.2.0) + - AEPLifecycle (5.2.0): + - AEPCore (< 6.0.0, >= 5.2.0) - AEPRulesEngine (5.0.0) - - AEPServices (5.0.0) - - AEPSignal (5.0.0): - - AEPCore (< 6.0.0, >= 5.0.0) + - AEPServices (5.2.0) + - AEPSignal (5.2.0): + - AEPCore (< 6.0.0, >= 5.2.0) - SwiftLint (0.52.0) DEPENDENCIES: @@ -52,17 +52,17 @@ SPEC REPOS: SPEC CHECKSUMS: AEPAssurance: 7f260ded4df38a70a06efebade8c33a3e3221984 - AEPCore: f1c3e9238bb12e7e1103f4407c341ebc65aeab5b - AEPEdge: 0873041dfb29f3126260f2dc16d548a1fefbe0c4 + AEPCore: db53082c207c28166ed6aa9ae6262a55e95c78aa + AEPEdge: edf73ae8900016940cd7fcb29a89a576a1c6b0ae AEPEdgeConsent: d7db1d19eb4c1e2146360ed3c8df315f671b26d5 AEPEdgeIdentity: 3161ff33434586962946912d6b8e9e8fca1c4d23 - AEPIdentity: a65c1eba43a06f01b0dab191b27a53a81adada57 - AEPLifecycle: d4e0e1e86d6225d87203875d67f56c48f7ab7f67 + AEPIdentity: 3c597ef3d734d726f6a48ae10cdecb36fbeb28ca + AEPLifecycle: 0e2ddb26751320b88ea7471e63751f38c4ccdc10 AEPRulesEngine: fe5800653a4bee07b1e41e61b4d5551f0dba557b - AEPServices: e42e5118128e81c0f797fdfb1dc9c4a714d644b8 - AEPSignal: b146a3d4e5af51ff588f4f1ffbd40f1541325143 + AEPServices: d959143d13fde7e8464c19527df6baacdef765ce + AEPSignal: 3b9052359e25bdea9fa677955ed20db7702e17d5 SwiftLint: 13280e21cdda6786ad908dc6e416afe5acd1fcb7 PODFILE CHECKSUM: 6c01a16ccaa4ad210fcb2a0841c0df8d98fd1f78 -COCOAPODS: 1.15.0 +COCOAPODS: 1.15.2 diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 53c4425..77e9509 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -22,8 +22,8 @@ public extension Optimize { /// - Parameter decisionScopes: An array of decision scopes. /// - Parameter xdm: Additional XDM-formatted data to be sent in the personalization request. /// - Parameter data: Additional free-form data to be sent in the personalization request. - @objc(updatePropositions:withXdm:andData:) - static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil) { + @objc(updatePropositions:withXdm:andData:completion:) + static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil,_ completion: ((Bool, Error?) -> Void)? = nil) { let flattenedDecisionScopes = decisionScopes .filter { $0.isValid } .compactMap { $0.asDictionary() } @@ -54,7 +54,17 @@ public extension Optimize { source: EventSource.requestContent, data: eventData) - MobileCore.dispatch(event: event) + MobileCore.dispatch(event: event, timeout: 10) { responseEvent in + guard let responseEvent = responseEvent else { + completion?(false, AEPError.callbackTimeout) + return + } + if let error = responseEvent.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] as? AEPError { + completion?(false, error) + return + } + completion?(true, nil) + } } /// This API retrieves the previously fetched decisions for the provided decision scopes from the in-memory extension cache. diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 43ea518..8e4eef3 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -225,10 +225,19 @@ public class Optimize: NSObject, Extension { // response event failed or timed out, remove this event's ID from the requested event IDs dictionary and kick-off queue. self.updateRequestEventIdsInProgress.removeValue(forKey: edgeEvent.id.uuidString) self.propositionsInProgress.removeAll() - + self.dispatch(event: event.createErrorResponseEvent(AEPError.callbackTimeout)) self.eventsQueue.start() return } + + + let responseEventToSend = event.createResponseEvent( + name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + type: EventType.optimize, + source: EventSource.responseContent, + data: [ OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId ] + ) + self.dispatch(event: responseEventToSend) let updateCompleteEvent = responseEvent.createChainedEvent(name: OptimizeConstants.EventNames.OPTIMIZE_UPDATE_COMPLETE, type: EventType.optimize, From d965939de8ba6059573acde59b81c89f95fbc50a Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Wed, 21 Aug 2024 15:16:08 +0530 Subject: [PATCH 02/39] MOB-19932 - Get proposition handling update --- Sources/AEPOptimize/Optimize.swift | 54 +++-- .../OptimizeFunctionalTests.swift | 194 ++++++++++++++++++ 2 files changed, 234 insertions(+), 14 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 43ea518..1ab80c8 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -80,20 +80,9 @@ public class Optimize: NSObject, Extension { } public func onRegistered() { - registerListener(type: EventType.optimize, source: EventSource.requestContent) { event in - if event.isUpdateEvent { - self.processUpdatePropositions(event: event) - } else if event.isGetEvent { - // Queue the get propositions event in internal events queue to ensure any prior update requests are completed - // before it is processed. - self.eventsQueue.add(event) - } else if event.isTrackEvent { - self.processTrackPropositions(event: event) - } else { - Log.warning(label: OptimizeConstants.LOG_TAG, "Ignoring event! Cannot determine the type of request event.") - return - } - } + registerListener(type: EventType.optimize, + source: EventSource.requestContent, + listener: processOptimizeRequestContent(event:)) registerListener(type: EventType.edge, source: OptimizeConstants.EventSource.EDGE_PERSONALIZATION_DECISIONS, @@ -139,6 +128,43 @@ public class Optimize: NSObject, Extension { return true } + /// Processes the propositions request event, dispatched with type `EventType.optimize` and source `EventSource.requestContent`. + /// + /// It processes events based on the type of the requested event type + /// - Parameter event: propositions request event + private func processOptimizeRequestContent(event: Event) { + if event.isUpdateEvent { + processUpdatePropositions(event: event) + } else if event.isGetEvent { + guard let decisionScopes: [DecisionScope] = event.getTypedData(for: OptimizeConstants.EventDataKeys.DECISION_SCOPES), + !decisionScopes.isEmpty + else { + /// If decision scopes are not present in the event data, then adding it to the event queue + eventsQueue.add(event) + Log.warning(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or empty in the event data.") + return + } + /// Fetch propositions and check if all of the decision scopes are present in the cache + let fetchedPropositions = decisionScopes.filter { self.cachedPropositions.keys.contains($0) } + /// Check if the decision scopes are currently in progress in `updateRequestEventIdsInProgress` + let scopesInProgress = decisionScopes.filter { scope in + updateRequestEventIdsInProgress.values.flatMap { $0 }.contains(scope) + } + if decisionScopes.count == fetchedPropositions.count && scopesInProgress.isEmpty { + processGetPropositions(event: event) + } else { + /// Not all decision scopes are present in the cache or requested scopes are currently in progress, adding it to the event queue + eventsQueue.add(event) + Log.warning(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or currently in progress.") + } + } else if event.isTrackEvent { + processTrackPropositions(event: event) + } else { + Log.warning(label: OptimizeConstants.LOG_TAG, "Ignoring event! Cannot determine the type of request event.") + return + } + } + // MARK: Event Listeners /// Processes the update propositions request event, dispatched with type `EventType.optimize` and source `EventSource.requestContent`. diff --git a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift index 2c0412b..76c1681 100644 --- a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift +++ b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift @@ -847,6 +847,200 @@ class OptimizeFunctionalTests: XCTestCase { XCTAssertEqual("true", proposition?.offers[0].characteristics?["testing"]) } + func testGetPropositions_dispatchPropositionFromCacheBeforeNextUpdate() throws { + /// Setup + let decisionScopeA = DecisionScope(name: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYSIsInBsYWNlbWVudElkIjoic2NvcGUtYV9wbGFjZW1lbnQifQ.KW1HKVJHTTdmUkJZUmM5UEhNdURtOGdT") + + let decisionScopeB = DecisionScope(name: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYiIsInBsYWNlbWVudElkIjoic2NvcGUtYl9wbGFjZW1lbnQifQ.QzNxT1dBZ1Z1M0Z5dW84SjdKak1nY2c1") + + let propositionA = """ + { + "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "scope": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYSIsInBsYWNlbWVudElkIjoic2NvcGUtYV9wbGFjZW1lbnQifQ.KW1HKVJHTTdmUkJZUmM5UEhNdURtOGdT" + } + """.data(using: .utf8)! + + guard let propositionForScopeA = try? JSONDecoder().decode(OptimizeProposition.self, from: propositionA) else { + XCTFail("Propositions should be valid.") + return + } + + /// Cache decisionScopeA + optimize.cachedPropositions = [ + decisionScopeA: propositionForScopeA + ] + + let getEvent = Event( + name: "Get Propositions Request", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.requestContent", + data: [ + "requesttype": "getpropositions", + "decisionscopes": [ + ["name": decisionScopeA.name] + ] + ] + ) + + let updateEvent = Event( + name: "Update Propositions Request", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.requestContent", + data: [ + "requesttype": "updatepropositions", + "decisionscopes": [ + ["name": decisionScopeB.name] + ] + ] + ) + + mockRuntime.simulateSharedState(for: ("com.adobe.module.configuration", updateEvent), + data: ([ + "edge.configId": "ffffffff-ffff-ffff-ffff-ffffffffffff"] as [String: Any], .set)) + /// Dispatch the Update event + mockRuntime.simulateComingEvents(updateEvent) + + let updateEventIdsInProgress = optimize.getUpdateRequestEventIdsInProgress() + XCTAssertEqual(1, updateEventIdsInProgress.count) + + let optimizeContentComplete = Event( + name: "Optimize Update Propositions Complete", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.contentComplete", + data: [ + "completedUpdateRequestForEventId": updateEvent.id.uuidString + ] + ) + + let expectationGet = XCTestExpectation(description: "Get event should be processed first.") + + var dispatchedEvents = [Event]() + + mockRuntime.onEventDispatch = { event in + dispatchedEvents.append(event) + expectationGet.fulfill() + } + + /// Dispatch the events + mockRuntime.simulateComingEvents(getEvent) + mockRuntime.simulateComingEvents(optimizeContentComplete) + + /// Verify + wait(for: [expectationGet], timeout: 5) + + XCTAssertEqual(mockRuntime.firstEvent?.type, "com.adobe.eventType.optimize") + XCTAssertEqual(mockRuntime.firstEvent?.source, "com.adobe.eventSource.responseContent") + + guard let propositionsDictionary: [DecisionScope: OptimizeProposition] = mockRuntime.firstEvent?.getTypedData(for: "propositions") else { + XCTFail("Propositions dictionary should be valid.") + return + } + + XCTAssertEqual(propositionsDictionary[decisionScopeA]?.id, optimize.cachedPropositions[decisionScopeA]?.id) + XCTAssertEqual("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", propositionsDictionary[decisionScopeA]?.id) + XCTAssertEqual("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYSIsInBsYWNlbWVudElkIjoic2NvcGUtYV9wbGFjZW1lbnQifQ.KW1HKVJHTTdmUkJZUmM5UEhNdURtOGdT", propositionsDictionary[decisionScopeA]?.scope) + } + + func testGetPropositions_ScopesFromEventIsUpdateInProgress() { + // Setup + let decisionScopeA = DecisionScope(name: "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==") + + let cachedPropositionJSON = """ + { + "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "scope": "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==" + } + """.data(using: .utf8)! + + let updatedPropositionJSON = """ + { + "id": "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", + "scope": "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==" + } + """.data(using: .utf8)! + + guard let cachedPropositionForScopeA = try? JSONDecoder().decode(OptimizeProposition.self, from: cachedPropositionJSON), + let updatedPropositionForScopeA = try? JSONDecoder().decode(OptimizeProposition.self, from: updatedPropositionJSON) else { + XCTFail("Propositions should be valid.") + return + } + + /// Same Decision Scope is already cached with old propositions + optimize.cachedPropositions = [ + decisionScopeA: cachedPropositionForScopeA + ] + + let updateEvent = Event( + name: "Optimize Update Propositions Request", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.requestContent", + data: [ + "requesttype": "updatepropositions", + "decisionscopes": [ + ["name": decisionScopeA.name] + ] + ] + ) + + let getEvent = Event( + name: "Optimize Get Propositions Request", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.requestContent", + data: [ + "requesttype": "getpropositions", + "decisionscopes": [ + ["name": decisionScopeA.name] + ] + ] + ) + + mockRuntime.simulateSharedState(for: ("com.adobe.module.configuration", updateEvent), + data: ([ + "edge.configId": "ffffffff-ffff-ffff-ffff-ffffffffffff"] as [String: Any], .set)) + + /// Simulating update & get events + mockRuntime.simulateComingEvents(updateEvent) + mockRuntime.simulateComingEvents(getEvent) + + XCTAssertTrue(mockRuntime.dispatchedEvents.isEmpty) + + let optimizeContentComplete = Event( + name: "Optimize Update Propositions Complete", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.contentComplete", + data: [ + "completedUpdateRequestForEventId": updateEvent.id.uuidString, + "propositions" : [updatedPropositionJSON] + ] + ) + + mockRuntime.simulateComingEvents(optimizeContentComplete) + optimize.cachedPropositions[decisionScopeA] = updatedPropositionForScopeA + + /// After the update is complete, the get event should now dispatch + let expectation = XCTestExpectation(description: "Get propositions request should now dispatch response event after update completion.") + + mockRuntime.onEventDispatch = { event in + expectation.fulfill() + } + + wait(for: [expectation], timeout: 10) + XCTAssertEqual(mockRuntime.dispatchedEvents.count, 1) + + let dispatchedEvent = mockRuntime.dispatchedEvents.first + XCTAssertEqual(dispatchedEvent?.type, "com.adobe.eventType.optimize") + XCTAssertEqual(dispatchedEvent?.source, "com.adobe.eventSource.responseContent") + + /// Validate that the Get proposition response contains the updated proposition + guard let propositionsDictionary: [DecisionScope: OptimizeProposition] = dispatchedEvent?.getTypedData(for: "propositions") else { + XCTFail("Propositions dictionary should be valid.") + return + } + XCTAssertEqual(propositionsDictionary[decisionScopeA]?.id, updatedPropositionForScopeA.id) + XCTAssertEqual("BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", propositionsDictionary[decisionScopeA]?.id) + XCTAssertEqual("eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", propositionsDictionary[decisionScopeA]?.scope) + } + func testGetPropositions_notAllDecisionScopesInCache() { // setup let propositionsData = From dc0be28bedf5ff5fc325f4ef0243a0ad20d2a5ad Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Wed, 21 Aug 2024 19:51:37 +0530 Subject: [PATCH 03/39] fixed bug as typecasting error.rawValue which is an Integer to an AEPError will never succeed --- Sources/AEPOptimize/Event+Optimize.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AEPOptimize/Event+Optimize.swift b/Sources/AEPOptimize/Event+Optimize.swift index 85d18ed..4f4cc14 100644 --- a/Sources/AEPOptimize/Event+Optimize.swift +++ b/Sources/AEPOptimize/Event+Optimize.swift @@ -65,7 +65,7 @@ extension Event { type: EventType.optimize, source: EventSource.responseContent, data: [ - OptimizeConstants.EventDataKeys.RESPONSE_ERROR: error.rawValue + OptimizeConstants.EventDataKeys.RESPONSE_ERROR: error ]) } } From a722582a5493db028a5f34ffd33c960cde5df055 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 22 Aug 2024 16:30:18 +0530 Subject: [PATCH 04/39] updated valid edge response updateProposition's test with callback functionality --- .../IntegrationTests/OptimizeIntegrationTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index cd76df9..1b07bc5 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -105,7 +105,10 @@ class OptimizeIntegrationTests: XCTestCase { placementId: "xcore:offer-placement:1111111111111111") // update propositions - Optimize.updatePropositions(for: [decisionScope], withXdm: nil) + Optimize.updatePropositions(for: [decisionScope], withXdm: nil){ (status,error) in + XCTAssertTrue(status) + requestExpectation.fulfill() + } wait(for: [requestExpectation], timeout: 2) } From 46007b5c0c6d971c5c3af71fc5695c3a4df5f528 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 22 Aug 2024 17:05:37 +0530 Subject: [PATCH 05/39] Delete Podfile.lock --- Podfile.lock | 68 ---------------------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 Podfile.lock diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 4c0376c..0000000 --- a/Podfile.lock +++ /dev/null @@ -1,68 +0,0 @@ -PODS: - - AEPAssurance (5.0.0): - - AEPCore (< 6.0.0, >= 5.0.0) - - AEPServices (< 6.0.0, >= 5.0.0) - - AEPCore (5.2.0): - - AEPRulesEngine (< 6.0.0, >= 5.0.0) - - AEPServices (< 6.0.0, >= 5.2.0) - - AEPEdge (5.0.2): - - AEPCore (< 6.0.0, >= 5.1.0) - - AEPEdgeIdentity (< 6.0.0, >= 5.0.0) - - AEPEdgeConsent (5.0.0): - - AEPCore (< 6.0.0, >= 5.0.0) - - AEPEdge (< 6.0.0, >= 5.0.0) - - AEPEdgeIdentity (5.0.0): - - AEPCore (< 6.0.0, >= 5.0.0) - - AEPIdentity (5.2.0): - - AEPCore (< 6.0.0, >= 5.2.0) - - AEPLifecycle (5.2.0): - - AEPCore (< 6.0.0, >= 5.2.0) - - AEPRulesEngine (5.0.0) - - AEPServices (5.2.0) - - AEPSignal (5.2.0): - - AEPCore (< 6.0.0, >= 5.2.0) - - SwiftLint (0.52.0) - -DEPENDENCIES: - - AEPAssurance - - AEPCore - - AEPEdge - - AEPEdgeConsent - - AEPEdgeIdentity - - AEPIdentity - - AEPLifecycle - - AEPRulesEngine - - AEPServices - - AEPSignal - - SwiftLint (= 0.52.0) - -SPEC REPOS: - trunk: - - AEPAssurance - - AEPCore - - AEPEdge - - AEPEdgeConsent - - AEPEdgeIdentity - - AEPIdentity - - AEPLifecycle - - AEPRulesEngine - - AEPServices - - AEPSignal - - SwiftLint - -SPEC CHECKSUMS: - AEPAssurance: 7f260ded4df38a70a06efebade8c33a3e3221984 - AEPCore: db53082c207c28166ed6aa9ae6262a55e95c78aa - AEPEdge: edf73ae8900016940cd7fcb29a89a576a1c6b0ae - AEPEdgeConsent: d7db1d19eb4c1e2146360ed3c8df315f671b26d5 - AEPEdgeIdentity: 3161ff33434586962946912d6b8e9e8fca1c4d23 - AEPIdentity: 3c597ef3d734d726f6a48ae10cdecb36fbeb28ca - AEPLifecycle: 0e2ddb26751320b88ea7471e63751f38c4ccdc10 - AEPRulesEngine: fe5800653a4bee07b1e41e61b4d5551f0dba557b - AEPServices: d959143d13fde7e8464c19527df6baacdef765ce - AEPSignal: 3b9052359e25bdea9fa677955ed20db7702e17d5 - SwiftLint: 13280e21cdda6786ad908dc6e416afe5acd1fcb7 - -PODFILE CHECKSUM: 6c01a16ccaa4ad210fcb2a0841c0df8d98fd1f78 - -COCOAPODS: 1.15.2 From 83e2768e4d02933108d375cd454f3769956cad92 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 22 Aug 2024 17:50:01 +0530 Subject: [PATCH 06/39] updated failed test case due to bugfix change also linting changes --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 2 +- Sources/AEPOptimize/Optimize.swift | 4 +--- .../FunctionalTests/OptimizeFunctionalTests.swift | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 77e9509..ce35a6d 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -23,7 +23,7 @@ public extension Optimize { /// - Parameter xdm: Additional XDM-formatted data to be sent in the personalization request. /// - Parameter data: Additional free-form data to be sent in the personalization request. @objc(updatePropositions:withXdm:andData:completion:) - static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil,_ completion: ((Bool, Error?) -> Void)? = nil) { + static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: ((Bool, Error?) -> Void)? = nil) { let flattenedDecisionScopes = decisionScopes .filter { $0.isValid } .compactMap { $0.asDictionary() } diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 8e4eef3..9194923 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -229,9 +229,7 @@ public class Optimize: NSObject, Extension { self.eventsQueue.start() return } - - - let responseEventToSend = event.createResponseEvent( + let responseEventToSend = event.createResponseEvent( name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, type: EventType.optimize, source: EventSource.responseContent, diff --git a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift index 2c0412b..080f923 100644 --- a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift +++ b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift @@ -1096,7 +1096,7 @@ class OptimizeFunctionalTests: XCTestCase { let dispatchedEvent = mockRuntime.dispatchedEvents.first XCTAssertEqual("com.adobe.eventType.optimize", dispatchedEvent?.type) XCTAssertEqual("com.adobe.eventSource.responseContent", dispatchedEvent?.source) - XCTAssertEqual(AEPError.invalidRequest, AEPError(rawValue: dispatchedEvent?.data?["responseerror"] as? Int ?? 1000)) + XCTAssertEqual(AEPError.invalidRequest, dispatchedEvent?.data?["responseerror"] as! AEPError) XCTAssertNil(dispatchedEvent?.data?["propositions"]) } From 9012e541dd9464a3adcceeed887a013934746c2c Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 22 Aug 2024 17:57:22 +0530 Subject: [PATCH 07/39] minor change to pass linting check --- Sources/AEPOptimize/Optimize.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 9194923..acc0215 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -230,11 +230,11 @@ public class Optimize: NSObject, Extension { return } let responseEventToSend = event.createResponseEvent( - name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, - type: EventType.optimize, - source: EventSource.responseContent, - data: [ OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId ] - ) + name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + type: EventType.optimize, + source: EventSource.responseContent, + data: [ OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId ] + ) self.dispatch(event: responseEventToSend) let updateCompleteEvent = responseEvent.createChainedEvent(name: OptimizeConstants.EventNames.OPTIMIZE_UPDATE_COMPLETE, From 83da900a86d0a5dd6eb1de55f2ea5bce895b41c5 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 22 Aug 2024 18:08:41 +0530 Subject: [PATCH 08/39] minor linting change --- Sources/AEPOptimize/Optimize.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index acc0215..366e8db 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -233,9 +233,11 @@ public class Optimize: NSObject, Extension { name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, type: EventType.optimize, source: EventSource.responseContent, - data: [ OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId ] + data: [ + OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId + ] ) - self.dispatch(event: responseEventToSend) + self.dispatch(event: responseEventToSend) let updateCompleteEvent = responseEvent.createChainedEvent(name: OptimizeConstants.EventNames.OPTIMIZE_UPDATE_COMPLETE, type: EventType.optimize, From c1162772281d20a5a455e5b313cab1118ec5c39b Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 22 Aug 2024 18:26:38 +0530 Subject: [PATCH 09/39] minor space linting change --- Sources/AEPOptimize/Optimize.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 366e8db..7b2458b 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -233,7 +233,7 @@ public class Optimize: NSObject, Extension { name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, type: EventType.optimize, source: EventSource.responseContent, - data: [ + data: [ OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId ] ) From 3ea520d166fd859e2d52469e18b0235470d1240f Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Thu, 22 Aug 2024 20:43:41 +0530 Subject: [PATCH 10/39] Minor logs & naming changes --- Sources/AEPOptimize/Optimize.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 1ab80c8..14a5fb7 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -136,26 +136,26 @@ public class Optimize: NSObject, Extension { if event.isUpdateEvent { processUpdatePropositions(event: event) } else if event.isGetEvent { - guard let decisionScopes: [DecisionScope] = event.getTypedData(for: OptimizeConstants.EventDataKeys.DECISION_SCOPES), - !decisionScopes.isEmpty + guard let eventDecisionScopes: [DecisionScope] = event.getTypedData(for: OptimizeConstants.EventDataKeys.DECISION_SCOPES), + !eventDecisionScopes.isEmpty else { /// If decision scopes are not present in the event data, then adding it to the event queue eventsQueue.add(event) - Log.warning(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or empty in the event data.") + Log.trace(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or empty in the event data.") return } /// Fetch propositions and check if all of the decision scopes are present in the cache - let fetchedPropositions = decisionScopes.filter { self.cachedPropositions.keys.contains($0) } + let fetchedPropositions = eventDecisionScopes.filter { self.cachedPropositions.keys.contains($0) } /// Check if the decision scopes are currently in progress in `updateRequestEventIdsInProgress` - let scopesInProgress = decisionScopes.filter { scope in + let scopesInProgress = eventDecisionScopes.filter { scope in updateRequestEventIdsInProgress.values.flatMap { $0 }.contains(scope) } - if decisionScopes.count == fetchedPropositions.count && scopesInProgress.isEmpty { + if eventDecisionScopes.count == fetchedPropositions.count && scopesInProgress.isEmpty { processGetPropositions(event: event) } else { /// Not all decision scopes are present in the cache or requested scopes are currently in progress, adding it to the event queue eventsQueue.add(event) - Log.warning(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or currently in progress.") + Log.trace(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or currently in progress.") } } else if event.isTrackEvent { processTrackPropositions(event: event) From ca462b6276d3ac0c3b39b885d54bffcc88bd71f3 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Fri, 23 Aug 2024 18:32:54 +0530 Subject: [PATCH 11/39] fixed failed test case for bugfix --- Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift b/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift index 9003ca4..5f4a8de 100644 --- a/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift @@ -124,6 +124,6 @@ class Event_OptimizeTests: XCTestCase { XCTAssertEqual("com.adobe.eventSource.responseContent", errorResponseEvent.source) XCTAssertNotNil(errorResponseEvent.data) XCTAssertEqual(1, errorResponseEvent.data?.count) - XCTAssertEqual(6, errorResponseEvent.data?["responseerror"] as? Int) + XCTAssertEqual(AEPError.invalidRequest, errorResponseEvent.data?["responseerror"] as! AEPError) } } From e06e507f596f6a03281c196ffbd5f818953af6ad Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Fri, 23 Aug 2024 19:15:06 +0530 Subject: [PATCH 12/39] added few relevant comments --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 1 + Sources/AEPOptimize/Optimize.swift | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index ce35a6d..73d23bf 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -22,6 +22,7 @@ public extension Optimize { /// - Parameter decisionScopes: An array of decision scopes. /// - Parameter xdm: Additional XDM-formatted data to be sent in the personalization request. /// - Parameter data: Additional free-form data to be sent in the personalization request. + /// - Parameter completion: An optional callback to be used by consumer @objc(updatePropositions:withXdm:andData:completion:) static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: ((Bool, Error?) -> Void)? = nil) { let flattenedDecisionScopes = decisionScopes diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 7b2458b..b73aa6f 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -222,13 +222,15 @@ public class Optimize: NSObject, Extension { let responseEvent = responseEvent, let requestEventId = responseEvent.requestEventId else { - // response event failed or timed out, remove this event's ID from the requested event IDs dictionary and kick-off queue. + // response event failed or timed out, remove this event's ID from the requested event IDs dictionary, dispatch an error response event and kick-off queue. self.updateRequestEventIdsInProgress.removeValue(forKey: edgeEvent.id.uuidString) self.propositionsInProgress.removeAll() self.dispatch(event: event.createErrorResponseEvent(AEPError.callbackTimeout)) self.eventsQueue.start() return } + + // response event to provide success callback to updateProposition public api let responseEventToSend = event.createResponseEvent( name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, type: EventType.optimize, From 9de838bbcb0ef2fe1735ef317d9da71605852b26 Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Mon, 26 Aug 2024 21:11:26 +0530 Subject: [PATCH 13/39] Dispatch error if decision scopes are not present in the event --- Sources/AEPOptimize/Optimize.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 14a5fb7..7f432ee 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -130,7 +130,7 @@ public class Optimize: NSObject, Extension { /// Processes the propositions request event, dispatched with type `EventType.optimize` and source `EventSource.requestContent`. /// - /// It processes events based on the type of the requested event type + /// It processes events based on the "requesttype" in the event data /// - Parameter event: propositions request event private func processOptimizeRequestContent(event: Event) { if event.isUpdateEvent { @@ -139,9 +139,8 @@ public class Optimize: NSObject, Extension { guard let eventDecisionScopes: [DecisionScope] = event.getTypedData(for: OptimizeConstants.EventDataKeys.DECISION_SCOPES), !eventDecisionScopes.isEmpty else { - /// If decision scopes are not present in the event data, then adding it to the event queue - eventsQueue.add(event) - Log.trace(label: OptimizeConstants.LOG_TAG, "Decision scopes are either not present or empty in the event data.") + Log.debug(label: OptimizeConstants.LOG_TAG, "Decision scopes, in event data, is either not present or empty.") + dispatch(event: event.createErrorResponseEvent(AEPError.invalidRequest)) return } /// Fetch propositions and check if all of the decision scopes are present in the cache From fe4df017fc9ec173558bcca4a3b53a8ec2d5cdac Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Mon, 26 Aug 2024 21:12:36 +0530 Subject: [PATCH 14/39] Added a test for partially cached scopes for a get proposition --- .../OptimizeFunctionalTests.swift | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift index 76c1681..6b3103c 100644 --- a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift +++ b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift @@ -1041,6 +1041,113 @@ class OptimizeFunctionalTests: XCTestCase { XCTAssertEqual("eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", propositionsDictionary[decisionScopeA]?.scope) } + func testGetPropositions_fewDecisionScopesNotInCacheAndGetToBeQueued() { + /// Setup + let decisionScopeA = DecisionScope(name: "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==") + let decisionScopeB = DecisionScope(name: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYiIsInBsYWNlbWVudElkIjoic2NvcGUtYl9wbGFjZW1lbnQifQ.QzNxT1dBZ1Z1M0Z5dW84SjdKak1nY2c1") + + let propositionA = """ + { + "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "scope": "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==" + } + """.data(using: .utf8)! + + let propositionB = """ + { + "id": "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", + "scope": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYiIsInBsYWNlbWVudElkIjoic2NvcGUtYl9wbGFjZW1lbnQifQ.QzNxT1dBZ1Z1M0Z5dW84SjdKak1nY2c1" + } + """.data(using: .utf8)! + + guard let cachedPropositionForScopeA = try? JSONDecoder().decode(OptimizeProposition.self, from: propositionA), + let cachedPropositionForScopeB = try? JSONDecoder().decode(OptimizeProposition.self, from: propositionB) else { + XCTFail("Propositions should be valid.") + return + } + + /// decisionScopeA is already cached. + optimize.cachedPropositions = [ + decisionScopeA: cachedPropositionForScopeA + ] + + /// Creating a get event with a decisionScopeB that is currently not present in the cache. + let getEvent = Event( + name: "Optimize Get Propositions Request", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.requestContent", + data: [ + "requesttype": "getpropositions", + "decisionscopes": [ + ["name": decisionScopeA.name], + ["name": decisionScopeB.name] + ] + ] + ) + + /// Update event with decisionScopeB. + let updateEvent = Event( + name: "Optimize Update Propositions Request", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.requestContent", + data: [ + "requesttype": "updatepropositions", + "decisionscopes": [ + ["name": decisionScopeB.name] + ] + ] + ) + + mockRuntime.simulateSharedState(for: ("com.adobe.module.configuration", updateEvent), + data: ([ + "edge.configId": "ffffffff-ffff-ffff-ffff-ffffffffffff"] as [String: Any], .set)) + + /// Dispatching the update event. + mockRuntime.simulateComingEvents(updateEvent) + optimize.setUpdateRequestEventIdsInProgress(updateEvent.id.uuidString, expectedScopes: [decisionScopeB]) + optimize.setPropositionsInProgress([decisionScopeB : cachedPropositionForScopeB]) + + let optimizeContentComplete = Event( + name: "Optimize Update Propositions Complete", + type: "com.adobe.eventType.optimize", + source: "com.adobe.eventSource.contentComplete", + data: [ + "completedUpdateRequestForEventId": updateEvent.id.uuidString, + "propositions" : [propositionB] + ] + ) + + let expectationGet = XCTestExpectation(description: "Get event should be queued.") + mockRuntime.onEventDispatch = { event in + expectationGet.fulfill() + } + + /// Dispatch the get event Immediately. + mockRuntime.simulateComingEvents(getEvent) + + /// Dispatching the proposition complete event after a delay of 1 second to simulate a real use case. + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {[weak self] in + self?.mockRuntime.simulateComingEvents(optimizeContentComplete) + }) + + wait(for: [expectationGet], timeout: 10) + + /// Verify that the get proposition event was queued & is the last event to be executed. + XCTAssertEqual(mockRuntime.firstEvent?.type, "com.adobe.eventType.optimize") + XCTAssertEqual(mockRuntime.firstEvent?.source, "com.adobe.eventSource.responseContent") + + guard let propositionsDictionary: [DecisionScope: OptimizeProposition] = mockRuntime.firstEvent?.getTypedData(for: "propositions") else { + XCTFail("Propositions dictionary should be valid.") + return + } + + /// Verify that the proposition for decisionScopeB is present in the reponse event as well as in the cache. + XCTAssertTrue(propositionsDictionary.keys.contains(decisionScopeB)) + XCTAssertEqual("BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", propositionsDictionary[decisionScopeB]?.id) + XCTAssertEqual("BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", optimize.cachedPropositions[decisionScopeB]?.id) + XCTAssertEqual(propositionsDictionary[decisionScopeB]?.id, optimize.cachedPropositions[decisionScopeB]?.id) + } + func testGetPropositions_notAllDecisionScopesInCache() { // setup let propositionsData = From 0a0ad96546f6dcf82be43f6d5743a3d8e3bb0173 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Wed, 28 Aug 2024 15:56:52 +0530 Subject: [PATCH 15/39] Restored Podfile.lock --- Podfile.lock | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Podfile.lock diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..58f6e1d --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,68 @@ +PODS: + - AEPAssurance (5.0.0): + - AEPCore (< 6.0.0, >= 5.0.0) + - AEPServices (< 6.0.0, >= 5.0.0) + - AEPCore (5.0.0): + - AEPRulesEngine (< 6.0.0, >= 5.0.0) + - AEPServices (< 6.0.0, >= 5.0.0) + - AEPEdge (5.0.1): + - AEPCore (< 6.0.0, >= 5.0.0) + - AEPEdgeIdentity (< 6.0.0, >= 5.0.0) + - AEPEdgeConsent (5.0.0): + - AEPCore (< 6.0.0, >= 5.0.0) + - AEPEdge (< 6.0.0, >= 5.0.0) + - AEPEdgeIdentity (5.0.0): + - AEPCore (< 6.0.0, >= 5.0.0) + - AEPIdentity (5.0.0): + - AEPCore (< 6.0.0, >= 5.0.0) + - AEPLifecycle (5.0.0): + - AEPCore (< 6.0.0, >= 5.0.0) + - AEPRulesEngine (5.0.0) + - AEPServices (5.0.0) + - AEPSignal (5.0.0): + - AEPCore (< 6.0.0, >= 5.0.0) + - SwiftLint (0.52.0) + +DEPENDENCIES: + - AEPAssurance + - AEPCore + - AEPEdge + - AEPEdgeConsent + - AEPEdgeIdentity + - AEPIdentity + - AEPLifecycle + - AEPRulesEngine + - AEPServices + - AEPSignal + - SwiftLint (= 0.52.0) + +SPEC REPOS: + trunk: + - AEPAssurance + - AEPCore + - AEPEdge + - AEPEdgeConsent + - AEPEdgeIdentity + - AEPIdentity + - AEPLifecycle + - AEPRulesEngine + - AEPServices + - AEPSignal + - SwiftLint + +SPEC CHECKSUMS: + AEPAssurance: 7f260ded4df38a70a06efebade8c33a3e3221984 + AEPCore: f1c3e9238bb12e7e1103f4407c341ebc65aeab5b + AEPEdge: 0873041dfb29f3126260f2dc16d548a1fefbe0c4 + AEPEdgeConsent: d7db1d19eb4c1e2146360ed3c8df315f671b26d5 + AEPEdgeIdentity: 3161ff33434586962946912d6b8e9e8fca1c4d23 + AEPIdentity: a65c1eba43a06f01b0dab191b27a53a81adada57 + AEPLifecycle: d4e0e1e86d6225d87203875d67f56c48f7ab7f67 + AEPRulesEngine: fe5800653a4bee07b1e41e61b4d5551f0dba557b + AEPServices: e42e5118128e81c0f797fdfb1dc9c4a714d644b8 + AEPSignal: b146a3d4e5af51ff588f4f1ffbd40f1541325143 + SwiftLint: 13280e21cdda6786ad908dc6e416afe5acd1fcb7 + +PODFILE CHECKSUM: 6c01a16ccaa4ad210fcb2a0841c0df8d98fd1f78 + +COCOAPODS: 1.15.0 From f293e42111972e5d7a7112937e8591ebe9da5b8c Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 29 Aug 2024 15:05:34 +0530 Subject: [PATCH 16/39] updated update propositions public api to return list of successful decision scopes and AEPOptimizeError --- AEPOptimize.xcodeproj/project.pbxproj | 4 ++ Sources/AEPOptimize/Event+Optimize.swift | 12 ++++++ Sources/AEPOptimize/Optimize+PublicAPI.swift | 21 +++++----- Sources/AEPOptimize/Optimize.swift | 16 +++++--- Sources/AEPOptimize/OptimizeError.swift | 39 +++++++++++++++++++ .../OptimizeIntegrationTests.swift | 4 +- 6 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 Sources/AEPOptimize/OptimizeError.swift diff --git a/AEPOptimize.xcodeproj/project.pbxproj b/AEPOptimize.xcodeproj/project.pbxproj index 1c996bc..5c12c0f 100644 --- a/AEPOptimize.xcodeproj/project.pbxproj +++ b/AEPOptimize.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 78AF6DBB284FD9AA0022EB24 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78AF6DB9284FD9AA0022EB24 /* MainInterface.storyboard */; }; 78AF6DBF284FD9AA0022EB24 /* AEPOptimizeDemoAppExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 78AF6DB5284FD9AA0022EB24 /* AEPOptimizeDemoAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 79AF0BBA3B4B3F0D2B46C847 /* Pods_AEPOptimize.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89FA9F1584C29AFB176C28B9 /* Pods_AEPOptimize.framework */; }; + B6B401C52C8052210082B3FE /* OptimizeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B401C42C8052210082B3FE /* OptimizeError.swift */; }; C8076C4D265EE786006BEC5D /* TargetProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8076C3F265EE768006BEC5D /* TargetProduct.swift */; }; C8076C52265EE789006BEC5D /* TargetOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8076C46265EE776006BEC5D /* TargetOrder.swift */; }; C8076C57265EE78E006BEC5D /* DictionaryRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8076C45265EE776006BEC5D /* DictionaryRowView.swift */; }; @@ -187,6 +188,7 @@ AB699BC71E1531AB6A02B9AD /* Pods-shared-AEPOptimizeDemoAppExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-AEPOptimizeDemoAppExtension.debug.xcconfig"; path = "Target Support Files/Pods-shared-AEPOptimizeDemoAppExtension/Pods-shared-AEPOptimizeDemoAppExtension.debug.xcconfig"; sourceTree = ""; }; AB8BAF0E948D2E2747418C4E /* Pods-FunctionalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FunctionalTests.debug.xcconfig"; path = "Target Support Files/Pods-FunctionalTests/Pods-FunctionalTests.debug.xcconfig"; sourceTree = ""; }; B588A06BD3941420A1C1CBC5 /* Pods-AEPOptimizeDemoObjC.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AEPOptimizeDemoObjC.debug.xcconfig"; path = "Target Support Files/Pods-AEPOptimizeDemoObjC/Pods-AEPOptimizeDemoObjC.debug.xcconfig"; sourceTree = ""; }; + B6B401C42C8052210082B3FE /* OptimizeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizeError.swift; sourceTree = ""; }; BEA1EA2DAE5E666C7DEEA655 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; C2AB724370848D9EA2ABD4AA /* Pods-AEPOptimizeDemoAppExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AEPOptimizeDemoAppExtension.release.xcconfig"; path = "Target Support Files/Pods-AEPOptimizeDemoAppExtension/Pods-AEPOptimizeDemoAppExtension.release.xcconfig"; sourceTree = ""; }; C592630F611720CFBE4F0009 /* Pods_AEPOptimizeDemoObjC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AEPOptimizeDemoObjC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -517,6 +519,7 @@ C88C5F1126152DAB003AE3DE /* DecisionScope.swift */, C88C5EDC2614F640003AE3DE /* Offer.swift */, C8DE6B692684181A0076623F /* Offer+Tracking.swift */, + B6B401C42C8052210082B3FE /* OptimizeError.swift */, C88C5EE02614F693003AE3DE /* OfferType.swift */, C88C5F0D26152CFF003AE3DE /* OptimizeProposition.swift */, C8DE6BEE2686848A0076623F /* Proposition+Tracking.swift */, @@ -1224,6 +1227,7 @@ C88C5F0E26152CFF003AE3DE /* OptimizeProposition.swift in Sources */, C88C5F1226152DAB003AE3DE /* DecisionScope.swift in Sources */, C88C5E952613249C003AE3DE /* OptimizeConstants.swift in Sources */, + B6B401C52C8052210082B3FE /* OptimizeError.swift in Sources */, C88C5EE12614F693003AE3DE /* OfferType.swift in Sources */, C88C5F4126187F0B003AE3DE /* String+Optimize.swift in Sources */, C8276BE4261EC74F00508873 /* Array+Optimize.swift in Sources */, diff --git a/Sources/AEPOptimize/Event+Optimize.swift b/Sources/AEPOptimize/Event+Optimize.swift index 4f4cc14..0d7a492 100644 --- a/Sources/AEPOptimize/Event+Optimize.swift +++ b/Sources/AEPOptimize/Event+Optimize.swift @@ -68,4 +68,16 @@ extension Event { OptimizeConstants.EventDataKeys.RESPONSE_ERROR: error ]) } + + /// Creates a response event with specified AEPOptimizeError type added in the Event data. + /// - Parameter error: type of AEPOptimizeError + /// - Returns: error response Event + func createErrorResponseEvent(_ error: AEPOptimizeError) -> Event { + createResponseEvent(name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + type: EventType.optimize, + source: EventSource.responseContent, + data: [ + OptimizeConstants.EventDataKeys.RESPONSE_ERROR: error + ]) + } } diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 73d23bf..1b3c9b7 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -22,9 +22,9 @@ public extension Optimize { /// - Parameter decisionScopes: An array of decision scopes. /// - Parameter xdm: Additional XDM-formatted data to be sent in the personalization request. /// - Parameter data: Additional free-form data to be sent in the personalization request. - /// - Parameter completion: An optional callback to be used by consumer + /// - Parameter completion: Optional completion handler invoked with list of successful decision scopes and errors, if any @objc(updatePropositions:withXdm:andData:completion:) - static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: ((Bool, Error?) -> Void)? = nil) { + static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: (([DecisionScope]?, AEPOptimizeError?) -> Void)? = nil) { let flattenedDecisionScopes = decisionScopes .filter { $0.isValid } .compactMap { $0.asDictionary() } @@ -54,17 +54,20 @@ public extension Optimize { type: EventType.optimize, source: EventSource.requestContent, data: eventData) - MobileCore.dispatch(event: event, timeout: 10) { responseEvent in guard let responseEvent = responseEvent else { - completion?(false, AEPError.callbackTimeout) - return - } - if let error = responseEvent.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] as? AEPError { - completion?(false, error) + let timeoutError = AEPOptimizeError( + type: nil, + status: 408, + title: "Request Timeout", + detail: "Update proposition request resulted in a timeout.", + aepError: AEPError.callbackTimeout) + completion?(nil, timeoutError) return } - completion?(true, nil) + let result = responseEvent.data?[OptimizeConstants.EventDataKeys.DECISION_SCOPES] as? [DecisionScope] + let error = responseEvent.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] as? AEPOptimizeError + completion?(result, error) } } diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index b73aa6f..c7169ed 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -216,8 +216,8 @@ public class Optimize: NSObject, Extension { // add the Edge event to update propositions in the events queue. eventsQueue.add(edgeEvent) - // Increase timeout to 5s to ensure edge requests have enough time to complete. - MobileCore.dispatch(event: edgeEvent, timeout: 5) { responseEvent in + // Increase timeout to 10s to ensure edge requests have enough time to complete. + MobileCore.dispatch(event: edgeEvent, timeout: 10) { responseEvent in guard let responseEvent = responseEvent, let requestEventId = responseEvent.requestEventId @@ -225,18 +225,24 @@ public class Optimize: NSObject, Extension { // response event failed or timed out, remove this event's ID from the requested event IDs dictionary, dispatch an error response event and kick-off queue. self.updateRequestEventIdsInProgress.removeValue(forKey: edgeEvent.id.uuidString) self.propositionsInProgress.removeAll() - self.dispatch(event: event.createErrorResponseEvent(AEPError.callbackTimeout)) + let timeoutError = AEPOptimizeError( + type: nil, + status: 408, + title: "Request Timeout", + detail: "Update proposition request resulted in a timeout.", + aepError: AEPError.callbackTimeout) + self.dispatch(event: event.createErrorResponseEvent(timeoutError)) self.eventsQueue.start() return } - + // response event to provide success callback to updateProposition public api let responseEventToSend = event.createResponseEvent( name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, type: EventType.optimize, source: EventSource.responseContent, data: [ - OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID: requestEventId + OptimizeConstants.EventDataKeys.DECISION_SCOPES: Array(self.propositionsInProgress.keys) ] ) self.dispatch(event: responseEventToSend) diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift new file mode 100644 index 0000000..4e65c16 --- /dev/null +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -0,0 +1,39 @@ +// Delete this line +/* +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. +*/ + +import AEPCore +import Foundation + +/// AEPOptimizeError class used to create AEPOptimizeError from error details received from Experience Edge. +@objc(AEPOptimizeError) +public class AEPOptimizeError: NSObject { + let type: String? + let status: Int? + let title: String? + let detail: String? + var aepError = AEPError.none + + public init(type: String?, status: Int?, title: String?, detail: String?, aepError: AEPError? = nil) { + self.type = type + self.status = status + self.title = title + self.detail = detail + if let aepError { + self.aepError = aepError + } else { + if status == 400 { + self.aepError = .networkError + } + } + } +} diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index 1b07bc5..e2c5d4f 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -105,8 +105,8 @@ class OptimizeIntegrationTests: XCTestCase { placementId: "xcore:offer-placement:1111111111111111") // update propositions - Optimize.updatePropositions(for: [decisionScope], withXdm: nil){ (status,error) in - XCTAssertTrue(status) + Optimize.updatePropositions(for: [decisionScope], withXdm: nil){ (scope,error) in + XCTAssertNotNil(scope) requestExpectation.fulfill() } From ae822b2d19abfaf86fdd431575c5c41c8a111087 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 29 Aug 2024 17:49:19 +0530 Subject: [PATCH 17/39] Synced with 5.0.2 branch | updated test cases. --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 3 ++- Sources/AEPOptimize/Optimize.swift | 3 ++- Sources/AEPOptimize/OptimizeError.swift | 20 ++++++++--------- .../OptimizeFunctionalTests.swift | 22 +++++++++++-------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 1b3c9b7..f649d43 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -61,7 +61,8 @@ public extension Optimize { status: 408, title: "Request Timeout", detail: "Update proposition request resulted in a timeout.", - aepError: AEPError.callbackTimeout) + aepError: AEPError.callbackTimeout + ) completion?(nil, timeoutError) return } diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 4a3e7a5..a465046 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -255,7 +255,8 @@ public class Optimize: NSObject, Extension { status: 408, title: "Request Timeout", detail: "Update proposition request resulted in a timeout.", - aepError: AEPError.callbackTimeout) + aepError: AEPError.callbackTimeout + ) self.dispatch(event: event.createErrorResponseEvent(timeoutError)) self.eventsQueue.start() return diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 4e65c16..abc311d 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -1,15 +1,15 @@ // Delete this line /* -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. -*/ + 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. + */ import AEPCore import Foundation diff --git a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift index d7fe346..51d3611 100644 --- a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift +++ b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift @@ -1021,13 +1021,15 @@ class OptimizeFunctionalTests: XCTestCase { let expectation = XCTestExpectation(description: "Get propositions request should now dispatch response event after update completion.") mockRuntime.onEventDispatch = { event in - expectation.fulfill() + if event.responseID == getEvent.id { + expectation.fulfill() + } } - wait(for: [expectation], timeout: 10) - XCTAssertEqual(mockRuntime.dispatchedEvents.count, 1) + wait(for: [expectation], timeout: 12) + XCTAssertEqual(mockRuntime.dispatchedEvents.count, 2) - let dispatchedEvent = mockRuntime.dispatchedEvents.first + let dispatchedEvent = mockRuntime.secondEvent XCTAssertEqual(dispatchedEvent?.type, "com.adobe.eventType.optimize") XCTAssertEqual(dispatchedEvent?.source, "com.adobe.eventSource.responseContent") @@ -1119,7 +1121,9 @@ class OptimizeFunctionalTests: XCTestCase { let expectationGet = XCTestExpectation(description: "Get event should be queued.") mockRuntime.onEventDispatch = { event in - expectationGet.fulfill() + if event.responseID == getEvent.id { + expectationGet.fulfill() + } } /// Dispatch the get event Immediately. @@ -1130,13 +1134,13 @@ class OptimizeFunctionalTests: XCTestCase { self?.mockRuntime.simulateComingEvents(optimizeContentComplete) }) - wait(for: [expectationGet], timeout: 10) + wait(for: [expectationGet], timeout: 12) /// Verify that the get proposition event was queued & is the last event to be executed. - XCTAssertEqual(mockRuntime.firstEvent?.type, "com.adobe.eventType.optimize") - XCTAssertEqual(mockRuntime.firstEvent?.source, "com.adobe.eventSource.responseContent") + XCTAssertEqual(mockRuntime.secondEvent?.type, "com.adobe.eventType.optimize") + XCTAssertEqual(mockRuntime.secondEvent?.source, "com.adobe.eventSource.responseContent") - guard let propositionsDictionary: [DecisionScope: OptimizeProposition] = mockRuntime.firstEvent?.getTypedData(for: "propositions") else { + guard let propositionsDictionary: [DecisionScope: OptimizeProposition] = mockRuntime.secondEvent?.getTypedData(for: "propositions") else { XCTFail("Propositions dictionary should be valid.") return } From 117eb3619f9304960c9fbb58ebf5c203967951d9 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 29 Aug 2024 17:51:29 +0530 Subject: [PATCH 18/39] minor formatting change --- Sources/AEPOptimize/Optimize.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index a465046..225b9bc 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -3,7 +3,7 @@ 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 From 93fdd3109018e0a2059c994b8b48850cca5bf906 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 29 Aug 2024 17:53:58 +0530 Subject: [PATCH 19/39] minor code formatting --- Sources/AEPOptimize/Optimize.swift | 2 +- Sources/AEPOptimize/OptimizeError.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 225b9bc..a465046 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -3,7 +3,7 @@ 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 diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index abc311d..8905cef 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -4,7 +4,7 @@ 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 From dd12423e4aa6dd5364bd911785a3be8193d6e6b3 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 29 Aug 2024 19:12:23 +0530 Subject: [PATCH 20/39] added test cases for updateProposition api callback and verified for timeout --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 1 + Sources/AEPOptimize/OptimizeError.swift | 10 ++-- .../OptimizeIntegrationTests.swift | 60 +++++++++++++++++++ .../UnitTests/OptimizePublicAPITests.swift | 20 +++++++ 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index f649d43..31a0229 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -32,6 +32,7 @@ public extension Optimize { guard !flattenedDecisionScopes.isEmpty else { Log.warning(label: OptimizeConstants.LOG_TAG, "Cannot update propositions, provided decision scopes array is empty or has invalid items.") + completion?(nil,nil) return } diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 8905cef..8122f4d 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -17,11 +17,11 @@ import Foundation /// AEPOptimizeError class used to create AEPOptimizeError from error details received from Experience Edge. @objc(AEPOptimizeError) public class AEPOptimizeError: NSObject { - let type: String? - let status: Int? - let title: String? - let detail: String? - var aepError = AEPError.none + public let type: String? + public let status: Int? + public let title: String? + public let detail: String? + public var aepError = AEPError.none public init(type: String?, status: Int?, title: String?, detail: String?, aepError: AEPError? = nil) { self.type = type diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index e2c5d4f..9d73e25 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -54,6 +54,66 @@ class OptimizeIntegrationTests: XCTestCase { } wait(for: [initExpectation], timeout: 1) } + + func testUpdatePropositions_timeoutError() { + // setup + let timeoutResponse = HTTPURLResponse(url: URL(string: "https://edge.adobedc.net/ee/v1/interact?configId=configId&requestId=requestId")!, statusCode: 408, httpVersion: nil, headerFields: nil) + let responseString = """ + {\ + "requestId":"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",\ + "handle":[],\ + "errors":[\ + {\ + "type":"EXEG-0201-503",\ + "status":408,\ + "title":"Request timed out. Please try again."\ + }\ + ]\ + } + """ + + // mock edge response + let requestExpectation = XCTestExpectation(description: "Request for mock service response.") + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("edge.adobedc.net/ee/v1/interact?configId=configId") { + requestExpectation.fulfill() + return (data: responseString.data(using: .utf8), response: timeoutResponse, error: nil) + } + return (data: nil, response: timeoutResponse, error: nil) + } + + // init extensions + initExtensionsAndWait() + + // update configuration + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "edge.configId": "configId"]) + + let decisionScope = DecisionScope(activityId: "xcore:offer-activity:1111111111111111", + placementId: "xcore:offer-placement:1111111111111111") + + let exp = expectation(description: "The Update Proposition should result in a time out") + + // test + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) {scope, error in + // verify + XCTAssertNil(scope) + XCTAssertNotNil(error) + XCTAssertTrue(error?.status == 408) + XCTAssertTrue(error?.aepError == .callbackTimeout) + XCTAssertTrue(error?.title == "Request Timeout") + XCTAssertTrue(error?.detail == "Update proposition request resulted in a timeout.") + exp.fulfill() + } + + wait(for: [exp, requestExpectation], timeout: 12) + } + func testUpdatePropositions_validEdgeRequest() { // setup diff --git a/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift b/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift index ea7e87c..704e1d6 100644 --- a/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift @@ -72,6 +72,26 @@ class OptimizePublicAPITests: XCTestCase { // verify wait(for: [expectation], timeout: 1) } + + func testUpdatePropositions_updateTimeout() { + // setup + let expectation = XCTestExpectation(description: "The Update proposition request should time out.") + expectation.assertForOverFulfill = true + + let decisionScope = DecisionScope(name: "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==") + + // test + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { scope, error in + XCTAssert(error?.status == 408) + XCTAssert(error?.title == "Request Timeout") + XCTAssert(error?.detail == "Update proposition request resulted in a timeout.") + XCTAssert(error?.aepError == .callbackTimeout) + expectation.fulfill() + } + + // verify + wait(for: [expectation], timeout: 11) + } func testUpdatePropositions_validDecisionScopeWithXdmAndData() { // setup From f1514bc39273f975a493258b555072eafc282c2c Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Fri, 30 Aug 2024 13:14:09 +0530 Subject: [PATCH 21/39] minor change and formatting --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 2 +- .../IntegrationTests/OptimizeIntegrationTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 31a0229..9a3cdb1 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -32,7 +32,7 @@ public extension Optimize { guard !flattenedDecisionScopes.isEmpty else { Log.warning(label: OptimizeConstants.LOG_TAG, "Cannot update propositions, provided decision scopes array is empty or has invalid items.") - completion?(nil,nil) + completion?(nil, nil) return } diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index 9d73e25..7b1a79c 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -64,13 +64,13 @@ class OptimizeIntegrationTests: XCTestCase { "handle":[],\ "errors":[\ {\ - "type":"EXEG-0201-503",\ + "type":"EXEG-0201-408",\ "status":408,\ "title":"Request timed out. Please try again."\ }\ ]\ } - """ + """ // mock edge response let requestExpectation = XCTestExpectation(description: "Request for mock service response.") From 2b65030761dac7dc00a25ea0c68d57c97ba64968 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Mon, 2 Sep 2024 16:03:00 +0530 Subject: [PATCH 22/39] mapped aepError values in OptimizeError class on basis of status and changed default value of aepError to unexpected --- Sources/AEPOptimize/OptimizeError.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 8122f4d..312f0a2 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -21,7 +21,7 @@ public class AEPOptimizeError: NSObject { public let status: Int? public let title: String? public let detail: String? - public var aepError = AEPError.none + public var aepError = AEPError.unexpected public init(type: String?, status: Int?, title: String?, detail: String?, aepError: AEPError? = nil) { self.type = type @@ -31,7 +31,13 @@ public class AEPOptimizeError: NSObject { if let aepError { self.aepError = aepError } else { - if status == 400 { + if status == 408 { + self.aepError = .callbackTimeout + } else if status == 400 || status == 403 || status == 404 { + self.aepError = .invalidRequest + } else if status == 429 || status == 500 || status == 503 { + self.aepError = .serverError + } else if status == 502 || status == 504 { self.aepError = .networkError } } From 841c1b822b8fc08729280d5b212a680757086349 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Mon, 9 Sep 2024 20:13:25 +0530 Subject: [PATCH 23/39] updated updateProposition api to return [DecisionScope: OptimizeProposition]? instead of [DecisionScope]? in callback and code review changes --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 10 +++++----- Sources/AEPOptimize/Optimize.swift | 8 ++++---- Sources/AEPOptimize/OptimizeConstants.swift | 11 +++++++++++ Sources/AEPOptimize/OptimizeError.swift | 2 +- .../FunctionalTests/OptimizeFunctionalTests.swift | 14 ++++++++++++++ .../OptimizeIntegrationTests.swift | 11 ++++++----- .../UnitTests/OptimizePublicAPITests.swift | 9 +++++---- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 9a3cdb1..124cb75 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -24,7 +24,7 @@ public extension Optimize { /// - Parameter data: Additional free-form data to be sent in the personalization request. /// - Parameter completion: Optional completion handler invoked with list of successful decision scopes and errors, if any @objc(updatePropositions:withXdm:andData:completion:) - static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: (([DecisionScope]?, AEPOptimizeError?) -> Void)? = nil) { + static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: (([DecisionScope: OptimizeProposition]?, AEPOptimizeError?) -> Void)? = nil) { let flattenedDecisionScopes = decisionScopes .filter { $0.isValid } .compactMap { $0.asDictionary() } @@ -59,15 +59,15 @@ public extension Optimize { guard let responseEvent = responseEvent else { let timeoutError = AEPOptimizeError( type: nil, - status: 408, - title: "Request Timeout", - detail: "Update proposition request resulted in a timeout.", + status: OptimizeConstants.ErrorData.Timeout.STATUS, + title: OptimizeConstants.ErrorData.Timeout.TITLE, + detail: OptimizeConstants.ErrorData.Timeout.DETAIL, aepError: AEPError.callbackTimeout ) completion?(nil, timeoutError) return } - let result = responseEvent.data?[OptimizeConstants.EventDataKeys.DECISION_SCOPES] as? [DecisionScope] + let result = responseEvent.data?[OptimizeConstants.EventDataKeys.PROPOSITIONS] as? [DecisionScope: OptimizeProposition] let error = responseEvent.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] as? AEPOptimizeError completion?(result, error) } diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index a465046..f69a6c6 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -252,9 +252,9 @@ public class Optimize: NSObject, Extension { self.propositionsInProgress.removeAll() let timeoutError = AEPOptimizeError( type: nil, - status: 408, - title: "Request Timeout", - detail: "Update proposition request resulted in a timeout.", + status: OptimizeConstants.ErrorData.Timeout.STATUS, + title: OptimizeConstants.ErrorData.Timeout.TITLE, + detail: OptimizeConstants.ErrorData.Timeout.DETAIL, aepError: AEPError.callbackTimeout ) self.dispatch(event: event.createErrorResponseEvent(timeoutError)) @@ -268,7 +268,7 @@ public class Optimize: NSObject, Extension { type: EventType.optimize, source: EventSource.responseContent, data: [ - OptimizeConstants.EventDataKeys.DECISION_SCOPES: Array(self.propositionsInProgress.keys) + OptimizeConstants.EventDataKeys.PROPOSITIONS: self.propositionsInProgress ] ) self.dispatch(event: responseEventToSend) diff --git a/Sources/AEPOptimize/OptimizeConstants.swift b/Sources/AEPOptimize/OptimizeConstants.swift index 0968d2a..52b98a0 100644 --- a/Sources/AEPOptimize/OptimizeConstants.swift +++ b/Sources/AEPOptimize/OptimizeConstants.swift @@ -69,7 +69,10 @@ enum OptimizeConstants { static let PAYLOAD = "payload" enum ErrorKeys { static let TYPE = "type" + static let STATUS = "status" + static let TITLE = "title" static let DETAIL = "detail" + static let REPORT = "report" } } @@ -119,4 +122,12 @@ enum OptimizeConstants { static let SCHEMA_OFFER_IMAGE = "https://ns.adobe.com/experience/offer-management/content-component-imagelink" static let SCHEMA_OFFER_TEXT = "https://ns.adobe.com/experience/offer-management/content-component-text" } + + enum ErrorData { + enum Timeout { + static let STATUS = 408 + static let TITLE = "Request Timeout" + static let DETAIL = "Update/Get proposition request resulted in a timeout." + } + } } diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 312f0a2..a669808 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -16,7 +16,7 @@ import Foundation /// AEPOptimizeError class used to create AEPOptimizeError from error details received from Experience Edge. @objc(AEPOptimizeError) -public class AEPOptimizeError: NSObject { +public class AEPOptimizeError: NSObject, Error { public let type: String? public let status: Int? public let title: String? diff --git a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift index 51d3611..7bd4690 100644 --- a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift +++ b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift @@ -1029,6 +1029,13 @@ class OptimizeFunctionalTests: XCTestCase { wait(for: [expectation], timeout: 12) XCTAssertEqual(mockRuntime.dispatchedEvents.count, 2) + // first event will be a timeout error response event as in functional test we don't recieve response from edge + let firstEvent = mockRuntime.firstEvent + XCTAssertEqual(firstEvent?.type, "com.adobe.eventType.optimize") + XCTAssertEqual(firstEvent?.source, "com.adobe.eventSource.responseContent") + XCTAssertNotNil(firstEvent?.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR]) + XCTAssertNil(firstEvent?.data?[OptimizeConstants.EventDataKeys.DECISION_SCOPES]) + let dispatchedEvent = mockRuntime.secondEvent XCTAssertEqual(dispatchedEvent?.type, "com.adobe.eventType.optimize") XCTAssertEqual(dispatchedEvent?.source, "com.adobe.eventSource.responseContent") @@ -1136,6 +1143,13 @@ class OptimizeFunctionalTests: XCTestCase { wait(for: [expectationGet], timeout: 12) + // first event will be a timeout error response event as in functional test we don't recieve response from edge + let firstEvent = mockRuntime.firstEvent + XCTAssertEqual(firstEvent?.type, "com.adobe.eventType.optimize") + XCTAssertEqual(firstEvent?.source, "com.adobe.eventSource.responseContent") + XCTAssertNotNil(firstEvent?.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR]) + XCTAssertNil(firstEvent?.data?[OptimizeConstants.EventDataKeys.DECISION_SCOPES]) + /// Verify that the get proposition event was queued & is the last event to be executed. XCTAssertEqual(mockRuntime.secondEvent?.type, "com.adobe.eventType.optimize") XCTAssertEqual(mockRuntime.secondEvent?.source, "com.adobe.eventSource.responseContent") diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index 7b1a79c..560a2a8 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -100,14 +100,14 @@ class OptimizeIntegrationTests: XCTestCase { let exp = expectation(description: "The Update Proposition should result in a time out") // test - Optimize.updatePropositions(for: [decisionScope], withXdm: nil) {scope, error in + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) {propositions, error in // verify - XCTAssertNil(scope) + XCTAssertNil(propositions) XCTAssertNotNil(error) XCTAssertTrue(error?.status == 408) XCTAssertTrue(error?.aepError == .callbackTimeout) XCTAssertTrue(error?.title == "Request Timeout") - XCTAssertTrue(error?.detail == "Update proposition request resulted in a timeout.") + XCTAssertTrue(error?.detail == "Update/Get proposition request resulted in a timeout.") exp.fulfill() } @@ -165,8 +165,9 @@ class OptimizeIntegrationTests: XCTestCase { placementId: "xcore:offer-placement:1111111111111111") // update propositions - Optimize.updatePropositions(for: [decisionScope], withXdm: nil){ (scope,error) in - XCTAssertNotNil(scope) + Optimize.updatePropositions(for: [decisionScope], withXdm: nil){ (propositions,error) in + XCTAssertNotNil(propositions) + XCTAssertNil(error) requestExpectation.fulfill() } diff --git a/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift b/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift index 704e1d6..7b5d80e 100644 --- a/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift @@ -81,10 +81,11 @@ class OptimizePublicAPITests: XCTestCase { let decisionScope = DecisionScope(name: "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==") // test - Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { scope, error in - XCTAssert(error?.status == 408) - XCTAssert(error?.title == "Request Timeout") - XCTAssert(error?.detail == "Update proposition request resulted in a timeout.") + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { propositions, error in + XCTAssert(error?.status == OptimizeConstants.ErrorData.Timeout.STATUS) + XCTAssert(error?.aepError == .callbackTimeout) + XCTAssert(error?.title == OptimizeConstants.ErrorData.Timeout.TITLE) + XCTAssert(error?.detail == OptimizeConstants.ErrorData.Timeout.DETAIL) XCTAssert(error?.aepError == .callbackTimeout) expectation.fulfill() } From 421a6efaad46c9b7d66f5c711086bdc9b9288ee3 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Tue, 10 Sep 2024 22:43:42 +0530 Subject: [PATCH 24/39] removed createErrorResponseEvent(_ error: AEPError) as now we will have AEPOptimizeError as argument and minor changes --- Sources/AEPOptimize/Event+Optimize.swift | 12 ------------ Sources/AEPOptimize/Optimize+PublicAPI.swift | 17 ++++++++++++----- Sources/AEPOptimize/Optimize.swift | 18 ++++++++++++++++-- Sources/AEPOptimize/OptimizeConstants.swift | 6 ++++++ .../OptimizeFunctionalTests.swift | 3 ++- .../OptimizeIntegrationTests.swift | 12 ++++++++---- .../UnitTests/Event+OptimizeTests.swift | 15 +++++++++++++-- .../UnitTests/OptimizePublicAPITests.swift | 14 +++++++++----- 8 files changed, 66 insertions(+), 31 deletions(-) diff --git a/Sources/AEPOptimize/Event+Optimize.swift b/Sources/AEPOptimize/Event+Optimize.swift index 0d7a492..901870f 100644 --- a/Sources/AEPOptimize/Event+Optimize.swift +++ b/Sources/AEPOptimize/Event+Optimize.swift @@ -57,18 +57,6 @@ extension Event { return try? JSONDecoder().decode(T.self, from: jsonData) } - /// Creates a response event with specified AEPError type added in the Event data. - /// - Parameter error: type of AEPError - /// - Returns: error response Event - func createErrorResponseEvent(_ error: AEPError) -> Event { - createResponseEvent(name: OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, - type: EventType.optimize, - source: EventSource.responseContent, - data: [ - OptimizeConstants.EventDataKeys.RESPONSE_ERROR: error - ]) - } - /// Creates a response event with specified AEPOptimizeError type added in the Event data. /// - Parameter error: type of AEPOptimizeError /// - Returns: error response Event diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index 124cb75..f6b69e1 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -22,9 +22,9 @@ public extension Optimize { /// - Parameter decisionScopes: An array of decision scopes. /// - Parameter xdm: Additional XDM-formatted data to be sent in the personalization request. /// - Parameter data: Additional free-form data to be sent in the personalization request. - /// - Parameter completion: Optional completion handler invoked with list of successful decision scopes and errors, if any + /// - Parameter completion: Optional completion handler invoked with map of successful decision scopes to propositions and errors, if any @objc(updatePropositions:withXdm:andData:completion:) - static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: (([DecisionScope: OptimizeProposition]?, AEPOptimizeError?) -> Void)? = nil) { + static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil, _ completion: (([DecisionScope: OptimizeProposition]?, Error?) -> Void)? = nil) { let flattenedDecisionScopes = decisionScopes .filter { $0.isValid } .compactMap { $0.asDictionary() } @@ -32,7 +32,14 @@ public extension Optimize { guard !flattenedDecisionScopes.isEmpty else { Log.warning(label: OptimizeConstants.LOG_TAG, "Cannot update propositions, provided decision scopes array is empty or has invalid items.") - completion?(nil, nil) + let aepOptimizeError = AEPOptimizeError( + type: nil, + status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, + title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, + detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, + aepError: AEPError.invalidRequest + ) + completion?(nil, aepOptimizeError) return } @@ -109,8 +116,8 @@ public extension Optimize { return } - if let error = responseEvent.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] as? AEPError { - completion(nil, error) + if let error = responseEvent.data?[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] as? AEPOptimizeError { + completion(nil, error.aepError) return } diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index f69a6c6..062c59b 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -140,7 +140,14 @@ public class Optimize: NSObject, Extension { !eventDecisionScopes.isEmpty else { Log.debug(label: OptimizeConstants.LOG_TAG, "Decision scopes, in event data, is either not present or empty.") - dispatch(event: event.createErrorResponseEvent(AEPError.invalidRequest)) + let aepOptimizeError = AEPOptimizeError( + type: nil, + status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, + title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, + detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, + aepError: AEPError.invalidRequest + ) + dispatch(event: event.createErrorResponseEvent(aepOptimizeError)) return } /// Fetch propositions and check if all of the decision scopes are present in the cache @@ -409,7 +416,14 @@ public class Optimize: NSObject, Extension { !decisionScopes.isEmpty else { Log.debug(label: OptimizeConstants.LOG_TAG, "Decision scopes, in event data, is either not present or empty.") - dispatch(event: event.createErrorResponseEvent(AEPError.invalidRequest)) + let aepOptimizeError = AEPOptimizeError( + type: nil, + status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, + title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, + detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, + aepError: AEPError.invalidRequest + ) + dispatch(event: event.createErrorResponseEvent(aepOptimizeError)) return } diff --git a/Sources/AEPOptimize/OptimizeConstants.swift b/Sources/AEPOptimize/OptimizeConstants.swift index 52b98a0..b013a41 100644 --- a/Sources/AEPOptimize/OptimizeConstants.swift +++ b/Sources/AEPOptimize/OptimizeConstants.swift @@ -129,5 +129,11 @@ enum OptimizeConstants { static let TITLE = "Request Timeout" static let DETAIL = "Update/Get proposition request resulted in a timeout." } + + enum InvalidRequest { + static let STATUS = 400 + static let TITLE = "Invalid Request" + static let DETAIL = "Decision scopes, in event data, is either not present or empty." + } } } diff --git a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift index 7bd4690..f94724c 100644 --- a/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift +++ b/Tests/AEPOptimizeTests/FunctionalTests/OptimizeFunctionalTests.swift @@ -1413,9 +1413,10 @@ class OptimizeFunctionalTests: XCTestCase { XCTAssertEqual(1, mockRuntime.dispatchedEvents.count) let dispatchedEvent = mockRuntime.dispatchedEvents.first + let errorData = dispatchedEvent?.data?["responseerror"] as? AEPOptimizeError XCTAssertEqual("com.adobe.eventType.optimize", dispatchedEvent?.type) XCTAssertEqual("com.adobe.eventSource.responseContent", dispatchedEvent?.source) - XCTAssertEqual(AEPError.invalidRequest, dispatchedEvent?.data?["responseerror"] as! AEPError) + XCTAssertEqual(AEPError.invalidRequest, errorData?.aepError) XCTAssertNil(dispatchedEvent?.data?["propositions"]) } diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index 560a2a8..8301823 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -102,12 +102,16 @@ class OptimizeIntegrationTests: XCTestCase { // test Optimize.updatePropositions(for: [decisionScope], withXdm: nil) {propositions, error in // verify + guard let error = error as? AEPOptimizeError else { + XCTFail("Type mismatch in error received for Update Propositions") + return + } XCTAssertNil(propositions) XCTAssertNotNil(error) - XCTAssertTrue(error?.status == 408) - XCTAssertTrue(error?.aepError == .callbackTimeout) - XCTAssertTrue(error?.title == "Request Timeout") - XCTAssertTrue(error?.detail == "Update/Get proposition request resulted in a timeout.") + XCTAssertTrue(error.status == 408) + XCTAssertTrue(error.aepError == .callbackTimeout) + XCTAssertTrue(error.title == "Request Timeout") + XCTAssertTrue(error.detail == "Update/Get proposition request resulted in a timeout.") exp.fulfill() } diff --git a/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift b/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift index 5f4a8de..2d3adcf 100644 --- a/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift @@ -117,13 +117,24 @@ class Event_OptimizeTests: XCTestCase { source: "com.adobe.eventSource.requestContent", data: nil) - let errorResponseEvent = testEvent.createErrorResponseEvent(AEPError.invalidRequest) + let aepOptimizeError = AEPOptimizeError( + type: nil, + status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, + title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, + detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, + aepError: AEPError.invalidRequest + ) + let errorResponseEvent = testEvent.createErrorResponseEvent(aepOptimizeError) + + let errorData = errorResponseEvent.data?["responseerror"] as? AEPOptimizeError XCTAssertEqual("Optimize Response", errorResponseEvent.name) XCTAssertEqual("com.adobe.eventType.optimize", errorResponseEvent.type) XCTAssertEqual("com.adobe.eventSource.responseContent", errorResponseEvent.source) XCTAssertNotNil(errorResponseEvent.data) XCTAssertEqual(1, errorResponseEvent.data?.count) - XCTAssertEqual(AEPError.invalidRequest, errorResponseEvent.data?["responseerror"] as! AEPError) + XCTAssertEqual(400, errorData?.status) + XCTAssertEqual("Invalid Request", errorData?.title) + XCTAssertEqual(AEPError.invalidRequest, errorData?.aepError) } } diff --git a/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift b/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift index 7b5d80e..6899a28 100644 --- a/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/OptimizePublicAPITests.swift @@ -82,11 +82,15 @@ class OptimizePublicAPITests: XCTestCase { // test Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { propositions, error in - XCTAssert(error?.status == OptimizeConstants.ErrorData.Timeout.STATUS) - XCTAssert(error?.aepError == .callbackTimeout) - XCTAssert(error?.title == OptimizeConstants.ErrorData.Timeout.TITLE) - XCTAssert(error?.detail == OptimizeConstants.ErrorData.Timeout.DETAIL) - XCTAssert(error?.aepError == .callbackTimeout) + guard let error = error as? AEPOptimizeError else { + XCTFail("Type mismatch in error received for Update Propositions") + return + } + XCTAssert(error.status == OptimizeConstants.ErrorData.Timeout.STATUS) + XCTAssert(error.aepError == .callbackTimeout) + XCTAssert(error.title == OptimizeConstants.ErrorData.Timeout.TITLE) + XCTAssert(error.detail == OptimizeConstants.ErrorData.Timeout.DETAIL) + XCTAssert(error.aepError == .callbackTimeout) expectation.fulfill() } From 98cc12dae836ce2a4b57b85081e665bd2b6a45f0 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 12 Sep 2024 19:34:24 +0530 Subject: [PATCH 25/39] providing error details received from edge error response in update proposition api callback. --- Sources/AEPOptimize/Event+Optimize.swift | 5 ++ Sources/AEPOptimize/Optimize.swift | 56 ++++++++++++++++++++- Sources/AEPOptimize/OptimizeConstants.swift | 10 ++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/Event+Optimize.swift b/Sources/AEPOptimize/Event+Optimize.swift index 901870f..3d8cefa 100644 --- a/Sources/AEPOptimize/Event+Optimize.swift +++ b/Sources/AEPOptimize/Event+Optimize.swift @@ -18,6 +18,11 @@ import Foundation extension Event { // MARK: - AEP Response Event handle + /// Verify event type and source for Edge error response event. + var isEdgeErrorResponseEvent: Bool { + type == EventType.edge && source == OptimizeConstants.EventSource.EDGE_ERROR_RESPONSE + } + /// Verify event type and source for Edge personalization:decisions event. var isPersonalizationDecisionResponse: Bool { type == EventType.edge && source == OptimizeConstants.EventSource.EDGE_PERSONALIZATION_DECISIONS diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 062c59b..5656424 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -32,6 +32,13 @@ public class Optimize: NSObject, Extension { /// Dispatch queue used to protect against simultaneous access of our containers from multiple threads private let queue: DispatchQueue = .init(label: "com.adobe.optimize.containers.queue") + /// a dictionary containing the update event IDs and corresponding errors as received from Edge SDK + private var _errorUpdateRequestEventIds: [String: AEPOptimizeError] = [:] + private var errorUpdateRequestEventIds: [String: AEPOptimizeError] { + get { queue.sync { self._errorUpdateRequestEventIds } } + set { queue.async { self._errorUpdateRequestEventIds = newValue } } + } + /// a dictionary containing the update event IDs (and corresponding requested scopes) for Edge events that haven't yet received an Edge completion response. private var _updateRequestEventIdsInProgress: [String: [DecisionScope]] = [:] private var updateRequestEventIdsInProgress: [String: [DecisionScope]] { @@ -60,6 +67,13 @@ public class Optimize: NSObject, Extension { } #endif + /// Array containing recoverable network error codes being retried by Edge Network Service + private let recoverableNetworkErrorCodes: [Int] = [OptimizeConstants.RecoverableHttpResponseCodes.clientTimeout.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.tooManyRequests.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.badGateway.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.serviceUnavailable.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.gatewayTimeout.rawValue] + /// Array containing the schema strings for the proposition items supported by the SDK, sent in the personalization query request. static let supportedSchemas = [ // Target schemas @@ -268,6 +282,8 @@ public class Optimize: NSObject, Extension { self.eventsQueue.start() return } + // Error response received for Edge request event UUID (if any) + let edgeError = self.errorUpdateRequestEventIds[requestEventId] // response event to provide success callback to updateProposition public api let responseEventToSend = event.createResponseEvent( @@ -275,7 +291,8 @@ public class Optimize: NSObject, Extension { type: EventType.optimize, source: EventSource.responseContent, data: [ - OptimizeConstants.EventDataKeys.PROPOSITIONS: self.propositionsInProgress + OptimizeConstants.EventDataKeys.PROPOSITIONS: self.propositionsInProgress, + OptimizeConstants.EventDataKeys.RESPONSE_ERROR: edgeError as Any ] ) self.dispatch(event: responseEventToSend) @@ -395,16 +412,43 @@ public class Optimize: NSObject, Extension { /// It logs error related information specifying error type along with a detailed message. /// - Parameter event: Edge error response event. private func processEdgeErrorResponse(event: Event) { + guard + event.isEdgeErrorResponseEvent, + let requestEventId = event.requestEventId, + updateRequestEventIdsInProgress.contains(where: { $0.key == requestEventId }) + else { + Log.debug(label: OptimizeConstants.LOG_TAG, + """ + Ignoring Edge event, either handle type is not errorResponseContent, or the response isn't intended for this extension. + """) + return + } let errorType = event.data?[OptimizeConstants.Edge.ErrorKeys.TYPE] as? String + let errorStatus = event.data?[OptimizeConstants.Edge.ErrorKeys.STATUS] as? Int + let errorTitle = event.data?[OptimizeConstants.Edge.ErrorKeys.TITLE] as? String let errorDetail = event.data?[OptimizeConstants.Edge.ErrorKeys.DETAIL] as? String + let errorReport = event.data?[OptimizeConstants.Edge.ErrorKeys.REPORT] as? [String: Any] let errorString = """ Decisioning Service error, type: \(errorType ?? OptimizeConstants.ERROR_UNKNOWN), \ - detail: \(errorDetail ?? OptimizeConstants.ERROR_UNKNOWN)" + status: \(errorStatus ?? OptimizeConstants.UNKNOWN_STATUS), \ + title: \(errorTitle ?? OptimizeConstants.ERROR_UNKNOWN), \ + detail: \(errorDetail ?? OptimizeConstants.ERROR_UNKNOWN), \ + report: \(errorReport ?? [:])" """ Log.warning(label: OptimizeConstants.LOG_TAG, errorString) + + if let errorStatus = errorStatus, shouldSuppressRecoverableError(status: errorStatus) { + let aepOptimizeError = AEPOptimizeError(type: errorType, status: errorStatus, title: errorTitle, detail: errorDetail) + guard let edgeEventRequestId = event.requestEventId else { + Log.debug(label: OptimizeConstants.LOG_TAG, "No valid edge event request ID found for error response event.") + return + } + // store the error response as an AEPOptimizeError in error dictionary per edge request + errorUpdateRequestEventIds[edgeEventRequestId] = aepOptimizeError + } } /// Processes the get propositions request event, dispatched with type `EventType.optimize` and source `EventSource.requestContent`. @@ -486,6 +530,14 @@ public class Optimize: NSObject, Extension { cachedPropositions.removeAll() } + /// Helper function to check if edge error response received should be suppressed as it is already being retried on Edge + private func shouldSuppressRecoverableError(status: Int) -> Bool { + if recoverableNetworkErrorCodes.contains(status) { + return false + } + return true + } + #if DEBUG /// For testing purposes only func setUpdateRequestEventIdsInProgress(_ eventId: String, expectedScopes: [DecisionScope]) { diff --git a/Sources/AEPOptimize/OptimizeConstants.swift b/Sources/AEPOptimize/OptimizeConstants.swift index b013a41..638b0dd 100644 --- a/Sources/AEPOptimize/OptimizeConstants.swift +++ b/Sources/AEPOptimize/OptimizeConstants.swift @@ -26,6 +26,7 @@ enum OptimizeConstants { static let XDM_ITEM_COUNT = "xdm:itemCount" static let ERROR_UNKNOWN = "unknown" + static let UNKNOWN_STATUS = 0 enum EventNames { static let UPDATE_PROPOSITIONS_REQUEST = "Optimize Update Propositions Request" @@ -136,4 +137,13 @@ enum OptimizeConstants { static let DETAIL = "Decision scopes, in event data, is either not present or empty." } } + + // enum containing http response codes being retried by Edge Network Service. + enum RecoverableHttpResponseCodes: Int { + case clientTimeout = 408 + case tooManyRequests = 429 + case badGateway = 502 + case serviceUnavailable = 503 + case gatewayTimeout = 504 + } } From 97f769f436c4fd46f714d433b5e8b85299cb64f1 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Mon, 16 Sep 2024 14:48:53 +0530 Subject: [PATCH 26/39] added updateProposition api without callback for backward compatibility and code optimization --- Sources/AEPOptimize/Optimize+PublicAPI.swift | 27 +++++++------- Sources/AEPOptimize/Optimize.swift | 24 ++----------- Sources/AEPOptimize/OptimizeConstants.swift | 13 +++++++ Sources/AEPOptimize/OptimizeError.swift | 36 ++++++++++++++++--- .../UnitTests/Event+OptimizeTests.swift | 8 +---- 5 files changed, 62 insertions(+), 46 deletions(-) diff --git a/Sources/AEPOptimize/Optimize+PublicAPI.swift b/Sources/AEPOptimize/Optimize+PublicAPI.swift index f6b69e1..eaf4c1d 100644 --- a/Sources/AEPOptimize/Optimize+PublicAPI.swift +++ b/Sources/AEPOptimize/Optimize+PublicAPI.swift @@ -16,6 +16,17 @@ import Foundation @objc public extension Optimize { + /// This API dispatches an Event for the Edge network extension to fetch decision propositions for the provided decision scopes from the decisioning Services enabled behind Experience Edge. + /// + /// The returned decision propositions are cached in memory in the Optimize SDK extension and can be retrieved using `getPropositions(for:_:)` API. + /// - Parameter decisionScopes: An array of decision scopes. + /// - Parameter xdm: Additional XDM-formatted data to be sent in the personalization request. + /// - Parameter data: Additional free-form data to be sent in the personalization request. + @objc(updatePropositions:withXdm:andData:) + static func updatePropositions(for decisionScopes: [DecisionScope], withXdm xdm: [String: Any]?, andData data: [String: Any]? = nil) { + updatePropositions(for: decisionScopes, withXdm: xdm, andData: data, nil) + } + /// This API dispatches an Event for the Edge network extension to fetch decision propositions for the provided decision scopes from the decisioning Services enabled behind Experience Edge. /// /// The returned decision propositions are cached in memory in the Optimize SDK extension and can be retrieved using `getPropositions(for:_:)` API. @@ -32,13 +43,7 @@ public extension Optimize { guard !flattenedDecisionScopes.isEmpty else { Log.warning(label: OptimizeConstants.LOG_TAG, "Cannot update propositions, provided decision scopes array is empty or has invalid items.") - let aepOptimizeError = AEPOptimizeError( - type: nil, - status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, - title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, - detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, - aepError: AEPError.invalidRequest - ) + let aepOptimizeError = AEPOptimizeError.createAEPOptimizInvalidRequestError() completion?(nil, aepOptimizeError) return } @@ -64,13 +69,7 @@ public extension Optimize { data: eventData) MobileCore.dispatch(event: event, timeout: 10) { responseEvent in guard let responseEvent = responseEvent else { - let timeoutError = AEPOptimizeError( - type: nil, - status: OptimizeConstants.ErrorData.Timeout.STATUS, - title: OptimizeConstants.ErrorData.Timeout.TITLE, - detail: OptimizeConstants.ErrorData.Timeout.DETAIL, - aepError: AEPError.callbackTimeout - ) + let timeoutError = AEPOptimizeError.createAEPOptimizeTimeoutError() completion?(nil, timeoutError) return } diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 062c59b..2d0129e 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -140,13 +140,7 @@ public class Optimize: NSObject, Extension { !eventDecisionScopes.isEmpty else { Log.debug(label: OptimizeConstants.LOG_TAG, "Decision scopes, in event data, is either not present or empty.") - let aepOptimizeError = AEPOptimizeError( - type: nil, - status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, - title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, - detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, - aepError: AEPError.invalidRequest - ) + let aepOptimizeError = AEPOptimizeError.createAEPOptimizInvalidRequestError() dispatch(event: event.createErrorResponseEvent(aepOptimizeError)) return } @@ -257,13 +251,7 @@ public class Optimize: NSObject, Extension { // response event failed or timed out, remove this event's ID from the requested event IDs dictionary, dispatch an error response event and kick-off queue. self.updateRequestEventIdsInProgress.removeValue(forKey: edgeEvent.id.uuidString) self.propositionsInProgress.removeAll() - let timeoutError = AEPOptimizeError( - type: nil, - status: OptimizeConstants.ErrorData.Timeout.STATUS, - title: OptimizeConstants.ErrorData.Timeout.TITLE, - detail: OptimizeConstants.ErrorData.Timeout.DETAIL, - aepError: AEPError.callbackTimeout - ) + let timeoutError = AEPOptimizeError.createAEPOptimizeTimeoutError() self.dispatch(event: event.createErrorResponseEvent(timeoutError)) self.eventsQueue.start() return @@ -416,13 +404,7 @@ public class Optimize: NSObject, Extension { !decisionScopes.isEmpty else { Log.debug(label: OptimizeConstants.LOG_TAG, "Decision scopes, in event data, is either not present or empty.") - let aepOptimizeError = AEPOptimizeError( - type: nil, - status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, - title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, - detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, - aepError: AEPError.invalidRequest - ) + let aepOptimizeError = AEPOptimizeError.createAEPOptimizInvalidRequestError() dispatch(event: event.createErrorResponseEvent(aepOptimizeError)) return } diff --git a/Sources/AEPOptimize/OptimizeConstants.swift b/Sources/AEPOptimize/OptimizeConstants.swift index b013a41..f532179 100644 --- a/Sources/AEPOptimize/OptimizeConstants.swift +++ b/Sources/AEPOptimize/OptimizeConstants.swift @@ -136,4 +136,17 @@ enum OptimizeConstants { static let DETAIL = "Decision scopes, in event data, is either not present or empty." } } + + enum HTTPResponseCodes: Int { + case success = 200 + case noContent = 204 + case multiStatus = 207 + case invalidRequest = 400 + case clientTimeout = 408 + case tooManyRequests = 429 + case internalServerError = 500 + case badGateway = 502 + case serviceUnavailable = 503 + case gatewayTimeout = 504 + } } diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index a669808..510d83d 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -17,6 +17,7 @@ import Foundation /// AEPOptimizeError class used to create AEPOptimizeError from error details received from Experience Edge. @objc(AEPOptimizeError) public class AEPOptimizeError: NSObject, Error { + typealias HTTPResponseCodes = OptimizeConstants.HTTPResponseCodes public let type: String? public let status: Int? public let title: String? @@ -31,15 +32,42 @@ public class AEPOptimizeError: NSObject, Error { if let aepError { self.aepError = aepError } else { - if status == 408 { + // map edge error response to AEPError on the basis of status (if received) + guard let status else { + return + } + if status == HTTPResponseCodes.clientTimeout.rawValue { self.aepError = .callbackTimeout - } else if status == 400 || status == 403 || status == 404 { + } else if (400...499).contains(status) && status != HTTPResponseCodes.tooManyRequests.rawValue { self.aepError = .invalidRequest - } else if status == 429 || status == 500 || status == 503 { + } else if status == HTTPResponseCodes.tooManyRequests.rawValue, + status == HTTPResponseCodes.internalServerError.rawValue, + status == HTTPResponseCodes.serviceUnavailable.rawValue { self.aepError = .serverError - } else if status == 502 || status == 504 { + } else if status == HTTPResponseCodes.badGateway.rawValue, + status == HTTPResponseCodes.gatewayTimeout.rawValue { self.aepError = .networkError } } } + + static func createAEPOptimizeTimeoutError() -> AEPOptimizeError { + return AEPOptimizeError( + type: nil, + status: OptimizeConstants.ErrorData.Timeout.STATUS, + title: OptimizeConstants.ErrorData.Timeout.TITLE, + detail: OptimizeConstants.ErrorData.Timeout.DETAIL, + aepError: AEPError.callbackTimeout + ) + } + + static func createAEPOptimizInvalidRequestError() -> AEPOptimizeError { + return AEPOptimizeError( + type: nil, + status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, + title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, + detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, + aepError: AEPError.invalidRequest + ) + } } diff --git a/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift b/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift index 2d3adcf..0363c30 100644 --- a/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/Event+OptimizeTests.swift @@ -117,13 +117,7 @@ class Event_OptimizeTests: XCTestCase { source: "com.adobe.eventSource.requestContent", data: nil) - let aepOptimizeError = AEPOptimizeError( - type: nil, - status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, - title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, - detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, - aepError: AEPError.invalidRequest - ) + let aepOptimizeError = AEPOptimizeError.createAEPOptimizInvalidRequestError() let errorResponseEvent = testEvent.createErrorResponseEvent(aepOptimizeError) From 14539897a92de0bc8bf5b41d76ce08dcd30ab411 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Mon, 16 Sep 2024 15:38:45 +0530 Subject: [PATCH 27/39] minor formatting change for code optimization --- Sources/AEPOptimize/OptimizeError.swift | 26 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 510d83d..93e02b8 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -24,6 +24,17 @@ public class AEPOptimizeError: NSObject, Error { public let detail: String? public var aepError = AEPError.unexpected + private let serverErrors = [ + HTTPResponseCodes.tooManyRequests.rawValue, + HTTPResponseCodes.internalServerError.rawValue, + HTTPResponseCodes.serviceUnavailable.rawValue + ] + + private let networkError = [ + HTTPResponseCodes.badGateway.rawValue, + HTTPResponseCodes.gatewayTimeout.rawValue + ] + public init(type: String?, status: Int?, title: String?, detail: String?, aepError: AEPError? = nil) { self.type = type self.status = status @@ -38,21 +49,18 @@ public class AEPOptimizeError: NSObject, Error { } if status == HTTPResponseCodes.clientTimeout.rawValue { self.aepError = .callbackTimeout - } else if (400...499).contains(status) && status != HTTPResponseCodes.tooManyRequests.rawValue { - self.aepError = .invalidRequest - } else if status == HTTPResponseCodes.tooManyRequests.rawValue, - status == HTTPResponseCodes.internalServerError.rawValue, - status == HTTPResponseCodes.serviceUnavailable.rawValue { + } else if serverErrors.contains(status) { self.aepError = .serverError - } else if status == HTTPResponseCodes.badGateway.rawValue, - status == HTTPResponseCodes.gatewayTimeout.rawValue { + } else if networkError.contains(status) { self.aepError = .networkError + } else if (400...499).contains(status) { + self.aepError = .invalidRequest } } } static func createAEPOptimizeTimeoutError() -> AEPOptimizeError { - return AEPOptimizeError( + AEPOptimizeError( type: nil, status: OptimizeConstants.ErrorData.Timeout.STATUS, title: OptimizeConstants.ErrorData.Timeout.TITLE, @@ -62,7 +70,7 @@ public class AEPOptimizeError: NSObject, Error { } static func createAEPOptimizInvalidRequestError() -> AEPOptimizeError { - return AEPOptimizeError( + AEPOptimizeError( type: nil, status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, From 9fb13bb34f3f33e6afd2ead7c00b89ba9a3089fb Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Mon, 16 Sep 2024 15:50:00 +0530 Subject: [PATCH 28/39] Minor formatting --- Sources/AEPOptimize/OptimizeError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 93e02b8..5ba39af 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -53,7 +53,7 @@ public class AEPOptimizeError: NSObject, Error { self.aepError = .serverError } else if networkError.contains(status) { self.aepError = .networkError - } else if (400...499).contains(status) { + } else if (400 ... 499).contains(status) { self.aepError = .invalidRequest } } From a187a51ee7aedb8842e39516c228f843ad338f24 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Mon, 16 Sep 2024 18:21:25 +0530 Subject: [PATCH 29/39] rebasing with fix/updatePropositions --- Sources/AEPOptimize/Event+Optimize.swift | 5 ++ Sources/AEPOptimize/Optimize.swift | 56 ++++++++++++++++++++- Sources/AEPOptimize/OptimizeConstants.swift | 1 + 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/Event+Optimize.swift b/Sources/AEPOptimize/Event+Optimize.swift index 901870f..3d8cefa 100644 --- a/Sources/AEPOptimize/Event+Optimize.swift +++ b/Sources/AEPOptimize/Event+Optimize.swift @@ -18,6 +18,11 @@ import Foundation extension Event { // MARK: - AEP Response Event handle + /// Verify event type and source for Edge error response event. + var isEdgeErrorResponseEvent: Bool { + type == EventType.edge && source == OptimizeConstants.EventSource.EDGE_ERROR_RESPONSE + } + /// Verify event type and source for Edge personalization:decisions event. var isPersonalizationDecisionResponse: Bool { type == EventType.edge && source == OptimizeConstants.EventSource.EDGE_PERSONALIZATION_DECISIONS diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 2d0129e..4084f50 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -32,6 +32,13 @@ public class Optimize: NSObject, Extension { /// Dispatch queue used to protect against simultaneous access of our containers from multiple threads private let queue: DispatchQueue = .init(label: "com.adobe.optimize.containers.queue") + /// a dictionary containing the update event IDs and corresponding errors as received from Edge SDK + private var _errorUpdateRequestEventIds: [String: AEPOptimizeError] = [:] + private var errorUpdateRequestEventIds: [String: AEPOptimizeError] { + get { queue.sync { self._errorUpdateRequestEventIds } } + set { queue.async { self._errorUpdateRequestEventIds = newValue } } + } + /// a dictionary containing the update event IDs (and corresponding requested scopes) for Edge events that haven't yet received an Edge completion response. private var _updateRequestEventIdsInProgress: [String: [DecisionScope]] = [:] private var updateRequestEventIdsInProgress: [String: [DecisionScope]] { @@ -60,6 +67,13 @@ public class Optimize: NSObject, Extension { } #endif + /// Array containing recoverable network error codes being retried by Edge Network Service + private let recoverableNetworkErrorCodes: [Int] = [OptimizeConstants.RecoverableHttpResponseCodes.clientTimeout.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.tooManyRequests.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.badGateway.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.serviceUnavailable.rawValue, + OptimizeConstants.RecoverableHttpResponseCodes.gatewayTimeout.rawValue] + /// Array containing the schema strings for the proposition items supported by the SDK, sent in the personalization query request. static let supportedSchemas = [ // Target schemas @@ -256,6 +270,8 @@ public class Optimize: NSObject, Extension { self.eventsQueue.start() return } + // Error response received for Edge request event UUID (if any) + let edgeError = self.errorUpdateRequestEventIds[requestEventId] // response event to provide success callback to updateProposition public api let responseEventToSend = event.createResponseEvent( @@ -263,7 +279,8 @@ public class Optimize: NSObject, Extension { type: EventType.optimize, source: EventSource.responseContent, data: [ - OptimizeConstants.EventDataKeys.PROPOSITIONS: self.propositionsInProgress + OptimizeConstants.EventDataKeys.PROPOSITIONS: self.propositionsInProgress, + OptimizeConstants.EventDataKeys.RESPONSE_ERROR: edgeError as Any ] ) self.dispatch(event: responseEventToSend) @@ -383,16 +400,43 @@ public class Optimize: NSObject, Extension { /// It logs error related information specifying error type along with a detailed message. /// - Parameter event: Edge error response event. private func processEdgeErrorResponse(event: Event) { + guard + event.isEdgeErrorResponseEvent, + let requestEventId = event.requestEventId, + updateRequestEventIdsInProgress.contains(where: { $0.key == requestEventId }) + else { + Log.debug(label: OptimizeConstants.LOG_TAG, + """ + Ignoring Edge event, either handle type is not errorResponseContent, or the response isn't intended for this extension. + """) + return + } let errorType = event.data?[OptimizeConstants.Edge.ErrorKeys.TYPE] as? String + let errorStatus = event.data?[OptimizeConstants.Edge.ErrorKeys.STATUS] as? Int + let errorTitle = event.data?[OptimizeConstants.Edge.ErrorKeys.TITLE] as? String let errorDetail = event.data?[OptimizeConstants.Edge.ErrorKeys.DETAIL] as? String + let errorReport = event.data?[OptimizeConstants.Edge.ErrorKeys.REPORT] as? [String: Any] let errorString = """ Decisioning Service error, type: \(errorType ?? OptimizeConstants.ERROR_UNKNOWN), \ - detail: \(errorDetail ?? OptimizeConstants.ERROR_UNKNOWN)" + status: \(errorStatus ?? OptimizeConstants.UNKNOWN_STATUS), \ + title: \(errorTitle ?? OptimizeConstants.ERROR_UNKNOWN), \ + detail: \(errorDetail ?? OptimizeConstants.ERROR_UNKNOWN), \ + report: \(errorReport ?? [:])" """ Log.warning(label: OptimizeConstants.LOG_TAG, errorString) + + if let errorStatus = errorStatus, shouldSuppressRecoverableError(status: errorStatus) { + let aepOptimizeError = AEPOptimizeError(type: errorType, status: errorStatus, title: errorTitle, detail: errorDetail) + guard let edgeEventRequestId = event.requestEventId else { + Log.debug(label: OptimizeConstants.LOG_TAG, "No valid edge event request ID found for error response event.") + return + } + // store the error response as an AEPOptimizeError in error dictionary per edge request + errorUpdateRequestEventIds[edgeEventRequestId] = aepOptimizeError + } } /// Processes the get propositions request event, dispatched with type `EventType.optimize` and source `EventSource.requestContent`. @@ -468,6 +512,14 @@ public class Optimize: NSObject, Extension { cachedPropositions.removeAll() } + /// Helper function to check if edge error response received should be suppressed as it is already being retried on Edge + private func shouldSuppressRecoverableError(status: Int) -> Bool { + if recoverableNetworkErrorCodes.contains(status) { + return false + } + return true + } + #if DEBUG /// For testing purposes only func setUpdateRequestEventIdsInProgress(_ eventId: String, expectedScopes: [DecisionScope]) { diff --git a/Sources/AEPOptimize/OptimizeConstants.swift b/Sources/AEPOptimize/OptimizeConstants.swift index f532179..20e9117 100644 --- a/Sources/AEPOptimize/OptimizeConstants.swift +++ b/Sources/AEPOptimize/OptimizeConstants.swift @@ -26,6 +26,7 @@ enum OptimizeConstants { static let XDM_ITEM_COUNT = "xdm:itemCount" static let ERROR_UNKNOWN = "unknown" + static let UNKNOWN_STATUS = 0 enum EventNames { static let UPDATE_PROPOSITIONS_REQUEST = "Optimize Update Propositions Request" From 9476d91c3cc0103aee27a3c65c6444dff169a1f7 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Wed, 18 Sep 2024 15:42:12 +0530 Subject: [PATCH 30/39] -propositionsInProgress should not be removed if response is coming from different request -added alert UI for error handling in update proposition for test app --- Sources/AEPOptimize/Optimize.swift | 11 +++++------ TestApps/AEPOptimizeDemoSwiftUI/OffersView.swift | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 4084f50..93ae83e 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -68,11 +68,11 @@ public class Optimize: NSObject, Extension { #endif /// Array containing recoverable network error codes being retried by Edge Network Service - private let recoverableNetworkErrorCodes: [Int] = [OptimizeConstants.RecoverableHttpResponseCodes.clientTimeout.rawValue, - OptimizeConstants.RecoverableHttpResponseCodes.tooManyRequests.rawValue, - OptimizeConstants.RecoverableHttpResponseCodes.badGateway.rawValue, - OptimizeConstants.RecoverableHttpResponseCodes.serviceUnavailable.rawValue, - OptimizeConstants.RecoverableHttpResponseCodes.gatewayTimeout.rawValue] + private let recoverableNetworkErrorCodes: [Int] = [OptimizeConstants.HTTPResponseCodes.clientTimeout.rawValue, + OptimizeConstants.HTTPResponseCodes.tooManyRequests.rawValue, + OptimizeConstants.HTTPResponseCodes.badGateway.rawValue, + OptimizeConstants.HTTPResponseCodes.serviceUnavailable.rawValue, + OptimizeConstants.HTTPResponseCodes.gatewayTimeout.rawValue] /// Array containing the schema strings for the proposition items supported by the SDK, sent in the personalization query request. static let supportedSchemas = [ @@ -356,7 +356,6 @@ public class Optimize: NSObject, Extension { """ Ignoring Edge event, either handle type is not personalization:decisions, or the response isn't intended for this extension. """) - propositionsInProgress.removeAll() return } diff --git a/TestApps/AEPOptimizeDemoSwiftUI/OffersView.swift b/TestApps/AEPOptimizeDemoSwiftUI/OffersView.swift index b5b769f..aa345d4 100644 --- a/TestApps/AEPOptimizeDemoSwiftUI/OffersView.swift +++ b/TestApps/AEPOptimizeDemoSwiftUI/OffersView.swift @@ -160,8 +160,20 @@ struct OffersView: View { htmlDecisionScope, jsonDecisionScope, targetScope - ], withXdm: ["xdmKey": "1234"], - andData: data) + ], withXdm: ["xdmKey": "1234"], andData: data) { data, error in + if let error = error as? AEPOptimizeError { + errorAlert = true + if let errorStatus = error.status { + errorMessage = (error.title ?? "Unexpected Error") + " : " + String(errorStatus) + }else{ + errorMessage = error.title ?? "Unexpected Error" + } + } + } + + } + .alert(isPresented: $errorAlert) { + Alert(title: Text("Error: Update Propositions"), message: Text(errorMessage), dismissButton: .default(Text("OK"))) } CustomButtonView(buttonTitle: "Get Propositions") { From 60f45d2bbe20bf5309ba6950c0791afea1e53ec7 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Wed, 18 Sep 2024 17:15:55 +0530 Subject: [PATCH 31/39] minor code correction --- Sources/AEPOptimize/Optimize.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 93ae83e..c67d589 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -427,7 +427,7 @@ public class Optimize: NSObject, Extension { Log.warning(label: OptimizeConstants.LOG_TAG, errorString) - if let errorStatus = errorStatus, shouldSuppressRecoverableError(status: errorStatus) { + if let errorStatus = errorStatus, !shouldSuppressRecoverableError(status: errorStatus) { let aepOptimizeError = AEPOptimizeError(type: errorType, status: errorStatus, title: errorTitle, detail: errorDetail) guard let edgeEventRequestId = event.requestEventId else { Log.debug(label: OptimizeConstants.LOG_TAG, "No valid edge event request ID found for error response event.") @@ -514,9 +514,9 @@ public class Optimize: NSObject, Extension { /// Helper function to check if edge error response received should be suppressed as it is already being retried on Edge private func shouldSuppressRecoverableError(status: Int) -> Bool { if recoverableNetworkErrorCodes.contains(status) { - return false + return true } - return true + return false } #if DEBUG From 5b40a6c844e52b7cb7b8ec63e285e73a18f0367c Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Fri, 20 Sep 2024 21:17:57 +0530 Subject: [PATCH 32/39] added report field in AEPOptimizeError as it provides necessary additional information received from Edge error response --- Sources/AEPOptimize/Optimize.swift | 2 +- Sources/AEPOptimize/OptimizeError.swift | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index c67d589..376b56f 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -428,7 +428,7 @@ public class Optimize: NSObject, Extension { Log.warning(label: OptimizeConstants.LOG_TAG, errorString) if let errorStatus = errorStatus, !shouldSuppressRecoverableError(status: errorStatus) { - let aepOptimizeError = AEPOptimizeError(type: errorType, status: errorStatus, title: errorTitle, detail: errorDetail) + let aepOptimizeError = AEPOptimizeError(type: errorType, status: errorStatus, title: errorTitle, detail: errorDetail, report: errorReport) guard let edgeEventRequestId = event.requestEventId else { Log.debug(label: OptimizeConstants.LOG_TAG, "No valid edge event request ID found for error response event.") return diff --git a/Sources/AEPOptimize/OptimizeError.swift b/Sources/AEPOptimize/OptimizeError.swift index 5ba39af..702b101 100644 --- a/Sources/AEPOptimize/OptimizeError.swift +++ b/Sources/AEPOptimize/OptimizeError.swift @@ -22,6 +22,7 @@ public class AEPOptimizeError: NSObject, Error { public let status: Int? public let title: String? public let detail: String? + public let report: [String: Any]? public var aepError = AEPError.unexpected private let serverErrors = [ @@ -35,11 +36,12 @@ public class AEPOptimizeError: NSObject, Error { HTTPResponseCodes.gatewayTimeout.rawValue ] - public init(type: String?, status: Int?, title: String?, detail: String?, aepError: AEPError? = nil) { + public init(type: String?, status: Int?, title: String?, detail: String?, report: [String: Any]?, aepError: AEPError? = nil) { self.type = type self.status = status self.title = title self.detail = detail + self.report = report if let aepError { self.aepError = aepError } else { @@ -65,6 +67,7 @@ public class AEPOptimizeError: NSObject, Error { status: OptimizeConstants.ErrorData.Timeout.STATUS, title: OptimizeConstants.ErrorData.Timeout.TITLE, detail: OptimizeConstants.ErrorData.Timeout.DETAIL, + report: nil, aepError: AEPError.callbackTimeout ) } @@ -75,6 +78,7 @@ public class AEPOptimizeError: NSObject, Error { status: OptimizeConstants.ErrorData.InvalidRequest.STATUS, title: OptimizeConstants.ErrorData.InvalidRequest.TITLE, detail: OptimizeConstants.ErrorData.InvalidRequest.DETAIL, + report: nil, aepError: AEPError.invalidRequest ) } From dc44836e49614abe35edfb1c3304d2d12747ad17 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Tue, 24 Sep 2024 21:09:17 +0530 Subject: [PATCH 33/39] updated integration test for invalidEdgeResponse with assertions for AEPOptimizeError received in update proposition callback fixed build errors for AEPOptimize objective-C test app --- Sources/AEPOptimize/Optimize.swift | 12 +++++----- TestApps/AEPOptimizeDemoObjC/ViewController.m | 24 +++++++++---------- .../OptimizeIntegrationTests.swift | 24 +++++++++++++++---- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Sources/AEPOptimize/Optimize.swift b/Sources/AEPOptimize/Optimize.swift index 376b56f..147f554 100644 --- a/Sources/AEPOptimize/Optimize.swift +++ b/Sources/AEPOptimize/Optimize.swift @@ -33,10 +33,10 @@ public class Optimize: NSObject, Extension { private let queue: DispatchQueue = .init(label: "com.adobe.optimize.containers.queue") /// a dictionary containing the update event IDs and corresponding errors as received from Edge SDK - private var _errorUpdateRequestEventIds: [String: AEPOptimizeError] = [:] - private var errorUpdateRequestEventIds: [String: AEPOptimizeError] { - get { queue.sync { self._errorUpdateRequestEventIds } } - set { queue.async { self._errorUpdateRequestEventIds = newValue } } + private var _updateRequestEventIdsErrors: [String: AEPOptimizeError] = [:] + private var updateRequestEventIdsErrors: [String: AEPOptimizeError] { + get { queue.sync { self._updateRequestEventIdsErrors } } + set { queue.async { self._updateRequestEventIdsErrors = newValue } } } /// a dictionary containing the update event IDs (and corresponding requested scopes) for Edge events that haven't yet received an Edge completion response. @@ -271,7 +271,7 @@ public class Optimize: NSObject, Extension { return } // Error response received for Edge request event UUID (if any) - let edgeError = self.errorUpdateRequestEventIds[requestEventId] + let edgeError = self.updateRequestEventIdsErrors[requestEventId] // response event to provide success callback to updateProposition public api let responseEventToSend = event.createResponseEvent( @@ -434,7 +434,7 @@ public class Optimize: NSObject, Extension { return } // store the error response as an AEPOptimizeError in error dictionary per edge request - errorUpdateRequestEventIds[edgeEventRequestId] = aepOptimizeError + updateRequestEventIdsErrors[edgeEventRequestId] = aepOptimizeError } } diff --git a/TestApps/AEPOptimizeDemoObjC/ViewController.m b/TestApps/AEPOptimizeDemoObjC/ViewController.m index 7e1e442..3e28c41 100644 --- a/TestApps/AEPOptimizeDemoObjC/ViewController.m +++ b/TestApps/AEPOptimizeDemoObjC/ViewController.m @@ -31,33 +31,33 @@ - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. - [AEPMobileOptimize onPropositionsUpdate:^(NSDictionary *propositionsDict) { + [AEPMobileOptimize onPropositionsUpdate:^(NSDictionary *propositionsDict) { - AEPProposition* textProposition = propositionsDict[textDecisionScope]; + AEPOptimizeProposition* textProposition = propositionsDict[textDecisionScope]; NSLog(@"Callback Logging %ld Text Offer(s)", [textProposition.offers count]); for(AEPOffer* offer in textProposition.offers) { NSLog(@"Callback Text Offer Content:%@", offer.content); } - AEPProposition* imageProposition = propositionsDict[imageDecisionScope]; + AEPOptimizeProposition* imageProposition = propositionsDict[imageDecisionScope]; NSLog(@"Callback Logging %ld Image Offer(s)", [imageProposition.offers count]); for(AEPOffer* offer in imageProposition.offers) { NSLog(@"Callback Image Offer Content:%@", offer.content); } - AEPProposition* htmlProposition = propositionsDict[htmlDecisionScope]; + AEPOptimizeProposition* htmlProposition = propositionsDict[htmlDecisionScope]; NSLog(@"Callback Logging %ld Html Offer(s)", [htmlProposition.offers count]); for(AEPOffer* offer in htmlProposition.offers) { NSLog(@"Callback Html Offer Content:%@", offer.content); } - AEPProposition* jsonProposition = propositionsDict[jsonDecisionScope]; + AEPOptimizeProposition* jsonProposition = propositionsDict[jsonDecisionScope]; NSLog(@"Callback Logging %ld Json Offer(s)", [jsonProposition.offers count]); for(AEPOffer* offer in jsonProposition.offers) { NSLog(@"Callback Json Offer Content:%@", offer.content); } - AEPProposition* targetProposition = propositionsDict[targetDecisionScope]; + AEPOptimizeProposition* targetProposition = propositionsDict[targetDecisionScope]; NSLog(@"Callback Logging %ld Target Offer(s)", [targetProposition.offers count]); for(AEPOffer* offer in targetProposition.offers) { NSLog(@"Callback Target Offer Content:%@", offer.content); @@ -96,37 +96,37 @@ - (IBAction)getPropositions:(id)sender { htmlDecisionScope, jsonDecisionScope, targetDecisionScope - ] completion:^(NSDictionary* propositionsDict, NSError* error) { + ] completion:^(NSDictionary* propositionsDict, NSError* error) { if (error != nil) { NSLog(@"Get propositions failed with error: %@", [error localizedDescription]); return; } - AEPProposition* textProposition = propositionsDict[textDecisionScope]; + AEPOptimizeProposition* textProposition = propositionsDict[textDecisionScope]; NSLog(@"Logging %ld Text Offer(s)", [textProposition.offers count]); for(AEPOffer* offer in textProposition.offers) { NSLog(@"Text Offer Content:%@", offer.content); } - AEPProposition* imageProposition = propositionsDict[imageDecisionScope]; + AEPOptimizeProposition* imageProposition = propositionsDict[imageDecisionScope]; NSLog(@"Logging %ld Image Offer(s)", [imageProposition.offers count]); for(AEPOffer* offer in imageProposition.offers) { NSLog(@"Image Offer Content:%@", offer.content); } - AEPProposition* htmlProposition = propositionsDict[htmlDecisionScope]; + AEPOptimizeProposition* htmlProposition = propositionsDict[htmlDecisionScope]; NSLog(@"Logging %ld Html Offer(s)", [htmlProposition.offers count]); for(AEPOffer* offer in htmlProposition.offers) { NSLog(@"Html Offer Content:%@", offer.content); } - AEPProposition* jsonProposition = propositionsDict[jsonDecisionScope]; + AEPOptimizeProposition* jsonProposition = propositionsDict[jsonDecisionScope]; NSLog(@"Logging %ld Json Offer(s)", [jsonProposition.offers count]); for(AEPOffer* offer in jsonProposition.offers) { NSLog(@"Json Offer Content:%@", offer.content); } - AEPProposition* targetProposition = propositionsDict[targetDecisionScope]; + AEPOptimizeProposition* targetProposition = propositionsDict[targetDecisionScope]; NSLog(@"Logging %ld Target Offer(s)", [targetProposition.offers count]); for(AEPOffer* offer in targetProposition.offers) { NSLog(@"Target Offer Content:%@", offer.content); diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index 8301823..08281d8 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -467,9 +467,10 @@ class OptimizeIntegrationTests: XCTestCase { "handle":[],\ "errors":[\ {\ - "type":"EXEG-0201-503",\ - "status":503,\ - "title":"The 'com.adobe.experience.platform.ode' service is temporarily unable to serve this request. Please try again later."\ + "type":"EXEG-0201-400",\ + "status":400,\ + "title":"Invalid Request",\ + "detail":"Request cannot be processed as few parameters are missing. Please check and try again later."\ }\ ]\ } @@ -500,9 +501,22 @@ class OptimizeIntegrationTests: XCTestCase { let decisionScope = DecisionScope(activityId: "xcore:offer-activity:1111111111111111", placementId: "xcore:offer-placement:1111111111111111") + let invalidResponseExpectation = XCTestExpectation(description: "updatePropositions should result in a inavlid resoponse from Edge Experience Network.") + // update propositions - Optimize.updatePropositions(for: [decisionScope], withXdm: nil) - wait(for: [requestExpectation], timeout: 2) + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { data, error in + guard let error = error as? AEPOptimizeError else { + XCTFail("Type mismatch in error received for Update Propositions") + return + } + XCTAssertNotNil(error) + XCTAssertTrue(error.status == 400) + XCTAssertTrue(error.aepError == .invalidRequest) + XCTAssertTrue(error.title == "Invalid Request") + XCTAssertTrue(error.detail == "Request cannot be processed as few parameters are missing. Please check and try again later.") + invalidResponseExpectation.fulfill() + } + wait(for: [requestExpectation, invalidResponseExpectation], timeout: 12) // get propositions let retrieveExpectation = XCTestExpectation(description: "getPropositions should not return propositions, if update request errors out.") From ec071a4bde4e17deb7aa4bb98e4cdf2679fb4427 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Wed, 25 Sep 2024 19:09:40 +0530 Subject: [PATCH 34/39] added separate integration test case for update proposition invalid edge response instead of modifying existing test --- .../OptimizeIntegrationTests.swift | 82 ++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift index 08281d8..39608bb 100644 --- a/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift +++ b/Tests/AEPOptimizeTests/IntegrationTests/OptimizeIntegrationTests.swift @@ -457,10 +457,10 @@ class OptimizeIntegrationTests: XCTestCase { } wait(for: [retrieveExpectation], timeout: 2) } - - func testGetPropositions_invalidEdgeResponse() { - // setup - let validResponse = HTTPURLResponse(url: URL(string: "https://edge.adobedc.net/ee/v1/interact?configId=configId&requestId=requestId")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + func testUpdateProposition_invalidEdgeResponse() { + //setup + let inValidResponse = HTTPURLResponse(url: URL(string: "https://edge.adobedc.net/ee/v1/interact?configId=configId&requestId=requestId")!, statusCode: 400, httpVersion: nil, headerFields: nil) let responseString = """ {\ "requestId":"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",\ @@ -475,6 +475,63 @@ class OptimizeIntegrationTests: XCTestCase { ]\ } """ + + let requestExpectation = XCTestExpectation(description: "updatePropositions should result in a valid personalization query request to the Edge network.") + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("edge.adobedc.net/ee/v1/interact?configId=configId") { + requestExpectation.fulfill() + return (data: responseString.data(using: .utf8), response: inValidResponse, error: nil) + } + return (data: nil, response: inValidResponse, error: nil) + } + + // init extensions + initExtensionsAndWait() + + // update configuration + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "edge.configId": "configId"]) + + let decisionScope = DecisionScope(activityId: "xcore:offer-activity:1111111111111111", + placementId: "xcore:offer-placement:1111111111111111") + + let invalidResponseExpectation = XCTestExpectation(description: "updatePropositions should result in a inavlid resoponse from Edge Experience Network.") + + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { data, error in + if let error = error as? AEPOptimizeError { + XCTAssertNotNil(error) + XCTAssertTrue(error.status == 400) + XCTAssertTrue(error.aepError == .invalidRequest) + XCTAssertTrue(error.title == "Invalid Request") + XCTAssertTrue(error.detail == "Request cannot be processed as few parameters are missing. Please check and try again later.") + } + invalidResponseExpectation.fulfill() + } + + wait(for: [requestExpectation, invalidResponseExpectation], timeout: 12) + } + + func testGetPropositions_invalidEdgeResponse() { + // setup + let validResponse = HTTPURLResponse(url: URL(string: "https://edge.adobedc.net/ee/v1/interact?configId=configId&requestId=requestId")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let responseString = """ + {\ + "requestId":"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",\ + "handle":[],\ + "errors":[\ + {\ + "type":"EXEG-0201-503",\ + "status":503,\ + "title":"The 'com.adobe.experience.platform.ode' service is temporarily unable to serve this request. Please try again later."\ + }\ + ]\ + } + """ // mock edge response let requestExpectation = XCTestExpectation(description: "updatePropositions should result in a valid personalization query request to the Edge network.") @@ -501,22 +558,9 @@ class OptimizeIntegrationTests: XCTestCase { let decisionScope = DecisionScope(activityId: "xcore:offer-activity:1111111111111111", placementId: "xcore:offer-placement:1111111111111111") - let invalidResponseExpectation = XCTestExpectation(description: "updatePropositions should result in a inavlid resoponse from Edge Experience Network.") - // update propositions - Optimize.updatePropositions(for: [decisionScope], withXdm: nil) { data, error in - guard let error = error as? AEPOptimizeError else { - XCTFail("Type mismatch in error received for Update Propositions") - return - } - XCTAssertNotNil(error) - XCTAssertTrue(error.status == 400) - XCTAssertTrue(error.aepError == .invalidRequest) - XCTAssertTrue(error.title == "Invalid Request") - XCTAssertTrue(error.detail == "Request cannot be processed as few parameters are missing. Please check and try again later.") - invalidResponseExpectation.fulfill() - } - wait(for: [requestExpectation, invalidResponseExpectation], timeout: 12) + Optimize.updatePropositions(for: [decisionScope], withXdm: nil) + wait(for: [requestExpectation], timeout: 2) // get propositions let retrieveExpectation = XCTestExpectation(description: "getPropositions should not return propositions, if update request errors out.") From 1c4d02a48f799ec616134f719a99fa68d7c173c3 Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Thu, 26 Sep 2024 17:12:51 +0530 Subject: [PATCH 35/39] Fix: Changed Offer Score value from Int to Double --- Sources/AEPOptimize/Offer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AEPOptimize/Offer.swift b/Sources/AEPOptimize/Offer.swift index 3a9fd2f..90d3f00 100644 --- a/Sources/AEPOptimize/Offer.swift +++ b/Sources/AEPOptimize/Offer.swift @@ -23,7 +23,7 @@ public class Offer: NSObject, Codable { @objc public let etag: String /// Offer priority score - @objc public let score: Int + @objc public let score: Double /// Offer schema string @objc public let schema: String @@ -72,7 +72,7 @@ public class Offer: NSObject, Codable { // Try and decode format, if present. Target response doesn't contain etag, so setting the default value to empty string. etag = try container.decodeIfPresent(String.self, forKey: .etag) ?? "" - score = try container.decodeIfPresent(Int.self, forKey: .score) ?? 0 + score = try container.decodeIfPresent(Double.self, forKey: .score) ?? 0 schema = try container.decode(String.self, forKey: .schema) From 9e8747343e622a9f4f5be932e833257870d99b29 Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Fri, 27 Sep 2024 10:15:52 +0530 Subject: [PATCH 36/39] Add: Added test case for a valid offer with score value as double --- .../UnitTests/OfferTests.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift b/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift index 88102cd..117b241 100644 --- a/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift @@ -162,6 +162,28 @@ class OfferTests: XCTestCase { }\ } """ + + private let OFFER_WITH_SCORE_AS_DOUBLE = +""" +{\ + "id": "xcore:personalized-offer:2222222222222222",\ + "etag": "7",\ + "score": 6.43,\ + "schema": "https://ns.adobe.com/experience/offer-management/content-component-text",\ + "data": {\ + "id": "xcore:personalized-offer:2222222222222222",\ + "format": "text/plain",\ + "content": "This is a plain text content!",\ + "language": [\ + "en-us"\ + ],\ + "characteristics": {\ + "mobile": "true"\ + }\ + }\ +} +""" + private let OFFER_INVALID_NO_CONTENT = """ @@ -365,6 +387,27 @@ class OfferTests: XCTestCase { XCTAssertEqual("true", offer.characteristics?["mobile"]) } + func testOffer_withScoreAsDouble() { + guard let offerData = OFFER_WITH_SCORE_AS_DOUBLE.data(using: .utf8), + let offer = try? JSONDecoder().decode(Offer.self, from: offerData) + else { + XCTFail("Offer should be valid.") + return + } + /// Assert the score is of type Double and has the correct value + XCTAssertTrue(type(of: offer.score) == Double.self, "Score should be a Double.") + XCTAssertEqual(6.43, offer.score, accuracy: 0.001, "Score value should be 6.43.") + + /// Other assertions to validate the remaining offer data + XCTAssertEqual("7", offer.etag) + XCTAssertEqual(OfferType.text, offer.type) + XCTAssertEqual("This is a plain text content!", offer.content) + XCTAssertEqual(1, offer.language?.count) + XCTAssertEqual("en-us", offer.language?[0]) + XCTAssertEqual("true", offer.characteristics?["mobile"]) + } + + func testOffer_invalidNoContent() { guard let offerData = OFFER_INVALID_NO_CONTENT.data(using: .utf8) else { XCTFail("Offer json data should be valid.") From 48717cc71cfc01b94028792ab692e5feb1c33c87 Mon Sep 17 00:00:00 2001 From: Shwetansh Srivastava Date: Fri, 27 Sep 2024 10:19:35 +0530 Subject: [PATCH 37/39] minor formatting --- Tests/AEPOptimizeTests/UnitTests/OfferTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift b/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift index 117b241..be21a11 100644 --- a/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift +++ b/Tests/AEPOptimizeTests/UnitTests/OfferTests.swift @@ -407,7 +407,6 @@ class OfferTests: XCTestCase { XCTAssertEqual("true", offer.characteristics?["mobile"]) } - func testOffer_invalidNoContent() { guard let offerData = OFFER_INVALID_NO_CONTENT.data(using: .utf8) else { XCTFail("Offer json data should be valid.") From 781983bae82fac8c0f64cba18ff24c2e80a59476 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 3 Oct 2024 20:44:04 +0530 Subject: [PATCH 38/39] changes SDK version form 5.0.1 to 5.1.0 --- AEPOptimize.xcodeproj/project.pbxproj | 4 ++-- Sources/AEPOptimize/OptimizeConstants.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AEPOptimize.xcodeproj/project.pbxproj b/AEPOptimize.xcodeproj/project.pbxproj index 5c12c0f..8b84983 100644 --- a/AEPOptimize.xcodeproj/project.pbxproj +++ b/AEPOptimize.xcodeproj/project.pbxproj @@ -1656,7 +1656,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.0.1; + MARKETING_VERSION = 5.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.AEPOptimize; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1685,7 +1685,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.0.1; + MARKETING_VERSION = 5.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.AEPOptimize; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/Sources/AEPOptimize/OptimizeConstants.swift b/Sources/AEPOptimize/OptimizeConstants.swift index 20e9117..73d05d6 100644 --- a/Sources/AEPOptimize/OptimizeConstants.swift +++ b/Sources/AEPOptimize/OptimizeConstants.swift @@ -13,7 +13,7 @@ enum OptimizeConstants { static let EXTENSION_NAME = "com.adobe.optimize" static let FRIENDLY_NAME = "Optimize" - static let EXTENSION_VERSION = "5.0.1" + static let EXTENSION_VERSION = "5.1.0" static let LOG_TAG = FRIENDLY_NAME static let DECISION_SCOPE_NAME = "name" From 7ab4b70b65f547791ac11ee31941062711a3aac0 Mon Sep 17 00:00:00 2001 From: akhiljain1907 Date: Thu, 3 Oct 2024 20:47:00 +0530 Subject: [PATCH 39/39] updated podspec file --- AEPOptimize.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AEPOptimize.podspec b/AEPOptimize.podspec index 7788275..4702ef4 100644 --- a/AEPOptimize.podspec +++ b/AEPOptimize.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPOptimize" - s.version = "5.0.1" + s.version = "5.1.0" s.summary = "Experience Platform Optimize extension for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC The Experience Platform Optimize extension provides APIs to enable real-time personalization workflows in the Adobe Experience Platform SDKs using Adobe Target or Adobe Journey Optimizer Offer Decisioning.