diff --git a/CHANGELOG.md b/CHANGELOG.md index a5debc630aeb..e74f4483eaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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()], }); ``` diff --git a/dev-packages/e2e-tests/test-applications/browser-bfcache/README.md b/dev-packages/e2e-tests/test-applications/browser-bfcache/README.md index 95505052cc6a..988fb2527945 100644 --- a/dev-packages/e2e-tests/test-applications/browser-bfcache/README.md +++ b/dev-packages/e2e-tests/test-applications/browser-bfcache/README.md @@ -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=` (see `src/main.ts`). @@ -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. diff --git a/dev-packages/e2e-tests/test-applications/browser-bfcache/src/main.ts b/dev-packages/e2e-tests/test-applications/browser-bfcache/src/main.ts index 6cac805d7d72..e53f75c4a45a 100644 --- a/dev-packages/e2e-tests/test-applications/browser-bfcache/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/browser-bfcache/src/main.ts @@ -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', diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index b7a1f410bfea..86a58bbc9be6 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -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'; diff --git a/packages/browser/src/integrations/bfcache.ts b/packages/browser/src/integrations/bfcacheMetrics.ts similarity index 67% rename from packages/browser/src/integrations/bfcache.ts rename to packages/browser/src/integrations/bfcacheMetrics.ts index 81616cbaef9c..82b1deac4852 100644 --- a/packages/browser/src/integrations/bfcache.ts +++ b/packages/browser/src/integrations/bfcacheMetrics.ts @@ -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. * @@ -48,56 +48,58 @@ interface CollectedReason { /** * Captures bfcache hit/miss counters and Chromium notRestoredReasons when available. */ -export const bfcacheIntegration = defineIntegration((options: Partial = {}) => { - 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 = {}) => { + 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. diff --git a/packages/browser/test/integrations/bfcache.test.ts b/packages/browser/test/integrations/bfcacheMetrics.test.ts similarity index 98% rename from packages/browser/test/integrations/bfcache.test.ts rename to packages/browser/test/integrations/bfcacheMetrics.test.ts index 8ac43575f37d..6f04081f948e 100644 --- a/packages/browser/test/integrations/bfcache.test.ts +++ b/packages/browser/test/integrations/bfcacheMetrics.test.ts @@ -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();