Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 100 logs per batch (#6768)

## 9.0.0-alpha.1

### Breaking Changes
Expand Down
16 changes: 13 additions & 3 deletions Sources/Swift/Tools/SentryLogBatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -24,14 +25,16 @@ 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 (100), and buffer size.
/// - Parameters:
/// - options: The Sentry configuration options
/// - 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: 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,
Expand All @@ -40,7 +43,8 @@ import Foundation
self.init(
options: options,
flushTimeout: 5,
maxBufferSizeBytes: 1_024 * 1_024, // 1MB
maxLogCount: 100, // Maximum 100 logs per batch
maxBufferSizeBytes: 1_024 * 1_024, // 1MB buffer size
dispatchQueue: dispatchQueue,
delegate: delegate
)
Expand All @@ -50,21 +54,26 @@ 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.
/// - 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` or `maxBufferSizeBytes` limit is reached.
@_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
Expand Down Expand Up @@ -189,7 +198,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()
Expand Down
30 changes: 27 additions & 3 deletions Tests/SentryTests/SentryLogBatcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading