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
89 changes: 88 additions & 1 deletion packages/logs/src/boot/logsPublicApi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { ContextManager, TimeStamp } from '@datadog/browser-core'
import { monitor, display, createContextManager } from '@datadog/browser-core'
import { monitor, display, createContextManager, ErrorSource } from '@datadog/browser-core'
import type { Logger, LogsMessage } from '../domain/logger'
import { HandlerType } from '../domain/logger'
import { StatusType } from '../domain/logger/isAuthorized'
import type { CommonContext } from '../rawLogsEvent.types'
import type { LogsPublicApi } from './logsPublicApi'
import { makeLogsPublicApi } from './logsPublicApi'
import type { StartLogs } from './startLogs'
import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle'

const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx' }

Expand Down Expand Up @@ -254,5 +255,91 @@ describe('logs entry', () => {
expect(accountContext.clearContext).toHaveBeenCalledTimes(1)
})
})

describe('sendRawLog', () => {
let logsPublicApi: LogsPublicApi
let mockLifeCycle: LifeCycle
let logCollectedSpy: jasmine.Spy

beforeEach(() => {
mockLifeCycle = new LifeCycle()
logCollectedSpy = jasmine.createSpy('logCollected')
mockLifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, logCollectedSpy)

startLogs = jasmine.createSpy().and.callFake(() => ({
handleLog: handleLogSpy,
getInternalContext,
lifeCycle: mockLifeCycle,
}))

logsPublicApi = makeLogsPublicApi(startLogs)
logsPublicApi.init(DEFAULT_INIT_CONFIGURATION)
})

it('should send log directly to LOG_COLLECTED event', () => {
const log = {
date: 1234567890,
message: 'test message',
status: 'info' as const,
origin: ErrorSource.LOGGER,
ddsource: 'dd_debugger',
hostname: 'test-hostname',
}

logsPublicApi.sendRawLog(log)

expect(logCollectedSpy).toHaveBeenCalledTimes(1)
expect(logCollectedSpy).toHaveBeenCalledWith(log)
})

it('should bypass assembly (no default context added)', () => {
const log = {
date: 1234567890,
message: 'test message',
status: 'info' as const,
origin: ErrorSource.LOGGER,
ddsource: 'dd_debugger',
hostname: 'test-hostname',
logger: { name: 'test-logger' },
dd: { version: '1.0' },
debugger: { snapshot: { captures: [] } },
}

logsPublicApi.sendRawLog(log)

expect(logCollectedSpy).toHaveBeenCalledTimes(1)
const collectedLog = logCollectedSpy.calls.mostRecent().args[0]
// Verify the log is sent as-is without default context
expect(collectedLog).toBe(log)
expect(collectedLog.ddsource).toBe('dd_debugger')
expect(collectedLog.logger).toEqual({ name: 'test-logger' })
expect(collectedLog.dd).toEqual({ version: '1.0' })
expect(collectedLog.debugger).toEqual({ snapshot: { captures: [] } })
// Verify no default view context was added
expect(collectedLog.view).toBeUndefined()
})

it('should handle when lifecycle is not available', () => {
startLogs = jasmine.createSpy().and.callFake(() => ({
handleLog: handleLogSpy,
getInternalContext,
// No lifeCycle
}))

logsPublicApi = makeLogsPublicApi(startLogs)
logsPublicApi.init(DEFAULT_INIT_CONFIGURATION)

const log = {
date: 1234567890,
message: 'test message',
status: 'info' as const,
origin: ErrorSource.LOGGER,
}

expect(() => {
logsPublicApi.sendRawLog(log)
}).not.toThrow()
})
})
})
})
19 changes: 19 additions & 0 deletions packages/logs/src/boot/logsPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { buildCommonContext } from '../domain/contexts/commonContext'
import type { InternalContext } from '../domain/contexts/internalContext'
import type { StartLogs, StartLogsResult } from './startLogs'
import { createPreStartStrategy } from './preStartLogs'
import type { LogsEvent } from '../logsEvent.types'
import { LifeCycleEventType, type LifeCycle } from '../domain/lifeCycle'

export interface LoggerConfiguration {
level?: StatusType
Expand Down Expand Up @@ -253,6 +255,16 @@ export interface LogsPublicApi extends PublicApi {
* @internal
*/
getInternalContext: (startTime?: number) => InternalContext | undefined

/**
* Send a raw log event directly to the logs pipeline, bypassing assembly.
* This method sends the log event directly to LOG_COLLECTED lifecycle event,
* skipping the assembly step that adds default context.
*
* @internal
* @param log - The log event to send
*/
sendRawLog: (log: LogsEvent & Context) => void
}

export interface Strategy {
Expand All @@ -263,6 +275,7 @@ export interface Strategy {
userContext: ContextManager
getInternalContext: StartLogsResult['getInternalContext']
handleLog: StartLogsResult['handleLog']
lifeCycle?: LifeCycle
}

export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi {
Expand Down Expand Up @@ -351,6 +364,12 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi {

getInternalContext: monitor((startTime) => strategy.getInternalContext(startTime)),

sendRawLog: monitor((log: LogsEvent & Context) => {
if (strategy.lifeCycle) {
strategy.lifeCycle.notify(LifeCycleEventType.LOG_COLLECTED, log)
}
}),

setUser: defineContextMethod(getStrategy, CustomerContextKey.userContext, ContextManagerMethod.setContext),

getUser: defineContextMethod(getStrategy, CustomerContextKey.userContext, ContextManagerMethod.getContext),
Expand Down
4 changes: 4 additions & 0 deletions packages/logs/src/boot/preStartLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export function createPreStartStrategy(
startLogsResult.handleLog(message, statusType, handlingStack, context, date)
)
},

get lifeCycle() {
return undefined
},
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/logs/src/boot/startLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function startLogs(
accountContext,
globalContext,
userContext,
lifeCycle,
stop: () => {
cleanupTasks.forEach((task) => task())
},
Expand Down
30 changes: 30 additions & 0 deletions packages/rum-core/src/boot/rumPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { RumSessionManager } from '../domain/rumSessionManager'
import type { ReplayStats } from '../rawRumEvent.types'
import { ActionType, VitalType } from '../rawRumEvent.types'
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
import { sendLiveDebuggerLog, liveDebug } from '../domain/liveDebugger/liveDebuggerLogger'
import type { ViewOptions } from '../domain/view/trackViews'
import type {
AddDurationVitalOptions,
Expand Down Expand Up @@ -239,6 +240,27 @@ export interface RumPublicApi extends PublicApi {
*/
clearGlobalContext(): void

/**
* Send a log event to Datadog logs from the live debugger.
* This function sends log events that will be collected by the Datadog Logs SDK if it is initialized.
*
* @category Live Debugger
* @param data - The data object to send as a log event
*/
sendLiveDebuggerLog: (data: object) => void

/**
* Send a debug log event to Datadog logs from the live debugger, matching dd-trace-js send method signature.
* This function sends logs directly without default RUM context, bypassing assembly.
*
* @category Live Debugger
* @param message - The log message (will be truncated to 8KB if needed)
* @param logger - Logger information
* @param dd - Datadog context information
* @param snapshot - Debugger snapshot data
*/
liveDebug: (message?: string, logger?: any, dd?: any, snapshot?: any) => void

/**
* Set user information to all events, stored in `@usr`
*
Expand Down Expand Up @@ -699,6 +721,14 @@ export function makeRumPublicApi(
'clear-global-context'
),

sendLiveDebuggerLog: monitor((data) => {
sendLiveDebuggerLog(data)
}),

liveDebug: monitor((message?: string, logger?: any, dd?: any, snapshot?: any) => {
liveDebug(message, logger, dd, snapshot)
}),

setUser: defineContextMethod(
getStrategy,
CustomerContextKey.userContext,
Expand Down
12 changes: 12 additions & 0 deletions packages/rum-core/src/boot/startRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import { createHooks } from '../domain/hooks'
import { startEventCollection } from '../domain/event/eventCollection'
import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry'
import type { RecorderApi, ProfilerApi } from './rumPublicApi'
import { startFirebaseRemoteConfigIntegration } from '../domain/liveDebugger/firebaseRemoteConfig'
import { monitorError } from '@datadog/browser-core'

export type StartRum = typeof startRum
export type StartRumResult = ReturnType<StartRum>
Expand Down Expand Up @@ -193,6 +195,16 @@ export function startRumEventCollection(
const userContext = startUserContext(hooks, configuration, session, 'rum')
const accountContext = startAccountContext(hooks, configuration, 'rum')

// Initialize Firebase Remote Config integration if enabled
if (configuration.allowLiveDebugger && configuration.liveDebuggerId) {
startFirebaseRemoteConfigIntegration(
globalContext,
configuration.liveDebuggerId,
configuration.firebaseConfig,
configuration.firebaseVersion
).catch(monitorError)
}

const actionCollection = startActionCollection(
lifeCycle,
hooks,
Expand Down
59 changes: 59 additions & 0 deletions packages/rum-core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,49 @@ export interface RumInitConfiguration extends InitConfiguration {
*/
remoteConfigurationProxy?: string | undefined

/**
* Enable Firebase Remote Config integration for live debugger functionality.
* When enabled, the SDK will listen to Firebase Remote Config values and set
* global context properties in `dd_<id>` format.
*
* @category Data Collection
* @defaultValue false
*/
allowLiveDebugger?: boolean | undefined

/**
* The ID to use for live debugger global context properties.
* Global context properties will be set as `dd_<liveDebuggerId>`.
* If not provided, the live debugger ID will be obtained from Firebase Remote Config.
*
* @category Data Collection
*/
liveDebuggerId?: string | undefined

/**
* Firebase configuration for Remote Config integration.
* If provided, the SDK will initialize Firebase Remote Config automatically.
*
* @category Data Collection
*/
firebaseConfig?: {
apiKey: string
authDomain: string
projectId: string
storageBucket?: string
messagingSenderId?: string
appId: string
measurementId?: string
} | undefined

/**
* Firebase SDK version to load (defaults to '10.7.1').
* Only used if firebaseConfig is provided.
*
* @category Data Collection
*/
firebaseVersion?: string | undefined

// tracing options
/**
* A list of request URLs used to inject tracing headers.
Expand Down Expand Up @@ -310,6 +353,18 @@ export interface RumConfiguration extends Configuration {
profilingSampleRate: number
propagateTraceBaggage: boolean
allowedGraphQlUrls: GraphQlUrlOption[]
allowLiveDebugger: boolean
liveDebuggerId: string | undefined
firebaseConfig?: {
apiKey: string
authDomain: string
projectId: string
storageBucket?: string
messagingSenderId?: string
appId: string
measurementId?: string
}
firebaseVersion?: string
}

export function validateAndBuildRumConfiguration(
Expand Down Expand Up @@ -388,6 +443,10 @@ export function validateAndBuildRumConfiguration(
profilingSampleRate: initConfiguration.profilingSampleRate ?? 0,
propagateTraceBaggage: !!initConfiguration.propagateTraceBaggage,
allowedGraphQlUrls,
allowLiveDebugger: !!initConfiguration.allowLiveDebugger,
liveDebuggerId: initConfiguration.liveDebuggerId,
firebaseConfig: initConfiguration.firebaseConfig,
firebaseVersion: initConfiguration.firebaseVersion || '10.7.1',
...baseConfiguration,
}
}
Expand Down
Loading
Loading