diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6a967d269..387e02d09c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -401,6 +401,38 @@ jobs: if: failure() run: ./scripts/ci-diagnostics.sh + # This will be replaced once #6945 is merged. + cocoalumberjack-integration-unit-tests: + name: SentryCocoaLumberjack Unit Tests + if: needs.files-changed.outputs.run_unit_tests_for_prs == 'true' + needs: files-changed + runs-on: macos-15 + steps: + - uses: actions/checkout@v6 + + - name: Select Xcode + run: ./scripts/ci-select-xcode.sh 16.4 + + - name: Setup local sentry-cocoa dependency + working-directory: 3rd-party-integrations/SentryCocoaLumberjack + run: swift package edit sentry-cocoa --path ../.. + + - name: Run CocoaLumberjack tests + working-directory: 3rd-party-integrations/SentryCocoaLumberjack + run: swift test + + - name: Archiving Raw Logs + uses: actions/upload-artifact@v5 + if: ${{ failure() || cancelled() }} + with: + name: raw-output-cocoalumberjack-integration + path: | + 3rd-party-integrations/SentryCocoaLumberjack/.build/**/*.log + + - name: Run CI Diagnostics + if: failure() + run: ./scripts/ci-diagnostics.sh + # This check validates that either all unit tests passed or were skipped, which allows us # to make unit tests a required check with only running the unit tests when required. # So, we don't have to run unit tests, for example, for Changelog or ReadMe changes. diff --git a/3rd-party-integrations/SentryCocoaLumberjack/.gitignore b/3rd-party-integrations/SentryCocoaLumberjack/.gitignore new file mode 100644 index 0000000000..cffaa2c670 --- /dev/null +++ b/3rd-party-integrations/SentryCocoaLumberjack/.gitignore @@ -0,0 +1,100 @@ +# --- macOS --- + +# General +.DS_Store +__MACOSX/ +.AppleDouble +.LSOverride +Icon[] + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# --- Swift --- + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# --- Xcode --- + +## User settings +xcuserdata/ + +# Archive +*.xcarchive diff --git a/3rd-party-integrations/SentryCocoaLumberjack/Package.swift b/3rd-party-integrations/SentryCocoaLumberjack/Package.swift new file mode 100644 index 0000000000..ea58843b16 --- /dev/null +++ b/3rd-party-integrations/SentryCocoaLumberjack/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:6.0 +import PackageDescription + +let package = Package( + name: "SentryCocoaLumberjack", + platforms: [.iOS(.v15), .macOS(.v10_14), .tvOS(.v15), .watchOS(.v8), .visionOS(.v1)], + products: [ + .library( + name: "SentryCocoaLumberjack", + targets: ["SentryCocoaLumberjack"] + ) + ], + dependencies: [ + .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", from: "3.8.0"), + .package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.0.0") + ], + targets: [ + .target( + name: "SentryCocoaLumberjack", + dependencies: [ + .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), + .product(name: "Sentry", package: "sentry-cocoa") + ] + ), + .testTarget( + name: "SentryCocoaLumberjackTests", + dependencies: [ + "SentryCocoaLumberjack", + .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), + .product(name: "Sentry", package: "sentry-cocoa") + ] + ) + ] +) diff --git a/3rd-party-integrations/SentryCocoaLumberjack/README.md b/3rd-party-integrations/SentryCocoaLumberjack/README.md new file mode 100644 index 0000000000..1fb196df4c --- /dev/null +++ b/3rd-party-integrations/SentryCocoaLumberjack/README.md @@ -0,0 +1,87 @@ +# Sentry CocoaLumberjack Integration + +A [CocoaLumberjack](https://github.com/CocoaLumberjack/CocoaLumberjack) logger that forwards log entries to Sentry's structured logging system, automatically capturing application logs with full context including source location, thread information, and log levels. + +> [!NOTE] +> This repo is a mirror of [github.com/getsentry/sentry-cocoa](https://github.com/getsentry/sentry-cocoa). The source code lives in `3rd-party-integrations/SentryCocoaLumberjack/`. This allows users to import only what they need via SPM while keeping all integration code in the main repository. + +## Installation + +### Swift Package Manager + +Add the following dependencies to your `Package.swift` or Xcode package dependencies: + +```swift +dependencies: [ + .package(url: "https://github.com/getsentry/sentry-cocoa-cocoalumberjack", from: "9.0.0") +] +``` + +## Quick Start + +```swift +import Sentry +import SentryCocoaLumberjack +import CocoaLumberjackSwift + +SentrySDK.start { options in + options.dsn = "YOUR_DSN" + options.enableLogs = true +} + +DDLog.add(SentryCocoaLumberjackLogger(), with: .info) + +DDLogInfo("User logged in") +DDLogError("Payment failed") +DDLogWarn("API rate limit approaching") +DDLogDebug("Processing request") +DDLogVerbose("Detailed trace information") +``` + +## Configuration + +### Log Level Threshold + +Use CocoaLumberjack's built-in filtering API when adding the logger to set the minimum log level for messages to be sent to Sentry: + +```swift +// Only send error logs to Sentry +DDLog.add(SentryCocoaLumberjackLogger(), with: .error) +``` + +## Log Level Mapping + +CocoaLumberjack log levels are automatically mapped to Sentry log levels: + +| CocoaLumberjack Level | Sentry Log Level | +| --------------------- | ---------------- | +| `.error` | `.error` | +| `.warning` | `.warn` | +| `.info` | `.info` | +| `.debug` | `.debug` | +| `.verbose` | `.trace` | + +## Automatic Attributes + +The logger automatically includes the following attributes with every log entry: + +- `sentry.origin`: `"auto.logging.cocoalumberjack"` +- `cocoalumberjack.level`: The original CocoaLumberjack log level (error, warning, info, debug, verbose) +- `cocoalumberjack.file`: The source file name +- `cocoalumberjack.function`: The function name +- `cocoalumberjack.line`: The line number +- `cocoalumberjack.context`: The log context (integer) +- `cocoalumberjack.timestamp`: The log timestamp +- `cocoalumberjack.threadID`: The thread ID +- `cocoalumberjack.threadName`: The thread name (if available) +- `cocoalumberjack.queueLabel`: The dispatch queue label (if available) + +## Documentation + +- [Sentry Cocoa SDK Documentation](https://docs.sentry.io/platforms/apple/) +- [Sentry Logs Documentation](https://docs.sentry.io/platforms/apple/logs/) +- [CocoaLumberjack Repo](https://github.com/CocoaLumberjack/CocoaLumberjack) + +## License + +This integration follows the same license as the Sentry Cocoa SDK. See the [LICENSE](https://github.com/getsentry/sentry-cocoa/blob/main/LICENSE.md) file for details. diff --git a/3rd-party-integrations/SentryCocoaLumberjack/Sources/SentryCocoaLumberjackLogger.swift b/3rd-party-integrations/SentryCocoaLumberjack/Sources/SentryCocoaLumberjackLogger.swift new file mode 100644 index 0000000000..babec8d09c --- /dev/null +++ b/3rd-party-integrations/SentryCocoaLumberjack/Sources/SentryCocoaLumberjackLogger.swift @@ -0,0 +1,114 @@ +import CocoaLumberjackSwift +import Sentry + +/// A CocoaLumberjack logger that forwards log entries to Sentry's structured logging system. +/// +/// `SentryCocoaLumberjackLogger` implements CocoaLumberjack's `DDAbstractLogger` protocol, allowing you +/// to integrate Sentry's structured logging capabilities with CocoaLumberjack. This enables you to capture +/// application logs from CocoaLumberjack and send them to Sentry for analysis and monitoring. +/// +/// ## Level Filtering +/// Use CocoaLumberjack's built-in filtering API when adding the logger: +/// ```swift +/// DDLog.add(SentryCocoaLumberjackLogger(), with: .info) +/// ``` +/// This ensures only logs at or above the specified level are sent to Sentry. +/// +/// ## Level Mapping +/// CocoaLumberjack log levels are mapped to Sentry log levels: +/// - `.error` → `.error` +/// - `.warning` → `.warn` +/// - `.info` → `.info` +/// - `.debug` → `.debug` +/// - `.verbose` → `.trace` +/// +/// ## Usage +/// ```swift +/// import CocoaLumberjackSwift +/// import Sentry +/// import SentryCocoaLumberjack +/// +/// // Initialize Sentry SDK +/// SentrySDK.start { options in +/// options.dsn = "YOUR_DSN" +/// } +/// +/// // Add SentryCocoaLumberjackLogger to CocoaLumberjack +/// // Only logs at .info level and above will be sent to Sentry +/// DDLog.add(SentryCocoaLumberjackLogger(), with: .info) +/// +/// // Use CocoaLumberjack as usual +/// DDLogInfo("User logged in") +/// DDLogError("Payment failed") +/// ``` +/// +/// - Note: Sentry Logs is currently in Beta. See the [Sentry Logs Documentation](https://docs.sentry.io/platforms/apple/logs/). +/// - Warning: This logger requires Sentry SDK to be initialized before use. +public class SentryCocoaLumberjackLogger: DDAbstractLogger { + + /// Creates a new SentryCocoaLumberjackLogger instance. + /// + /// - Note: Make sure to initialize the Sentry SDK before creating this logger. + /// Use `DDLog.add(_:with:)` to configure log level filtering. + public override init() { + super.init() + } + + /// Logs a message from CocoaLumberjack to Sentry. + /// + /// - Parameter logMessage: The log message from CocoaLumberjack containing the message, level, and metadata. + public override func log(message logMessage: DDLogMessage) { + var attributes: [String: Any] = [:] + attributes["sentry.origin"] = "auto.logging.cocoalumberjack" + + attributes["cocoalumberjack.level"] = logFlagToString(logMessage.flag) + attributes["cocoalumberjack.file"] = logMessage.file + attributes["cocoalumberjack.function"] = logMessage.function ?? "" + attributes["cocoalumberjack.line"] = String(logMessage.line) + attributes["cocoalumberjack.context"] = String(logMessage.context) + attributes["cocoalumberjack.timestamp"] = logMessage.timestamp.timeIntervalSince1970 + attributes["cocoalumberjack.threadID"] = String(logMessage.threadID) + + if let threadName = logMessage.threadName, !threadName.isEmpty { + attributes["cocoalumberjack.threadName"] = threadName + } + + if !logMessage.queueLabel.isEmpty { + attributes["cocoalumberjack.queueLabel"] = logMessage.queueLabel + } + + forwardToSentry(message: logMessage.message, flag: logMessage.flag, attributes: attributes) + } + + private func forwardToSentry(message: String, flag: DDLogFlag, attributes: [String: Any]) { + if flag.contains(.error) { + SentrySDK.logger.error(message, attributes: attributes) + } else if flag.contains(.warning) { + SentrySDK.logger.warn(message, attributes: attributes) + } else if flag.contains(.info) { + SentrySDK.logger.info(message, attributes: attributes) + } else if flag.contains(.debug) { + SentrySDK.logger.debug(message, attributes: attributes) + } else if flag.contains(.verbose) { + SentrySDK.logger.trace(message, attributes: attributes) + } else { + SentrySDK.logger.info(message, attributes: attributes) + } + } + + private func logFlagToString(_ flag: DDLogFlag) -> String { + if flag.contains(.error) { + return "error" + } else if flag.contains(.warning) { + return "warning" + } else if flag.contains(.info) { + return "info" + } else if flag.contains(.debug) { + return "debug" + } else if flag.contains(.verbose) { + return "verbose" + } else { + return "unknown" + } + } +} diff --git a/3rd-party-integrations/SentryCocoaLumberjack/Tests/SentryCocoaLumberjackLoggerTests.swift b/3rd-party-integrations/SentryCocoaLumberjack/Tests/SentryCocoaLumberjackLoggerTests.swift new file mode 100644 index 0000000000..792dde5bf9 --- /dev/null +++ b/3rd-party-integrations/SentryCocoaLumberjack/Tests/SentryCocoaLumberjackLoggerTests.swift @@ -0,0 +1,349 @@ +import CocoaLumberjackSwift +import Sentry +@_spi(Private) @testable import SentryCocoaLumberjack +import XCTest + +// swiftlint:disable cyclomatic_complexity file_length type_body_length + +final class SentryCocoaLumberjackLoggerTests: XCTestCase { + + private var capturedLogs: [SentryLog] = [] + + override func setUp() { + super.setUp() + capturedLogs = [] + SentrySDK.start { options in + options.dsn = "https://test@test.ingest.sentry.io/123456" + options.enableLogs = true + options.beforeSendLog = { [weak self] log in + self?.capturedLogs.append(log) + return nil + } + } + } + + override func tearDown() { + super.tearDown() + DDLog.removeAllLoggers() + SentrySDK.close() + capturedLogs = [] + } + + private func getSut() -> SentryCocoaLumberjackLogger { + return SentryCocoaLumberjackLogger() + } + + // MARK: - Basic Logging Tests + + func testLog_WithErrorLevel() throws { + let sut = getSut() + let logMessage = DDLogMessage( + format: "Test error message", + formatted: "Test error message", + level: .error, + flag: .error, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 100, + tag: nil, + options: [], + timestamp: Date() + ) + + sut.log(message: logMessage) + + try assertLogCaptured( + .error, + "Test error message", + [ + "sentry.origin": SentryLog.Attribute(string: "auto.logging.cocoalumberjack"), + "cocoalumberjack.level": SentryLog.Attribute(string: "error"), + "cocoalumberjack.file": SentryLog.Attribute(string: "TestFile.swift"), + "cocoalumberjack.function": SentryLog.Attribute(string: "testFunction"), + "cocoalumberjack.line": SentryLog.Attribute(string: "100"), + "cocoalumberjack.context": SentryLog.Attribute(string: "0") + ] + ) + } + + func testLog_WithWarningLevel() throws { + let sut = getSut() + let logMessage = DDLogMessage( + format: "Test warning message", + formatted: "Test warning message", + level: .warning, + flag: .warning, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 75, + tag: nil, + options: [], + timestamp: Date() + ) + + sut.log(message: logMessage) + + try assertLogCaptured( + .warn, + "Test warning message", + [ + "sentry.origin": SentryLog.Attribute(string: "auto.logging.cocoalumberjack"), + "cocoalumberjack.level": SentryLog.Attribute(string: "warning"), + "cocoalumberjack.file": SentryLog.Attribute(string: "TestFile.swift"), + "cocoalumberjack.function": SentryLog.Attribute(string: "testFunction"), + "cocoalumberjack.line": SentryLog.Attribute(string: "75"), + "cocoalumberjack.context": SentryLog.Attribute(string: "0") + ] + ) + } + + func testLog_WithInfoLevel() throws { + let sut = getSut() + let logMessage = DDLogMessage( + format: "Test info message", + formatted: "Test info message", + level: .info, + flag: .info, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 42, + tag: nil, + options: [], + timestamp: Date() + ) + + sut.log(message: logMessage) + + try assertLogCaptured( + .info, + "Test info message", + [ + "sentry.origin": SentryLog.Attribute(string: "auto.logging.cocoalumberjack"), + "cocoalumberjack.level": SentryLog.Attribute(string: "info"), + "cocoalumberjack.file": SentryLog.Attribute(string: "TestFile.swift"), + "cocoalumberjack.function": SentryLog.Attribute(string: "testFunction"), + "cocoalumberjack.line": SentryLog.Attribute(string: "42"), + "cocoalumberjack.context": SentryLog.Attribute(string: "0") + ] + ) + } + + func testLog_WithDebugLevel() throws { + let sut = getSut() + let logMessage = DDLogMessage( + format: "Test debug message", + formatted: "Test debug message", + level: .debug, + flag: .debug, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 50, + tag: nil, + options: [], + timestamp: Date() + ) + + sut.log(message: logMessage) + + try assertLogCaptured( + .debug, + "Test debug message", + [ + "sentry.origin": SentryLog.Attribute(string: "auto.logging.cocoalumberjack"), + "cocoalumberjack.level": SentryLog.Attribute(string: "debug"), + "cocoalumberjack.file": SentryLog.Attribute(string: "TestFile.swift"), + "cocoalumberjack.function": SentryLog.Attribute(string: "testFunction"), + "cocoalumberjack.line": SentryLog.Attribute(string: "50"), + "cocoalumberjack.context": SentryLog.Attribute(string: "0") + ] + ) + } + + func testLog_WithVerboseLevel() throws { + let sut = getSut() + let logMessage = DDLogMessage( + format: "Test verbose message", + formatted: "Test verbose message", + level: .verbose, + flag: .verbose, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 10, + tag: nil, + options: [], + timestamp: Date() + ) + + sut.log(message: logMessage) + + try assertLogCaptured( + .trace, + "Test verbose message", + [ + "sentry.origin": SentryLog.Attribute(string: "auto.logging.cocoalumberjack"), + "cocoalumberjack.level": SentryLog.Attribute(string: "verbose"), + "cocoalumberjack.file": SentryLog.Attribute(string: "TestFile.swift"), + "cocoalumberjack.function": SentryLog.Attribute(string: "testFunction"), + "cocoalumberjack.line": SentryLog.Attribute(string: "10"), + "cocoalumberjack.context": SentryLog.Attribute(string: "0") + ] + ) + } + + // MARK: - Metadata Tests + + func testLog_WithThreadName() throws { + let sut = getSut() + let logMessage = DDLogMessage( + format: "Test with thread name", + formatted: "Test with thread name", + level: .info, + flag: .info, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 20, + tag: nil, + options: [], + timestamp: Date() + ) + + // Thread name is automatically captured by DDLogMessage + sut.log(message: logMessage) + + XCTAssertEqual(capturedLogs.count, 1, "Expected exactly one log to be captured") + + let capturedLog = try XCTUnwrap(capturedLogs.last) + XCTAssertEqual(capturedLog.level, .info) + XCTAssertEqual(capturedLog.body, "Test with thread name") + + // Verify thread ID is present + XCTAssertNotNil(capturedLog.attributes["cocoalumberjack.threadID"]) + } + + func testLog_WithContext() throws { + let sut = getSut() + let customContext = 12_345 + let logMessage = DDLogMessage( + format: "Test with context", + formatted: "Test with context", + level: .info, + flag: .info, + context: customContext, + file: "TestFile.swift", + function: "testFunction", + line: 30, + tag: nil, + options: [], + timestamp: Date() + ) + + sut.log(message: logMessage) + + try assertLogCaptured( + .info, + "Test with context", + [ + "sentry.origin": SentryLog.Attribute(string: "auto.logging.cocoalumberjack"), + "cocoalumberjack.level": SentryLog.Attribute(string: "info"), + "cocoalumberjack.file": SentryLog.Attribute(string: "TestFile.swift"), + "cocoalumberjack.function": SentryLog.Attribute(string: "testFunction"), + "cocoalumberjack.line": SentryLog.Attribute(string: "30"), + "cocoalumberjack.context": SentryLog.Attribute(string: "12345") + ] + ) + } + + func testLog_WithTimestamp() throws { + let sut = getSut() + let customTimestamp = Date(timeIntervalSince1970: 1_234_567_890.123) + let logMessage = DDLogMessage( + format: "Test with timestamp", + formatted: "Test with timestamp", + level: .info, + flag: .info, + context: 0, + file: "TestFile.swift", + function: "testFunction", + line: 40, + tag: nil, + options: [], + timestamp: customTimestamp + ) + + sut.log(message: logMessage) + + XCTAssertEqual(capturedLogs.count, 1, "Expected exactly one log to be captured") + + let capturedLog = try XCTUnwrap(capturedLogs.last) + XCTAssertEqual(capturedLog.level, .info) + XCTAssertEqual(capturedLog.body, "Test with timestamp") + + // Verify timestamp is captured + let timestampAttribute = try XCTUnwrap(capturedLog.attributes["cocoalumberjack.timestamp"], "Missing cocoalumberjack.timestamp attribute") + + XCTAssertEqual(timestampAttribute.type, "double") + let timestampValue = try XCTUnwrap(timestampAttribute.value as? Double, "Expected cocoalumberjack.timestamp to be a Double") + XCTAssertEqual(timestampValue, 1_234_567_890.123, accuracy: 0.001) + } + + // MARK: - Helper Methods + + private func assertLogCaptured( + _ expectedLevel: SentryLog.Level, + _ expectedBody: String, + _ expectedAttributes: [String: SentryLog.Attribute], + file: StaticString = #filePath, + line: UInt = #line + ) throws { + let capturedLog = getLastCapturedLog(file: file, line: line) + + XCTAssertEqual(capturedLog.level, expectedLevel, "Log level mismatch", file: file, line: line) + XCTAssertEqual(capturedLog.body, expectedBody, "Log body mismatch", file: file, line: line) + + // Only verify the user-provided attributes, not the auto-enriched ones + for (key, expectedAttribute) in expectedAttributes { + let actualAttribute = try XCTUnwrap(capturedLog.attributes[key], "Missing attribute key: \(key)", file: file, line: line) + + XCTAssertEqual(actualAttribute.type, expectedAttribute.type, "Attribute type mismatch for key: \(key)", file: file, line: line) + + // Compare values based on type + switch expectedAttribute.type { + case "string": + let expectedValue = try XCTUnwrap(expectedAttribute.value as? String, "Expected string value for expected attribute key: \(key)", file: file, line: line) + let actualValue = try XCTUnwrap(actualAttribute.value as? String, "Expected string value for actual attribute key: \(key)", file: file, line: line) + XCTAssertEqual(actualValue, expectedValue, "String attribute value mismatch for key: \(key)", file: file, line: line) + case "boolean": + let expectedValue = try XCTUnwrap(expectedAttribute.value as? Bool, "Expected boolean value for expected attribute key: \(key)", file: file, line: line) + let actualValue = try XCTUnwrap(actualAttribute.value as? Bool, "Expected boolean value for actual attribute key: \(key)", file: file, line: line) + XCTAssertEqual(actualValue, expectedValue, "Boolean attribute value mismatch for key: \(key)", file: file, line: line) + case "integer": + let expectedValue = try XCTUnwrap(expectedAttribute.value as? Int, "Expected integer value for expected attribute key: \(key)", file: file, line: line) + let actualValue = try XCTUnwrap(actualAttribute.value as? Int, "Expected integer value for actual attribute key: \(key)", file: file, line: line) + XCTAssertEqual(actualValue, expectedValue, "Integer attribute value mismatch for key: \(key)", file: file, line: line) + case "double": + let expectedValue = try XCTUnwrap(expectedAttribute.value as? Double, "Expected double value for expected attribute key: \(key)", file: file, line: line) + let actualValue = try XCTUnwrap(actualAttribute.value as? Double, "Expected double value for actual attribute key: \(key)", file: file, line: line) + XCTAssertEqual(actualValue, expectedValue, accuracy: 0.000001, "Double attribute value mismatch for key: \(key)", file: file, line: line) + default: + XCTFail("Unknown attribute type for key: \(key). Type: \(expectedAttribute.type)", file: file, line: line) + } + } + } + + private func getLastCapturedLog(file: StaticString = #filePath, line: UInt = #line) -> SentryLog { + XCTAssertEqual(capturedLogs.count, 1, "Expected exactly one log to be captured", file: file, line: line) + guard let lastLog = capturedLogs.last else { + XCTFail("No logs captured", file: file, line: line) + return SentryLog(level: .info, body: "", attributes: [:]) + } + return lastLog + } +} + +// swiftlint:enable cyclomatic_complexity file_length type_body_length diff --git a/Utils/VersionBump/main.swift b/Utils/VersionBump/main.swift index fe6ad54076..53adb24abf 100644 --- a/Utils/VersionBump/main.swift +++ b/Utils/VersionBump/main.swift @@ -42,7 +42,8 @@ let files = [ "./SentrySwiftUI.podspec", "./Sources/Sentry/SentryMeta.m", "./Tests/HybridSDKTest/HybridPod.podspec", - "./3rd-party-integrations/SentrySwiftLog/Package.swift" + "./3rd-party-integrations/SentrySwiftLog/Package.swift", + "./3rd-party-integrations/SentryCocoaLumberjack/Package.swift" ] // Files that only accept the format x.x.x in order to release an app using the framework.