From b839f8cb52b676241452dba00fa0b8b166cb3bfe Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Tue, 11 Jun 2024 18:00:51 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[RUM-4014]=20DD=5FLOGS:=20add=20han?= =?UTF-8?q?dling=20stack=20in=20beforeSend=20context=20(#2786)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/console/consoleObservable.ts | 2 +- packages/logs/src/boot/logsPublicApi.spec.ts | 3 +- packages/logs/src/boot/logsPublicApi.ts | 3 +- packages/logs/src/boot/preStartLogs.spec.ts | 7 +- packages/logs/src/boot/preStartLogs.ts | 6 +- packages/logs/src/boot/startLogs.spec.ts | 10 +- packages/logs/src/domain/assembly.spec.ts | 3 +- .../domain/console/consoleCollection.spec.ts | 24 +++- .../src/domain/console/consoleCollection.ts | 26 ++++- packages/logs/src/domain/logger.spec.ts | 42 ++++++- packages/logs/src/domain/logger.ts | 107 ++++++++++-------- .../logs/src/domain/logger/isAuthorized.ts | 36 ++++++ .../domain/logger/loggerCollection.spec.ts | 21 ++-- .../src/domain/logger/loggerCollection.ts | 42 +++---- .../networkErrorCollection.spec.ts | 7 +- .../networkError/networkErrorCollection.ts | 3 +- .../domain/report/reportCollection.spec.ts | 2 +- .../src/domain/report/reportCollection.ts | 2 +- packages/logs/src/domain/reportError.ts | 2 +- .../runtimeErrorCollection.spec.ts | 2 +- .../runtimeError/runtimeErrorCollection.ts | 2 +- packages/logs/src/domainContext.types.ts | 15 ++- packages/logs/src/entries/main.ts | 4 +- packages/logs/src/rawLogsEvent.types.ts | 2 +- test/e2e/lib/framework/createTest.ts | 5 + test/e2e/lib/framework/pageSetups.ts | 4 +- test/e2e/scenario/microfrontend.scenario.ts | 83 ++++++++++++-- 27 files changed, 340 insertions(+), 125 deletions(-) create mode 100644 packages/logs/src/domain/logger/isAuthorized.ts diff --git a/packages/core/src/domain/console/consoleObservable.ts b/packages/core/src/domain/console/consoleObservable.ts index 6bde7abb57..1d76edcec9 100644 --- a/packages/core/src/domain/console/consoleObservable.ts +++ b/packages/core/src/domain/console/consoleObservable.ts @@ -13,7 +13,7 @@ export interface ConsoleLog { message: string api: ConsoleApiName stack?: string - handlingStack?: string + handlingStack: string fingerprint?: string causes?: RawErrorCause[] } diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 4be9fea3dc..19ec379396 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,7 +1,8 @@ import type { TimeStamp } from '@datadog/browser-core' import { monitor, display, removeStorageListeners } from '@datadog/browser-core' import type { Logger, LogsMessage } from '../domain/logger' -import { HandlerType, StatusType } 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' diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index d9810f09f9..2c6397369c 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -16,7 +16,8 @@ import { createTrackingConsentState, } from '@datadog/browser-core' import type { LogsInitConfiguration } from '../domain/configuration' -import type { HandlerType, StatusType } from '../domain/logger' +import type { HandlerType } from '../domain/logger' +import type { StatusType } from '../domain/logger/isAuthorized' import { Logger } from '../domain/logger' import { buildCommonContext } from '../domain/contexts/commonContext' import type { InternalContext } from '../domain/contexts/internalContext' diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index a06b815875..98404d255c 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -3,7 +3,8 @@ import type { TimeStamp, TrackingConsentState } from '@datadog/browser-core' import { ONE_SECOND, TrackingConsent, createTrackingConsentState, display } from '@datadog/browser-core' import type { CommonContext } from '../rawLogsEvent.types' import type { HybridInitConfiguration, LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' -import { StatusType, type Logger } from '../domain/logger' +import type { Logger } from '../domain/logger' +import { StatusType } from '../domain/logger/isAuthorized' import type { Strategy } from './logsPublicApi' import { createPreStartStrategy } from './preStartLogs' import type { StartLogsResult } from './startLogs' @@ -21,8 +22,8 @@ describe('preStartLogs', () => { let clock: Clock function getLoggedMessage(index: number) { - const [message, logger, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) - return { message, logger, savedCommonContext, savedDate } + const [message, logger, handlingStack, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) + return { message, logger, handlingStack, savedCommonContext, savedDate } } beforeEach(() => { diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index db84989f46..98c8256d71 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -76,8 +76,10 @@ export function createPreStartStrategy( getInternalContext: noop as () => undefined, - handleLog(message, statusType, context = getCommonContext(), date = timeStampNow()) { - bufferApiCalls.add((startLogsResult) => startLogsResult.handleLog(message, statusType, context, date)) + handleLog(message, statusType, handlingStack, context = getCommonContext(), date = timeStampNow()) { + bufferApiCalls.add((startLogsResult) => + startLogsResult.handleLog(message, statusType, handlingStack, context, date) + ) }, } } diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 73dac10083..15e3de8260 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -27,7 +27,8 @@ import { import type { LogsConfiguration } from '../domain/configuration' import { validateAndBuildLogsConfiguration } from '../domain/configuration' -import { HandlerType, Logger, StatusType } from '../domain/logger' +import { HandlerType, Logger } from '../domain/logger' +import { StatusType } from '../domain/logger/isAuthorized' import type { startLoggerCollection } from '../domain/logger/loggerCollection' import type { LogsEvent } from '../logsEvent.types' import { startLogs } from './startLogs' @@ -94,7 +95,12 @@ describe('logs', () => { )) registerCleanupTask(stopLogs) - handleLog({ message: 'message', status: StatusType.warn, context: { foo: 'bar' } }, logger, COMMON_CONTEXT) + handleLog( + { message: 'message', status: StatusType.warn, context: { foo: 'bar' } }, + logger, + 'fake-handling-stack', + COMMON_CONTEXT + ) expect(requests.length).toEqual(1) expect(requests[0].url).toContain(baseConfiguration.logsEndpointBuilder.build('xhr', DEFAULT_PAYLOAD)) diff --git a/packages/logs/src/domain/assembly.spec.ts b/packages/logs/src/domain/assembly.spec.ts index ad320f731d..2f9a927786 100644 --- a/packages/logs/src/domain/assembly.spec.ts +++ b/packages/logs/src/domain/assembly.spec.ts @@ -14,7 +14,8 @@ import type { CommonContext } from '../rawLogsEvent.types' import { startLogsAssembly } from './assembly' import type { LogsConfiguration } from './configuration' import { validateAndBuildLogsConfiguration } from './configuration' -import { Logger, StatusType } from './logger' +import { Logger } from './logger' +import { StatusType } from './logger/isAuthorized' import type { LogsSessionManager } from './logsSessionManager' import { LifeCycle, LifeCycleEventType } from './lifeCycle' diff --git a/packages/logs/src/domain/console/consoleCollection.spec.ts b/packages/logs/src/domain/console/consoleCollection.spec.ts index e826f54b67..ed5d0527d8 100644 --- a/packages/logs/src/domain/console/consoleCollection.spec.ts +++ b/packages/logs/src/domain/console/consoleCollection.spec.ts @@ -1,5 +1,6 @@ import type { ErrorWithCause } from '@datadog/browser-core' -import { ErrorSource, noop, objectEntries } from '@datadog/browser-core' +import { ErrorSource, ExperimentalFeature, noop, objectEntries } from '@datadog/browser-core' +import { mockExperimentalFeatures } from '@datadog/browser-core/test' import type { RawConsoleLogsEvent } from '../../rawLogsEvent.types' import { validateAndBuildLogsConfiguration } from '../configuration' import type { RawLogsEventCollectedData } from '../lifeCycle' @@ -51,6 +52,27 @@ describe('console collection', () => { error: whatever(), }) + expect(rawLogsEvents[0].domainContext).not.toBeDefined() + + expect(consoleSpies[api]).toHaveBeenCalled() + }) + }) + + objectEntries(LogStatusForApi).forEach(([api]) => { + it(`should add domainContext to logs from console.${api}`, () => { + mockExperimentalFeatures([ExperimentalFeature.MICRO_FRONTEND]) + ;({ stop: stopConsoleCollection } = startConsoleCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: 'all' })!, + lifeCycle + )) + + /* eslint-disable-next-line no-console */ + console[api as keyof typeof LogStatusForApi]('foo', 'bar') + + expect(rawLogsEvents[0].domainContext).toEqual({ + handlingStack: jasmine.any(String), + }) + expect(consoleSpies[api]).toHaveBeenCalled() }) }) diff --git a/packages/logs/src/domain/console/consoleCollection.ts b/packages/logs/src/domain/console/consoleCollection.ts index 62f3b3cbc3..18b1b181b9 100644 --- a/packages/logs/src/domain/console/consoleCollection.ts +++ b/packages/logs/src/domain/console/consoleCollection.ts @@ -1,9 +1,17 @@ import type { Context, ClocksState, ConsoleLog } from '@datadog/browser-core' -import { timeStampNow, ConsoleApiName, ErrorSource, initConsoleObservable } from '@datadog/browser-core' +import { + timeStampNow, + ConsoleApiName, + ErrorSource, + initConsoleObservable, + isExperimentalFeatureEnabled, + ExperimentalFeature, +} from '@datadog/browser-core' import type { LogsConfiguration } from '../configuration' -import type { LifeCycle } from '../lifeCycle' +import type { LifeCycle, RawLogsEventCollectedData } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' +import type { RawLogsEvent } from '../../rawLogsEvent.types' export interface ProvidedError { startClocks: ClocksState @@ -21,7 +29,7 @@ export const LogStatusForApi = { } export function startConsoleCollection(configuration: LogsConfiguration, lifeCycle: LifeCycle) { const consoleSubscription = initConsoleObservable(configuration.forwardConsoleLogs).subscribe((log: ConsoleLog) => { - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + const collectedData: RawLogsEventCollectedData = { rawLogsEvent: { date: timeStampNow(), message: log.message, @@ -36,7 +44,15 @@ export function startConsoleCollection(configuration: LogsConfiguration, lifeCyc : undefined, status: LogStatusForApi[log.api], }, - }) + } + + if (isExperimentalFeatureEnabled(ExperimentalFeature.MICRO_FRONTEND)) { + collectedData.domainContext = { + handlingStack: log.handlingStack, + } + } + + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, collectedData) }) return { diff --git a/packages/logs/src/domain/logger.spec.ts b/packages/logs/src/domain/logger.spec.ts index 72f8f2d46d..0809100378 100644 --- a/packages/logs/src/domain/logger.spec.ts +++ b/packages/logs/src/domain/logger.spec.ts @@ -1,11 +1,12 @@ import type { ErrorWithCause } from '@datadog/browser-core' import { NO_ERROR_STACK_PRESENT_MESSAGE, createCustomerDataTracker, noop } from '@datadog/browser-core' import type { LogsMessage } from './logger' -import { HandlerType, Logger, STATUSES, StatusType } from './logger' +import { HandlerType, Logger, STATUSES } from './logger' +import { StatusType } from './logger/isAuthorized' describe('Logger', () => { let logger: Logger - let handleLogSpy: jasmine.Spy<(message: LogsMessage, logger: Logger) => void> + let handleLogSpy: jasmine.Spy<(message: LogsMessage, logger: Logger, handlingStack?: string) => void> function getLoggedMessage(index: number) { return handleLogSpy.calls.argsFor(index)[0] @@ -15,12 +16,20 @@ describe('Logger', () => { return handleLogSpy.calls.argsFor(index)[1] } + function getLoggedHandlingStack(index: number) { + return handleLogSpy.calls.argsFor(index)[2] + } + beforeEach(() => { handleLogSpy = jasmine.createSpy() logger = new Logger(handleLogSpy, createCustomerDataTracker(noop)) }) describe('log methods', () => { + beforeEach(() => { + logger.setLevel(StatusType.ok) + }) + it("'logger.log' should have info status by default", () => { logger.log('message') @@ -45,6 +54,35 @@ describe('Logger', () => { }, }) }) + + it(`'logger.${status}' should create an handling stack`, () => { + logger[status]('message') + + expect(getLoggedHandlingStack(0)).toBeDefined() + }) + + it(`'logger.${status}' should not create an handling stack if the handler is 'console'`, () => { + logger.setHandler(HandlerType.console) + logger[status]('message') + + expect(getLoggedHandlingStack(0)).not.toBeDefined() + }) + + it(`'logger.${status}' should not create an handling stack if the handler is 'silent'`, () => { + logger.setHandler(HandlerType.silent) + logger[status]('message') + + expect(getLoggedHandlingStack(0)).not.toBeDefined() + }) + }) + + it('should not create an handling stack if level is below the logger level', () => { + logger.setLevel(StatusType.warn) + logger.log('message') + logger.warn('message') + + expect(getLoggedHandlingStack(0)).not.toBeDefined() + expect(getLoggedHandlingStack(1)).toBeDefined() }) it("'logger.log' should send the log message", () => { diff --git a/packages/logs/src/domain/logger.ts b/packages/logs/src/domain/logger.ts index 8726a292dd..bbe9de17b3 100644 --- a/packages/logs/src/domain/logger.ts +++ b/packages/logs/src/domain/logger.ts @@ -10,9 +10,11 @@ import { monitored, sanitize, NonErrorPrefix, + createHandlingStack, } from '@datadog/browser-core' import type { RawLoggerLogsEvent } from '../rawLogsEvent.types' +import { isAuthorized, StatusType } from './logger/isAuthorized' export interface LogsMessage { message: string @@ -20,20 +22,6 @@ export interface LogsMessage { context?: Context } -export const StatusType = { - ok: 'ok', - debug: 'debug', - info: 'info', - notice: 'notice', - warn: 'warn', - error: 'error', - critical: 'critical', - alert: 'alert', - emerg: 'emerg', -} as const - -export type StatusType = (typeof StatusType)[keyof typeof StatusType] - export const HandlerType = { console: 'console', http: 'http', @@ -43,11 +31,13 @@ export const HandlerType = { export type HandlerType = (typeof HandlerType)[keyof typeof HandlerType] export const STATUSES = Object.keys(StatusType) as StatusType[] +// note: it is safe to merge declarations as long as the methods are actually defined on the prototype +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class Logger { private contextManager: ContextManager constructor( - private handleLogStrategy: (logsMessage: LogsMessage, logger: Logger) => void, + private handleLogStrategy: (logsMessage: LogsMessage, logger: Logger, handlingStack?: string) => void, customerDataTracker: CustomerDataTracker, name?: string, private handlerType: HandlerType | HandlerType[] = HandlerType.http, @@ -62,7 +52,13 @@ export class Logger { } @monitored - log(message: string, messageContext?: object, status: StatusType = StatusType.info, error?: Error) { + logImplementation( + message: string, + messageContext?: object, + status: StatusType = StatusType.info, + error?: Error, + handlingStack?: string + ) { let errorContext: RawLoggerLogsEvent['error'] if (error !== undefined && error !== null) { @@ -96,44 +92,19 @@ export class Logger { context, status, }, - this + this, + handlingStack ) } - ok(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.ok, error) - } - - debug(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.debug, error) - } - - info(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.info, error) - } - - notice(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.notice, error) - } - - warn(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.warn, error) - } - - error(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.error, error) - } - - critical(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.critical, error) - } + log(message: string, messageContext?: object, status: StatusType = StatusType.info, error?: Error) { + let handlingStack: string | undefined - alert(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.alert, error) - } + if (isAuthorized(status, HandlerType.http, this)) { + handlingStack = createHandlingStack() + } - emerg(message: string, messageContext?: object, error?: Error) { - this.log(message, messageContext, StatusType.emerg, error) + this.logImplementation(message, messageContext, status, error, handlingStack) } setContext(context: object) { @@ -172,3 +143,41 @@ export class Logger { return this.level } } + +/* eslint-disable local-rules/disallow-side-effects */ +Logger.prototype.ok = createLoggerMethod(StatusType.ok) +Logger.prototype.debug = createLoggerMethod(StatusType.debug) +Logger.prototype.info = createLoggerMethod(StatusType.info) +Logger.prototype.notice = createLoggerMethod(StatusType.notice) +Logger.prototype.warn = createLoggerMethod(StatusType.warn) +Logger.prototype.error = createLoggerMethod(StatusType.error) +Logger.prototype.critical = createLoggerMethod(StatusType.critical) +Logger.prototype.alert = createLoggerMethod(StatusType.alert) +Logger.prototype.emerg = createLoggerMethod(StatusType.emerg) +/* eslint-enable local-rules/disallow-side-effects */ + +// note: it is safe to merge declarations as long as the methods are actually defined on the prototype +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface Logger { + ok(message: string, messageContext?: object, error?: Error): void + debug(message: string, messageContext?: object, error?: Error): void + info(message: string, messageContext?: object, error?: Error): void + notice(message: string, messageContext?: object, error?: Error): void + warn(message: string, messageContext?: object, error?: Error): void + error(message: string, messageContext?: object, error?: Error): void + critical(message: string, messageContext?: object, error?: Error): void + alert(message: string, messageContext?: object, error?: Error): void + emerg(message: string, messageContext?: object, error?: Error): void +} + +function createLoggerMethod(status: StatusType) { + return function (this: Logger, message: string, messageContext?: object, error?: Error) { + let handlingStack: string | undefined + + if (isAuthorized(status, HandlerType.http, this)) { + handlingStack = createHandlingStack() + } + + this.logImplementation(message, messageContext, status, error, handlingStack) + } +} diff --git a/packages/logs/src/domain/logger/isAuthorized.ts b/packages/logs/src/domain/logger/isAuthorized.ts new file mode 100644 index 0000000000..bc57e66d2b --- /dev/null +++ b/packages/logs/src/domain/logger/isAuthorized.ts @@ -0,0 +1,36 @@ +import { includes } from '@datadog/browser-core' +import type { Logger, HandlerType } from '../logger' + +export function isAuthorized(status: StatusType, handlerType: HandlerType, logger: Logger) { + const loggerHandler = logger.getHandler() + const sanitizedHandlerType = Array.isArray(loggerHandler) ? loggerHandler : [loggerHandler] + return ( + STATUS_PRIORITIES[status] >= STATUS_PRIORITIES[logger.getLevel()] && includes(sanitizedHandlerType, handlerType) + ) +} + +export const StatusType = { + ok: 'ok', + debug: 'debug', + info: 'info', + notice: 'notice', + warn: 'warn', + error: 'error', + critical: 'critical', + alert: 'alert', + emerg: 'emerg', +} as const + +export const STATUS_PRIORITIES: { [key in StatusType]: number } = { + [StatusType.ok]: 0, + [StatusType.debug]: 1, + [StatusType.info]: 2, + [StatusType.notice]: 4, + [StatusType.warn]: 5, + [StatusType.error]: 6, + [StatusType.critical]: 7, + [StatusType.alert]: 8, + [StatusType.emerg]: 9, +} + +export type StatusType = (typeof StatusType)[keyof typeof StatusType] diff --git a/packages/logs/src/domain/logger/loggerCollection.spec.ts b/packages/logs/src/domain/logger/loggerCollection.spec.ts index 48764cbbda..61d82d7b04 100644 --- a/packages/logs/src/domain/logger/loggerCollection.spec.ts +++ b/packages/logs/src/domain/logger/loggerCollection.spec.ts @@ -12,9 +12,11 @@ import { mockClock } from '@datadog/browser-core/test' import type { CommonContext, RawLoggerLogsEvent } from '../../rawLogsEvent.types' import type { RawLogsEventCollectedData } from '../lifeCycle' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' -import { HandlerType, Logger, StatusType } from '../logger' +import { HandlerType, Logger } from '../logger' +import { StatusType } from './isAuthorized' import { startLoggerCollection } from './loggerCollection' +const HANDLING_STACK = 'handlingStack' const COMMON_CONTEXT = {} as CommonContext const FAKE_DATE = 1234 as TimeStamp @@ -57,6 +59,7 @@ describe('logger collection', () => { handleLog( { message: 'message', status: StatusType.error, context: { bar: 'from-message' } }, logger, + HANDLING_STACK, COMMON_CONTEXT ) @@ -79,7 +82,7 @@ describe('logger collection', () => { ]) { it(`should use console.${api} to log messages with status ${status}`, () => { logger.setLevel(StatusType.ok) - handleLog({ message: 'message', status }, logger, COMMON_CONTEXT) + handleLog({ message: 'message', status }, logger, HANDLING_STACK, COMMON_CONTEXT) expect(originalConsoleMethods[api]).toHaveBeenCalled() }) @@ -87,13 +90,13 @@ describe('logger collection', () => { it('does not print the log if its status is below the logger level', () => { logger.setLevel(StatusType.warn) - handleLog({ message: 'message', status: StatusType.info }, logger, COMMON_CONTEXT) + handleLog({ message: 'message', status: StatusType.info }, logger, HANDLING_STACK, COMMON_CONTEXT) expect(originalConsoleMethods.info).not.toHaveBeenCalled() }) it('does not print the log and does not crash if its status is unknown', () => { - handleLog({ message: 'message', status: 'unknown' as StatusType }, logger, COMMON_CONTEXT) + handleLog({ message: 'message', status: 'unknown' as StatusType }, logger, HANDLING_STACK, COMMON_CONTEXT) expect(originalConsoleMethods.info).not.toHaveBeenCalled() expect(originalConsoleMethods.log).not.toHaveBeenCalled() @@ -114,6 +117,7 @@ describe('logger collection', () => { handleLog( { message: 'message', status: StatusType.error, context: { bar: 'from-message' } }, logger, + HANDLING_STACK, COMMON_CONTEXT ) @@ -129,24 +133,27 @@ describe('logger collection', () => { bar: 'from-message', }, savedCommonContext: COMMON_CONTEXT, + domainContext: { + handlingStack: HANDLING_STACK, + }, }) }) it('should send the saved date when present', () => { - handleLog({ message: 'message', status: StatusType.error }, logger, COMMON_CONTEXT, FAKE_DATE) + handleLog({ message: 'message', status: StatusType.error }, logger, HANDLING_STACK, COMMON_CONTEXT, FAKE_DATE) expect(rawLogsEvents[0].rawLogsEvent.date).toEqual(FAKE_DATE) }) it('does not send the log if its status is below the logger level', () => { logger.setLevel(StatusType.warn) - handleLog({ message: 'message', status: StatusType.info }, logger, COMMON_CONTEXT) + handleLog({ message: 'message', status: StatusType.info }, logger, HANDLING_STACK, COMMON_CONTEXT) expect(rawLogsEvents.length).toBe(0) }) it('does not send the log and does not crash if its status is unknown', () => { - handleLog({ message: 'message', status: 'unknown' as StatusType }, logger, COMMON_CONTEXT) + handleLog({ message: 'message', status: 'unknown' as StatusType }, logger, HANDLING_STACK, COMMON_CONTEXT) expect(rawLogsEvents.length).toBe(0) }) diff --git a/packages/logs/src/domain/logger/loggerCollection.ts b/packages/logs/src/domain/logger/loggerCollection.ts index 5d1a29dc15..9e065fcd44 100644 --- a/packages/logs/src/domain/logger/loggerCollection.ts +++ b/packages/logs/src/domain/logger/loggerCollection.ts @@ -1,35 +1,24 @@ import type { Context, TimeStamp } from '@datadog/browser-core' import { - ConsoleApiName, - includes, combine, ErrorSource, timeStampNow, originalConsoleMethods, globalConsole, + ConsoleApiName, } from '@datadog/browser-core' -import type { CommonContext } from '../../rawLogsEvent.types' -import type { LifeCycle } from '../lifeCycle' +import type { CommonContext, RawLogsEvent } from '../../rawLogsEvent.types' +import type { LifeCycle, RawLogsEventCollectedData } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' import type { Logger, LogsMessage } from '../logger' -import { StatusType, HandlerType } from '../logger' - -export const STATUS_PRIORITIES: { [key in StatusType]: number } = { - [StatusType.ok]: 0, - [StatusType.debug]: 1, - [StatusType.info]: 2, - [StatusType.notice]: 4, - [StatusType.warn]: 5, - [StatusType.error]: 6, - [StatusType.critical]: 7, - [StatusType.alert]: 8, - [StatusType.emerg]: 9, -} +import { HandlerType } from '../logger' +import { isAuthorized, StatusType } from './isAuthorized' export function startLoggerCollection(lifeCycle: LifeCycle) { function handleLog( logsMessage: LogsMessage, logger: Logger, + handlingStack?: string, savedCommonContext?: CommonContext, savedDate?: TimeStamp ) { @@ -40,7 +29,7 @@ export function startLoggerCollection(lifeCycle: LifeCycle) { } if (isAuthorized(logsMessage.status, HandlerType.http, logger)) { - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + const rawLogEventData: RawLogsEventCollectedData = { rawLogsEvent: { date: savedDate || timeStampNow(), message: logsMessage.message, @@ -49,7 +38,13 @@ export function startLoggerCollection(lifeCycle: LifeCycle) { }, messageContext, savedCommonContext, - }) + } + + if (handlingStack) { + rawLogEventData.domainContext = { handlingStack } + } + + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, rawLogEventData) } } @@ -57,15 +52,6 @@ export function startLoggerCollection(lifeCycle: LifeCycle) { handleLog, } } - -export function isAuthorized(status: StatusType, handlerType: HandlerType, logger: Logger) { - const loggerHandler = logger.getHandler() - const sanitizedHandlerType = Array.isArray(loggerHandler) ? loggerHandler : [loggerHandler] - return ( - STATUS_PRIORITIES[status] >= STATUS_PRIORITIES[logger.getLevel()] && includes(sanitizedHandlerType, handlerType) - ) -} - const loggerToConsoleApiName: { [key in StatusType]: ConsoleApiName } = { [StatusType.ok]: ConsoleApiName.debug, [StatusType.debug]: ConsoleApiName.debug, diff --git a/packages/logs/src/domain/networkError/networkErrorCollection.spec.ts b/packages/logs/src/domain/networkError/networkErrorCollection.spec.ts index 7151b0deb5..0f71637c30 100644 --- a/packages/logs/src/domain/networkError/networkErrorCollection.spec.ts +++ b/packages/logs/src/domain/networkError/networkErrorCollection.spec.ts @@ -5,7 +5,7 @@ import type { RawNetworkLogsEvent } from '../../rawLogsEvent.types' import type { LogsConfiguration } from '../configuration' import type { RawLogsEventCollectedData } from '../lifeCycle' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' import { computeFetchErrorText, @@ -110,7 +110,10 @@ describe('network error collection', () => { fetchStubManager.whenAllComplete(() => { expect(rawLogsEvents.length).toEqual(1) - expect(rawLogsEvents[0].domainContext).toEqual({ isAborted: true }) + expect(rawLogsEvents[0].domainContext).toEqual({ + isAborted: true, + handlingStack: jasmine.any(String), + }) done() }) }) diff --git a/packages/logs/src/domain/networkError/networkErrorCollection.ts b/packages/logs/src/domain/networkError/networkErrorCollection.ts index ab2a4cb409..eb33d813ce 100644 --- a/packages/logs/src/domain/networkError/networkErrorCollection.ts +++ b/packages/logs/src/domain/networkError/networkErrorCollection.ts @@ -16,7 +16,7 @@ import type { LogsConfiguration } from '../configuration' import type { LifeCycle } from '../lifeCycle' import type { LogsEventDomainContext } from '../../domainContext.types' import { LifeCycleEventType } from '../lifeCycle' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' export function startNetworkErrorCollection(configuration: LogsConfiguration, lifeCycle: LifeCycle) { if (!configuration.forwardErrorsToLogs) { @@ -48,6 +48,7 @@ export function startNetworkErrorCollection(configuration: LogsConfiguration, li function onResponseDataAvailable(responseData: unknown) { const domainContext: LogsEventDomainContext = { isAborted: request.isAborted, + handlingStack: request.handlingStack, } lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { diff --git a/packages/logs/src/domain/report/reportCollection.spec.ts b/packages/logs/src/domain/report/reportCollection.spec.ts index d5130e4041..6e42916765 100644 --- a/packages/logs/src/domain/report/reportCollection.spec.ts +++ b/packages/logs/src/domain/report/reportCollection.spec.ts @@ -4,7 +4,7 @@ import type { RawReportLogsEvent } from '../../rawLogsEvent.types' import { validateAndBuildLogsConfiguration } from '../configuration' import type { RawLogsEventCollectedData } from '../lifeCycle' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' import { startReportCollection } from './reportCollection' describe('reports', () => { diff --git a/packages/logs/src/domain/report/reportCollection.ts b/packages/logs/src/domain/report/reportCollection.ts index e0d0dee341..c48a165921 100644 --- a/packages/logs/src/domain/report/reportCollection.ts +++ b/packages/logs/src/domain/report/reportCollection.ts @@ -9,7 +9,7 @@ import { import type { LogsConfiguration } from '../configuration' import type { LifeCycle } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' export interface ProvidedError { startClocks: ClocksState diff --git a/packages/logs/src/domain/reportError.ts b/packages/logs/src/domain/reportError.ts index 382c9dc3c3..e7029d6163 100644 --- a/packages/logs/src/domain/reportError.ts +++ b/packages/logs/src/domain/reportError.ts @@ -2,7 +2,7 @@ import type { RawError } from '@datadog/browser-core' import { ErrorSource, addTelemetryDebug } from '@datadog/browser-core' import type { LifeCycle } from './lifeCycle' import { LifeCycleEventType } from './lifeCycle' -import { StatusType } from './logger' +import { StatusType } from './logger/isAuthorized' export function startReportError(lifeCycle: LifeCycle) { return (error: RawError) => { diff --git a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts index f0516a773c..2a7a4f945f 100644 --- a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts +++ b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts @@ -2,7 +2,7 @@ import type { ErrorWithCause } from '@datadog/browser-core' import { ErrorSource } from '@datadog/browser-core' import type { RawRuntimeLogsEvent } from '../../rawLogsEvent.types' import type { LogsConfiguration } from '../configuration' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' import type { RawLogsEventCollectedData } from '../lifeCycle' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' import { startRuntimeErrorCollection } from './runtimeErrorCollection' diff --git a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts index 7345c02823..80920a62a3 100644 --- a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts +++ b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts @@ -3,7 +3,7 @@ import { noop, ErrorSource, trackRuntimeError, Observable } from '@datadog/brows import type { LogsConfiguration } from '../configuration' import type { LifeCycle } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' -import { StatusType } from '../logger' +import { StatusType } from '../logger/isAuthorized' export interface ProvidedError { startClocks: ClocksState diff --git a/packages/logs/src/domainContext.types.ts b/packages/logs/src/domainContext.types.ts index 1df7217422..6dcecad6f5 100644 --- a/packages/logs/src/domainContext.types.ts +++ b/packages/logs/src/domainContext.types.ts @@ -2,8 +2,21 @@ import type { ErrorSource } from '@datadog/browser-core' export type LogsEventDomainContext = T extends typeof ErrorSource.NETWORK ? NetworkLogsEventDomainContext - : never + : T extends typeof ErrorSource.CONSOLE + ? ConsoleLogsEventDomainContext + : T extends typeof ErrorSource.LOGGER + ? LoggerLogsEventDomainContext + : never export type NetworkLogsEventDomainContext = { isAborted: boolean + handlingStack?: string +} + +export type ConsoleLogsEventDomainContext = { + handlingStack: string +} + +export type LoggerLogsEventDomainContext = { + handlingStack: string } diff --git a/packages/logs/src/entries/main.ts b/packages/logs/src/entries/main.ts index d5611596a5..933d529ac0 100644 --- a/packages/logs/src/entries/main.ts +++ b/packages/logs/src/entries/main.ts @@ -3,10 +3,12 @@ import type { LogsPublicApi } from '../boot/logsPublicApi' import { makeLogsPublicApi } from '../boot/logsPublicApi' import { startLogs } from '../boot/startLogs' -export { Logger, LogsMessage, StatusType, HandlerType } from '../domain/logger' +export { Logger, LogsMessage, HandlerType } from '../domain/logger' +export { StatusType } from '../domain/logger/isAuthorized' export { LoggerConfiguration, LogsPublicApi as LogsGlobal } from '../boot/logsPublicApi' export { LogsInitConfiguration } from '../domain/configuration' export { LogsEvent } from '../logsEvent.types' +export { LogsEventDomainContext } from '../domainContext.types' export const datadogLogs = makeLogsPublicApi(startLogs) diff --git a/packages/logs/src/rawLogsEvent.types.ts b/packages/logs/src/rawLogsEvent.types.ts index d172f52285..afbc0ffa29 100644 --- a/packages/logs/src/rawLogsEvent.types.ts +++ b/packages/logs/src/rawLogsEvent.types.ts @@ -1,5 +1,5 @@ import type { Context, ErrorSource, RawErrorCause, TimeStamp, User } from '@datadog/browser-core' -import type { StatusType } from './domain/logger' +import type { StatusType } from './domain/logger/isAuthorized' export type RawLogsEvent = | RawConsoleLogsEvent diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index 59bde70f26..8ef9c684f6 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -73,6 +73,11 @@ class TestBuilder { return this } + withLogsInit(logsInit: (initConfiguration: LogsInitConfiguration) => void) { + this.logsInit = logsInit + return this + } + withLogs(logsInitConfiguration?: Partial) { this.logsConfiguration = { ...DEFAULT_LOGS_CONFIGURATION, ...logsInitConfiguration } return this diff --git a/test/e2e/lib/framework/pageSetups.ts b/test/e2e/lib/framework/pageSetups.ts index 56fdc6349c..ee794e5c65 100644 --- a/test/e2e/lib/framework/pageSetups.ts +++ b/test/e2e/lib/framework/pageSetups.ts @@ -61,7 +61,7 @@ n=o.getElementsByTagName(u)[0];n.parentNode.insertBefore(d,n) ${formatSnippet('./datadog-logs.js', 'DD_LOGS')} DD_LOGS.onReady(function () { DD_LOGS.setGlobalContext(${JSON.stringify(options.context)}) - DD_LOGS.init(${formatConfiguration(options.logs, servers)}) + ;(${options.logsInit.toString()})(${formatConfiguration(options.logs, servers)}) }) ` @@ -97,7 +97,7 @@ export function bundleSetup(options: SetupOptions, servers: Servers) { ` } diff --git a/test/e2e/scenario/microfrontend.scenario.ts b/test/e2e/scenario/microfrontend.scenario.ts index 02874cf10c..d9bb7ff54c 100644 --- a/test/e2e/scenario/microfrontend.scenario.ts +++ b/test/e2e/scenario/microfrontend.scenario.ts @@ -1,10 +1,11 @@ -import type { RumEvent, RumEventDomainContext, RumInitConfiguration } from '@datadog/browser-rum-core/src' -import { withBrowserLogs } from '../lib/helpers/browser' +import type { RumEvent, RumEventDomainContext, RumInitConfiguration } from '@datadog/browser-rum-core' +import type { LogsEvent, LogsInitConfiguration, LogsEventDomainContext } from '@datadog/browser-logs' +import { flushBrowserLogs, withBrowserLogs } from '../lib/helpers/browser' import { flushEvents, createTest } from '../lib/framework' const HANDLING_STACK_REGEX = /^Error: \n\s+at testHandlingStack @/ -const CONFIG: Partial = { +const RUM_CONFIG: Partial = { service: 'main-service', version: '1.0.0', enableExperimentalFeatures: ['micro_frontend'], @@ -17,9 +18,21 @@ const CONFIG: Partial = { }, } +const LOGS_CONFIG: Partial = { + forwardConsoleLogs: 'all', + enableExperimentalFeatures: ['micro_frontend'], + beforeSend: (event: LogsEvent, domainContext: LogsEventDomainContext) => { + if (domainContext && 'handlingStack' in domainContext) { + event.context = { handlingStack: domainContext.handlingStack } + } + + return true + }, +} + describe('microfrontend', () => { createTest('expose handling stack for fetch requests') - .withRum(CONFIG) + .withRum(RUM_CONFIG) .withRumInit((configuration) => { window.DD_RUM!.init(configuration) @@ -40,7 +53,7 @@ describe('microfrontend', () => { }) createTest('expose handling stack for xhr requests') - .withRum(CONFIG) + .withRum(RUM_CONFIG) .withRumInit((configuration) => { window.DD_RUM!.init(configuration) @@ -62,7 +75,7 @@ describe('microfrontend', () => { }) createTest('expose handling stack for DD_RUM.addAction') - .withRum(CONFIG) + .withRum(RUM_CONFIG) .withRumInit((configuration) => { window.DD_RUM!.init(configuration) @@ -82,7 +95,7 @@ describe('microfrontend', () => { }) createTest('expose handling stack for DD_RUM.addError') - .withRum(CONFIG) + .withRum(RUM_CONFIG) .withRumInit((configuration) => { window.DD_RUM!.init(configuration) @@ -102,7 +115,7 @@ describe('microfrontend', () => { }) createTest('expose handling stack for console errors') - .withRum(CONFIG) + .withRum(RUM_CONFIG) .withRumInit((configuration) => { window.DD_RUM!.init(configuration) @@ -126,8 +139,60 @@ describe('microfrontend', () => { expect(event?.context?.handlingStack).toMatch(HANDLING_STACK_REGEX) }) + describe('console apis', () => { + createTest('expose handling stack for console.log') + .withLogs(LOGS_CONFIG) + .withLogsInit((configuration) => { + window.DD_LOGS!.init(configuration) + + function testHandlingStack() { + console.log('foo') + } + + testHandlingStack() + }) + .run(async ({ intakeRegistry }) => { + await flushEvents() + + const event = intakeRegistry.logsEvents[0] + + await flushBrowserLogs() + + expect(event).toBeTruthy() + expect(event?.context).toEqual({ + handlingStack: jasmine.stringMatching(HANDLING_STACK_REGEX), + }) + }) + }) + + describe('logger apis', () => { + createTest('expose handling stack for DD_LOGS.logger.log') + .withLogs(LOGS_CONFIG) + .withLogsInit((configuration) => { + window.DD_LOGS!.init(configuration) + + function testHandlingStack() { + window.DD_LOGS!.logger.log('foo') + } + + testHandlingStack() + }) + .run(async ({ intakeRegistry }) => { + await flushEvents() + + const event = intakeRegistry.logsEvents[0] + + await flushBrowserLogs() + + expect(event).toBeTruthy() + expect(event?.context).toEqual({ + handlingStack: jasmine.stringMatching(HANDLING_STACK_REGEX), + }) + }) + }) + createTest('allow to modify service and version') - .withRum(CONFIG) + .withRum(RUM_CONFIG) .withRumInit((configuration) => { window.DD_RUM!.init({ ...configuration,