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
12 changes: 6 additions & 6 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '149 KB',
limit: '152 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand All @@ -319,7 +319,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '153 KB',
limit: '156 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand All @@ -335,31 +335,31 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: false,
brotli: false,
limit: '268 KB',
limit: '272 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '272 KB',
limit: '276 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
gzip: false,
brotli: false,
limit: '282 KB',
limit: '286 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '286 KB',
limit: '290 KB',
disablePlugins: ['@size-limit/esbuild'],
},
// Next.js SDK (ESM)
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
},
"dependencies": {
"@sentry/core": "10.67.0",
"@sentry/conventions": "^0.16.0"
"@sentry/conventions": "^0.16.0",
"web-vitals": "^6.0.1"
},
"scripts": {
"build": "run-p build:transpile build:types",
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export { extractNetworkProtocol } from './metrics/utils';

export { trackClsAsSpan, trackInpAsSpan, trackLcpAsSpan } from './metrics/webVitalSpans';

export { whenIdleOrHidden } from './metrics/web-vitals/lib/whenIdleOrHidden';
export { whenIdleOrHidden } from './metrics/web-vitals-helpers/whenIdleOrHidden';

export { addClickKeypressInstrumentationHandler } from './instrument/dom';

Expand Down
6 changes: 3 additions & 3 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import {
import { isValidLcpMetric } from './lcp';
import { resourceTimingToSpanAttributes } from './resourceTiming';
import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
import { getActivationStart } from './web-vitals/lib/getActivationStart';
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
import { getActivationStart } from './web-vitals-helpers/getActivationStart';
import { getNavigationEntry } from './web-vitals-helpers/getNavigationEntry';
import { getVisibilityWatcher } from './web-vitals-helpers/getVisibilityWatcher';
import { DEBUG_BUILD } from '../debug-build';
import { SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes';
import { BROWSER_BROWSER_PAINT_SPAN_OP } from '@sentry/conventions/op';
Expand Down
102 changes: 69 additions & 33 deletions packages/browser-utils/src/metrics/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { debug, getFunctionName } from '@sentry/core';
import { onCLS, onINP, onLCP, onTTFB } from 'web-vitals';
import { DEBUG_BUILD } from '../debug-build';
import { onCLS } from './web-vitals/getCLS';
import { onINP } from './web-vitals/getINP';
import { onLCP } from './web-vitals/getLCP';
import { observe } from './web-vitals/lib/observe';
import { onTTFB } from './web-vitals/onTTFB';

type InstrumentHandlerTypePerformanceObserver =
| 'longtask'
Expand Down Expand Up @@ -47,6 +43,10 @@ export interface PerformanceLongAnimationFrameTiming extends PerformanceEntry {
scripts: PerformanceScriptTiming[];
}

// Locally-defined to match web-vitals' `Metric` shape without importing it: web-vitals' type
// entrypoint carries a `declare global` block that references DOM globals not present in every
// TypeScript lib version (e.g. `NavigationType`), which leaks into and breaks consumers on older
// TS. Keeping this local keeps web-vitals' global augmentations out of our published types.
interface Metric {
/**
* The name of the metric (in acronym form).
Expand Down Expand Up @@ -89,13 +89,20 @@ interface Metric {
entries: PerformanceEntry[];

/**
* The type of navigation
* The type of navigation.
*
* Navigation Timing API (or `undefined` if the browser doesn't
* support that API). For pages that are restored from the bfcache, this
* value will be 'back-forward-cache'.
*/
navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore';
navigationType:
| 'navigate'
| 'reload'
| 'back-forward'
| 'back-forward-cache'
| 'prerender'
| 'restore'
| 'soft-navigation';
}

type InstrumentHandlerType = InstrumentHandlerTypeMetric | InstrumentHandlerTypePerformanceObserver;
Expand Down Expand Up @@ -213,14 +220,31 @@ function triggerHandlers(type: InstrumentHandlerType, data: unknown): void {
}
}

/**
* Wraps a metric callback so that metrics reported after a back/forward-cache restore are ignored.
*
* web-vitals re-reports each metric after a bfcache restore (tagged with a `back-forward-cache`
* navigation type). We intentionally drop those for now: our reporting assumes one set of vitals
* per page load, so surfacing bfcache re-reports would skew the data until we're ready to model
* and communicate them.
*/
function withoutBfcache(callback: (metric: Metric) => void): (metric: Metric) => void {
return metric => {
if (metric.navigationType === 'back-forward-cache') {
return;
}
callback(metric);
};
}

function instrumentCls(): StopListening {
return onCLS(
metric => {
withoutBfcache(metric => {
triggerHandlers('cls', {
metric,
});
_previousCls = metric;
},
}),
// We want the callback to be called whenever the CLS value updates.
// By default, the callback is only called when the tab goes to the background.
{ reportAllChanges: true },
Expand All @@ -229,34 +253,38 @@ function instrumentCls(): StopListening {

function instrumentLcp(): StopListening {
return onLCP(
metric => {
withoutBfcache(metric => {
triggerHandlers('lcp', {
metric,
});
_previousLcp = metric;
},
}),
// We want the callback to be called whenever the LCP value updates.
// By default, the callback is only called when the tab goes to the background.
{ reportAllChanges: true },
);
}

function instrumentTtfb(): StopListening {
return onTTFB(metric => {
triggerHandlers('ttfb', {
metric,
});
_previousTtfb = metric;
});
return onTTFB(
withoutBfcache(metric => {
triggerHandlers('ttfb', {
metric,
});
_previousTtfb = metric;
}),
);
}

function instrumentInp(): void {
return onINP(metric => {
triggerHandlers('inp', {
metric,
});
_previousInp = metric;
});
function instrumentInp(): StopListening {
return onINP(
withoutBfcache(metric => {
triggerHandlers('inp', {
metric,
});
_previousInp = metric;
}),
);
}

function addMetricObserver(
Expand All @@ -283,20 +311,28 @@ function addMetricObserver(
}

function instrumentPerformanceObserver(type: InstrumentHandlerTypePerformanceObserver): void {
const options: PerformanceObserverInit = {};
const options: PerformanceObserverInit = { type, buffered: true };

// Special per-type options we want to use
if (type === 'event') {
options.durationThreshold = 0;
(options as PerformanceObserverInit & { durationThreshold?: number }).durationThreshold = 0;
}

observe(
type,
entries => {
triggerHandlers(type, { entries });
},
options,
);
try {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
const po = new PerformanceObserver(list => {
// Delay by a microtask to work around a bug in Safari where the
// callback is invoked synchronously rather than in a separate task.
// See: https://github.com/GoogleChrome/web-vitals/issues/277
void Promise.resolve().then(() => {
triggerHandlers(type, { entries: list.getEntries() });
});
});
po.observe(options);
}
} catch {
// Unsupported entry type; nothing to observe.
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment on lines +325 to +335

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A TypeError will be thrown in browsers where PerformanceObserver.supportedEntryTypes is undefined, causing performance observer initialization to fail silently.
Severity: MEDIUM

Suggested Fix

Use optional chaining to safely access the includes method: PerformanceObserver.supportedEntryTypes?.includes(type). This prevents the TypeError in environments where supportedEntryTypes is not defined, allowing the code to fail gracefully.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/browser-utils/src/metrics/instrument.ts#L311-L335

Potential issue: The `instrumentPerformanceObserver` function directly accesses
`PerformanceObserver.supportedEntryTypes.includes(type)`. In environments where
`PerformanceObserver` exists but the `supportedEntryTypes` property is `undefined`
(e.g., older browsers), this will throw a `TypeError`. The surrounding `try/catch` block
will swallow the error, but it will silently prevent the performance observer from being
initialized, leading to a loss of performance metrics from affected user agents.

}

function addHandler(type: InstrumentHandlerType, handler: InstrumentHandlerCallback): void {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/metrics/userTiming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
stringMatchesSomePattern,
} from '@sentry/core';
import { getBrowserPerformanceAPI, msToSec, startAndEndSpan } from './utils';
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
import { getNavigationEntry } from './web-vitals-helpers/getNavigationEntry';

interface UserTimingOptions {
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/metrics/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Client, SentrySpan, Span, SpanTimeInput, StartSpanOptions } from '@sentry/core';
import { spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core';
import { WINDOW } from '../types';
import { onHidden } from './web-vitals/lib/onHidden';
import { onHidden } from './web-vitals-helpers/onHidden';

export type WebVitalReportEvent = 'pagehide' | 'navigation';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { WINDOW } from '../../../types';
import { WINDOW } from '../../types';

// sentry-specific change:
// add optional param to not check for responseStart (see comment below)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { WINDOW } from '../../../types';
import { WINDOW } from '../../types';
import { getActivationStart } from './getActivationStart';
import { addPageListener, removePageListener } from './globalListeners';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WINDOW } from '../../../types';
import { WINDOW } from '../../types';

/**
* web-vitals 5.1.0 switched listeners to be added on the window rather than the document.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { WINDOW } from '../../../types';
import { WINDOW } from '../../types';
import { addPageListener } from './globalListeners';

export interface OnHiddenCallback {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* limitations under the License.
*/

import { WINDOW } from '../../../types.js';
import { addPageListener, removePageListener } from './globalListeners.js';
import { runOnce } from './runOnce.js';
import { WINDOW } from '../../types';
import { addPageListener, removePageListener } from './globalListeners';
import { runOnce } from './runOnce';

/**
* Runs the passed callback during the next idle period, or immediately
Expand Down
Loading
Loading