Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: context container in event payload #130

Merged
merged 4 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import Combine
import os

public class Confidence: ConfidenceEventSender {
public let clientSecret: String
Expand Down Expand Up @@ -113,8 +114,8 @@ public class Confidence: ConfidenceEventSender {
.eraseToAnyPublisher()
}

public func track(eventName: String, message: ConfidenceStruct) {
eventSenderEngine.emit(
public func track(eventName: String, message: ConfidenceStruct) throws {
try eventSenderEngine.emit(
eventName: eventName,
message: message,
context: getContext()
Expand All @@ -128,9 +129,14 @@ public class Confidence: ConfidenceEventSender {
guard let self = self else {
return
}
self.track(eventName: event.name, message: event.message)
if event.shouldFlush {
eventSenderEngine.flush()
do {
try self.track(eventName: event.name, message: event.message)
if event.shouldFlush {
eventSenderEngine.flush()
}
} catch {
Logger(subsystem: "com.confidence", category: "track").warning(
"Error from EventProducer, failed to track event: \(event.name)")
}
}
.store(in: &cancellables)
Expand Down
3 changes: 3 additions & 0 deletions Sources/Confidence/ConfidenceError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum ConfidenceError: Error, Equatable {
case internalError(message: String)
case parseError(message: String)
case invalidContextError
case invalidContextInMessage
}

extension ConfidenceError: CustomStringConvertible {
Expand Down Expand Up @@ -62,6 +63,8 @@ extension ConfidenceError: CustomStringConvertible {
return "Flag not found for key \(key)"
case .invalidContextError:
return "Invalid context error"
case .invalidContextInMessage:
return "Field 'context' is not allowed in event's data"
}
}
}
4 changes: 2 additions & 2 deletions Sources/Confidence/ConfidenceEventSender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ public protocol ConfidenceEventSender: Contextual {
Upon return, the event has been correctly stored and will be emitted to the backend
according to the configured flushing logic
*/
func track(eventName: String, message: ConfidenceStruct)
func track(eventName: String, message: ConfidenceStruct) throws
/**
The ConfidenceProducer can be used to push context changes or event tracking
*/
func track(producer: ConfidenceProducer)
func track(producer: ConfidenceProducer) throws

/**
Schedule a manual flush of the event data currently stored on disk
Expand Down
6 changes: 3 additions & 3 deletions Sources/Confidence/EventSenderEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ protocol FlushPolicy {
}

protocol EventSenderEngine {
func emit(eventName: String, message: ConfidenceStruct, context: ConfidenceStruct)
func emit(eventName: String, message: ConfidenceStruct, context: ConfidenceStruct) throws
func shutdown()
func flush()
}
Expand Down Expand Up @@ -119,10 +119,10 @@ final class EventSenderEngineImpl: EventSenderEngine {
semaphore.signal()
}

func emit(eventName: String, message: ConfidenceStruct, context: ConfidenceStruct) {
func emit(eventName: String, message: ConfidenceStruct, context: ConfidenceStruct) throws {
writeReqChannel.send(ConfidenceEvent(
name: eventName,
payload: payloadMerger.merge(context: context, message: message),
payload: try payloadMerger.merge(context: context, message: message),
eventTime: Date.backport.now)
)
}
Expand Down
11 changes: 7 additions & 4 deletions Sources/Confidence/PayloadMerger.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import Foundation

internal protocol PayloadMerger {
func merge(context: ConfidenceStruct, message: ConfidenceStruct) -> ConfidenceStruct
func merge(context: ConfidenceStruct, message: ConfidenceStruct) throws -> ConfidenceStruct
}

internal struct PayloadMergerImpl: PayloadMerger {
func merge(context: ConfidenceStruct, message: ConfidenceStruct) -> ConfidenceStruct {
var map: ConfidenceStruct = context
map += message
func merge(context: ConfidenceStruct, message: ConfidenceStruct) throws -> ConfidenceStruct {
guard message["context"] == nil else {
throw ConfidenceError.invalidContextInMessage
}
var map: ConfidenceStruct = message
map["context"] = ConfidenceValue.init(structure: context)
return map
}
}
Expand Down
11 changes: 11 additions & 0 deletions Tests/ConfidenceTests/ConfidenceFeatureProviderTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,17 @@ class ConfidenceFeatureProviderTest: XCTestCase {
XCTAssertEqual(client.resolveStats, 1)
XCTAssertEqual(flagApplier.applyCallCount, 0)
}

func testInvalidContextInMessage() async throws {
let confidence = Confidence.Builder(clientSecret: "test")
.build()

XCTAssertThrowsError(
try confidence.track(eventName: "test", message: ["context": ConfidenceValue(string: "test")])
) { error in
XCTAssertEqual(error as? ConfidenceError, ConfidenceError.invalidContextInMessage)
}
}
}

final class DispatchQueueFake: DispatchQueueType {
Expand Down
28 changes: 16 additions & 12 deletions Tests/ConfidenceTests/EventSenderEngineTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,27 @@ final class EventSenderEngineTest: XCTestCase {
let cancellable = uploaderMock.subject.sink { _ in
expectation.fulfill()
}
eventSenderEngine.emit(
try eventSenderEngine.emit(
eventName: "my_event",
message: [
"a": .init(integer: 0),
"message": .init(integer: 1),
"a": .init(integer: 0)
],
context: [
"a": .init(integer: 2),
"message": .init(integer: 3) // the root "message" overrides this
"d": .init(integer: 3)
])


wait(for: [expectation], timeout: 5)
XCTAssertEqual(try XCTUnwrap(uploaderMock.calledRequest)[0].eventDefinition, "my_event")
XCTAssertEqual(try XCTUnwrap(uploaderMock.calledRequest)[0].payload, NetworkStruct(fields: [
"a": .number(0.0),
"message": .number(1.0)
"context": .structure(
.init(fields: [
"a": .number(2),
"d": .number(3)
])
)
]))
cancellable.cancel()
}
Expand All @@ -96,7 +100,7 @@ final class EventSenderEngineTest: XCTestCase {
writeQueue: writeQueue
)

eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
try eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
// TODO: We need to wait for writeReqChannel to complete to make this test meaningful
XCTAssertNil(uploaderMock.calledRequest)
}
Expand All @@ -115,7 +119,7 @@ final class EventSenderEngineTest: XCTestCase {
flushPolicies: [ImmidiateFlushPolicy()],
writeQueue: writeQueue
)
eventSenderEngine.emit(eventName: "testEvent", message: ConfidenceStruct(), context: ConfidenceStruct())
try eventSenderEngine.emit(eventName: "testEvent", message: ConfidenceStruct(), context: ConfidenceStruct())
let expectation = expectation(description: "events batched")
storageMock.eventsRemoved{
expectation.fulfill()
Expand All @@ -140,7 +144,7 @@ final class EventSenderEngineTest: XCTestCase {
writeQueue: writeQueue
)

eventSenderEngine.emit(eventName: "testEvent", message: ConfidenceStruct(), context: ConfidenceStruct())
try eventSenderEngine.emit(eventName: "testEvent", message: ConfidenceStruct(), context: ConfidenceStruct())

writeQueue.sync {
XCTAssertEqual(storageMock.isEmpty(), false)
Expand All @@ -157,10 +161,10 @@ final class EventSenderEngineTest: XCTestCase {
writeQueue: writeQueue
)

eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
try eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
try eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
try eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])
try eventSenderEngine.emit(eventName: "Hello", message: [:], context: [:])


writeQueue.sync {
Expand Down
24 changes: 20 additions & 4 deletions Tests/ConfidenceTests/PayloadMergerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,31 @@ import XCTest
@testable import Confidence

class PayloadMergerTests: XCTestCase {
func testMerge() {
func testMerge() throws {
let context = ["a": ConfidenceValue(string: "hello"), "b": ConfidenceValue(string: "world")]
let message = ["b": ConfidenceValue(string: "west"), "c": ConfidenceValue(string: "world")]
let expected = [
"a": ConfidenceValue(string: "hello"),
"b": ConfidenceValue(string: "west"),
"c": ConfidenceValue(string: "world")
"c": ConfidenceValue(string: "world"),
"context": ConfidenceValue(structure: [
"a": ConfidenceValue(string: "hello"),
"b": ConfidenceValue(string: "world"),
])
]
let merged = PayloadMergerImpl().merge(context: context, message: message)
let merged = try PayloadMergerImpl().merge(context: context, message: message)
XCTAssertEqual(merged, expected)
}

func testInvalidMessage() throws {
let context = ["a": ConfidenceValue(string: "hello"), "b": ConfidenceValue(string: "world")]
let message = [
"b": ConfidenceValue(string: "west"),
"context": ConfidenceValue(string: "world") // simple value context is lost
]
XCTAssertThrowsError(
try PayloadMergerImpl().merge(context: context, message: message)
) { error in
XCTAssertEqual(error as? ConfidenceError, ConfidenceError.invalidContextInMessage)
}
}
}
Loading