@sentry/remix
Version:
Official Sentry SDK for Remix
206 lines (164 loc) • 6.11 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const core = require('@sentry/core');
const react = require('@sentry/react');
const React = require('react');
const debugBuild = require('../utils/debug-build.js');
const remixRouteParameterization = require('./remixRouteParameterization.js');
let _useEffect;
let _useLocation;
let _useMatches;
let _instrumentNavigation;
function getInitPathName() {
if (react.WINDOW.location) {
return react.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 ? remixRouteParameterization.maybeParameterizeRemixRoute(pathname) : undefined;
if (parameterizedRoute) {
// We have a parameterized route from the manifest (dynamic route)
return { name: parameterizedRoute, source: 'route' };
}
if (remixRouteParameterization.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 = remixRouteParameterization.maybeParameterizeRemixRoute(initPathName);
const spanName = parameterizedRoute || initPathName;
const source = parameterizedRoute ? 'route' : 'url';
const spanContext = {
name: spanName,
op: 'pageload',
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.remix',
[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
},
};
react.startBrowserTracingPageLoadSpan(client, spanContext);
}
function startNavigationSpan(matches, location) {
const lastMatch = matches[matches.length - 1];
const client = react.getClient();
if (!client || !lastMatch) {
return;
}
const { name, source } = getTransactionNameAndSource(location.pathname, lastMatch.id);
const spanContext = {
name,
op: 'navigation',
attributes: {
[core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix',
[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
},
};
react.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) {
debugBuild.DEBUG_BUILD &&
!core.isNodeEnv() &&
core.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);
core.getCurrentScope().setTransactionName(name);
const activeRootSpan = core.getActiveSpan();
if (activeRootSpan) {
const transaction = core.getRootSpan(activeRootSpan);
if (transaction) {
transaction.updateName(name);
transaction.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
}
}
}
isBaseLocation = true;
}, []);
_useEffect(() => {
const activeRootSpan = core.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;
}
exports.setGlobals = setGlobals;
exports.startPageloadSpan = startPageloadSpan;
exports.withSentry = withSentry;
//# sourceMappingURL=performance.js.map