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: Make sure fetch requests are being timed correctly #2772

Merged
merged 3 commits into from Jul 24, 2020
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
- [react] feat: Add instrumentation for React Router v3 (#2759)
- [apm/tracing] fix: Make sure fetch requests are being timed correctly (#2772)

## 5.20.0

Expand Down
10 changes: 1 addition & 9 deletions packages/apm/src/integrations/tracing.ts
Expand Up @@ -661,15 +661,7 @@ export class Tracing implements Integration {
case 'resource':
const resourceName = entry.name.replace(window.location.origin, '');
if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
// We need to update existing spans with new timing info
if (transactionSpan.spanRecorder) {
transactionSpan.spanRecorder.spans.map((finishedSpan: Span) => {
if (finishedSpan.description && finishedSpan.description.indexOf(resourceName) !== -1) {
finishedSpan.startTimestamp = timeOrigin + startTime;
finishedSpan.endTimestamp = finishedSpan.startTimestamp + duration;
}
});
}
// do nothing, we can't adjust timings just based on urls
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
} else {
const resource = transactionSpan.startChild({
description: `${entry.initiatorType} ${resourceName}`,
Expand Down
6 changes: 5 additions & 1 deletion packages/tracing/src/browser/backgroundtab.ts
Expand Up @@ -19,7 +19,11 @@ export function registerBackgroundTabDetection(): void {
logger.log(
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> since tab moved to the background, op: ${activeTransaction.op}`,
);
activeTransaction.setStatus(SpanStatus.Cancelled);
// We should not set status if it is already set, this prevent important statuses like
// error or data loss from being overwritten on transaction.
if (!activeTransaction.status) {
activeTransaction.setStatus(SpanStatus.Cancelled);
}
activeTransaction.setTag('visibilitychange', 'document.hidden');
activeTransaction.finish();
}
Expand Down
35 changes: 12 additions & 23 deletions packages/tracing/src/browser/metrics.ts
@@ -1,6 +1,5 @@
import { getGlobalObject, logger } from '@sentry/utils';

import { Span } from '../span';
import { Transaction } from '../transaction';

import { msToSec } from './utils';
Expand Down Expand Up @@ -216,30 +215,20 @@ function addResourceSpans(
timeOrigin: number,
): number | undefined {
if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
// We need to update existing spans with new timing info
if (transaction.spanRecorder) {
transaction.spanRecorder.spans.map((finishedSpan: Span) => {
if (finishedSpan.description && finishedSpan.description.indexOf(resourceName) !== -1) {
finishedSpan.startTimestamp = timeOrigin + startTime;
finishedSpan.endTimestamp = finishedSpan.startTimestamp + duration;
}
});
}
} else {
const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;

transaction.startChild({
description: `${entry.initiatorType} ${resourceName}`,
endTimestamp,
op: 'resource',
startTimestamp,
});

return endTimestamp;
return undefined;
}

return undefined;
const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;

transaction.startChild({
description: `${entry.initiatorType} ${resourceName}`,
endTimestamp,
op: 'resource',
startTimestamp,
});

return endTimestamp;
}

/** Create performance navigation related spans */
Expand Down
9 changes: 5 additions & 4 deletions packages/tracing/src/browser/request.ts
Expand Up @@ -40,9 +40,9 @@ export interface RequestInstrumentationOptions {
}

/** Data returned from fetch callback */
interface FetchData {
export interface FetchData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to export FetchData and fetchCallback?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tests

args: any[];
fetchData: {
fetchData?: {
method: string;
url: string;
// span_id
Expand Down Expand Up @@ -123,12 +123,12 @@ export function registerRequestInstrumentation(_options?: Partial<RequestInstrum
/**
* Create and track fetch request spans
*/
function fetchCallback(
export function fetchCallback(
handlerData: FetchData,
shouldCreateSpan: (url: string) => boolean,
spans: Record<string, Span>,
): void {
if (!shouldCreateSpan(handlerData.fetchData.url) || !handlerData.fetchData) {
if (!handlerData.fetchData || !shouldCreateSpan(handlerData.fetchData.url)) {
return;
}

Expand All @@ -154,6 +154,7 @@ function fetchCallback(
op: 'http',
});

handlerData.fetchData.__span = span.spanId;
spans[span.spanId] = span;

const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
Expand Down
4 changes: 2 additions & 2 deletions packages/tracing/src/idletransaction.ts
Expand Up @@ -63,7 +63,7 @@ export class IdleTransaction extends Transaction {
private _prevHeartbeatString: string | undefined;

// Amount of times heartbeat has counted. Will cause transaction to finish after 3 beats.
private _heartbeatCounter: number = 1;
private _heartbeatCounter: number = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this wrong before?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off by 1


// We should not use heartbeat if we finished a transaction
private _finished: boolean = false;
Expand Down Expand Up @@ -119,7 +119,7 @@ export class IdleTransaction extends Transaction {

if (this._heartbeatCounter >= 3) {
logger.log(
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> Heartbeat safeguard kicked in since content hasn't changed for 3 beats`,
`[Tracing] Transaction: ${SpanStatus.DeadlineExceeded} -> Heartbeat safeguard kicked in since content hasn't changed for 3 beats`,
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
);
this.setStatus(SpanStatus.DeadlineExceeded);
this.setTag('heartbeat', 'failed');
Expand Down
115 changes: 114 additions & 1 deletion packages/tracing/test/browser/request.test.ts
@@ -1,4 +1,25 @@
import { registerRequestInstrumentation } from '../../src/browser/request';
import { BrowserClient } from '@sentry/browser';
import { Hub, makeMain } from '@sentry/hub';

import { Span, Transaction } from '../../src';
import { fetchCallback, FetchData, registerRequestInstrumentation } from '../../src/browser/request';
import { addExtensionMethods } from '../../src/hubextensions';

declare global {
namespace NodeJS {
// tslint:disable-next-line: completed-docs
interface Global {
// Have to mock out Request because it is not defined in jest environment
Request: Request;
}
}
}

beforeAll(() => {
addExtensionMethods();
// @ts-ignore
global.Request = {};
});

const mockAddInstrumentationHandler = jest.fn();
let mockFetchCallback = jest.fn();
Expand Down Expand Up @@ -47,3 +68,95 @@ describe('registerRequestInstrumentation', () => {
expect(mockXHRCallback()).toBe(undefined);
});
});

describe('fetchCallback', () => {
let hub: Hub;
let transaction: Transaction;
beforeEach(() => {
hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
makeMain(hub);
transaction = hub.startTransaction({ name: 'organizations/users/:userid', op: 'pageload' }) as Transaction;
hub.configureScope(scope => scope.setSpan(transaction));
});

afterEach(() => {
if (transaction) {
transaction.finish();
}
hub.configureScope(scope => scope.setSpan(undefined));
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
});

it('does not create span if it should not be created', () => {
const shouldCreateSpan = (url: string): boolean => url === '/organizations';
const data: FetchData = {
args: ['/users'],
fetchData: {
method: 'GET',
url: '/users',
},
startTimestamp: 1595509730275,
};
const spans = {};

fetchCallback(data, shouldCreateSpan, spans);
expect(spans).toEqual({});
});

it('does not create span if there is no fetch data', () => {
const shouldCreateSpan = (_: string): boolean => true;
const data: FetchData = {
args: [],
startTimestamp: 1595509730275,
};
const spans = {};

fetchCallback(data, shouldCreateSpan, spans);
expect(spans).toEqual({});
});

it('creates and finishes fetch span on active transaction', () => {
const shouldCreateSpan = (_: string): boolean => true;
const data: FetchData = {
args: ['/users'],
fetchData: {
method: 'GET',
url: '/users',
},
startTimestamp: 1595509730275,
};
const spans: Record<string, Span> = {};

// Start fetch request
fetchCallback(data, shouldCreateSpan, spans);
const spanKey = Object.keys(spans)[0];

const fetchSpan = spans[spanKey];
expect(fetchSpan).toBeInstanceOf(Span);
expect(fetchSpan.data).toEqual({
method: 'GET',
type: 'fetch',
url: '/users',
});
expect(fetchSpan.description).toBe('GET /users');
expect(fetchSpan.op).toBe('http');
if (data.fetchData) {
expect(data.fetchData.__span).toBeDefined();
} else {
fail('Fetch data does not exist');
}

const newData = {
...data,
endTimestamp: data.startTimestamp + 12343234,
};

// End fetch request
fetchCallback(newData, shouldCreateSpan, spans);
expect(spans).toEqual({});
if (transaction.spanRecorder) {
expect(transaction.spanRecorder.spans[1].endTimestamp).toBeDefined();
} else {
fail('Transaction does not have span recorder');
}
});
});