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
40 changes: 16 additions & 24 deletions packages/vue/src/tanstackrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
return lastMatch?.routeId !== '__root__' ? lastMatch : undefined;
};

const applyRouteMatch = (
span: NonNullable<ReturnType<typeof startBrowserTracingPageLoadSpan>>,
match: RouteMatch | undefined,
toLocation: TanstackRouterLocation,
fallbackName: string,
): void => {
span.updateName(match ? match.routeId : fallbackName);
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url');
span.setAttributes({
[URL_TEMPLATE]: match?.routeId,
...locationToSpanUrlAttributes(router, toLocation),
...routeMatchToParamSpanAttributes(match),
});
};

const initialWindowLocation = WINDOW.location;
if (instrumentPageLoad && initialWindowLocation) {
const routeMatch = resolveRouteMatch(
Expand Down Expand Up @@ -89,15 +104,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
}
const { toLocation } = onResolvedArgs as TanstackRouterSubscribeArgs;
const resolvedMatch = resolveRouteMatch(toLocation.pathname, toLocation.search);
if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) {
pageloadSpan.updateName(resolvedMatch.routeId);
pageloadSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[URL_TEMPLATE]: resolvedMatch.routeId,
...locationToSpanUrlAttributes(router, toLocation),
...routeMatchToParamSpanAttributes(resolvedMatch),
});
}
applyRouteMatch(pageloadSpan, resolvedMatch, toLocation, toLocation.pathname);
});
}

Comment thread
sentry[bot] marked this conversation as resolved.
Expand All @@ -108,21 +115,6 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
// span on the first `onBeforeLoad`, rename it on later ones, and clear it on `onResolved`.
let inFlightNavigationSpan: ReturnType<typeof startBrowserTracingNavigationSpan> | undefined;

const applyRouteMatch = (
span: NonNullable<typeof inFlightNavigationSpan>,
match: RouteMatch | undefined,
toLocation: TanstackRouterLocation,
fallbackName: string,
): void => {
span.updateName(match ? match.routeId : fallbackName);
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url');
span.setAttributes({
[URL_TEMPLATE]: match?.routeId,
...locationToSpanUrlAttributes(router, toLocation),
...routeMatchToParamSpanAttributes(match),
});
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
router.subscribe('onBeforeLoad', (onBeforeLoadArgs: any) => {
const { toLocation, fromLocation } = onBeforeLoadArgs as TanstackRouterSubscribeArgs;
Expand Down
100 changes: 100 additions & 0 deletions packages/vue/test/tanstackrouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const mockNavigationSpan = {
setAttributes: vi.fn(),
};

const mockPageloadSpan = {
updateName: vi.fn(),
setAttribute: vi.fn(),
setAttributes: vi.fn(),
};

describe('tanstackRouterBrowserTracingIntegration', () => {
const mockMatchedRoutes = [
{
Expand Down Expand Up @@ -72,6 +78,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => {
beforeEach(() => {
vi.clearAllMocks();
startBrowserTracingNavigationSpanSpy.mockReturnValue(mockNavigationSpan as any);
startBrowserTracingPageLoadSpanSpy.mockReturnValue(mockPageloadSpan as any);

// Mock window.location
vi.stubGlobal('window', {
Expand Down Expand Up @@ -130,6 +137,99 @@ describe('tanstackRouterBrowserTracingIntegration', () => {
expect(startBrowserTracingPageLoadSpanSpy).not.toHaveBeenCalled();
});

it('updates pageload span URL attributes on redirect to the same route template', () => {
const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
instrumentPageLoad: true,
instrumentNavigation: false,
});

integration.afterAllSetup(mockClient as any);

const onResolvedCallback = getSubscribeCallback('onResolved');
expect(onResolvedCallback).toBeDefined();

(mockRouter.matchRoutes as any).mockReturnValueOnce([
{
routeId: '/test/:id',
pathname: '/test/456',
params: { id: '456' },
},
]);

onResolvedCallback({
toLocation: {
pathname: '/test/456',
search: {},
},
});

expect(mockPageloadSpan.setAttributes).toHaveBeenCalledWith(
expect.objectContaining({
[URL_TEMPLATE]: '/test/:id',
'url.path': '/test/456',
'url.full': expect.any(String),
'url.path.parameter.id': '456',
'params.id': '456',
}),
);
});

it('clears url.template on pageload onResolved when the final destination does not match a route', () => {
const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
instrumentPageLoad: true,
instrumentNavigation: false,
});

integration.afterAllSetup(mockClient as any);

const onResolvedCallback = getSubscribeCallback('onResolved');
expect(onResolvedCallback).toBeDefined();

// A redirect during pageload lands on a URL that no longer matches a route.
(mockRouter.matchRoutes as any).mockReturnValueOnce([{ routeId: '__root__', params: {} }]);

onResolvedCallback({
toLocation: { pathname: '/unknown/path', search: {} },
});

expect(mockPageloadSpan.updateName).toHaveBeenCalledWith('/unknown/path');
expect(mockPageloadSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
expect(mockPageloadSpan.setAttributes).toHaveBeenLastCalledWith(
expect.objectContaining({
[URL_TEMPLATE]: undefined,
'url.path': '/unknown/path',
'url.full': expect.any(String),
}),
);
});

it('unsubscribes the pageload onResolved handler after the first resolution', () => {
const unsubscribe = vi.fn();
(mockRouter.subscribe as any).mockImplementation((eventType: string) => {
if (eventType === 'onResolved') {
return unsubscribe;
}
return vi.fn();
});

const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
instrumentPageLoad: true,
instrumentNavigation: false,
});

integration.afterAllSetup(mockClient as any);

const onResolvedCallback = getSubscribeCallback('onResolved');
expect(onResolvedCallback).toBeDefined();

onResolvedCallback({ toLocation: { pathname: '/test/456', search: {} } });

// The handler must detach itself so later resolutions (e.g. subsequent navigations)
// don't touch the pageload span again.
expect(unsubscribe).toHaveBeenCalledTimes(1);
expect(mockPageloadSpan.updateName).toHaveBeenCalledTimes(1);
});

const getSubscribeCallback = (eventType: string): ((...args: any[]) => void) =>
(mockRouter.subscribe as any).mock.calls.find(
(call: [string, (...args: any[]) => void]) => call[0] === eventType,
Expand Down
Loading