@sentry/remix
Version:
Official Sentry SDK for Remix
202 lines (161 loc) • 6.01 kB
JavaScript
import { isNodeEnv, debug, getCurrentScope, getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { startBrowserTracingPageLoadSpan, WINDOW, getClient, startBrowserTracingNavigationSpan } from '@sentry/react';
import * as React from 'react';
import { DEBUG_BUILD } from '../utils/debug-build.js';
import { maybeParameterizeRemixRoute, hasManifest } from './remixRouteParameterization.js';
let _useEffect;
let _useLocation;
let _useMatches;
let _instrumentNavigation;
function getInitPathName() {
if (WINDOW.location) {
return WINDOW.location.pathname;
}
return undefined;
}
/**
* Determines the transaction name and source for a route.
* Handles three cases:
* 1. Dynamic routes with manifest (Vite apps): Use parameterized path with source 'route'
* 2. Static routes with manifest (Vite apps): Use pathname with source 'url'
* 3. Legacy apps without manifest: Use route ID with source 'route'
*/
function getTransactionNameAndSource(
pathname,
routeId,
) {
const parameterizedRoute = pathname ? maybeParameterizeRemixRoute(pathname) : undefined;
if (parameterizedRoute) {
// We have a parameterized route from the manifest (dynamic route)
return { name: parameterizedRoute, source: 'route' };
}
if (hasManifest()) {
// We have a manifest but no parameterization (static route)
// Use the pathname with source 'url'
return { name: pathname || routeId, source: 'url' };
}
// No manifest available (legacy app without Vite plugin)
// Fall back to route ID for backward compatibility
return { name: routeId, source: 'route' };
}
function startPageloadSpan(client) {
const initPathName = getInitPathName();
if (!initPathName) {
return;
}
// Try to parameterize the route using the route manifest
const parameterizedRoute = maybeParameterizeRemixRoute(initPathName);
const spanName = parameterizedRoute || initPathName;
const source = parameterizedRoute ? 'route' : 'url';
const spanContext = {
name: spanName,
op: 'pageload',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.remix',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
},
};
startBrowserTracingPageLoadSpan(client, spanContext);
}
function startNavigationSpan(matches, location) {
const lastMatch = matches[matches.length - 1];
const client = getClient();
if (!client || !lastMatch) {
return;
}
const { name, source } = getTransactionNameAndSource(location.pathname, lastMatch.id);
const spanContext = {
name,
op: 'navigation',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
},
};
startBrowserTracingNavigationSpan(client, spanContext);
}
/**
* Wraps a remix `root` (see: https://remix.run/docs/en/main/start/quickstart#the-root-route)
* To enable pageload/navigation tracing on every route.
*
* @param OrigApp The Remix root to wrap
* @param useEffect The `useEffect` hook from `react`
* @param useLocation The `useLocation` hook from `@remix-run/react`
* @param useMatches The `useMatches` hook from `@remix-run/react`
* @param instrumentNavigation Whether to instrument navigation spans. Defaults to `true`.
*/
function withSentry(
OrigApp,
useEffect,
useLocation,
useMatches,
instrumentNavigation,
) {
const SentryRoot = (props) => {
setGlobals({ useEffect, useLocation, useMatches, instrumentNavigation: instrumentNavigation || true });
// Early return when any of the required functions is not available.
if (!_useEffect || !_useLocation || !_useMatches) {
DEBUG_BUILD &&
!isNodeEnv() &&
debug.warn('Remix SDK was unable to wrap your root because of one or more missing parameters.');
// @ts-expect-error Setting more specific React Component typing for `R` generic above
// will break advanced type inference done by react router params
return React.createElement(OrigApp, { ...props,} );
}
let isBaseLocation = false;
const location = _useLocation();
const matches = _useMatches();
_useEffect(() => {
const lastMatch = matches?.[matches.length - 1];
if (lastMatch) {
const { name, source } = getTransactionNameAndSource(location.pathname, lastMatch.id);
getCurrentScope().setTransactionName(name);
const activeRootSpan = getActiveSpan();
if (activeRootSpan) {
const transaction = getRootSpan(activeRootSpan);
if (transaction) {
transaction.updateName(name);
transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
}
}
}
isBaseLocation = true;
}, []);
_useEffect(() => {
const activeRootSpan = getActiveSpan();
if (isBaseLocation) {
if (activeRootSpan) {
activeRootSpan.end();
}
return;
}
if (_instrumentNavigation && matches?.length) {
if (activeRootSpan) {
activeRootSpan.end();
}
startNavigationSpan(matches, location);
}
}, [location]);
isBaseLocation = false;
// @ts-expect-error Setting more specific React Component typing for `R` generic above
// will break advanced type inference done by react router params
return React.createElement(OrigApp, { ...props,} );
};
// @ts-expect-error Setting more specific React Component typing for `R` generic above
// will break advanced type inference done by react router params
return SentryRoot;
}
function setGlobals({
useEffect,
useLocation,
useMatches,
instrumentNavigation,
}
) {
_useEffect = useEffect || _useEffect;
_useLocation = useLocation || _useLocation;
_useMatches = useMatches || _useMatches;
_instrumentNavigation = instrumentNavigation ?? _instrumentNavigation;
}
export { setGlobals, startPageloadSpan, withSentry };
//# sourceMappingURL=performance.js.map