Skip to content
8 changes: 8 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
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 */; };
5156E0E0A1B2C3D4E5F60004 /* OSObservabilityEventRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5156E0E0A1B2C3D4E5F60003 /* OSObservabilityEventRecorderTests.swift */; };
A5048F01A1B2C3D4E5F60002 /* OSFeatureFlagsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5048F01A1B2C3D4E5F60001 /* OSFeatureFlagsStore.swift */; };
A5048F01A1B2C3D4E5F60004 /* OSFeatureFlagsBackendService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5048F01A1B2C3D4E5F60003 /* OSFeatureFlagsBackendService.swift */; };
A5048F01A1B2C3D4E5F60006 /* OSFeatureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5048F01A1B2C3D4E5F60005 /* OSFeatureManager.swift */; };
Expand All @@ -92,6 +93,7 @@
C73D4E5F60718293A4B5C6D7 /* OSRemoteLoggingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C91A2B0D84F1E9A3C5B6D8E /* OSRemoteLoggingConfiguration.swift */; };
ACE2175908241BB46C9F1829 /* OSRemoteLoggingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */; };
7732574D325D34CC7C498199 /* OSRemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */; };
5156E0E0A1B2C3D4E5F60002 /* OSObservabilityEventRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5156E0E0A1B2C3D4E5F60001 /* OSObservabilityEventRecorder.swift */; };
698F58A488FCE503DFD5247F /* OSLogCrashHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */; };
3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; };
3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */; };
Expand Down Expand Up @@ -1809,6 +1811,7 @@
3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerPlatformProvider.swift; sourceTree = "<group>"; };
3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinByteArray+Data.swift"; sourceTree = "<group>"; };
3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = "<group>"; };
5156E0E0A1B2C3D4E5F60003 /* OSObservabilityEventRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSObservabilityEventRecorderTests.swift; sourceTree = "<group>"; };
A5048F01A1B2C3D4E5F60001 /* OSFeatureFlagsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFeatureFlagsStore.swift; sourceTree = "<group>"; };
A5048F01A1B2C3D4E5F60003 /* OSFeatureFlagsBackendService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFeatureFlagsBackendService.swift; sourceTree = "<group>"; };
A5048F01A1B2C3D4E5F60005 /* OSFeatureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFeatureManager.swift; sourceTree = "<group>"; };
Expand All @@ -1823,6 +1826,7 @@
C0462F96E1AADF655F3B3765 /* OSRemoteLoggingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSRemoteLoggingController.h; sourceTree = "<group>"; };
658E6E9E6BC6BBF702BCBD33 /* OSRemoteLoggingControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLoggingControllerTests.swift; sourceTree = "<group>"; };
6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRemoteLogger.swift; sourceTree = "<group>"; };
5156E0E0A1B2C3D4E5F60001 /* OSObservabilityEventRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSObservabilityEventRecorder.swift; sourceTree = "<group>"; };
ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogCrashHandler.swift; sourceTree = "<group>"; };
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 = "<group>"; };
Expand Down Expand Up @@ -2323,6 +2327,7 @@
ACD3284210A7DF2597594778 /* OSLogCrashHandler.swift */,
3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */,
6972EE491A57C79EFE56D4C8 /* OSRemoteLogger.swift */,
5156E0E0A1B2C3D4E5F60001 /* OSObservabilityEventRecorder.swift */,
);
path = Logging;
sourceTree = "<group>";
Expand Down Expand Up @@ -2607,6 +2612,7 @@
5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */,
3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */,
3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */,
5156E0E0A1B2C3D4E5F60003 /* OSObservabilityEventRecorderTests.swift */,
A5048F01A1B2C3D4E5F60009 /* OSFeatureFlagsTests.swift */,
A5048F01A1B2C3D4E5F6100A /* OSFeatureFlagsRefreshServiceTests.swift */,
3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */,
Expand Down Expand Up @@ -4493,6 +4499,7 @@
3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */,
698F58A488FCE503DFD5247F /* OSLogCrashHandler.swift in Sources */,
7732574D325D34CC7C498199 /* OSRemoteLogger.swift in Sources */,
5156E0E0A1B2C3D4E5F60002 /* OSObservabilityEventRecorder.swift in Sources */,
3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */,
3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */,
5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */,
Expand Down Expand Up @@ -4655,6 +4662,7 @@
3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */,
3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */,
3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */,
5156E0E0A1B2C3D4E5F60004 /* OSObservabilityEventRecorderTests.swift in Sources */,
A5048F01A1B2C3D4E5F6000A /* OSFeatureFlagsTests.swift in Sources */,
A5048F01A1B2C3D4E5F6100B /* OSFeatureFlagsRefreshServiceTests.swift in Sources */,
C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ final class FileLogStore: ILogFileStore {
) {
self.rootURL = URL(fileURLWithPath: rootPath, isDirectory: true)
self.fileManager = fileManager
let crashLogger = OSCrashLogger()
self.crashWarn = crashWarn ?? { crashLogger.warn(message: $0) }
let consoleLogger = OSConsoleLogger()
self.crashWarn = crashWarn ?? { consoleLogger.warn(message: $0) }
self.attributeLookup = attributeLookup
try? createRootDirectory()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ struct OSResolvedStackFrame: Equatable {
let symbolName: String?
}

/// Prints KMP crash-reporter diagnostics with `NSLog`. Honors the console log level
/// without going through `OneSignalLog` (listeners, alert UI, remote sink).
final class OSCrashLogger: ILogger {
/// Prints KMP diagnostics with `NSLog`, honoring the console log level without going through
/// `OneSignalLog` (listeners, alert UI, remote telemetry). Each caller documents why it cannot
/// afford that path.
final class OSConsoleLogger: ILogger {
/// Marks the lines that skipped `OneSignalLog`. Every message arrives already naming its
/// own component, so nothing finer is needed here.
private static let prefix = "[OSConsoleLogger]"

private let consoleLogLevel: () -> ONE_S_LOG_LEVEL
private let write: (String) -> Void

Expand Down Expand Up @@ -75,7 +80,7 @@ final class OSCrashLogger: ILogger {
guard level.rawValue <= consoleLogLevel().rawValue else {
return
}
write("[OneSignal crash] \(label): \(message)")
write("\(Self.prefix) \(label): \(message)")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
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
@_implementationOnly import OneSignalKMP

/// Mirrors the KMP `ObservabilityEvent` catalog so a call site can name an event without seeing KMP
/// types, which this module imports implementation-only.
@_spi(OneSignalInternal)
public enum OSObservabilityEvent: CaseIterable {
/// Temporary: comes out once its usage question is answered.
case deviceGesture

/// The `event.name` of the KMP entry, so a test can check the mirror against the catalog.
var eventName: String {
kmpEvent.eventName
}
}

/// The producer-facing contract, so a call site can take a spy in tests.
@_spi(OneSignalInternal)
public protocol OSObservabilityEventRecorderProtocol: AnyObject {
/// Never throws or blocks. Drops when the event's flag is off or the per-process cap is
/// reached; queues, bounded, until remote telemetry is attached.
func record(event: OSObservabilityEvent, attributes: [String: String])
}

/// The attach side, driven by `OSRemoteLogger`. Internal because the telemetry is a KMP type.
protocol OSObservabilityEventRecorderAttaching: AnyObject {
func attach(_ telemetry: ILogTelemetry)

/// Ignored unless `telemetry` is the attached one, so a logger that lost the install race
/// cannot detach the winner.
func detach(_ telemetry: ILogTelemetry)
}

/// Wraps the shared KMP recorder, which owns the flag check, the pre-attach queue and the
/// per-process cap. Events ride the remote logger's telemetry, so they share the crash gate
/// rather than the severity filter.
@_spi(OneSignalInternal)
public final class OSObservabilityEventRecorder: OSObservabilityEventRecorderProtocol, OSObservabilityEventRecorderAttaching {
public static let shared = OSObservabilityEventRecorder(isFeatureEnabled: featureIsEnabledIfInitialized)

private let recorder: IObservabilityEventRecorder

/// - Parameter isFeatureEnabled: the feature-manager read for a catalog flag key.
init(isFeatureEnabled: @escaping (String) -> Bool) {
// Console-only logger: `attach` runs under the remote logger's lifecycle lock, and
// `OneSignalLog` reaches app listeners synchronously, so a listener that re-enters the
// SDK would deadlock on that lock.
recorder = LoggerFactory.shared.createObservabilityEventRecorder(
flags: OSFeatureFlagReader(isFeatureEnabled: isFeatureEnabled),
logger: OSConsoleLogger()
)
}

/// Never constructs the feature manager: first touch latches every APP_STARTUP flag from
/// whatever storage returns, and a record can arrive from any thread at any time. Off until
/// the manager exists, which is also the documented state before the first flags fetch.
static func featureIsEnabledIfInitialized(_ key: String) -> Bool {
OSFeatureManager.enabledFeatureKeysIfInitialized().contains(key)
Comment thread
nan-li marked this conversation as resolved.
}

public func record(event: OSObservabilityEvent, attributes: [String: String]) {
recorder.record(event: event.kmpEvent, attributes: attributes)
}

/// Drops whatever an earlier app id queued; called beside the other app-id-change resets.
public func reset() {
recorder.reset()
}

func attach(_ telemetry: ILogTelemetry) {
recorder.attach(telemetry: telemetry)
}

func detach(_ telemetry: ILogTelemetry) {
recorder.detach(telemetry: telemetry)
}
}

/// Answers the KMP recorder's flag lookups; each event's own gate decides which flag to ask about.
private final class OSFeatureFlagReader: IFeatureFlagReader {
private let isFeatureEnabled: (String) -> Bool

init(isFeatureEnabled: @escaping (String) -> Bool) {
self.isFeatureEnabled = isFeatureEnabled
}

func isEnabled(flag: FeatureFlag) -> Bool {
isFeatureEnabled(flag.key)
}
}

private extension OSObservabilityEvent {
var kmpEvent: ObservabilityEvent {
switch self {
case .deviceGesture:
return .deviceGesture
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {
private let crashUploader: LogCrashUploader
private let logger: IOSLogger
private let lifecycle: OSRemoteLoggerLifecycle
private let eventRecorder: OSObservabilityEventRecorderAttaching
/// Guarded by `lifecycleOperationLock`. The recorder is shared across instances, and a logger
/// that lost the install race is shut down without ever starting; it must not detach the winner.
private var didAttachEventRecorder = false
private let lifecycleOperationLock = NSLock()
private let uploaderOwner = UUID()

Expand Down Expand Up @@ -262,15 +266,17 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {
)
}

private init(
/// Internal so tests can stand in an event recorder; production goes through the convenience initializers.
init(
installIdProvider: @escaping () -> String,
onesignalIdProvider: @escaping () -> String?,
pushSubscriptionIdProvider: @escaping () -> String?,
appStateProvider: @escaping () -> String,
featureFlagsProvider: @escaping () -> [String],
remoteLogLevelProvider: @escaping () -> String?,
exporterLoggingEnabledProvider: @escaping () -> Bool,
requestSenderOverride: RequestSender?
requestSenderOverride: RequestSender?,
eventRecorder: OSObservabilityEventRecorderAttaching = OSObservabilityEventRecorder.shared
) {
let provider = OSLoggerPlatformProvider(
installIdProvider: installIdProvider,
Expand All @@ -282,15 +288,15 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {
exporterLoggingEnabledProvider: exporterLoggingEnabledProvider
)
let logger = IOSLogger()
let crashLogger = OSCrashLogger()
let consoleLogger = OSConsoleLogger()
let lifecycle = OSRemoteLoggerLifecycle()
let fileStore = FileLogStore(rootPath: provider.crashStoragePath)
// Console-only logger on purpose. Exporter diagnostics describe the POST that
// ships log records, so routing them through OneSignalLog would feed each POST
// back into the export queue as a new record and never settle.
let httpSender = Self.makeHttpSender(
requestSender: requestSenderOverride,
logger: crashLogger,
logger: consoleLogger,
isDiagnosticsEnabled: exporterLoggingEnabledProvider,
lifecycle: lifecycle
)
Expand All @@ -304,7 +310,7 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {
)
let crashReporter = LoggerFactory.shared.createCrashReporter(
crashTelemetry: crashTelemetry,
logger: crashLogger
logger: consoleLogger
)
let crashHandler = OSLogCrashHandler(reporter: crashReporter)
let crashUploader = LoggerFactory.shared.createCrashUploader(
Expand All @@ -320,6 +326,7 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {
self.crashUploader = crashUploader
self.logger = logger
self.lifecycle = lifecycle
self.eventRecorder = eventRecorder
}

private static func makeHttpSender(
Expand Down Expand Up @@ -354,6 +361,9 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {
}

crashHandler.initialize()
// Events ride this telemetry, so the recorder follows it here and in shutdown().
eventRecorder.attach(telemetry)
didAttachEventRecorder = true
lifecycleOperationLock.unlock()
let owner = uploaderOwner
let crashUploader = self.crashUploader
Expand Down Expand Up @@ -439,6 +449,10 @@ public final class OSRemoteLogger: OSRemoteLoggerProtocol {

OSCrashUploaderCoordinator.shared.cancel(owner: uploaderOwner)
crashHandler.unregister()
if didAttachEventRecorder {
eventRecorder.detach(telemetry)
Comment thread
nan-li marked this conversation as resolved.
didAttachEventRecorder = false
Comment thread
nan-li marked this conversation as resolved.
}
lifecycleOperationLock.unlock()

// `telemetry.shutdown()` blocks for up to five seconds draining buffered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ extension OSLogCrashHandlerTests {
)
let reporter = LoggerFactory.shared.createCrashReporter(
crashTelemetry: telemetry,
logger: OSCrashLogger()
logger: OSConsoleLogger()
)
return OSLogCrashHandler(reporter: reporter)
}
Expand Down
Loading
Loading