diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 27c9e3844c..feca2412b1 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,5 +1,5 @@ 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' @@ -7,6 +7,7 @@ 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' } @@ -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() + }) + }) }) }) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index c96b30f66e..fd819ff4f5 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -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 @@ -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 { @@ -263,6 +275,7 @@ export interface Strategy { userContext: ContextManager getInternalContext: StartLogsResult['getInternalContext'] handleLog: StartLogsResult['handleLog'] + lifeCycle?: LifeCycle } export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { @@ -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), diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 257811bab0..2367c29eb5 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -106,6 +106,10 @@ export function createPreStartStrategy( startLogsResult.handleLog(message, statusType, handlingStack, context, date) ) }, + + get lifeCycle() { + return undefined + }, } } diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 929de4096f..957f305204 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -112,6 +112,7 @@ export function startLogs( accountContext, globalContext, userContext, + lifeCycle, stop: () => { cleanupTasks.forEach((task) => task()) }, diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 5086fb216f..c31f270efe 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -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, @@ -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` * @@ -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, diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 9d15884abe..4ad1397b2c 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -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 @@ -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, diff --git a/packages/rum-core/src/domain/configuration/configuration.ts b/packages/rum-core/src/domain/configuration/configuration.ts index f41e76b0ca..9032803f24 100644 --- a/packages/rum-core/src/domain/configuration/configuration.ts +++ b/packages/rum-core/src/domain/configuration/configuration.ts @@ -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_` 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_`. + * 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. @@ -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( @@ -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, } } diff --git a/packages/rum-core/src/domain/liveDebugger/firebaseRemoteConfig.spec.ts b/packages/rum-core/src/domain/liveDebugger/firebaseRemoteConfig.spec.ts new file mode 100644 index 0000000000..8ed9d6f179 --- /dev/null +++ b/packages/rum-core/src/domain/liveDebugger/firebaseRemoteConfig.spec.ts @@ -0,0 +1,144 @@ +import { registerCleanupTask } from '@datadog/browser-core/test' +import type { ContextManager } from '@datadog/browser-core' +import { createContextManager } from '@datadog/browser-core' +import { startFirebaseRemoteConfigIntegration } from './firebaseRemoteConfig' + +describe('firebaseRemoteConfig', () => { + let globalContext: ContextManager + let mockFirebaseRemoteConfig: any + let originalFirebase: any + + beforeEach(() => { + globalContext = createContextManager() + originalFirebase = (window as any).firebase + + mockFirebaseRemoteConfig = { + getValue: jasmine.createSpy('getValue').and.returnValue({ + asBoolean: () => true, + asString: () => 'true', + }), + onConfigUpdated: jasmine.createSpy('onConfigUpdated'), + ensureInitialized: jasmine.createSpy('ensureInitialized').and.returnValue(Promise.resolve()), + } + + ;(window as any).firebase = { + remoteConfig: jasmine.createSpy('remoteConfig').and.returnValue(mockFirebaseRemoteConfig), + } + + spyOn(globalContext, 'setContextProperty') + }) + + afterEach(() => { + ;(window as any).firebase = originalFirebase + }) + + describe('startFirebaseRemoteConfigIntegration', () => { + it('should initialize Firebase Remote Config when allowLiveDebugger is enabled', async () => { + const liveDebuggerId = 'test-id-123' + + await startFirebaseRemoteConfigIntegration(globalContext, liveDebuggerId) + + expect((window as any).firebase.remoteConfig).toHaveBeenCalled() + expect(mockFirebaseRemoteConfig.ensureInitialized).toHaveBeenCalled() + }) + + it('should set global context property with dd_ format on init', async () => { + const liveDebuggerId = 'test-id-123' + mockFirebaseRemoteConfig.getValue.and.returnValue({ + asBoolean: () => true, + }) + + await startFirebaseRemoteConfigIntegration(globalContext, liveDebuggerId) + + expect(globalContext.setContextProperty).toHaveBeenCalledWith(`dd_${liveDebuggerId}`, true) + }) + + it('should send log event on initialization', async () => { + const liveDebuggerId = 'test-id-123' + mockFirebaseRemoteConfig.getValue.and.returnValue({ + asBoolean: () => true, + }) + + await startFirebaseRemoteConfigIntegration(globalContext, liveDebuggerId) + + // Verify that global context was set, which indicates log was sent + expect(globalContext.setContextProperty).toHaveBeenCalledWith(`dd_${liveDebuggerId}`, true) + }) + + it('should listen to config updates', async () => { + const liveDebuggerId = 'test-id-123' + let configUpdateCallback: (() => void) | undefined + + mockFirebaseRemoteConfig.onConfigUpdated.and.callFake((callback: () => void) => { + configUpdateCallback = callback + }) + + await startFirebaseRemoteConfigIntegration(globalContext, liveDebuggerId) + + expect(mockFirebaseRemoteConfig.onConfigUpdated).toHaveBeenCalled() + + // Simulate config update + if (configUpdateCallback) { + mockFirebaseRemoteConfig.getValue.and.returnValue({ + asBoolean: () => false, + }) + configUpdateCallback() + } + + expect(globalContext.setContextProperty).toHaveBeenCalledWith(`dd_${liveDebuggerId}`, false) + // Verify it was called twice - once on init, once on update + expect(globalContext.setContextProperty).toHaveBeenCalledTimes(2) + }) + + it('should convert string "true" to boolean true', async () => { + const liveDebuggerId = 'test-id-123' + mockFirebaseRemoteConfig.getValue.and.returnValue({ + asBoolean: () => true, + asString: () => 'true', + }) + + await startFirebaseRemoteConfigIntegration(globalContext, liveDebuggerId) + + expect(globalContext.setContextProperty).toHaveBeenCalledWith(`dd_${liveDebuggerId}`, true) + }) + + it('should convert string "false" to boolean false', async () => { + const liveDebuggerId = 'test-id-123' + mockFirebaseRemoteConfig.getValue.and.returnValue({ + asBoolean: () => false, + asString: () => 'false', + }) + + await startFirebaseRemoteConfigIntegration(globalContext, liveDebuggerId) + + expect(globalContext.setContextProperty).toHaveBeenCalledWith(`dd_${liveDebuggerId}`, false) + }) + + it('should handle when Firebase SDK is not available', async () => { + ;(window as any).firebase = undefined + + await expectAsync( + startFirebaseRemoteConfigIntegration(globalContext, 'test-id') + ).toBeRejected() + }) + + it('should handle when Firebase Remote Config is not available', async () => { + ;(window as any).firebase = { + remoteConfig: undefined, + } + + await expectAsync( + startFirebaseRemoteConfigIntegration(globalContext, 'test-id') + ).toBeRejected() + }) + + it('should handle errors during initialization gracefully', async () => { + mockFirebaseRemoteConfig.ensureInitialized.and.returnValue(Promise.reject(new Error('Init failed'))) + + await expectAsync( + startFirebaseRemoteConfigIntegration(globalContext, 'test-id') + ).toBeRejected() + }) + }) +}) + diff --git a/packages/rum-core/src/domain/liveDebugger/firebaseRemoteConfig.ts b/packages/rum-core/src/domain/liveDebugger/firebaseRemoteConfig.ts new file mode 100644 index 0000000000..587560e25e --- /dev/null +++ b/packages/rum-core/src/domain/liveDebugger/firebaseRemoteConfig.ts @@ -0,0 +1,175 @@ +import type { ContextManager } from '@datadog/browser-core' +import { dateNow, display, setInterval } from '@datadog/browser-core' +import { sendLiveDebuggerLog } from './liveDebuggerLogger' +import type { FirebaseConfig } from './initializeFirebase' +import { initializeFirebase } from './initializeFirebase' + +interface FirebaseRemoteConfigValue { + asBoolean(): boolean + asString(): string + getSource?(): string +} + +interface FirebaseRemoteConfig { + getValue(key: string): FirebaseRemoteConfigValue + getBoolean?(key: string): boolean + onConfigUpdated?(callback: () => void): void + ensureInitialized(): Promise + fetchAndActivate?(): Promise + settings?: { + minimumFetchIntervalMillis?: number + } + defaultConfig?: Record +} + +interface Firebase { + remoteConfig: () => FirebaseRemoteConfig +} + +interface BrowserWindow extends Window { + firebase?: Firebase +} + +/** + * Start Firebase Remote Config integration for live debugger. + * This function initializes Firebase Remote Config, listens to config changes, + * and sets global context properties in `dd_` format. + * + * @param globalContext - The global context manager to set properties on + * @param liveDebuggerId - The ID to use for the global context property key + * @param firebaseConfig - Optional Firebase configuration. If provided, the SDK will initialize Firebase automatically. + * @param firebaseVersion - Optional Firebase SDK version to load (defaults to '10.7.1') + * @returns A promise that resolves when initialization is complete + */ +export async function startFirebaseRemoteConfigIntegration( + globalContext: ContextManager, + liveDebuggerId: string, + firebaseConfig?: FirebaseConfig, + firebaseVersion?: string +): Promise { + // Initialize Firebase if config is provided + if (firebaseConfig) { + try { + await initializeFirebase(firebaseConfig, firebaseVersion) + } catch (error) { + display.error(`Failed to initialize Firebase: ${String(error)}`) + throw error + } + } + + const browserWindow = window as BrowserWindow + + if (!browserWindow.firebase) { + throw new Error('Firebase SDK is not available. Please include Firebase SDK before initializing the RUM SDK or provide firebaseConfig.') + } + + if (!browserWindow.firebase.remoteConfig) { + throw new Error('Firebase Remote Config is not available. Please include Firebase Remote Config SDK.') + } + + try { + const remoteConfig = browserWindow.firebase.remoteConfig() + + // Set minimum fetch interval to 0 for development (no cache) + // Note: Firebase enforces a minimum of 60 seconds in production + if (remoteConfig.settings) { + remoteConfig.settings.minimumFetchIntervalMillis = 0 + } + + // Ensure Remote Config is initialized + await remoteConfig.ensureInitialized() + + // Function to update the global context property and send log + const updateContextAndLog = () => { + try { + const configValue = remoteConfig.getValue(`dd_${liveDebuggerId}`) + + // Get string value first - Firebase Remote Config stores values as strings + const stringValue = configValue.asString() + let booleanValue: boolean + + // Firebase Remote Config asBoolean() can be unreliable for string values + // Parse the string value explicitly to handle "true"/"false" strings + const normalizedString = stringValue.toLowerCase().trim() + if (normalizedString === 'true' || normalizedString === '1') { + booleanValue = true + } else if (normalizedString === 'false' || normalizedString === '0' || normalizedString === '') { + booleanValue = false + } else { + // Try asBoolean() as fallback, but log a warning if it seems wrong + booleanValue = configValue.asBoolean() + if (normalizedString !== 'true' && normalizedString !== 'false' && booleanValue) { + display.warn(`Firebase Remote Config value "${stringValue}" for key "${liveDebuggerId}" converted to boolean ${booleanValue}`) + } + } + + // Set global context property with dd_ format + globalContext.setContextProperty(`dd_${liveDebuggerId}`, booleanValue) + + // Send log event + sendLiveDebuggerLog({ + key: liveDebuggerId, + value: booleanValue, + id: liveDebuggerId, + timestamp: dateNow(), + stringValue, + }) + + /// TODO SOME LOGIC ON THE VALUE OF THE CONFIG + + /// TODO END OF LOGIC ON THE VALUE OF THE CONFIG + } catch (error) { + display.error(`Error updating live debugger context: ${String(error)}`) + } + } + + // Set initial value + updateContextAndLog() + + // Fetch initial config from server + if (remoteConfig.fetchAndActivate) { + try { + await remoteConfig.fetchAndActivate() + updateContextAndLog() + } catch (error) { + display.warn(`Failed to fetch initial Firebase Remote Config: ${String(error)}`) + } + } + + // Set up config update listener + // Note: onConfigUpdated only fires when fetchAndActivate() is called and new config is available + // So we need to periodically fetch to detect changes + if (typeof remoteConfig.onConfigUpdated === 'function') { + remoteConfig.onConfigUpdated(() => { + updateContextAndLog() + }) + } + + // Periodically fetch config updates + // Use shorter interval for development (when minimumFetchIntervalMillis is 0) + // In production, Firebase will enforce minimum 60 seconds regardless + const fetchInterval = 10000 // 10 seconds - Firebase will enforce its own minimum in production + setInterval(() => { + // Use void to explicitly ignore the promise return value + void (async () => { + try { + if (remoteConfig.fetchAndActivate) { + const activated = await remoteConfig.fetchAndActivate() + if (activated) { + // Config was activated, onConfigUpdated callback will fire and updateContextAndLog will be called + // But we also call it here to ensure updates happen even if callback isn't available + updateContextAndLog() + } + } + } catch { + // Silently handle fetch errors - Firebase may rate limit or reject requests if too frequent + // The error is expected when minimumFetchIntervalMillis hasn't elapsed + } + })() + }, fetchInterval) + } catch (error) { + display.error(`Failed to initialize Firebase Remote Config integration: ${String(error)}`) + throw error + } +} + diff --git a/packages/rum-core/src/domain/liveDebugger/initializeFirebase.ts b/packages/rum-core/src/domain/liveDebugger/initializeFirebase.ts new file mode 100644 index 0000000000..327edfaab0 --- /dev/null +++ b/packages/rum-core/src/domain/liveDebugger/initializeFirebase.ts @@ -0,0 +1,125 @@ +import { display } from '@datadog/browser-core' + +export interface FirebaseConfig { + apiKey: string + authDomain: string + projectId: string + storageBucket?: string + messagingSenderId?: string + appId: string + measurementId?: string +} + +interface FirebaseApp { + name?: string + options?: { + projectId?: string + } +} + +interface FirebaseNamespace { + initializeApp: (config: FirebaseConfig) => FirebaseApp + apps?: FirebaseApp[] + remoteConfig: () => any +} + +interface BrowserWindow extends Window { + firebase?: FirebaseNamespace +} + +/** + * Load Firebase SDK scripts dynamically + */ +function loadFirebaseScripts(version: string = '10.7.1'): Promise { + return new Promise((resolve, reject) => { + // Check if Firebase is already loaded + if ((window as BrowserWindow).firebase) { + resolve() + return + } + + const scripts = [ + `https://www.gstatic.com/firebasejs/${version}/firebase-app-compat.js`, + `https://www.gstatic.com/firebasejs/${version}/firebase-remote-config-compat.js`, + ] + + let loadedCount = 0 + const totalScripts = scripts.length + let hasError = false + + scripts.forEach((src) => { + const script = document.createElement('script') + script.src = src + script.async = true + script.onload = () => { + if (!hasError) { + loadedCount++ + if (loadedCount === totalScripts) { + resolve() + } + } + } + script.onerror = () => { + hasError = true + reject(new Error(`Failed to load Firebase script: ${src}`)) + } + document.head.appendChild(script) + }) + }) +} + +/** + * Initialize Firebase Remote Config with the provided configuration + */ +export async function initializeFirebase( + firebaseConfig: FirebaseConfig, + firebaseVersion?: string +): Promise { + const browserWindow = window as BrowserWindow + const version = firebaseVersion || '10.7.1' + + // Load Firebase SDK scripts if not already loaded + if (!browserWindow.firebase) { + try { + await loadFirebaseScripts(version) + } catch (error) { + display.error(`Failed to load Firebase SDK: ${String(error)}`) + throw error + } + } + + // Check if Firebase is available after loading + if (!browserWindow.firebase) { + throw new Error('Firebase SDK failed to load') + } + + // Initialize Firebase app if not already initialized + try { + // Check if Firebase app is already initialized for this project + const existingApps = browserWindow.firebase.apps || [] + const isAlreadyInitialized = existingApps.some( + (app: FirebaseApp) => app?.options?.projectId === firebaseConfig.projectId + ) + + if (!isAlreadyInitialized) { + browserWindow.firebase.initializeApp(firebaseConfig) + display.log('Firebase initialized successfully') + } else { + display.log('Firebase app already initialized for this project') + } + } catch (error: any) { + // Firebase might already be initialized, which is fine + // Check if it's a duplicate app error + if (error?.code === 'app/duplicate-app') { + display.log('Firebase app already initialized') + } else { + display.debug(`Firebase initialization note: ${String(error)}`) + } + } + + // Verify Firebase Remote Config is available + if (!browserWindow.firebase.remoteConfig) { + throw new Error('Firebase Remote Config is not available after initialization') + } +} + diff --git a/packages/rum-core/src/domain/liveDebugger/liveDebuggerLogger.spec.ts b/packages/rum-core/src/domain/liveDebugger/liveDebuggerLogger.spec.ts new file mode 100644 index 0000000000..e1954e1c55 --- /dev/null +++ b/packages/rum-core/src/domain/liveDebugger/liveDebuggerLogger.spec.ts @@ -0,0 +1,185 @@ +import { registerCleanupTask } from '@datadog/browser-core/test' +import { sendLiveDebuggerLog, liveDebug } from './liveDebuggerLogger' + +describe('liveDebuggerLogger', () => { + let originalDDLogs: any + let mockLogger: any + + beforeEach(() => { + mockLogger = { + info: jasmine.createSpy('info'), + error: jasmine.createSpy('error'), + warn: jasmine.createSpy('warn'), + } + originalDDLogs = (window as any).DD_LOGS + ;(window as any).DD_LOGS = { + logger: mockLogger, + } + }) + + afterEach(() => { + ;(window as any).DD_LOGS = originalDDLogs + }) + + describe('sendLiveDebuggerLog', () => { + it('should send log event when DD_LOGS is available', () => { + const data = { key: 'test-key', value: true, id: 'test-id' } + + sendLiveDebuggerLog(data) + + expect(mockLogger.info).toHaveBeenCalledWith('Live Debugger event', data) + }) + + it('should handle when DD_LOGS is not available', () => { + ;(window as any).DD_LOGS = undefined + + expect(() => { + sendLiveDebuggerLog({ key: 'test-key', value: true }) + }).not.toThrow() + }) + + it('should handle when DD_LOGS.logger is not available', () => { + ;(window as any).DD_LOGS = {} + + expect(() => { + sendLiveDebuggerLog({ key: 'test-key', value: true }) + }).not.toThrow() + }) + + it('should send log with correct data structure', () => { + const data = { + key: 'remote-config-key', + value: false, + id: 'debugger-id-123', + timestamp: Date.now(), + } + + sendLiveDebuggerLog(data) + + expect(mockLogger.info).toHaveBeenCalledWith('Live Debugger event', data) + }) + }) + + describe('liveDebug', () => { + let mockSendRawLog: jasmine.Spy + let mockGetInitConfiguration: jasmine.Spy + + beforeEach(() => { + mockSendRawLog = jasmine.createSpy('sendRawLog') + mockGetInitConfiguration = jasmine.createSpy('getInitConfiguration').and.returnValue({ service: 'test-service' }) + ;(window as any).DD_LOGS = { + sendRawLog: mockSendRawLog, + getInitConfiguration: mockGetInitConfiguration, + } + // Mock window.location.hostname + Object.defineProperty(window, 'location', { + value: { hostname: 'test-hostname' }, + writable: true, + }) + }) + + it('should send log when DD_LOGS.sendRawLog is available', () => { + liveDebug('test message', { name: 'test-logger' }, { version: '1.0' }, { captures: [] }) + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.message).toBe('test message') + expect(payload.logger).toEqual({ name: 'test-logger' }) + expect(payload.dd).toEqual({ version: '1.0' }) + expect(payload.debugger).toEqual({ snapshot: { captures: [] } }) + }) + + it('should handle when DD_LOGS is not available', () => { + ;(window as any).DD_LOGS = undefined + + expect(() => { + liveDebug('test message') + }).not.toThrow() + expect(mockSendRawLog).not.toHaveBeenCalled() + }) + + it('should handle when sendRawLog is not available', () => { + ;(window as any).DD_LOGS = { + getInitConfiguration: mockGetInitConfiguration, + } + + expect(() => { + liveDebug('test message') + }).not.toThrow() + expect(mockSendRawLog).not.toHaveBeenCalled() + }) + + it('should construct payload with correct structure matching dd-trace-js', () => { + liveDebug('test message', { name: 'logger' }, { version: '1.0' }, { snapshot: 'data' }) + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.ddsource).toBe('dd_debugger') + expect(payload.hostname).toBe('test-hostname') + expect(payload.service).toBe('test-service') + expect(payload.message).toBe('test message') + expect(payload.logger).toEqual({ name: 'logger' }) + expect(payload.dd).toEqual({ version: '1.0' }) + expect(payload.debugger).toEqual({ snapshot: { snapshot: 'data' } }) + expect(payload.date).toBeDefined() + expect(payload.status).toBe('info') + expect(payload.origin).toBeDefined() + }) + + it('should truncate message to 8KB if needed', () => { + const longMessage = 'a'.repeat(9 * 1024) // 9KB message + liveDebug(longMessage) + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.message.length).toBe(8 * 1024 + 1) // 8KB + '…' + expect(payload.message.endsWith('…')).toBe(true) + }) + + it('should include all parameters (message, logger, dd, snapshot)', () => { + const message = 'test message' + const logger = { name: 'test-logger', level: 'info' } + const dd = { version: '1.0', env: 'prod' } + const snapshot = { captures: [{ id: '1' }] } + + liveDebug(message, logger, dd, snapshot) + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.message).toBe(message) + expect(payload.logger).toBe(logger) + expect(payload.dd).toBe(dd) + expect(payload.debugger).toEqual({ snapshot }) + }) + + it('should handle empty message', () => { + liveDebug(undefined, { name: 'logger' }, {}, {}) + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.message).toBe('') + }) + + it('should not include service if not available in config', () => { + mockGetInitConfiguration.and.returnValue({}) + liveDebug('test message') + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.service).toBeUndefined() + }) + + it('should handle when getInitConfiguration is not available', () => { + ;(window as any).DD_LOGS = { + sendRawLog: mockSendRawLog, + } + liveDebug('test message') + + expect(mockSendRawLog).toHaveBeenCalledTimes(1) + const payload = mockSendRawLog.calls.mostRecent().args[0] + expect(payload.service).toBeUndefined() + }) + }) +}) + + diff --git a/packages/rum-core/src/domain/liveDebugger/liveDebuggerLogger.ts b/packages/rum-core/src/domain/liveDebugger/liveDebuggerLogger.ts new file mode 100644 index 0000000000..49a91b43cc --- /dev/null +++ b/packages/rum-core/src/domain/liveDebugger/liveDebuggerLogger.ts @@ -0,0 +1,87 @@ +import type { Context } from '@datadog/browser-core' +import { timeStampNow, ErrorSource } from '@datadog/browser-core' + +interface BrowserWindow extends Window { + DD_LOGS?: { + logger?: { + info: (message: string, context?: Context) => void + } + sendRawLog?: (log: any) => void + getInitConfiguration?: () => { service?: string } | undefined + } +} + +const MAX_MESSAGE_LENGTH = 8 * 1024 // 8KB + +/** + * Send a log event to Datadog logs from the live debugger. + * This function checks if the Logs SDK is available and sends the log event. + * + * @param data - The data object to send as a log event + */ +export function sendLiveDebuggerLog(data: object): void { + const browserWindow = window as BrowserWindow + if (browserWindow.DD_LOGS?.logger?.info) { + browserWindow.DD_LOGS.logger.info('Live Debugger event', {debuggerContext: data} as Context) + } +} + +/** + * 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. + * + * @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 + */ +export function liveDebug(message?: string, logger?: any, dd?: any, snapshot?: any): void { + const browserWindow = window as BrowserWindow + + if (!browserWindow.DD_LOGS?.sendRawLog) { + return + } + + // Get hostname from browser + const hostname = typeof window !== 'undefined' && window.location ? window.location.hostname : 'unknown' + + // Get service from logs initialization configuration (defined during DD_LOGS.init()) + const initConfig = browserWindow.DD_LOGS.getInitConfiguration?.() + const service = initConfig?.service + + // Get application_id from RUM internal context if available (same way regular loggers get it) + let applicationId: string | undefined + try { + const ddRum = (window as any).DD_RUM + if (ddRum && typeof ddRum.getInternalContext === 'function') { + const getInternalContext = ddRum.getInternalContext as (startTime?: number) => { application_id?: string } | undefined + const rumInternalContext = getInternalContext() + applicationId = rumInternalContext?.application_id + } + } catch { + // Ignore errors when accessing RUM context + } + + // Truncate message to 8KB if needed + const truncatedMessage = + message && message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, MAX_MESSAGE_LENGTH)}…` : message + + // Construct payload matching dd-trace-js structure + const payload = { + date: timeStampNow(), + message: truncatedMessage || '', + status: 'info' as const, + origin: ErrorSource.LOGGER, + ddsource: 'dd_debugger', + hostname, + ...(service && { service }), + ...(applicationId && { application_id: applicationId }), + logger, + dd, + debugger: { snapshot }, + } + + browserWindow.DD_LOGS.sendRawLog(payload) +} + + diff --git a/sandbox/.gitignore b/sandbox/.gitignore index 72e8ffc0db..e69de29bb2 100644 --- a/sandbox/.gitignore +++ b/sandbox/.gitignore @@ -1 +0,0 @@ -* diff --git a/sandbox/LIVE_DEBUGGER_TEST.md b/sandbox/LIVE_DEBUGGER_TEST.md new file mode 100644 index 0000000000..9e3cf28efa --- /dev/null +++ b/sandbox/LIVE_DEBUGGER_TEST.md @@ -0,0 +1,136 @@ +# Live Debugger Test Page + +This test page demonstrates the Firebase Remote Config integration with the Datadog RUM SDK's live debugger feature. + +## Setup + +### Option 1: Using Real Firebase Remote Config + +1. **Create a Firebase Project** (if you don't have one): + - Go to [Firebase Console](https://console.firebase.google.com/) + - Create a new project or use an existing one + - Get your Firebase config from Project Settings + +2. **Configure Firebase Remote Config**: + - In Firebase Console, go to Remote Config + - Create a new parameter with a key (e.g., `test-live-debugger-id`) + - Set the value type to **Boolean** + - Set a default value (true or false) + - Publish the configuration + +3. **Update the Test Page**: + - Open `live-debugger-test.html` in a browser + - Paste your Firebase config JSON in the "Firebase Config" field + - Set the "Live Debugger ID" to match your Firebase Remote Config parameter key + - Configure your Datadog credentials + - Click "Initialize SDKs" + +### Option 2: Using Mock Firebase (For Testing Without Real Firebase) + +The test page includes a mock Firebase Remote Config implementation that uses localStorage for testing: + +1. **Start the Dev Server**: + ```bash + yarn dev + ``` + +2. **Open the Test Page**: + - Navigate to `http://localhost:8080/live-debugger-test.html` + - Leave the Firebase config as-is (it will use mock Firebase) + - Set your Live Debugger ID (e.g., `test-live-debugger-id`) + - Configure your Datadog credentials + - Click "Initialize SDKs" + +3. **Test Value Changes**: + - Click "Set Mock Firebase Value (Toggle)" to change the value + - Watch the "Global Context Property" update automatically + - Check the logs to see log events being sent + +## Features + +The test page provides: + +1. **Configuration Form**: Set up Firebase and Datadog credentials +2. **Status Display**: Shows initialization status +3. **Global Context Display**: Shows the `dd_` property value in real-time +4. **Actions**: + - Send Test Log Event: Test the `sendLiveDebuggerLog` API + - Fetch Firebase Config: Manually trigger a Firebase Remote Config fetch + - Set Mock Firebase Value: Toggle mock Firebase value (when using mock) + - Refresh Context: Manually refresh the global context display + - Check Firebase Value: Check the current Firebase Remote Config value +5. **Logs**: Real-time log of all operations and events + +## Testing Scenarios + +### Test 1: Initial Value +1. Initialize SDKs +2. Check that the global context property `dd_` is set +3. Verify the value matches Firebase Remote Config default value + +### Test 2: Value Changes +1. Initialize SDKs +2. Change the Firebase Remote Config value in Firebase Console (or use mock toggle) +3. Wait for Firebase to fetch updates (or manually click "Fetch Firebase Config") +4. Verify the global context property updates automatically +5. Check logs for log events being sent + +### Test 3: Log Events +1. Initialize SDKs (make sure Logs SDK is initialized) +2. Click "Send Test Log Event" +3. Check Datadog Logs Explorer for the log event + +### Test 4: Error Handling +1. Try initializing without Firebase SDK loaded +2. Try initializing with invalid Firebase config +3. Verify error messages are displayed + +## Expected Behavior + +When working correctly: + +1. **On Initialization**: + - Firebase Remote Config is initialized + - RUM SDK reads the initial value from Firebase Remote Config + - Global context property `dd_` is set to the boolean value + - A log event is sent with the initial value + +2. **On Value Change**: + - Firebase Remote Config `onConfigUpdated` callback fires + - Global context property is updated + - A new log event is sent with the updated value + +3. **Global Context**: + - Property key format: `dd_` + - Property value: boolean (true/false) + - Accessible via `DD_RUM.getGlobalContext()` + +## Troubleshooting + +### Firebase SDK Not Found +- Make sure Firebase SDK scripts are loaded before initializing RUM SDK +- Check browser console for errors + +### Global Context Property Not Set +- Verify `allowLiveDebugger` is set to `true` in RUM init +- Verify `liveDebuggerId` matches Firebase Remote Config parameter key +- Check browser console for errors + +### Log Events Not Sent +- Make sure Datadog Logs SDK is initialized +- Check that `DD_LOGS` is available in the browser console +- Verify logs are being sent to Datadog (check Logs Explorer) + +### Value Not Updating +- Firebase Remote Config has a minimum fetch interval (default: 1 hour) +- Use "Fetch Firebase Config" button to manually trigger a fetch +- For testing, the mock Firebase updates every 2 seconds + +## Notes + +- The test page uses Firebase SDK v10.7.1 (compat version) +- For production use, ensure Firebase Remote Config is properly configured +- The mock Firebase implementation is for testing only and uses localStorage +- Real Firebase Remote Config requires proper authentication and project setup + + diff --git a/sandbox/live-debugger-test.html b/sandbox/live-debugger-test.html new file mode 100644 index 0000000000..71951f96b7 --- /dev/null +++ b/sandbox/live-debugger-test.html @@ -0,0 +1,578 @@ + + + + + + Live Debugger Test - Firebase Remote Config + + + +
+

🔥 Live Debugger Test - Firebase Remote Config

+ +
+

Configuration

+
+ + + + + + + + + + + + + + + + +
+
+ +
+

Status

+
+
Waiting for initialization...
+
+
+ +
+

Global Context Property

+
Not initialized
+ + +
+ +
+

Actions

+ + + + +
+ +
+

Logs

+
+
+
+ + + + + + + + + + + + + diff --git a/sandbox/sample_debug.json b/sandbox/sample_debug.json new file mode 100644 index 0000000000..3a3eb354cb --- /dev/null +++ b/sandbox/sample_debug.json @@ -0,0 +1,1781 @@ +{ + "id": "AwAAAZrufDYYQY3dPQAAABhBWnJ1ZkRZWUFBQjB1M2FmS1htWF93QVcAAAAkZDE5YWVlN2MtNGMxYS00M2RmLWI5NDAtYWZhNjhiOGRkMGYwAAAAAg", + "content": { + "timestamp": "2025-12-05T12:28:29.080Z", + "tags": [ + "account:staging", + "adp_enabled:false", + "agent_release_candidate_cluster:false", + "agent_version:7.75.0-devel_git.129.78229dd", + "app:debugger-demo-java", + "auto-discovery.cluster-autoscaler.k8s.io/stripe", + "autoscaling_group:us1-staging-dog-stripe-k8s-ng-asg-ca3efd3980d20273", + "availability-zone:us-east-1c", + "aws_account:727006795293", + "aws_autoscaling_groupname:us1-staging-dog-stripe-k8s-ng-asg-ca3efd3980d20273", + "aws_ec2_fleet-id:fleet-f197b1a6-0386-6ebe-86b8-2522d4730a41", + "aws_ec2launchtemplate_id:lt-0f484a829555482c6", + "aws_ec2launchtemplate_version:1", + "chart_name:debugger-demo-java", + "cloud_provider:aws", + "cluster_name:stripe", + "cnab.installation:helm/v1::debugger-demo-java.debugger-backend.stripe.us1.staging.dog", + "container.baseimage.buildstamp:2025-12-03t16:38:14z", + "container.baseimage.isgbi:yes", + "container.baseimage.name:images/base/gbi-ubuntu_2404", + "container.baseimage.os:ubuntu_noble_lts", + "container_id:7abe58cdfcb3a0149685d4d773c05caac3d640b3d79501d9f3c43b7f48aa9c8c", + "container_name:debugger-demo-java", + "cpu_arch:arm64", + "datacenter:us1.staging.dog", + "dd_compute_k8s_platform_version:v6-267-0", + "debugger_version:1.57.0-snapshot_4e48384724", + "default_env:staging", + "display_container_name:debugger-demo-java_debugger-demo-java-77886949f5-q6hmp", + "entrypoint.basedir:target", + "entrypoint.name:spring-petclinic-2.7.3", + "entrypoint.type:jar", + "env:staging", + "git.commit.sha:fd8163131f3150b86b792eee85eb583df81615da", + "git.repository_url:https://github.com/datadog/debugger-demos", + "host:i-02c9b2ab1d22671cd", + "host_name:debugger-demo-java-77886949f5-q6hmp", + "iam_profile:k8s/us1-staging-dog-stripe-kube-node_v2", + "image:ami-0fc22cd196ef840ea", + "image_id:727006795293.dkr.ecr.us-east-1.amazonaws.com/debugger-demo-services@sha256:bff807f964564f9b31db2e30bdea7acb50bbacde9c93beaf132f22f03d03d026", + "image_name:727006795293.dkr.ecr.us-east-1.amazonaws.com/debugger-demo-services", + "image_tag:debugger-demo-java-v84990627-fd816313", + "instance-type:m7g.16xlarge", + "instance_type:m7g.16xlarge", + "is_kube_cluster_experimental:false", + "k8s.io/cluster-autoscaler/enabled:yes", + "k8s.io/cluster-autoscaler/node-template/autoscaling-options/scaledownunneededtime:5m0s", + "k8s.io/cluster-autoscaler/node-template/autoscaling-options/scaledownutilizationthreshold:0.95", + "k8s.io/cluster-autoscaler/node-template/label/agent-profile.datadoghq.com/name:compute-nodeless-600m-v1", + "k8s.io/cluster-autoscaler/node-template/label/agent.datadoghq.com/datadogagentprofile:compute-nodeless-600m-v1", + "k8s.io/cluster-autoscaler/node-template/label/class:nodeless", + "k8s.io/cluster-autoscaler/node-template/label/node-role.kubernetes.io/compute", + "k8s.io/cluster-autoscaler/node-template/label/node-role.kubernetes.io/nodeless", + "k8s.io/cluster-autoscaler/node-template/label/node-role.kubernetes.io/nodeless-arm64-d-m7g-16xlarge", + "k8s.io/cluster-autoscaler/node-template/label/node.datadoghq.com/cpu_arch:arm64", + "k8s.io/cluster-autoscaler/node-template/label/node.datadoghq.com/flavor:standard", + "k8s.io/cluster-autoscaler/node-template/label/nodegroups.datadoghq.com/cluster-autoscaler:true", + "k8s.io/cluster-autoscaler/node-template/label/nodegroups.datadoghq.com/enable-eni-pd:true", + "k8s.io/cluster-autoscaler/node-template/label/nodegroups.datadoghq.com/local-storage:false", + "k8s.io/cluster-autoscaler/node-template/label/nodegroups.datadoghq.com/name:nodeless-arm64-d-m7g-16xlarge", + "k8s.io/cluster-autoscaler/node-template/label/nodegroups.datadoghq.com/namespace:kube-system", + "k8s.io/cluster-autoscaler/node-template/label/scalingset:cpu_arch-arm64", + "k8s.io/cluster-autoscaler/node-template/label/topology.ebs.csi.aws.com/zone:us-east-1c", + "k8s.io/cluster-autoscaler/node-template/resources/cpu:63870m", + "k8s.io/cluster-autoscaler/node-template/resources/ephemeral-storage:212137024683", + "k8s.io/cluster-autoscaler/node-template/resources/kubernetes.io/network-bandwidth:3576mi", + "k8s.io/cluster-autoscaler/node-template/resources/memory:252589250233", + "k8s.io/cluster-autoscaler/node-template/resources/pods:640", + "k8s.io/cluster-autoscaler/node-template/taint/cpu_arch:arm64:noschedule", + "k8s.io/cluster-autoscaler/node-template/taint/node:nodeless:noschedule", + "kernel:none", + "kube_cluster_name:stripe", + "kube_container_name:debugger-demo-java", + "kube_deployment:debugger-demo-java", + "kube_namespace:debugger-backend", + "kube_node:ip-10-128-94-120.ec2.internal", + "kube_node_role:compute", + "kube_node_role:nodeless", + "kube_node_role:nodeless-arm64-d-m7g-16xlarge", + "kube_ownerref_kind:replicaset", + "kube_ownerref_name:debugger-demo-java-77886949f5", + "kube_qos:guaranteed", + "kube_replica_set:debugger-demo-java-77886949f5", + "kube_service:debugger-demo-java", + "kubernetes.io/cluster/stripe:owned", + "kubernetes_cluster:stripe", + "name:kube-system_nodeless-arm64-d-m7g-16xlarge", + "ng_cluster_autoscaler:true", + "ng_local_storage:false", + "node.datadoghq.com/base-image:ubuntu_22_04", + "node.datadoghq.com/cgroup:v2", + "node.datadoghq.com/flavor:standard", + "node.datadoghq.com/version:v6-267-0", + "nodegroup:kube-system_nodeless-arm64-d-m7g-16xlarge", + "nodegroups.datadoghq.com/name:nodeless-arm64-d-m7g-16xlarge", + "nodegroups.datadoghq.com/namespace:kube-system", + "nodegroups.datadoghq.com/nodegroup-set:kube-system_nodeless-arm64", + "nodegroups.datadoghq.com/owner:k8s-dynamic-nodegroup-controller", + "orch_cluster_id:4c9f3702-c3bd-4d69-871b-cfa039a397df", + "pod_name:debugger-demo-java-77886949f5-q6hmp", + "pod_phase:running", + "region:us-east-1", + "role:kube-node", + "security-group-name:common", + "security-group-name:us1-staging-dog-stripe-k8s-node", + "security_group_name:common", + "security_group_name:us1-staging-dog-stripe-k8s-node", + "server.name:tomcat", + "server.type:tomcat", + "service:debugger-demo-java", + "short_image:debugger-demo-services", + "site:datad0g.com", + "source:dd_debugger", + "team", + "team:debugger", + "terraform.managed:true", + "terraform.module:cloud-inventory/terraform-modules/aws-network-security-group", + "terraform.module_version:1.1.0", + "terraform.path:cloud-inventory/aws/staging/network", + "version:debugger-demo-java-v84990627-fd816313" + ], + "host": "ip-10-128-94-120.ec2.internal-stripe", + "service": "debugger-demo-java", + "message": "Executed org.springframework.samples.petclinic.vet.VetController.showVetList, it took 13.236871ms", + "attributes": { + "service": "debugger-demo-java", + "logger": { + "thread_id": 67, + "method": "showVetList", + "thread_name": "http-nio-8080-exec-5", + "name": "org.springframework.samples.petclinic.vet.VetController", + "version": 2 + }, + "debugger": { + "snapshot": { + "stack": [ + { + "fileName": "VetController.java", + "function": "org.springframework.samples.petclinic.vet.VetController.showVetList", + "lineNumber": 131 + }, + { + "function": "jdk.internal.reflect.GeneratedMethodAccessor130.invoke", + "lineNumber": -1 + }, + { + "fileName": "DelegatingMethodAccessorImpl.java", + "function": "jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke", + "lineNumber": 43 + }, + { + "fileName": "Method.java", + "function": "java.lang.reflect.Method.invoke", + "lineNumber": 566 + }, + { + "fileName": "InvocableHandlerMethod.java", + "function": "org.springframework.web.method.support.InvocableHandlerMethod.doInvoke", + "lineNumber": 205 + }, + { + "fileName": "InvocableHandlerMethod.java", + "function": "org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest", + "lineNumber": 150 + }, + { + "fileName": "ServletInvocableHandlerMethod.java", + "function": "org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle", + "lineNumber": 117 + }, + { + "fileName": "RequestMappingHandlerAdapter.java", + "function": "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod", + "lineNumber": 895 + }, + { + "fileName": "RequestMappingHandlerAdapter.java", + "function": "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal", + "lineNumber": 808 + }, + { + "fileName": "AbstractHandlerMethodAdapter.java", + "function": "org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle", + "lineNumber": 87 + }, + { + "fileName": "DispatcherServlet.java", + "function": "org.springframework.web.servlet.DispatcherServlet.doDispatch", + "lineNumber": 1070 + }, + { + "fileName": "DispatcherServlet.java", + "function": "org.springframework.web.servlet.DispatcherServlet.doService", + "lineNumber": 963 + }, + { + "fileName": "FrameworkServlet.java", + "function": "org.springframework.web.servlet.FrameworkServlet.processRequest", + "lineNumber": 1006 + }, + { + "fileName": "FrameworkServlet.java", + "function": "org.springframework.web.servlet.FrameworkServlet.doGet", + "lineNumber": 898 + }, + { + "fileName": "HttpServlet.java", + "function": "javax.servlet.http.HttpServlet.service", + "lineNumber": 529 + }, + { + "fileName": "FrameworkServlet.java", + "function": "org.springframework.web.servlet.FrameworkServlet.service", + "lineNumber": 883 + }, + { + "fileName": "HttpServlet.java", + "function": "javax.servlet.http.HttpServlet.service", + "lineNumber": 623 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 197 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "WsFilter.java", + "function": "org.apache.tomcat.websocket.server.WsFilter.doFilter", + "lineNumber": 51 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "ResourceUrlEncodingFilter.java", + "function": "org.springframework.web.servlet.resource.ResourceUrlEncodingFilter.doFilter", + "lineNumber": 67 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "RequestContextFilter.java", + "function": "org.springframework.web.filter.RequestContextFilter.doFilterInternal", + "lineNumber": 100 + }, + { + "fileName": "OncePerRequestFilter.java", + "function": "org.springframework.web.filter.OncePerRequestFilter.doFilter", + "lineNumber": 117 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "FormContentFilter.java", + "function": "org.springframework.web.filter.FormContentFilter.doFilterInternal", + "lineNumber": 93 + }, + { + "fileName": "OncePerRequestFilter.java", + "function": "org.springframework.web.filter.OncePerRequestFilter.doFilter", + "lineNumber": 117 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "HandlerMappingResourceNameFilter.java", + "function": "datadog.trace.instrumentation.springweb.HandlerMappingResourceNameFilter.doFilterInternal", + "lineNumber": 57 + }, + { + "fileName": "OncePerRequestFilter.java", + "function": "org.springframework.web.filter.OncePerRequestFilter.doFilter", + "lineNumber": 117 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "WebMvcMetricsFilter.java", + "function": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal", + "lineNumber": 96 + }, + { + "fileName": "OncePerRequestFilter.java", + "function": "org.springframework.web.filter.OncePerRequestFilter.doFilter", + "lineNumber": 117 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "CharacterEncodingFilter.java", + "function": "org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal", + "lineNumber": 201 + }, + { + "fileName": "OncePerRequestFilter.java", + "function": "org.springframework.web.filter.OncePerRequestFilter.doFilter", + "lineNumber": 117 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "ServletRequestPathFilter.java", + "function": "org.springframework.web.filter.ServletRequestPathFilter.doFilter", + "lineNumber": 56 + }, + { + "fileName": "DelegatingFilterProxy.java", + "function": "org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate", + "lineNumber": 354 + }, + { + "fileName": "DelegatingFilterProxy.java", + "function": "org.springframework.web.filter.DelegatingFilterProxy.doFilter", + "lineNumber": 267 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter", + "lineNumber": 166 + }, + { + "fileName": "ApplicationFilterChain.java", + "function": "org.apache.catalina.core.ApplicationFilterChain.doFilter", + "lineNumber": 142 + }, + { + "fileName": "StandardWrapperValve.java", + "function": "org.apache.catalina.core.StandardWrapperValve.invoke", + "lineNumber": 166 + }, + { + "fileName": "StandardContextValve.java", + "function": "org.apache.catalina.core.StandardContextValve.invoke", + "lineNumber": 88 + }, + { + "fileName": "AuthenticatorBase.java", + "function": "org.apache.catalina.authenticator.AuthenticatorBase.invoke", + "lineNumber": 481 + }, + { + "fileName": "StandardHostValve.java", + "function": "org.apache.catalina.core.StandardHostValve.invoke", + "lineNumber": 127 + }, + { + "fileName": "ErrorReportValve.java", + "function": "org.apache.catalina.valves.ErrorReportValve.invoke", + "lineNumber": 83 + }, + { + "fileName": "StandardEngineValve.java", + "function": "org.apache.catalina.core.StandardEngineValve.invoke", + "lineNumber": 72 + }, + { + "fileName": "RemoteIpValve.java", + "function": "org.apache.catalina.valves.RemoteIpValve.invoke", + "lineNumber": 763 + }, + { + "fileName": "CoyoteAdapter.java", + "function": "org.apache.catalina.connector.CoyoteAdapter.service", + "lineNumber": 344 + }, + { + "fileName": "Http11Processor.java", + "function": "org.apache.coyote.http11.Http11Processor.service", + "lineNumber": 398 + }, + { + "fileName": "AbstractProcessorLight.java", + "function": "org.apache.coyote.AbstractProcessorLight.process", + "lineNumber": 63 + }, + { + "fileName": "AbstractProtocol.java", + "function": "org.apache.coyote.AbstractProtocol$ConnectionHandler.process", + "lineNumber": 935 + }, + { + "fileName": "NioEndpoint.java", + "function": "org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun", + "lineNumber": 1831 + }, + { + "fileName": "SocketProcessorBase.java", + "function": "org.apache.tomcat.util.net.SocketProcessorBase.run", + "lineNumber": 52 + }, + { + "fileName": "ThreadPoolExecutor.java", + "function": "org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker", + "lineNumber": 973 + }, + { + "fileName": "ThreadPoolExecutor.java", + "function": "org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run", + "lineNumber": 491 + }, + { + "fileName": "TaskThread.java", + "function": "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run", + "lineNumber": 63 + }, + { + "fileName": "Thread.java", + "function": "java.lang.Thread.run", + "lineNumber": 829 + } + ], + "captures": { + "return": { + "arguments": { + "this": { + "type": "org.springframework.samples.petclinic.vet.VetController", + "fields": { + "result": { + "type": "int", + "value": "0" + }, + "garbageStart": { + "type": "long", + "value": "1764937706330" + }, + "executor": { + "type": "java.util.concurrent.Executors$FinalizableDelegatedExecutorService", + "fields": { + "e": { + "type": "java.util.concurrent.ThreadPoolExecutor", + "fields": { + "termination": { + "notCapturedReason": "depth", + "type": "java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject" + }, + "handler": { + "notCapturedReason": "depth", + "type": "java.util.concurrent.ThreadPoolExecutor$AbortPolicy" + }, + "threadFactory": { + "notCapturedReason": "depth", + "type": "org.springframework.samples.petclinic.vet.VetController$$Lambda$1619/0x0000000100e47c40" + }, + "keepAliveTime": { + "type": "long", + "value": "0" + }, + "largestPoolSize": { + "type": "int", + "value": "1" + }, + "allowCoreThreadTimeOut": { + "type": "boolean", + "value": "false" + }, + "corePoolSize": { + "type": "int", + "value": "1" + }, + "ctl": { + "type": "java.util.concurrent.atomic.AtomicInteger", + "value": "-536870911" + }, + "completedTaskCount": { + "type": "long", + "value": "0" + }, + "workQueue": { + "notCapturedReason": "depth", + "type": "java.util.concurrent.LinkedBlockingQueue" + }, + "maximumPoolSize": { + "type": "int", + "value": "1" + }, + "workers": { + "notCapturedReason": "depth", + "type": "java.util.HashSet" + }, + "mainLock": { + "notCapturedReason": "depth", + "type": "java.util.concurrent.locks.ReentrantLock" + } + } + } + } + }, + "vetsPreloaded": { + "size": "6", + "elements": [ + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "James" + }, + "lastName": { + "type": "java.lang.String", + "value": "Carter" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "1" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Helen" + }, + "lastName": { + "type": "java.lang.String", + "value": "Leary" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "2" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Linda" + }, + "lastName": { + "type": "java.lang.String", + "value": "Douglas" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "3" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Rafael" + }, + "lastName": { + "type": "java.lang.String", + "value": "Ortega" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "4" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Henry" + }, + "lastName": { + "type": "java.lang.String", + "value": "Stevens" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "5" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Sharon" + }, + "lastName": { + "type": "java.lang.String", + "value": "Jenkins" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "6" + } + } + } + ], + "type": "java.util.ArrayList" + }, + "garbage": { + "size": "1", + "elements": [ + { + "size": "4096", + "elements": [ + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + }, + { + "notCapturedReason": "depth", + "type": "java.lang.Object" + } + ], + "notCapturedReason": "collectionSize", + "type": "java.lang.Object[]" + } + ], + "type": "java.util.ArrayList" + }, + "vetRepository": { + "type": "com.sun.proxy.$Proxy193", + "fields": { + "h": { + "type": "org.springframework.aop.framework.JdkDynamicAopProxy", + "fields": { + "equalsDefined": { + "type": "boolean", + "value": "false" + }, + "proxiedInterfaces": { + "notCapturedReason": "depth", + "type": "java.lang.Class[]" + }, + "hashCodeDefined": { + "type": "boolean", + "value": "false" + }, + "advised": { + "notCapturedReason": "depth", + "type": "org.springframework.aop.framework.ProxyFactory" + } + } + } + } + }, + "counter": { + "type": "int", + "value": "0" + }, + "syntheticLiveSet": { + "type": "java.util.concurrent.atomic.AtomicReference", + "fields": { + "value": { + "isNull": true, + "type": "java.lang.Object" + } + } + } + } + }, + "model": { + "type": "org.springframework.validation.support.BindingAwareModelMap", + "fields": { + "head": { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "isNull": true, + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "type": "java.lang.Integer", + "value": "2" + }, + "hash": { + "type": "int", + "value": "-719823880" + }, + "key": { + "type": "java.lang.String", + "value": "totalPages" + } + } + }, + "value": { + "type": "java.lang.Integer", + "value": "1" + }, + "hash": { + "type": "int", + "value": "601099388" + }, + "key": { + "type": "java.lang.String", + "value": "currentPage" + } + } + }, + "modCount": { + "type": "int", + "value": "4" + }, + "size": { + "type": "int", + "value": "4" + }, + "loadFactor": { + "type": "float", + "value": "0.75" + }, + "entrySet": { + "isNull": true, + "type": "java.util.Set" + }, + "tail": { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "type": "java.lang.Long", + "value": "6" + }, + "hash": { + "type": "int", + "value": "-725724574" + }, + "key": { + "type": "java.lang.String", + "value": "totalItems" + } + } + }, + "after": { + "isNull": true, + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "size": "5", + "elements": [ + { + "notCapturedReason": "depth", + "type": "org.springframework.samples.petclinic.vet.Vet" + }, + { + "notCapturedReason": "depth", + "type": "org.springframework.samples.petclinic.vet.Vet" + }, + { + "notCapturedReason": "depth", + "type": "org.springframework.samples.petclinic.vet.Vet" + }, + { + "notCapturedReason": "depth", + "type": "org.springframework.samples.petclinic.vet.Vet" + }, + { + "notCapturedReason": "depth", + "type": "org.springframework.samples.petclinic.vet.Vet" + } + ], + "type": "java.util.Collections$UnmodifiableRandomAccessList" + }, + "hash": { + "type": "int", + "value": "1345684249" + }, + "key": { + "type": "java.lang.String", + "value": "listVets" + } + } + }, + "values": { + "isNull": true, + "type": "java.util.Collection" + }, + "threshold": { + "type": "int", + "value": "12" + }, + "keySet": { + "isNull": true, + "type": "java.util.Set" + }, + "accessOrder": { + "type": "boolean", + "value": "false" + }, + "table": { + "size": "16", + "elements": [ + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "type": "java.lang.Long", + "value": "6" + }, + "hash": { + "type": "int", + "value": "-725724574" + }, + "key": { + "type": "java.lang.String", + "value": "totalItems" + } + } + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "type": "java.lang.Integer", + "value": "2" + }, + "hash": { + "type": "int", + "value": "-719823880" + }, + "key": { + "type": "java.lang.String", + "value": "totalPages" + } + } + }, + { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "isNull": true, + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "notCapturedReason": "depth", + "type": "java.util.Collections$UnmodifiableRandomAccessList" + }, + "hash": { + "type": "int", + "value": "1345684249" + }, + "key": { + "type": "java.lang.String", + "value": "listVets" + } + } + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "type": "java.util.LinkedHashMap$Entry", + "fields": { + "next": { + "isNull": true, + "type": "java.util.HashMap$Node" + }, + "before": { + "isNull": true, + "type": "java.util.LinkedHashMap$Entry" + }, + "after": { + "notCapturedReason": "depth", + "type": "java.util.LinkedHashMap$Entry" + }, + "value": { + "type": "java.lang.Integer", + "value": "1" + }, + "hash": { + "type": "int", + "value": "601099388" + }, + "key": { + "type": "java.lang.String", + "value": "currentPage" + } + } + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + }, + { + "isNull": true, + "type": "java.lang.Object" + } + ], + "type": "java.util.HashMap$Node[]" + } + } + }, + "page": { + "type": "int", + "value": "1" + }, + "uuid": { + "type": "java.lang.String", + "value": "bee79531-cd25-40c3-84cc-1eb7338dc8c0" + } + }, + "locals": { + "paginated": { + "type": "org.springframework.data.domain.PageImpl", + "fields": { + "total": { + "type": "long", + "value": "6" + }, + "pageable": { + "type": "org.springframework.data.domain.PageRequest", + "fields": { + "size": { + "type": "int", + "value": "5" + }, + "sort": { + "type": "org.springframework.data.domain.Sort", + "fields": { + "orders": { + "notCapturedReason": "depth", + "type": "java.util.Arrays$ArrayList" + } + } + }, + "page": { + "type": "int", + "value": "0" + } + } + }, + "content": { + "size": "5", + "elements": [ + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "James" + }, + "lastName": { + "type": "java.lang.String", + "value": "Carter" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "1" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Helen" + }, + "lastName": { + "type": "java.lang.String", + "value": "Leary" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "2" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Linda" + }, + "lastName": { + "type": "java.lang.String", + "value": "Douglas" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "3" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Rafael" + }, + "lastName": { + "type": "java.lang.String", + "value": "Ortega" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "4" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Henry" + }, + "lastName": { + "type": "java.lang.String", + "value": "Stevens" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "5" + } + } + } + ], + "type": "java.util.ArrayList" + } + } + }, + "vets": { + "type": "org.springframework.samples.petclinic.vet.Vets", + "fields": { + "vets": { + "size": "5", + "elements": [ + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "James" + }, + "lastName": { + "type": "java.lang.String", + "value": "Carter" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "1" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Helen" + }, + "lastName": { + "type": "java.lang.String", + "value": "Leary" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "2" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Linda" + }, + "lastName": { + "type": "java.lang.String", + "value": "Douglas" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "3" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Rafael" + }, + "lastName": { + "type": "java.lang.String", + "value": "Ortega" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "4" + } + } + }, + { + "type": "org.springframework.samples.petclinic.vet.Vet", + "fields": { + "firstName": { + "type": "java.lang.String", + "value": "Henry" + }, + "lastName": { + "type": "java.lang.String", + "value": "Stevens" + }, + "specialties": { + "notCapturedReason": "depth", + "type": "org.hibernate.collection.internal.PersistentSet" + }, + "id": { + "type": "java.lang.Integer", + "value": "5" + } + } + } + ], + "type": "java.util.ArrayList" + } + } + }, + "@return": { + "type": "java.lang.String", + "value": "vets/vetList" + } + } + } + }, + "language": "java", + "id": "baa4f52d-970f-46eb-acb7-bfe5fe466743", + "probe": { + "location": { + "method": "showVetList", + "type": "org.springframework.samples.petclinic.vet.VetController" + }, + "id": "c5ac1d07-3898-41ce-bedb-df609049593f", + "version": 0 + }, + "timestamp": 1764937706331 + } + }, + "dac": { + "dataset": "dd_debugger" + }, + "host": "ip-10-128-94-120.ec2.internal-stripe", + "host_id": 35184374449326, + "timestamp": { + "@": { + "logmatic": { + "date": { + "date": 1764937706331 + } + } + } + }, + "status": "info" + } + } +} \ No newline at end of file