From a9439049de4f7d51fe679222de83013c6bd60fc8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 5 Aug 2026 16:18:48 -0700 Subject: [PATCH 01/15] feat: [SDK-4977] wire KMP remote logging lifecycle Co-authored-by: Cursor --- OneSignal-KMP-SDK | 2 +- .../OneSignal.xcodeproj/project.pbxproj | 18 + .../Source/Logging/OSRemoteLogger.swift | 128 +++++++ .../Source/OSRemoteLoggingController.h | 39 ++ .../Source/OSRemoteLoggingController.swift | 353 ++++++++++++++++++ iOS_SDK/OneSignalSDK/Source/OneSignal.m | 3 + .../OSRemoteLoggingControllerTests.swift | 122 ++++++ 7 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift create mode 100644 iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h create mode 100644 iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift create mode 100644 iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift diff --git a/OneSignal-KMP-SDK b/OneSignal-KMP-SDK index d417111af..f289944ec 160000 --- a/OneSignal-KMP-SDK +++ b/OneSignal-KMP-SDK @@ -1 +1 @@ -Subproject commit d417111afa4e81ab80e49931944230e6a0ad47df +Subproject commit f289944ec7420c14ae5b8231667be3976773dcfd diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 10f512fa6..7c717b267 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -75,6 +75,11 @@ 3C14E3B32FAE54C006ED053 /* OSLoggerPlatformProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */; }; 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */; }; 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */; }; + 497700000000000000000003 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000001 /* OSRemoteLoggingController.swift */; }; + 497700000000000000000004 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000001 /* OSRemoteLoggingController.swift */; }; + 497700000000000000000005 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000001 /* OSRemoteLoggingController.swift */; }; + 497700000000000000000007 /* OSRemoteLoggingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000006 /* OSRemoteLoggingControllerTests.swift */; }; + 497700000000000000000009 /* OSRemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000008 /* OSRemoteLogger.swift */; }; 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; }; 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */; }; 3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */; }; @@ -1789,6 +1794,10 @@ 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerPlatformProvider.swift; sourceTree = ""; }; 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinByteArray+Data.swift"; sourceTree = ""; }; 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = ""; }; + 497700000000000000000001 /* OSRemoteLoggingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingController.swift; sourceTree = ""; }; + 497700000000000000000002 /* OSRemoteLoggingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSRemoteLoggingController.h; sourceTree = ""; }; + 497700000000000000000006 /* OSRemoteLoggingControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingControllerTests.swift; sourceTree = ""; }; + 497700000000000000000008 /* OSRemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLogger.swift; sourceTree = ""; }; DEF5CCF12539321A0003E9CC /* UnitTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnitTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEF5CCF32539321A0003E9CC /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; DEF5CCF42539321A0003E9CC /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -2285,6 +2294,7 @@ 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */, 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */, 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */, + 497700000000000000000008 /* OSRemoteLogger.swift */, ); path = Logging; sourceTree = ""; @@ -2665,6 +2675,7 @@ 4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */, 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, + 497700000000000000000006 /* OSRemoteLoggingControllerTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -2697,6 +2708,8 @@ 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */, 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */, DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */, + 497700000000000000000002 /* OSRemoteLoggingController.h */, + 497700000000000000000001 /* OSRemoteLoggingController.swift */, 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */, ); path = Source; @@ -4379,6 +4392,7 @@ 9124120E1E73342200E41FD7 /* OneSignal.m in Sources */, 9124121E1E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */, 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */, + 497700000000000000000003 /* OSRemoteLoggingController.swift in Sources */, 912412471E73369600E41FD7 /* OneSignalHelper.m in Sources */, CA8E19062193C76D009DA223 /* OSInAppMessagingHelpers.m in Sources */, 7AAA60682485D0420004FADE /* OSMigrationController.m in Sources */, @@ -4424,6 +4438,7 @@ 3C14E3B12FAE54C006ED053 /* OneSignalLogHttpSender.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */, + 497700000000000000000009 /* OSRemoteLogger.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */, @@ -4530,6 +4545,7 @@ 9124123F1E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m in Sources */, 7AECE59F23675F6300537907 /* OSFocusTimeProcessorFactory.m in Sources */, DEC08B012947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, + 497700000000000000000004 /* OSRemoteLoggingController.swift in Sources */, CA1A6E7120DC2E73001C41B9 /* OneSignalDialogRequest.m in Sources */, 912412331E73342200E41FD7 /* OneSignalTracker.m in Sources */, 7A674F1C2360D82E001F9ACD /* OSBaseFocusTimeProcessor.m in Sources */, @@ -4624,6 +4640,7 @@ CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */, 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, + 497700000000000000000007 /* OSRemoteLoggingControllerTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, @@ -4654,6 +4671,7 @@ 03CCCC852835F291004BF794 /* UIApplicationDelegateSwizzlingTests.m in Sources */, 4529DEEA1FA8360C00CEAB1D /* UIApplicationOverrider.m in Sources */, DEC08B022947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, + 497700000000000000000005 /* OSRemoteLoggingController.swift in Sources */, 7A93269E25AF4F0300BBEC27 /* OSPendingCallbacks.m in Sources */, 7AECE59823674AB700537907 /* OSUnattributedFocusTimeProcessor.m in Sources */, 7A5A818224897693002E07C8 /* MigrationTests.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift new file mode 100644 index 000000000..e1bb5a3cb --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift @@ -0,0 +1,128 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation + +public protocol OSRemoteLoggerProtocol: AnyObject { + var kmpVersion: String { get } + var crashStoragePath: String { get } + + func log(level: String, message: String) + func forceFlush() + func shutdown() +} + +#if !targetEnvironment(macCatalyst) + +@_implementationOnly import OneSignalKMP + +/// Owns the KMP-specific logger composition while exposing a platform-neutral +/// lifecycle API to the umbrella framework. +public final class OSRemoteLogger: OSRemoteLoggerProtocol { + private let telemetry: ILogTelemetryRemote + private let platformProvider: OSLoggerPlatformProvider + + public init( + installIdProvider: @escaping () -> String, + onesignalIdProvider: @escaping () -> String?, + pushSubscriptionIdProvider: @escaping () -> String?, + appStateProvider: @escaping () -> String, + featureFlagsProvider: @escaping () -> [String], + remoteLogLevelProvider: @escaping () -> String?, + exporterLoggingEnabledProvider: @escaping () -> Bool + ) { + let provider = OSLoggerPlatformProvider( + installIdProvider: installIdProvider, + onesignalIdProvider: onesignalIdProvider, + pushSubscriptionIdProvider: pushSubscriptionIdProvider, + appStateProvider: appStateProvider, + featureFlagsProvider: featureFlagsProvider, + remoteLogLevelProvider: remoteLogLevelProvider, + exporterLoggingEnabledProvider: exporterLoggingEnabledProvider + ) + let logger = IOSLogger() + self.platformProvider = provider + self.telemetry = LoggerFactory.shared.createRemoteTelemetry( + platformProvider: provider, + httpSender: OneSignalLogHttpSender( + logger: logger, + isDiagnosticsEnabled: exporterLoggingEnabledProvider + ) + ) + } + + public var kmpVersion: String { + LoggerBuildInfo.shared.KMP_VERSION + } + + public var crashStoragePath: String { + platformProvider.crashStoragePath + } + + public func log(level: String, message: String) { + LogLoggingHelper.shared.log( + telemetry: telemetry, + level: level, + message: message, + exceptionType: nil, + exceptionMessage: nil, + exceptionStacktrace: nil, + completionHandler: { _ in } + ) + } + + public func forceFlush() { + telemetry.forceFlush(completionHandler: { _ in }) + } + + public func shutdown() { + telemetry.shutdown() + } +} + +#else + +public final class OSRemoteLogger: OSRemoteLoggerProtocol { + public init( + installIdProvider: @escaping () -> String, + onesignalIdProvider: @escaping () -> String?, + pushSubscriptionIdProvider: @escaping () -> String?, + appStateProvider: @escaping () -> String, + featureFlagsProvider: @escaping () -> [String], + remoteLogLevelProvider: @escaping () -> String?, + exporterLoggingEnabledProvider: @escaping () -> Bool + ) {} + + public let kmpVersion = "unavailable" + public let crashStoragePath = "unavailable" + + public func log(level: String, message: String) {} + public func forceFlush() {} + public func shutdown() {} +} + +#endif diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h new file mode 100644 index 000000000..2801cf0be --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h @@ -0,0 +1,39 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OSRemoteLoggingController : NSObject + ++ (void)configure; ++ (void)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift new file mode 100644 index 000000000..85ba0e48b --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -0,0 +1,353 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation +import OneSignalCore +import OneSignalOSCore +import OneSignalUser +import UIKit + +struct OSRemoteLoggingConfiguration { + static let featureFlagName = "SDK_CUSTOM_LOGGING" + private static let featureFlagKey = "sdk_custom_logging" + + let isFeatureEnabled: Bool + let logLevel: String? + + var isRemoteLoggingEnabled: Bool { + isFeatureEnabled && logLevel != nil && logLevel != "NONE" + } + + static var current: OSRemoteLoggingConfiguration { + let params = OSRemoteParamController.shared().remoteParams as? [String: Any] ?? [:] + return OSRemoteLoggingConfiguration(remoteParams: params) + } + + init(remoteParams: [String: Any]) { + let loggingConfig = remoteParams["logging_config"] as? [String: Any] + let featureFlags = remoteParams["feature_flags"] as? [String: Any] + let enabledFlagNames = + (remoteParams["sdk_remote_feature_flags"] as? [String]) + ?? (remoteParams["sdkRemoteFeatureFlags"] as? [String]) + ?? (remoteParams["feature_flags"] as? [String]) + ?? [] + isFeatureEnabled = + Self.boolValue(remoteParams[Self.featureFlagKey]) + ?? Self.boolValue(featureFlags?[Self.featureFlagKey]) + ?? Self.boolValue(loggingConfig?[Self.featureFlagKey]) + ?? enabledFlagNames.contains { $0.caseInsensitiveCompare(Self.featureFlagKey) == .orderedSame } + logLevel = (loggingConfig?["log_level"] as? String)?.uppercased() + } + + func allows(_ level: ONE_S_LOG_LEVEL) -> Bool { + guard isRemoteLoggingEnabled, let threshold = logLevel.flatMap(Self.oneSignalLevel) else { + return false + } + return level != .LL_NONE && level.rawValue <= threshold.rawValue + } + + static func levelName(_ level: ONE_S_LOG_LEVEL) -> String { + switch level { + case .LL_FATAL: + return "FATAL" + case .LL_ERROR: + return "ERROR" + case .LL_WARN: + return "WARN" + case .LL_INFO: + return "INFO" + case .LL_DEBUG: + return "DEBUG" + case .LL_VERBOSE: + return "VERBOSE" + default: + return "NONE" + } + } + + private static func oneSignalLevel(_ value: String) -> ONE_S_LOG_LEVEL? { + switch value { + case "FATAL": + return .LL_FATAL + case "ERROR": + return .LL_ERROR + case "WARN", "WARNING": + return .LL_WARN + case "INFO": + return .LL_INFO + case "DEBUG": + return .LL_DEBUG + case "VERBOSE", "TRACE": + return .LL_VERBOSE + default: + return nil + } + } + + private static func boolValue(_ value: Any?) -> Bool? { + if let value = value as? Bool { + return value + } + if let value = value as? NSNumber { + return value.boolValue + } + if let value = value as? String { + switch value.lowercased() { + case "true", "1": + return true + case "false", "0": + return false + default: + return nil + } + } + return nil + } +} + +@objc(OSRemoteLoggingController) +final class OSRemoteLoggingController: NSObject, OSLogListener { + typealias RemoteLoggerFactory = (OSRemoteLoggerProviders) -> OSRemoteLoggerProtocol + + private static let shared = OSRemoteLoggingController() + private static let installIdKey = "PREFS_OS_INSTALL_ID" + private static let installId: String = { + let defaults = OneSignalUserDefaults.initShared() + if let saved = defaults.getSavedString(forKey: installIdKey, defaultValue: nil) { + return saved + } + let generated = UUID().uuidString + defaults.saveString(forKey: installIdKey, withValue: generated) + return generated + }() + + private let stateQueue = DispatchQueue(label: "com.onesignal.logger.remote-lifecycle") + private let appStateLock = NSLock() + private let notificationCenter: NotificationCenter + private let remoteLoggerFactory: RemoteLoggerFactory + private var configuration = OSRemoteLoggingConfiguration(remoteParams: [:]) + private var remoteLogger: OSRemoteLoggerProtocol? + private var appState = "unknown" + private var notificationTokens: [NSObjectProtocol] = [] + private var isListening = false + + init( + notificationCenter: NotificationCenter = .default, + remoteLoggerFactory: @escaping RemoteLoggerFactory = { providers in + OSRemoteLogger( + installIdProvider: providers.installId, + onesignalIdProvider: providers.onesignalId, + pushSubscriptionIdProvider: providers.pushSubscriptionId, + appStateProvider: providers.appState, + featureFlagsProvider: providers.featureFlags, + remoteLogLevelProvider: providers.remoteLogLevel, + exporterLoggingEnabledProvider: providers.exporterLoggingEnabled + ) + } + ) { + self.notificationCenter = notificationCenter + self.remoteLoggerFactory = remoteLoggerFactory + super.init() + } + + @objc class func configure() { + shared.configure(with: .current) + } + + @objc class func reset() { + shared.shutdown() + } + + func configure(remoteParams: [String: Any]) { + configure(with: OSRemoteLoggingConfiguration(remoteParams: remoteParams)) + } + + func onLogEvent(_ event: OneSignalLogEvent) { + stateQueue.async { [weak self] in + guard let self, + self.configuration.allows(event.level), + let remoteLogger = self.remoteLogger else { + return + } + remoteLogger.log( + level: OSRemoteLoggingConfiguration.levelName(event.level), + message: self.message(from: event) + ) + } + } + + func forceFlush() { + stateQueue.async { [weak self] in + self?.remoteLogger?.forceFlush() + } + } + + func shutdown() { + stateQueue.sync { + stopRemoteLogging() + } + } + + private func configure(with newConfiguration: OSRemoteLoggingConfiguration) { + updateAppState(Self.currentApplicationState()) + stateQueue.sync { + self.configuration = newConfiguration + guard newConfiguration.isRemoteLoggingEnabled else { + self.stopRemoteLogging() + self.logStartupDiagnostic(activePath: "local") + return + } + + if self.remoteLogger == nil { + self.remoteLogger = self.remoteLoggerFactory(self.makeProviders()) + OneSignalLog.debug().__add(self) + self.isListening = true + self.registerLifecycleObservers() + self.logStartupDiagnostic(activePath: "kmp") + } + } + } + + private func stopRemoteLogging() { + if isListening { + OneSignalLog.debug().__remove(self) + isListening = false + } + notificationTokens.forEach(notificationCenter.removeObserver) + notificationTokens.removeAll() + let activeRemoteLogger = remoteLogger + remoteLogger = nil + activeRemoteLogger?.shutdown() + } + + private func registerLifecycleObservers() { + observe(UIApplication.didBecomeActiveNotification, appState: "foreground") + observe(Notification.Name("UISceneDidActivateNotification"), appState: "foreground") + observe(UIApplication.willResignActiveNotification, appState: "unknown") + observe(Notification.Name("UISceneWillDeactivateNotification"), appState: "unknown") + observe(UIApplication.didEnterBackgroundNotification, appState: "background", flush: true) + observe(Notification.Name("UISceneDidEnterBackgroundNotification"), appState: "background", flush: true) + notificationTokens.append( + notificationCenter.addObserver( + forName: UIApplication.willTerminateNotification, + object: nil, + queue: nil + ) { [weak self] _ in + self?.shutdown() + } + ) + } + + private func logStartupDiagnostic(activePath: String) { + let kmpVersion = remoteLogger?.kmpVersion ?? "unavailable" + let crashStoragePath = remoteLogger?.crashStoragePath ?? "unavailable" + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OneSignal logging initialized: sdk=\(ONESIGNAL_VERSION), " + + "kmp=\(kmpVersion), path=\(activePath), " + + "\(OSRemoteLoggingConfiguration.featureFlagName)=\(configuration.isFeatureEnabled), " + + "crash_dir=\(crashStoragePath)" + ) + } + + private func makeProviders() -> OSRemoteLoggerProviders { + OSRemoteLoggerProviders( + installId: { Self.installId }, + onesignalId: { OneSignalUserManagerImpl.sharedInstance.onesignalId }, + pushSubscriptionId: { OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId }, + appState: { [weak self] in self?.currentAppState ?? "unknown" }, + featureFlags: { + OSRemoteLoggingConfiguration.current.isFeatureEnabled + ? [OSRemoteLoggingConfiguration.featureFlagName] + : [] + }, + remoteLogLevel: { OSRemoteLoggingConfiguration.current.logLevel }, + exporterLoggingEnabled: { false } + ) + } + + private var currentAppState: String { + appStateLock.lock() + defer { appStateLock.unlock() } + return appState + } + + private func updateAppState(_ value: String) { + appStateLock.lock() + appState = value + appStateLock.unlock() + } + + private func observe( + _ name: Notification.Name, + appState: String, + flush: Bool = false + ) { + notificationTokens.append( + notificationCenter.addObserver(forName: name, object: nil, queue: nil) { [weak self] _ in + self?.updateAppState(appState) + if flush { + self?.forceFlush() + } + } + ) + } + + private static func currentApplicationState() -> String { + let state: UIApplication.State + if Thread.isMainThread { + state = UIApplication.shared.applicationState + } else { + state = DispatchQueue.main.sync { UIApplication.shared.applicationState } + } + switch state { + case .active: + return "foreground" + case .background: + return "background" + default: + return "unknown" + } + } + + private func message(from event: OneSignalLogEvent) -> String { + let prefix = "\(OSRemoteLoggingConfiguration.levelName(event.level)): " + return event.entry.hasPrefix(prefix) + ? String(event.entry.dropFirst(prefix.count)) + : event.entry + } +} + +struct OSRemoteLoggerProviders { + let installId: () -> String + let onesignalId: () -> String? + let pushSubscriptionId: () -> String? + let appState: () -> String + let featureFlags: () -> [String] + let remoteLogLevel: () -> String? + let exporterLoggingEnabled: () -> Bool +} diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 7a128f170..88c6ee482 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -29,6 +29,7 @@ #import "OneSignalFramework.h" #import #import "OneSignalInternal.h" +#import "OSRemoteLoggingController.h" #import "OneSignalTracker.h" #import "OneSignalTrackIAP.h" #import "OneSignalJailbreakDetection.h" @@ -137,6 +138,7 @@ + (OneSignalReceiveReceiptsController*)receiveReceiptsController { //TODO: This is related to unit tests and will change with um tests + (void)clearStatics { + [OSRemoteLoggingController reset]; [OneSignalIdentifiers setCurrentAppId:nil]; launchOptions = false; appSettings = nil; @@ -735,6 +737,7 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { } [[OSRemoteParamController sharedController] saveRemoteParams:result]; + [OSRemoteLoggingController configure]; if ([[OSRemoteParamController sharedController] hasLocationKey]) { BOOL shared = [result[IOS_LOCATION_SHARED] boolValue]; let oneSignalLocation = NSClassFromString(ONE_SIGNAL_LOCATION_CLASS_NAME); diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift new file mode 100644 index 000000000..c654ad924 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift @@ -0,0 +1,122 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation +import OneSignalCore +import OneSignalOSCore +import UIKit +import XCTest + +final class OSRemoteLoggingControllerTests: XCTestCase { + func testConfigurationRequiresFeatureFlagAndRemoteLevel() { + let levelOnly = OSRemoteLoggingConfiguration( + remoteParams: ["logging_config": ["log_level": "WARN"]] + ) + XCTAssertFalse(levelOnly.isRemoteLoggingEnabled) + + let flagOnly = OSRemoteLoggingConfiguration( + remoteParams: ["sdk_custom_logging": true] + ) + XCTAssertFalse(flagOnly.isRemoteLoggingEnabled) + + let enabled = OSRemoteLoggingConfiguration( + remoteParams: [ + "sdk_custom_logging": true, + "logging_config": ["log_level": "warn"] + ] + ) + XCTAssertTrue(enabled.isRemoteLoggingEnabled) + XCTAssertTrue(enabled.allows(.LL_ERROR)) + XCTAssertTrue(enabled.allows(.LL_WARN)) + XCTAssertFalse(enabled.allows(.LL_INFO)) + + let enabledFromFlagList = OSRemoteLoggingConfiguration( + remoteParams: [ + "sdk_remote_feature_flags": ["sdk_custom_logging"], + "logging_config": ["log_level": "ERROR"] + ] + ) + XCTAssertTrue(enabledFromFlagList.isRemoteLoggingEnabled) + } + + func testControllerRoutesLogsAndFlushesOnBackground() { + let notificationCenter = NotificationCenter() + let telemetry = RemoteTelemetrySpy() + telemetry.emitExpectation = expectation(description: "routes matching log") + telemetry.flushExpectation = expectation(description: "flushes on background") + let controller = OSRemoteLoggingController( + notificationCenter: notificationCenter, + remoteLoggerFactory: { _ in telemetry } + ) + + controller.configure( + remoteParams: [ + "sdk_custom_logging": true, + "logging_config": ["log_level": "ERROR"] + ] + ) + OneSignalLog.onesignalLog(.LL_INFO, message: "not uploaded") + OneSignalLog.onesignalLog(.LL_ERROR, message: "uploaded") + notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + wait(for: [telemetry.emitExpectation!, telemetry.flushExpectation!], timeout: 2) + controller.shutdown() + + XCTAssertEqual(telemetry.messages, ["uploaded"]) + XCTAssertEqual(telemetry.levels, ["ERROR"]) + XCTAssertEqual(telemetry.shutdownCount, 1) + } +} + +private final class RemoteTelemetrySpy: OSRemoteLoggerProtocol { + private let lock = NSLock() + var emitExpectation: XCTestExpectation? + var flushExpectation: XCTestExpectation? + let kmpVersion = "test" + let crashStoragePath = "/test" + private(set) var levels: [String] = [] + private(set) var messages: [String] = [] + private(set) var shutdownCount = 0 + + func log(level: String, message: String) { + lock.lock() + levels.append(level) + messages.append(message) + lock.unlock() + emitExpectation?.fulfill() + } + + func forceFlush() { + flushExpectation?.fulfill() + } + + func shutdown() { + lock.lock() + shutdownCount += 1 + lock.unlock() + } +} From 7a3e9af385bcee6daa6d62de1249ae60401de2d7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 7 Aug 2026 11:09:05 -0700 Subject: [PATCH 02/15] fix: [SDK-4977] harden remote logging lifecycle Preserve raw log bodies, validate remote configuration consistently, and keep lifecycle flushes alive through completion. Co-authored-by: Cursor --- .../OneSignalCore/Source/OneSignalLog.h | 1 + .../OneSignalCore/Source/OneSignalLog.m | 9 +- .../Source/Logging/OSRemoteLogger.swift | 10 +- .../Source/OSRemoteLoggingController.swift | 139 ++++++++++-------- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 16 +- .../OSRemoteLoggingControllerTests.swift | 131 +++++++++++++++-- 6 files changed, 230 insertions(+), 76 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h index 43c92024a..43062ac35 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h @@ -39,6 +39,7 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) { @interface OneSignalLogEvent : NSObject @property(readonly)ONE_S_LOG_LEVEL level; @property(readonly, nonnull)NSString *entry; +@property(readonly, nonnull)NSString *message; @end @protocol OSLogListener diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m index 2ed5b3896..140289aa1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m @@ -31,9 +31,10 @@ #import "OSCopyOnWriteSet.h" @implementation OneSignalLogEvent -- (instancetype)initWithLevel:(ONE_S_LOG_LEVEL)level entry:(NSString*)entry { +- (instancetype)initWithLevel:(ONE_S_LOG_LEVEL)level entry:(NSString*)entry message:(NSString*)message { _level = level; _entry = entry; + _message = message; return self; } @end @@ -115,7 +116,11 @@ void onesignal_Log(ONE_S_LOG_LEVEL logLevel, NSString* message) { for (NSObject *listener in OneSignalLog.logListeners.allObjects) { if ([listener respondsToSelector:@selector(onLogEvent:)]) { - OneSignalLogEvent *event = [[OneSignalLogEvent alloc] initWithLevel:logLevel entry:[levelString stringByAppendingString:message]]; + OneSignalLogEvent *event = [[OneSignalLogEvent alloc] + initWithLevel:logLevel + entry:[levelString stringByAppendingString:message] + message:message + ]; [listener onLogEvent:event]; } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift index e1bb5a3cb..ec14e930d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift @@ -32,7 +32,7 @@ public protocol OSRemoteLoggerProtocol: AnyObject { var crashStoragePath: String { get } func log(level: String, message: String) - func forceFlush() + func forceFlush(completion: @escaping () -> Void) func shutdown() } @@ -95,8 +95,8 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol { ) } - public func forceFlush() { - telemetry.forceFlush(completionHandler: { _ in }) + public func forceFlush(completion: @escaping () -> Void) { + telemetry.forceFlush(completionHandler: { _ in completion() }) } public func shutdown() { @@ -121,7 +121,9 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol { public let crashStoragePath = "unavailable" public func log(level: String, message: String) {} - public func forceFlush() {} + public func forceFlush(completion: @escaping () -> Void) { + completion() + } public func shutdown() {} } diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index 85ba0e48b..0b0f63b53 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -34,12 +34,16 @@ import UIKit struct OSRemoteLoggingConfiguration { static let featureFlagName = "SDK_CUSTOM_LOGGING" private static let featureFlagKey = "sdk_custom_logging" + private static let featureFlagsKey = "sdk_remote_feature_flags" + private static let loggingConfigKey = "logging_config" + private static let logLevelKey = "log_level" let isFeatureEnabled: Bool let logLevel: String? + private let threshold: ONE_S_LOG_LEVEL? var isRemoteLoggingEnabled: Bool { - isFeatureEnabled && logLevel != nil && logLevel != "NONE" + isFeatureEnabled && threshold != nil && threshold != .LL_NONE } static var current: OSRemoteLoggingConfiguration { @@ -48,23 +52,21 @@ struct OSRemoteLoggingConfiguration { } init(remoteParams: [String: Any]) { - let loggingConfig = remoteParams["logging_config"] as? [String: Any] - let featureFlags = remoteParams["feature_flags"] as? [String: Any] - let enabledFlagNames = - (remoteParams["sdk_remote_feature_flags"] as? [String]) - ?? (remoteParams["sdkRemoteFeatureFlags"] as? [String]) - ?? (remoteParams["feature_flags"] as? [String]) - ?? [] - isFeatureEnabled = - Self.boolValue(remoteParams[Self.featureFlagKey]) - ?? Self.boolValue(featureFlags?[Self.featureFlagKey]) - ?? Self.boolValue(loggingConfig?[Self.featureFlagKey]) - ?? enabledFlagNames.contains { $0.caseInsensitiveCompare(Self.featureFlagKey) == .orderedSame } - logLevel = (loggingConfig?["log_level"] as? String)?.uppercased() + let loggingConfig = remoteParams[Self.loggingConfigKey] as? [String: Any] + let enabledFlagNames = remoteParams[Self.featureFlagsKey] as? [String] ?? [] + let parsedThreshold = (loggingConfig?[Self.logLevelKey] as? String) + .map { $0.uppercased() } + .flatMap(Self.oneSignalLevel) + + isFeatureEnabled = enabledFlagNames.contains { + $0.caseInsensitiveCompare(Self.featureFlagKey) == .orderedSame + } + threshold = parsedThreshold + logLevel = parsedThreshold.map(Self.levelName) } func allows(_ level: ONE_S_LOG_LEVEL) -> Bool { - guard isRemoteLoggingEnabled, let threshold = logLevel.flatMap(Self.oneSignalLevel) else { + guard isRemoteLoggingEnabled, let threshold else { return false } return level != .LL_NONE && level.rawValue <= threshold.rawValue @@ -108,25 +110,6 @@ struct OSRemoteLoggingConfiguration { } } - private static func boolValue(_ value: Any?) -> Bool? { - if let value = value as? Bool { - return value - } - if let value = value as? NSNumber { - return value.boolValue - } - if let value = value as? String { - switch value.lowercased() { - case "true", "1": - return true - case "false", "0": - return false - default: - return nil - } - } - return nil - } } @objc(OSRemoteLoggingController) @@ -135,6 +118,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private static let shared = OSRemoteLoggingController() private static let installIdKey = "PREFS_OS_INSTALL_ID" + private static let backgroundTaskPrefix = "com.onesignal.logger.flush." private static let installId: String = { let defaults = OneSignalUserDefaults.initShared() if let saved = defaults.getSavedString(forKey: installIdKey, defaultValue: nil) { @@ -149,6 +133,9 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private let appStateLock = NSLock() private let notificationCenter: NotificationCenter private let remoteLoggerFactory: RemoteLoggerFactory + private let usesScenes: () -> Bool + private let beginBackgroundTask: (String) -> Void + private let endBackgroundTask: (String) -> Void private var configuration = OSRemoteLoggingConfiguration(remoteParams: [:]) private var remoteLogger: OSRemoteLoggerProtocol? private var appState = "unknown" @@ -157,6 +144,9 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { init( notificationCenter: NotificationCenter = .default, + usesScenes: @escaping () -> Bool = { OSBundleUtils.isAppUsingUIScene() }, + beginBackgroundTask: @escaping (String) -> Void = OSBackgroundTaskManager.beginBackgroundTask, + endBackgroundTask: @escaping (String) -> Void = OSBackgroundTaskManager.endBackgroundTask, remoteLoggerFactory: @escaping RemoteLoggerFactory = { providers in OSRemoteLogger( installIdProvider: providers.installId, @@ -170,6 +160,9 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } ) { self.notificationCenter = notificationCenter + self.usesScenes = usesScenes + self.beginBackgroundTask = beginBackgroundTask + self.endBackgroundTask = endBackgroundTask self.remoteLoggerFactory = remoteLoggerFactory super.init() } @@ -200,9 +193,13 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } } - func forceFlush() { + func forceFlush(completion: @escaping () -> Void = {}) { stateQueue.async { [weak self] in - self?.remoteLogger?.forceFlush() + guard let remoteLogger = self?.remoteLogger else { + completion() + return + } + remoteLogger.forceFlush(completion: completion) } } @@ -215,19 +212,23 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private func configure(with newConfiguration: OSRemoteLoggingConfiguration) { updateAppState(Self.currentApplicationState()) stateQueue.sync { + let previousLogLevel = self.configuration.logLevel self.configuration = newConfiguration guard newConfiguration.isRemoteLoggingEnabled else { self.stopRemoteLogging() - self.logStartupDiagnostic(activePath: "local") return } + if previousLogLevel != newConfiguration.logLevel { + self.stopRemoteLogging() + } + if self.remoteLogger == nil { - self.remoteLogger = self.remoteLoggerFactory(self.makeProviders()) + self.remoteLogger = self.remoteLoggerFactory(self.makeProviders(configuration: newConfiguration)) + self.logStartupDiagnostic() OneSignalLog.debug().__add(self) self.isListening = true self.registerLifecycleObservers() - self.logStartupDiagnostic(activePath: "kmp") } } } @@ -245,47 +246,48 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } private func registerLifecycleObservers() { - observe(UIApplication.didBecomeActiveNotification, appState: "foreground") - observe(Notification.Name("UISceneDidActivateNotification"), appState: "foreground") - observe(UIApplication.willResignActiveNotification, appState: "unknown") - observe(Notification.Name("UISceneWillDeactivateNotification"), appState: "unknown") - observe(UIApplication.didEnterBackgroundNotification, appState: "background", flush: true) - observe(Notification.Name("UISceneDidEnterBackgroundNotification"), appState: "background", flush: true) + if usesScenes() { + observe(Notification.Name("UISceneDidActivateNotification"), appState: "foreground") + observe(Notification.Name("UISceneWillDeactivateNotification"), appState: "unknown") + observe(Notification.Name("UISceneDidEnterBackgroundNotification"), appState: "background", flush: true) + } else { + observe(UIApplication.didBecomeActiveNotification, appState: "foreground") + observe(UIApplication.willResignActiveNotification, appState: "unknown") + observe(UIApplication.didEnterBackgroundNotification, appState: "background", flush: true) + } notificationTokens.append( notificationCenter.addObserver( forName: UIApplication.willTerminateNotification, object: nil, queue: nil ) { [weak self] _ in - self?.shutdown() + self?.flushForLifecycle(shutdownAfterFlush: true) } ) } - private func logStartupDiagnostic(activePath: String) { + private func logStartupDiagnostic() { let kmpVersion = remoteLogger?.kmpVersion ?? "unavailable" let crashStoragePath = remoteLogger?.crashStoragePath ?? "unavailable" OneSignalLog.onesignalLog( .LL_WARN, message: "OneSignal logging initialized: sdk=\(ONESIGNAL_VERSION), " - + "kmp=\(kmpVersion), path=\(activePath), " + + "kmp=\(kmpVersion), path=kmp, " + "\(OSRemoteLoggingConfiguration.featureFlagName)=\(configuration.isFeatureEnabled), " + "crash_dir=\(crashStoragePath)" ) } - private func makeProviders() -> OSRemoteLoggerProviders { + private func makeProviders(configuration: OSRemoteLoggingConfiguration) -> OSRemoteLoggerProviders { OSRemoteLoggerProviders( installId: { Self.installId }, onesignalId: { OneSignalUserManagerImpl.sharedInstance.onesignalId }, pushSubscriptionId: { OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId }, appState: { [weak self] in self?.currentAppState ?? "unknown" }, featureFlags: { - OSRemoteLoggingConfiguration.current.isFeatureEnabled - ? [OSRemoteLoggingConfiguration.featureFlagName] - : [] + configuration.isFeatureEnabled ? [OSRemoteLoggingConfiguration.featureFlagName] : [] }, - remoteLogLevel: { OSRemoteLoggingConfiguration.current.logLevel }, + remoteLogLevel: { configuration.logLevel }, exporterLoggingEnabled: { false } ) } @@ -311,12 +313,36 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { notificationCenter.addObserver(forName: name, object: nil, queue: nil) { [weak self] _ in self?.updateAppState(appState) if flush { - self?.forceFlush() + self?.flushForLifecycle(shutdownAfterFlush: false) } } ) } + private func flushForLifecycle(shutdownAfterFlush: Bool) { + let taskIdentifier = Self.backgroundTaskPrefix + UUID().uuidString + let endBackgroundTask = self.endBackgroundTask + beginBackgroundTask(taskIdentifier) + stateQueue.async { [weak self] in + guard let self, let remoteLogger = self.remoteLogger else { + endBackgroundTask(taskIdentifier) + return + } + remoteLogger.forceFlush { [weak self] in + guard let self else { + endBackgroundTask(taskIdentifier) + return + } + self.stateQueue.async { + if shutdownAfterFlush { + self.stopRemoteLogging() + } + endBackgroundTask(taskIdentifier) + } + } + } + } + private static func currentApplicationState() -> String { let state: UIApplication.State if Thread.isMainThread { @@ -335,10 +361,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } private func message(from event: OneSignalLogEvent) -> String { - let prefix = "\(OSRemoteLoggingConfiguration.levelName(event.level)): " - return event.entry.hasPrefix(prefix) - ? String(event.entry.dropFirst(prefix.count)) - : event.entry + event.message } } diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 88c6ee482..52c075527 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -656,6 +656,7 @@ + (void)handleAppIdChange:(NSString*)appId { initDone = false; _downloadedParameters = false; _didCallDownloadParameters = false; + [OSRemoteLoggingController reset]; let sharedUserDefaults = OneSignalUserDefaults.initShared; @@ -700,9 +701,16 @@ + (void)setConsentRequired:(BOOL)required { + (void)setConsentGiven:(BOOL)granted { [OSPrivacyConsentController consentGranted:granted]; + if (!granted) { + [OSRemoteLoggingController reset]; + } - if (!granted || !delayedInitializationForPrivacyConsent || _delayedInitParameters == nil) + if (!granted || !delayedInitializationForPrivacyConsent || _delayedInitParameters == nil) { + if (granted) { + [OSRemoteLoggingController configure]; + } return; + } // Try to init again using delayed params [self initialize:_delayedInitParameters.appId withLaunchOptions:_delayedInitParameters.launchOptions]; delayedInitializationForPrivacyConsent = false; @@ -737,7 +745,6 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { } [[OSRemoteParamController sharedController] saveRemoteParams:result]; - [OSRemoteLoggingController configure]; if ([[OSRemoteParamController sharedController] hasLocationKey]) { BOOL shared = [result[IOS_LOCATION_SHARED] boolValue]; let oneSignalLocation = NSClassFromString(ONE_SIGNAL_LOCATION_CLASS_NAME); @@ -751,6 +758,11 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { [[OSRemoteParamController sharedController] savePrivacyConsentRequired:required]; [OSPrivacyConsentController setRequiresPrivacyConsent:required]; } + if ([OSPrivacyConsentController requiresUserPrivacyConsent]) { + [OSRemoteLoggingController reset]; + } else { + [OSRemoteLoggingController configure]; + } if (result[OUTCOMES_PARAM] && result[OUTCOMES_PARAM][IOS_OUTCOMES_V2_SERVICE_ENABLE]) [[OSOutcomeEventsCache sharedOutcomeEventsCache] saveOutcomesV2ServiceEnabled:[result[OUTCOMES_PARAM][IOS_OUTCOMES_V2_SERVICE_ENABLE] boolValue]]; diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift index c654ad924..232cfeca8 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift @@ -32,6 +32,14 @@ import UIKit import XCTest final class OSRemoteLoggingControllerTests: XCTestCase { + private var controllers: [OSRemoteLoggingController] = [] + + override func tearDown() { + controllers.forEach { $0.shutdown() } + controllers.removeAll() + super.tearDown() + } + func testConfigurationRequiresFeatureFlagAndRemoteLevel() { let levelOnly = OSRemoteLoggingConfiguration( remoteParams: ["logging_config": ["log_level": "WARN"]] @@ -39,13 +47,13 @@ final class OSRemoteLoggingControllerTests: XCTestCase { XCTAssertFalse(levelOnly.isRemoteLoggingEnabled) let flagOnly = OSRemoteLoggingConfiguration( - remoteParams: ["sdk_custom_logging": true] + remoteParams: ["sdk_remote_feature_flags": ["sdk_custom_logging"]] ) XCTAssertFalse(flagOnly.isRemoteLoggingEnabled) let enabled = OSRemoteLoggingConfiguration( remoteParams: [ - "sdk_custom_logging": true, + "sdk_remote_feature_flags": ["sdk_custom_logging"], "logging_config": ["log_level": "warn"] ] ) @@ -54,13 +62,13 @@ final class OSRemoteLoggingControllerTests: XCTestCase { XCTAssertTrue(enabled.allows(.LL_WARN)) XCTAssertFalse(enabled.allows(.LL_INFO)) - let enabledFromFlagList = OSRemoteLoggingConfiguration( + let invalidLevel = OSRemoteLoggingConfiguration( remoteParams: [ "sdk_remote_feature_flags": ["sdk_custom_logging"], - "logging_config": ["log_level": "ERROR"] + "logging_config": ["log_level": "OFF"] ] ) - XCTAssertTrue(enabledFromFlagList.isRemoteLoggingEnabled) + XCTAssertFalse(invalidLevel.isRemoteLoggingEnabled) } func testControllerRoutesLogsAndFlushesOnBackground() { @@ -68,14 +76,22 @@ final class OSRemoteLoggingControllerTests: XCTestCase { let telemetry = RemoteTelemetrySpy() telemetry.emitExpectation = expectation(description: "routes matching log") telemetry.flushExpectation = expectation(description: "flushes on background") - let controller = OSRemoteLoggingController( + let backgroundTaskEnded = expectation(description: "ends background task after flush") + var startedTask: String? + var endedTask: String? + let controller = makeController( notificationCenter: notificationCenter, + beginBackgroundTask: { startedTask = $0 }, + endBackgroundTask: { + endedTask = $0 + backgroundTaskEnded.fulfill() + }, remoteLoggerFactory: { _ in telemetry } ) controller.configure( remoteParams: [ - "sdk_custom_logging": true, + "sdk_remote_feature_flags": ["sdk_custom_logging"], "logging_config": ["log_level": "ERROR"] ] ) @@ -83,12 +99,104 @@ final class OSRemoteLoggingControllerTests: XCTestCase { OneSignalLog.onesignalLog(.LL_ERROR, message: "uploaded") notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - wait(for: [telemetry.emitExpectation!, telemetry.flushExpectation!], timeout: 2) - controller.shutdown() + wait( + for: [telemetry.emitExpectation!, telemetry.flushExpectation!, backgroundTaskEnded], + timeout: 2 + ) XCTAssertEqual(telemetry.messages, ["uploaded"]) XCTAssertEqual(telemetry.levels, ["ERROR"]) + XCTAssertEqual(endedTask, startedTask) + } + + func testWarnUsesRawMessageWithoutConsolePrefix() { + let telemetry = RemoteTelemetrySpy() + telemetry.emitExpectation = expectation(description: "routes warning") + let controller = makeController(remoteLoggerFactory: { _ in telemetry }) + controller.configure(remoteParams: Self.remoteParams(level: "WARN")) + + OneSignalLog.onesignalLog(.LL_WARN, message: "warning body") + + wait(for: [telemetry.emitExpectation!], timeout: 2) + XCTAssertEqual(telemetry.messages, ["warning body"]) + XCTAssertEqual(telemetry.levels, ["WARN"]) + } + + func testDisablingConfigurationStopsRemoteLogging() { + let telemetry = RemoteTelemetrySpy() + let controller = makeController(remoteLoggerFactory: { _ in telemetry }) + controller.configure(remoteParams: Self.remoteParams(level: "ERROR")) + controller.configure(remoteParams: [:]) + telemetry.emitExpectation = expectation(description: "does not route after disable") + telemetry.emitExpectation?.isInverted = true + + OneSignalLog.onesignalLog(.LL_ERROR, message: "not uploaded") + + wait(for: [telemetry.emitExpectation!], timeout: 0.2) XCTAssertEqual(telemetry.shutdownCount, 1) + XCTAssertTrue(telemetry.messages.isEmpty) + } + + func testTerminationFlushesBeforeShutdown() { + let notificationCenter = NotificationCenter() + let telemetry = RemoteTelemetrySpy() + telemetry.flushExpectation = expectation(description: "flushes on termination") + telemetry.shutdownExpectation = expectation(description: "shuts down after flush") + let controller = makeController( + notificationCenter: notificationCenter, + remoteLoggerFactory: { _ in telemetry } + ) + controller.configure(remoteParams: Self.remoteParams(level: "ERROR")) + + notificationCenter.post(name: UIApplication.willTerminateNotification, object: nil) + + wait(for: [telemetry.flushExpectation!, telemetry.shutdownExpectation!], timeout: 2) + XCTAssertEqual(telemetry.shutdownCount, 1) + } + + func testSceneAppsObserveOnlySceneLifecycle() { + let notificationCenter = NotificationCenter() + let telemetry = RemoteTelemetrySpy() + let controller = makeController( + notificationCenter: notificationCenter, + usesScenes: { true }, + remoteLoggerFactory: { _ in telemetry } + ) + controller.configure(remoteParams: Self.remoteParams(level: "ERROR")) + telemetry.flushExpectation = expectation(description: "ignores application background") + telemetry.flushExpectation?.isInverted = true + + notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + wait(for: [telemetry.flushExpectation!], timeout: 0.2) + + telemetry.flushExpectation = expectation(description: "flushes on scene background") + notificationCenter.post(name: Notification.Name("UISceneDidEnterBackgroundNotification"), object: nil) + wait(for: [telemetry.flushExpectation!], timeout: 2) + } + + private func makeController( + notificationCenter: NotificationCenter = NotificationCenter(), + usesScenes: @escaping () -> Bool = { false }, + beginBackgroundTask: @escaping (String) -> Void = { _ in }, + endBackgroundTask: @escaping (String) -> Void = { _ in }, + remoteLoggerFactory: @escaping OSRemoteLoggingController.RemoteLoggerFactory + ) -> OSRemoteLoggingController { + let controller = OSRemoteLoggingController( + notificationCenter: notificationCenter, + usesScenes: usesScenes, + beginBackgroundTask: beginBackgroundTask, + endBackgroundTask: endBackgroundTask, + remoteLoggerFactory: remoteLoggerFactory + ) + controllers.append(controller) + return controller + } + + private static func remoteParams(level: String) -> [String: Any] { + [ + "sdk_remote_feature_flags": ["sdk_custom_logging"], + "logging_config": ["log_level": level] + ] } } @@ -96,6 +204,7 @@ private final class RemoteTelemetrySpy: OSRemoteLoggerProtocol { private let lock = NSLock() var emitExpectation: XCTestExpectation? var flushExpectation: XCTestExpectation? + var shutdownExpectation: XCTestExpectation? let kmpVersion = "test" let crashStoragePath = "/test" private(set) var levels: [String] = [] @@ -110,13 +219,15 @@ private final class RemoteTelemetrySpy: OSRemoteLoggerProtocol { emitExpectation?.fulfill() } - func forceFlush() { + func forceFlush(completion: @escaping () -> Void) { flushExpectation?.fulfill() + completion() } func shutdown() { lock.lock() shutdownCount += 1 lock.unlock() + shutdownExpectation?.fulfill() } } From 26859ac30551161527974482a1da5e3ef8dd08de Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 7 Aug 2026 11:12:50 -0700 Subject: [PATCH 03/15] chore(sdk): bump KMP submodule to 72581f3 --- OneSignal-KMP-SDK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneSignal-KMP-SDK b/OneSignal-KMP-SDK index f289944ec..72581f399 160000 --- a/OneSignal-KMP-SDK +++ b/OneSignal-KMP-SDK @@ -1 +1 @@ -Subproject commit f289944ec7420c14ae5b8231667be3976773dcfd +Subproject commit 72581f39968104755f322b7dc48f5b3e4a401403 From a150dce33140a02e2dd499f645db0e3835633c85 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 7 Aug 2026 14:07:14 -0700 Subject: [PATCH 04/15] fix: [SDK-4977] construct remote logger on main Create UIKit-backed platform metadata on the main thread without holding the lifecycle queue, preventing thread violations and reentrant listener deadlocks. Co-authored-by: Cursor --- .../Source/OSRemoteLoggingController.swift | 69 ++++++++++++++++--- .../OSRemoteLoggingControllerTests.swift | 50 ++++++++++++++ 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index 0b0f63b53..b76930fcd 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -137,6 +137,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private let beginBackgroundTask: (String) -> Void private let endBackgroundTask: (String) -> Void private var configuration = OSRemoteLoggingConfiguration(remoteParams: [:]) + private var configurationGeneration = 0 private var remoteLogger: OSRemoteLoggerProtocol? private var appState = "unknown" private var notificationTokens: [NSObjectProtocol] = [] @@ -205,13 +206,17 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { func shutdown() { stateQueue.sync { + configurationGeneration += 1 stopRemoteLogging() } } private func configure(with newConfiguration: OSRemoteLoggingConfiguration) { updateAppState(Self.currentApplicationState()) + var startGeneration: Int? stateQueue.sync { + self.configurationGeneration += 1 + let generation = self.configurationGeneration let previousLogLevel = self.configuration.logLevel self.configuration = newConfiguration guard newConfiguration.isRemoteLoggingEnabled else { @@ -224,13 +229,43 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } if self.remoteLogger == nil { - self.remoteLogger = self.remoteLoggerFactory(self.makeProviders(configuration: newConfiguration)) - self.logStartupDiagnostic() - OneSignalLog.debug().__add(self) - self.isListening = true - self.registerLifecycleObservers() + startGeneration = generation } } + + guard let startGeneration else { + return + } + + let providers = makeProviders(configuration: newConfiguration) + let newRemoteLogger = Self.onMain { + remoteLoggerFactory(providers) + } + var installed = false + stateQueue.sync { + guard self.configurationGeneration == startGeneration, + self.configuration.matches(newConfiguration), + self.remoteLogger == nil else { + return + } + self.remoteLogger = newRemoteLogger + installed = true + } + guard installed else { + newRemoteLogger.shutdown() + return + } + + logStartupDiagnostic(remoteLogger: newRemoteLogger, configuration: newConfiguration) + stateQueue.sync { + guard self.remoteLogger === newRemoteLogger, + self.configuration.matches(newConfiguration) else { + return + } + OneSignalLog.debug().__add(self) + self.isListening = true + self.registerLifecycleObservers() + } } private func stopRemoteLogging() { @@ -266,15 +301,16 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { ) } - private func logStartupDiagnostic() { - let kmpVersion = remoteLogger?.kmpVersion ?? "unavailable" - let crashStoragePath = remoteLogger?.crashStoragePath ?? "unavailable" + private func logStartupDiagnostic( + remoteLogger: OSRemoteLoggerProtocol, + configuration: OSRemoteLoggingConfiguration + ) { OneSignalLog.onesignalLog( .LL_WARN, message: "OneSignal logging initialized: sdk=\(ONESIGNAL_VERSION), " - + "kmp=\(kmpVersion), path=kmp, " + + "kmp=\(remoteLogger.kmpVersion), path=kmp, " + "\(OSRemoteLoggingConfiguration.featureFlagName)=\(configuration.isFeatureEnabled), " - + "crash_dir=\(crashStoragePath)" + + "crash_dir=\(remoteLogger.crashStoragePath)" ) } @@ -360,11 +396,24 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } } + private static func onMain(_ work: () -> T) -> T { + if Thread.isMainThread { + return work() + } + return DispatchQueue.main.sync(execute: work) + } + private func message(from event: OneSignalLogEvent) -> String { event.message } } +private extension OSRemoteLoggingConfiguration { + func matches(_ other: OSRemoteLoggingConfiguration) -> Bool { + isFeatureEnabled == other.isFeatureEnabled && logLevel == other.logLevel + } +} + struct OSRemoteLoggerProviders { let installId: () -> String let onesignalId: () -> String? diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift index 232cfeca8..c28dc74d9 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift @@ -174,6 +174,41 @@ final class OSRemoteLoggingControllerTests: XCTestCase { wait(for: [telemetry.flushExpectation!], timeout: 2) } + func testConstructsRemoteLoggerOnMainWhenConfiguredInBackground() { + let constructed = expectation(description: "constructs logger on main") + let configured = expectation(description: "finishes background configuration") + let telemetry = RemoteTelemetrySpy() + let controller = makeController { _ in + XCTAssertTrue(Thread.isMainThread) + constructed.fulfill() + return telemetry + } + + DispatchQueue.global().async { + controller.configure(remoteParams: Self.remoteParams(level: "ERROR")) + configured.fulfill() + } + + wait(for: [constructed, configured], timeout: 2) + } + + func testStartupDiagnosticCanResetControllerWithoutDeadlock() { + let reset = expectation(description: "resets from startup diagnostic listener") + let telemetry = RemoteTelemetrySpy() + let controller = makeController(remoteLoggerFactory: { _ in telemetry }) + let listener = ReentrantLogListener { + controller.shutdown() + reset.fulfill() + } + OneSignalLog.debug().__add(listener) + defer { OneSignalLog.debug().__remove(listener) } + + controller.configure(remoteParams: Self.remoteParams(level: "ERROR")) + + wait(for: [reset], timeout: 2) + XCTAssertEqual(telemetry.shutdownCount, 1) + } + private func makeController( notificationCenter: NotificationCenter = NotificationCenter(), usesScenes: @escaping () -> Bool = { false }, @@ -200,6 +235,21 @@ final class OSRemoteLoggingControllerTests: XCTestCase { } } +private final class ReentrantLogListener: NSObject, OSLogListener { + private let onLog: () -> Void + + init(onLog: @escaping () -> Void) { + self.onLog = onLog + } + + func onLogEvent(_ event: OneSignalLogEvent) { + guard event.message.hasPrefix("OneSignal logging initialized:") else { + return + } + onLog() + } +} + private final class RemoteTelemetrySpy: OSRemoteLoggerProtocol { private let lock = NSLock() var emitExpectation: XCTestExpectation? From 8674e553a7903493b622ce0dd30ab9dd1368e5bd Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 10 Aug 2026 10:12:31 -0700 Subject: [PATCH 05/15] fix: [SDK-4977] remove unsupported consent gate Keep remote logging aligned with the ticket and Android behavior until consent-aware binary transport is explicitly required. Co-authored-by: Cursor --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 52c075527..e888405e7 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -701,16 +701,9 @@ + (void)setConsentRequired:(BOOL)required { + (void)setConsentGiven:(BOOL)granted { [OSPrivacyConsentController consentGranted:granted]; - if (!granted) { - [OSRemoteLoggingController reset]; - } - if (!granted || !delayedInitializationForPrivacyConsent || _delayedInitParameters == nil) { - if (granted) { - [OSRemoteLoggingController configure]; - } + if (!granted || !delayedInitializationForPrivacyConsent || _delayedInitParameters == nil) return; - } // Try to init again using delayed params [self initialize:_delayedInitParameters.appId withLaunchOptions:_delayedInitParameters.launchOptions]; delayedInitializationForPrivacyConsent = false; @@ -745,6 +738,7 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { } [[OSRemoteParamController sharedController] saveRemoteParams:result]; + [OSRemoteLoggingController configure]; if ([[OSRemoteParamController sharedController] hasLocationKey]) { BOOL shared = [result[IOS_LOCATION_SHARED] boolValue]; let oneSignalLocation = NSClassFromString(ONE_SIGNAL_LOCATION_CLASS_NAME); @@ -758,11 +752,6 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { [[OSRemoteParamController sharedController] savePrivacyConsentRequired:required]; [OSPrivacyConsentController setRequiresPrivacyConsent:required]; } - if ([OSPrivacyConsentController requiresUserPrivacyConsent]) { - [OSRemoteLoggingController reset]; - } else { - [OSRemoteLoggingController configure]; - } if (result[OUTCOMES_PARAM] && result[OUTCOMES_PARAM][IOS_OUTCOMES_V2_SERVICE_ENABLE]) [[OSOutcomeEventsCache sharedOutcomeEventsCache] saveOutcomesV2ServiceEnabled:[result[OUTCOMES_PARAM][IOS_OUTCOMES_V2_SERVICE_ENABLE] boolValue]]; From 27d826ea0b88a7cf7d8e2086fe82d397a1e2ee61 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 09:58:44 -0700 Subject: [PATCH 06/15] fix: [SDK-4977] keep remote logging changes internal Avoid adding public log-event API solely for exporter plumbing and use conventional opaque Xcode object IDs. Co-authored-by: Cursor --- .../OneSignal.xcodeproj/project.pbxproj | 36 +++++++++---------- .../OneSignalCore/Source/OneSignalLog.h | 1 - .../OneSignalCore/Source/OneSignalLog.m | 9 ++--- .../Source/OSRemoteLoggingController.swift | 5 ++- .../OSRemoteLoggingControllerTests.swift | 2 +- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 7c717b267..b11bdaa43 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -75,11 +75,11 @@ 3C14E3B32FAE54C006ED053 /* OSLoggerPlatformProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */; }; 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */; }; 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */; }; - 497700000000000000000003 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000001 /* OSRemoteLoggingController.swift */; }; - 497700000000000000000004 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000001 /* OSRemoteLoggingController.swift */; }; - 497700000000000000000005 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000001 /* OSRemoteLoggingController.swift */; }; - 497700000000000000000007 /* OSRemoteLoggingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000006 /* OSRemoteLoggingControllerTests.swift */; }; - 497700000000000000000009 /* OSRemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497700000000000000000008 /* OSRemoteLogger.swift */; }; + B96A3B6BA8CC49EE4796D9BF /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; + 25898119922BDCDA7AF0B9CC /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; + 9EAF92032D0429FA35E04417 /* OSRemoteLoggingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */; }; + ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */; }; + 7732574D325D34CC7C498199 /* OSRemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */; }; 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; }; 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */; }; 3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */; }; @@ -1794,10 +1794,10 @@ 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerPlatformProvider.swift; sourceTree = ""; }; 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinByteArray+Data.swift"; sourceTree = ""; }; 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = ""; }; - 497700000000000000000001 /* OSRemoteLoggingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingController.swift; sourceTree = ""; }; - 497700000000000000000002 /* OSRemoteLoggingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSRemoteLoggingController.h; sourceTree = ""; }; - 497700000000000000000006 /* OSRemoteLoggingControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingControllerTests.swift; sourceTree = ""; }; - 497700000000000000000008 /* OSRemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLogger.swift; sourceTree = ""; }; + 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingController.swift; sourceTree = ""; }; + C0462F96E1AADF655F3B3765 /* OSRemoteLoggingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSRemoteLoggingController.h; sourceTree = ""; }; + 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingControllerTests.swift; sourceTree = ""; }; + 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLogger.swift; sourceTree = ""; }; DEF5CCF12539321A0003E9CC /* UnitTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnitTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEF5CCF32539321A0003E9CC /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; DEF5CCF42539321A0003E9CC /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -2294,7 +2294,7 @@ 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */, 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */, 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */, - 497700000000000000000008 /* OSRemoteLogger.swift */, + 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */, ); path = Logging; sourceTree = ""; @@ -2675,7 +2675,7 @@ 4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */, 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, - 497700000000000000000006 /* OSRemoteLoggingControllerTests.swift */, + 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -2708,8 +2708,8 @@ 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */, 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */, DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */, - 497700000000000000000002 /* OSRemoteLoggingController.h */, - 497700000000000000000001 /* OSRemoteLoggingController.swift */, + C0462F96E1AADF655F3B3765 /* OSRemoteLoggingController.h */, + 8A72F938F8A3808AC1FF7F3C /* OSRemoteLoggingController.swift */, 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */, ); path = Source; @@ -4392,7 +4392,7 @@ 9124120E1E73342200E41FD7 /* OneSignal.m in Sources */, 9124121E1E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */, 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */, - 497700000000000000000003 /* OSRemoteLoggingController.swift in Sources */, + B96A3B6BA8CC49EE4796D9BF /* OSRemoteLoggingController.swift in Sources */, 912412471E73369600E41FD7 /* OneSignalHelper.m in Sources */, CA8E19062193C76D009DA223 /* OSInAppMessagingHelpers.m in Sources */, 7AAA60682485D0420004FADE /* OSMigrationController.m in Sources */, @@ -4438,7 +4438,7 @@ 3C14E3B12FAE54C006ED053 /* OneSignalLogHttpSender.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */, - 497700000000000000000009 /* OSRemoteLogger.swift in Sources */, + 7732574D325D34CC7C498199 /* OSRemoteLogger.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */, @@ -4545,7 +4545,7 @@ 9124123F1E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m in Sources */, 7AECE59F23675F6300537907 /* OSFocusTimeProcessorFactory.m in Sources */, DEC08B012947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, - 497700000000000000000004 /* OSRemoteLoggingController.swift in Sources */, + 25898119922BDCDA7AF0B9CC /* OSRemoteLoggingController.swift in Sources */, CA1A6E7120DC2E73001C41B9 /* OneSignalDialogRequest.m in Sources */, 912412331E73342200E41FD7 /* OneSignalTracker.m in Sources */, 7A674F1C2360D82E001F9ACD /* OSBaseFocusTimeProcessor.m in Sources */, @@ -4640,7 +4640,7 @@ CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */, 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, - 497700000000000000000007 /* OSRemoteLoggingControllerTests.swift in Sources */, + ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, @@ -4671,7 +4671,7 @@ 03CCCC852835F291004BF794 /* UIApplicationDelegateSwizzlingTests.m in Sources */, 4529DEEA1FA8360C00CEAB1D /* UIApplicationOverrider.m in Sources */, DEC08B022947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, - 497700000000000000000005 /* OSRemoteLoggingController.swift in Sources */, + 9EAF92032D0429FA35E04417 /* OSRemoteLoggingController.swift in Sources */, 7A93269E25AF4F0300BBEC27 /* OSPendingCallbacks.m in Sources */, 7AECE59823674AB700537907 /* OSUnattributedFocusTimeProcessor.m in Sources */, 7A5A818224897693002E07C8 /* MigrationTests.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h index 43062ac35..43c92024a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h @@ -39,7 +39,6 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) { @interface OneSignalLogEvent : NSObject @property(readonly)ONE_S_LOG_LEVEL level; @property(readonly, nonnull)NSString *entry; -@property(readonly, nonnull)NSString *message; @end @protocol OSLogListener diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m index 140289aa1..2ed5b3896 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m @@ -31,10 +31,9 @@ #import "OSCopyOnWriteSet.h" @implementation OneSignalLogEvent -- (instancetype)initWithLevel:(ONE_S_LOG_LEVEL)level entry:(NSString*)entry message:(NSString*)message { +- (instancetype)initWithLevel:(ONE_S_LOG_LEVEL)level entry:(NSString*)entry { _level = level; _entry = entry; - _message = message; return self; } @end @@ -116,11 +115,7 @@ void onesignal_Log(ONE_S_LOG_LEVEL logLevel, NSString* message) { for (NSObject *listener in OneSignalLog.logListeners.allObjects) { if ([listener respondsToSelector:@selector(onLogEvent:)]) { - OneSignalLogEvent *event = [[OneSignalLogEvent alloc] - initWithLevel:logLevel - entry:[levelString stringByAppendingString:message] - message:message - ]; + OneSignalLogEvent *event = [[OneSignalLogEvent alloc] initWithLevel:logLevel entry:[levelString stringByAppendingString:message]]; [listener onLogEvent:event]; } } diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index b76930fcd..1a24b85fc 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -404,7 +404,10 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } private func message(from event: OneSignalLogEvent) -> String { - event.message + guard let separator = event.entry.range(of: ": ") else { + return event.entry + } + return String(event.entry[separator.upperBound...]) } } diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift index c28dc74d9..c6d4cfe22 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift @@ -243,7 +243,7 @@ private final class ReentrantLogListener: NSObject, OSLogListener { } func onLogEvent(_ event: OneSignalLogEvent) { - guard event.message.hasPrefix("OneSignal logging initialized:") else { + guard event.entry.contains("OneSignal logging initialized:") else { return } onLog() From 308f310f10ee0c682912dcbb53bf3f0fb515ae75 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 10:39:16 -0700 Subject: [PATCH 07/15] refactor: [SDK-4977] derive remote log level Keep the parsed threshold as the single source of truth for remote logging configuration. Co-authored-by: Cursor --- iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index 1a24b85fc..4a5f83e8c 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -39,9 +39,12 @@ struct OSRemoteLoggingConfiguration { private static let logLevelKey = "log_level" let isFeatureEnabled: Bool - let logLevel: String? private let threshold: ONE_S_LOG_LEVEL? + var logLevel: String? { + threshold.map(Self.levelName) + } + var isRemoteLoggingEnabled: Bool { isFeatureEnabled && threshold != nil && threshold != .LL_NONE } @@ -62,7 +65,6 @@ struct OSRemoteLoggingConfiguration { $0.caseInsensitiveCompare(Self.featureFlagKey) == .orderedSame } threshold = parsedThreshold - logLevel = parsedThreshold.map(Self.levelName) } func allows(_ level: ONE_S_LOG_LEVEL) -> Bool { From 16fc15a0b759f1619ca12a6e7d4a25d91208c46d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 10:43:39 -0700 Subject: [PATCH 08/15] fix: [SDK-4977] use available logging config Avoid reading feature flags from remote params until iOS FeatureManager exposes the backend field. Co-authored-by: Cursor --- .../Source/OSRemoteLoggingController.swift | 24 +++----------- .../OSRemoteLoggingControllerTests.swift | 32 +++---------------- 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index 4a5f83e8c..c14ba8dc9 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -32,13 +32,9 @@ import OneSignalUser import UIKit struct OSRemoteLoggingConfiguration { - static let featureFlagName = "SDK_CUSTOM_LOGGING" - private static let featureFlagKey = "sdk_custom_logging" - private static let featureFlagsKey = "sdk_remote_feature_flags" private static let loggingConfigKey = "logging_config" private static let logLevelKey = "log_level" - let isFeatureEnabled: Bool private let threshold: ONE_S_LOG_LEVEL? var logLevel: String? { @@ -46,7 +42,7 @@ struct OSRemoteLoggingConfiguration { } var isRemoteLoggingEnabled: Bool { - isFeatureEnabled && threshold != nil && threshold != .LL_NONE + threshold != nil && threshold != .LL_NONE } static var current: OSRemoteLoggingConfiguration { @@ -56,14 +52,10 @@ struct OSRemoteLoggingConfiguration { init(remoteParams: [String: Any]) { let loggingConfig = remoteParams[Self.loggingConfigKey] as? [String: Any] - let enabledFlagNames = remoteParams[Self.featureFlagsKey] as? [String] ?? [] let parsedThreshold = (loggingConfig?[Self.logLevelKey] as? String) .map { $0.uppercased() } .flatMap(Self.oneSignalLevel) - isFeatureEnabled = enabledFlagNames.contains { - $0.caseInsensitiveCompare(Self.featureFlagKey) == .orderedSame - } threshold = parsedThreshold } @@ -258,7 +250,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { return } - logStartupDiagnostic(remoteLogger: newRemoteLogger, configuration: newConfiguration) + logStartupDiagnostic(remoteLogger: newRemoteLogger) stateQueue.sync { guard self.remoteLogger === newRemoteLogger, self.configuration.matches(newConfiguration) else { @@ -303,15 +295,11 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { ) } - private func logStartupDiagnostic( - remoteLogger: OSRemoteLoggerProtocol, - configuration: OSRemoteLoggingConfiguration - ) { + private func logStartupDiagnostic(remoteLogger: OSRemoteLoggerProtocol) { OneSignalLog.onesignalLog( .LL_WARN, message: "OneSignal logging initialized: sdk=\(ONESIGNAL_VERSION), " + "kmp=\(remoteLogger.kmpVersion), path=kmp, " - + "\(OSRemoteLoggingConfiguration.featureFlagName)=\(configuration.isFeatureEnabled), " + "crash_dir=\(remoteLogger.crashStoragePath)" ) } @@ -322,9 +310,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { onesignalId: { OneSignalUserManagerImpl.sharedInstance.onesignalId }, pushSubscriptionId: { OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId }, appState: { [weak self] in self?.currentAppState ?? "unknown" }, - featureFlags: { - configuration.isFeatureEnabled ? [OSRemoteLoggingConfiguration.featureFlagName] : [] - }, + featureFlags: { [] }, remoteLogLevel: { configuration.logLevel }, exporterLoggingEnabled: { false } ) @@ -415,7 +401,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private extension OSRemoteLoggingConfiguration { func matches(_ other: OSRemoteLoggingConfiguration) -> Bool { - isFeatureEnabled == other.isFeatureEnabled && logLevel == other.logLevel + logLevel == other.logLevel } } diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift index c6d4cfe22..f7a5b936e 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift @@ -40,22 +40,9 @@ final class OSRemoteLoggingControllerTests: XCTestCase { super.tearDown() } - func testConfigurationRequiresFeatureFlagAndRemoteLevel() { - let levelOnly = OSRemoteLoggingConfiguration( - remoteParams: ["logging_config": ["log_level": "WARN"]] - ) - XCTAssertFalse(levelOnly.isRemoteLoggingEnabled) - - let flagOnly = OSRemoteLoggingConfiguration( - remoteParams: ["sdk_remote_feature_flags": ["sdk_custom_logging"]] - ) - XCTAssertFalse(flagOnly.isRemoteLoggingEnabled) - + func testConfigurationUsesRemoteLogLevel() { let enabled = OSRemoteLoggingConfiguration( - remoteParams: [ - "sdk_remote_feature_flags": ["sdk_custom_logging"], - "logging_config": ["log_level": "warn"] - ] + remoteParams: ["logging_config": ["log_level": "warn"]] ) XCTAssertTrue(enabled.isRemoteLoggingEnabled) XCTAssertTrue(enabled.allows(.LL_ERROR)) @@ -63,10 +50,7 @@ final class OSRemoteLoggingControllerTests: XCTestCase { XCTAssertFalse(enabled.allows(.LL_INFO)) let invalidLevel = OSRemoteLoggingConfiguration( - remoteParams: [ - "sdk_remote_feature_flags": ["sdk_custom_logging"], - "logging_config": ["log_level": "OFF"] - ] + remoteParams: ["logging_config": ["log_level": "OFF"]] ) XCTAssertFalse(invalidLevel.isRemoteLoggingEnabled) } @@ -90,10 +74,7 @@ final class OSRemoteLoggingControllerTests: XCTestCase { ) controller.configure( - remoteParams: [ - "sdk_remote_feature_flags": ["sdk_custom_logging"], - "logging_config": ["log_level": "ERROR"] - ] + remoteParams: ["logging_config": ["log_level": "ERROR"]] ) OneSignalLog.onesignalLog(.LL_INFO, message: "not uploaded") OneSignalLog.onesignalLog(.LL_ERROR, message: "uploaded") @@ -228,10 +209,7 @@ final class OSRemoteLoggingControllerTests: XCTestCase { } private static func remoteParams(level: String) -> [String: Any] { - [ - "sdk_remote_feature_flags": ["sdk_custom_logging"], - "logging_config": ["log_level": level] - ] + ["logging_config": ["log_level": level]] } } From 110a56a649540078462b44166b9ca8554b7a1f12 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 10:50:26 -0700 Subject: [PATCH 09/15] fix: [SDK-4977] restore logging on cold start Cache the app-scoped remote log level so telemetry can start before the next remote-params request completes. Co-authored-by: Cursor --- .../Source/OSRemoteLoggingController.h | 1 + .../Source/OSRemoteLoggingController.swift | 37 ++++++++++++++++++- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 4 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h index 2801cf0be..749f9898b 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h @@ -32,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OSRemoteLoggingController : NSObject + (void)configure; ++ (void)configureFromCache; + (void)reset; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index c14ba8dc9..a8ae97d9a 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -112,6 +112,9 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private static let shared = OSRemoteLoggingController() private static let installIdKey = "PREFS_OS_INSTALL_ID" + private static let cachedConfigurationKey = "PREFS_OS_REMOTE_LOGGING_CONFIGURATION" + private static let cachedAppIdKey = "app_id" + private static let cachedLogLevelKey = "log_level" private static let backgroundTaskPrefix = "com.onesignal.logger.flush." private static let installId: String = { let defaults = OneSignalUserDefaults.initShared() @@ -163,13 +166,45 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } @objc class func configure() { - shared.configure(with: .current) + let configuration = OSRemoteLoggingConfiguration.current + cache(configuration: configuration) + shared.configure(with: configuration) + } + + @objc class func configureFromCache() { + guard let appId = OneSignalIdentifiers.currentAppId, + let cached = OneSignalUserDefaults.initStandard().getSavedDictionary( + forKey: cachedConfigurationKey, + defaultValue: nil + ), + cached[cachedAppIdKey] as? String == appId else { + return + } + let logLevel = cached[cachedLogLevelKey] as? String + let remoteParams = logLevel.map { + ["logging_config": ["log_level": $0]] + } ?? [:] + shared.configure(with: OSRemoteLoggingConfiguration(remoteParams: remoteParams)) } @objc class func reset() { shared.shutdown() } + private class func cache(configuration: OSRemoteLoggingConfiguration) { + guard let appId = OneSignalIdentifiers.currentAppId else { + return + } + var cached: [String: Any] = [cachedAppIdKey: appId] + if let logLevel = configuration.logLevel { + cached[cachedLogLevelKey] = logLevel + } + OneSignalUserDefaults.initStandard().saveDictionary( + forKey: cachedConfigurationKey, + withValue: cached + ) + } + func configure(remoteParams: [String: Any]) { configure(with: OSRemoteLoggingConfiguration(remoteParams: remoteParams)) } diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index e888405e7..f589922aa 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -587,7 +587,9 @@ + (void)init { OSBackgroundTaskManager.taskHandler = [OSBackgroundTaskHandlerImpl new]; [self registerForAPNsToken]; - + + [OSRemoteLoggingController configureFromCache]; + // Wrapper SDK's call init twice and pass null as the appId on the first call // the app ID is required to download parameters, so do not download params until the appID is provided if (!_didCallDownloadParameters && OneSignalIdentifiers.currentAppId && OneSignalIdentifiers.currentAppId != (id)[NSNull null]) From 4ab4bbab2e8dbf7b2926b10547f1ae1064d1ea3b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 16:42:06 -0700 Subject: [PATCH 10/15] fix: [SDK-4977] route logs through private sink Keep remote telemetry independent from the customer-facing log listener and preserve raw log messages. Co-authored-by: Cursor --- .../OneSignalCore/Source/OneSignalLog.m | 11 +++++ .../Source/OSRemoteLoggingController.swift | 48 ++++++++++++------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m index 2ed5b3896..116a03566 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m @@ -42,6 +42,9 @@ @implementation OneSignalLog static ONE_S_LOG_LEVEL _nsLogLevel = ONE_S_LL_WARN; static ONE_S_LOG_LEVEL _alertLogLevel = ONE_S_LL_NONE; +static NSString * const OSInternalLogNotification = @"com.onesignal.internal.log"; +static NSString * const OSInternalLogLevelKey = @"level"; +static NSString * const OSInternalLogMessageKey = @"message"; + (Class)Debug { return self; @@ -113,6 +116,14 @@ void onesignal_Log(ONE_S_LOG_LEVEL logLevel, NSString* message) { [[OSDialogInstanceManager sharedInstance] presentDialogWithTitle:levelString withMessage:message withActions:nil cancelTitle:NSLocalizedString(@"Close", @"Close button") withActionCompletion:nil]; } + [[NSNotificationCenter defaultCenter] + postNotificationName:OSInternalLogNotification + object:nil + userInfo:@{ + OSInternalLogLevelKey: @(logLevel), + OSInternalLogMessageKey: message + }]; + for (NSObject *listener in OneSignalLog.logListeners.allObjects) { if ([listener respondsToSelector:@selector(onLogEvent:)]) { OneSignalLogEvent *event = [[OneSignalLogEvent alloc] initWithLevel:logLevel entry:[levelString stringByAppendingString:message]]; diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index a8ae97d9a..7ce7c546d 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -107,10 +107,13 @@ struct OSRemoteLoggingConfiguration { } @objc(OSRemoteLoggingController) -final class OSRemoteLoggingController: NSObject, OSLogListener { +final class OSRemoteLoggingController: NSObject { typealias RemoteLoggerFactory = (OSRemoteLoggerProviders) -> OSRemoteLoggerProtocol private static let shared = OSRemoteLoggingController() + private static let internalLogNotification = Notification.Name("com.onesignal.internal.log") + private static let internalLogLevelKey = "level" + private static let internalLogMessageKey = "message" private static let installIdKey = "PREFS_OS_INSTALL_ID" private static let cachedConfigurationKey = "PREFS_OS_REMOTE_LOGGING_CONFIGURATION" private static let cachedAppIdKey = "app_id" @@ -129,6 +132,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private let stateQueue = DispatchQueue(label: "com.onesignal.logger.remote-lifecycle") private let appStateLock = NSLock() private let notificationCenter: NotificationCenter + private let logNotificationCenter: NotificationCenter private let remoteLoggerFactory: RemoteLoggerFactory private let usesScenes: () -> Bool private let beginBackgroundTask: (String) -> Void @@ -138,10 +142,11 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { private var remoteLogger: OSRemoteLoggerProtocol? private var appState = "unknown" private var notificationTokens: [NSObjectProtocol] = [] - private var isListening = false + private var logObserverToken: NSObjectProtocol? init( notificationCenter: NotificationCenter = .default, + logNotificationCenter: NotificationCenter = .default, usesScenes: @escaping () -> Bool = { OSBundleUtils.isAppUsingUIScene() }, beginBackgroundTask: @escaping (String) -> Void = OSBackgroundTaskManager.beginBackgroundTask, endBackgroundTask: @escaping (String) -> Void = OSBackgroundTaskManager.endBackgroundTask, @@ -158,6 +163,7 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { } ) { self.notificationCenter = notificationCenter + self.logNotificationCenter = logNotificationCenter self.usesScenes = usesScenes self.beginBackgroundTask = beginBackgroundTask self.endBackgroundTask = endBackgroundTask @@ -209,16 +215,16 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { configure(with: OSRemoteLoggingConfiguration(remoteParams: remoteParams)) } - func onLogEvent(_ event: OneSignalLogEvent) { + private func onInternalLog(level: ONE_S_LOG_LEVEL, message: String) { stateQueue.async { [weak self] in guard let self, - self.configuration.allows(event.level), + self.configuration.allows(level), let remoteLogger = self.remoteLogger else { return } remoteLogger.log( - level: OSRemoteLoggingConfiguration.levelName(event.level), - message: self.message(from: event) + level: OSRemoteLoggingConfiguration.levelName(level), + message: message ) } } @@ -291,16 +297,15 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { self.configuration.matches(newConfiguration) else { return } - OneSignalLog.debug().__add(self) - self.isListening = true + self.registerLogSink() self.registerLifecycleObservers() } } private func stopRemoteLogging() { - if isListening { - OneSignalLog.debug().__remove(self) - isListening = false + if let logObserverToken { + logNotificationCenter.removeObserver(logObserverToken) + self.logObserverToken = nil } notificationTokens.forEach(notificationCenter.removeObserver) notificationTokens.removeAll() @@ -330,6 +335,21 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { ) } + private func registerLogSink() { + logObserverToken = logNotificationCenter.addObserver( + forName: Self.internalLogNotification, + object: nil, + queue: nil + ) { [weak self] notification in + guard let rawLevel = notification.userInfo?[Self.internalLogLevelKey] as? NSNumber, + let level = ONE_S_LOG_LEVEL(rawValue: rawLevel.uintValue), + let message = notification.userInfo?[Self.internalLogMessageKey] as? String else { + return + } + self?.onInternalLog(level: level, message: message) + } + } + private func logStartupDiagnostic(remoteLogger: OSRemoteLoggerProtocol) { OneSignalLog.onesignalLog( .LL_WARN, @@ -426,12 +446,6 @@ final class OSRemoteLoggingController: NSObject, OSLogListener { return DispatchQueue.main.sync(execute: work) } - private func message(from event: OneSignalLogEvent) -> String { - guard let separator = event.entry.range(of: ": ") else { - return event.entry - } - return String(event.entry[separator.upperBound...]) - } } private extension OSRemoteLoggingConfiguration { From d89c02480ecc6351ae73d59c2c8377e563a5a50e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 16:59:28 -0700 Subject: [PATCH 11/15] fix: [SDK-4977] initialize logging before startup work Restore cached telemetry before initialization logs and other critical startup operations run. Co-authored-by: Cursor --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index f589922aa..b8ab6e2ea 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -573,6 +573,8 @@ + (void)setupProtectedDataObserverOnce { Called after setAppId and setLaunchOptions, depending on which one is called last (order does not matter) */ + (void)init { + [OSRemoteLoggingController configureFromCache]; + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"launchOptions is set and appId of %@ is set, initializing OneSignal...", OneSignalIdentifiers.currentAppId]]; [self setupProtectedDataObserverOnce]; @@ -588,8 +590,6 @@ + (void)init { [self registerForAPNsToken]; - [OSRemoteLoggingController configureFromCache]; - // Wrapper SDK's call init twice and pass null as the appId on the first call // the app ID is required to download parameters, so do not download params until the appID is provided if (!_didCallDownloadParameters && OneSignalIdentifiers.currentAppId && OneSignalIdentifiers.currentAppId != (id)[NSNull null]) From 4825109c9a3a1d72082d86c91c1e5f49146be199 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 12 Aug 2026 17:14:31 -0700 Subject: [PATCH 12/15] fix: [SDK-4977] avoid consent warnings in telemetry Read the internal cached OneSignal ID without invoking the customer-facing consent-gated accessor for every log event. Co-authored-by: Cursor --- .../OneSignalSDK/Source/OSRemoteLoggingController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index 7ce7c546d..5a2178b8d 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -362,7 +362,12 @@ final class OSRemoteLoggingController: NSObject { private func makeProviders(configuration: OSRemoteLoggingConfiguration) -> OSRemoteLoggerProviders { OSRemoteLoggerProviders( installId: { Self.installId }, - onesignalId: { OneSignalUserManagerImpl.sharedInstance.onesignalId }, + onesignalId: { + OneSignalUserDefaults.initShared().getSavedString( + forKey: OS_SNAPSHOT_ONESIGNAL_ID, + defaultValue: nil + ) + }, pushSubscriptionId: { OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId }, appState: { [weak self] in self?.currentAppState ?? "unknown" }, featureFlags: { [] }, From a6ead4b1042ecb835fe53572d11451e412888527 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 11:05:55 -0700 Subject: [PATCH 13/15] fix: [SDK-4977] route current user data through private telemetry sink Keep remote telemetry independent from presentation listeners while preserving structured errors and reading the live OneSignal ID without consent warnings. Co-authored-by: Cursor --- .../OneSignal.xcodeproj/project.pbxproj | 4 ++ .../OneSignalCore/Source/OneSignalCore.h | 1 + .../OneSignalCore/Source/OneSignalLog.m | 36 ++++++++--- .../Source/OneSignalLogInternal.h | 49 +++++++++++++++ .../OneSignalCoreTests/LoggingTests.swift | 28 +++++++++ .../Source/Logging/OSRemoteLogger.swift | 46 +++++++++++++- .../Source/OneSignalUserManagerImpl.swift | 7 ++- .../OneSignalUserTests.swift | 11 +++- .../Source/OSRemoteLoggingController.swift | 61 ++++++------------- .../OSRemoteLoggingControllerTests.swift | 46 +++++++++++++- 10 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLogInternal.h diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index b11bdaa43..078da8380 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -443,6 +443,7 @@ DE7D1832270279D9002D3A5D /* OSNotificationClasses.h in Headers */ = {isa = PBXBuildFile; fileRef = DE7D1831270279D9002D3A5D /* OSNotificationClasses.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE7D183427027A73002D3A5D /* OneSignalLog.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7D183327027A73002D3A5D /* OneSignalLog.m */; }; DE7D183627027AA0002D3A5D /* OneSignalLog.h in Headers */ = {isa = PBXBuildFile; fileRef = DE7D183527027AA0002D3A5D /* OneSignalLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7B30D5F3E124B58A41C9D20 /* OneSignalLogInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = C2F81A6D904B47E3B5A02C11 /* OneSignalLogInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE7D183827027CC4002D3A5D /* OneSignalAttachmentHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = DE7D183727027CC4002D3A5D /* OneSignalAttachmentHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE7D183A27027CD7002D3A5D /* OneSignalAttachmentHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7D183927027CD7002D3A5D /* OneSignalAttachmentHandler.m */; }; DE7D183B27027EFC002D3A5D /* NSURL+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = CA36A42B208FDEFB003EFA9A /* NSURL+OneSignal.m */; }; @@ -1694,6 +1695,7 @@ DE7D1831270279D9002D3A5D /* OSNotificationClasses.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSNotificationClasses.h; sourceTree = ""; }; DE7D183327027A73002D3A5D /* OneSignalLog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalLog.m; sourceTree = ""; }; DE7D183527027AA0002D3A5D /* OneSignalLog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalLog.h; sourceTree = ""; }; + C2F81A6D904B47E3B5A02C11 /* OneSignalLogInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalLogInternal.h; sourceTree = ""; }; DE7D183727027CC4002D3A5D /* OneSignalAttachmentHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalAttachmentHandler.h; sourceTree = ""; }; DE7D183927027CD7002D3A5D /* OneSignalAttachmentHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalAttachmentHandler.m; sourceTree = ""; }; DE7D1842270283B9002D3A5D /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk/System/Library/Frameworks/UserNotifications.framework; sourceTree = DEVELOPER_DIR; }; @@ -2828,6 +2830,7 @@ 4529DF0B1FA932AC00CEAB1D /* OneSignalTrackFirebaseAnalytics.m */, DE7D1831270279D9002D3A5D /* OSNotificationClasses.h */, DE7D183527027AA0002D3A5D /* OneSignalLog.h */, + C2F81A6D904B47E3B5A02C11 /* OneSignalLogInternal.h */, DE7D183327027A73002D3A5D /* OneSignalLog.m */, 3CCF44BC299B17290021964D /* OneSignalWrapper.h */, 3CCF44BD299B17290021964D /* OneSignalWrapper.m */, @@ -3386,6 +3389,7 @@ DE971754274C48CF00FC409E /* OSPrivacyConsentController.h in Headers */, 3CE8CC4E2911ADD1000DB0D3 /* OSDeviceUtils.h in Headers */, DE7D183627027AA0002D3A5D /* OneSignalLog.h in Headers */, + A7B30D5F3E124B58A41C9D20 /* OneSignalLogInternal.h in Headers */, 3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */, 3CCF44BE299B17290021964D /* OneSignalWrapper.h in Headers */, DE7D182D270273B0002D3A5D /* OSNotification.h in Headers */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h index 4c8cdc8ef..4e22d1281 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h @@ -33,6 +33,7 @@ #import #import #import +#import #import #import #import diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m index 116a03566..a44a50eab 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.m @@ -26,7 +26,9 @@ */ #import +#import #import "OneSignalLog.h" +#import "OneSignalLogInternal.h" #import "OSDialogInstanceManager.h" #import "OSCopyOnWriteSet.h" @@ -42,9 +44,8 @@ @implementation OneSignalLog static ONE_S_LOG_LEVEL _nsLogLevel = ONE_S_LL_WARN; static ONE_S_LOG_LEVEL _alertLogLevel = ONE_S_LL_NONE; -static NSString * const OSInternalLogNotification = @"com.onesignal.internal.log"; -static NSString * const OSInternalLogLevelKey = @"level"; -static NSString * const OSInternalLogMessageKey = @"message"; +static os_unfair_lock _internalLogSinkLock = OS_UNFAIR_LOCK_INIT; +static NSObject *_internalLogSink; + (Class)Debug { return self; @@ -75,6 +76,20 @@ + (void)removeLogListener:(NSObject*_Nonnull)listener { [self.logListeners removeObject:listener]; } ++ (void)setInternalLogSink:(NSObject *)sink { + os_unfair_lock_lock(&_internalLogSinkLock); + _internalLogSink = sink; + os_unfair_lock_unlock(&_internalLogSinkLock); +} + ++ (void)removeInternalLogSink:(NSObject *)sink { + os_unfair_lock_lock(&_internalLogSinkLock); + if (_internalLogSink == sink) { + _internalLogSink = nil; + } + os_unfair_lock_unlock(&_internalLogSinkLock); +} + + (void)onesignalLog:(ONE_S_LOG_LEVEL)logLevel message:(NSString* _Nonnull)message { onesignal_Log(logLevel, message); } @@ -116,13 +131,14 @@ void onesignal_Log(ONE_S_LOG_LEVEL logLevel, NSString* message) { [[OSDialogInstanceManager sharedInstance] presentDialogWithTitle:levelString withMessage:message withActions:nil cancelTitle:NSLocalizedString(@"Close", @"Close button") withActionCompletion:nil]; } - [[NSNotificationCenter defaultCenter] - postNotificationName:OSInternalLogNotification - object:nil - userInfo:@{ - OSInternalLogLevelKey: @(logLevel), - OSInternalLogMessageKey: message - }]; + os_unfair_lock_lock(&_internalLogSinkLock); + NSObject *internalLogSink = _internalLogSink; + os_unfair_lock_unlock(&_internalLogSinkLock); + [internalLogSink captureLogWithLevel:logLevel + message:message + exceptionType:nil + exceptionMessage:nil + exceptionStacktrace:nil]; for (NSObject *listener in OneSignalLog.logListeners.allObjects) { if ([listener respondsToSelector:@selector(onLogEvent:)]) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLogInternal.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLogInternal.h new file mode 100644 index 000000000..6f0962e2d --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLogInternal.h @@ -0,0 +1,49 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +#import "OneSignalLog.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol OSInternalLogSink + +- (void)captureLogWithLevel:(ONE_S_LOG_LEVEL)level + message:(NSString *)message + exceptionType:(nullable NSString *)exceptionType + exceptionMessage:(nullable NSString *)exceptionMessage + exceptionStacktrace:(nullable NSString *)exceptionStacktrace; + +@end + +@interface OneSignalLog (Internal) + ++ (void)setInternalLogSink:(NSObject *)sink NS_SWIFT_NAME(__setInternalLogSink(_:)); ++ (void)removeInternalLogSink:(NSObject *)sink NS_SWIFT_NAME(__removeInternalLogSink(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift index 953537141..b0a2cf8fe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/LoggingTests.swift @@ -43,6 +43,22 @@ class TestLogListener: NSObject, OSLogListener { } } +private final class TestInternalLogSink: NSObject, OSInternalLogSink { + var levels: [ONE_S_LOG_LEVEL] = [] + var messages: [String] = [] + + func captureLog( + with level: ONE_S_LOG_LEVEL, + message: String, + exceptionType: String?, + exceptionMessage: String?, + exceptionStacktrace: String? + ) { + levels.append(level) + messages.append(message) + } +} + final class LoggingTests: XCTestCase { override func setUpWithError() throws { OneSignalLog.setLogLevel(.LL_NONE) @@ -141,4 +157,16 @@ final class LoggingTests: XCTestCase { // Then XCTAssertEqual(calls, ["DEBUG: test"]) } + + func testInternalSinkReceivesRawLogAndCanBeRemoved() { + let sink = TestInternalLogSink() + OneSignalLog.__setInternalLogSink(sink) + + OneSignalLog.onesignalLog(.LL_WARN, message: "raw message") + OneSignalLog.__removeInternalLogSink(sink) + OneSignalLog.onesignalLog(.LL_ERROR, message: "not captured") + + XCTAssertEqual(sink.levels, [.LL_WARN]) + XCTAssertEqual(sink.messages, ["raw message"]) + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift index ec14e930d..e8cd3d7e0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSRemoteLogger.swift @@ -36,6 +36,17 @@ public protocol OSRemoteLoggerProtocol: AnyObject { func shutdown() } +@_spi(OneSignalInternal) +public protocol OSStructuredRemoteLoggerProtocol: OSRemoteLoggerProtocol { + func log( + level: String, + message: String, + exceptionType: String?, + exceptionMessage: String?, + exceptionStacktrace: String? + ) +} + #if !targetEnvironment(macCatalyst) @_implementationOnly import OneSignalKMP @@ -84,13 +95,29 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol { } public func log(level: String, message: String) { - LogLoggingHelper.shared.log( - telemetry: telemetry, + log( level: level, message: message, exceptionType: nil, exceptionMessage: nil, - exceptionStacktrace: nil, + exceptionStacktrace: nil + ) + } + + public func log( + level: String, + message: String, + exceptionType: String?, + exceptionMessage: String?, + exceptionStacktrace: String? + ) { + LogLoggingHelper.shared.log( + telemetry: telemetry, + level: level, + message: message, + exceptionType: exceptionType, + exceptionMessage: exceptionMessage, + exceptionStacktrace: exceptionStacktrace, completionHandler: { _ in } ) } @@ -104,6 +131,9 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol { } } +@_spi(OneSignalInternal) +extension OSRemoteLogger: OSStructuredRemoteLoggerProtocol {} + #else public final class OSRemoteLogger: OSRemoteLoggerProtocol { @@ -121,10 +151,20 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol { public let crashStoragePath = "unavailable" public func log(level: String, message: String) {} + public func log( + level: String, + message: String, + exceptionType: String?, + exceptionMessage: String?, + exceptionStacktrace: String? + ) {} public func forceFlush(completion: @escaping () -> Void) { completion() } public func shutdown() {} } +@_spi(OneSignalInternal) +extension OSRemoteLogger: OSStructuredRemoteLoggerProtocol {} + #endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index eb7608b2d..d9850691f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -690,7 +690,12 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "onesignalId") else { return nil } - return _user?.identityModel.onesignalId + return internalOnesignalId + } + + @_spi(OneSignalInternal) + public var internalOnesignalId: String? { + _user?.identityModel.onesignalId } public func addObserver(_ observer: OSUserStateObserver) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index bfe416b46..a66d10394 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -31,7 +31,7 @@ import OneSignalCoreMocks import OneSignalUserMocks // Testable import OSCore to allow setting a different poll flush interval @testable import OneSignalOSCore -@testable import OneSignalUser +@_spi(OneSignalInternal) @testable import OneSignalUser final class OneSignalUserTests: XCTestCase { @@ -48,6 +48,15 @@ final class OneSignalUserTests: XCTestCase { override func tearDownWithError() throws { } + func testInternalOnesignalIdTracksCurrentUser() { + let manager = OneSignalUserManagerImpl.sharedInstance + OneSignalUserMocks.setUserManagerInternalUser(externalId: "user-a", onesignalId: "osid-a") + XCTAssertEqual(manager.internalOnesignalId, "osid-a") + + OneSignalUserMocks.setUserManagerInternalUser(externalId: "user-b", onesignalId: "osid-b") + XCTAssertEqual(manager.internalOnesignalId, "osid-b") + } + // Comparable to Android test: "externalId is backed by the identity model" func testLoginSetsExternalId() throws { /* Setup */ diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index 5a2178b8d..e5ec6032f 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -27,8 +27,8 @@ import Foundation import OneSignalCore -import OneSignalOSCore -import OneSignalUser +@_spi(OneSignalInternal) import OneSignalOSCore +@_spi(OneSignalInternal) import OneSignalUser import UIKit struct OSRemoteLoggingConfiguration { @@ -107,13 +107,10 @@ struct OSRemoteLoggingConfiguration { } @objc(OSRemoteLoggingController) -final class OSRemoteLoggingController: NSObject { - typealias RemoteLoggerFactory = (OSRemoteLoggerProviders) -> OSRemoteLoggerProtocol +final class OSRemoteLoggingController: NSObject, OSInternalLogSink { + typealias RemoteLoggerFactory = (OSRemoteLoggerProviders) -> OSStructuredRemoteLoggerProtocol private static let shared = OSRemoteLoggingController() - private static let internalLogNotification = Notification.Name("com.onesignal.internal.log") - private static let internalLogLevelKey = "level" - private static let internalLogMessageKey = "message" private static let installIdKey = "PREFS_OS_INSTALL_ID" private static let cachedConfigurationKey = "PREFS_OS_REMOTE_LOGGING_CONFIGURATION" private static let cachedAppIdKey = "app_id" @@ -132,21 +129,18 @@ final class OSRemoteLoggingController: NSObject { private let stateQueue = DispatchQueue(label: "com.onesignal.logger.remote-lifecycle") private let appStateLock = NSLock() private let notificationCenter: NotificationCenter - private let logNotificationCenter: NotificationCenter private let remoteLoggerFactory: RemoteLoggerFactory private let usesScenes: () -> Bool private let beginBackgroundTask: (String) -> Void private let endBackgroundTask: (String) -> Void private var configuration = OSRemoteLoggingConfiguration(remoteParams: [:]) private var configurationGeneration = 0 - private var remoteLogger: OSRemoteLoggerProtocol? + private var remoteLogger: OSStructuredRemoteLoggerProtocol? private var appState = "unknown" private var notificationTokens: [NSObjectProtocol] = [] - private var logObserverToken: NSObjectProtocol? init( notificationCenter: NotificationCenter = .default, - logNotificationCenter: NotificationCenter = .default, usesScenes: @escaping () -> Bool = { OSBundleUtils.isAppUsingUIScene() }, beginBackgroundTask: @escaping (String) -> Void = OSBackgroundTaskManager.beginBackgroundTask, endBackgroundTask: @escaping (String) -> Void = OSBackgroundTaskManager.endBackgroundTask, @@ -163,7 +157,6 @@ final class OSRemoteLoggingController: NSObject { } ) { self.notificationCenter = notificationCenter - self.logNotificationCenter = logNotificationCenter self.usesScenes = usesScenes self.beginBackgroundTask = beginBackgroundTask self.endBackgroundTask = endBackgroundTask @@ -215,7 +208,13 @@ final class OSRemoteLoggingController: NSObject { configure(with: OSRemoteLoggingConfiguration(remoteParams: remoteParams)) } - private func onInternalLog(level: ONE_S_LOG_LEVEL, message: String) { + func captureLog( + with level: ONE_S_LOG_LEVEL, + message: String, + exceptionType: String?, + exceptionMessage: String?, + exceptionStacktrace: String? + ) { stateQueue.async { [weak self] in guard let self, self.configuration.allows(level), @@ -224,7 +223,10 @@ final class OSRemoteLoggingController: NSObject { } remoteLogger.log( level: OSRemoteLoggingConfiguration.levelName(level), - message: message + message: message, + exceptionType: exceptionType, + exceptionMessage: exceptionMessage, + exceptionStacktrace: exceptionStacktrace ) } } @@ -297,16 +299,13 @@ final class OSRemoteLoggingController: NSObject { self.configuration.matches(newConfiguration) else { return } - self.registerLogSink() + OneSignalLog.__setInternalLogSink(self) self.registerLifecycleObservers() } } private func stopRemoteLogging() { - if let logObserverToken { - logNotificationCenter.removeObserver(logObserverToken) - self.logObserverToken = nil - } + OneSignalLog.__removeInternalLogSink(self) notificationTokens.forEach(notificationCenter.removeObserver) notificationTokens.removeAll() let activeRemoteLogger = remoteLogger @@ -335,22 +334,7 @@ final class OSRemoteLoggingController: NSObject { ) } - private func registerLogSink() { - logObserverToken = logNotificationCenter.addObserver( - forName: Self.internalLogNotification, - object: nil, - queue: nil - ) { [weak self] notification in - guard let rawLevel = notification.userInfo?[Self.internalLogLevelKey] as? NSNumber, - let level = ONE_S_LOG_LEVEL(rawValue: rawLevel.uintValue), - let message = notification.userInfo?[Self.internalLogMessageKey] as? String else { - return - } - self?.onInternalLog(level: level, message: message) - } - } - - private func logStartupDiagnostic(remoteLogger: OSRemoteLoggerProtocol) { + private func logStartupDiagnostic(remoteLogger: OSStructuredRemoteLoggerProtocol) { OneSignalLog.onesignalLog( .LL_WARN, message: "OneSignal logging initialized: sdk=\(ONESIGNAL_VERSION), " @@ -362,12 +346,7 @@ final class OSRemoteLoggingController: NSObject { private func makeProviders(configuration: OSRemoteLoggingConfiguration) -> OSRemoteLoggerProviders { OSRemoteLoggerProviders( installId: { Self.installId }, - onesignalId: { - OneSignalUserDefaults.initShared().getSavedString( - forKey: OS_SNAPSHOT_ONESIGNAL_ID, - defaultValue: nil - ) - }, + onesignalId: { OneSignalUserManagerImpl.sharedInstance.internalOnesignalId }, pushSubscriptionId: { OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId }, appState: { [weak self] in self?.currentAppState ?? "unknown" }, featureFlags: { [] }, diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift index f7a5b936e..dac5c59aa 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift +++ b/iOS_SDK/OneSignalSDK/UnitTests/OSRemoteLoggingControllerTests.swift @@ -27,7 +27,7 @@ import Foundation import OneSignalCore -import OneSignalOSCore +@_spi(OneSignalInternal) import OneSignalOSCore import UIKit import XCTest @@ -103,6 +103,26 @@ final class OSRemoteLoggingControllerTests: XCTestCase { XCTAssertEqual(telemetry.levels, ["WARN"]) } + func testInternalSinkForwardsStructuredExceptionFields() { + let telemetry = RemoteTelemetrySpy() + telemetry.emitExpectation = expectation(description: "routes structured exception") + let controller = makeController(remoteLoggerFactory: { _ in telemetry }) + controller.configure(remoteParams: Self.remoteParams(level: "ERROR")) + + controller.captureLog( + with: .LL_ERROR, + message: "failed", + exceptionType: "ExampleError", + exceptionMessage: "details", + exceptionStacktrace: "frame" + ) + + wait(for: [telemetry.emitExpectation!], timeout: 2) + XCTAssertEqual(telemetry.exceptionTypes, ["ExampleError"]) + XCTAssertEqual(telemetry.exceptionMessages, ["details"]) + XCTAssertEqual(telemetry.exceptionStacktraces, ["frame"]) + } + func testDisablingConfigurationStopsRemoteLogging() { let telemetry = RemoteTelemetrySpy() let controller = makeController(remoteLoggerFactory: { _ in telemetry }) @@ -228,7 +248,7 @@ private final class ReentrantLogListener: NSObject, OSLogListener { } } -private final class RemoteTelemetrySpy: OSRemoteLoggerProtocol { +private final class RemoteTelemetrySpy: OSStructuredRemoteLoggerProtocol { private let lock = NSLock() var emitExpectation: XCTestExpectation? var flushExpectation: XCTestExpectation? @@ -237,12 +257,34 @@ private final class RemoteTelemetrySpy: OSRemoteLoggerProtocol { let crashStoragePath = "/test" private(set) var levels: [String] = [] private(set) var messages: [String] = [] + private(set) var exceptionTypes: [String?] = [] + private(set) var exceptionMessages: [String?] = [] + private(set) var exceptionStacktraces: [String?] = [] private(set) var shutdownCount = 0 func log(level: String, message: String) { + log( + level: level, + message: message, + exceptionType: nil, + exceptionMessage: nil, + exceptionStacktrace: nil + ) + } + + func log( + level: String, + message: String, + exceptionType: String?, + exceptionMessage: String?, + exceptionStacktrace: String? + ) { lock.lock() levels.append(level) messages.append(message) + exceptionTypes.append(exceptionType) + exceptionMessages.append(exceptionMessage) + exceptionStacktraces.append(exceptionStacktrace) lock.unlock() emitExpectation?.fulfill() } From d1701dc9f7342c58160548598c47df7575c384c8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 11:12:01 -0700 Subject: [PATCH 14/15] fix: [SDK-4977] capture initialization logs from cache Start cached telemetry at the setter entry points so setAppId and setLaunchOptions logs are included without routing a new app ID through stale configuration. Co-authored-by: Cursor --- iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h | 2 +- iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift | 6 ++++-- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h index 749f9898b..f197ff4c5 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.h @@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OSRemoteLoggingController : NSObject + (void)configure; -+ (void)configureFromCache; ++ (void)configureFromCacheForAppId:(nullable NSString *)appId; + (void)reset; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift index e5ec6032f..6cdea28b9 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift +++ b/iOS_SDK/OneSignalSDK/Source/OSRemoteLoggingController.swift @@ -170,13 +170,15 @@ final class OSRemoteLoggingController: NSObject, OSInternalLogSink { shared.configure(with: configuration) } - @objc class func configureFromCache() { - guard let appId = OneSignalIdentifiers.currentAppId, + @objc(configureFromCacheForAppId:) + class func configureFromCache(appId: String?) { + guard let appId, let cached = OneSignalUserDefaults.initStandard().getSavedDictionary( forKey: cachedConfigurationKey, defaultValue: nil ), cached[cachedAppIdKey] as? String == appId else { + shared.shutdown() return } let logLevel = cached[cachedLogLevelKey] as? String diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index b8ab6e2ea..9399a514b 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -247,6 +247,7 @@ + (void)initialize:(nonnull NSString*)newAppId withLaunchOptions:(nullable NSDic // TODO: For release, note this change in migration guide: // No longer reading appID from plist @"OneSignal_APPID" and @"GameThrive_APPID" + (void)setAppId:(nullable NSString*)newAppId { + [OSRemoteLoggingController configureFromCacheForAppId:newAppId ?: OneSignalIdentifiers.storedAppId]; [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"setAppId called with appId: %@!", newAppId]]; if (!newAppId || newAppId.length == 0) { @@ -288,6 +289,7 @@ + (BOOL)isValidAppId:(NSString*)appId { Note: While this is called via `initialize`, it is also called directly from wrapper SDKs. */ + (void)setLaunchOptions:(nullable NSDictionary*)newLaunchOptions { + [OSRemoteLoggingController configureFromCacheForAppId:OneSignalIdentifiers.currentAppId ?: OneSignalIdentifiers.storedAppId]; [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"setLaunchOptions() called with launchOptions: %@!", launchOptions.description]]; // Don't continue if the newLaunchOptions are nil @@ -573,8 +575,6 @@ + (void)setupProtectedDataObserverOnce { Called after setAppId and setLaunchOptions, depending on which one is called last (order does not matter) */ + (void)init { - [OSRemoteLoggingController configureFromCache]; - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"launchOptions is set and appId of %@ is set, initializing OneSignal...", OneSignalIdentifiers.currentAppId]]; [self setupProtectedDataObserverOnce]; From ee322101b6dcfc3a71bef7914911b1e454613302 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 17:26:57 -0700 Subject: [PATCH 15/15] refactor: [SDK-4977] centralize cached logging startup Initialize cached telemetry once at the shared SDK entry point before setAppId and setLaunchOptions emit logs. Co-authored-by: Cursor --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 9399a514b..0b3313bf8 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -235,6 +235,7 @@ + (void)logout { Note: wrappers may call this method with a null appId. */ + (void)initialize:(nonnull NSString*)newAppId withLaunchOptions:(nullable NSDictionary*)launchOptions { + [OSRemoteLoggingController configureFromCacheForAppId:newAppId ?: OneSignalIdentifiers.storedAppId]; [self setAppId:newAppId]; [self setLaunchOptions:launchOptions]; [self init]; @@ -247,7 +248,6 @@ + (void)initialize:(nonnull NSString*)newAppId withLaunchOptions:(nullable NSDic // TODO: For release, note this change in migration guide: // No longer reading appID from plist @"OneSignal_APPID" and @"GameThrive_APPID" + (void)setAppId:(nullable NSString*)newAppId { - [OSRemoteLoggingController configureFromCacheForAppId:newAppId ?: OneSignalIdentifiers.storedAppId]; [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"setAppId called with appId: %@!", newAppId]]; if (!newAppId || newAppId.length == 0) { @@ -286,10 +286,8 @@ + (BOOL)isValidAppId:(NSString*)appId { 1/2 steps in OneSignal init, relying on setAppId (usage order does not matter) Sets the iOS sepcific app settings Method must be called to successfully init OneSignal - Note: While this is called via `initialize`, it is also called directly from wrapper SDKs. */ + (void)setLaunchOptions:(nullable NSDictionary*)newLaunchOptions { - [OSRemoteLoggingController configureFromCacheForAppId:OneSignalIdentifiers.currentAppId ?: OneSignalIdentifiers.storedAppId]; [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"setLaunchOptions() called with launchOptions: %@!", launchOptions.description]]; // Don't continue if the newLaunchOptions are nil