From 047d54da57fa4f626ec7da760c175a542ed2e3c6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 5 Aug 2026 15:13:19 -0700 Subject: [PATCH 1/6] feat: [SDK-4976] add Swift adapters for KMP logger Provide native logging, platform metadata, protobuf transport, and durable crash storage so the shared logger can be wired into the iOS lifecycle. Co-authored-by: Cursor --- OneSignal-KMP-SDK | 2 +- .../OneSignal.xcodeproj/project.pbxproj | 84 ++++ .../Source/OSLoggerAdapters.swift | 451 ++++++++++++++++++ .../UnitTests/OSLoggerAdaptersTests.swift | 171 +++++++ 4 files changed, 707 insertions(+), 1 deletion(-) create mode 100644 iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift create mode 100644 iOS_SDK/OneSignalSDK/UnitTests/OSLoggerAdaptersTests.swift diff --git a/OneSignal-KMP-SDK b/OneSignal-KMP-SDK index 5c9f5a582..b7fe6fac0 160000 --- a/OneSignal-KMP-SDK +++ b/OneSignal-KMP-SDK @@ -1 +1 @@ -Subproject commit 5c9f5a582543ef1b01db5bb33b4607ded294fad3 +Subproject commit b7fe6fac07e9c84ee708fead4ac0fc98ac5a3b63 diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index ff45d89b3..56c243ce2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -69,6 +69,10 @@ 3C14E39F2AFAE39B006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; }; + 3C14E3A52AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */; }; + 3C14E3A62AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */; }; + 3C14E3A72AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */; }; + 3C14E3A82AFAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B042947D4E900C81DA3 /* OSLoggerAdaptersTests.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 */; }; @@ -1777,6 +1781,8 @@ DEBAAEB62A4381AE00BF2C1C /* OSInAppMessageMigrationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageMigrationController.h; sourceTree = ""; }; DEBAAEB72A4381AE00BF2C1C /* OSInAppMessageMigrationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageMigrationController.m; sourceTree = ""; }; DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalSwiftInterface.swift; sourceTree = ""; }; + DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdapters.swift; sourceTree = ""; }; + DEC08B042947D4E900C81DA3 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.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 = ""; }; @@ -2639,6 +2645,7 @@ 4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */, 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, + DEC08B042947D4E900C81DA3 /* OSLoggerAdaptersTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -2671,6 +2678,7 @@ 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */, 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */, DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */, + DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */, 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */, ); path = Source; @@ -4353,6 +4361,7 @@ 9124120E1E73342200E41FD7 /* OneSignal.m in Sources */, 9124121E1E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */, 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */, + 3C14E3A52AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */, 912412471E73369600E41FD7 /* OneSignalHelper.m in Sources */, CA8E19062193C76D009DA223 /* OSInAppMessagingHelpers.m in Sources */, 7AAA60682485D0420004FADE /* OSMigrationController.m in Sources */, @@ -4499,6 +4508,7 @@ 9124123F1E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m in Sources */, 7AECE59F23675F6300537907 /* OSFocusTimeProcessorFactory.m in Sources */, DEC08B012947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, + 3C14E3A62AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */, CA1A6E7120DC2E73001C41B9 /* OneSignalDialogRequest.m in Sources */, 912412331E73342200E41FD7 /* OneSignalTracker.m in Sources */, 7A674F1C2360D82E001F9ACD /* OSBaseFocusTimeProcessor.m in Sources */, @@ -4592,6 +4602,7 @@ CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */, 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, + 3C14E3A82AFAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, @@ -4622,6 +4633,7 @@ 03CCCC852835F291004BF794 /* UIApplicationDelegateSwizzlingTests.m in Sources */, 4529DEEA1FA8360C00CEAB1D /* UIApplicationOverrider.m in Sources */, DEC08B022947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, + 3C14E3A72AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */, 7A93269E25AF4F0300BBEC27 /* OSPendingCallbacks.m in Sources */, 7AECE59823674AB700537907 /* OSUnattributedFocusTimeProcessor.m in Sources */, 7A5A818224897693002E07C8 /* MigrationTests.m in Sources */, @@ -7119,6 +7131,14 @@ CA2951B72167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_GCD_PERFORMANCE = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7145,6 +7165,14 @@ CA2951B82167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7190,6 +7218,14 @@ CA2951B92167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; @@ -7299,6 +7335,14 @@ CA2951C32167FB950064227A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_GCD_PERFORMANCE = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7325,6 +7369,14 @@ CA2951C42167FB950064227A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_CODE_COVERAGE = YES; @@ -7372,6 +7424,14 @@ CA2951C52167FB950064227A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; @@ -7481,6 +7541,14 @@ DE3D8F3928C15839008C2BBF /* Test */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_GCD_PERFORMANCE = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7507,6 +7575,14 @@ DE3D8F3A28C15839008C2BBF /* Test */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_CODE_COVERAGE = YES; @@ -7554,6 +7630,14 @@ DE3D8F3B28C15839008C2BBF /* Test */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; diff --git a/iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift b/iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift new file mode 100644 index 000000000..4276b423c --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift @@ -0,0 +1,451 @@ +/* + 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 Darwin +import Foundation +import OneSignalCore +import OneSignalKMP +import OneSignalUser +import UIKit + +final class OSLoggerAdapter: NSObject, ILogger { + func error(message: String) { + OneSignalLog.onesignalLog(.LL_ERROR, message: message) + } + + func warn(message: String) { + OneSignalLog.onesignalLog(.LL_WARN, message: message) + } + + func info(message: String) { + OneSignalLog.onesignalLog(.LL_INFO, message: message) + } + + func debug(message: String) { + OneSignalLog.onesignalLog(.LL_DEBUG, message: message) + } +} + +final class OSLogHttpSender: NSObject, ILogHttpSender { + private static let defaultSession: URLSession = { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 10 + configuration.timeoutIntervalForResource = 10 + return URLSession(configuration: configuration) + }() + + typealias RequestSender = ( + URLRequest, + @escaping (Data?, URLResponse?, Error?) -> Void + ) -> Void + + init(session: URLSession = OSLogHttpSender.defaultSession) { + self.requestSender = { request, completion in + session.dataTask(with: request, completionHandler: completion).resume() + } + } + + init(requestSender: @escaping RequestSender) { + self.requestSender = requestSender + } + + private let requestSender: RequestSender + + func send( + request: LogHttpRequest, + completionHandler: @escaping (LogHttpResponse?, Error?) -> Void + ) { + guard let url = URL(string: request.url) else { + completionHandler( + LogHttpResponse(success: false, statusCode: -1, message: "Invalid log request URL"), + nil + ) + return + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = request.body.data + request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } + urlRequest.setValue(request.contentType, forHTTPHeaderField: "Content-Type") + + requestSender(urlRequest) { _, response, error in + if let error = error { + completionHandler( + LogHttpResponse(success: false, statusCode: -1, message: error.localizedDescription), + nil + ) + return + } + + guard let response = response as? HTTPURLResponse else { + completionHandler( + LogHttpResponse(success: false, statusCode: -1, message: "Missing HTTP response"), + nil + ) + return + } + + completionHandler( + LogHttpResponse( + success: (200...299).contains(response.statusCode), + statusCode: Int32(response.statusCode), + message: nil + ), + nil + ) + } + } +} + +final class OSLogFileStore: NSObject, ILogFileStore { + private static let ownedSuffix = ".otlp" + + private let rootURL: URL + private let fileManager: FileManager + private let ioQueue = DispatchQueue(label: "com.onesignal.logger.file-store", qos: .utility) + + init(rootPath: String, fileManager: FileManager = .default) { + self.rootURL = URL(fileURLWithPath: rootPath, isDirectory: true) + self.fileManager = fileManager + super.init() + try? createRootDirectory() + } + + func save(bytes: KotlinByteArray) -> Bool { + do { + try createRootDirectory() + let id = "\(Int64(Date().timeIntervalSince1970 * 1_000))-\(UUID().uuidString)\(Self.ownedSuffix)" + try writeDurably(bytes.data, to: rootURL.appendingPathComponent(id)) + return true + } catch { + return false + } + } + + func listReadable( + minAgeMillis: Int64, + completionHandler: @escaping ([StoredLogFile]?, Error?) -> Void + ) { + ioQueue.async { + do { + let entries = try self.readableEntries(minAgeMillis: minAgeMillis) + completionHandler(entries, nil) + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSLogFileStore listReadable failed: \(error.localizedDescription)" + ) + completionHandler([], nil) + } + } + } + + func delete(id: String, completionHandler: @escaping (Error?) -> Void) { + ioQueue.async { + guard self.isSafeEntryId(id) else { + completionHandler(nil) + return + } + + do { + let url = self.rootURL.appendingPathComponent(id) + if self.fileManager.fileExists(atPath: url.path) { + try self.fileManager.removeItem(at: url) + } + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSLogFileStore delete failed: \(error.localizedDescription)" + ) + } + completionHandler(nil) + } + } + + func deleteUnrecognizedEntries( + minAgeMillis: Int64, + completionHandler: @escaping (KotlinInt?, Error?) -> Void + ) { + ioQueue.async { + var deleted = 0 + do { + for url in try self.fileURLs() where !url.lastPathComponent.hasSuffix(Self.ownedSuffix) { + guard try self.isOldEnough(url, minAgeMillis: minAgeMillis) else { + continue + } + try self.fileManager.removeItem(at: url) + deleted += 1 + } + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSLogFileStore cleanup failed: \(error.localizedDescription)" + ) + } + completionHandler(KotlinInt(int: Int32(deleted)), nil) + } + } + + private func readableEntries(minAgeMillis: Int64) throws -> [StoredLogFile] { + try fileURLs() + .filter { $0.lastPathComponent.hasSuffix(Self.ownedSuffix) } + .filter { try isOldEnough($0, minAgeMillis: minAgeMillis) } + .compactMap { url in + guard let data = try? Data(contentsOf: url) else { + return nil + } + return StoredLogFile(id: url.lastPathComponent, bytes: data.kotlinByteArray) + } + } + + private func fileURLs() throws -> [URL] { + guard fileManager.fileExists(atPath: rootURL.path) else { + return [] + } + return try fileManager.contentsOfDirectory( + at: rootURL, + includingPropertiesForKeys: [.contentModificationDateKey, .isRegularFileKey], + options: [.skipsHiddenFiles] + ).filter { + (try? $0.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true + } + } + + private func isOldEnough(_ url: URL, minAgeMillis: Int64) throws -> Bool { + let values = try url.resourceValues(forKeys: [.contentModificationDateKey]) + guard let modifiedAt = values.contentModificationDate else { + return false + } + return Date().timeIntervalSince(modifiedAt) * 1_000 >= Double(max(0, minAgeMillis)) + } + + private func isSafeEntryId(_ id: String) -> Bool { + !id.isEmpty && URL(fileURLWithPath: id).lastPathComponent == id + } + + private func createRootDirectory() throws { + try fileManager.createDirectory(at: rootURL, withIntermediateDirectories: true) + } + + private func writeDurably(_ data: Data, to targetURL: URL) throws { + let temporaryURL = targetURL.appendingPathExtension("tmp") + let descriptor = open(temporaryURL.path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR) + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + var isClosed = false + defer { + if !isClosed { + close(descriptor) + } + } + + do { + try data.withUnsafeBytes { rawBuffer in + guard var pointer = rawBuffer.baseAddress else { + return + } + var remaining = rawBuffer.count + while remaining > 0 { + let count = Darwin.write(descriptor, pointer, remaining) + guard count > 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + pointer = pointer.advanced(by: count) + remaining -= count + } + } + guard fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + guard close(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + isClosed = true + try fileManager.moveItem(at: temporaryURL, to: targetURL) + try syncDirectory() + } catch { + try? fileManager.removeItem(at: temporaryURL) + throw error + } + } + + private func syncDirectory() throws { + let descriptor = open(rootURL.path, O_RDONLY) + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + defer { close(descriptor) } + guard fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + } +} + +final class OSLoggerPlatformProvider: NSObject, ILoggerPlatformProvider { + private static let installIdKey = "PREFS_OS_INSTALL_ID" + private static let processStartedAt = processStartDate() + + private lazy var installId: String = { + let defaults = OneSignalUserDefaults.initShared() + if let saved = defaults.getSavedString(forKey: Self.installIdKey, defaultValue: nil) { + return saved + } + let generated = UUID().uuidString + defaults.saveString(forKey: Self.installIdKey, withValue: generated) + return generated + }() + + func getInstallId(completionHandler: @escaping (String?, Error?) -> Void) { + completionHandler(installId, nil) + } + + let sdkBase = "ios" + let sdkBaseVersion = ONESIGNAL_VERSION + let appPackageId = Bundle.main.bundleIdentifier ?? "unknown" + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" + let deviceManufacturer = "Apple" + let deviceModel = OSDeviceUtils.getDeviceVariant() ?? "unknown" + let osName = UIDevice.current.systemName + let osVersion = UIDevice.current.systemVersion + let osBuildId = OSLoggerPlatformProvider.systemValue(named: "kern.osversion") + let sdkWrapper = OneSignalWrapper.sdkType + let sdkWrapperVersion = OneSignalWrapper.sdkVersion + let enabledFeatureFlags: [String] = [] + + var appId: String? { + OneSignalIdentifiers.currentAppId + } + + var onesignalId: String? { + OneSignalUserManagerImpl.sharedInstance.onesignalId + } + + var pushSubscriptionId: String? { + OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId + } + + var appState: String { + switch UIApplication.shared.applicationState { + case .active: + return "foreground" + case .background: + return "background" + default: + return "unknown" + } + } + + var processUptime: Int64 { + Int64(max(0, Date().timeIntervalSince(Self.processStartedAt) * 1_000)) + } + + var currentThreadName: String { + if let name = Thread.current.name, !name.isEmpty { + return name + } + if Thread.isMainThread { + return "main" + } + var name = [CChar](repeating: 0, count: 64) + return pthread_getname_np(pthread_self(), &name, name.count) == 0 + ? String(cString: name) + : "unknown" + } + + let crashStoragePath: String = { + let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + ?? FileManager.default.temporaryDirectory + return caches + .appendingPathComponent("onesignal", isDirectory: true) + .appendingPathComponent("logger", isDirectory: true) + .appendingPathComponent("crashes", isDirectory: true) + .path + }() + + let minFileAgeForReadMillis: Int64 = 5_000 + + var isRemoteLoggingEnabled: Bool { + guard let level = remoteLogLevel else { + return false + } + return level != "NONE" + } + + var remoteLogLevel: String? { + let config = OSRemoteParamController.shared().remoteParams["logging_config"] as? [String: Any] + return (config?["log_level"] as? String)?.uppercased() + } + + let isExporterLoggingEnabled = false + + var appIdForHeaders: String { + appId ?? "" + } + + let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + private static func systemValue(named name: String) -> String { + var size = 0 + guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { + return "unknown" + } + var value = [CChar](repeating: 0, count: size) + guard sysctlbyname(name, &value, &size, nil, 0) == 0 else { + return "unknown" + } + return String(cString: value) + } + + private static func processStartDate() -> Date { + var processInfo = kinfo_proc() + var size = MemoryLayout.stride + var name = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + guard sysctl(&name, u_int(name.count), &processInfo, &size, nil, 0) == 0 else { + return Date() + } + let start = processInfo.kp_proc.p_starttime + return Date( + timeIntervalSince1970: TimeInterval(start.tv_sec) + TimeInterval(start.tv_usec) / 1_000_000 + ) + } +} + +private extension KotlinByteArray { + var data: Data { + Data((0.. KotlinByteArray { + KotlinByteArray(size: Int32(bytes.count)) { index in + KotlinByte(value: Int8(bitPattern: bytes[Int(index.int32Value)])) + } + } +} + +private final class LoggerAdapterListener: NSObject, OSLogListener { + var levels: [ONE_S_LOG_LEVEL] = [] + + func onLogEvent(_ event: OneSignalLogEvent) { + levels.append(event.level) + } +} + +private extension KotlinByteArray { + var bytes: [UInt8] { + (0.. Date: Thu, 6 Aug 2026 10:41:09 -0700 Subject: [PATCH 2/6] chore(sdk): bump KMP submodule --- OneSignal-KMP-SDK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneSignal-KMP-SDK b/OneSignal-KMP-SDK index b7fe6fac0..07bbfadff 160000 --- a/OneSignal-KMP-SDK +++ b/OneSignal-KMP-SDK @@ -1 +1 @@ -Subproject commit b7fe6fac07e9c84ee708fead4ac0fc98ac5a3b63 +Subproject commit 07bbfadffd887304da0a8b6cdbdbcdd499905611 From 833293dbd120a8a98fbc2ec05987f44c1b83589a Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 6 Aug 2026 10:53:28 -0700 Subject: [PATCH 3/6] refactor(logger): move KMP adapters to OneSignalOSCore --- .../OneSignal.xcodeproj/project.pbxproj | 92 +++- .../Source/Logging/KotlinByteArray+Data.swift | 43 ++ .../Source/Logging/OSLogFileStore.swift | 224 +++++++++ .../Source/Logging/OSLogHttpSender.swift | 116 +++++ .../Source/Logging/OSLoggerAdapter.swift | 50 ++ .../Logging/OSLoggerPlatformProvider.swift | 192 ++++++++ .../OSLoggerAdaptersTests.swift | 12 +- .../Source/OSLoggerAdapters.swift | 451 ------------------ 8 files changed, 715 insertions(+), 465 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogFileStore.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift rename iOS_SDK/OneSignalSDK/{UnitTests => OneSignalOSCoreTests}/OSLoggerAdaptersTests.swift (92%) delete mode 100644 iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 56c243ce2..b52c884e4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -69,10 +69,12 @@ 3C14E39F2AFAE39B006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; }; - 3C14E3A52AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */; }; - 3C14E3A62AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */; }; - 3C14E3A72AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */; }; - 3C14E3A82AFAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08B042947D4E900C81DA3 /* OSLoggerAdaptersTests.swift */; }; + 3C14E3B02FAE54C006ED053 /* OSLoggerAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */; }; + 3C14E3B12FAE54C006ED053 /* OSLogHttpSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AA2FAE54C006ED053 /* OSLogHttpSender.swift */; }; + 3C14E3B22FAE54C006ED053 /* OSLogFileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AB2FAE54C006ED053 /* OSLogFileStore.swift */; }; + 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 */; }; 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 */; }; @@ -1781,8 +1783,12 @@ DEBAAEB62A4381AE00BF2C1C /* OSInAppMessageMigrationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageMigrationController.h; sourceTree = ""; }; DEBAAEB72A4381AE00BF2C1C /* OSInAppMessageMigrationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageMigrationController.m; sourceTree = ""; }; DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalSwiftInterface.swift; sourceTree = ""; }; - DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdapters.swift; sourceTree = ""; }; - DEC08B042947D4E900C81DA3 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = ""; }; + 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdapter.swift; sourceTree = ""; }; + 3C14E3AA2FAE54C006ED053 /* OSLogHttpSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHttpSender.swift; sourceTree = ""; }; + 3C14E3AB2FAE54C006ED053 /* OSLogFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogFileStore.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -2248,6 +2254,7 @@ isa = PBXGroup; children = ( 5BC1DE652C90BC9F00CA8807 /* Consistency */, + 3C14E3AF2FAE54C006ED053 /* Logging */, 3C115163289A259500565C41 /* OneSignalOSCore.h */, 3C115188289ADEA300565C41 /* OSModelStore.swift */, 3C5C6FFB2FCB8DED00102E2C /* OneSignalIdentifiers.swift */, @@ -2270,6 +2277,18 @@ path = Source; sourceTree = ""; }; + 3C14E3AF2FAE54C006ED053 /* Logging */ = { + isa = PBXGroup; + children = ( + 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */, + 3C14E3AB2FAE54C006ED053 /* OSLogFileStore.swift */, + 3C14E3AA2FAE54C006ED053 /* OSLogHttpSender.swift */, + 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */, + 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */, + ); + path = Logging; + sourceTree = ""; + }; 3C70222A2ECF126B001768C6 /* OneSignalInAppMessagesMocks */ = { isa = PBXGroup; children = ( @@ -2537,6 +2556,7 @@ children = ( 5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */, 3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */, + 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */, 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */, 3C23A21E2FCE0AA1001D32E3 /* OSResilientStorageTests.swift */, 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */, @@ -2645,7 +2665,6 @@ 4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */, 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, - DEC08B042947D4E900C81DA3 /* OSLoggerAdaptersTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -2678,7 +2697,6 @@ 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */, 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */, DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */, - DEC08B032947D4E900C81DA3 /* OSLoggerAdapters.swift */, 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */, ); path = Source; @@ -4361,7 +4379,6 @@ 9124120E1E73342200E41FD7 /* OneSignal.m in Sources */, 9124121E1E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */, 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */, - 3C14E3A52AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */, 912412471E73369600E41FD7 /* OneSignalHelper.m in Sources */, CA8E19062193C76D009DA223 /* OSInAppMessagingHelpers.m in Sources */, 7AAA60682485D0420004FADE /* OSMigrationController.m in Sources */, @@ -4400,9 +4417,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */, DEFB3E652BB7346D00E65DAD /* OSLiveActivities.swift in Sources */, + 3C14E3B22FAE54C006ED053 /* OSLogFileStore.swift in Sources */, 5BC1DE602C90B83900CA8807 /* OSConsistencyKeyEnum.swift in Sources */, + 3C14E3B12FAE54C006ED053 /* OSLogHttpSender.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, + 3C14E3B02FAE54C006ED053 /* OSLoggerAdapter.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */, @@ -4423,6 +4444,7 @@ DEFB3E672BB735B500E65DAD /* OSStubLiveActivities.swift in Sources */, 5BC1DE622C90B85A00CA8807 /* OSIamFetchOffsetKey.swift in Sources */, 3C11518D289AF5E800565C41 /* OSModelChangedHandler.swift in Sources */, + 3C14E3B32FAE54C006ED053 /* OSLoggerPlatformProvider.swift in Sources */, 3C8E6DF928A6D89E0031E48A /* OSOperationExecutor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4508,7 +4530,6 @@ 9124123F1E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m in Sources */, 7AECE59F23675F6300537907 /* OSFocusTimeProcessorFactory.m in Sources */, DEC08B012947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, - 3C14E3A62AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */, CA1A6E7120DC2E73001C41B9 /* OneSignalDialogRequest.m in Sources */, 912412331E73342200E41FD7 /* OneSignalTracker.m in Sources */, 7A674F1C2360D82E001F9ACD /* OSBaseFocusTimeProcessor.m in Sources */, @@ -4562,6 +4583,7 @@ 3C23A21F2FCE0AA1001D32E3 /* OSResilientStorageTests.swift in Sources */, 3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */, 3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */, + 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */, 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4602,7 +4624,6 @@ CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */, 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, - 3C14E3A82AFAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, @@ -4633,7 +4654,6 @@ 03CCCC852835F291004BF794 /* UIApplicationDelegateSwizzlingTests.m in Sources */, 4529DEEA1FA8360C00CEAB1D /* UIApplicationOverrider.m in Sources */, DEC08B022947D4E900C81DA3 /* OneSignalSwiftInterface.swift in Sources */, - 3C14E3A72AFAE54C006ED053 /* OSLoggerAdapters.swift in Sources */, 7A93269E25AF4F0300BBEC27 /* OSPendingCallbacks.m in Sources */, 7AECE59823674AB700537907 /* OSUnattributedFocusTimeProcessor.m in Sources */, 7A5A818224897693002E07C8 /* MigrationTests.m in Sources */, @@ -5390,6 +5410,14 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -5450,6 +5478,14 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -6940,6 +6976,14 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -6986,6 +7030,14 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -7040,6 +7092,14 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -9145,6 +9205,14 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ENABLE_MODULES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift new file mode 100644 index 000000000..e11b1318d --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift @@ -0,0 +1,43 @@ +/* + 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 +@_implementationOnly import OneSignalKMP + +extension KotlinByteArray { + var data: Data { + Data((0.. Bool { + do { + try createRootDirectory() + let timestamp = Int64(Date().timeIntervalSince1970 * 1_000) + let id = "\(timestamp)-\(UUID().uuidString)\(Self.ownedFileSuffix)" + try writeDurably(bytes.data, to: rootURL.appendingPathComponent(id)) + return true + } catch { + return false + } + } + + func listReadable( + minAgeMillis: Int64, + completionHandler: @escaping ([StoredLogFile]?, Error?) -> Void + ) { + ioQueue.async { + do { + let entries = try self.readableEntries(minAgeMillis: minAgeMillis) + completionHandler(entries, nil) + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSLogFileStore listReadable failed: \(error.localizedDescription)" + ) + completionHandler([], nil) + } + } + } + + func delete(id: String, completionHandler: @escaping (Error?) -> Void) { + ioQueue.async { + guard self.isSafeEntryId(id) else { + completionHandler(nil) + return + } + + do { + let url = self.rootURL.appendingPathComponent(id) + if self.fileManager.fileExists(atPath: url.path) { + try self.fileManager.removeItem(at: url) + } + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSLogFileStore delete failed: \(error.localizedDescription)" + ) + } + completionHandler(nil) + } + } + + func deleteUnrecognizedEntries( + minAgeMillis: Int64, + completionHandler: @escaping (KotlinInt?, Error?) -> Void + ) { + ioQueue.async { + var deleted = 0 + do { + for url in try self.fileURLs() where !url.lastPathComponent.hasSuffix(Self.ownedFileSuffix) { + guard try self.isOldEnough(url, minAgeMillis: minAgeMillis) else { + continue + } + try self.fileManager.removeItem(at: url) + deleted += 1 + } + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSLogFileStore cleanup failed: \(error.localizedDescription)" + ) + } + completionHandler(KotlinInt(int: Int32(deleted)), nil) + } + } + + private func readableEntries(minAgeMillis: Int64) throws -> [StoredLogFile] { + try fileURLs() + .filter { $0.lastPathComponent.hasSuffix(Self.ownedFileSuffix) } + .filter { try isOldEnough($0, minAgeMillis: minAgeMillis) } + .compactMap { url in + guard let data = try? Data(contentsOf: url) else { + return nil + } + return StoredLogFile(id: url.lastPathComponent, bytes: data.kotlinByteArray) + } + } + + private func fileURLs() throws -> [URL] { + guard fileManager.fileExists(atPath: rootURL.path) else { + return [] + } + return try fileManager.contentsOfDirectory( + at: rootURL, + includingPropertiesForKeys: [.contentModificationDateKey, .isRegularFileKey], + options: [.skipsHiddenFiles] + ).filter { + (try? $0.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true + } + } + + private func isOldEnough(_ url: URL, minAgeMillis: Int64) throws -> Bool { + let values = try url.resourceValues(forKeys: [.contentModificationDateKey]) + guard let modifiedAt = values.contentModificationDate else { + return false + } + return Date().timeIntervalSince(modifiedAt) * 1_000 >= Double(max(0, minAgeMillis)) + } + + private func isSafeEntryId(_ id: String) -> Bool { + !id.isEmpty && URL(fileURLWithPath: id).lastPathComponent == id + } + + private func createRootDirectory() throws { + try fileManager.createDirectory(at: rootURL, withIntermediateDirectories: true) + } + + private func writeDurably(_ data: Data, to targetURL: URL) throws { + let temporaryURL = targetURL.appendingPathExtension("tmp") + let descriptor = open(temporaryURL.path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR) + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + var isClosed = false + defer { + if !isClosed { + close(descriptor) + } + } + + do { + try data.withUnsafeBytes { rawBuffer in + guard var pointer = rawBuffer.baseAddress else { + return + } + var remaining = rawBuffer.count + while remaining > 0 { + let count = Darwin.write(descriptor, pointer, remaining) + guard count > 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + pointer = pointer.advanced(by: count) + remaining -= count + } + } + guard fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + guard close(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + isClosed = true + try fileManager.moveItem(at: temporaryURL, to: targetURL) + try syncDirectory() + } catch { + try? fileManager.removeItem(at: temporaryURL) + throw error + } + } + + private func syncDirectory() throws { + let descriptor = open(rootURL.path, O_RDONLY) + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + defer { close(descriptor) } + guard fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift new file mode 100644 index 000000000..76e8f1d1b --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift @@ -0,0 +1,116 @@ +/* + 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 +@_implementationOnly import OneSignalKMP + +/// Sends the KMP logger's encoded OTLP requests using the native URL loading system. +final class OSLogHttpSender: ILogHttpSender { + private static let requestTimeout: TimeInterval = 10 + private static let transportFailureStatusCode: Int32 = -1 + private static let defaultSession: URLSession = { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = requestTimeout + configuration.timeoutIntervalForResource = requestTimeout + return URLSession(configuration: configuration) + }() + + typealias RequestSender = ( + URLRequest, + @escaping (Data?, URLResponse?, Error?) -> Void + ) -> Void + + init(session: URLSession = OSLogHttpSender.defaultSession) { + self.requestSender = { request, completion in + session.dataTask(with: request, completionHandler: completion).resume() + } + } + + init(requestSender: @escaping RequestSender) { + self.requestSender = requestSender + } + + private let requestSender: RequestSender + + func send( + request: LogHttpRequest, + completionHandler: @escaping (LogHttpResponse?, Error?) -> Void + ) { + guard let url = URL(string: request.url) else { + completionHandler( + LogHttpResponse( + success: false, + statusCode: Self.transportFailureStatusCode, + message: "Invalid log request URL" + ), + nil + ) + return + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = request.body.data + request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } + urlRequest.setValue(request.contentType, forHTTPHeaderField: "Content-Type") + + requestSender(urlRequest) { _, response, error in + if let error = error { + completionHandler( + LogHttpResponse( + success: false, + statusCode: Self.transportFailureStatusCode, + message: error.localizedDescription + ), + nil + ) + return + } + + guard let response = response as? HTTPURLResponse else { + completionHandler( + LogHttpResponse( + success: false, + statusCode: Self.transportFailureStatusCode, + message: "Missing HTTP response" + ), + nil + ) + return + } + + completionHandler( + LogHttpResponse( + success: (200...299).contains(response.statusCode), + statusCode: Int32(response.statusCode), + message: nil + ), + nil + ) + } + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift new file mode 100644 index 000000000..1b254fd95 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift @@ -0,0 +1,50 @@ +/* + 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 + +/// Routes diagnostic messages from the shared logger through the iOS SDK's +/// existing logging pipeline. +final class OSLoggerAdapter: ILogger { + func error(message: String) { + OneSignalLog.onesignalLog(.LL_ERROR, message: message) + } + + func warn(message: String) { + OneSignalLog.onesignalLog(.LL_WARN, message: message) + } + + func info(message: String) { + OneSignalLog.onesignalLog(.LL_INFO, message: message) + } + + func debug(message: String) { + OneSignalLog.onesignalLog(.LL_DEBUG, message: message) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift new file mode 100644 index 000000000..ec429042a --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -0,0 +1,192 @@ +/* + 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 Darwin +import Foundation +import OneSignalCore +@_implementationOnly import OneSignalKMP +import UIKit + +/// Supplies iOS SDK state and platform metadata to the shared logger. +/// +/// User identifiers are injected by the composition layer because +/// `OneSignalUser` depends on `OneSignalOSCore`; importing it here would create +/// a circular module dependency. +final class OSLoggerPlatformProvider: ILoggerPlatformProvider { + typealias IdentifierProvider = () -> String? + typealias AppStateProvider = () -> String + + private enum Constants { + static let installIdKey = "PREFS_OS_INSTALL_ID" + static let sdkBase = "ios" + static let deviceManufacturer = "Apple" + static let unknown = "unknown" + static let osBuildName = "kern.osversion" + static let loggingConfigKey = "logging_config" + static let logLevelKey = "log_level" + static let disabledLogLevel = "NONE" + static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] + + /// Prevents reading a report while a terminating process may still be + /// completing its durable write. + static let minimumFileAgeMillis: Int64 = 5_000 + } + + private static let processStartedAt = processStartDate() + private let onesignalIdProvider: IdentifierProvider + private let pushSubscriptionIdProvider: IdentifierProvider + private let appStateProvider: AppStateProvider + + init( + onesignalIdProvider: @escaping IdentifierProvider, + pushSubscriptionIdProvider: @escaping IdentifierProvider, + appStateProvider: @escaping AppStateProvider + ) { + self.onesignalIdProvider = onesignalIdProvider + self.pushSubscriptionIdProvider = pushSubscriptionIdProvider + self.appStateProvider = appStateProvider + } + + private lazy var installId: String = { + let defaults = OneSignalUserDefaults.initShared() + if let saved = defaults.getSavedString(forKey: Constants.installIdKey, defaultValue: nil) { + return saved + } + let generated = UUID().uuidString + defaults.saveString(forKey: Constants.installIdKey, withValue: generated) + return generated + }() + + func getInstallId(completionHandler: @escaping (String?, Error?) -> Void) { + completionHandler(installId, nil) + } + + let sdkBase = Constants.sdkBase + let sdkBaseVersion = ONESIGNAL_VERSION + let appPackageId = Bundle.main.bundleIdentifier ?? Constants.unknown + let appVersion = + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + ?? Constants.unknown + let deviceManufacturer = Constants.deviceManufacturer + let deviceModel = OSDeviceUtils.getDeviceVariant() ?? Constants.unknown + let osName = UIDevice.current.systemName + let osVersion = UIDevice.current.systemVersion + let osBuildId = OSLoggerPlatformProvider.systemValue(named: Constants.osBuildName) + let sdkWrapper = OneSignalWrapper.sdkType + let sdkWrapperVersion = OneSignalWrapper.sdkVersion + let enabledFeatureFlags: [String] = [] + + var appId: String? { + OneSignalIdentifiers.currentAppId + } + + var onesignalId: String? { + onesignalIdProvider() + } + + var pushSubscriptionId: String? { + pushSubscriptionIdProvider() + } + + var appState: String { + appStateProvider() + } + + var processUptime: Int64 { + Int64(max(0, Date().timeIntervalSince(Self.processStartedAt) * 1_000)) + } + + var currentThreadName: String { + if let name = Thread.current.name, !name.isEmpty { + return name + } + if Thread.isMainThread { + return "main" + } + var name = [CChar](repeating: 0, count: 64) + return pthread_getname_np(pthread_self(), &name, name.count) == 0 + ? String(cString: name) + : Constants.unknown + } + + let crashStoragePath: String = { + let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + ?? FileManager.default.temporaryDirectory + return Constants.crashDirectoryComponents.reduce(caches) { + $0.appendingPathComponent($1, isDirectory: true) + }.path + }() + + let minFileAgeForReadMillis = Constants.minimumFileAgeMillis + + var isRemoteLoggingEnabled: Bool { + guard let level = remoteLogLevel else { + return false + } + return level != Constants.disabledLogLevel + } + + var remoteLogLevel: String? { + let config = + OSRemoteParamController.shared().remoteParams[Constants.loggingConfigKey] + as? [String: Any] + return (config?[Constants.logLevelKey] as? String)?.uppercased() + } + + let isExporterLoggingEnabled = false + + var appIdForHeaders: String { + appId ?? "" + } + + let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + private static func systemValue(named name: String) -> String { + var size = 0 + guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { + return Constants.unknown + } + var value = [CChar](repeating: 0, count: size) + guard sysctlbyname(name, &value, &size, nil, 0) == 0 else { + return Constants.unknown + } + return String(cString: value) + } + + private static func processStartDate() -> Date { + var processInfo = kinfo_proc() + var size = MemoryLayout.stride + var name = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + guard sysctl(&name, u_int(name.count), &processInfo, &size, nil, 0) == 0 else { + return Date() + } + let start = processInfo.kp_proc.p_starttime + return Date( + timeIntervalSince1970: TimeInterval(start.tv_sec) + TimeInterval(start.tv_usec) / 1_000_000 + ) + } +} diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift similarity index 92% rename from iOS_SDK/OneSignalSDK/UnitTests/OSLoggerAdaptersTests.swift rename to iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 664fd670d..9c50c1475 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -28,6 +28,7 @@ import Foundation import OneSignalCore import OneSignalKMP +@testable import OneSignalOSCore import XCTest final class OSLoggerAdaptersTests: XCTestCase { @@ -133,8 +134,12 @@ final class OSLoggerAdaptersTests: XCTestCase { XCTAssertEqual(listener.levels, [.LL_ERROR, .LL_WARN, .LL_INFO, .LL_DEBUG]) } - func testPlatformProviderReturnsStableInstallIdAndPlatformMetadata() { - let provider = OSLoggerPlatformProvider() + func testPlatformProviderReturnsInjectedIdentifiersAndPlatformMetadata() { + let provider = OSLoggerPlatformProvider( + onesignalIdProvider: { "onesignal-id" }, + pushSubscriptionIdProvider: { "subscription-id" }, + appStateProvider: { "foreground" } + ) var firstInstallId: String? var secondInstallId: String? @@ -143,6 +148,9 @@ final class OSLoggerAdaptersTests: XCTestCase { XCTAssertFalse(firstInstallId?.isEmpty ?? true) XCTAssertEqual(firstInstallId, secondInstallId) + XCTAssertEqual(provider.onesignalId, "onesignal-id") + XCTAssertEqual(provider.pushSubscriptionId, "subscription-id") + XCTAssertEqual(provider.appState, "foreground") XCTAssertEqual(provider.sdkBase, "ios") XCTAssertFalse(provider.appPackageId.isEmpty) XCTAssertFalse(provider.osVersion.isEmpty) diff --git a/iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift b/iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift deleted file mode 100644 index 4276b423c..000000000 --- a/iOS_SDK/OneSignalSDK/Source/OSLoggerAdapters.swift +++ /dev/null @@ -1,451 +0,0 @@ -/* - 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 Darwin -import Foundation -import OneSignalCore -import OneSignalKMP -import OneSignalUser -import UIKit - -final class OSLoggerAdapter: NSObject, ILogger { - func error(message: String) { - OneSignalLog.onesignalLog(.LL_ERROR, message: message) - } - - func warn(message: String) { - OneSignalLog.onesignalLog(.LL_WARN, message: message) - } - - func info(message: String) { - OneSignalLog.onesignalLog(.LL_INFO, message: message) - } - - func debug(message: String) { - OneSignalLog.onesignalLog(.LL_DEBUG, message: message) - } -} - -final class OSLogHttpSender: NSObject, ILogHttpSender { - private static let defaultSession: URLSession = { - let configuration = URLSessionConfiguration.default - configuration.timeoutIntervalForRequest = 10 - configuration.timeoutIntervalForResource = 10 - return URLSession(configuration: configuration) - }() - - typealias RequestSender = ( - URLRequest, - @escaping (Data?, URLResponse?, Error?) -> Void - ) -> Void - - init(session: URLSession = OSLogHttpSender.defaultSession) { - self.requestSender = { request, completion in - session.dataTask(with: request, completionHandler: completion).resume() - } - } - - init(requestSender: @escaping RequestSender) { - self.requestSender = requestSender - } - - private let requestSender: RequestSender - - func send( - request: LogHttpRequest, - completionHandler: @escaping (LogHttpResponse?, Error?) -> Void - ) { - guard let url = URL(string: request.url) else { - completionHandler( - LogHttpResponse(success: false, statusCode: -1, message: "Invalid log request URL"), - nil - ) - return - } - - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = "POST" - urlRequest.httpBody = request.body.data - request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } - urlRequest.setValue(request.contentType, forHTTPHeaderField: "Content-Type") - - requestSender(urlRequest) { _, response, error in - if let error = error { - completionHandler( - LogHttpResponse(success: false, statusCode: -1, message: error.localizedDescription), - nil - ) - return - } - - guard let response = response as? HTTPURLResponse else { - completionHandler( - LogHttpResponse(success: false, statusCode: -1, message: "Missing HTTP response"), - nil - ) - return - } - - completionHandler( - LogHttpResponse( - success: (200...299).contains(response.statusCode), - statusCode: Int32(response.statusCode), - message: nil - ), - nil - ) - } - } -} - -final class OSLogFileStore: NSObject, ILogFileStore { - private static let ownedSuffix = ".otlp" - - private let rootURL: URL - private let fileManager: FileManager - private let ioQueue = DispatchQueue(label: "com.onesignal.logger.file-store", qos: .utility) - - init(rootPath: String, fileManager: FileManager = .default) { - self.rootURL = URL(fileURLWithPath: rootPath, isDirectory: true) - self.fileManager = fileManager - super.init() - try? createRootDirectory() - } - - func save(bytes: KotlinByteArray) -> Bool { - do { - try createRootDirectory() - let id = "\(Int64(Date().timeIntervalSince1970 * 1_000))-\(UUID().uuidString)\(Self.ownedSuffix)" - try writeDurably(bytes.data, to: rootURL.appendingPathComponent(id)) - return true - } catch { - return false - } - } - - func listReadable( - minAgeMillis: Int64, - completionHandler: @escaping ([StoredLogFile]?, Error?) -> Void - ) { - ioQueue.async { - do { - let entries = try self.readableEntries(minAgeMillis: minAgeMillis) - completionHandler(entries, nil) - } catch { - OneSignalLog.onesignalLog( - .LL_WARN, - message: "OSLogFileStore listReadable failed: \(error.localizedDescription)" - ) - completionHandler([], nil) - } - } - } - - func delete(id: String, completionHandler: @escaping (Error?) -> Void) { - ioQueue.async { - guard self.isSafeEntryId(id) else { - completionHandler(nil) - return - } - - do { - let url = self.rootURL.appendingPathComponent(id) - if self.fileManager.fileExists(atPath: url.path) { - try self.fileManager.removeItem(at: url) - } - } catch { - OneSignalLog.onesignalLog( - .LL_WARN, - message: "OSLogFileStore delete failed: \(error.localizedDescription)" - ) - } - completionHandler(nil) - } - } - - func deleteUnrecognizedEntries( - minAgeMillis: Int64, - completionHandler: @escaping (KotlinInt?, Error?) -> Void - ) { - ioQueue.async { - var deleted = 0 - do { - for url in try self.fileURLs() where !url.lastPathComponent.hasSuffix(Self.ownedSuffix) { - guard try self.isOldEnough(url, minAgeMillis: minAgeMillis) else { - continue - } - try self.fileManager.removeItem(at: url) - deleted += 1 - } - } catch { - OneSignalLog.onesignalLog( - .LL_WARN, - message: "OSLogFileStore cleanup failed: \(error.localizedDescription)" - ) - } - completionHandler(KotlinInt(int: Int32(deleted)), nil) - } - } - - private func readableEntries(minAgeMillis: Int64) throws -> [StoredLogFile] { - try fileURLs() - .filter { $0.lastPathComponent.hasSuffix(Self.ownedSuffix) } - .filter { try isOldEnough($0, minAgeMillis: minAgeMillis) } - .compactMap { url in - guard let data = try? Data(contentsOf: url) else { - return nil - } - return StoredLogFile(id: url.lastPathComponent, bytes: data.kotlinByteArray) - } - } - - private func fileURLs() throws -> [URL] { - guard fileManager.fileExists(atPath: rootURL.path) else { - return [] - } - return try fileManager.contentsOfDirectory( - at: rootURL, - includingPropertiesForKeys: [.contentModificationDateKey, .isRegularFileKey], - options: [.skipsHiddenFiles] - ).filter { - (try? $0.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true - } - } - - private func isOldEnough(_ url: URL, minAgeMillis: Int64) throws -> Bool { - let values = try url.resourceValues(forKeys: [.contentModificationDateKey]) - guard let modifiedAt = values.contentModificationDate else { - return false - } - return Date().timeIntervalSince(modifiedAt) * 1_000 >= Double(max(0, minAgeMillis)) - } - - private func isSafeEntryId(_ id: String) -> Bool { - !id.isEmpty && URL(fileURLWithPath: id).lastPathComponent == id - } - - private func createRootDirectory() throws { - try fileManager.createDirectory(at: rootURL, withIntermediateDirectories: true) - } - - private func writeDurably(_ data: Data, to targetURL: URL) throws { - let temporaryURL = targetURL.appendingPathExtension("tmp") - let descriptor = open(temporaryURL.path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR) - guard descriptor >= 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - var isClosed = false - defer { - if !isClosed { - close(descriptor) - } - } - - do { - try data.withUnsafeBytes { rawBuffer in - guard var pointer = rawBuffer.baseAddress else { - return - } - var remaining = rawBuffer.count - while remaining > 0 { - let count = Darwin.write(descriptor, pointer, remaining) - guard count > 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - pointer = pointer.advanced(by: count) - remaining -= count - } - } - guard fsync(descriptor) == 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - guard close(descriptor) == 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - isClosed = true - try fileManager.moveItem(at: temporaryURL, to: targetURL) - try syncDirectory() - } catch { - try? fileManager.removeItem(at: temporaryURL) - throw error - } - } - - private func syncDirectory() throws { - let descriptor = open(rootURL.path, O_RDONLY) - guard descriptor >= 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - defer { close(descriptor) } - guard fsync(descriptor) == 0 else { - throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) - } - } -} - -final class OSLoggerPlatformProvider: NSObject, ILoggerPlatformProvider { - private static let installIdKey = "PREFS_OS_INSTALL_ID" - private static let processStartedAt = processStartDate() - - private lazy var installId: String = { - let defaults = OneSignalUserDefaults.initShared() - if let saved = defaults.getSavedString(forKey: Self.installIdKey, defaultValue: nil) { - return saved - } - let generated = UUID().uuidString - defaults.saveString(forKey: Self.installIdKey, withValue: generated) - return generated - }() - - func getInstallId(completionHandler: @escaping (String?, Error?) -> Void) { - completionHandler(installId, nil) - } - - let sdkBase = "ios" - let sdkBaseVersion = ONESIGNAL_VERSION - let appPackageId = Bundle.main.bundleIdentifier ?? "unknown" - let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" - let deviceManufacturer = "Apple" - let deviceModel = OSDeviceUtils.getDeviceVariant() ?? "unknown" - let osName = UIDevice.current.systemName - let osVersion = UIDevice.current.systemVersion - let osBuildId = OSLoggerPlatformProvider.systemValue(named: "kern.osversion") - let sdkWrapper = OneSignalWrapper.sdkType - let sdkWrapperVersion = OneSignalWrapper.sdkVersion - let enabledFeatureFlags: [String] = [] - - var appId: String? { - OneSignalIdentifiers.currentAppId - } - - var onesignalId: String? { - OneSignalUserManagerImpl.sharedInstance.onesignalId - } - - var pushSubscriptionId: String? { - OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId - } - - var appState: String { - switch UIApplication.shared.applicationState { - case .active: - return "foreground" - case .background: - return "background" - default: - return "unknown" - } - } - - var processUptime: Int64 { - Int64(max(0, Date().timeIntervalSince(Self.processStartedAt) * 1_000)) - } - - var currentThreadName: String { - if let name = Thread.current.name, !name.isEmpty { - return name - } - if Thread.isMainThread { - return "main" - } - var name = [CChar](repeating: 0, count: 64) - return pthread_getname_np(pthread_self(), &name, name.count) == 0 - ? String(cString: name) - : "unknown" - } - - let crashStoragePath: String = { - let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first - ?? FileManager.default.temporaryDirectory - return caches - .appendingPathComponent("onesignal", isDirectory: true) - .appendingPathComponent("logger", isDirectory: true) - .appendingPathComponent("crashes", isDirectory: true) - .path - }() - - let minFileAgeForReadMillis: Int64 = 5_000 - - var isRemoteLoggingEnabled: Bool { - guard let level = remoteLogLevel else { - return false - } - return level != "NONE" - } - - var remoteLogLevel: String? { - let config = OSRemoteParamController.shared().remoteParams["logging_config"] as? [String: Any] - return (config?["log_level"] as? String)?.uppercased() - } - - let isExporterLoggingEnabled = false - - var appIdForHeaders: String { - appId ?? "" - } - - let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - - private static func systemValue(named name: String) -> String { - var size = 0 - guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { - return "unknown" - } - var value = [CChar](repeating: 0, count: size) - guard sysctlbyname(name, &value, &size, nil, 0) == 0 else { - return "unknown" - } - return String(cString: value) - } - - private static func processStartDate() -> Date { - var processInfo = kinfo_proc() - var size = MemoryLayout.stride - var name = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] - guard sysctl(&name, u_int(name.count), &processInfo, &size, nil, 0) == 0 else { - return Date() - } - let start = processInfo.kp_proc.p_starttime - return Date( - timeIntervalSince1970: TimeInterval(start.tv_sec) + TimeInterval(start.tv_usec) / 1_000_000 - ) - } -} - -private extension KotlinByteArray { - var data: Data { - Data((0.. Date: Thu, 6 Aug 2026 11:05:37 -0700 Subject: [PATCH 4/6] fix: [SDK-4976] harden iOS KMP logger adapters Make crash-path providers injection-based and thread-safe, align HTTP diagnostics and persistence edge cases with Android, and use the KMP pinned-memory byte bridge. Co-authored-by: Cursor --- OneSignal-KMP-SDK | 2 +- .../OneSignal.xcodeproj/project.pbxproj | 16 ++-- .../Source/Logging/KotlinByteArray+Data.swift | 6 +- ...ileStore.swift => OSLoggerFileStore.swift} | 26 +++-- ...pSender.swift => OSLoggerHttpSender.swift} | 55 +++++++++-- .../Logging/OSLoggerPlatformProvider.swift | 76 +++++++-------- .../OSLoggerAdaptersTests.swift | 95 ++++++++++++++++--- 7 files changed, 194 insertions(+), 82 deletions(-) rename iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/{OSLogFileStore.swift => OSLoggerFileStore.swift} (88%) rename iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/{OSLogHttpSender.swift => OSLoggerHttpSender.swift} (65%) diff --git a/OneSignal-KMP-SDK b/OneSignal-KMP-SDK index 07bbfadff..d417111af 160000 --- a/OneSignal-KMP-SDK +++ b/OneSignal-KMP-SDK @@ -1 +1 @@ -Subproject commit 07bbfadffd887304da0a8b6cdbdbcdd499905611 +Subproject commit d417111afa4e81ab80e49931944230e6a0ad47df diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index b52c884e4..14fe7593e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -70,8 +70,8 @@ 3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; }; 3C14E3B02FAE54C006ED053 /* OSLoggerAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */; }; - 3C14E3B12FAE54C006ED053 /* OSLogHttpSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AA2FAE54C006ED053 /* OSLogHttpSender.swift */; }; - 3C14E3B22FAE54C006ED053 /* OSLogFileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AB2FAE54C006ED053 /* OSLogFileStore.swift */; }; + 3C14E3B12FAE54C006ED053 /* OSLoggerHttpSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AA2FAE54C006ED053 /* OSLoggerHttpSender.swift */; }; + 3C14E3B22FAE54C006ED053 /* OSLoggerFileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AB2FAE54C006ED053 /* OSLoggerFileStore.swift */; }; 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 */; }; @@ -1784,8 +1784,8 @@ DEBAAEB72A4381AE00BF2C1C /* OSInAppMessageMigrationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageMigrationController.m; sourceTree = ""; }; DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalSwiftInterface.swift; sourceTree = ""; }; 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdapter.swift; sourceTree = ""; }; - 3C14E3AA2FAE54C006ED053 /* OSLogHttpSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHttpSender.swift; sourceTree = ""; }; - 3C14E3AB2FAE54C006ED053 /* OSLogFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogFileStore.swift; sourceTree = ""; }; + 3C14E3AA2FAE54C006ED053 /* OSLoggerHttpSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerHttpSender.swift; sourceTree = ""; }; + 3C14E3AB2FAE54C006ED053 /* OSLoggerFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerFileStore.swift; sourceTree = ""; }; 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 = ""; }; @@ -2281,8 +2281,8 @@ isa = PBXGroup; children = ( 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */, - 3C14E3AB2FAE54C006ED053 /* OSLogFileStore.swift */, - 3C14E3AA2FAE54C006ED053 /* OSLogHttpSender.swift */, + 3C14E3AB2FAE54C006ED053 /* OSLoggerFileStore.swift */, + 3C14E3AA2FAE54C006ED053 /* OSLoggerHttpSender.swift */, 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */, 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */, ); @@ -4419,9 +4419,9 @@ files = ( 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */, DEFB3E652BB7346D00E65DAD /* OSLiveActivities.swift in Sources */, - 3C14E3B22FAE54C006ED053 /* OSLogFileStore.swift in Sources */, + 3C14E3B22FAE54C006ED053 /* OSLoggerFileStore.swift in Sources */, 5BC1DE602C90B83900CA8807 /* OSConsistencyKeyEnum.swift in Sources */, - 3C14E3B12FAE54C006ED053 /* OSLogHttpSender.swift in Sources */, + 3C14E3B12FAE54C006ED053 /* OSLoggerHttpSender.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, 3C14E3B02FAE54C006ED053 /* OSLoggerAdapter.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift index e11b1318d..e2bb5b328 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift @@ -30,14 +30,12 @@ import Foundation extension KotlinByteArray { var data: Data { - Data((0.. Bool { + guard bytes.size > 0 else { + return false + } do { try createRootDirectory() let timestamp = Int64(Date().timeIntervalSince1970 * 1_000) @@ -74,7 +78,7 @@ final class OSLogFileStore: ILogFileStore { } catch { OneSignalLog.onesignalLog( .LL_WARN, - message: "OSLogFileStore listReadable failed: \(error.localizedDescription)" + message: "OSLoggerFileStore listReadable failed: \(error.localizedDescription)" ) completionHandler([], nil) } @@ -96,7 +100,7 @@ final class OSLogFileStore: ILogFileStore { } catch { OneSignalLog.onesignalLog( .LL_WARN, - message: "OSLogFileStore delete failed: \(error.localizedDescription)" + message: "OSLoggerFileStore delete failed: \(error.localizedDescription)" ) } completionHandler(nil) @@ -110,7 +114,8 @@ final class OSLogFileStore: ILogFileStore { ioQueue.async { var deleted = 0 do { - for url in try self.fileURLs() where !url.lastPathComponent.hasSuffix(Self.ownedFileSuffix) { + for url in try self.fileURLs() + where url.lastPathComponent.hasSuffix(Self.temporaryFileSuffix) { guard try self.isOldEnough(url, minAgeMillis: minAgeMillis) else { continue } @@ -120,7 +125,7 @@ final class OSLogFileStore: ILogFileStore { } catch { OneSignalLog.onesignalLog( .LL_WARN, - message: "OSLogFileStore cleanup failed: \(error.localizedDescription)" + message: "OSLoggerFileStore cleanup failed: \(error.localizedDescription)" ) } completionHandler(KotlinInt(int: Int32(deleted)), nil) @@ -180,6 +185,10 @@ final class OSLogFileStore: ILogFileStore { close(descriptor) } } + try fileManager.setAttributes( + [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], + ofItemAtPath: temporaryURL.path + ) do { try data.withUnsafeBytes { rawBuffer in @@ -189,6 +198,9 @@ final class OSLogFileStore: ILogFileStore { var remaining = rawBuffer.count while remaining > 0 { let count = Darwin.write(descriptor, pointer, remaining) + if count < 0 && errno == EINTR { + continue + } guard count > 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift similarity index 65% rename from iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift rename to iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift index 76e8f1d1b..9e0d6bc78 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogHttpSender.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift @@ -29,9 +29,10 @@ import Foundation @_implementationOnly import OneSignalKMP /// Sends the KMP logger's encoded OTLP requests using the native URL loading system. -final class OSLogHttpSender: ILogHttpSender { +final class OSLoggerHttpSender: ILogHttpSender { private static let requestTimeout: TimeInterval = 10 private static let transportFailureStatusCode: Int32 = -1 + private static let maximumDiagnosticBodyLength = 500 private static let defaultSession: URLSession = { let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = requestTimeout @@ -44,17 +45,31 @@ final class OSLogHttpSender: ILogHttpSender { @escaping (Data?, URLResponse?, Error?) -> Void ) -> Void - init(session: URLSession = OSLogHttpSender.defaultSession) { + init( + session: URLSession = OSLoggerHttpSender.defaultSession, + logger: ILogger = OSLoggerAdapter(), + isDiagnosticsEnabled: @escaping () -> Bool = { false } + ) { self.requestSender = { request, completion in session.dataTask(with: request, completionHandler: completion).resume() } + self.logger = logger + self.isDiagnosticsEnabled = isDiagnosticsEnabled } - init(requestSender: @escaping RequestSender) { + init( + requestSender: @escaping RequestSender, + logger: ILogger = OSLoggerAdapter(), + isDiagnosticsEnabled: @escaping () -> Bool = { false } + ) { self.requestSender = requestSender + self.logger = logger + self.isDiagnosticsEnabled = isDiagnosticsEnabled } private let requestSender: RequestSender + private let logger: ILogger + private let isDiagnosticsEnabled: () -> Bool func send( request: LogHttpRequest, @@ -75,11 +90,16 @@ final class OSLogHttpSender: ILogHttpSender { var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" urlRequest.httpBody = request.body.data - request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } urlRequest.setValue(request.contentType, forHTTPHeaderField: "Content-Type") + request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } - requestSender(urlRequest) { _, response, error in + requestSender(urlRequest) { data, response, error in if let error = error { + if self.isDiagnosticsEnabled() { + self.logger.warn( + message: "OSLoggerHttpSender: POST \(request.url) failed: \(error.localizedDescription)" + ) + } completionHandler( LogHttpResponse( success: false, @@ -103,14 +123,35 @@ final class OSLogHttpSender: ILogHttpSender { return } + let success = (200...299).contains(response.statusCode) + let responseBody = data.flatMap { String(data: $0, encoding: .utf8) } + if self.isDiagnosticsEnabled() { + if success { + self.logger.debug( + message: "OSLoggerHttpSender: POST \(request.url) -> \(response.statusCode) OK " + + "(\(request.body.size)B)" + ) + } else { + self.logger.warn( + message: "OSLoggerHttpSender: POST \(request.url) -> \(response.statusCode) " + + "(ct=\(request.contentType), \(request.body.size)B) " + + "body=\(responseBody.map(Self.truncatedDiagnosticBody) ?? "nil")" + ) + } + } + completionHandler( LogHttpResponse( - success: (200...299).contains(response.statusCode), + success: success, statusCode: Int32(response.statusCode), - message: nil + message: success ? nil : responseBody ), nil ) } } + + private static func truncatedDiagnosticBody(_ body: String) -> String { + String(body.prefix(maximumDiagnosticBodyLength)) + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index ec429042a..7e7b335fa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -33,21 +33,23 @@ import UIKit /// Supplies iOS SDK state and platform metadata to the shared logger. /// -/// User identifiers are injected by the composition layer because +/// Mutable SDK state is injected by the composition layer because /// `OneSignalUser` depends on `OneSignalOSCore`; importing it here would create -/// a circular module dependency. +/// a circular module dependency. Providers must be safe to invoke from arbitrary +/// threads, including synchronously from a crash handler. final class OSLoggerPlatformProvider: ILoggerPlatformProvider { typealias IdentifierProvider = () -> String? + typealias InstallIdProvider = () -> String typealias AppStateProvider = () -> String + typealias FeatureFlagsProvider = () -> [String] + typealias LogLevelProvider = () -> String? + typealias BoolProvider = () -> Bool private enum Constants { - static let installIdKey = "PREFS_OS_INSTALL_ID" static let sdkBase = "ios" static let deviceManufacturer = "Apple" static let unknown = "unknown" static let osBuildName = "kern.osversion" - static let loggingConfigKey = "logging_config" - static let logLevelKey = "log_level" static let disabledLogLevel = "NONE" static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] @@ -56,33 +58,36 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let minimumFileAgeMillis: Int64 = 5_000 } - private static let processStartedAt = processStartDate() + private let installIdProvider: InstallIdProvider private let onesignalIdProvider: IdentifierProvider private let pushSubscriptionIdProvider: IdentifierProvider private let appStateProvider: AppStateProvider + private let featureFlagsProvider: FeatureFlagsProvider + private let remoteLogLevelProvider: LogLevelProvider + private let exporterLoggingEnabledProvider: BoolProvider init( + installIdProvider: @escaping InstallIdProvider, onesignalIdProvider: @escaping IdentifierProvider, pushSubscriptionIdProvider: @escaping IdentifierProvider, - appStateProvider: @escaping AppStateProvider + appStateProvider: @escaping AppStateProvider, + featureFlagsProvider: @escaping FeatureFlagsProvider, + remoteLogLevelProvider: @escaping LogLevelProvider, + exporterLoggingEnabledProvider: @escaping BoolProvider ) { + self.installIdProvider = installIdProvider self.onesignalIdProvider = onesignalIdProvider self.pushSubscriptionIdProvider = pushSubscriptionIdProvider self.appStateProvider = appStateProvider + self.featureFlagsProvider = featureFlagsProvider + self.remoteLogLevelProvider = remoteLogLevelProvider + self.exporterLoggingEnabledProvider = exporterLoggingEnabledProvider } - private lazy var installId: String = { - let defaults = OneSignalUserDefaults.initShared() - if let saved = defaults.getSavedString(forKey: Constants.installIdKey, defaultValue: nil) { - return saved - } - let generated = UUID().uuidString - defaults.saveString(forKey: Constants.installIdKey, withValue: generated) - return generated - }() - + /// The completion must run inline because crash reporting blocks the + /// crashing thread until this value is returned. func getInstallId(completionHandler: @escaping (String?, Error?) -> Void) { - completionHandler(installId, nil) + completionHandler(installIdProvider(), nil) } let sdkBase = Constants.sdkBase @@ -98,7 +103,9 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { let osBuildId = OSLoggerPlatformProvider.systemValue(named: Constants.osBuildName) let sdkWrapper = OneSignalWrapper.sdkType let sdkWrapperVersion = OneSignalWrapper.sdkVersion - let enabledFeatureFlags: [String] = [] + var enabledFeatureFlags: [String] { + featureFlagsProvider() + } var appId: String? { OneSignalIdentifiers.currentAppId @@ -117,7 +124,7 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { } var processUptime: Int64 { - Int64(max(0, Date().timeIntervalSince(Self.processStartedAt) * 1_000)) + Int64(ProcessInfo.processInfo.systemUptime * 1_000) } var currentThreadName: String { @@ -128,9 +135,11 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { return "main" } var name = [CChar](repeating: 0, count: 64) - return pthread_getname_np(pthread_self(), &name, name.count) == 0 - ? String(cString: name) - : Constants.unknown + guard pthread_getname_np(pthread_self(), &name, name.count) == 0 else { + return Constants.unknown + } + let threadName = String(cString: name) + return threadName.isEmpty ? Constants.unknown : threadName } let crashStoragePath: String = { @@ -151,13 +160,12 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { } var remoteLogLevel: String? { - let config = - OSRemoteParamController.shared().remoteParams[Constants.loggingConfigKey] - as? [String: Any] - return (config?[Constants.logLevelKey] as? String)?.uppercased() + remoteLogLevelProvider()?.uppercased() } - let isExporterLoggingEnabled = false + var isExporterLoggingEnabled: Bool { + exporterLoggingEnabledProvider() + } var appIdForHeaders: String { appId ?? "" @@ -177,16 +185,4 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { return String(cString: value) } - private static func processStartDate() -> Date { - var processInfo = kinfo_proc() - var size = MemoryLayout.stride - var name = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] - guard sysctl(&name, u_int(name.count), &processInfo, &size, nil, 0) == 0 else { - return Date() - } - let start = processInfo.kp_proc.p_starttime - return Date( - timeIntervalSince1970: TimeInterval(start.tv_sec) + TimeInterval(start.tv_usec) / 1_000_000 - ) - } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 9c50c1475..05811eb2f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -45,7 +45,7 @@ final class OSLoggerAdaptersTests: XCTestCase { } func testFileStoreSynchronouslySavesAndListsPayload() throws { - let store = OSLogFileStore(rootPath: temporaryDirectory.path) + let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) let payload = makeKotlinBytes([1, 2, 3, 255]) XCTAssertTrue(store.save(bytes: payload)) @@ -64,18 +64,21 @@ final class OSLoggerAdaptersTests: XCTestCase { wait(for: [listed], timeout: 2) } - func testFileStoreDeletesOnlyOldUnrecognizedEntries() throws { - let store = OSLogFileStore(rootPath: temporaryDirectory.path) + func testFileStoreDeletesOnlyInterruptedTemporaryWrites() throws { + let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) XCTAssertTrue(store.save(bytes: makeKotlinBytes([1]))) - let foreignURL = temporaryDirectory.appendingPathComponent("legacy-crash") + let temporaryURL = temporaryDirectory.appendingPathComponent("interrupted.otlp.tmp") + try Data([2]).write(to: temporaryURL) + let foreignURL = temporaryDirectory.appendingPathComponent("unowned") try Data([2]).write(to: foreignURL) - let cleaned = expectation(description: "cleans foreign payload") + let cleaned = expectation(description: "cleans interrupted write") store.deleteUnrecognizedEntries(minAgeMillis: 0) { count, error in XCTAssertNil(error) XCTAssertEqual(count?.int32Value, 1) - XCTAssertFalse(FileManager.default.fileExists(atPath: foreignURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: temporaryURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: foreignURL.path)) cleaned.fulfill() } wait(for: [cleaned], timeout: 2) @@ -88,11 +91,17 @@ final class OSLoggerAdaptersTests: XCTestCase { ) } + func testFileStoreRejectsEmptyPayload() { + let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) + + XCTAssertFalse(store.save(bytes: makeKotlinBytes([]))) + } + func testHttpSenderPostsEncodedBytesAndPassesHeaders() { let sent = expectation(description: "sends payload") - let sender = OSLogHttpSender { request, completion in + let sender = OSLoggerHttpSender { request, completion in XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/x-protobuf") + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/custom") XCTAssertEqual(request.value(forHTTPHeaderField: "SDK-Version"), "onesignal/ios/test") XCTAssertEqual(request.httpBody, Data([4, 5, 6])) let response = HTTPURLResponse( @@ -106,7 +115,10 @@ final class OSLoggerAdaptersTests: XCTestCase { let request = LogHttpRequest( url: "https://example.com/sdk/log", - headers: ["SDK-Version": "onesignal/ios/test"], + headers: [ + "Content-Type": "application/custom", + "SDK-Version": "onesignal/ios/test" + ], contentType: "application/x-protobuf", body: makeKotlinBytes([4, 5, 6]) ) @@ -120,6 +132,40 @@ final class OSLoggerAdaptersTests: XCTestCase { wait(for: [sent], timeout: 2) } + func testHttpSenderReturnsAndLogsFailureBodyWhenDiagnosticsEnabled() { + let logger = TestLogger() + let sender = OSLoggerHttpSender( + requestSender: { request, completion in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 403, + httpVersion: nil, + headerFields: nil + )! + completion(Data("denied".utf8), response, nil) + }, + logger: logger, + isDiagnosticsEnabled: { true } + ) + let request = LogHttpRequest( + url: "https://example.com/sdk/log", + headers: [:], + contentType: "application/x-protobuf", + body: makeKotlinBytes([1]) + ) + let sent = expectation(description: "returns failed response") + + sender.send(request: request) { response, error in + XCTAssertNil(error) + XCTAssertEqual(response?.statusCode, 403) + XCTAssertEqual(response?.message, "denied") + XCTAssertEqual(logger.warnings.count, 1) + sent.fulfill() + } + + wait(for: [sent], timeout: 2) + } + func testLoggerDelegatesToOneSignalLog() { let listener = LoggerAdapterListener() OneSignalLog.debug().__add(listener) @@ -136,9 +182,13 @@ final class OSLoggerAdaptersTests: XCTestCase { func testPlatformProviderReturnsInjectedIdentifiersAndPlatformMetadata() { let provider = OSLoggerPlatformProvider( + installIdProvider: { "install-id" }, onesignalIdProvider: { "onesignal-id" }, pushSubscriptionIdProvider: { "subscription-id" }, - appStateProvider: { "foreground" } + appStateProvider: { "foreground" }, + featureFlagsProvider: { ["feature"] }, + remoteLogLevelProvider: { "warn" }, + exporterLoggingEnabledProvider: { true } ) var firstInstallId: String? var secondInstallId: String? @@ -146,11 +196,14 @@ final class OSLoggerAdaptersTests: XCTestCase { provider.getInstallId { value, _ in firstInstallId = value } provider.getInstallId { value, _ in secondInstallId = value } - XCTAssertFalse(firstInstallId?.isEmpty ?? true) + XCTAssertEqual(firstInstallId, "install-id") XCTAssertEqual(firstInstallId, secondInstallId) XCTAssertEqual(provider.onesignalId, "onesignal-id") XCTAssertEqual(provider.pushSubscriptionId, "subscription-id") XCTAssertEqual(provider.appState, "foreground") + XCTAssertEqual(provider.enabledFeatureFlags, ["feature"]) + XCTAssertEqual(provider.remoteLogLevel, "WARN") + XCTAssertTrue(provider.isExporterLoggingEnabled) XCTAssertEqual(provider.sdkBase, "ios") XCTAssertFalse(provider.appPackageId.isEmpty) XCTAssertFalse(provider.osVersion.isEmpty) @@ -158,9 +211,7 @@ final class OSLoggerAdaptersTests: XCTestCase { } private func makeKotlinBytes(_ bytes: [UInt8]) -> KotlinByteArray { - KotlinByteArray(size: Int32(bytes.count)) { index in - KotlinByte(value: Int8(bitPattern: bytes[Int(index.int32Value)])) - } + AppleByteArrayInterop.shared.toByteArray(data: Data(bytes)) } } @@ -172,8 +223,22 @@ private final class LoggerAdapterListener: NSObject, OSLogListener { } } +private final class TestLogger: ILogger { + var warnings: [String] = [] + + func error(message: String) {} + + func warn(message: String) { + warnings.append(message) + } + + func info(message: String) {} + + func debug(message: String) {} +} + private extension KotlinByteArray { var bytes: [UInt8] { - (0.. Date: Thu, 6 Aug 2026 13:31:59 -0700 Subject: [PATCH 5/6] fix: [SDK-4976] preserve process and Catalyst compatibility Measure process uptime in the boot-relative timebase, omit unavailable KMP adapters from Catalyst, and verify Kotlin can invoke the pure Swift adapters end to end. Co-authored-by: Cursor --- .../Source/Logging/KotlinByteArray+Data.swift | 5 ++ .../Source/Logging/OSLoggerAdapter.swift | 5 ++ .../Source/Logging/OSLoggerFileStore.swift | 14 ++-- .../Source/Logging/OSLoggerHttpSender.swift | 5 ++ .../Logging/OSLoggerPlatformProvider.swift | 61 +++++++++++++++- .../OSLoggerAdaptersTests.swift | 73 ++++++++++++++++--- 6 files changed, 146 insertions(+), 17 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift index e2bb5b328..31b38b0bd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift @@ -25,6 +25,9 @@ THE SOFTWARE. */ +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + import Foundation @_implementationOnly import OneSignalKMP @@ -39,3 +42,5 @@ extension Data { AppleByteArrayInterop.shared.toByteArray(data: self) } } + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift index 1b254fd95..1b21f4e07 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift @@ -25,6 +25,9 @@ THE SOFTWARE. */ +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + import Foundation import OneSignalCore @_implementationOnly import OneSignalKMP @@ -48,3 +51,5 @@ final class OSLoggerAdapter: ILogger { OneSignalLog.onesignalLog(.LL_DEBUG, message: message) } } + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift index 873a75b59..5e255bc64 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift @@ -25,6 +25,9 @@ THE SOFTWARE. */ +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + import Darwin import Foundation import OneSignalCore @@ -185,12 +188,11 @@ final class OSLoggerFileStore: ILogFileStore { close(descriptor) } } - try fileManager.setAttributes( - [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], - ofItemAtPath: temporaryURL.path - ) - do { + try fileManager.setAttributes( + [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], + ofItemAtPath: temporaryURL.path + ) try data.withUnsafeBytes { rawBuffer in guard var pointer = rawBuffer.baseAddress else { return @@ -234,3 +236,5 @@ final class OSLoggerFileStore: ILogFileStore { } } } + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift index 9e0d6bc78..61e0f50b7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift @@ -25,6 +25,9 @@ THE SOFTWARE. */ +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + import Foundation @_implementationOnly import OneSignalKMP @@ -155,3 +158,5 @@ final class OSLoggerHttpSender: ILogHttpSender { String(body.prefix(maximumDiagnosticBodyLength)) } } + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index 7e7b335fa..c8ae20ee2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -25,6 +25,9 @@ THE SOFTWARE. */ +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + import Darwin import Foundation import OneSignalCore @@ -36,7 +39,8 @@ import UIKit /// Mutable SDK state is injected by the composition layer because /// `OneSignalUser` depends on `OneSignalOSCore`; importing it here would create /// a circular module dependency. Providers must be safe to invoke from arbitrary -/// threads, including synchronously from a crash handler. +/// threads, including synchronously from a crash handler. Construct this adapter +/// on the main thread because its static device metadata comes from `UIDevice`. final class OSLoggerPlatformProvider: ILoggerPlatformProvider { typealias IdentifierProvider = () -> String? typealias InstallIdProvider = () -> String @@ -58,6 +62,9 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let minimumFileAgeMillis: Int64 = 5_000 } + private static let processStartedAtUptime = + processStartUptime() ?? ProcessInfo.processInfo.systemUptime + private let installIdProvider: InstallIdProvider private let onesignalIdProvider: IdentifierProvider private let pushSubscriptionIdProvider: IdentifierProvider @@ -124,7 +131,10 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { } var processUptime: Int64 { - Int64(ProcessInfo.processInfo.systemUptime * 1_000) + Self.processUptimeMillis( + systemUptime: ProcessInfo.processInfo.systemUptime, + processStartUptime: Self.processStartedAtUptime + ) } var currentThreadName: String { @@ -185,4 +195,51 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { return String(cString: value) } + static func processUptimeMillis( + systemUptime: TimeInterval, + processStartUptime: TimeInterval + ) -> Int64 { + Int64(max(0, systemUptime - processStartUptime) * 1_000) + } + + private static func processStartUptime() -> TimeInterval? { + var processInfo = kinfo_proc() + var processInfoSize = MemoryLayout.stride + var processName = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + guard sysctl( + &processName, + u_int(processName.count), + &processInfo, + &processInfoSize, + nil, + 0 + ) == 0 else { + return nil + } + + var bootTime = timeval() + var bootTimeSize = MemoryLayout.stride + var bootName = [CTL_KERN, KERN_BOOTTIME] + guard sysctl( + &bootName, + u_int(bootName.count), + &bootTime, + &bootTimeSize, + nil, + 0 + ) == 0 else { + return nil + } + + let processStart = timeInterval(processInfo.kp_proc.p_starttime) + let systemBoot = timeInterval(bootTime) + return max(0, processStart - systemBoot) + } + + private static func timeInterval(_ value: timeval) -> TimeInterval { + TimeInterval(value.tv_sec) + TimeInterval(value.tv_usec) / 1_000_000 + } + } + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 05811eb2f..9e7afbbda 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -180,16 +180,40 @@ final class OSLoggerAdaptersTests: XCTestCase { XCTAssertEqual(listener.levels, [.LL_ERROR, .LL_WARN, .LL_INFO, .LL_DEBUG]) } - func testPlatformProviderReturnsInjectedIdentifiersAndPlatformMetadata() { - let provider = OSLoggerPlatformProvider( - installIdProvider: { "install-id" }, - onesignalIdProvider: { "onesignal-id" }, - pushSubscriptionIdProvider: { "subscription-id" }, - appStateProvider: { "foreground" }, - featureFlagsProvider: { ["feature"] }, - remoteLogLevelProvider: { "warn" }, - exporterLoggingEnabledProvider: { true } + func testKmpPipelineInvokesSwiftAdapters() throws { + let listener = LoggerAdapterListener() + OneSignalLog.debug().__add(listener) + defer { OneSignalLog.debug().__remove(listener) } + let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) + let logger = OSLoggerAdapter() + let telemetry = LoggerFactory.shared.createCrashLocalTelemetry( + platformProvider: makePlatformProvider(), + fileStore: store + ) + let reporter = LoggerFactory.shared.createCrashReporter( + crashTelemetry: telemetry, + logger: logger + ) + let crash = CrashData( + threadName: "test", + exceptionType: "TestError", + exceptionMessage: "test message", + stacktrace: "test stack" + ) + + _ = try reporter.saveNonFatal(crash: crash) + + XCTAssertEqual(listener.levels, [.LL_INFO, .LL_INFO]) + XCTAssertEqual( + try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) + .filter { $0.hasSuffix(".otlp") } + .count, + 1 ) + } + + func testPlatformProviderReturnsInjectedIdentifiersAndPlatformMetadata() { + let provider = makePlatformProvider() var firstInstallId: String? var secondInstallId: String? @@ -210,6 +234,35 @@ final class OSLoggerAdaptersTests: XCTestCase { XCTAssertGreaterThanOrEqual(provider.processUptime, 0) } + func testProcessUptimeUsesProcessStartAndClampsClockMismatch() { + XCTAssertEqual( + OSLoggerPlatformProvider.processUptimeMillis( + systemUptime: 100, + processStartUptime: 95 + ), + 5_000 + ) + XCTAssertEqual( + OSLoggerPlatformProvider.processUptimeMillis( + systemUptime: 95, + processStartUptime: 100 + ), + 0 + ) + } + + private func makePlatformProvider() -> OSLoggerPlatformProvider { + OSLoggerPlatformProvider( + installIdProvider: { "install-id" }, + onesignalIdProvider: { "onesignal-id" }, + pushSubscriptionIdProvider: { "subscription-id" }, + appStateProvider: { "foreground" }, + featureFlagsProvider: { ["feature"] }, + remoteLogLevelProvider: { "warn" }, + exporterLoggingEnabledProvider: { true } + ) + } + private func makeKotlinBytes(_ bytes: [UInt8]) -> KotlinByteArray { AppleByteArrayInterop.shared.toByteArray(data: Data(bytes)) } @@ -223,7 +276,7 @@ private final class LoggerAdapterListener: NSObject, OSLogListener { } } -private final class TestLogger: ILogger { +private final class TestLogger: NSObject, ILogger { var warnings: [String] = [] func error(message: String) {} From 9a286be8477b84aabe5fc61b730ab37e3a7c4fab Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 7 Aug 2026 10:10:49 -0700 Subject: [PATCH 6/6] refactor: [SDK-4976] align logger names with Android Use matching concrete component names so the iOS and Android logger implementations can be compared directly. Co-authored-by: Cursor --- .../OneSignal.xcodeproj/project.pbxproj | 24 +++++++++---------- ...ggerFileStore.swift => FileLogStore.swift} | 8 +++---- ...{OSLoggerAdapter.swift => IOSLogger.swift} | 2 +- ...der.swift => OneSignalLogHttpSender.swift} | 14 +++++------ .../OSLoggerAdaptersTests.swift | 16 ++++++------- 5 files changed, 32 insertions(+), 32 deletions(-) rename iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/{OSLoggerFileStore.swift => FileLogStore.swift} (96%) rename iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/{OSLoggerAdapter.swift => IOSLogger.swift} (98%) rename iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/{OSLoggerHttpSender.swift => OneSignalLogHttpSender.swift} (91%) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 14fe7593e..10f512fa6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -69,9 +69,9 @@ 3C14E39F2AFAE39B006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; }; - 3C14E3B02FAE54C006ED053 /* OSLoggerAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */; }; - 3C14E3B12FAE54C006ED053 /* OSLoggerHttpSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AA2FAE54C006ED053 /* OSLoggerHttpSender.swift */; }; - 3C14E3B22FAE54C006ED053 /* OSLoggerFileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AB2FAE54C006ED053 /* OSLoggerFileStore.swift */; }; + 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */; }; + 3C14E3B12FAE54C006ED053 /* OneSignalLogHttpSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */; }; + 3C14E3B22FAE54C006ED053 /* FileLogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AB2FAE54C006ED053 /* FileLogStore.swift */; }; 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 */; }; @@ -1783,9 +1783,9 @@ DEBAAEB62A4381AE00BF2C1C /* OSInAppMessageMigrationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageMigrationController.h; sourceTree = ""; }; DEBAAEB72A4381AE00BF2C1C /* OSInAppMessageMigrationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageMigrationController.m; sourceTree = ""; }; DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalSwiftInterface.swift; sourceTree = ""; }; - 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdapter.swift; sourceTree = ""; }; - 3C14E3AA2FAE54C006ED053 /* OSLoggerHttpSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerHttpSender.swift; sourceTree = ""; }; - 3C14E3AB2FAE54C006ED053 /* OSLoggerFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerFileStore.swift; sourceTree = ""; }; + 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSLogger.swift; sourceTree = ""; }; + 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalLogHttpSender.swift; sourceTree = ""; }; + 3C14E3AB2FAE54C006ED053 /* FileLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogStore.swift; sourceTree = ""; }; 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 = ""; }; @@ -2281,9 +2281,9 @@ isa = PBXGroup; children = ( 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */, - 3C14E3AB2FAE54C006ED053 /* OSLoggerFileStore.swift */, - 3C14E3AA2FAE54C006ED053 /* OSLoggerHttpSender.swift */, - 3C14E3A92FAE54C006ED053 /* OSLoggerAdapter.swift */, + 3C14E3AB2FAE54C006ED053 /* FileLogStore.swift */, + 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */, + 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */, 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */, ); path = Logging; @@ -4419,11 +4419,11 @@ files = ( 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */, DEFB3E652BB7346D00E65DAD /* OSLiveActivities.swift in Sources */, - 3C14E3B22FAE54C006ED053 /* OSLoggerFileStore.swift in Sources */, + 3C14E3B22FAE54C006ED053 /* FileLogStore.swift in Sources */, 5BC1DE602C90B83900CA8807 /* OSConsistencyKeyEnum.swift in Sources */, - 3C14E3B12FAE54C006ED053 /* OSLoggerHttpSender.swift in Sources */, + 3C14E3B12FAE54C006ED053 /* OneSignalLogHttpSender.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, - 3C14E3B02FAE54C006ED053 /* OSLoggerAdapter.swift in Sources */, + 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/FileLogStore.swift similarity index 96% rename from iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift rename to iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/FileLogStore.swift index 5e255bc64..85bf04266 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerFileStore.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/FileLogStore.swift @@ -38,7 +38,7 @@ import OneSignalCore /// Writes are synchronous and durable because fatal handlers may terminate the /// process immediately after `save` returns. Directory scans and cleanup run on /// a utility queue to keep disk I/O off the caller. -final class OSLoggerFileStore: ILogFileStore { +final class FileLogStore: ILogFileStore { /// Complete records use `.otlp`; interrupted durable writes leave /// `.otlp.tmp` files that are safe to reap after the minimum-age gate. private static let ownedFileSuffix = ".otlp" @@ -81,7 +81,7 @@ final class OSLoggerFileStore: ILogFileStore { } catch { OneSignalLog.onesignalLog( .LL_WARN, - message: "OSLoggerFileStore listReadable failed: \(error.localizedDescription)" + message: "FileLogStore listReadable failed: \(error.localizedDescription)" ) completionHandler([], nil) } @@ -103,7 +103,7 @@ final class OSLoggerFileStore: ILogFileStore { } catch { OneSignalLog.onesignalLog( .LL_WARN, - message: "OSLoggerFileStore delete failed: \(error.localizedDescription)" + message: "FileLogStore delete failed: \(error.localizedDescription)" ) } completionHandler(nil) @@ -128,7 +128,7 @@ final class OSLoggerFileStore: ILogFileStore { } catch { OneSignalLog.onesignalLog( .LL_WARN, - message: "OSLoggerFileStore cleanup failed: \(error.localizedDescription)" + message: "FileLogStore cleanup failed: \(error.localizedDescription)" ) } completionHandler(KotlinInt(int: Int32(deleted)), nil) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/IOSLogger.swift similarity index 98% rename from iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift rename to iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/IOSLogger.swift index 1b21f4e07..682cf1360 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerAdapter.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/IOSLogger.swift @@ -34,7 +34,7 @@ import OneSignalCore /// Routes diagnostic messages from the shared logger through the iOS SDK's /// existing logging pipeline. -final class OSLoggerAdapter: ILogger { +final class IOSLogger: ILogger { func error(message: String) { OneSignalLog.onesignalLog(.LL_ERROR, message: message) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OneSignalLogHttpSender.swift similarity index 91% rename from iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift rename to iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OneSignalLogHttpSender.swift index 61e0f50b7..1d3bc5814 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerHttpSender.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OneSignalLogHttpSender.swift @@ -32,7 +32,7 @@ import Foundation @_implementationOnly import OneSignalKMP /// Sends the KMP logger's encoded OTLP requests using the native URL loading system. -final class OSLoggerHttpSender: ILogHttpSender { +final class OneSignalLogHttpSender: ILogHttpSender { private static let requestTimeout: TimeInterval = 10 private static let transportFailureStatusCode: Int32 = -1 private static let maximumDiagnosticBodyLength = 500 @@ -49,8 +49,8 @@ final class OSLoggerHttpSender: ILogHttpSender { ) -> Void init( - session: URLSession = OSLoggerHttpSender.defaultSession, - logger: ILogger = OSLoggerAdapter(), + session: URLSession = OneSignalLogHttpSender.defaultSession, + logger: ILogger = IOSLogger(), isDiagnosticsEnabled: @escaping () -> Bool = { false } ) { self.requestSender = { request, completion in @@ -62,7 +62,7 @@ final class OSLoggerHttpSender: ILogHttpSender { init( requestSender: @escaping RequestSender, - logger: ILogger = OSLoggerAdapter(), + logger: ILogger = IOSLogger(), isDiagnosticsEnabled: @escaping () -> Bool = { false } ) { self.requestSender = requestSender @@ -100,7 +100,7 @@ final class OSLoggerHttpSender: ILogHttpSender { if let error = error { if self.isDiagnosticsEnabled() { self.logger.warn( - message: "OSLoggerHttpSender: POST \(request.url) failed: \(error.localizedDescription)" + message: "OneSignalLogHttpSender: POST \(request.url) failed: \(error.localizedDescription)" ) } completionHandler( @@ -131,12 +131,12 @@ final class OSLoggerHttpSender: ILogHttpSender { if self.isDiagnosticsEnabled() { if success { self.logger.debug( - message: "OSLoggerHttpSender: POST \(request.url) -> \(response.statusCode) OK " + message: "OneSignalLogHttpSender: POST \(request.url) -> \(response.statusCode) OK " + "(\(request.body.size)B)" ) } else { self.logger.warn( - message: "OSLoggerHttpSender: POST \(request.url) -> \(response.statusCode) " + message: "OneSignalLogHttpSender: POST \(request.url) -> \(response.statusCode) " + "(ct=\(request.contentType), \(request.body.size)B) " + "body=\(responseBody.map(Self.truncatedDiagnosticBody) ?? "nil")" ) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 9e7afbbda..9c856d1e0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -45,7 +45,7 @@ final class OSLoggerAdaptersTests: XCTestCase { } func testFileStoreSynchronouslySavesAndListsPayload() throws { - let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) + let store = FileLogStore(rootPath: temporaryDirectory.path) let payload = makeKotlinBytes([1, 2, 3, 255]) XCTAssertTrue(store.save(bytes: payload)) @@ -65,7 +65,7 @@ final class OSLoggerAdaptersTests: XCTestCase { } func testFileStoreDeletesOnlyInterruptedTemporaryWrites() throws { - let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) + let store = FileLogStore(rootPath: temporaryDirectory.path) XCTAssertTrue(store.save(bytes: makeKotlinBytes([1]))) let temporaryURL = temporaryDirectory.appendingPathComponent("interrupted.otlp.tmp") @@ -92,14 +92,14 @@ final class OSLoggerAdaptersTests: XCTestCase { } func testFileStoreRejectsEmptyPayload() { - let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) + let store = FileLogStore(rootPath: temporaryDirectory.path) XCTAssertFalse(store.save(bytes: makeKotlinBytes([]))) } func testHttpSenderPostsEncodedBytesAndPassesHeaders() { let sent = expectation(description: "sends payload") - let sender = OSLoggerHttpSender { request, completion in + let sender = OneSignalLogHttpSender { request, completion in XCTAssertEqual(request.httpMethod, "POST") XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/custom") XCTAssertEqual(request.value(forHTTPHeaderField: "SDK-Version"), "onesignal/ios/test") @@ -134,7 +134,7 @@ final class OSLoggerAdaptersTests: XCTestCase { func testHttpSenderReturnsAndLogsFailureBodyWhenDiagnosticsEnabled() { let logger = TestLogger() - let sender = OSLoggerHttpSender( + let sender = OneSignalLogHttpSender( requestSender: { request, completion in let response = HTTPURLResponse( url: request.url!, @@ -171,7 +171,7 @@ final class OSLoggerAdaptersTests: XCTestCase { OneSignalLog.debug().__add(listener) defer { OneSignalLog.debug().__remove(listener) } - let logger = OSLoggerAdapter() + let logger = IOSLogger() logger.error(message: "error") logger.warn(message: "warn") logger.info(message: "info") @@ -184,8 +184,8 @@ final class OSLoggerAdaptersTests: XCTestCase { let listener = LoggerAdapterListener() OneSignalLog.debug().__add(listener) defer { OneSignalLog.debug().__remove(listener) } - let store = OSLoggerFileStore(rootPath: temporaryDirectory.path) - let logger = OSLoggerAdapter() + let store = FileLogStore(rootPath: temporaryDirectory.path) + let logger = IOSLogger() let telemetry = LoggerFactory.shared.createCrashLocalTelemetry( platformProvider: makePlatformProvider(), fileStore: store