Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 15 additions & 23 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -104,27 +97,26 @@ export class BrowserClient extends Client<BrowserClientOptions> {
};
}

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();
});
}
});
}
Expand Down
82 changes: 27 additions & 55 deletions packages/browser/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
});
});

Expand Down
Loading