Skip to content

Commit c6c54a1

Browse files
authored
fix: Make sure fetch requests are being timed correctly (#2772)
1 parent 82abd33 commit c6c54a1

File tree

7 files changed

+141
-45
lines changed

7 files changed

+141
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

89
## 5.20.0
910

packages/apm/src/integrations/tracing.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -660,17 +660,9 @@ export class Tracing implements Integration {
660660
break;
661661
case 'resource':
662662
const resourceName = entry.name.replace(window.location.origin, '');
663-
if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
664-
// We need to update existing spans with new timing info
665-
if (transactionSpan.spanRecorder) {
666-
transactionSpan.spanRecorder.spans.map((finishedSpan: Span) => {
667-
if (finishedSpan.description && finishedSpan.description.indexOf(resourceName) !== -1) {
668-
finishedSpan.startTimestamp = timeOrigin + startTime;
669-
finishedSpan.endTimestamp = finishedSpan.startTimestamp + duration;
670-
}
671-
});
672-
}
673-
} else {
663+
// we already instrument based on fetch and xhr, so we don't need to
664+
// duplicate spans here.
665+
if (entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch') {
674666
const resource = transactionSpan.startChild({
675667
description: `${entry.initiatorType} ${resourceName}`,
676668
op: `resource`,

packages/tracing/src/browser/backgroundtab.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ export function registerBackgroundTabDetection(): void {
1919
logger.log(
2020
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> since tab moved to the background, op: ${activeTransaction.op}`,
2121
);
22-
activeTransaction.setStatus(SpanStatus.Cancelled);
22+
// We should not set status if it is already set, this prevent important statuses like
23+
// error or data loss from being overwritten on transaction.
24+
if (!activeTransaction.status) {
25+
activeTransaction.setStatus(SpanStatus.Cancelled);
26+
}
2327
activeTransaction.setTag('visibilitychange', 'document.hidden');
2428
activeTransaction.finish();
2529
}

packages/tracing/src/browser/metrics.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getGlobalObject, logger } from '@sentry/utils';
22

3-
import { Span } from '../span';
43
import { Transaction } from '../transaction';
54

65
import { msToSec } from './utils';
@@ -215,31 +214,23 @@ function addResourceSpans(
215214
duration: number,
216215
timeOrigin: number,
217216
): number | undefined {
217+
// we already instrument based on fetch and xhr, so we don't need to
218+
// duplicate spans here.
218219
if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
219-
// We need to update existing spans with new timing info
220-
if (transaction.spanRecorder) {
221-
transaction.spanRecorder.spans.map((finishedSpan: Span) => {
222-
if (finishedSpan.description && finishedSpan.description.indexOf(resourceName) !== -1) {
223-
finishedSpan.startTimestamp = timeOrigin + startTime;
224-
finishedSpan.endTimestamp = finishedSpan.startTimestamp + duration;
225-
}
226-
});
227-
}
228-
} else {
229-
const startTimestamp = timeOrigin + startTime;
230-
const endTimestamp = startTimestamp + duration;
231-
232-
transaction.startChild({
233-
description: `${entry.initiatorType} ${resourceName}`,
234-
endTimestamp,
235-
op: 'resource',
236-
startTimestamp,
237-
});
238-
239-
return endTimestamp;
220+
return undefined;
240221
}
241222

242-
return undefined;
223+
const startTimestamp = timeOrigin + startTime;
224+
const endTimestamp = startTimestamp + duration;
225+
226+
transaction.startChild({
227+
description: `${entry.initiatorType} ${resourceName}`,
228+
endTimestamp,
229+
op: 'resource',
230+
startTimestamp,
231+
});
232+
233+
return endTimestamp;
243234
}
244235

245236
/** Create performance navigation related spans */

packages/tracing/src/browser/request.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ export interface RequestInstrumentationOptions {
4040
}
4141

4242
/** Data returned from fetch callback */
43-
interface FetchData {
43+
export interface FetchData {
4444
args: any[];
45-
fetchData: {
45+
fetchData?: {
4646
method: string;
4747
url: string;
4848
// span_id
@@ -104,7 +104,7 @@ export function registerRequestInstrumentation(_options?: Partial<RequestInstrum
104104
if (traceFetch) {
105105
addInstrumentationHandler({
106106
callback: (handlerData: FetchData) => {
107-
fetchCallback(handlerData, shouldCreateSpan, spans);
107+
_fetchCallback(handlerData, shouldCreateSpan, spans);
108108
},
109109
type: 'fetch',
110110
});
@@ -123,12 +123,12 @@ export function registerRequestInstrumentation(_options?: Partial<RequestInstrum
123123
/**
124124
* Create and track fetch request spans
125125
*/
126-
function fetchCallback(
126+
export function _fetchCallback(
127127
handlerData: FetchData,
128128
shouldCreateSpan: (url: string) => boolean,
129129
spans: Record<string, Span>,
130130
): void {
131-
if (!shouldCreateSpan(handlerData.fetchData.url) || !handlerData.fetchData) {
131+
if (!handlerData.fetchData || !shouldCreateSpan(handlerData.fetchData.url)) {
132132
return;
133133
}
134134

@@ -154,6 +154,7 @@ function fetchCallback(
154154
op: 'http',
155155
});
156156

157+
handlerData.fetchData.__span = span.spanId;
157158
spans[span.spanId] = span;
158159

159160
const request = (handlerData.args[0] = handlerData.args[0] as string | Request);

packages/tracing/src/idletransaction.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class IdleTransaction extends Transaction {
6363
private _prevHeartbeatString: string | undefined;
6464

6565
// Amount of times heartbeat has counted. Will cause transaction to finish after 3 beats.
66-
private _heartbeatCounter: number = 1;
66+
private _heartbeatCounter: number = 0;
6767

6868
// We should not use heartbeat if we finished a transaction
6969
private _finished: boolean = false;
@@ -118,9 +118,7 @@ export class IdleTransaction extends Transaction {
118118
this._prevHeartbeatString = heartbeatString;
119119

120120
if (this._heartbeatCounter >= 3) {
121-
logger.log(
122-
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> Heartbeat safeguard kicked in since content hasn't changed for 3 beats`,
123-
);
121+
logger.log(`[Tracing] Transaction finished because of no change for 3 heart beats`);
124122
this.setStatus(SpanStatus.DeadlineExceeded);
125123
this.setTag('heartbeat', 'failed');
126124
this.finish();

packages/tracing/test/browser/request.test.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,25 @@
1-
import { registerRequestInstrumentation } from '../../src/browser/request';
1+
import { BrowserClient } from '@sentry/browser';
2+
import { Hub, makeMain } from '@sentry/hub';
3+
4+
import { Span, Transaction } from '../../src';
5+
import { _fetchCallback, FetchData, registerRequestInstrumentation } from '../../src/browser/request';
6+
import { addExtensionMethods } from '../../src/hubextensions';
7+
8+
declare global {
9+
namespace NodeJS {
10+
// tslint:disable-next-line: completed-docs
11+
interface Global {
12+
// Have to mock out Request because it is not defined in jest environment
13+
Request: Request;
14+
}
15+
}
16+
}
17+
18+
beforeAll(() => {
19+
addExtensionMethods();
20+
// @ts-ignore
21+
global.Request = {};
22+
});
223

324
const mockAddInstrumentationHandler = jest.fn();
425
let mockFetchCallback = jest.fn();
@@ -47,3 +68,91 @@ describe('registerRequestInstrumentation', () => {
4768
expect(mockXHRCallback()).toBe(undefined);
4869
});
4970
});
71+
72+
describe('_fetchCallback()', () => {
73+
let hub: Hub;
74+
let transaction: Transaction;
75+
beforeAll(() => {
76+
hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
77+
makeMain(hub);
78+
});
79+
80+
beforeEach(() => {
81+
transaction = hub.startTransaction({ name: 'organizations/users/:userid', op: 'pageload' }) as Transaction;
82+
hub.configureScope(scope => scope.setSpan(transaction));
83+
});
84+
85+
it('does not create span if it should not be created', () => {
86+
const shouldCreateSpan = (url: string): boolean => url === '/organizations';
87+
const data: FetchData = {
88+
args: ['/users'],
89+
fetchData: {
90+
method: 'GET',
91+
url: '/users',
92+
},
93+
startTimestamp: 1595509730275,
94+
};
95+
const spans = {};
96+
97+
_fetchCallback(data, shouldCreateSpan, spans);
98+
expect(spans).toEqual({});
99+
});
100+
101+
it('does not create span if there is no fetch data', () => {
102+
const shouldCreateSpan = (_: string): boolean => true;
103+
const data: FetchData = {
104+
args: [],
105+
startTimestamp: 1595509730275,
106+
};
107+
const spans = {};
108+
109+
_fetchCallback(data, shouldCreateSpan, spans);
110+
expect(spans).toEqual({});
111+
});
112+
113+
it('creates and finishes fetch span on active transaction', () => {
114+
const shouldCreateSpan = (_: string): boolean => true;
115+
const data: FetchData = {
116+
args: ['/users'],
117+
fetchData: {
118+
method: 'GET',
119+
url: '/users',
120+
},
121+
startTimestamp: 1595509730275,
122+
};
123+
const spans: Record<string, Span> = {};
124+
125+
// Start fetch request
126+
_fetchCallback(data, shouldCreateSpan, spans);
127+
const spanKey = Object.keys(spans)[0];
128+
129+
const fetchSpan = spans[spanKey];
130+
expect(fetchSpan).toBeInstanceOf(Span);
131+
expect(fetchSpan.data).toEqual({
132+
method: 'GET',
133+
type: 'fetch',
134+
url: '/users',
135+
});
136+
expect(fetchSpan.description).toBe('GET /users');
137+
expect(fetchSpan.op).toBe('http');
138+
if (data.fetchData) {
139+
expect(data.fetchData.__span).toBeDefined();
140+
} else {
141+
fail('Fetch data does not exist');
142+
}
143+
144+
const newData = {
145+
...data,
146+
endTimestamp: data.startTimestamp + 12343234,
147+
};
148+
149+
// End fetch request
150+
_fetchCallback(newData, shouldCreateSpan, spans);
151+
expect(spans).toEqual({});
152+
if (transaction.spanRecorder) {
153+
expect(transaction.spanRecorder.spans[1].endTimestamp).toBeDefined();
154+
} else {
155+
fail('Transaction does not have span recorder');
156+
}
157+
});
158+
});

0 commit comments

Comments
 (0)