Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vue): Start pageload transaction earlier to capture missing spans #5983

Merged
merged 3 commits into from Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 27 additions & 13 deletions packages/vue/src/router.ts
@@ -1,5 +1,8 @@
import { captureException } from '@sentry/browser';
import { Transaction, TransactionContext, TransactionSource } from '@sentry/types';
import { WINDOW } from '@sentry/utils';

import { getActiveTransaction } from './tracing';

export type VueRouterInstrumentation = <T extends Transaction>(
startTransaction: (context: TransactionContext) => T | undefined,
Expand Down Expand Up @@ -31,7 +34,7 @@ interface VueRouter {
}

/**
* Creates routing instrumentation for Vue Router v2
* Creates routing instrumentation for Vue Router v2, v3 and v4
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param router The Vue Router instance that is used
*/
Expand All @@ -41,6 +44,23 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
startTransactionOnPageLoad: boolean = true,
startTransactionOnLocationChange: boolean = true,
) => {
const tags = {
'routing.instrumentation': 'vue-router',
};

// We have to start the pageload transaction as early as possible (before the router's `beforeEach` hook
// is called) to not miss child spans of the pageload.
if (startTransactionOnPageLoad) {
startTransaction({
name: WINDOW.location.pathname,
op: 'pageload',
tags,
metadata: {
source: 'url',
},
});
}

router.onError(error => captureException(error));

router.beforeEach((to, from, next) => {
Expand All @@ -54,9 +74,6 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
// hence only '==' instead of '===', because `undefined == null` evaluates to `true`
const isPageLoadNavigation = from.name == null && from.matched.length === 0;

const tags = {
'routing.instrumentation': 'vue-router',
};
const data = {
params: to.params,
query: to.query,
Expand All @@ -74,15 +91,12 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
}

if (startTransactionOnPageLoad && isPageLoadNavigation) {
startTransaction({
name: transactionName,
op: 'pageload',
tags,
data,
metadata: {
source: transactionSource,
},
});
const pageloadTransaction = getActiveTransaction();
if (pageloadTransaction) {
pageloadTransaction.setName(transactionName, transactionSource);
pageloadTransaction.setData('params', data.params);
pageloadTransaction.setData('query', data.query);
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
}
}

if (startTransactionOnLocationChange && !isPageLoadNavigation) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/src/tracing.ts
Expand Up @@ -29,7 +29,7 @@ const HOOKS: { [key in Operation]: Hook[] } = {
};

/** Grabs active transaction off scope, if any */
function getActiveTransaction(): Transaction | undefined {
export function getActiveTransaction(): Transaction | undefined {
return getCurrentHub().getScope()?.getTransaction();
}

Expand Down Expand Up @@ -117,8 +117,8 @@ export const createTracingMixins = (options: TracingOptions): Mixins => {
// The before hook did not start the tracking span, so the span was not added.
// This is probably because it happened before there is an active transaction
if (!span) return;

span.finish();

finishRootSpan(this, timestampInSeconds(), options.timeout);
}
};
Expand Down
74 changes: 64 additions & 10 deletions packages/vue/test/router.test.ts
@@ -1,8 +1,11 @@
import * as SentryBrowser from '@sentry/browser';
import { Transaction } from '@sentry/types';

import { vueRouterInstrumentation } from '../src';
import { Route } from '../src/router';

import * as vueTracing from '../src/tracing';

const captureExceptionSpy = jest.spyOn(SentryBrowser, 'captureException');

const mockVueRouter = {
Expand Down Expand Up @@ -74,13 +77,12 @@ describe('vueRouterInstrumentation()', () => {
});

it.each([
['initialPageloadRoute', 'normalRoute1', 'pageload', '/books/:bookId/chapter/:chapterId', 'route'],
['normalRoute1', 'normalRoute2', 'navigation', '/accounts/:accountId', 'route'],
['normalRoute2', 'namedRoute', 'navigation', 'login-screen', 'custom'],
['normalRoute2', 'unmatchedRoute', 'navigation', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'],
['normalRoute1', 'normalRoute2', '/accounts/:accountId', 'route'],
['normalRoute2', 'namedRoute', 'login-screen', 'custom'],
['normalRoute2', 'unmatchedRoute', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'],
])(
'should return instrumentation that instruments VueRouter.beforeEach(%s, %s)',
(fromKey, toKey, op, transactionName, transactionSource) => {
'should return instrumentation that instruments VueRouter.beforeEach(%s, %s) for navigations',
(fromKey, toKey, transactionName, transactionSource) => {
// create instrumentation
const instrument = vueRouterInstrumentation(mockVueRouter);

Expand All @@ -95,7 +97,8 @@ describe('vueRouterInstrumentation()', () => {
const to = testRoutes[toKey];
beforeEachCallback(to, from, mockNext);

expect(mockStartTransaction).toHaveBeenCalledTimes(1);
// first startTx call happens when the instrumentation is initialized (for pageloads)
expect(mockStartTransaction).toHaveBeenCalledTimes(2);
expect(mockStartTransaction).toHaveBeenCalledWith({
name: transactionName,
metadata: {
Expand All @@ -105,12 +108,63 @@ describe('vueRouterInstrumentation()', () => {
params: to.params,
query: to.query,
},
op: op,
op: 'navigation',
tags: {
'routing.instrumentation': 'vue-router',
},
});

expect(mockNext).toHaveBeenCalledTimes(1);
},
);

it.each([
['initialPageloadRoute', 'normalRoute1', '/books/:bookId/chapter/:chapterId', 'route'],
['initialPageloadRoute', 'namedRoute', 'login-screen', 'custom'],
['initialPageloadRoute', 'unmatchedRoute', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'],
])(
'should return instrumentation that instruments VueRouter.beforeEach(%s, %s) for pageloads',
(fromKey, toKey, _transactionName, _transactionSource) => {
const mockedTxn = {
setName: jest.fn(),
setData: jest.fn(),
};
const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => {
return mockedTxn;
});
jest.spyOn(vueTracing, 'getActiveTransaction').mockImplementation(() => mockedTxn as unknown as Transaction);

// create instrumentation
const instrument = vueRouterInstrumentation(mockVueRouter);

// instrument
instrument(customMockStartTxn, true, true);

// check for transaction start
expect(customMockStartTxn).toHaveBeenCalledTimes(1);
expect(customMockStartTxn).toHaveBeenCalledWith({
name: '/',
metadata: {
source: 'url',
},
op: 'pageload',
tags: {
'routing.instrumentation': 'vue-router',
},
});

const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];

const from = testRoutes[fromKey];
const to = testRoutes[toKey];

beforeEachCallback(to, from, mockNext);
expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1);

expect(mockedTxn.setName).toHaveBeenCalledWith(_transactionName, _transactionSource);
expect(mockedTxn.setData).toHaveBeenNthCalledWith(1, 'params', to.params);
expect(mockedTxn.setData).toHaveBeenNthCalledWith(2, 'query', to.query);

expect(mockNext).toHaveBeenCalledTimes(1);
},
);
Expand Down Expand Up @@ -148,7 +202,7 @@ describe('vueRouterInstrumentation()', () => {
// create instrumentation
const instrument = vueRouterInstrumentation(mockVueRouter);

// instrument
// instrument (this will call startTrransaction once for pageloads but we can ignore that)
instrument(mockStartTransaction, true, startTransactionOnLocationChange);

// check
Expand All @@ -157,7 +211,7 @@ describe('vueRouterInstrumentation()', () => {
const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
beforeEachCallback(testRoutes['normalRoute2'], testRoutes['normalRoute1'], mockNext);

expect(mockStartTransaction).toHaveBeenCalledTimes(expectedCallsAmount);
expect(mockStartTransaction).toHaveBeenCalledTimes(expectedCallsAmount + 1);
},
);
});