From caf5d0fa737c6695d5ebc15324fd2a891bc3626b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 13 Nov 2025 14:56:00 +0100 Subject: [PATCH 1/5] Fix: Batch max 1000 logs --- Sources/Swift/Tools/SentryLogBatcher.swift | 17 +++++++++-- Tests/SentryTests/SentryLogBatcherTests.swift | 30 +++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 72d52fbe1be..949dcc6c278 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -12,6 +12,7 @@ import Foundation private let options: Options private let flushTimeout: TimeInterval + private let maxLogCount: Int private let maxBufferSizeBytes: Int private let dispatchQueue: SentryDispatchQueueWrapper @@ -24,7 +25,7 @@ import Foundation private weak var delegate: SentryLogBatcherDelegate? - /// Convenience initializer with default flush timeout and buffer size. + /// Convenience initializer with default flush timeout, max log count (1000), and buffer size. /// - Parameters: /// - options: The Sentry configuration options /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state @@ -32,6 +33,8 @@ import Foundation /// /// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety. /// Passing a concurrent queue will result in undefined behavior and potential data races. + /// + /// - Note: This initializer sets `maxLogCount` to 1000, ensuring we never send more than 1000 logs in a single batch. @_spi(Private) public convenience init( options: Options, dispatchQueue: SentryDispatchQueueWrapper, @@ -40,7 +43,8 @@ import Foundation self.init( options: options, flushTimeout: 5, - maxBufferSizeBytes: 1_024 * 1_024, // 1MB + maxLogCount: 1_000, // Maximum 1000 logs per batch + maxBufferSizeBytes: 1_024 * 1_024, // 1MB buffer size dispatchQueue: dispatchQueue, delegate: delegate ) @@ -50,21 +54,27 @@ import Foundation /// - Parameters: /// - options: The Sentry configuration options /// - flushTimeout: The timeout interval after which buffered logs will be flushed + /// - maxLogCount: The maximum number of logs to batch before triggering an immediate flush (default: 1000) /// - maxBufferSizeBytes: The maximum buffer size in bytes before triggering an immediate flush /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state /// - delegate: The delegate to handle captured log batches /// /// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety. /// Passing a concurrent queue will result in undefined behavior and potential data races. + /// + /// - Note: Logs are flushed when either `maxLogCount` (1000) or `maxBufferSizeBytes` limit is reached, + /// ensuring we never send more than 1000 logs in a single batch. @_spi(Private) public init( options: Options, flushTimeout: TimeInterval, + maxLogCount: Int, maxBufferSizeBytes: Int, dispatchQueue: SentryDispatchQueueWrapper, delegate: SentryLogBatcherDelegate ) { self.options = options self.flushTimeout = flushTimeout + self.maxLogCount = maxLogCount self.maxBufferSizeBytes = maxBufferSizeBytes self.dispatchQueue = dispatchQueue self.delegate = delegate @@ -189,7 +199,8 @@ import Foundation encodedLogs.append(encodedLog) encodedLogsSize += encodedLog.count - if encodedLogsSize >= maxBufferSizeBytes { + // Flush when we reach max log count or max buffer size + if encodedLogs.count >= maxLogCount || encodedLogsSize >= maxBufferSizeBytes { performCaptureLogs() } else if encodedLogsWereEmpty && timerWorkItem == nil { startTimer() diff --git a/Tests/SentryTests/SentryLogBatcherTests.swift b/Tests/SentryTests/SentryLogBatcherTests.swift index bc315004f92..962eba5238d 100644 --- a/Tests/SentryTests/SentryLogBatcherTests.swift +++ b/Tests/SentryTests/SentryLogBatcherTests.swift @@ -24,7 +24,8 @@ final class SentryLogBatcherTests: XCTestCase { sut = SentryLogBatcher( options: options, flushTimeout: 0.1, // Very small timeout for testing - maxBufferSizeBytes: 800, // byte limit for testing (log with attributes ~390 bytes) + maxLogCount: 10, // Maximum 10 logs per batch + maxBufferSizeBytes: 8_000, // byte limit for testing (log with attributes ~390 bytes) dispatchQueue: testDispatchQueue, delegate: testDelegate ) @@ -69,7 +70,7 @@ final class SentryLogBatcherTests: XCTestCase { func testBufferReachesMaxSize_FlushesImmediately() throws { // Arrange - let largeLogBody = String(repeating: "A", count: 600) // Larger than 500 byte limit + let largeLogBody = String(repeating: "A", count: 8_000) // Larger than 8000 byte limit let largeLog = createTestLog(body: largeLogBody) // Act @@ -84,6 +85,27 @@ final class SentryLogBatcherTests: XCTestCase { XCTAssertEqual(capturedLogs[0].body, largeLogBody) } + // MARK: - Max Log Count Tests + + func testMaxLogCount_FlushesWhenReached() throws { + // Act - Add exactly maxLogCount logs + for i in 0..<9 { + let log = createTestLog(body: "Log \(i + 1)") + sut.addLog(log, scope: scope) + } + + XCTAssertEqual(testDelegate.captureLogsDataInvocations.count, 0) + + let log = createTestLog(body: "Log \(10)") // Reached 10 max logs limit + sut.addLog(log, scope: scope) + + // Assert - Should have flushed once when reaching maxLogCount + XCTAssertEqual(testDelegate.captureLogsDataInvocations.count, 1) + + let capturedLogs = testDelegate.getCapturedLogs() + XCTAssertEqual(capturedLogs.count, 10, "Should have captured exactly \(10) logs") + } + // MARK: - Timeout Tests func testTimeout_FlushesAfterDelay() throws { @@ -185,7 +207,7 @@ final class SentryLogBatcherTests: XCTestCase { func testScheduledFlushAfterBufferAlreadyFlushed_DoesNothing() throws { // Arrange - let largeLogBody = String(repeating: "B", count: 300) + let largeLogBody = String(repeating: "B", count: 4_000) let log1 = createTestLog(body: largeLogBody) let log2 = createTestLog(body: largeLogBody) @@ -235,6 +257,7 @@ final class SentryLogBatcherTests: XCTestCase { let sutWithRealQueue = SentryLogBatcher( options: options, flushTimeout: 5, + maxLogCount: 1_000, // Maximum 1000 logs per batch maxBufferSizeBytes: 10_000, dispatchQueue: SentryDispatchQueueWrapper(), delegate: testDelegate @@ -264,6 +287,7 @@ final class SentryLogBatcherTests: XCTestCase { let sutWithRealQueue = SentryLogBatcher( options: options, flushTimeout: 0.2, + maxLogCount: 1_000, // Maximum 1000 logs per batch maxBufferSizeBytes: 10_000, dispatchQueue: SentryDispatchQueueWrapper(), delegate: testDelegate From 7cd7084fe04df8510c17f1fe32c59c6f273acfa5 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 13 Nov 2025 15:09:08 +0100 Subject: [PATCH 2/5] add cl entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e3fa3329b0..1699a2bb36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - [HTTP Client errors](https://docs.sentry.io/platforms/apple/guides/ios/configuration/http-client-errors/) now mark sessions as errored (#6633) +### Fixes + +- Limit log batching to maximum 1000 logs per batch (#6768) + ## 9.0.0-alpha.1 ### Breaking Changes From 80f85271c2024bbe73dbc91d99bf5fd0be5206ab Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 13 Nov 2025 15:19:33 +0100 Subject: [PATCH 3/5] update documentation --- Sources/Swift/Tools/SentryLogBatcher.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 949dcc6c278..4f5ef7b99c5 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -34,7 +34,7 @@ import Foundation /// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety. /// Passing a concurrent queue will result in undefined behavior and potential data races. /// - /// - Note: This initializer sets `maxLogCount` to 1000, ensuring we never send more than 1000 logs in a single batch. + /// - Note: This initializer sets `maxLogCount` to 1000, which is a hard limit in Replay. @_spi(Private) public convenience init( options: Options, dispatchQueue: SentryDispatchQueueWrapper, @@ -54,7 +54,7 @@ import Foundation /// - Parameters: /// - options: The Sentry configuration options /// - flushTimeout: The timeout interval after which buffered logs will be flushed - /// - maxLogCount: The maximum number of logs to batch before triggering an immediate flush (default: 1000) + /// - maxLogCount: Maximum number of logs to batch before triggering an immediate flush. Default 1000 is a hard limit in Replay. /// - maxBufferSizeBytes: The maximum buffer size in bytes before triggering an immediate flush /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state /// - delegate: The delegate to handle captured log batches @@ -62,8 +62,7 @@ import Foundation /// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety. /// Passing a concurrent queue will result in undefined behavior and potential data races. /// - /// - Note: Logs are flushed when either `maxLogCount` (1000) or `maxBufferSizeBytes` limit is reached, - /// ensuring we never send more than 1000 logs in a single batch. + /// - Note: Logs are flushed when either `maxLogCount` or `maxBufferSizeBytes` limit is reached. @_spi(Private) public init( options: Options, flushTimeout: TimeInterval, From 429b5847189142ec358ae4fdf3f276b2c5b34ce5 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 13 Nov 2025 16:04:57 +0100 Subject: [PATCH 4/5] Lower limit --- CHANGELOG.md | 2 +- Sources/Swift/Tools/SentryLogBatcher.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1699a2bb36c..fb7613ca9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Fixes -- Limit log batching to maximum 1000 logs per batch (#6768) +- Limit log batching to maximum 100 logs per batch (#6768) ## 9.0.0-alpha.1 diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 4f5ef7b99c5..e2fe972204f 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -34,7 +34,7 @@ import Foundation /// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety. /// Passing a concurrent queue will result in undefined behavior and potential data races. /// - /// - Note: This initializer sets `maxLogCount` to 1000, which is a hard limit in Replay. + /// - Note: Setting `maxLogCount` to 100. While Replay hard limit is 1000, we keep this lower, as it's hard to lower once released. @_spi(Private) public convenience init( options: Options, dispatchQueue: SentryDispatchQueueWrapper, @@ -43,7 +43,7 @@ import Foundation self.init( options: options, flushTimeout: 5, - maxLogCount: 1_000, // Maximum 1000 logs per batch + maxLogCount: 100, // Maximum 100 logs per batch maxBufferSizeBytes: 1_024 * 1_024, // 1MB buffer size dispatchQueue: dispatchQueue, delegate: delegate @@ -54,7 +54,7 @@ import Foundation /// - Parameters: /// - options: The Sentry configuration options /// - flushTimeout: The timeout interval after which buffered logs will be flushed - /// - maxLogCount: Maximum number of logs to batch before triggering an immediate flush. Default 1000 is a hard limit in Replay. + /// - maxLogCount: Maximum number of logs to batch before triggering an immediate flush. /// - maxBufferSizeBytes: The maximum buffer size in bytes before triggering an immediate flush /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state /// - delegate: The delegate to handle captured log batches From 27ac6b1f4af41400fb4d06fed3c66bfe86b8995b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 13 Nov 2025 16:56:53 +0100 Subject: [PATCH 5/5] fix comment --- Sources/Swift/Tools/SentryLogBatcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index e2fe972204f..5151907c0ea 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -25,7 +25,7 @@ import Foundation private weak var delegate: SentryLogBatcherDelegate? - /// Convenience initializer with default flush timeout, max log count (1000), and buffer size. + /// Convenience initializer with default flush timeout, max log count (100), and buffer size. /// - Parameters: /// - options: The Sentry configuration options /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state