Skip to content

Commit 5ebe8a2

Browse files
committed
add tests for sampling
1 parent 20f4bbc commit 5ebe8a2

File tree

1 file changed

+219
-30
lines changed

1 file changed

+219
-30
lines changed

packages/tracing/test/hub.test.ts

Lines changed: 219 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,245 @@
11
import { BrowserClient } from '@sentry/browser';
2-
import { Hub } from '@sentry/hub';
2+
import * as hubModuleRaw from '@sentry/hub'; // for mocking
3+
import { getMainCarrier, Hub } from '@sentry/hub';
4+
import { NodeClient } from '@sentry/node';
5+
import * as utilsModule from '@sentry/utils'; // for mocking
6+
import { getGlobalObject, isNodeEnv, logger } from '@sentry/utils';
7+
import * as nodeHttpModule from 'http';
38

49
import { addExtensionMethods } from '../src/hubextensions';
510

11+
// Do this once so that we'll be able to spy on hub methods later. If this isn't done, it results in "TypeError: Cannot
12+
// set property <methodYouWantToSpyOn> of #<Object> which has only a getter." This just converts the module object
13+
// (which has no setters) to a regular object (with regular properties which can be gotten or set). See
14+
// https://stackoverflow.com/a/53307822/.
15+
16+
// (This doesn't affect the utils module because it uses `export * from './myModule' syntax rather than `export
17+
// {<individually named methods>} from './myModule'` syntax in its index.ts. Only *named* exports seem to trigger the
18+
// problem.)
19+
const hubModule = { ...hubModuleRaw}
20+
621
addExtensionMethods();
722

823
describe('Hub', () => {
24+
beforeEach(() => {
25+
jest.spyOn(logger, 'warn');
26+
jest.spyOn(logger, 'log');
27+
jest.spyOn(utilsModule, 'isNodeEnv');
28+
29+
// NB: Upon refactoring, this spy was no longer needed. Leaving it in as an excuse to leave in the note above, so
30+
// that it can save future folks the headache.
31+
jest.spyOn(hubModule, 'getActiveDomain');
32+
});
33+
934
afterEach(() => {
10-
jest.resetAllMocks();
35+
jest.restoreAllMocks();
1136
jest.useRealTimers();
1237
});
1338

14-
describe('getTransaction', () => {
15-
test('simple invoke', () => {
39+
describe('getTransaction()', () => {
40+
41+
it('should find a transaction which has been set on the scope', () => {
1642
const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
17-
const transaction = hub.startTransaction({ name: 'foo' });
43+
const transaction = hub.startTransaction({ name: 'dogpark' });
1844
hub.configureScope(scope => {
1945
scope.setSpan(transaction);
2046
});
21-
hub.configureScope(s => {
22-
expect(s.getTransaction()).toBe(transaction);
23-
});
47+
48+
expect(hub.getScope()?.getTransaction()).toBe(transaction)
49+
2450
});
2551

26-
test('not invoke', () => {
52+
it("should not find an open transaction if it's not on the scope", () => {
2753
const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
28-
const transaction = hub.startTransaction({ name: 'foo' });
29-
hub.configureScope(s => {
30-
expect(s.getTransaction()).toBeUndefined();
31-
});
32-
transaction.finish();
54+
hub.startTransaction({ name: 'dogpark' });
55+
56+
expect(hub.getScope()?.getTransaction()).toBeUndefined()
3357
});
34-
});
58+
}); // end describe('getTransaction()')
59+
60+
describe('transaction sampling', () => {
61+
describe('options', () => {
62+
63+
it("should call tracesSampler if it's defined", () => {
64+
const tracesSampler = jest.fn();
65+
const hub = new Hub(new BrowserClient({ tracesSampler }));
66+
hub.startTransaction({ name: 'dogpark' });
67+
68+
expect(tracesSampler).toHaveBeenCalled();
69+
});
70+
71+
it('should prefer tracesSampler to tracesSampleRate', () => {
72+
const tracesSampler = jest.fn();
73+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 1, tracesSampler: tracesSampler }));
74+
hub.startTransaction({ name: 'dogpark' });
75+
76+
expect(tracesSampler).toHaveBeenCalled();
77+
});
78+
79+
}); // end describe('options')
80+
81+
describe('default sample context', () => {
82+
83+
it('should extract request data for default sampling context when in node', () => {
84+
// make sure we look like we're in node
85+
(isNodeEnv as jest.Mock).mockReturnValue(true);
86+
87+
// pre-normalization request object
88+
const mockRequestObject = ({
89+
headers: { ears: 'furry', nose: 'wet', tongue: 'panting', cookie: 'favorite=zukes' },
90+
method: 'wagging',
91+
protocol: 'mutualsniffing',
92+
hostname: 'the.dog.park',
93+
originalUrl: '/by/the/trees/?chase=me&please=thankyou',
94+
} as unknown) as nodeHttpModule.IncomingMessage;
95+
96+
// The "as unknown as nodeHttpModule.IncomingMessage" casting above keeps TS happy, but doesn't actually mean that
97+
// mockRequestObject IS an instance of our desired class. Fix that so that when we search for it by type, we
98+
// actually find it.
99+
Object.setPrototypeOf(mockRequestObject, nodeHttpModule.IncomingMessage.prototype);
100+
101+
// in production, the domain will have at minimum the request and the response, so make a response object to prove
102+
// that our code identifying the request in domain.members works
103+
const mockResponseObject = new nodeHttpModule.ServerResponse(mockRequestObject);
104+
105+
// normally the node request handler does this, but that's not part of this test
106+
(getMainCarrier().__SENTRY__!.extensions as any).domain = {
107+
active: { members: [mockRequestObject, mockResponseObject] },
108+
};
109+
110+
const tracesSampler = jest.fn();
111+
const hub = new Hub(new NodeClient({ tracesSampler }));
112+
hub.startTransaction({ name: 'dogpark' });
113+
114+
// post-normalization request object
115+
expect(tracesSampler).toHaveBeenCalledWith(expect.objectContaining({
116+
request: {
117+
headers: { ears: 'furry', nose: 'wet', tongue: 'panting', cookie: 'favorite=zukes' },
118+
method: 'wagging',
119+
url: 'http://the.dog.park/by/the/trees/?chase=me&please=thankyou',
120+
cookies: { favorite: 'zukes' },
121+
query_string: 'chase=me&please=thankyou',
122+
},
123+
}));
124+
});
125+
126+
it('should extract window.location/self.location for default sampling context when in browser/service worker', () => {
127+
// make sure we look like we're in the browser
128+
(isNodeEnv as jest.Mock).mockReturnValue(false);
129+
130+
const dogParkLocation = {
131+
hash: '#next-to-the-fountain',
132+
host: 'the.dog.park',
133+
hostname: 'the.dog.park',
134+
href: 'mutualsniffing://the.dog.park/by/the/trees/?chase=me&please=thankyou#next-to-the-fountain',
135+
origin: "'mutualsniffing://the.dog.park",
136+
pathname: '/by/the/trees/',
137+
port: '',
138+
protocol: 'mutualsniffing:',
139+
search: '?chase=me&please=thankyou',
140+
};
141+
142+
getGlobalObject().location = dogParkLocation as any;
143+
144+
const tracesSampler = jest.fn();
145+
const hub = new Hub(new BrowserClient({ tracesSampler }));
146+
hub.startTransaction({ name: 'dogpark' });
147+
148+
expect(tracesSampler).toHaveBeenCalledWith(expect.objectContaining({ location: dogParkLocation }));
149+
});
150+
}); // end describe('defaultSampleContext')
151+
152+
describe('while sampling', () => {
35153

36-
describe('spans', () => {
37-
describe('sampling', () => {
38-
test('set tracesSampleRate 0 on transaction', () => {
154+
it('should not sample transactions when tracing is disabled', () => {
155+
// neither tracesSampleRate nor tracesSampler is defined -> tracing disabled
156+
const hub = new Hub(new BrowserClient({}));
157+
const transaction = hub.startTransaction({ name: 'dogpark' });
158+
159+
expect(transaction.sampled).toBe(false);
160+
});
161+
162+
it('should not sample transactions when tracesSampleRate is 0', () => {
39163
const hub = new Hub(new BrowserClient({ tracesSampleRate: 0 }));
40-
const transaction = hub.startTransaction({ name: 'foo' });
164+
const transaction = hub.startTransaction({ name: 'dogpark' });
165+
41166
expect(transaction.sampled).toBe(false);
42167
});
43-
test('set tracesSampleRate 1 on transaction', () => {
168+
169+
it('should sample transactions when tracesSampleRate is 1', () => {
44170
const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
45-
const transaction = hub.startTransaction({ name: 'foo' });
46-
expect(transaction.sampled).toBeTruthy();
47-
});
48-
test('set tracesSampleRate should be propergated to children', () => {
49-
const hub = new Hub(new BrowserClient({ tracesSampleRate: 0 }));
50-
const transaction = hub.startTransaction({ name: 'foo' });
51-
const child = transaction.startChild({ op: 'test' });
52-
expect(child.sampled).toBeFalsy();
171+
const transaction = hub.startTransaction({ name: 'dogpark' });
172+
173+
expect(transaction.sampled).toBe(true);
53174
});
175+
176+
it("should reject tracesSampleRates which aren't numbers", () => {
177+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 'dogs!' as any }));
178+
hub.startTransaction({ name: 'dogpark' });
179+
180+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Sample rate must be a number'));
54181
});
55-
});
56-
});
182+
183+
it('should reject tracesSampleRates less than 0', () => {
184+
const hub = new Hub(new BrowserClient({ tracesSampleRate: -26 }));
185+
hub.startTransaction({ name: 'dogpark' });
186+
187+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Sample rate must be between 0 and 1'));
188+
});
189+
190+
it('should reject tracesSampleRates greater than 1', () => {
191+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 26 }));
192+
hub.startTransaction({ name: 'dogpark' });
193+
194+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Sample rate must be between 0 and 1'));
195+
});
196+
197+
it("should reject tracesSampler return values which aren't numbers", () => {
198+
const tracesSampler = jest.fn().mockReturnValue("dogs!")
199+
const hub = new Hub(new BrowserClient({ tracesSampler }));
200+
hub.startTransaction({ name: 'dogpark' });
201+
202+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Sample rate must be a number'));
203+
});
204+
205+
it('should reject tracesSampler return values less than 0', () => {
206+
const tracesSampler = jest.fn().mockReturnValue(-12)
207+
const hub = new Hub(new BrowserClient({ tracesSampler }));
208+
hub.startTransaction({ name: 'dogpark' });
209+
210+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Sample rate must be between 0 and 1'));
211+
});
212+
213+
it('should reject tracesSampler return values greater than 1', () => {
214+
const tracesSampler = jest.fn().mockReturnValue(31)
215+
const hub = new Hub(new BrowserClient({ tracesSampler }));
216+
hub.startTransaction({ name: 'dogpark' });
217+
218+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Sample rate must be between 0 and 1'));
219+
});
220+
}); // end describe('while sampling')
221+
222+
it('should propagate sampling decision to child spans', () => {
223+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 0 }));
224+
const transaction = hub.startTransaction({ name: 'dogpark' });
225+
const child = transaction.startChild({ op: 'test' });
226+
227+
expect(child.sampled).toBe(false);
228+
});
229+
230+
it('should drop transactions with sampled = false', () => {
231+
const client = new BrowserClient({ tracesSampleRate: 0 })
232+
jest.spyOn(client, 'captureEvent')
233+
234+
const hub = new Hub(client);
235+
const transaction = hub.startTransaction({ name: 'dogpark' });
236+
237+
jest.spyOn(transaction, 'finish')
238+
transaction.finish()
239+
240+
expect(transaction.sampled).toBe(false);
241+
expect(transaction.finish).toReturnWith(undefined);
242+
expect(client.captureEvent).not.toBeCalled()
243+
});
244+
}); // end describe('transaction sampling')
245+
}); // end describe('Hub')

0 commit comments

Comments
 (0)