Skip to content
14 changes: 11 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ let package = Package(
targets: [
.target(
name: "KlaviyoCore",
dependencies: [.product(name: "AnyCodable", package: "AnyCodable")],
dependencies: [
.product(name: "AnyCodable", package: "AnyCodable")
],
path: "Sources/KlaviyoCore"
),
.testTarget(
Expand All @@ -51,7 +53,10 @@ let package = Package(
),
.target(
name: "KlaviyoSwift",
dependencies: [.product(name: "AnyCodable", package: "AnyCodable"), "KlaviyoCore"],
dependencies: [
.product(name: "AnyCodable", package: "AnyCodable"),
"KlaviyoCore"
],
path: "Sources/KlaviyoSwift",
resources: [.copy("PrivacyInfo.xcprivacy")]
),
Expand All @@ -71,7 +76,10 @@ let package = Package(
),
.target(
name: "KlaviyoForms",
dependencies: ["KlaviyoSwift"],
dependencies: [
"KlaviyoSwift",
"KlaviyoCore"
],
path: "Sources/KlaviyoForms",
resources: [
.process("InAppForms/Assets"),
Expand Down
44 changes: 41 additions & 3 deletions Sources/KlaviyoCore/Utils/Logger+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,40 @@
// Created by Andrew Balmer on 7/25/25.
//

import Foundation
import OSLog

// MARK: - Logging Configuration

/// Thread-safe global logging toggle for the Klaviyo SDK.
///

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using package access level instead of public.

Since the package uses swift-tools-version: 5.9, you can use the package access level here. This would make KlaviyoLogConfig accessible from all modules within the package (KlaviyoCore, KlaviyoSwift, KlaviyoForms, KlaviyoLocation) but not from external SDK consumers.

Currently, with public access, SDK consumers can bypass the intended KlaviyoSDK.setLoggingEnabled(_:) API and directly call KlaviyoLogConfig.shared.isLoggingEnabled = false. Using package access would enforce the designed API surface:

package final class KlaviyoLogConfig: @unchecked Sendable {
    package static let shared = KlaviyoLogConfig()

    private let lock = NSLock()
    private var _isLoggingEnabled: Bool = true

    private init() {}

    package var isLoggingEnabled: Bool {
        // ...
    }
}

This ensures consumers use KlaviyoSDK().setLoggingEnabled(_:) as intended.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in da46dc7 — Changed KlaviyoLogConfig from public to package access level (class, shared instance, and isLoggingEnabled property). SDK consumers must now use KlaviyoSDK().setLoggingEnabled(_:) as the intended API surface.

/// When logging is disabled, all `Logger` instances across every module
/// return `Logger(OSLog.disabled)`, which the OS optimises to a no-op.
package final class KlaviyoLogConfig: @unchecked Sendable {
package static let shared = KlaviyoLogConfig()

private let lock = NSLock()
private var _isLoggingEnabled: Bool = true

private init() {}

/// Whether logging is currently enabled across all Klaviyo SDK modules.
package var isLoggingEnabled: Bool {
get {
lock.lock()
defer { lock.unlock() }
return _isLoggingEnabled
}
set {
lock.lock()
defer { lock.unlock() }
_isLoggingEnabled = newValue
}
}
}

// MARK: - Logger Convenience Initializers

@available(iOS 14.0, *)
extension Logger {
private static var subsystem = "com.klaviyo.klaviyo-swift-sdk.klaviyoCore"
Expand All @@ -21,11 +53,17 @@ extension Logger {
@available(iOS 14.0, *)
extension Logger {
/// Logger for ``Codable`` events (JSON encoding & decoding)
static let codable = Logger(category: "Encoding/Decoding Logger")
static var codable: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Encoding/Decoding Logger") : Logger(OSLog.disabled)
}

/// Logger for networking events
static let networking = Logger(category: "Networking")
static var networking: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Networking") : Logger(OSLog.disabled)
}

/// Logger for app navigation and deep linking events
static let navigation = Logger(category: "Linking and Navigation")
static var navigation: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Linking and Navigation") : Logger(OSLog.disabled)
}
}
6 changes: 5 additions & 1 deletion Sources/KlaviyoCore/Utils/LoggerClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ public struct LoggerClient {
}

public var error: (String) -> Void
public static let production = Self(error: { message in os_log("%{public}s", type: .error, message) })
public static let production = Self(error: { message in
guard KlaviyoLogConfig.shared.isLoggingEnabled else { return }
os_log("%{public}s", type: .error, message)
})
}

@usableFromInline
Expand All @@ -28,6 +31,7 @@ func runtimeWarn(
line: UInt? = nil
) {
#if DEBUG
guard KlaviyoLogConfig.shared.isLoggingEnabled else { return }
Comment thread
cursor[bot] marked this conversation as resolved.
let message = message()
let category = category ?? "Runtime Warning"
#if canImport(os)
Expand Down
13 changes: 10 additions & 3 deletions Sources/KlaviyoForms/Utilities/Logger+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Andrew Balmer on 1/28/25.
//

import KlaviyoCore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing explicit dependency in Package.swift.

This file now imports KlaviyoCore directly, but the KlaviyoForms target in Package.swift only declares KlaviyoSwift as a dependency:

.target(
    name: "KlaviyoForms",
    dependencies: ["KlaviyoSwift"],  // ← no explicit KlaviyoCore
    ...
)

This works today because KlaviyoSwift transitively depends on KlaviyoCore, but relying on transitive imports is fragile. If KlaviyoSwift ever refactored its dependency graph, this would silently break.

Please add "KlaviyoCore" as an explicit dependency:

.target(
    name: "KlaviyoForms",
    dependencies: ["KlaviyoSwift", "KlaviyoCore"],
    ...
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in da46dc7 — Added "KlaviyoCore" as an explicit dependency for the KlaviyoForms target in Package.swift.

import OSLog

@available(iOS 14.0, *)
Expand All @@ -21,13 +22,19 @@ extension Logger {
@available(iOS 14.0, *)
extension Logger {
/// Logger for Javascript console log messages from a WKWebView relayed to the native layer.
static let webViewConsoleLogger = Logger(category: "WKWebView Console Log Relay")
static var webViewConsoleLogger: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "WKWebView Console Log Relay") : Logger(OSLog.disabled)
}

/// Logger for WKWebView related events.
///
/// - Note: Javascript console logs relayed to the native layer should be handled by the ``webViewConsoleLogger``.
static let webViewLogger = Logger(category: "WKWebView Event Handling")
static var webViewLogger: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "WKWebView Event Handling") : Logger(OSLog.disabled)
}

/// Logger for filesystem operations.
static let filesystem = Logger(category: "Filesystem")
static var filesystem: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Filesystem") : Logger(OSLog.disabled)
}
}
5 changes: 4 additions & 1 deletion Sources/KlaviyoLocation/Utilities/Logger+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
// Created by Andrew Balmer on 10/8/24.
//

import KlaviyoCore
import OSLog

@available(iOS 14.0, *)
extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier ?? ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just now catching this -- not major but shouldn't this be "com.klaviyo.klaviyo-swift-sdk.klaviyoLocation" to be consistent with the other subsystems

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5970121 — the KlaviyoLocation subsystem is now "com.klaviyo.klaviyo-swift-sdk.klaviyoLocation", consistent with the other modules.


Generated by Claude Code


/// Logs events related to location services.
static let geoservices = Logger(subsystem: subsystem, category: "geoservices")
static var geoservices: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(subsystem: subsystem, category: "geoservices") : Logger(OSLog.disabled)
}
}
22 changes: 22 additions & 0 deletions Sources/KlaviyoSwift/Klaviyo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ public struct KlaviyoSDK {
state.pushTokenData?.pushToken
}

/// Whether logging is currently enabled for the Klaviyo SDK.
///
/// Logging is enabled by default. When disabled, all `os.Logger` output,
/// legacy `LoggerClient` error logging, and runtime warnings are silenced.
public var isLoggingEnabled: Bool {
KlaviyoLogConfig.shared.isLoggingEnabled
}

/// Enable or disable logging for the Klaviyo SDK.
///
/// When disabled, all log output across KlaviyoCore, KlaviyoSwift,
/// KlaviyoForms, and KlaviyoLocation is silenced. Re-enabling restores
/// logging immediately.
///
/// - Parameter enabled: Pass `true` to enable logging, `false` to disable.
/// - Returns: The current `KlaviyoSDK` instance, for chaining.
@discardableResult
public func setLoggingEnabled(_ enabled: Bool) -> KlaviyoSDK {
KlaviyoLogConfig.shared.isLoggingEnabled = enabled
return self
}

/// 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.)
Expand Down
4 changes: 3 additions & 1 deletion Sources/KlaviyoSwift/Utilities/EventBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import OSLog

@available(iOS 14.0, *)
extension Logger {
fileprivate static let eventBuffer = Logger(subsystem: "com.klaviyo.klaviyo-swift-sdk.klaviyoSwift", category: "Event Buffering")
fileprivate static var eventBuffer: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(subsystem: "com.klaviyo.klaviyo-swift-sdk.klaviyoSwift", category: "Event Buffering") : Logger(OSLog.disabled)
}
}

/// Manages a thread-safe buffer of recent events for replay to new subscribers.
Expand Down
17 changes: 13 additions & 4 deletions Sources/KlaviyoSwift/Utilities/Logger+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Andrew Balmer on 5/27/25.
//

import KlaviyoCore
import OSLog

@available(iOS 14.0, *)
Expand All @@ -21,14 +22,22 @@ extension Logger {
@available(iOS 14.0, *)
extension Logger {
/// Logger for ``Codable`` events (JSON encoding & decoding)
static let codableLogger = Logger(category: "Encoding/Decoding Logger")
static var codableLogger: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Encoding/Decoding Logger") : Logger(OSLog.disabled)
}

/// Logger for state events that run through the reducer
static let stateLogger = Logger(category: "State logger")
static var stateLogger: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "State logger") : Logger(OSLog.disabled)
}

/// Logger for notification events.
static let notifications = Logger(category: "Notifications logger")
static var notifications: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Notifications logger") : Logger(OSLog.disabled)
}

/// Logger for app navigation and deep linking events
static let navigation = Logger(category: "Linking and Navigation")
static var navigation: Logger {
KlaviyoLogConfig.shared.isLoggingEnabled ? Logger(category: "Linking and Navigation") : Logger(OSLog.disabled)
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
//
import Foundation
import Combine
import KlaviyoCore
#if canImport(os)
import os
#endif
Expand All @@ -46,7 +47,6 @@ final class Box<Wrapped> {
}
}

@_transparent
Comment thread
cursor[bot] marked this conversation as resolved.
@usableFromInline
@inline(__always)
func runtimeWarn(
Expand All @@ -56,6 +56,7 @@ func runtimeWarn(
line: UInt? = nil
) {
#if DEBUG
guard KlaviyoLogConfig.shared.isLoggingEnabled else { return }
let message = message()
let category = category ?? "Runtime Warning"
#if canImport(os)
Expand Down
110 changes: 110 additions & 0 deletions Tests/KlaviyoCoreTests/KlaviyoLogConfigTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// KlaviyoLogConfigTests.swift
// klaviyo-swift-sdk
//

@testable import KlaviyoCore
import OSLog
import XCTest

@available(iOS 14.0, *)
final class KlaviyoLogConfigTests: XCTestCase {
override func tearDown() {
// Always restore default state after each test
KlaviyoLogConfig.shared.isLoggingEnabled = true
super.tearDown()
}

// MARK: - Default State

func testDefaultIsEnabled() {
XCTAssertTrue(KlaviyoLogConfig.shared.isLoggingEnabled)
}

// MARK: - Toggle Behavior

func testDisableLogging() {
KlaviyoLogConfig.shared.isLoggingEnabled = false
XCTAssertFalse(KlaviyoLogConfig.shared.isLoggingEnabled)
}

func testReEnableLogging() {
KlaviyoLogConfig.shared.isLoggingEnabled = false
XCTAssertFalse(KlaviyoLogConfig.shared.isLoggingEnabled)

KlaviyoLogConfig.shared.isLoggingEnabled = true
XCTAssertTrue(KlaviyoLogConfig.shared.isLoggingEnabled)
}

// MARK: - Logger Instances

func testLoggersReturnRealLoggerWhenEnabled() {
KlaviyoLogConfig.shared.isLoggingEnabled = true

let logger = Logger.networking
// A real logger will have a non-disabled log; verify it doesn't crash
// and that it's different from a disabled logger.
// We can't directly compare Logger instances, but we can verify the
// computed property returns without error.
_ = logger
}

func testLoggersReturnDisabledLoggerWhenDisabled() {
KlaviyoLogConfig.shared.isLoggingEnabled = false

// These should all return Logger(OSLog.disabled) without crashing
let codable = Logger.codable
let networking = Logger.networking
let navigation = Logger.navigation

// Verify they don't crash when called
codable.info("This should be silenced")
networking.info("This should be silenced")
navigation.info("This should be silenced")
}

// MARK: - Thread Safety

func testConcurrentAccess() {
let iterations = 1000
let expectation = expectation(description: "Concurrent access completes")
expectation.expectedFulfillmentCount = iterations * 2

let queue = DispatchQueue(label: "test.concurrent", attributes: .concurrent)

for _ in 0..<iterations {
queue.async {
KlaviyoLogConfig.shared.isLoggingEnabled = false
expectation.fulfill()
}
queue.async {
_ = KlaviyoLogConfig.shared.isLoggingEnabled
expectation.fulfill()
}
}

wait(for: [expectation], timeout: 10)
}

func testConcurrentToggleDoesNotCrash() {
let iterations = 500
let expectation = expectation(description: "Concurrent toggle completes")
expectation.expectedFulfillmentCount = iterations * 2

let queue = DispatchQueue(label: "test.concurrent.toggle", attributes: .concurrent)

for i in 0..<iterations {
queue.async {
KlaviyoLogConfig.shared.isLoggingEnabled = (i % 2 == 0)
expectation.fulfill()
}
queue.async {
// Access a logger while toggling
_ = Logger.networking
expectation.fulfill()
}
}

wait(for: [expectation], timeout: 10)
}
}
Loading
Loading