From b281b4e4256b7f7b501babf4b82f98c6c29ef122 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 2 Mar 2026 19:08:39 +0100 Subject: [PATCH] Buffer Logs calls and solve lazy var on iOS logger creation --- .../reactnative/DdLogsImplementation.kt | 63 +++----- .../com/datadog/reactnative/DdLogsTest.kt | 143 +++--------------- .../ios/Sources/DdLogsImplementation.swift | 16 +- packages/core/src/logs/DdLogs.ts | 83 +++------- .../core/src/logs/__tests__/DdLogs.test.ts | 23 +-- .../__tests__/initialization.test.tsx | 13 ++ 6 files changed, 91 insertions(+), 250 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt index 2f9bceff2..0883b9293 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt @@ -16,20 +16,30 @@ import com.facebook.react.bridge.ReadableMap * The entry point to use Datadog's Logs feature. */ class DdLogsImplementation( - logger: Logger? = null, - private val datadog: DatadogWrapper = DatadogSDKWrapper() -) { - private val reactNativeLogger: Logger by lazy { - val bundleLogsWithRum = datadog.bundleLogsWithRum - val bundleLogsWithTraces = datadog.bundleLogsWithTraces - - logger ?: Logger.Builder(Datadog.getInstance()) + private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val logger: () -> Logger = { + Logger.Builder(Datadog.getInstance()) .setLogcatLogsEnabled(true) - .setBundleWithRumEnabled(bundleLogsWithRum) - .setBundleWithTraceEnabled(bundleLogsWithTraces) + .setBundleWithRumEnabled(datadog.bundleLogsWithRum) + .setBundleWithTraceEnabled(datadog.bundleLogsWithTraces) .setName("DdLogs") .build() } +) { + private var loggerInstance: Logger? = null + private val reactNativeLogger: Logger + get() { + if (loggerInstance == null) { + loggerInstance = logger() + } + return loggerInstance!! + } + + init { + DatadogSDKWrapperStorage.addOnInitializedListener { + loggerInstance = null + } + } /** * Send a log with Debug level. @@ -37,10 +47,6 @@ class DdLogsImplementation( * @param context The additional context to send. */ fun debug(message: String, context: ReadableMap, promise: Promise) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.d( message = message, attributes = context.toHashMap() + GlobalState.globalAttributes @@ -54,10 +60,6 @@ class DdLogsImplementation( * @param context The additional context to send. */ fun info(message: String, context: ReadableMap, promise: Promise) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.i( message = message, attributes = context.toHashMap() + GlobalState.globalAttributes @@ -71,10 +73,6 @@ class DdLogsImplementation( * @param context The additional context to send. */ fun warn(message: String, context: ReadableMap, promise: Promise) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.w( message = message, attributes = context.toHashMap() + GlobalState.globalAttributes @@ -88,10 +86,6 @@ class DdLogsImplementation( * @param context The additional context to send. */ fun error(message: String, context: ReadableMap, promise: Promise) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.e( message = message, attributes = context.toHashMap() + GlobalState.globalAttributes @@ -116,10 +110,6 @@ class DdLogsImplementation( context: ReadableMap, promise: Promise ) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.log( priority = AndroidLog.DEBUG, message = message, @@ -148,10 +138,6 @@ class DdLogsImplementation( context: ReadableMap, promise: Promise ) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.log( priority = AndroidLog.INFO, message = message, @@ -180,10 +166,6 @@ class DdLogsImplementation( context: ReadableMap, promise: Promise ) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.log( priority = AndroidLog.WARN, message = message, @@ -212,10 +194,6 @@ class DdLogsImplementation( context: ReadableMap, promise: Promise ) { - if (!datadog.isInitialized()) { - promise.reject(IllegalStateException(SDK_NOT_INITIALIZED_MESSAGE)) - return - } reactNativeLogger.log( priority = AndroidLog.ERROR, message = message, @@ -228,7 +206,6 @@ class DdLogsImplementation( } internal companion object { - private const val SDK_NOT_INITIALIZED_MESSAGE = "DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT" internal const val NAME = "DdLogs" } } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdLogsTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdLogsTest.kt index 671bd0503..eb307e718 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdLogsTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdLogsTest.kt @@ -8,7 +8,6 @@ package com.datadog.reactnative import android.util.Log import com.datadog.android.log.Logger -import com.datadog.tools.unit.GenericAssert.Companion.assertThat import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap import fr.xgouchet.elmyr.Forge @@ -25,11 +24,9 @@ import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -71,8 +68,7 @@ internal class DdLogsTest { @BeforeEach fun `set up`(forge: Forge) { - whenever(mockDatadog.isInitialized()) doReturn true - testedLogs = DdLogsImplementation(mockLogger, mockDatadog) + testedLogs = DdLogsImplementation(datadog = mockDatadog, logger = { mockLogger }) fakeErrorKind = forge.aNullable { forge.aString() } fakeErrorMessage = forge.aNullable { forge.aString() } fakeStacktrace = forge.aNullable { forge.aString() } @@ -272,129 +268,26 @@ internal class DdLogsTest { } @Test - fun `M not forward logs W SDK is not initialized`() { - // When - whenever(mockDatadog.isInitialized()) doReturn false - var newTestedLogs = DdLogsImplementation(mockLogger, mockDatadog) - newTestedLogs.debug(fakeMessage, mockContext, mockPromise) - newTestedLogs.info(fakeMessage, mockContext, mockPromise) - newTestedLogs.warn(fakeMessage, mockContext, mockPromise) - newTestedLogs.error(fakeMessage, mockContext, mockPromise) - testedLogs.debugWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) - testedLogs.infoWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) - testedLogs.warnWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) - testedLogs.errorWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise + fun `M recreate logger W SDK is initialized`() { + // Given + val mockLoggerFactory = mock<() -> Logger>() + whenever(mockLoggerFactory()).thenReturn(mockLogger) + val testedLogs = DdLogsImplementation( + datadog = mockDatadog, + logger = mockLoggerFactory ) + // When + testedLogs.debug(fakeMessage, mockContext, mockPromise) + // Then - verifyNoInteractions(mockLogger) - val exceptionCaptor = argumentCaptor() - verify(mockPromise, times(8)).reject(exceptionCaptor.capture()) - assertThat(exceptionCaptor.firstValue.message) - .isEqualTo("DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT") - - // When SDK is finally initialized - whenever(mockDatadog.isInitialized()) doReturn true - newTestedLogs.debug(fakeMessage, mockContext, mockPromise) - newTestedLogs.info(fakeMessage, mockContext, mockPromise) - newTestedLogs.warn(fakeMessage, mockContext, mockPromise) - newTestedLogs.error(fakeMessage, mockContext, mockPromise) - testedLogs.debugWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) - testedLogs.infoWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) - testedLogs.warnWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) - testedLogs.errorWithError( - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - mockContext, - mockPromise - ) + verify(mockLoggerFactory, times(1)).invoke() + + // When + DatadogSDKWrapperStorage.notifyOnInitializedListeners(mock()) // Then - verify(mockLogger).i(fakeMessage, attributes = mockContext.toHashMap()) - verify(mockLogger).d(fakeMessage, attributes = mockContext.toHashMap()) - verify(mockLogger).w(fakeMessage, attributes = mockContext.toHashMap()) - verify(mockLogger).e(fakeMessage, attributes = mockContext.toHashMap()) - verify(mockLogger).log( - Log.DEBUG, - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - attributes = mockContext.toHashMap() - ) - verify(mockLogger).log( - Log.INFO, - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - attributes = mockContext.toHashMap() - ) - verify(mockLogger).log( - Log.WARN, - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - attributes = mockContext.toHashMap() - ) - verify(mockLogger).log( - Log.ERROR, - fakeMessage, - fakeErrorKind, - fakeErrorMessage, - fakeStacktrace, - attributes = mockContext.toHashMap() - ) + testedLogs.debug(fakeMessage, mockContext, mockPromise) + verify(mockLoggerFactory, times(2)).invoke() } } diff --git a/packages/core/ios/Sources/DdLogsImplementation.swift b/packages/core/ios/Sources/DdLogsImplementation.swift index 8f0c7b909..86620b869 100644 --- a/packages/core/ios/Sources/DdLogsImplementation.swift +++ b/packages/core/ios/Sources/DdLogsImplementation.swift @@ -11,11 +11,21 @@ import DatadogCore @objc public class DdLogsImplementation: NSObject { - private lazy var logger: LoggerProtocol = loggerProvider() + private var loggerInstance: LoggerProtocol? + private var logger: LoggerProtocol { + if loggerInstance == nil { + loggerInstance = loggerProvider() + } + return loggerInstance! + } private let loggerProvider: () -> LoggerProtocol - + internal init(_ loggerProvider: @escaping () -> LoggerProtocol) { self.loggerProvider = loggerProvider + super.init() + DatadogSDKWrapper.shared.addOnSdkInitializedListener { [weak self] _ in + self?.loggerInstance = nil + } } @objc @@ -94,4 +104,4 @@ internal extension DatadogLogs.Logger.Configuration { consoleLogFormat: .short ) } -} \ No newline at end of file +} diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 14c2ca6ee..8dcb7421a 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -5,11 +5,12 @@ */ import { DdAttributes } from '../DdAttributes'; -import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; +import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../config/types/SdkVerbosity'; import { debugId } from '../metro/debugIdResolver'; import type { DdNativeLogsType } from '../nativeModulesTypes'; import { encodeAttributes } from '../sdk/AttributesEncoding/attributesEncoding'; +import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { ErrorSource, LogEventMapper } from '../types'; import { getGlobalInstance } from '../utils/singletonUtils'; @@ -23,7 +24,6 @@ import type { } from './types'; const LOGS_MODULE = 'com.datadog.reactnative.logs'; -const SDK_NOT_INITIALIZED_MESSAGE = 'DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT'; const generateEmptyPromise = () => new Promise(resolve => resolve()); @@ -110,21 +110,6 @@ class DdLogsWrapper implements DdLogsType { return this.log(args[0], args[1] ?? {}, 'error'); }; - /** - * Since the InternalLog does not have a verbosity set yet in this case, - * we use console.warn to warn the user in dev mode. - */ - private printLogDroppedSdkNotInitialized = ( - message: string, - status: 'debug' | 'info' | 'warn' | 'error' - ) => { - if (__DEV__) { - console.warn( - `${DATADOG_MESSAGE_PREFIX} Dropping ${status} log as the SDK is not initialized yet: "${message}"` - ); - } - }; - private printLogDroppedByMapper = ( message: string, status: 'debug' | 'info' | 'warn' | 'error' @@ -161,21 +146,12 @@ class DdLogsWrapper implements DdLogsType { } this.printLogTracked(event.message, status); - try { - return await this.nativeLogs[status]( + return bufferVoidNativeCall(() => + this.nativeLogs[status]( event.message, encodeAttributes(event.context) - ); - } catch (error) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (error.message === SDK_NOT_INITIALIZED_MESSAGE) { - this.printLogDroppedSdkNotInitialized(message, status); - return generateEmptyPromise(); - } - - throw error; - } + ) + ); }; private logWithError = async ( @@ -207,39 +183,30 @@ class DdLogsWrapper implements DdLogsType { } this.printLogTracked(mappedEvent.message, status); - try { - const encodedContext = encodeAttributes(mappedEvent.context); - const updatedContext = { - ...encodedContext, - [DdAttributes.errorSourceType]: 'react-native' - }; - - if (fingerprint && fingerprint !== '') { - updatedContext[DdAttributes.errorFingerprint] = fingerprint; - } - - const _debugId = debugId; - if (_debugId) { - updatedContext[DdAttributes.debugId] = _debugId; - } - - return await this.nativeLogs[`${status}WithError`]( + const encodedContext = encodeAttributes(mappedEvent.context); + const updatedContext = { + ...encodedContext, + [DdAttributes.errorSourceType]: 'react-native' + }; + + if (fingerprint && fingerprint !== '') { + updatedContext[DdAttributes.errorFingerprint] = fingerprint; + } + + const _debugId = debugId; + if (_debugId) { + updatedContext[DdAttributes.debugId] = _debugId; + } + + return bufferVoidNativeCall(() => + this.nativeLogs[`${status}WithError`]( mappedEvent.message, (mappedEvent as NativeLogWithError).errorKind, (mappedEvent as NativeLogWithError).errorMessage, (mappedEvent as NativeLogWithError).stacktrace, updatedContext - ); - } catch (error) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (error.message === SDK_NOT_INITIALIZED_MESSAGE) { - this.printLogDroppedSdkNotInitialized(message, status); - return generateEmptyPromise(); - } - - throw error; - } + ) + ); }; registerLogEventMapper(logEventMapper: LogEventMapper) { diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index c241fa3bc..e4f8fa384 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -12,7 +12,7 @@ import { CoreConfiguration } from '../../config/features/CoreConfiguration'; import { LogsConfiguration } from '../../config/features/LogsConfiguration'; import { RumConfiguration } from '../../config/features/RumConfiguration'; import { SdkVerbosity } from '../../config/types'; -import type { DdNativeLogsType } from '../../nativeModulesTypes'; +import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { ErrorSource } from '../../types'; import type { LogEventMapper, LogEvent } from '../../types'; import { DdLogs } from '../DdLogs'; @@ -31,6 +31,7 @@ describe('DdLogs', () => { beforeEach(() => { jest.clearAllMocks(); DdLogs.unregisterLogEventMapper(); + BufferSingleton.onInitialization(); }); it('registers event mapper and maps logs', async () => { @@ -477,26 +478,6 @@ describe('DdLogs', () => { }); }); - describe('when SDK is not initialized', () => { - beforeEach(() => { - jest.clearAllMocks(); - DdLogs.unregisterLogEventMapper(); - }); - - it('does not crash and warns user', async () => { - (NativeModules.DdLogs.info as jest.MockedFunction< - DdNativeLogsType['debug'] - >).mockRejectedValueOnce( - new Error('DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT') - ); - const consoleSpy = jest.spyOn(console, 'warn'); - await DdLogs.info('original message', {}); - expect(consoleSpy).toHaveBeenCalledWith( - 'DATADOG: Dropping info log as the SDK is not initialized yet: "original message"' - ); - }); - }); - describe('log context', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 596ab9532..76943f040 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -8,6 +8,7 @@ import { version as reactNativeVersion } from 'react-native/package.json'; import { NativeModules } from 'react-native'; import { InitializationMode } from '../../../config/types'; +import { DdLogs } from '../../../logs/DdLogs'; import { DdRum } from '../../../rum/DdRum'; import { PropagatorType, RumActionType } from '../../../rum/types'; import { DdTrace } from '../../../trace/DdTrace'; @@ -128,6 +129,10 @@ describe('DatadogProvider', () => { it('keeps events in the buffer then executes the buffer once initialization is done', async () => { // Given + await DdLogs.info('fake_info_log'); + await DdLogs.debug('fake_debug_log'); + await DdLogs.warn('fake_wanr_log'); + await DdLogs.error('fake_error_log'); NativeModules.DdTrace.startSpan.mockReturnValueOnce('good_span_id'); (nowMock as any).mockReturnValue('good_timestamp'); await DdRum.addAction(RumActionType.TAP, 'fakeAction'); @@ -138,6 +143,10 @@ describe('DatadogProvider', () => { (nowMock as any).mockReturnValue('bad_timestamp'); // Then + expect(NativeModules.DdLogs.info).not.toHaveBeenCalled(); + expect(NativeModules.DdLogs.debug).not.toHaveBeenCalled(); + expect(NativeModules.DdLogs.warn).not.toHaveBeenCalled(); + expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(NativeModules.DdRum.addAction).not.toHaveBeenCalled(); expect(NativeModules.DdTrace.startSpan).not.toHaveBeenCalled(); expect(NativeModules.DdTrace.finishSpan).not.toHaveBeenCalled(); @@ -148,6 +157,10 @@ describe('DatadogProvider', () => { // Then expect(NativeModules.DdSdk.initialize).toHaveBeenCalledTimes(1); + expect(NativeModules.DdLogs.info).toHaveBeenCalledTimes(1); + expect(NativeModules.DdLogs.debug).toHaveBeenCalledTimes(1); + expect(NativeModules.DdLogs.warn).toHaveBeenCalledTimes(1); + expect(NativeModules.DdLogs.error).toHaveBeenCalledTimes(1); expect(NativeModules.DdRum.addAction).toHaveBeenCalledTimes(1); expect(NativeModules.DdTrace.startSpan).toHaveBeenCalledTimes(1); expect(NativeModules.DdTrace.startSpan).toHaveBeenLastCalledWith(