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

add new headers #165

Merged
merged 10 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 4 additions & 3 deletions Sources/KlaviyoSwift/KlaviyoAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ struct KlaviyoAPI {
static var requestRateLimited: (KlaviyoRequest) -> Void = { _ in }
static var requestHttpError: (KlaviyoRequest, Int, Double) -> Void = { _, _, _ in }

var send: (KlaviyoRequest) async -> Result<Data, KlaviyoAPIError> = { request in
var send: (KlaviyoRequest, Int) async -> Result<Data, KlaviyoAPIError> = { request, attemptNumber in
let start = Date()

var urlRequest: URLRequest
do {
urlRequest = try request.urlRequest()
urlRequest = try request.urlRequest(attemptNumber)
} catch {
requestFailed(request, error, 0.0)
return .failure(.internalRequestError(error))
Expand Down Expand Up @@ -85,7 +85,7 @@ struct KlaviyoAPI {
}

extension KlaviyoAPI.KlaviyoRequest {
func urlRequest() throws -> URLRequest {
func urlRequest(_ attemptNumber: Int = StateManagementConstants.initialAttempt) throws -> URLRequest {
guard let url = url else {
throw KlaviyoAPI.KlaviyoAPIError.internalError("Invalid url string. API URL: \(environment.analytics.apiURL)")
}
Expand All @@ -96,6 +96,7 @@ extension KlaviyoAPI.KlaviyoRequest {
}
request.httpBody = body
request.httpMethod = "POST"
request.setValue("\(attemptNumber)/50", forHTTPHeaderField: "X-Klaviyo-Attempt-Count")

return request
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/KlaviyoSwift/KlaviyoState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ struct KlaviyoState: Equatable, Codable {
var initalizationState = InitializationState.uninitialized
var flushing = false
var flushInterval = StateManagementConstants.wifiFlushInterval
var retryInfo = RetryInfo.retry(0)
var retryInfo = RetryInfo.retry(StateManagementConstants.initialAttempt)
var pendingRequests: [PendingRequest] = []
var pendingProfile: [Profile.ProfileKey: AnyEncodable]?

Expand Down Expand Up @@ -190,7 +190,7 @@ struct KlaviyoState: Equatable, Codable {
}
var attributes = profile.data.attributes
var location = profile.data.attributes.location ?? .init()
var properties = profile.data.attributes.properties.value as? [String: Any] ?? [:]
let properties = profile.data.attributes.properties.value as? [String: Any] ?? [:]
let updatedProfile = Profile.updateProfileWithProperties(dict: pendingProfile)

if let firstName = updatedProfile.firstName {
Expand Down
4 changes: 3 additions & 1 deletion Sources/KlaviyoSwift/NetworkSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func createEmphemeralSession(protocolClasses: [AnyClass] = URLProtocolOverrides.
"User-Agent": NetworkSession.defaultUserAgent,
"revision": NetworkSession.currentApiRevision,
"content-type": NetworkSession.applicationJson,
"accept": NetworkSession.applicationJson
"accept": NetworkSession.applicationJson,
"X-Klaviyo-Mobile": NetworkSession.mobileHeader
]
configuration.protocolClasses = protocolClasses
return URLSession(configuration: configuration)
Expand All @@ -24,6 +25,7 @@ struct NetworkSession {
fileprivate static let currentApiRevision = "2023-07-15"
fileprivate static let applicationJson = "application/json"
fileprivate static let acceptedEncodings = ["br", "gzip", "deflate"]
fileprivate static let mobileHeader = "1"

static let defaultUserAgent = { () -> String in
let appContext = environment.analytics.appContextInfo()
Expand Down
4 changes: 4 additions & 0 deletions Sources/KlaviyoSwift/SDKRequestIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public func requestIterator() -> AsyncStream<SDKRequest> {
KlaviyoAPI.requestFailed = { _, _, _ in }
KlaviyoAPI.requestCompleted = { _, _, _ in }
KlaviyoAPI.requestHttpError = { _, _, _ in }
KlaviyoAPI.requestRateLimited = { _ in }
}
KlaviyoAPI.requestStarted = { request in
continuation.yield(SDKRequest.fromAPIRequest(request: request, response: .inProgress))
Expand All @@ -151,5 +152,8 @@ public func requestIterator() -> AsyncStream<SDKRequest> {
KlaviyoAPI.requestHttpError = { request, statusCode, duration in
continuation.yield(SDKRequest.fromAPIRequest(request: request, response: .httpError(statusCode, duration)))
}
KlaviyoAPI.requestRateLimited = { request in
continuation.yield(SDKRequest.fromAPIRequest(request: request, response: .reqeustError("Rate Limited", 0.0)))
}
}
}
14 changes: 10 additions & 4 deletions Sources/KlaviyoSwift/StateManagement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum StateManagementConstants {
static let cellularFlushInterval = 30.0
static let wifiFlushInterval = 10.0
static let maxQueueSize = 200
static let initialAttempt = 1
}

enum RetryInfo: Equatable {
Expand Down Expand Up @@ -293,7 +294,7 @@ struct KlaviyoReducer: ReducerProtocol {
state.requestsInFlight.removeAll { inflightRequest in
completedRequest.uuid == inflightRequest.uuid
}
state.retryInfo = RetryInfo.retry(0)
state.retryInfo = RetryInfo.retry(StateManagementConstants.initialAttempt)
if state.requestsInFlight.isEmpty {
state.flushing = false
return .none
Expand All @@ -313,8 +314,13 @@ struct KlaviyoReducer: ReducerProtocol {
return .none
}
let retryInfo = state.retryInfo
return .run { send in
let result = await environment.analytics.klaviyoAPI.send(request)
var numAttempts = 1
if case let .retry(attempts) = retryInfo {
numAttempts = attempts
}

return .run { [numAttempts] send in
let result = await environment.analytics.klaviyoAPI.send(request, numAttempts)
switch result {
case .success:
// TODO: may want to inspect response further.
Expand Down Expand Up @@ -361,7 +367,7 @@ struct KlaviyoReducer: ReducerProtocol {
switch retryInfo {
case let .retry(count):
exceededRetries = count > ErrorHandlingConstants.maxRetries
state.retryInfo = .retry(exceededRetries ? 0 : count)
state.retryInfo = .retry(exceededRetries ? 1 : count)
case let .retryWithBackoff(requestCount, totalCount, backOff):
exceededRetries = requestCount > ErrorHandlingConstants.maxRetries
state.retryInfo = .retryWithBackoff(requestCount: exceededRetries ? 0 : requestCount, totalRetryCount: totalCount, currentBackoff: backOff)
Expand Down
44 changes: 29 additions & 15 deletions Tests/KlaviyoSwiftTests/APIRequestErrorHandlingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ import XCTest

let TIMEOUT_NANOSECONDS: UInt64 = 10_000_000_000 // 10 seconds

@MainActor
class APIRequestErrorHandlingTests: XCTestCase {
@MainActor
override func setUp() async throws {
environment = KlaviyoEnvironment.test()
}

// MARK: - http error

@MainActor
func testSendRequestHttpFailureDequesRequest() async throws {
var initialState = INITIALIZED_TEST_STATE()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.httpError(500, TEST_RETURN_DATA)) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.httpError(500, TEST_RETURN_DATA)) }

_ = await store.send(.sendRequest)

Expand All @@ -35,13 +36,14 @@ class APIRequestErrorHandlingTests: XCTestCase {
}
}

@MainActor
func testSendRequestHttpFailureForPhoneNumberResetsStateAndDequesRequest() async throws {
var initialState = INITIALIZED_TEST_STATE_INVALID_PHONE()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_PHONE_NUMBER.data(using: .utf8)!)) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_PHONE_NUMBER.data(using: .utf8)!)) }

_ = await store.send(.sendRequest)

Expand All @@ -57,13 +59,14 @@ class APIRequestErrorHandlingTests: XCTestCase {
}
}

@MainActor
func testSendRequestHttpFailureForEmailResetsStateAndDequesRequest() async throws {
var initialState = INITIALIZED_TEST_STATE_INVALID_EMAIL()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_EMAIL.data(using: .utf8)!)) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_EMAIL.data(using: .utf8)!)) }

_ = await store.send(.sendRequest)

Expand All @@ -81,14 +84,15 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - network error

@MainActor
func testSendRequestFailureIncrementsRetryCount() async throws {
var initialState = INITIALIZED_TEST_STATE()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
let request2 = initialState.buildTokenRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!, pushToken: "new_token", enablement: .authorized)
initialState.requestsInFlight = [request, request2]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) }

_ = await store.send(.sendRequest)

Expand All @@ -100,6 +104,7 @@ class APIRequestErrorHandlingTests: XCTestCase {
}
}

@MainActor
func testSendRequestFailureWithBackoff() async throws {
var initialState = INITIALIZED_TEST_STATE()
initialState.retryInfo = .retryWithBackoff(requestCount: 1, totalRetryCount: 1, currentBackoff: 1)
Expand All @@ -108,7 +113,7 @@ class APIRequestErrorHandlingTests: XCTestCase {
initialState.requestsInFlight = [request, request2]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) }

_ = await store.send(.sendRequest)

Expand All @@ -120,6 +125,7 @@ class APIRequestErrorHandlingTests: XCTestCase {
}
}

@MainActor
func testSendRequestMaxRetries() async throws {
var initialState = INITIALIZED_TEST_STATE()
initialState.retryInfo = .retry(ErrorHandlingConstants.maxRetries)
Expand All @@ -130,7 +136,7 @@ class APIRequestErrorHandlingTests: XCTestCase {
initialState.requestsInFlight = [request, request2]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) }

_ = await store.send(.sendRequest)

Expand All @@ -144,6 +150,7 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - internal error

@MainActor
func testSendRequestInternalError() async throws {
// NOTE: should really happen but putting this in for possible future cases and test coverage
var initialState = INITIALIZED_TEST_STATE()
Expand All @@ -152,7 +159,7 @@ class APIRequestErrorHandlingTests: XCTestCase {
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.internalError("internal error!")) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.internalError("internal error!")) }

_ = await store.send(.sendRequest)

Expand All @@ -166,14 +173,15 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - internal request error

@MainActor
func testSendRequestInternalRequestError() async throws {
var initialState = INITIALIZED_TEST_STATE()

let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.internalRequestError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.internalRequestError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) }

_ = await store.send(.sendRequest)

Expand All @@ -187,14 +195,15 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - unknown error

@MainActor
func testSendRequestUnknownError() async throws {
var initialState = INITIALIZED_TEST_STATE()

let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.unknownError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.unknownError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) }

_ = await store.send(.sendRequest)

Expand All @@ -208,13 +217,14 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - data decoding error

@MainActor
func testSendRequestDataDecodingError() async throws {
var initialState = INITIALIZED_TEST_STATE()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.dataEncodingError(request)) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.dataEncodingError(request)) }

_ = await store.send(.sendRequest)

Expand All @@ -228,13 +238,14 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - invalid data

@MainActor
func testSendRequestInvalidData() async throws {
var initialState = INITIALIZED_TEST_STATE()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.invalidData) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.invalidData) }

_ = await store.send(.sendRequest)

Expand All @@ -248,13 +259,14 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - rate limit error

@MainActor
func testRateLimitErrorWithExistingRetry() async throws {
var initialState = INITIALIZED_TEST_STATE()
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.rateLimitError) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.rateLimitError) }

_ = await store.send(.sendRequest)

Expand All @@ -266,14 +278,15 @@ class APIRequestErrorHandlingTests: XCTestCase {
}
}

@MainActor
func testRateLimitErrorWithExistingBackoffRetry() async throws {
var initialState = INITIALIZED_TEST_STATE()
initialState.retryInfo = .retryWithBackoff(requestCount: 2, totalRetryCount: 2, currentBackoff: 4)
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.rateLimitError) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.rateLimitError) }

_ = await store.send(.sendRequest)

Expand All @@ -287,14 +300,15 @@ class APIRequestErrorHandlingTests: XCTestCase {

// MARK: - Missing or invalid response

@MainActor
func testMissingOrInvalidResponse() async throws {
var initialState = INITIALIZED_TEST_STATE()
initialState.retryInfo = .retryWithBackoff(requestCount: 2, totalRetryCount: 2, currentBackoff: 4)
let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!)
initialState.requestsInFlight = [request]
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())

environment.analytics.klaviyoAPI.send = { _ in .failure(.missingOrInvalidResponse(nil)) }
environment.analytics.klaviyoAPI.send = { _, _ in .failure(.missingOrInvalidResponse(nil)) }

_ = await store.send(.sendRequest)

Expand Down
3 changes: 2 additions & 1 deletion Tests/KlaviyoSwiftTests/AppLifeCycleEventsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Combine
import Foundation
import XCTest

@MainActor
class AppLifeCycleEventsTests: XCTestCase {
let passThroughSubject = PassthroughSubject<Notification, Never>()

Expand All @@ -25,12 +24,14 @@ class AppLifeCycleEventsTests: XCTestCase {
}
}

@MainActor
override func setUp() {
environment = KlaviyoEnvironment.test()
}

// MARK: - App Terminate

@MainActor
func testAppTerminateStopsReachability() {
environment.notificationCenterPublisher = getFilteredNotificaitonPublished(name: UIApplication.willTerminateNotification)
let expection = XCTestExpectation(description: "Stop reachability is called.")
Expand Down
Loading
Loading