From bfc5570e94cf5d3a85b0293538768686acdbaf0c Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 6 Aug 2024 17:32:22 -0400 Subject: [PATCH] feat(replay): Add a replay-specific logger Removes the old `logInfo` function and replaces it with a new replay-specific logger. Configuration is done when the replay integration is first initialized to avoid needing to pass around configuration options. This also means that we cannot select individual log statements to be added as breadcrumbs with `traceInternals` options. This also adds a `logger.exception` that wraps `captureException`. Note that only the following logging levels are supported: * info * log * warn * error With two additions: * exception * infoTick (needs a better name) - same as `info` but adds the breadcrumb in the next tick due to some pre-existing race conditions There are two configuration options: * enable(/disable)CaptureInternalExceptions * enable(/disable)TraceInternals --- .../src/coreHandlers/handleGlobalEvent.ts | 4 +- .../coreHandlers/handleNetworkBreadcrumbs.ts | 3 +- .../src/coreHandlers/util/fetchUtils.ts | 14 ++- .../src/coreHandlers/util/networkUtils.ts | 10 +- .../src/coreHandlers/util/xhrUtils.ts | 14 ++- .../EventBufferCompressionWorker.ts | 5 +- .../src/eventBuffer/EventBufferProxy.ts | 9 +- .../src/eventBuffer/WorkerHandler.ts | 8 +- .../replay-internal/src/eventBuffer/index.ts | 10 +- packages/replay-internal/src/replay.ts | 78 ++++++------- .../src/session/fetchSession.ts | 7 +- .../src/session/loadOrCreateSession.ts | 11 +- packages/replay-internal/src/util/addEvent.ts | 13 +-- .../src/util/handleRecordingEmit.ts | 11 +- packages/replay-internal/src/util/log.ts | 54 --------- packages/replay-internal/src/util/logger.ts | 105 ++++++++++++++++++ .../src/util/sendReplayRequest.ts | 5 +- .../test/integration/flush.test.ts | 13 ++- 18 files changed, 211 insertions(+), 163 deletions(-) delete mode 100644 packages/replay-internal/src/util/log.ts create mode 100644 packages/replay-internal/src/util/logger.ts diff --git a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts index 88651d449fe6..a13f4d24827e 100644 --- a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts @@ -1,10 +1,10 @@ import type { Event, EventHint } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { ReplayContainer } from '../types'; import { isErrorEvent, isFeedbackEvent, isReplayEvent, isTransactionEvent } from '../util/eventUtils'; import { isRrwebError } from '../util/isRrwebError'; +import { logger } from '../util/logger'; import { addFeedbackBreadcrumb } from './util/addFeedbackBreadcrumb'; import { shouldSampleForBufferEvent } from './util/shouldSampleForBufferEvent'; @@ -50,7 +50,7 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even // Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb // As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users if (isRrwebError(event, hint) && !replay.getOptions()._experiments.captureExceptions) { - DEBUG_BUILD && logger.log('[Replay] Ignoring error from rrweb internals', event); + DEBUG_BUILD && logger.log('Ignoring error from rrweb internals', event); return null; } diff --git a/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts index a31fc046b17a..fb6d0c017943 100644 --- a/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -1,9 +1,9 @@ import { getClient } from '@sentry/core'; import type { Breadcrumb, BreadcrumbHint, FetchBreadcrumbData, XhrBreadcrumbData } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { FetchHint, ReplayContainer, ReplayNetworkOptions, XhrHint } from '../types'; +import { logger } from '../util/logger'; import { captureFetchBreadcrumbToReplay, enrichFetchBreadcrumb } from './util/fetchUtils'; import { captureXhrBreadcrumbToReplay, enrichXhrBreadcrumb } from './util/xhrUtils'; @@ -80,6 +80,7 @@ export function beforeAddNetworkBreadcrumb( } } catch (e) { DEBUG_BUILD && logger.warn('Error when enriching network breadcrumb'); + DEBUG_BUILD && logger.exception(e); } } diff --git a/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts b/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts index b5c2c3c36305..c03ab9e62eb0 100644 --- a/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts @@ -1,6 +1,5 @@ import { setTimeout } from '@sentry-internal/browser-utils'; import type { Breadcrumb, FetchBreadcrumbData } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../debug-build'; import type { @@ -11,6 +10,7 @@ import type { ReplayNetworkRequestData, ReplayNetworkRequestOrResponse, } from '../../types'; +import { logger } from '../../util/logger'; import { addNetworkBreadcrumb } from './addNetworkBreadcrumb'; import { buildNetworkRequestOrResponse, @@ -42,7 +42,8 @@ export async function captureFetchBreadcrumbToReplay( const result = makeNetworkReplayBreadcrumb('resource.fetch', data); addNetworkBreadcrumb(options.replay, result); } catch (error) { - DEBUG_BUILD && logger.error('[Replay] Failed to capture fetch breadcrumb', error); + DEBUG_BUILD && logger.error('Failed to capture fetch breadcrumb'); + DEBUG_BUILD && logger.exception(error); } } @@ -192,7 +193,8 @@ function getResponseData( return buildNetworkRequestOrResponse(headers, size, undefined); } catch (error) { - DEBUG_BUILD && logger.warn('[Replay] Failed to serialize response body', error); + DEBUG_BUILD && logger.warn('Failed to serialize response body'); + DEBUG_BUILD && logger.exception(error); // fallback return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); } @@ -209,7 +211,8 @@ async function _parseFetchResponseBody(response: Response): Promise<[string | un const text = await _tryGetResponseText(res); return [text]; } catch (error) { - DEBUG_BUILD && logger.warn('[Replay] Failed to get text body from response', error); + DEBUG_BUILD && logger.warn('Failed to get text body from response'); + DEBUG_BUILD && logger.exception(error); return [undefined, 'BODY_PARSE_ERROR']; } } @@ -279,7 +282,8 @@ function _tryCloneResponse(response: Response): Response | void { return response.clone(); } catch (error) { // this can throw if the response was already consumed before - DEBUG_BUILD && logger.warn('[Replay] Failed to clone response body', error); + DEBUG_BUILD && logger.warn('Failed to clone response body'); + DEBUG_BUILD && logger.exception(error); } } diff --git a/packages/replay-internal/src/coreHandlers/util/networkUtils.ts b/packages/replay-internal/src/coreHandlers/util/networkUtils.ts index 06e96b7ab7df..b438f5924333 100644 --- a/packages/replay-internal/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay-internal/src/coreHandlers/util/networkUtils.ts @@ -1,4 +1,4 @@ -import { dropUndefinedKeys, logger, stringMatchesSomePattern } from '@sentry/utils'; +import { dropUndefinedKeys, stringMatchesSomePattern } from '@sentry/utils'; import { NETWORK_BODY_MAX_SIZE, WINDOW } from '../../constants'; import { DEBUG_BUILD } from '../../debug-build'; @@ -10,6 +10,7 @@ import type { ReplayNetworkRequestOrResponse, ReplayPerformanceEntry, } from '../../types'; +import { logger } from '../../util/logger'; /** Get the size of a body. */ export function getBodySize(body: RequestInit['body']): number | undefined { @@ -77,12 +78,13 @@ export function getBodyString(body: unknown): [string | undefined, NetworkMetaWa if (!body) { return [undefined]; } - } catch { - DEBUG_BUILD && logger.warn('[Replay] Failed to serialize body', body); + } catch (error) { + DEBUG_BUILD && logger.warn('Failed to serialize body', body); + DEBUG_BUILD && logger.exception(error); return [undefined, 'BODY_PARSE_ERROR']; } - DEBUG_BUILD && logger.info('[Replay] Skipping network body because of body type', body); + DEBUG_BUILD && logger.info('Skipping network body because of body type', body); return [undefined, 'UNPARSEABLE_BODY_TYPE']; } diff --git a/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts b/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts index b86e2d2991a9..dfeb53b720b3 100644 --- a/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts +++ b/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts @@ -1,6 +1,5 @@ import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; import type { Breadcrumb, XhrBreadcrumbData } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../debug-build'; import type { @@ -10,6 +9,7 @@ import type { ReplayNetworkRequestData, XhrHint, } from '../../types'; +import { logger } from '../../util/logger'; import { addNetworkBreadcrumb } from './addNetworkBreadcrumb'; import { buildNetworkRequestOrResponse, @@ -39,7 +39,8 @@ export async function captureXhrBreadcrumbToReplay( const result = makeNetworkReplayBreadcrumb('resource.xhr', data); addNetworkBreadcrumb(options.replay, result); } catch (error) { - DEBUG_BUILD && logger.error('[Replay] Failed to capture xhr breadcrumb', error); + DEBUG_BUILD && logger.warn('Failed to capture xhr breadcrumb'); + DEBUG_BUILD && logger.exception(error); } } @@ -161,7 +162,7 @@ function _getXhrResponseBody(xhr: XMLHttpRequest): [string | undefined, NetworkM errors.push(e); } - DEBUG_BUILD && logger.warn('[Replay] Failed to get xhr response body', ...errors); + DEBUG_BUILD && logger.warn('Failed to get xhr response body', ...errors); return [undefined]; } @@ -197,12 +198,13 @@ export function _parseXhrResponse( if (!body) { return [undefined]; } - } catch { - DEBUG_BUILD && logger.warn('[Replay] Failed to serialize body', body); + } catch (error) { + DEBUG_BUILD && logger.warn('Failed to serialize body', body); + DEBUG_BUILD && logger.exception(error); return [undefined, 'BODY_PARSE_ERROR']; } - DEBUG_BUILD && logger.info('[Replay] Skipping network body because of body type', body); + DEBUG_BUILD && logger.info('Skipping network body because of body type', body); return [undefined, 'UNPARSEABLE_BODY_TYPE']; } diff --git a/packages/replay-internal/src/eventBuffer/EventBufferCompressionWorker.ts b/packages/replay-internal/src/eventBuffer/EventBufferCompressionWorker.ts index 21206ea652ac..59e5b62b229c 100644 --- a/packages/replay-internal/src/eventBuffer/EventBufferCompressionWorker.ts +++ b/packages/replay-internal/src/eventBuffer/EventBufferCompressionWorker.ts @@ -1,9 +1,9 @@ import type { ReplayRecordingData } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants'; import { DEBUG_BUILD } from '../debug-build'; import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; +import { logger } from '../util/logger'; import { timestampToMs } from '../util/timestamp'; import { WorkerHandler } from './WorkerHandler'; import { EventBufferSizeExceededError } from './error'; @@ -88,7 +88,8 @@ export class EventBufferCompressionWorker implements EventBuffer { // We do not wait on this, as we assume the order of messages is consistent for the worker this._worker.postMessage('clear').then(null, e => { - DEBUG_BUILD && logger.warn('[Replay] Sending "clear" message to worker failed', e); + DEBUG_BUILD && logger.warn('Sending "clear" message to worker failed', e); + DEBUG_BUILD && logger.exception(e); }); } diff --git a/packages/replay-internal/src/eventBuffer/EventBufferProxy.ts b/packages/replay-internal/src/eventBuffer/EventBufferProxy.ts index af6645a89e69..17f78ccaf8b3 100644 --- a/packages/replay-internal/src/eventBuffer/EventBufferProxy.ts +++ b/packages/replay-internal/src/eventBuffer/EventBufferProxy.ts @@ -1,9 +1,8 @@ import type { ReplayRecordingData } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; -import { logInfo } from '../util/log'; +import { logger } from '../util/logger'; import { EventBufferArray } from './EventBufferArray'; import { EventBufferCompressionWorker } from './EventBufferCompressionWorker'; @@ -90,7 +89,8 @@ export class EventBufferProxy implements EventBuffer { } catch (error) { // If the worker fails to load, we fall back to the simple buffer. // Nothing more to do from our side here - logInfo('[Replay] Failed to load the compression worker, falling back to simple buffer'); + DEBUG_BUILD && logger.info('Failed to load the compression worker, falling back to simple buffer'); + DEBUG_BUILD && logger.exception(error); return; } @@ -117,7 +117,8 @@ export class EventBufferProxy implements EventBuffer { try { await Promise.all(addEventPromises); } catch (error) { - DEBUG_BUILD && logger.warn('[Replay] Failed to add events when switching buffers.', error); + DEBUG_BUILD && logger.error('Failed to add events when switching buffers.'); + DEBUG_BUILD && logger.exception(error); } } } diff --git a/packages/replay-internal/src/eventBuffer/WorkerHandler.ts b/packages/replay-internal/src/eventBuffer/WorkerHandler.ts index 1014521e652f..2ccc3ee94b3c 100644 --- a/packages/replay-internal/src/eventBuffer/WorkerHandler.ts +++ b/packages/replay-internal/src/eventBuffer/WorkerHandler.ts @@ -1,8 +1,6 @@ -import { logger } from '@sentry/utils'; - import { DEBUG_BUILD } from '../debug-build'; import type { WorkerRequest, WorkerResponse } from '../types'; -import { logInfo } from '../util/log'; +import { logger } from '../util/logger'; /** * Event buffer that uses a web worker to compress events. @@ -57,7 +55,7 @@ export class WorkerHandler { * Destroy the worker. */ public destroy(): void { - logInfo('[Replay] Destroying compression worker'); + DEBUG_BUILD && logger.info('Destroying compression worker'); this._worker.terminate(); } @@ -85,7 +83,7 @@ export class WorkerHandler { if (!response.success) { // TODO: Do some error handling, not sure what - DEBUG_BUILD && logger.error('[Replay]', response.response); + DEBUG_BUILD && logger.error('Error in compression worker: ', response.response); reject(new Error('Error in compression worker')); return; diff --git a/packages/replay-internal/src/eventBuffer/index.ts b/packages/replay-internal/src/eventBuffer/index.ts index 741cb5dedc91..56b64b80dbb4 100644 --- a/packages/replay-internal/src/eventBuffer/index.ts +++ b/packages/replay-internal/src/eventBuffer/index.ts @@ -1,7 +1,8 @@ import { getWorkerURL } from '@sentry-internal/replay-worker'; +import { DEBUG_BUILD } from '../debug-build'; import type { EventBuffer } from '../types'; -import { logInfo } from '../util/log'; +import { logger } from '../util/logger'; import { EventBufferArray } from './EventBufferArray'; import { EventBufferProxy } from './EventBufferProxy'; @@ -32,7 +33,7 @@ export function createEventBuffer({ } } - logInfo('[Replay] Using simple buffer'); + DEBUG_BUILD && logger.info('Using simple buffer'); return new EventBufferArray(); } @@ -44,11 +45,12 @@ function _loadWorker(customWorkerUrl?: string): EventBufferProxy | void { return; } - logInfo(`[Replay] Using compression worker${customWorkerUrl ? ` from ${customWorkerUrl}` : ''}`); + DEBUG_BUILD && logger.info(`Using compression worker${customWorkerUrl ? ` from ${customWorkerUrl}` : ''}`); const worker = new Worker(workerUrl); return new EventBufferProxy(worker); } catch (error) { - logInfo('[Replay] Failed to create compression worker'); + DEBUG_BUILD && logger.warn('Failed to create compression worker'); + DEBUG_BUILD && logger.exception(error); // Fall back to use simple event buffer array } } diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index f42d6ef6964a..a70a384b7c68 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -1,15 +1,8 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { EventType, record } from '@sentry-internal/rrweb'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - captureException, - getActiveSpan, - getClient, - getRootSpan, - spanToJSON, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getClient, getRootSpan, spanToJSON } from '@sentry/core'; import type { ReplayRecordingMode, Span } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger } from './util/logger'; import { BUFFER_CHECKOUT_TIME, @@ -60,7 +53,6 @@ import { debounce } from './util/debounce'; import { getHandleRecordingEmit } from './util/handleRecordingEmit'; import { isExpired } from './util/isExpired'; import { isSessionExpired } from './util/isSessionExpired'; -import { logInfo, logInfoNextTick } from './util/log'; import { sendReplay } from './util/sendReplay'; import type { SKIPPED } from './util/throttle'; import { THROTTLED, throttle } from './util/throttle'; @@ -212,6 +204,15 @@ export class ReplayContainer implements ReplayContainerInterface { if (slowClickConfig) { this.clickDetector = new ClickDetector(this, slowClickConfig); } + + // Configure replay logger w/ experimental options + if (this._options._experiments.traceInternals) { + logger.enableTraceInternals(); + } + + if (this._options._experiments.captureExceptions) { + logger.enableCaptureInternalExceptions(); + } } /** Get the event context. */ @@ -243,11 +244,7 @@ export class ReplayContainer implements ReplayContainerInterface { /** A wrapper to conditionally capture exceptions. */ public handleException(error: unknown): void { - DEBUG_BUILD && logger.error('[Replay]', error); - - if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) { - captureException(error); - } + DEBUG_BUILD && logger.exception(error); } /** @@ -273,7 +270,7 @@ export class ReplayContainer implements ReplayContainerInterface { if (!this.session) { // This should not happen, something wrong has occurred - this.handleException(new Error('Unable to initialize and create session')); + DEBUG_BUILD && logger.exception(new Error('Unable to initialize and create session')); return; } @@ -287,10 +284,7 @@ export class ReplayContainer implements ReplayContainerInterface { // In this case, we still want to continue in `session` recording mode this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session'; - logInfoNextTick( - `[Replay] Starting replay in ${this.recordingMode} mode`, - this._options._experiments.traceInternals, - ); + DEBUG_BUILD && logger.infoTick(`Starting replay in ${this.recordingMode} mode`); this._initializeRecording(); } @@ -304,16 +298,16 @@ export class ReplayContainer implements ReplayContainerInterface { */ public start(): void { if (this._isEnabled && this.recordingMode === 'session') { - DEBUG_BUILD && logger.info('[Replay] Recording is already in progress'); + DEBUG_BUILD && logger.info('Recording is already in progress'); return; } if (this._isEnabled && this.recordingMode === 'buffer') { - DEBUG_BUILD && logger.info('[Replay] Buffering is in progress, call `flush()` to save the replay'); + DEBUG_BUILD && logger.info('Buffering is in progress, call `flush()` to save the replay'); return; } - logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); + DEBUG_BUILD && logger.infoTick('Starting replay in session mode'); // Required as user activity is initially set in // constructor, so if `start()` is called after @@ -325,7 +319,6 @@ export class ReplayContainer implements ReplayContainerInterface { { maxReplayDuration: this._options.maxReplayDuration, sessionIdleExpire: this.timeouts.sessionIdleExpire, - traceInternals: this._options._experiments.traceInternals, }, { stickySession: this._options.stickySession, @@ -346,17 +339,16 @@ export class ReplayContainer implements ReplayContainerInterface { */ public startBuffering(): void { if (this._isEnabled) { - DEBUG_BUILD && logger.info('[Replay] Buffering is in progress, call `flush()` to save the replay'); + DEBUG_BUILD && logger.info('Buffering is in progress, call `flush()` to save the replay'); return; } - logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); + DEBUG_BUILD && logger.infoTick('Starting replay in buffer mode'); const session = loadOrCreateSession( { sessionIdleExpire: this.timeouts.sessionIdleExpire, maxReplayDuration: this._options.maxReplayDuration, - traceInternals: this._options._experiments.traceInternals, }, { stickySession: this._options.stickySession, @@ -436,10 +428,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._isEnabled = false; try { - logInfo( - `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`, - this._options._experiments.traceInternals, - ); + DEBUG_BUILD && logger.info(`Stopping Replay${reason ? ` triggered by ${reason}` : ''}`); this._removeListeners(); this.stopRecording(); @@ -476,7 +465,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._isPaused = true; this.stopRecording(); - logInfo('[Replay] Pausing replay', this._options._experiments.traceInternals); + DEBUG_BUILD && logger.info('Pausing replay'); } /** @@ -493,7 +482,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._isPaused = false; this.startRecording(); - logInfo('[Replay] Resuming replay', this._options._experiments.traceInternals); + DEBUG_BUILD && logger.info('Resuming replay'); } /** @@ -510,7 +499,7 @@ export class ReplayContainer implements ReplayContainerInterface { const activityTime = Date.now(); - logInfo('[Replay] Converting buffer to session', this._options._experiments.traceInternals); + DEBUG_BUILD && logger.info('Converting buffer to session'); // Allow flush to complete before resuming as a session recording, otherwise // the checkout from `startRecording` may be included in the payload. @@ -798,7 +787,6 @@ export class ReplayContainer implements ReplayContainerInterface { { sessionIdleExpire: this.timeouts.sessionIdleExpire, maxReplayDuration: this._options.maxReplayDuration, - traceInternals: this._options._experiments.traceInternals, previousSessionId, }, { @@ -990,7 +978,7 @@ export class ReplayContainer implements ReplayContainerInterface { // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION // ms, we will re-use the existing session, otherwise create a new // session - logInfo('[Replay] Document has become active, but session has expired'); + DEBUG_BUILD && logger.info('Document has become active, but session has expired'); return; } @@ -1106,7 +1094,7 @@ export class ReplayContainer implements ReplayContainerInterface { const replayId = this.getSessionId(); if (!this.session || !this.eventBuffer || !replayId) { - DEBUG_BUILD && logger.error('[Replay] No session or eventBuffer found to flush.'); + DEBUG_BUILD && logger.error('No session or eventBuffer found to flush.'); return; } @@ -1198,7 +1186,7 @@ export class ReplayContainer implements ReplayContainerInterface { } if (!this.checkAndHandleExpiredSession()) { - DEBUG_BUILD && logger.error('[Replay] Attempting to finish replay event after session expired.'); + DEBUG_BUILD && logger.error('Attempting to finish replay event after session expired.'); return; } @@ -1219,12 +1207,12 @@ export class ReplayContainer implements ReplayContainerInterface { const tooShort = duration < this._options.minReplayDuration; const tooLong = duration > this._options.maxReplayDuration + 5_000; if (tooShort || tooLong) { - logInfo( - `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${ - tooShort ? 'short' : 'long' - }, not sending replay.`, - this._options._experiments.traceInternals, - ); + DEBUG_BUILD && + logger.info( + `Session duration (${Math.floor(duration / 1000)}s) is too ${ + tooShort ? 'short' : 'long' + }, not sending replay.`, + ); if (tooShort) { this._debouncedFlush(); @@ -1234,7 +1222,7 @@ export class ReplayContainer implements ReplayContainerInterface { const eventBuffer = this.eventBuffer; if (eventBuffer && this.session.segmentId === 0 && !eventBuffer.hasCheckout) { - logInfo('[Replay] Flushing initial segment without checkout.', this._options._experiments.traceInternals); + DEBUG_BUILD && logger.info('Flushing initial segment without checkout.'); // TODO FN: Evaluate if we want to stop here, or remove this again? } diff --git a/packages/replay-internal/src/session/fetchSession.ts b/packages/replay-internal/src/session/fetchSession.ts index 43e162b5f3d6..031605bfde87 100644 --- a/packages/replay-internal/src/session/fetchSession.ts +++ b/packages/replay-internal/src/session/fetchSession.ts @@ -1,13 +1,14 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../constants'; +import { DEBUG_BUILD } from '../debug-build'; import type { Session } from '../types'; import { hasSessionStorage } from '../util/hasSessionStorage'; -import { logInfoNextTick } from '../util/log'; +import { logger } from '../util/logger'; import { makeSession } from './Session'; /** * Fetches a session from storage */ -export function fetchSession(traceInternals?: boolean): Session | null { +export function fetchSession(): Session | null { if (!hasSessionStorage()) { return null; } @@ -22,7 +23,7 @@ export function fetchSession(traceInternals?: boolean): Session | null { const sessionObj = JSON.parse(sessionStringFromStorage) as Session; - logInfoNextTick('[Replay] Loading existing session', traceInternals); + DEBUG_BUILD && logger.infoTick('Loading existing session'); return makeSession(sessionObj); } catch { diff --git a/packages/replay-internal/src/session/loadOrCreateSession.ts b/packages/replay-internal/src/session/loadOrCreateSession.ts index 1e1ac7664d40..d37c51590d54 100644 --- a/packages/replay-internal/src/session/loadOrCreateSession.ts +++ b/packages/replay-internal/src/session/loadOrCreateSession.ts @@ -1,5 +1,6 @@ +import { DEBUG_BUILD } from '../debug-build'; import type { Session, SessionOptions } from '../types'; -import { logInfoNextTick } from '../util/log'; +import { logger } from '../util/logger'; import { createSession } from './createSession'; import { fetchSession } from './fetchSession'; import { shouldRefreshSession } from './shouldRefreshSession'; @@ -10,23 +11,21 @@ import { shouldRefreshSession } from './shouldRefreshSession'; */ export function loadOrCreateSession( { - traceInternals, sessionIdleExpire, maxReplayDuration, previousSessionId, }: { sessionIdleExpire: number; maxReplayDuration: number; - traceInternals?: boolean; previousSessionId?: string; }, sessionOptions: SessionOptions, ): Session { - const existingSession = sessionOptions.stickySession && fetchSession(traceInternals); + const existingSession = sessionOptions.stickySession && fetchSession(); // No session exists yet, just create a new one if (!existingSession) { - logInfoNextTick('[Replay] Creating new session', traceInternals); + DEBUG_BUILD && logger.infoTick('Creating new session'); return createSession(sessionOptions, { previousSessionId }); } @@ -34,6 +33,6 @@ export function loadOrCreateSession( return existingSession; } - logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...'); + DEBUG_BUILD && logger.infoTick('Session in sessionStorage is expired, creating new one...'); return createSession(sessionOptions, { previousSessionId: existingSession.id }); } diff --git a/packages/replay-internal/src/util/addEvent.ts b/packages/replay-internal/src/util/addEvent.ts index f397ea0564f6..4a1c9dc2f6d2 100644 --- a/packages/replay-internal/src/util/addEvent.ts +++ b/packages/replay-internal/src/util/addEvent.ts @@ -1,11 +1,10 @@ import { EventType } from '@sentry-internal/rrweb'; import { getClient } from '@sentry/core'; -import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { EventBufferSizeExceededError } from '../eventBuffer/error'; import type { AddEventResult, RecordingEvent, ReplayContainer, ReplayFrameEvent, ReplayPluginOptions } from '../types'; -import { logInfoNextTick } from './log'; +import { logger } from './logger'; import { timestampToMs } from './timestamp'; function isCustomEvent(event: RecordingEvent): event is ReplayFrameEvent { @@ -109,10 +108,8 @@ export function shouldAddEvent(replay: ReplayContainer, event: RecordingEvent): // Throw out events that are +60min from the initial timestamp if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) { - logInfoNextTick( - `[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`, - replay.getOptions()._experiments.traceInternals, - ); + DEBUG_BUILD && + logger.infoTick(`Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`); return false; } @@ -128,8 +125,8 @@ function maybeApplyCallback( return callback(event); } } catch (error) { - DEBUG_BUILD && - logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error); + DEBUG_BUILD && logger.error('An error occured in the `beforeAddRecordingEvent` callback, skipping the event...'); + DEBUG_BUILD && logger.exception(error); return null; } diff --git a/packages/replay-internal/src/util/handleRecordingEmit.ts b/packages/replay-internal/src/util/handleRecordingEmit.ts index eaec29be261a..6b87845d793f 100644 --- a/packages/replay-internal/src/util/handleRecordingEmit.ts +++ b/packages/replay-internal/src/util/handleRecordingEmit.ts @@ -1,12 +1,11 @@ import { EventType } from '@sentry-internal/rrweb'; -import { logger } from '@sentry/utils'; import { updateClickDetectorForRecordingEvent } from '../coreHandlers/handleClick'; import { DEBUG_BUILD } from '../debug-build'; import { saveSession } from '../session/saveSession'; import type { RecordingEvent, ReplayContainer, ReplayOptionFrameEvent } from '../types'; import { addEventSync } from './addEvent'; -import { logInfo } from './log'; +import { logger } from './logger'; type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void; @@ -21,7 +20,7 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa return (event: RecordingEvent, _isCheckout?: boolean) => { // If this is false, it means session is expired, create and a new session and wait for checkout if (!replay.checkAndHandleExpiredSession()) { - DEBUG_BUILD && logger.warn('[Replay] Received replay event after session expired.'); + DEBUG_BUILD && logger.warn('Received replay event after session expired.'); return; } @@ -82,10 +81,8 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) { const earliestEvent = replay.eventBuffer.getEarliestTimestamp(); if (earliestEvent) { - logInfo( - `[Replay] Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`, - replay.getOptions()._experiments.traceInternals, - ); + DEBUG_BUILD && + logger.info(`Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`); replay.session.started = earliestEvent; diff --git a/packages/replay-internal/src/util/log.ts b/packages/replay-internal/src/util/log.ts deleted file mode 100644 index c847a093f02f..000000000000 --- a/packages/replay-internal/src/util/log.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { setTimeout } from '@sentry-internal/browser-utils'; -import { addBreadcrumb } from '@sentry/core'; -import { logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../debug-build'; - -/** - * Log a message in debug mode, and add a breadcrumb when _experiment.traceInternals is enabled. - */ -export function logInfo(message: string, shouldAddBreadcrumb?: boolean): void { - if (!DEBUG_BUILD) { - return; - } - - logger.info(message); - - if (shouldAddBreadcrumb) { - addLogBreadcrumb(message); - } -} - -/** - * Log a message, and add a breadcrumb in the next tick. - * This is necessary when the breadcrumb may be added before the replay is initialized. - */ -export function logInfoNextTick(message: string, shouldAddBreadcrumb?: boolean): void { - if (!DEBUG_BUILD) { - return; - } - - logger.info(message); - - if (shouldAddBreadcrumb) { - // Wait a tick here to avoid race conditions for some initial logs - // which may be added before replay is initialized - setTimeout(() => { - addLogBreadcrumb(message); - }, 0); - } -} - -function addLogBreadcrumb(message: string): void { - addBreadcrumb( - { - category: 'console', - data: { - logger: 'replay', - }, - level: 'info', - message, - }, - { level: 'info' }, - ); -} diff --git a/packages/replay-internal/src/util/logger.ts b/packages/replay-internal/src/util/logger.ts new file mode 100644 index 000000000000..57c11fa74caf --- /dev/null +++ b/packages/replay-internal/src/util/logger.ts @@ -0,0 +1,105 @@ +import { addBreadcrumb, captureException } from '@sentry/core'; +import type { SeverityLevel } from '@sentry/types'; +import { logger as coreLogger } from '@sentry/utils'; + +import { DEBUG_BUILD } from '../debug-build'; + +const CONSOLE_LEVELS = ['info', 'warn', 'error', 'log'] as const; + +type LoggerMethod = (...args: unknown[]) => void; +type LoggerConsoleMethods = Record<'info' | 'warn' | 'error' | 'log', LoggerMethod>; + +/** JSDoc */ +interface ReplayLogger extends LoggerConsoleMethods { + /** + * Calls `logger.info` but saves breadcrumb in the next tick due to race + * conditions before replay is initialized. + */ + infoTick(...args: unknown[]): void; + /** + * Captures exceptions (`Error`) if "capture internal exceptions" is enabled + */ + exception(error: unknown): void; + enableCaptureInternalExceptions(): void; + disableCaptureInternalExceptions(): void; + enableTraceInternals(): void; + disableTraceInternals(): void; +} + +function _addBreadcrumb(message: string, level: SeverityLevel = 'info'): void { + // Wait a tick here to avoid race conditions for some initial logs + // which may be added before replay is initialized + addBreadcrumb( + { + category: 'console', + data: { + logger: 'replay', + }, + level, + message: `[Replay] ${message}`, + }, + { level }, + ); +} + +function makeReplayLogger(): ReplayLogger { + let _capture = false; + let _trace = false; + + const _logger: Partial = { + exception: () => undefined, + infoTick: () => undefined, + enableCaptureInternalExceptions: () => { + _capture = true; + }, + disableCaptureInternalExceptions: () => { + _capture = false; + }, + enableTraceInternals: () => { + _trace = true; + }, + disableTraceInternals: () => { + _trace = false; + }, + }; + + if (DEBUG_BUILD) { + _logger.exception = (error: unknown) => { + coreLogger.error('[Replay] ', error); + + if (_capture) { + captureException(error); + } + + // No need for a breadcrumb is `_capture` is enabled since it should be + // captured as an exception + if (_trace && !_capture) { + _addBreadcrumb(error instanceof Error ? error.message : 'Unknown error'); + } + }; + + _logger.infoTick = (...args: unknown[]) => { + coreLogger.info('[Replay] ', ...args); + if (_trace) { + setTimeout(() => typeof args[0] === 'string' && _addBreadcrumb(args[0]), 0); + } + }; + + CONSOLE_LEVELS.forEach(name => { + _logger[name] = (...args: unknown[]) => { + coreLogger[name]('[Replay] ', ...args); + if (_trace && typeof args[0] === 'string') { + _addBreadcrumb(args[0]); + } + }; + }); + } else { + CONSOLE_LEVELS.forEach(name => { + _logger[name] = () => undefined; + }); + } + + return _logger as ReplayLogger; +} + +export const logger = makeReplayLogger(); diff --git a/packages/replay-internal/src/util/sendReplayRequest.ts b/packages/replay-internal/src/util/sendReplayRequest.ts index 03945bb479af..a623771af75b 100644 --- a/packages/replay-internal/src/util/sendReplayRequest.ts +++ b/packages/replay-internal/src/util/sendReplayRequest.ts @@ -5,9 +5,10 @@ import { resolvedSyncPromise } from '@sentry/utils'; import { isRateLimited, updateRateLimits } from '@sentry/utils'; import { REPLAY_EVENT_NAME, UNABLE_TO_SEND_REPLAY } from '../constants'; +import { DEBUG_BUILD } from '../debug-build'; import type { SendReplayData } from '../types'; import { createReplayEnvelope } from './createReplayEnvelope'; -import { logInfo } from './log'; +import { logger } from './logger'; import { prepareRecordingData } from './prepareRecordingData'; import { prepareReplayEvent } from './prepareReplayEvent'; @@ -57,7 +58,7 @@ export async function sendReplayRequest({ if (!replayEvent) { // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions client.recordDroppedEvent('event_processor', 'replay', baseEvent); - logInfo('An event processor returned `null`, will not send event.'); + DEBUG_BUILD && logger.info('An event processor returned `null`, will not send event.'); return resolvedSyncPromise({}); } diff --git a/packages/replay-internal/test/integration/flush.test.ts b/packages/replay-internal/test/integration/flush.test.ts index 999811de0a81..78a945d5fddd 100644 --- a/packages/replay-internal/test/integration/flush.test.ts +++ b/packages/replay-internal/test/integration/flush.test.ts @@ -19,6 +19,7 @@ import { clearSession } from '../../src/session/clearSession'; import type { EventBuffer } from '../../src/types'; import { createPerformanceEntries } from '../../src/util/createPerformanceEntries'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import { logger } from '../../src/util/logger'; import * as SendReplay from '../../src/util/sendReplay'; import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; import type { DomHandler } from '../types'; @@ -335,7 +336,8 @@ describe('Integration | flush', () => { }); it('logs warning if flushing initial segment without checkout', async () => { - replay.getOptions()._experiments.traceInternals = true; + // replay.getOptions()._experiments.traceInternals = true; + logger.enableTraceInternals(); sessionStorage.clear(); clearSession(replay); @@ -408,11 +410,11 @@ describe('Integration | flush', () => { }, ]); - replay.getOptions()._experiments.traceInternals = false; + logger.disableTraceInternals(); }); it('logs warning if adding event that is after maxReplayDuration', async () => { - replay.getOptions()._experiments.traceInternals = true; + logger.enableTraceInternals(); const spyLogger = vi.spyOn(SentryUtils.logger, 'info'); @@ -440,12 +442,13 @@ describe('Integration | flush', () => { expect(mockSendReplay).toHaveBeenCalledTimes(0); expect(spyLogger).toHaveBeenLastCalledWith( - `[Replay] Skipping event with timestamp ${ + '[Replay] ', + `Skipping event with timestamp ${ BASE_TIMESTAMP + MAX_REPLAY_DURATION + 100 } because it is after maxReplayDuration`, ); - replay.getOptions()._experiments.traceInternals = false; + logger.disableTraceInternals(); spyLogger.mockRestore(); });