From 6156f55f07429aa300b9dc357503c77729bb47a1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 7 May 2025 13:20:11 +0200 Subject: [PATCH] fix(logs): Ensure logs can be flushed correctly Stores the Client to LogBuffer WeakMap onto the global object so logs can be retrieved correctly during flushing. Previously, the WeakMap reference would be different at flush time, causing no logs to be found for any given client. Adds an express e2e tests to ensure logs are flushed correctly. --- .../test-applications/node-express/src/app.ts | 8 ++++++++ .../node-express/tests/logs.test.ts | 16 ++++++++++++++++ packages/core/src/logs/exports.ts | 14 ++++++++------ packages/core/src/utils-hoist/worldwide.ts | 8 ++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts index 6b320e26eb8a..086e7ebc292c 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts @@ -13,6 +13,9 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + _experiments: { + enableLogs: true, + }, }); import { TRPCError, initTRPC } from '@trpc/server'; @@ -30,6 +33,11 @@ app.get('/test-success', function (req, res) { res.send({ version: 'v1' }); }); +app.get('/test-log', function (req, res) { + Sentry.logger.debug('Accessed /test-log route'); + res.send({ message: 'Log sent' }); +}); + app.get('/test-param/:param', function (req, res) { res.send({ paramWas: req.params.param }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts new file mode 100644 index 000000000000..f2e125696af6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; +import type { SerializedLog, SerializedLogContainer } from '@sentry/core'; + +test('should send logs', async ({ baseURL }) => { + const logEnvelopePromise = waitForEnvelopeItem('node-express', envelope => { + return envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items[0]?.level === 'debug'; + }); + + await fetch(`${baseURL}/test-log`); + + const logEnvelope = await logEnvelopePromise; + const log = (logEnvelope[1] as SerializedLogContainer).items[0]; + expect(log?.level).toBe('debug'); + expect(log?.body).toBe('Accessed /test-log route'); +}); diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 8496f7fba595..a2451c6cc6c0 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -7,12 +7,14 @@ import { _getSpanForScope } from '../utils/spanOnScope'; import { isParameterizedString } from '../utils-hoist/is'; import { logger } from '../utils-hoist/logger'; import { timestampInSeconds } from '../utils-hoist/time'; +import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; import { createLogEnvelope } from './envelope'; const MAX_LOG_BUFFER_SIZE = 100; -const CLIENT_TO_LOG_BUFFER_MAP = new WeakMap>(); +// The reference to the Client <> LogBuffer map is stored to ensure it's always the same +GLOBAL_OBJ._sentryClientToLogBufferMap = new WeakMap>(); /** * Converts a log attribute to a serialized log attribute. @@ -149,11 +151,11 @@ export function _INTERNAL_captureLog( ), }; - const logBuffer = CLIENT_TO_LOG_BUFFER_MAP.get(client); + const logBuffer = _INTERNAL_getLogBuffer(client); if (logBuffer === undefined) { - CLIENT_TO_LOG_BUFFER_MAP.set(client, [serializedLog]); + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [serializedLog]); } else { - CLIENT_TO_LOG_BUFFER_MAP.set(client, [...logBuffer, serializedLog]); + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [...logBuffer, serializedLog]); if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) { _INTERNAL_flushLogsBuffer(client, logBuffer); } @@ -181,7 +183,7 @@ export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array const envelope = createLogEnvelope(logBuffer, clientOptions._metadata, clientOptions.tunnel, client.getDsn()); // Clear the log buffer after envelopes have been constructed. - CLIENT_TO_LOG_BUFFER_MAP.set(client, []); + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, []); client.emit('flushLogs'); @@ -199,5 +201,5 @@ export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array * @returns The log buffer for the given client. */ export function _INTERNAL_getLogBuffer(client: Client): Array | undefined { - return CLIENT_TO_LOG_BUFFER_MAP.get(client); + return GLOBAL_OBJ._sentryClientToLogBufferMap?.get(client); } diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 426831038f13..3a396d96f809 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -13,6 +13,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Carrier } from '../carrier'; +import type { Client } from '../client'; +import type { SerializedLog } from '../types-hoist/log'; import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ @@ -35,6 +37,12 @@ export type InternalGlobal = { id?: string; }; SENTRY_SDK_SOURCE?: SdkSource; + /** + * A map of Sentry clients to their log buffers. + * + * This is used to store logs that are sent to Sentry. + */ + _sentryClientToLogBufferMap?: WeakMap>; /** * Debug IDs are indirectly injected by Sentry CLI or bundler plugins to directly reference a particular source map * for resolving of a source file. The injected code will place an entry into the record for each loaded bundle/JS