Skip to content

Commit 26efb55

Browse files
committed
fix: Make sure fetch requests are being timed correctly
1 parent 82abd33 commit 26efb55

File tree

6 files changed

+139
-40
lines changed

6 files changed

+139
-40
lines changed

packages/apm/src/integrations/tracing.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -661,15 +661,7 @@ export class Tracing implements Integration {
661661
case 'resource':
662662
const resourceName = entry.name.replace(window.location.origin, '');
663663
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-
}
664+
// do nothing, we can't adjust timings just based on urls
673665
} else {
674666
const resource = transactionSpan.startChild({
675667
description: `${entry.initiatorType} ${resourceName}`,

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: 12 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';
@@ -216,30 +215,20 @@ function addResourceSpans(
216215
timeOrigin: number,
217216
): number | undefined {
218217
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;
218+
return undefined;
240219
}
241220

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

245234
/** Create performance navigation related spans */

packages/tracing/src/browser/request.ts

Lines changed: 5 additions & 4 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
@@ -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 & 2 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;
@@ -119,7 +119,7 @@ export class IdleTransaction extends Transaction {
119119

120120
if (this._heartbeatCounter >= 3) {
121121
logger.log(
122-
`[Tracing] Transaction: ${SpanStatus.Cancelled} -> Heartbeat safeguard kicked in since content hasn't changed for 3 beats`,
122+
`[Tracing] Transaction: ${SpanStatus.DeadlineExceeded} -> Heartbeat safeguard kicked in since content hasn't changed for 3 beats`,
123123
);
124124
this.setStatus(SpanStatus.DeadlineExceeded);
125125
this.setTag('heartbeat', 'failed');

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

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

0 commit comments

Comments
 (0)