Skip to content

Commit

Permalink
Merge pull request #121 from adobe/staging
Browse files Browse the repository at this point in the history
Staging to Main for 4.1.0 release
  • Loading branch information
cdhoffmann authored Oct 19, 2023
2 parents dc78d1c + a29cfa8 commit 3b824d7
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 50 deletions.
2 changes: 1 addition & 1 deletion AEPAssurance.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "AEPAssurance"
s.version = "4.0.0"
s.version = "4.1.0"
s.summary = "AEPAssurance SDK for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe."

s.description = <<-DESC
Expand Down
4 changes: 4 additions & 0 deletions AEPAssurance.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
7527DCA22926F15300FE0D8C /* AssuranceConnectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7527DCA12926F15300FE0D8C /* AssuranceConnectionDelegate.swift */; };
753867C32925735D0021BC3F /* AssuranceAuthorizingPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753867C22925735D0021BC3F /* AssuranceAuthorizingPresentation.swift */; };
753867C529257CFE0021BC3F /* AssuranceStatusPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753867C429257CFE0021BC3F /* AssuranceStatusPresentation.swift */; };
755A475D2A377F2A00FE00AA /* htmlSampleEscaped.txt in Resources */ = {isa = PBXBuildFile; fileRef = 755A475C2A377F2A00FE00AA /* htmlSampleEscaped.txt */; };
7881FA5928D50C8D0051F902 /* QuickConnectServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7881FA5828D50C8D0051F902 /* QuickConnectServiceTests.swift */; };
B601172227BAE3EF006D3968 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B601172127BAE3EF006D3968 /* Connection.swift */; };
B601172527BAE4AA006D3968 /* AdobeLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B601172427BAE4AA006D3968 /* AdobeLogo.swift */; };
Expand Down Expand Up @@ -227,6 +228,7 @@
7527DCA12926F15300FE0D8C /* AssuranceConnectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssuranceConnectionDelegate.swift; sourceTree = "<group>"; };
753867C22925735D0021BC3F /* AssuranceAuthorizingPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssuranceAuthorizingPresentation.swift; sourceTree = "<group>"; };
753867C429257CFE0021BC3F /* AssuranceStatusPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssuranceStatusPresentation.swift; sourceTree = "<group>"; };
755A475C2A377F2A00FE00AA /* htmlSampleEscaped.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = htmlSampleEscaped.txt; sourceTree = "<group>"; };
7881FA5828D50C8D0051F902 /* QuickConnectServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectServiceTests.swift; sourceTree = "<group>"; };
96C170474540581D23D5C489 /* Pods_TestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B601171F27BAE193006D3968 /* connection.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = connection.png; sourceTree = "<group>"; };
Expand Down Expand Up @@ -425,6 +427,7 @@
B603FA0827AB1D270033416B /* htmlSample.txt */,
B603FA0327A9F51B0033416B /* 40KBString.txt */,
B603F9FE27A9EC7D0033416B /* 20KBString.txt */,
755A475C2A377F2A00FE00AA /* htmlSampleEscaped.txt */,
);
path = Resources;
sourceTree = "<group>";
Expand Down Expand Up @@ -971,6 +974,7 @@
B603FA0427A9F51B0033416B /* 40KBString.txt in Resources */,
B6DB487F27B1D73500166FC4 /* 5KBString.txt in Resources */,
B603FA0927AB1D270033416B /* htmlSample.txt in Resources */,
755A475D2A377F2A00FE00AA /* htmlSampleEscaped.txt in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
6 changes: 3 additions & 3 deletions AEPAssurance/Source/AssuranceConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Foundation
enum AssuranceConstants {
static let EXTENSION_NAME = "com.adobe.assurance"
static let FRIENDLY_NAME = "Assurance"
static let EXTENSION_VERSION = "4.0.0"
static let EXTENSION_VERSION = "4.1.0"
static let LOG_TAG = FRIENDLY_NAME
static let DEFAULT_ENVIRONMENT = AssuranceEnvironment.prod

Expand Down Expand Up @@ -92,8 +92,8 @@ enum AssuranceConstants {
}

enum SharedStateKeys {
static let CLIENT_ID = "sessionid"
static let SESSION_ID = "clientid"
static let CLIENT_ID = "clientid"
static let SESSION_ID = "sessionid"
static let INTEGRATION_ID = "integrationid"
}

Expand Down
52 changes: 51 additions & 1 deletion AEPAssurance/Source/AssuranceEventChunker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@
import AEPServices
import Foundation

///
/// An EventChunker used for chunking and stitching of AssuranceEvents
///
protocol EventChunker {
func chunk(_ event: AssuranceEvent) -> [AssuranceEvent]
func stitch(_ chunkedEvents: [AssuranceEvent]) -> AssuranceEvent?
}

/// Class that brings the capability to chunk the AssuranceEvent if in need to satisfy the socket size limit.
struct AssuranceEventChunker {
struct AssuranceEventChunker: EventChunker {

/// The maximum size of data that an `AssuranceEvent` payload can hold after chunking
///
Expand Down Expand Up @@ -102,4 +110,46 @@ struct AssuranceEventChunker {
}
return chunkedEvents
}

///
/// Stitches chunked events together into one `AssuranceEvent` where the stitched events chunkData is now the new event's payload
///
/// - Parameter chunkedEvents: An array of chunked AssuranceEvents
/// - Returns: An `AssuranceEvent` which has the stitched data as the payload
func stitch(_ chunkedEvents: [AssuranceEvent]) -> AssuranceEvent? {
//exit early if chunkedEvents is empty
guard !chunkedEvents.isEmpty else { return nil }

var stitchedString = ""
// Stitch chunked payload data together
for event in chunkedEvents {
// Extract the payload string and unescape it. Currently escaped by the services
guard let payloadString = event.payload?[AssuranceConstants.AssuranceEvent.PayloadKey.CHUNK_DATA]?.stringValue else {
Log.warning(label: AssuranceConstants.LOG_TAG, "Error while attempting to stitch chunked event: Chunk payload was not in proper string format.")
return nil
}

stitchedString += payloadString
}
guard let stitchedStringData = stitchedString.data(using: .utf8) else {
Log.warning(label: AssuranceConstants.LOG_TAG, "Error while attempting to create data from stitched string")
return nil
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
var decodedPayload: [String: AnyCodable]? = nil
do {
decodedPayload = try decoder.decode([String:AnyCodable].self, from: stitchedStringData)
} catch {
Log.warning(label: AssuranceConstants.LOG_TAG, "Error while attempting to decode stitched JSON data: \(error)")
return nil
}
let referenceEvent = chunkedEvents[0]
return AssuranceEvent(type: referenceEvent.type,
payload: decodedPayload,
timestamp: referenceEvent.timestamp ?? Date(),
vendor: referenceEvent.vendor)

}
}
64 changes: 62 additions & 2 deletions AEPAssurance/Source/AssuranceSession+SocketDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,17 @@ extension AssuranceSession: SocketDelegate {
/// - event - the `AssuranceEvent` received from socket
func webSocket(_ socket: SocketConnectable, didReceiveEvent event: AssuranceEvent) {
Log.trace(label: AssuranceConstants.LOG_TAG, "Received event from assurance session - \(event.description)")

var eventToQueue = event
// Handle Chunked event before queuing
if event.isChunkedEvent {
guard let stitchedEvent = stitchEvent(event: event) else {
// Exit early if stitching fails
return
}
eventToQueue = stitchedEvent
}
// add the incoming event to inboundQueue and process them
inboundQueue.enqueue(newElement: event)
inboundQueue.enqueue(newElement: eventToQueue)
inboundSource.add(data: 1)
}

Expand All @@ -147,5 +155,57 @@ extension AssuranceSession: SocketDelegate {
stateManager.connectedWebSocketURL = socket.socketURL?.absoluteString
}
}

///
/// Organizes the events into the `chunkedEvents` dictionary by `chunkedID`, and stitches the
/// given event once we have received all of the chunks
///
/// - Parameter event: The chunked event to be stitched
/// - Returns: An `AssuranceEvent` which has the stitched data as the payload
private func stitchEvent(event: AssuranceEvent) -> AssuranceEvent? {
if let chunkedID = event.chunkedID {
// New chunked event received
if self.chunkedEvents[chunkedID] == nil {
self.chunkedEvents[chunkedID] = [event]
} else {
// Chunked event exists, add chunk
self.chunkedEvents[chunkedID]?.append(event)
// Check if this is the last chunk
if chunkedEvents[chunkedID]?.count == event.chunkedTotal {
// Sort and Stitch
if let sortedChunks = self.chunkedEvents[chunkedID]?.sorted(by: { $0.chunkedSequenceNumber! < $1.chunkedSequenceNumber! }) {
defer { self.chunkedEvents.removeValue(forKey: chunkedID) }
return socket.eventChunker.stitch(sortedChunks)
}
}
}
}
return nil
}

}

///
/// Extension on AssuranceEvent used for chunked event properties
///
fileprivate extension AssuranceEvent {
var isChunkedEvent: Bool {
if self.metadata?[AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID] != nil {
return true
} else {
return false
}
}

var chunkedID: String? {
return self.metadata?[AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID]?.stringValue
}

var chunkedSequenceNumber: Int? {
return self.metadata?[AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE]?.intValue
}

var chunkedTotal: Int? {
return self.metadata?[AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL]?.intValue
}
}
1 change: 1 addition & 0 deletions AEPAssurance/Source/AssuranceSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class AssuranceSession {
let sessionOrchestrator: AssuranceSessionOrchestrator
let outboundQueue: ThreadSafeQueue = ThreadSafeQueue<AssuranceEvent>(withLimit: 200)
let inboundQueue: ThreadSafeQueue = ThreadSafeQueue<AssuranceEvent>(withLimit: 200)
var chunkedEvents: [String: [AssuranceEvent]] = [:]
let inboundSource: DispatchSourceUserDataAdd = DispatchSource.makeUserDataAddSource(queue: DispatchQueue.global(qos: .default))
let outboundSource: DispatchSourceUserDataAdd = DispatchSource.makeUserDataAddSource(queue: DispatchQueue.global(qos: .default))
let pluginHub: PluginHub = PluginHub()
Expand Down
3 changes: 3 additions & 0 deletions AEPAssurance/Source/Socket/SocketConnectable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import Foundation
protocol SocketConnectable {
/// the web socket URL
var socketURL: URL? { get }

/// The event chunker used for chunking and stitching Events
var eventChunker: EventChunker { get }

/// the delegate that gets notified on socket events.
var delegate: SocketDelegate { get }
Expand Down
3 changes: 2 additions & 1 deletion AEPAssurance/Source/Socket/WebView/WebViewSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class WebViewSocket: NSObject, SocketConnectable, WKNavigationDelegate, WKScript

var delegate: SocketDelegate
var socketURL: URL?
let eventChunker = AssuranceEventChunker()

var eventChunker: EventChunker = AssuranceEventChunker()

/// variable tracking the current socket status
var socketState: SocketState = .unknown {
Expand Down
64 changes: 64 additions & 0 deletions AEPAssurance/UnitTests/AssuranceEventChunkerTests.swift

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions AEPAssurance/UnitTests/AssuranceSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,74 @@ class AssuranceSessionTests: XCTestCase {
wait(for: [stateManager.expectation!], timeout: 2.0)
XCTAssertTrue(stateManager.getAllExtensionStateDataCalled)
}

func test_session_receives_chunkedEvents() {
let mockEventChunker = MockEventChunker()
mockSocket.eventChunker = mockEventChunker
mockEventChunker.eventToReturn = tacoEvent
session.pluginHub.registerPlugin(mockPlugin, toSession: session)
mockPlugin.expectation = XCTestExpectation(description: "sends inbound event to respective plugin")
let chunkEvent1 = AssuranceEvent(type: "control",
payload: ["type":"Test"],
metadata: [AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID: "testChunkID",
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE: 1,
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL: 3])

let chunkEvent2 = AssuranceEvent(type: "control",
payload: ["type":"Test"],
metadata: [AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID: "testChunkID",
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE: 2,
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL: 3])

let chunkEvent3 = AssuranceEvent(type: "control",
payload: ["type":"Test"],
metadata: [AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID: "testChunkID",
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE: 3,
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL: 3])

session.webSocket(mockSocket, didReceiveEvent: chunkEvent1)
session.webSocket(mockSocket, didReceiveEvent: chunkEvent2)
session.webSocket(mockSocket, didReceiveEvent: chunkEvent3)

wait(for: [mockPlugin.expectation!], timeout: 2.0)
XCTAssertTrue(mockPlugin.eventReceived)
XCTAssertTrue(mockEventChunker.stitchCalled)
XCTAssertEqual(0, session.inboundQueue.size())
}

func test_session_receives_chunkedEvent_stitchFails() {
let mockEventChunker = MockEventChunker()
mockSocket.eventChunker = mockEventChunker
session.pluginHub.registerPlugin(mockPlugin, toSession: session)
mockPlugin.expectation = XCTestExpectation(description: "sends inbound event to respective plugin")
mockPlugin.expectation?.isInverted = true
let chunkEvent1 = AssuranceEvent(type: "control",
payload: ["type":"Test"],
metadata: [AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID: "testChunkID",
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE: 1,
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL: 3])

let chunkEvent2 = AssuranceEvent(type: "control",
payload: ["type":"Test"],
metadata: [AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID: "testChunkID",
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE: 2,
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL: 3])

let chunkEvent3 = AssuranceEvent(type: "control",
payload: ["type":"Test"],
metadata: [AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_ID: "testChunkID",
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_SEQUENCE: 3,
AssuranceConstants.AssuranceEvent.MetadataKey.CHUNK_TOTAL: 3])

session.webSocket(mockSocket, didReceiveEvent: chunkEvent1)
session.webSocket(mockSocket, didReceiveEvent: chunkEvent2)
session.webSocket(mockSocket, didReceiveEvent: chunkEvent3)

wait(for: [mockPlugin.expectation!], timeout: 2.0)
XCTAssertFalse(mockPlugin.eventReceived)
XCTAssertTrue(mockEventChunker.stitchCalled)
XCTAssertEqual(0, session.inboundQueue.size())
}


func test_session_disconnect() throws {
Expand Down
20 changes: 20 additions & 0 deletions AEPAssurance/UnitTests/Mocks/MockSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class MockSocket: SocketConnectable {
var socketURL: URL?
var delegate: SocketDelegate
var socketState: SocketState
var eventChunker: EventChunker = MockEventChunker()

required init(withDelegate delegate: SocketDelegate) {
self.delegate = delegate
Expand Down Expand Up @@ -51,3 +52,22 @@ class MockSocket: SocketConnectable {
self.socketState = state
}
}

class MockEventChunker: EventChunker {

var chunkCalled = false
var chunkedEventsToReturn: [AssuranceEvent] = []
func chunk(_ event: AEPAssurance.AssuranceEvent) -> [AEPAssurance.AssuranceEvent] {
chunkCalled = true
return chunkedEventsToReturn
}

var stitchCalled = false
var eventToReturn: AssuranceEvent? = nil
func stitch(_ chunkedEvents: [AEPAssurance.AssuranceEvent]) -> AEPAssurance.AssuranceEvent? {
stitchCalled = true
return eventToReturn
}


}
2 changes: 2 additions & 0 deletions AEPAssurance/UnitTests/PluginHubTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,11 @@ class PluginTaco: AssurancePlugin {
}

var eventReceived = false
var receivedEvent: AssuranceEvent? = nil
func receiveEvent(_ event: AssuranceEvent) {
expectation?.fulfill()
eventReceived = true
receivedEvent = event
}

var isSessionConnectedCalled = false
Expand Down
1 change: 1 addition & 0 deletions AEPAssurance/UnitTests/Resources/htmlSampleEscaped.txt

Large diffs are not rendered by default.

Loading

0 comments on commit 3b824d7

Please sign in to comment.