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

Improve handling of push token / profile updates #118

Merged
merged 20 commits into from
Dec 5, 2023
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
5 changes: 5 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Run ${{ matrix.config }} tests
run: make CONFIG=${{ matrix.config }} test-library

- uses: kishikawakatsumi/xcresulttool@v1
with:
path: TestResults.xcresult
if: success() || failure()
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ test-all: $(MAKE) CONFIG=debug test-library
test-library:
for platform in "$(PLATFORM_IOS)"; do \
xcodebuild test \
-resultBundlePath TestResults \
-enableCodeCoverage YES \
-configuration=$(CONFIG) \
-scheme klaviyo-swift-sdk-Package \
-destination platform="$$platform" || exit 1; \
Expand Down
8 changes: 5 additions & 3 deletions Sources/KlaviyoSwift/APIRequestErrorHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

import Foundation

let MAX_RETRIES = 50
let MAX_BACKOFF = 60 * 3 // 3 minutes
struct ErrorHandlingConstants {
static let maxRetries = 50
static let maxBackoff = 60 * 3 // 3 minutes
}

private func getDelaySeconds(for count: Int) -> Int {
let delay = Int(pow(2.0, Double(count)))
let jitter = environment.randomInt()
return min(delay + jitter, MAX_BACKOFF)
return min(delay + jitter, ErrorHandlingConstants.maxBackoff)
}

func handleRequestError(request: KlaviyoAPI.KlaviyoRequest, error: KlaviyoAPI.KlaviyoAPIError, retryInfo: RetryInfo) -> KlaviyoAction {
Expand Down
56 changes: 28 additions & 28 deletions Sources/KlaviyoSwift/AppContextInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,27 @@
import Foundation
import UIKit

private let info = Bundle.main.infoDictionary
private let DEFAULT_EXECUTABLE: String = (info?["CFBundleExecutable"] as? String) ??
(ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ?? "Unknown"
private let DEFAULT_BUNDLE_ID: String = info?["CFBundleIdentifier"] as? String ?? "Unknown"
private let DEFAULT_APP_VERSION: String = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
private let DEFAULT_APP_BUILD: String = info?["CFBundleVersion"] as? String ?? "Unknown"
private let DEFAULT_APP_NAME: String = info?["CFBundleName"] as? String ?? "Unknown"
private let DEFAULT_OS_VERSION = ProcessInfo.processInfo.operatingSystemVersion
private let DEFAULT_MANUFACTURER = "Apple"
private let DEFAULT_OS_NAME = "iOS"
private let DEFAULT_DEVICE_MODEL: String = {
var size = 0
sysctlbyname("hw.machine", nil, &size, nil, 0)
var machine = [CChar](repeating: 0, count: size)
sysctlbyname("hw.machine", &machine, &size, nil, 0)
return String(cString: machine)
}()
struct AppContextInfo {
private static let info = Bundle.main.infoDictionary
private static let defaultExecutable: String = (info?["CFBundleExecutable"] as? String) ??
(ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ?? "Unknown"
private static let defaultBundleId: String = info?["CFBundleIdentifier"] as? String ?? "Unknown"
private static let defaultAppVersion: String = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
private static let defaultAppBuild: String = info?["CFBundleVersion"] as? String ?? "Unknown"
private static let defaultAppName: String = info?["CFBundleName"] as? String ?? "Unknown"
private static let defaultOSVersion = ProcessInfo.processInfo.operatingSystemVersion
private static let defaultManufacturer = "Apple"
private static let defaultOSName = "iOS"
private static let defaultDeviceModel: String = {
var size = 0
sysctlbyname("hw.machine", nil, &size, nil, 0)
var machine = [CChar](repeating: 0, count: size)
sysctlbyname("hw.machine", &machine, &size, nil, 0)
return String(cString: machine)
}()

private let DEVICE_ID_STORE_KEY = "_klaviyo_device_id"
private static let deviceIdStoreKey = "_klaviyo_device_id"

struct AppContextInfo {
let executable: String
let bundleId: String
let appVersion: String
Expand All @@ -48,15 +48,15 @@ struct AppContextInfo {
"\(osName) \(osVersion)"
}

init(executable: String = DEFAULT_EXECUTABLE,
bundleId: String = DEFAULT_BUNDLE_ID,
appVersion: String = DEFAULT_APP_VERSION,
appBuild: String = DEFAULT_APP_BUILD,
appName: String = DEFAULT_APP_NAME,
version: OperatingSystemVersion = DEFAULT_OS_VERSION,
osName: String = DEFAULT_OS_NAME,
manufacturer: String = DEFAULT_MANUFACTURER,
deviceModel: String = DEFAULT_DEVICE_MODEL,
init(executable: String = defaultExecutable,
bundleId: String = defaultBundleId,
appVersion: String = defaultAppVersion,
appBuild: String = defaultAppBuild,
appName: String = defaultAppName,
version: OperatingSystemVersion = defaultOSVersion,
osName: String = defaultOSName,
manufacturer: String = defaultManufacturer,
deviceModel: String = defaultDeviceModel,
deviceId: String = UIDevice.current.identifierForVendor?.uuidString ?? "") {
self.executable = executable
self.bundleId = bundleId
Expand Down
108 changes: 85 additions & 23 deletions Sources/KlaviyoSwift/InternalAPIModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extension KlaviyoAPI.KlaviyoRequest {
}
}

let attributes: Attributes
var attributes: Attributes
init(profile: KlaviyoSwift.Profile, anonymousId: String) {
attributes = Attributes(
attributes: profile,
Expand All @@ -73,7 +73,7 @@ extension KlaviyoAPI.KlaviyoRequest {
}
}

let data: Profile
var data: Profile
}

struct CreateEventPayload: Equatable, Codable {
Expand Down Expand Up @@ -111,31 +111,15 @@ extension KlaviyoAPI.KlaviyoRequest {
}

let metric: Metric
let properties: AnyCodable
var properties: AnyCodable
let profile: Profile
let time: Date
let value: Double?
let uniqueId: String
init(attributes: KlaviyoSwift.Event,
anonymousId: String? = nil) {
let context = KlaviyoAPI.KlaviyoRequest._appContextInfo
let metadata = [
"Device ID": context.deviceId,
"Device Manufacturer": context.manufacturer,
"Device Model": context.deviceModel,
"OS Name": context.osName,
"OS Version": context.osVersion,
"SDK Name": __klaviyoSwiftName,
"SDK Version": __klaviyoSwiftVersion,
"App Name": context.appName,
"App ID": context.bundleId,
"App Version": context.appVersion,
"App Build": context.appBuild,
"Push Token": environment.analytics.state().pushToken as Any
]

metric = Metric(name: attributes.metric.name.value)
properties = AnyCodable(attributes.properties.merging(metadata) { _, new in new })
properties = AnyCodable(attributes.properties)
value = attributes.value
time = attributes.time
uniqueId = attributes.uniqueId
Expand All @@ -159,14 +143,34 @@ extension KlaviyoAPI.KlaviyoRequest {
}

var type = "event"
let attributes: Attributes
var attributes: Attributes
init(event: KlaviyoSwift.Event,
anonymousId: String? = nil) {
attributes = .init(attributes: event, anonymousId: anonymousId)
}
}

let data: Event
mutating func appendMetadataToProperties() {
let context = KlaviyoAPI.KlaviyoRequest._appContextInfo
let metadata = [
"Device ID": context.deviceId,
"Device Manufacturer": context.manufacturer,
"Device Model": context.deviceModel,
"OS Name": context.osName,
"OS Version": context.osVersion,
"SDK Name": __klaviyoSwiftName,
"SDK Version": __klaviyoSwiftVersion,
"App Name": context.appName,
"App ID": context.bundleId,
"App Version": context.appVersion,
"App Build": context.appBuild,
"Push Token": environment.analytics.state().pushTokenData?.pushToken as Any
]
let originalProperties = data.attributes.properties.value as? [String: Any] ?? [:]
data.attributes.properties = AnyCodable(originalProperties.merging(metadata) { _, new in new })
}

var data: Event
init(data: Event) {
self.data = data
}
Expand Down Expand Up @@ -294,9 +298,67 @@ extension KlaviyoAPI.KlaviyoRequest {
}
}

struct UnregisterPushTokenPayload: Equatable, Codable {
let data: PushToken

init(pushToken: String,
profile: KlaviyoSwift.Profile,
anonymousId: String) {
data = .init(
pushToken: pushToken,
profile: profile,
anonymousId: anonymousId)
}

struct PushToken: Equatable, Codable {
var type = "push-token-unregister"
var attributes: Attributes

init(pushToken: String,
profile: KlaviyoSwift.Profile,
anonymousId: String) {
attributes = .init(
pushToken: pushToken,
profile: profile,
anonymousId: anonymousId)
}

struct Attributes: Equatable, Codable {
let profile: Profile
let token: String
let platform: String = "ios"
let vendor: String = "APNs"

enum CodingKeys: String, CodingKey {
case token
case platform
case profile
case vendor
}

init(pushToken: String,
profile: KlaviyoSwift.Profile,
anonymousId: String) {
token = pushToken
self.profile = .init(attributes: profile, anonymousId: anonymousId)
}

struct Profile: Equatable, Codable {
let data: CreateProfilePayload.Profile

init(attributes: KlaviyoSwift.Profile,
anonymousId: String) {
data = .init(profile: attributes, anonymousId: anonymousId)
}
}
}
}
}

case createProfile(CreateProfilePayload)
case createEvent(CreateEventPayload)
case registerPushToken(PushTokenPayload)
case unregisterPushToken(UnregisterPushTokenPayload)
}
}

Expand Down Expand Up @@ -391,7 +453,7 @@ struct LegacyEvent: Equatable {

if eventName == "$opened_push" {
// Special handling for $opened_push include push token at the time of open
eventProperties["push_token"] = state.pushToken
eventProperties["push_token"] = state.pushTokenData?.pushToken
}
let identifiers: Event.Identifiers = .init(email: state.email, phoneNumber: state.phoneNumber, externalId: state.externalId)
let event = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.CreateEventPayload.Event(event: .init(name: .CustomEvent(eventName), properties: eventProperties, identifiers: identifiers, profile: customerProperties), anonymousId: state.anonymousId)
Expand Down
6 changes: 4 additions & 2 deletions Sources/KlaviyoSwift/Klaviyo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,12 @@ public struct KlaviyoSDK {

/// Returns the push token for the current user, if any.
public var pushToken: String? {
state.pushToken
state.pushTokenData?.pushToken
}

/// Initialize the swift SDK with the given api key.
/// NOTE: if the SDK has been initialized previously this will result in the profile
/// information being reset and the token data being reassigned (see ``resetProfile()`` for details.)
/// - Parameter apiKey: your public api key from the Klaviyo console
/// - Returns: a KlaviyoSDK instance
@discardableResult
Expand All @@ -402,7 +404,7 @@ public struct KlaviyoSDK {

/// Clears all stored profile identifiers (e.g. email or phone) and starts a new tracked profile.
/// NOTE: if a push token was registered to the current profile, Klaviyo will disassociate it
/// from the current profile. Call ``set(pushToken:)`` again to associate this device to a new profile.
/// from the current profile. Existing token data will be associated with a new anonymous profile.
/// This should be called whenever an active user in your app is removed (e.g. after a logout).

public func resetProfile() {
Expand Down
9 changes: 7 additions & 2 deletions Sources/KlaviyoSwift/KlaviyoAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ extension KlaviyoAPI.KlaviyoRequest {

var url: URL? {
switch endpoint {
case .createProfile, .createEvent, .registerPushToken:
case .createProfile, .createEvent, .registerPushToken, .unregisterPushToken:
return URL(string: "\(environment.analytics.apiURL)/\(path)/?company_id=\(apiKey)")
}
}
Expand All @@ -107,17 +107,22 @@ extension KlaviyoAPI.KlaviyoRequest {
return "client/events"
case .registerPushToken:
return "client/push-tokens"
case .unregisterPushToken:
return "client/push-token-unregister"
}
}

func encodeBody() throws -> Data {
switch endpoint {
case let .createProfile(payload):
return try environment.analytics.encodeJSON(AnyEncodable(payload))
case let .createEvent(payload):
case var .createEvent(payload):
payload.appendMetadataToProperties()
return try environment.analytics.encodeJSON(AnyEncodable(payload))
case let .registerPushToken(payload):
return try environment.analytics.encodeJSON(AnyEncodable(payload))
case let .unregisterPushToken(payload):
return try environment.analytics.encodeJSON(AnyEncodable(payload))
}
}
}
Loading