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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ Work in this release was contributed by @psh4607, @thijsw, @trinitiwowka, @nehap
- `DenoMongoose` => `Mongoose`
- `DenoMysql` => `Mysql`
- `DenoPostgres` => `Postgres`
- **feat(browser): Add `bfcacheIntegration` to track back/forward cache health**
- **feat(browser): Add `bfcacheMetricsIntegration` to track back/forward cache health**

The new opt-in `bfcacheIntegration` emits metrics about browser back/forward cache (bfcache) navigations, so you can
The new opt-in `bfcacheMetricsIntegration` emits metrics about browser back/forward cache (bfcache) navigations, so you can
measure how often back-button navigation is instant and what's blocking it.

```js
Sentry.init({
integrations: [Sentry.bfcacheIntegration()],
integrations: [Sentry.bfcacheMetricsIntegration()],
});
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# browser-bfcache

Exercises `bfcacheIntegration` against a **real** browser back/forward cache, covering hits,
Exercises `bfcacheMetricsIntegration` against a **real** browser back/forward cache, covering hits,
misses, and the real `notRestoredReasons` the browser reports (Chromium-only, and this app is
Chromium). Deliberately bfcache-ineligible pages are produced via `?botch=<case>` (see `src/main.ts`).

Expand All @@ -22,7 +22,7 @@ permissive than web.dev's list suggests, so the individual `?botch=` cases and t
the source of truth, not prose here. Some are gated on the browser version where behavior changed.

Reason extraction/classification (top/child/masked frames, nesting, caps) is covered by the unit test
at `packages/browser/test/integrations/bfcache.test.ts`; this app verifies the real end-to-end
at `packages/browser/test/integrations/bfcacheMetrics.test.ts`; this app verifies the real end-to-end
hit/miss + reason path.

If other tests later fit these same constraints, this app can be renamed to something broader.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/browser';

Sentry.init({
dsn: process.env.E2E_TEST_DSN,
integrations: [Sentry.bfcacheIntegration()],
integrations: [Sentry.bfcacheMetricsIntegration()],
release: 'e2e-test',
environment: 'qa',
tunnel: 'http://localhost:3031',
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance';
export { webVitalsIntegration } from './integrations/webVitals';
export { userTimingIntegration } from './integrations/usertiming';
export { bfcacheIntegration } from './integrations/bfcache';
export { bfcacheMetricsIntegration } from './integrations/bfcacheMetrics';
export { interactionsIntegration } from './integrations/interactions';

export type { RequestInstrumentationOptions } from './tracing/request';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { debug, defineIntegration, getCurrentScope, metrics } from '@sentry/core
import { DEBUG_BUILD } from '../debug-build';
import { WINDOW } from '../helpers';

const INTEGRATION_NAME = 'Bfcache';
const INTEGRATION_NAME = 'BfcacheMetrics';

type BFCacheOutcome = 'hit' | 'miss';

type BFCacheFrame = 'top' | 'child';

interface BFCacheIntegrationOptions {
interface BFCacheMetricsIntegrationOptions {
/**
* Maximum number of not-restored reasons to emit per miss.
*
Expand Down Expand Up @@ -48,56 +48,58 @@ interface CollectedReason {
/**
* Captures bfcache hit/miss counters and Chromium notRestoredReasons when available.
*/
export const bfcacheIntegration = defineIntegration((options: Partial<BFCacheIntegrationOptions> = {}) => {
const maxReasons = _resolveMaxReasons(options.maxReasons);

return {
name: INTEGRATION_NAME,

setupOnce() {
if (!WINDOW.addEventListener || !WINDOW.performance?.getEntriesByType) {
DEBUG_BUILD && debug.log(`[${INTEGRATION_NAME}] Browser APIs unavailable, skipping instrumentation.`);
return;
}

function onPageShow(event: PageTransitionEvent) {
const routeName = _getSegmentName();
if (event.persisted) {
_captureBFCacheNavigation('hit', 0, routeName);
return;
}
export const bfcacheMetricsIntegration = defineIntegration(
(options: Partial<BFCacheMetricsIntegrationOptions> = {}) => {
const maxReasons = _resolveMaxReasons(options.maxReasons);

const navigationEntry = WINDOW.performance.getEntriesByType('navigation')[0] as
| NavigationTimingWithNotRestoredReasons
| undefined;
return {
name: INTEGRATION_NAME,

if (navigationEntry?.type !== 'back_forward') {
setupOnce() {
if (!WINDOW.addEventListener || !WINDOW.performance?.getEntriesByType) {
DEBUG_BUILD && debug.log(`[${INTEGRATION_NAME}] Browser APIs unavailable, skipping instrumentation.`);
return;
}

const reasons = _collectNotRestoredReasons(navigationEntry.notRestoredReasons, maxReasons);
_captureBFCacheNavigation('miss', reasons.length, routeName);

// Measures how expensive the fallback reload was when a back/forward navigation missed bfcache.
if (typeof navigationEntry.duration === 'number' && navigationEntry.duration > 0) {
metrics.distribution('browser.bfcache.reload.duration', navigationEntry.duration, {
unit: 'millisecond',
attributes: _withOriginAttr({
[SENTRY_SEGMENT_NAME]: routeName,
}),
});
function onPageShow(event: PageTransitionEvent) {
const routeName = _getSegmentName();
if (event.persisted) {
_captureBFCacheNavigation('hit', 0, routeName);
return;
}

const navigationEntry = WINDOW.performance.getEntriesByType('navigation')[0] as
| NavigationTimingWithNotRestoredReasons
| undefined;

if (navigationEntry?.type !== 'back_forward') {
return;
}

const reasons = _collectNotRestoredReasons(navigationEntry.notRestoredReasons, maxReasons);
_captureBFCacheNavigation('miss', reasons.length, routeName);

// Measures how expensive the fallback reload was when a back/forward navigation missed bfcache.
if (typeof navigationEntry.duration === 'number' && navigationEntry.duration > 0) {
metrics.distribution('browser.bfcache.reload.duration', navigationEntry.duration, {
unit: 'millisecond',
attributes: _withOriginAttr({
[SENTRY_SEGMENT_NAME]: routeName,
}),
});
}

reasons.forEach(r => _captureBFCacheReason(r, routeName));
}

reasons.forEach(r => _captureBFCacheReason(r, routeName));
}

// Listener should stay active because the event can trigger for an initial show before the bfcache entry coming into the second one.
// This can be platform-dependent so we need to skip as many events till we get to the one containing the entry.
// So we can't have { once } or a cleanup logic here, which is fine because `setupOnce` registers it a single time regardless of how many clients are created.
WINDOW.addEventListener('pageshow', onPageShow, true);
},
};
}) satisfies IntegrationFn;
// Listener should stay active because the event can trigger for an initial show before the bfcache entry coming into the second one.
// This can be platform-dependent so we need to skip as many events till we get to the one containing the entry.
// So we can't have { once } or a cleanup logic here, which is fine because `setupOnce` registers it a single time regardless of how many clients are created.
WINDOW.addEventListener('pageshow', onPageShow, true);
},
};
},
) satisfies IntegrationFn;

/**
* Captures a bf navigation as a metric and records the outcome and reason count.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { debug } from '@sentry/core/browser';
import { _collectNotRestoredReasons, _resolveMaxReasons } from '../../src/integrations/bfcache';
import { _collectNotRestoredReasons, _resolveMaxReasons } from '../../src/integrations/bfcacheMetrics';

describe('bfcacheIntegration', () => {
describe('bfcacheMetricsIntegration', () => {
describe('_resolveMaxReasons', () => {
afterEach(() => {
vi.restoreAllMocks();
Expand Down
Loading