Skip to content

Commit

Permalink
[AMSDK-10268]Handle Analytics response identity events (#584)
Browse files Browse the repository at this point in the history
* fix link in doc

* Update Event.md (#579)

* Identity-Handle Analytics response identity events

Listen for Analytics response identity event
Dispatch Avid sync event
Add unit tests

* refactor the code

refactor the code move logic out of identity.swift

* Change isAdSynced in properties optional.

Change isAdSynced in properties optional

* Revert "Merge branch 'main' of github.com:adobe/aepsdk-core-ios into Identity_handleAnalyticsIdentity"

This reverts commit 7656fa7, reversing
changes made to 951ac90.

* Updates for review comments

Updates for review comments

Co-authored-by: Steve Benedick <sbenedic@adobe.com>
Co-authored-by: Nick Porter <43650450+nporter-adbe@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 26, 2021
1 parent 875eadf commit 84f7c9c
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 2 deletions.
5 changes: 5 additions & 0 deletions AEPIdentity/Sources/Event+Identity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,9 @@ extension Event {
var optOutHitSent: Bool {
return data?[IdentityConstants.Audience.OPTED_OUT_HIT_SENT] as? Bool ?? false
}

/// Reads the Analytics id if present in event data
var aid: String? {
return data?[IdentityConstants.Analytics.ANALYTICS_ID] as? String
}
}
6 changes: 6 additions & 0 deletions AEPIdentity/Sources/Identity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import Foundation
registerListener(type: EventType.genericIdentity, source: EventSource.requestContent, listener: handleIdentityRequest)
registerListener(type: EventType.configuration, source: EventSource.requestIdentity, listener: receiveConfigurationIdentity(event:))
registerListener(type: EventType.configuration, source: EventSource.responseContent, listener: handleConfigurationResponse)
registerListener(type: EventType.analytics, source: EventSource.responseIdentity, listener: handleAnalyticsResponseIdentity)
registerListener(type: EventType.audienceManager, source: EventSource.responseContent, listener: handleAudienceResponse(event:))
}

Expand Down Expand Up @@ -147,6 +148,11 @@ import Foundation
data: eventData as [String: Any])
dispatch(event: responseEvent)
}
/// Handles the analytics response event and dispatch an "AVID Sync" event
/// - Parameter event: the analytics response event
private func handleAnalyticsResponseIdentity(event: Event) {
state?.handleAnalyticsResponse(event: event, eventDispatcher: dispatch(event:))
}

/// Handles Audience Response Content events containing a flag which signals if the opt-out hit was sent by the Audience Extension.
/// If the flag is false, the Identity extension will send an opt-out hit to the configured Identity server.
Expand Down
2 changes: 2 additions & 0 deletions AEPIdentity/Sources/IdentityConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ enum IdentityConstants {
static let IDENTITY_RESPONSE_CONTENT_ONE_TIME = "IDENTITY_RESPONSE_CONTENT_ONE_TIME"
static let IDENTITY_URL_VARIABLES = "IDENTITY_URL_VARIABLES"
static let UPDATED_IDENTITY_RESPONSE = "UPDATED_IDENTITY_RESPONSE"
static let AVID_SYNC_EVENT = "AVID Sync"
}

enum EventDataKeys {
Expand All @@ -65,6 +66,7 @@ enum IdentityConstants {
static let VISITOR_ID_LOCATION_HINT = "locationhint"
static let VISITOR_IDS_LAST_SYNC = "lastsync"
static let MCPNS_DPID = "20920"
static let ANALYTICS_ID = "AVID"
}

enum DataStoreKeys {
Expand Down
3 changes: 3 additions & 0 deletions AEPIdentity/Sources/IdentityProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ struct IdentityProperties: Codable {
/// The current privacy status provided by the Configuration extension, defaults to `unknown`
var privacyStatus = PrivacyStatus.unknown

/// The aid synced status for handle analytics response event, set defaults to `false`
var isAidSynced: Bool? = false

/// Converts `IdentityProperties` into an event data representation
/// - Returns: A dictionary representing this `IdentityProperties`
func toEventData() -> [String: Any] {
Expand Down
35 changes: 33 additions & 2 deletions AEPIdentity/Sources/IdentityState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ class IdentityState {
identityProperties.blob = nil
identityProperties.locationHint = nil
identityProperties.customerIds?.removeAll()

// TODO: Clear AID from analytics
identityProperties.isAidSynced = false
identityProperties.pushIdentifier = nil
pushIdManager.updatePushId(pushId: nil)
identityProperties.saveToPersistence()
Expand All @@ -252,6 +251,38 @@ class IdentityState {
lastValidConfig = newConfig
}

/// Invoked each time when we receive Analytics Response event
/// - Parameters:
/// - event: the event from Analytics Respsonse
/// - eventDispatcher: a function which when invoked dispatches an `Event` to the `EventHub`
func handleAnalyticsResponse(event: Event, eventDispatcher: (Event) -> Void) {
guard let aid = event.aid, !aid.isEmpty else {
Log.debug(label: "\(LOG_TAG):\(#function)", "Analytics Tracking ID is not found or empty")
return
}

if !(identityProperties.isAidSynced ?? false) {
// dispatch events
let identifiers: [String: String] = [IdentityConstants.EventDataKeys.ANALYTICS_ID: aid]
let syncData: [String: Any] = [
IdentityConstants.EventDataKeys.IDENTIFIERS: identifiers,
IdentityConstants.EventDataKeys.FORCE_SYNC: false,
IdentityConstants.EventDataKeys.IS_SYNC_EVENT: true,
IdentityConstants.EventDataKeys.AUTHENTICATION_STATE: MobileVisitorAuthenticationState.unknown.rawValue,
]

identityProperties.isAidSynced = true
// save properties
identityProperties.saveToPersistence()

let avidEvent = Event(name: IdentityConstants.EventNames.AVID_SYNC_EVENT,
type: EventType.identity,
source: EventSource.requestIdentity,
data: syncData)
eventDispatcher(avidEvent)
}
}

// MARK: Private APIs

/// Inspects the current configuration to determine if a sync can be made, this is determined by if a valid org id is present and if the privacy is not set to opted-out
Expand Down
111 changes: 111 additions & 0 deletions AEPIdentity/Tests/IdentityStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,117 @@ class IdentityStateTests: XCTestCase {
XCTAssertTrue(mockHitQueue.calledSuspend) // we should have suspended the hit queue
XCTAssertEqual(PrivacyStatus.unknown, state.identityProperties.privacyStatus) // privacy status should change to opt in
}

// MARK: HandleAnalyticsResponse(...)
/// When aid sycned is false, we dispatch an event, set it to true and save to persistence
func testHandleAnalyticsResponseAidSyncedFalse() {
// setup
let dispatchedEventExpectation = XCTestExpectation(description: "one event should be dispatched")
dispatchedEventExpectation.expectedFulfillmentCount = 1 // 1 identity events
dispatchedEventExpectation.assertForOverFulfill = true
state.identityProperties.isAidSynced = false
XCTAssertTrue(state.identityProperties.isAidSynced == false)
let eventData = [IdentityConstants.Analytics.ANALYTICS_ID: "aid" ] as [String: Any]

let event = Event(name: "Test Analytics Response Event", type: EventType.analytics, source: EventSource.responseIdentity, data: eventData)

//test
state.handleAnalyticsResponse(event: event, eventDispatcher: { event in
XCTAssertEqual(IdentityConstants.EventNames.AVID_SYNC_EVENT, event.name)
let identifierValue = [IdentityConstants.EventDataKeys.ANALYTICS_ID: "aid" ] as [String: String]
XCTAssertEqual(identifierValue, event.data?[IdentityConstants.EventDataKeys.IDENTIFIERS]as? [String: String] )
XCTAssertEqual(false, event.data?[IdentityConstants.EventDataKeys.FORCE_SYNC]as? Bool)
XCTAssertEqual(true, event.data?[IdentityConstants.EventDataKeys.IS_SYNC_EVENT]as? Bool)
XCTAssertEqual(0, event.data?[IdentityConstants.EventDataKeys.AUTHENTICATION_STATE]as? Int)
dispatchedEventExpectation.fulfill()
})

// verify
wait(for: [dispatchedEventExpectation], timeout: 1)
XCTAssertTrue(state.identityProperties.isAidSynced == true)
XCTAssertEqual(1,mockDataStore.dict.count) // identity properties should have been saved to persistence
}

/// when aid synced is true, we don't dispatch event and don't save it to persistence
func testHandleAnalyticsResponseAidSyncedTrue() {
// setup
let dispatchedEventExpectation = XCTestExpectation(description: "no event should be dispatched")
dispatchedEventExpectation.assertForOverFulfill = true
state.identityProperties.isAidSynced = true
XCTAssertTrue(state.identityProperties.isAidSynced == true)
let eventData = [IdentityConstants.Analytics.ANALYTICS_ID: "aid" ] as [String: Any]
let event = Event(name: "Test Analytics Response Event", type: EventType.analytics, source: EventSource.responseIdentity, data: eventData)

//test
state.handleAnalyticsResponse(event: event, eventDispatcher: { _ in
dispatchedEventExpectation.fulfill()
})

// verify
XCTAssertTrue(state.identityProperties.isAidSynced == true)
XCTAssertEqual(0, mockDataStore.dict.count) // identity properties should not be saved to persistence
}

/// We set aid synced to false when privacy is opt out.
func testAidSyncedFalseAfterPrivacyOptOut() {
// setup
state.identityProperties.isAidSynced = true
XCTAssertTrue(state.identityProperties.isAidSynced == true)

let sharedStateExpectation = XCTestExpectation(description: "Shared state should be updated once")
var props = IdentityProperties()
props.privacyStatus = .unknown
props.ecid = ECID()

state = IdentityState(identityProperties: props, hitQueue: MockHitQueue(processor: MockHitProcessor()), pushIdManager: mockPushIdManager)
let event = Event(name: "Test event", type: EventType.identity, source: EventSource.requestIdentity, data: [IdentityConstants.Configuration.GLOBAL_CONFIG_PRIVACY: PrivacyStatus.optedOut.rawValue])

// test
state.processPrivacyChange(event: event, createSharedState: { (data, event) in
sharedStateExpectation.fulfill()
})

// verify
XCTAssertTrue(state.identityProperties.isAidSynced == false)
XCTAssertEqual(1, mockDataStore.dict.count) // identity properties should not be saved to persistence
}

/// We set aid synced to false when privacy is opt out, call handle analytics response, it set back to true
func testAidSyncedScenario() {
// setup
state.identityProperties.isAidSynced = true
XCTAssertTrue(state.identityProperties.isAidSynced == true)

let sharedStateExpectation = XCTestExpectation(description: "Shared state should be updated once")
var props = IdentityProperties()
props.privacyStatus = .unknown
props.ecid = ECID()

state = IdentityState(identityProperties: props, hitQueue: MockHitQueue(processor: MockHitProcessor()), pushIdManager: mockPushIdManager)
let event = Event(name: "Test event", type: EventType.identity, source: EventSource.requestIdentity, data: [IdentityConstants.Configuration.GLOBAL_CONFIG_PRIVACY: PrivacyStatus.optedOut.rawValue])

state.processPrivacyChange(event: event, createSharedState: { (data, event) in
sharedStateExpectation.fulfill()
})
//opt out, aid synced set to false
XCTAssertTrue(state.identityProperties.isAidSynced == false)

let dispatchedEventExpectation = XCTestExpectation(description: "one event should be dispatched")
dispatchedEventExpectation.expectedFulfillmentCount = 1
dispatchedEventExpectation.assertForOverFulfill = true

let eventData = [IdentityConstants.Analytics.ANALYTICS_ID: "aid" ] as [String: Any]
let repsonseEvent = Event(name: "Test Analytics Response Event", type: EventType.analytics, source: EventSource.responseIdentity, data: eventData)

//test
state.handleAnalyticsResponse(event: repsonseEvent, eventDispatcher: { _ in
dispatchedEventExpectation.fulfill()
})

// verify
XCTAssertTrue(state.identityProperties.isAidSynced == true)
XCTAssertEqual(1, mockDataStore.dict.count) // identity properties should not be saved to persistence
}
}

private extension Event {
Expand Down
63 changes: 63 additions & 0 deletions AEPIdentity/Tests/IdentityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,67 @@ class IdentityTests: XCTestCase {
let mockNetworkService = ServiceProvider.shared.networkService as! MockNetworkServiceOverrider
XCTAssertFalse(mockNetworkService.connectAsyncCalled) // network request for opt-out hit shouldn't have been sent
}

/// Tests that when receives a valid analytics event and response identity event source that we dispatch an Avid Sync event
func testAnalyticsResponseIdentityHappy() {
// setup
let eventData = [IdentityConstants.Analytics.ANALYTICS_ID: "aid" ] as [String: Any]
let event = Event(name: "Test Analytics Response Identity", type: EventType.analytics, source: EventSource.responseIdentity, data: eventData)

// test
mockRuntime.simulateComingEvent(event: event)

// verify
let actualEvent = mockRuntime.dispatchedEvents.first(where: { $0.source == EventSource.requestIdentity })
XCTAssertNotNil(actualEvent)
XCTAssertNotNil(actualEvent?.id)
XCTAssertEqual(IdentityConstants.EventNames.AVID_SYNC_EVENT, actualEvent?.name)
XCTAssertEqual(EventType.identity, actualEvent?.type)
let identifierValue = [IdentityConstants.EventDataKeys.ANALYTICS_ID: "aid" ] as [String: String]
XCTAssertEqual(identifierValue, actualEvent?.data?[IdentityConstants.EventDataKeys.IDENTIFIERS]as? [String: String] )
XCTAssertEqual(false, actualEvent?.data?[IdentityConstants.EventDataKeys.FORCE_SYNC]as? Bool)
XCTAssertEqual(true, actualEvent?.data?[IdentityConstants.EventDataKeys.IS_SYNC_EVENT]as? Bool)
XCTAssertEqual(0, actualEvent?.data?[IdentityConstants.EventDataKeys.AUTHENTICATION_STATE]as? Int)
}

/// Tests Handle Analytics Response Identity with empty aid that we don't dispatch an event
func testAnalyticsResponseIdentityWithEmptyAid() {
// setup
let eventData = [IdentityConstants.Analytics.ANALYTICS_ID: "" ] as [String: Any]
let event = Event(name: "Test Analytics Response Identity", type: EventType.analytics, source: EventSource.responseIdentity, data: eventData)

// test
mockRuntime.simulateComingEvent(event: event)

// verify
let actualEvent = mockRuntime.dispatchedEvents.first(where: { $0.source == EventSource.requestIdentity })
XCTAssertNil(actualEvent)
}

/// Tests Handle Analytics Response Identity with no aid key that we don't dispatch an event
func testAnalyticsResponseIdentityWithNoAid() {
// setup
let eventData = ["key": "aid" ] as [String: Any]
let event = Event(name: "Test Analytics Response Identity", type: EventType.analytics, source: EventSource.responseIdentity, data: eventData)

// test
mockRuntime.simulateComingEvent(event: event)

// verify
let actualEvent = mockRuntime.dispatchedEvents.first(where: { $0.source == EventSource.requestIdentity })
XCTAssertNil(actualEvent)
}

/// Tests Handle Analytics Response Identity with no event data that we don't dispatch an event
func testAnalyticsResponseIdentityWithNoEventData() {
// setup
let event = Event(name: "Test Analytics Response Identity", type: EventType.analytics, source: EventSource.responseIdentity, data: nil)

// test
mockRuntime.simulateComingEvent(event: event)

// verify
let actualEvent = mockRuntime.dispatchedEvents.first(where: { $0.source == EventSource.requestIdentity })
XCTAssertNil(actualEvent)
}
}

0 comments on commit 84f7c9c

Please sign in to comment.