diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 23edf25cc92b..5e9ab58b9121 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -9,14 +9,7 @@ import type { Scope, SeverityLevel, } from '@sentry/core/browser'; -import { - _INTERNAL_flushLogsBuffer, - _INTERNAL_flushMetricsBuffer, - addAutoIpAddressToSession, - applySdkMetadata, - Client, - getSDKSource, -} from '@sentry/core/browser'; +import { addAutoIpAddressToSession, applySdkMetadata, Client, getSDKSource } from '@sentry/core/browser'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; @@ -104,27 +97,26 @@ export class BrowserClient extends Client { }; } - const { sendClientReports, enableLogs, _experiments, enableMetrics: enableMetricsOption } = this._options; - - // todo(v11): Remove the experimental flag - // eslint-disable-next-line typescript/no-deprecated - const enableMetrics = enableMetricsOption ?? _experiments?.enableMetrics ?? true; + const { sendClientReports } = this._options; - // Flush logs and metrics when page becomes hidden (e.g., tab switch, navigation) - // todo(v11): Remove the experimental flag - if (WINDOW.document && (sendClientReports || enableLogs || enableMetrics)) { + // Flush buffered data when the page becomes hidden (e.g. tab switch, navigation, or the page + // being discarded). `flush()` emits the `flush` hook, which drains the span (streaming), log and + // metric buffers and hands the resulting envelopes to the transport (which uses `keepalive`). + // Client report outcomes don't listen to the `flush` hook, so we flush them separately. + if (WINDOW.document) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { if (sendClientReports) { this._flushOutcomes(); } - if (enableLogs) { - _INTERNAL_flushLogsBuffer(this); - } - - if (enableMetrics) { - _INTERNAL_flushMetricsBuffer(this); - } + // Defer the flush to a microtask so that visibilitychange listeners registered after this + // one have already run. In particular, browser tracing's background-tab detection ends the + // active pageload/navigation (segment) span when the page is hidden. Deferring ensures that + // segment span has been added to the streaming buffer before we flush, so it is sent + // together with its child spans instead of being orphaned. + queueMicrotask(() => { + void this.flush(); + }); } }); } diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 71e9a9be2ea6..7aac2c90d083 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -2,19 +2,17 @@ * @vitest-environment jsdom */ -import * as sentryCore from '@sentry/core/browser'; -import { Scope } from '@sentry/core/browser'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; -vi.mock('@sentry/core/browser', async requireActual => { - return { - ...((await requireActual()) as any), - _INTERNAL_flushLogsBuffer: vi.fn(), - }; -}); +function setDocumentHidden(): void { + if (WINDOW.document) { + Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden', configurable: true }); + WINDOW.document.dispatchEvent(new Event('visibilitychange')); + } +} describe('BrowserClient', () => { let client: BrowserClient; @@ -24,58 +22,32 @@ describe('BrowserClient', () => { vi.clearAllMocks(); }); - it('does not flush logs when logs are disabled', () => { - client = new BrowserClient( - getDefaultBrowserClientOptions({ - sendClientReports: true, - }), - ); - const scope = new Scope(); - scope.setClient(client); - - // Add some logs - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, scope); - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, scope); - - // Simulate visibility change to hidden - if (WINDOW.document) { - Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden' }); - WINDOW.document.dispatchEvent(new Event('visibilitychange')); - } - - expect(sentryCore._INTERNAL_flushLogsBuffer).not.toHaveBeenCalled(); - }); + it('flushes the client (spans, logs, metrics) when the page becomes hidden', async () => { + client = new BrowserClient(getDefaultBrowserClientOptions({ sendClientReports: true })); + const flushSpy = vi.spyOn(client, 'flush').mockReturnValue(Promise.resolve(true) as any); + const flushOutcomesSpy = vi.spyOn(client as any, '_flushOutcomes'); - describe('log flushing', () => { - beforeEach(() => { - vi.useFakeTimers(); - client = new BrowserClient( - getDefaultBrowserClientOptions({ - enableLogs: true, - sendClientReports: true, - }), - ); - }); + setDocumentHidden(); - it('flushes logs when page visibility changes to hidden', () => { - const flushOutcomesSpy = vi.spyOn(client as any, '_flushOutcomes'); + // The flush is deferred to a microtask so that visibilitychange listeners registered after the + // client's listener (e.g. browser tracing's background-tab detection) have already run. + expect(flushSpy).not.toHaveBeenCalled(); + await Promise.resolve(); - const scope = new Scope(); - scope.setClient(client); + expect(flushOutcomesSpy).toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalledTimes(1); + }); - // Add some logs - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, scope); - sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, scope); + it('does not flush outcomes when sendClientReports is disabled but still flushes the client', async () => { + client = new BrowserClient(getDefaultBrowserClientOptions({ sendClientReports: false })); + const flushSpy = vi.spyOn(client, 'flush').mockReturnValue(Promise.resolve(true) as any); + const flushOutcomesSpy = vi.spyOn(client as any, '_flushOutcomes'); - // Simulate visibility change to hidden - if (WINDOW.document) { - Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden' }); - WINDOW.document.dispatchEvent(new Event('visibilitychange')); - } + setDocumentHidden(); + await Promise.resolve(); - expect(flushOutcomesSpy).toHaveBeenCalled(); - expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client); - }); + expect(flushOutcomesSpy).not.toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalledTimes(1); }); });