Skip to content
Open
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 @@ -278,7 +278,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '148 KB',
limit: '152 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand All @@ -294,7 +294,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '152 KB',
limit: '156 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand All @@ -310,31 +310,31 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: false,
brotli: false,
limit: '267 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: '271 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: '281 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: '285 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 @@ -22,9 +22,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.
}

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