1 import MetricsApi from '../lib/MetricsApi';
2 import MetricsRequestService from '../lib/MetricsRequestService';
3 import type MetricsRequest from '../lib/types/MetricsRequest';
5 jest.mock('../lib/MetricsApi');
6 const metricsApi = new MetricsApi();
8 const getMetricsRequest = (name: string): MetricsRequest => ({
15 const defaultMetricsRequest = getMetricsRequest('Name');
17 describe('MetricsRequestService', () => {
23 jest.clearAllTimers();
30 it('makes construct correct request', () => {
31 const requestService = new MetricsRequestService(metricsApi, { reportMetrics: true });
33 requestService.report(defaultMetricsRequest);
35 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
36 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
38 body: JSON.stringify({ Metrics: [defaultMetricsRequest] }),
42 describe('report metrics', () => {
43 it('allows api calls if reportMetrics was set to true in constructor', () => {
44 const requestService = new MetricsRequestService(metricsApi, { reportMetrics: true });
46 requestService.report(defaultMetricsRequest);
48 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
51 it('disallows api calls if reportMetrics was set to false in constructor', () => {
52 const requestService = new MetricsRequestService(metricsApi, { reportMetrics: false });
54 requestService.report(defaultMetricsRequest);
56 expect(metricsApi.fetch).not.toHaveBeenCalled();
59 describe('setter', () => {
60 it('allows api calls if reportMetrics is set to true via setter', () => {
61 const requestService = new MetricsRequestService(metricsApi, { reportMetrics: false });
63 requestService.setReportMetrics(true);
64 requestService.report(defaultMetricsRequest);
66 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
69 it('disallows api calls if reportMetrics is set to false via setter', () => {
70 const requestService = new MetricsRequestService(metricsApi, { reportMetrics: true });
72 requestService.setReportMetrics(false);
73 requestService.report(defaultMetricsRequest);
75 expect(metricsApi.fetch).not.toHaveBeenCalled();
80 describe('batching', () => {
81 describe('batch parameters', () => {
82 it('does not batch if frequency is 0', () => {
83 const requestService = new MetricsRequestService(metricsApi, {
91 requestService.report(defaultMetricsRequest);
93 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
96 it('does not batch if frequency is negative', () => {
97 const requestService = new MetricsRequestService(metricsApi, {
105 requestService.report(defaultMetricsRequest);
107 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
110 it('does not batch if size is 0', () => {
111 const requestService = new MetricsRequestService(metricsApi, {
119 requestService.report(defaultMetricsRequest);
121 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
124 it('does not batch if size is negative', () => {
125 const requestService = new MetricsRequestService(metricsApi, {
133 requestService.report(defaultMetricsRequest);
135 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
138 it('batches if frequency and size are positive', () => {
139 const requestService = new MetricsRequestService(metricsApi, {
147 requestService.report(getMetricsRequest('Request 1'));
148 requestService.report(getMetricsRequest('Request 2'));
149 expect(metricsApi.fetch).not.toHaveBeenCalled();
151 jest.advanceTimersByTime(1);
152 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
154 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 1')] }),
157 jest.advanceTimersByTime(1);
158 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
160 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 2')] }),
165 describe('jails', () => {
166 it('defaults jailMax to 3', async () => {
167 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
168 const batchFrequency = 100;
170 const requestService = new MetricsRequestService(metricsApi, {
173 frequency: batchFrequency,
178 requestService.report(defaultMetricsRequest);
179 requestService.report(defaultMetricsRequest);
180 requestService.report(defaultMetricsRequest);
181 requestService.report(defaultMetricsRequest);
183 await jest.advanceTimersToNextTimerAsync();
184 await jest.advanceTimersToNextTimerAsync();
185 await jest.advanceTimersToNextTimerAsync();
186 await jest.advanceTimersToNextTimerAsync();
188 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
191 it('sets jailMax to value passed in constructor', async () => {
192 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
193 const batchFrequency = 1;
195 const requestService = new MetricsRequestService(metricsApi, {
198 frequency: batchFrequency,
204 requestService.report(defaultMetricsRequest);
205 requestService.report(defaultMetricsRequest);
207 requestService.report(defaultMetricsRequest);
208 requestService.report(defaultMetricsRequest);
210 await jest.advanceTimersToNextTimerAsync();
211 await jest.advanceTimersToNextTimerAsync();
213 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
216 it('resets jails if successful call is made and queue is clear', async () => {
217 metricsApi.fetch = jest
219 .mockRejectedValueOnce(undefined)
220 // Resolve second request
221 .mockResolvedValueOnce(undefined)
223 .mockRejectedValueOnce(undefined)
224 .mockRejectedValueOnce(undefined)
225 .mockRejectedValueOnce(undefined);
227 const batchFrequency = 1;
229 const requestService = new MetricsRequestService(metricsApi, {
232 frequency: batchFrequency,
238 requestService.report(defaultMetricsRequest);
239 requestService.report(defaultMetricsRequest);
240 // Will clear queue no items left in queue so jails will reset
241 await jest.advanceTimersToNextTimerAsync();
242 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
244 requestService.report(defaultMetricsRequest);
245 requestService.report(defaultMetricsRequest);
246 await jest.advanceTimersToNextTimerAsync(batchFrequency);
247 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
249 requestService.report(defaultMetricsRequest);
250 requestService.report(defaultMetricsRequest);
251 await jest.advanceTimersToNextTimerAsync(batchFrequency);
252 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
254 requestService.report(defaultMetricsRequest);
255 requestService.report(defaultMetricsRequest);
256 await jest.advanceTimersToNextTimerAsync(batchFrequency);
257 expect(metricsApi.fetch).toHaveBeenCalledTimes(4);
259 requestService.report(defaultMetricsRequest);
260 requestService.report(defaultMetricsRequest);
261 await jest.advanceTimersToNextTimerAsync(batchFrequency);
262 expect(metricsApi.fetch).toHaveBeenCalledTimes(4);
265 it('does not reset jails if there are items still in the queue', async () => {
266 metricsApi.fetch = jest
268 .mockRejectedValueOnce(undefined)
269 // Resolve second request
270 .mockResolvedValueOnce(undefined)
272 .mockRejectedValueOnce(undefined)
273 .mockRejectedValueOnce(undefined);
275 const batchFrequency = 1;
277 const requestService = new MetricsRequestService(metricsApi, {
280 frequency: batchFrequency,
286 requestService.report(defaultMetricsRequest);
287 requestService.report(defaultMetricsRequest);
288 requestService.report(defaultMetricsRequest);
289 requestService.report(defaultMetricsRequest);
290 requestService.report(defaultMetricsRequest);
292 await jest.advanceTimersToNextTimerAsync();
293 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
295 await jest.advanceTimersToNextTimerAsync(batchFrequency);
296 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
298 await jest.advanceTimersToNextTimerAsync(batchFrequency);
299 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
301 await jest.advanceTimersToNextTimerAsync(batchFrequency);
302 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
306 it('delays api calls until frequency timeout has expired', () => {
307 const batchFrequency = 100;
309 const requestService = new MetricsRequestService(metricsApi, {
312 frequency: batchFrequency,
317 requestService.report(defaultMetricsRequest);
318 expect(metricsApi.fetch).not.toHaveBeenCalled();
320 // fast-forward time until 1 millisecond before it should be executed
321 jest.advanceTimersByTime(batchFrequency - 1);
322 expect(metricsApi.fetch).not.toHaveBeenCalled();
324 // fast-forward until 1st call should be executed
325 jest.advanceTimersByTime(1);
326 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
329 it('does not call api if there are no items to process', () => {
330 const batchFrequency = 100;
332 const requestService = new MetricsRequestService(metricsApi, {
335 frequency: batchFrequency,
339 requestService.startBatchingProcess();
341 jest.advanceTimersByTime(batchFrequency);
342 expect(metricsApi.fetch).not.toHaveBeenCalled();
345 it('pauses batching if there are no items to process', async () => {
346 const batchFrequency = 100;
348 const requestService = new MetricsRequestService(metricsApi, {
351 frequency: batchFrequency,
355 requestService.stopBatchingProcess = jest.fn();
357 requestService.report(defaultMetricsRequest);
358 requestService.report(defaultMetricsRequest);
360 await jest.advanceTimersToNextTimerAsync();
361 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
362 expect(requestService.stopBatchingProcess).toHaveBeenCalledTimes(0);
364 await jest.advanceTimersToNextTimerAsync();
365 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
367 // Queue is empty, so we should stop batching
368 await jest.advanceTimersToNextTimerAsync();
369 expect(requestService.stopBatchingProcess).toHaveBeenCalledTimes(1);
372 it('batches calls in order they were reported', () => {
373 const batchFrequency = 100;
375 const requestService = new MetricsRequestService(metricsApi, {
378 frequency: batchFrequency,
383 requestService.report(getMetricsRequest('Request 1'));
384 requestService.report(getMetricsRequest('Request 2'));
385 requestService.report(getMetricsRequest('Request 3'));
386 requestService.report(getMetricsRequest('Request 4'));
387 expect(metricsApi.fetch).not.toHaveBeenCalled();
389 jest.advanceTimersByTime(batchFrequency);
390 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
392 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 1'), getMetricsRequest('Request 2')] }),
395 jest.advanceTimersByTime(batchFrequency);
396 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
398 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 3'), getMetricsRequest('Request 4')] }),
402 it('handles partial batches', () => {
403 const batchFrequency = 100;
405 const requestService = new MetricsRequestService(metricsApi, {
408 frequency: batchFrequency,
413 requestService.report(getMetricsRequest('Request 1'));
414 requestService.report(getMetricsRequest('Request 2'));
415 requestService.report(getMetricsRequest('Request 3'));
416 expect(metricsApi.fetch).not.toHaveBeenCalled();
418 jest.advanceTimersByTime(batchFrequency);
419 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
421 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 1'), getMetricsRequest('Request 2')] }),
424 jest.advanceTimersByTime(batchFrequency);
425 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
427 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 3')] }),
431 describe('progressive backoff', () => {
432 it('progressively backs off by a factor of the jail count if requests fail', async () => {
433 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
434 const batchFrequency = 1;
436 const requestService = new MetricsRequestService(metricsApi, {
439 frequency: batchFrequency,
445 requestService.report(getMetricsRequest('Request 1'));
446 requestService.report(getMetricsRequest('Request 2'));
447 requestService.report(getMetricsRequest('Request 3'));
448 requestService.report(getMetricsRequest('Request 4'));
449 requestService.report(getMetricsRequest('Request 5'));
450 requestService.report(getMetricsRequest('Request 6'));
452 expect(metricsApi.fetch).not.toHaveBeenCalled();
454 await jest.advanceTimersByTimeAsync(batchFrequency);
455 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
456 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
458 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 1'), getMetricsRequest('Request 2')] }),
461 // Requests will have failed, so should now take 2 times the batch frequency
462 await jest.advanceTimersByTimeAsync(batchFrequency);
463 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
465 await jest.advanceTimersByTimeAsync(batchFrequency);
466 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
467 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
469 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 3'), getMetricsRequest('Request 4')] }),
472 // Failed again, so should now take 3 times the batch frequency
473 await jest.advanceTimersByTimeAsync(batchFrequency);
474 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
475 await jest.advanceTimersByTimeAsync(batchFrequency);
476 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
477 await jest.advanceTimersByTimeAsync(batchFrequency);
479 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
480 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
482 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 3'), getMetricsRequest('Request 4')] }),
488 describe('startBatchingProcess', () => {
489 it('starts batching interval on first report call if batch params are defined', () => {
490 const requestService = new MetricsRequestService(metricsApi, {
498 requestService.report(defaultMetricsRequest);
499 expect(metricsApi.fetch).not.toHaveBeenCalled();
501 jest.advanceTimersByTime(1);
502 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
505 it('does not set batching interval if no batch params are set', () => {
506 const requestService = new MetricsRequestService(metricsApi, {
510 requestService.report(defaultMetricsRequest);
511 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
514 * Advance timers to when call should have been
515 * performed if batching was enabled and ensure
516 * no extra calls have been made
518 jest.advanceTimersByTime(1);
519 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
523 describe('stopBatchingProcess', () => {
524 it('stops any further batch requests from occurring until startBatchingProcess is called', () => {
525 const requestService = new MetricsRequestService(metricsApi, {
533 requestService.report(defaultMetricsRequest);
534 requestService.report(defaultMetricsRequest);
535 expect(metricsApi.fetch).not.toHaveBeenCalled();
537 jest.advanceTimersByTime(1);
538 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
540 requestService.stopBatchingProcess();
542 jest.advanceTimersByTime(4);
543 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
545 requestService.startBatchingProcess();
547 jest.advanceTimersByTime(1);
548 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
552 describe('processAllRequests', () => {
553 it('does not make a request if there is no queue', async () => {
554 const batchFrequency = 10;
555 const requestService = new MetricsRequestService(metricsApi, {
558 frequency: batchFrequency,
563 await requestService.processAllRequests();
564 expect(metricsApi.fetch).not.toHaveBeenCalled();
567 it('processes all requests in queue regardless of batch size', async () => {
568 const batchFrequency = 10;
569 const requestService = new MetricsRequestService(metricsApi, {
572 frequency: batchFrequency,
577 requestService.report(getMetricsRequest('Request 1'));
578 requestService.report(getMetricsRequest('Request 2'));
579 requestService.report(getMetricsRequest('Request 3'));
580 expect(metricsApi.fetch).not.toHaveBeenCalled();
582 jest.advanceTimersByTime(batchFrequency);
583 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
585 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 1')] }),
588 jest.advanceTimersByTime(1);
589 await requestService.processAllRequests();
591 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
592 expect(metricsApi.fetch).toHaveBeenCalledWith('/api/data/v1/metrics', {
594 body: JSON.stringify({ Metrics: [getMetricsRequest('Request 2'), getMetricsRequest('Request 3')] }),
598 it('clears queue', async () => {
599 const batchFrequency = 10;
600 const requestService = new MetricsRequestService(metricsApi, {
603 frequency: batchFrequency,
608 requestService.report(getMetricsRequest('Request 1'));
609 requestService.report(getMetricsRequest('Request 2'));
610 requestService.report(getMetricsRequest('Request 3'));
611 expect(metricsApi.fetch).not.toHaveBeenCalled();
613 await requestService.processAllRequests();
614 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
616 jest.advanceTimersByTime(batchFrequency);
617 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
620 describe('jails', () => {
621 it('defaults jailMax to 3', async () => {
622 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
623 const batchFrequency = 100;
625 const requestService = new MetricsRequestService(metricsApi, {
628 frequency: batchFrequency,
633 requestService.report(defaultMetricsRequest);
634 await requestService.processAllRequests();
635 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
637 requestService.report(defaultMetricsRequest);
638 await requestService.processAllRequests();
639 expect(metricsApi.fetch).toHaveBeenCalledTimes(2);
641 requestService.report(defaultMetricsRequest);
642 await requestService.processAllRequests();
643 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
645 requestService.report(defaultMetricsRequest);
646 await requestService.processAllRequests();
647 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
650 it('sets jailMax to value passed in constructor', async () => {
651 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
652 const batchFrequency = 1;
654 const requestService = new MetricsRequestService(metricsApi, {
657 frequency: batchFrequency,
663 requestService.report(defaultMetricsRequest);
664 await requestService.processAllRequests();
665 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
667 requestService.report(defaultMetricsRequest);
668 await requestService.processAllRequests();
669 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
674 describe('clearQueue', () => {
675 it('removes all items from queue', () => {
676 const batchFrequency = 10;
677 const requestService = new MetricsRequestService(metricsApi, {
680 frequency: batchFrequency,
685 requestService.report(getMetricsRequest('Request 1'));
686 requestService.report(getMetricsRequest('Request 2'));
687 requestService.report(getMetricsRequest('Request 3'));
688 expect(metricsApi.fetch).not.toHaveBeenCalled();
690 jest.advanceTimersByTime(batchFrequency);
691 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
693 requestService.clearQueue();
695 jest.advanceTimersByTime(batchFrequency);
696 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
700 describe('jailMax', () => {
701 it('defaults jailMax to 3', async () => {
702 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
704 const requestService = new MetricsRequestService(metricsApi, {
708 requestService.report(getMetricsRequest('Request 1'));
709 await jest.advanceTimersByTimeAsync(1);
710 requestService.report(getMetricsRequest('Request 2'));
711 await jest.advanceTimersByTimeAsync(1);
712 requestService.report(getMetricsRequest('Request 3'));
713 await jest.advanceTimersByTimeAsync(1);
714 requestService.report(getMetricsRequest('Request 4'));
716 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
719 it('sets jailMax to value passed in constructor', async () => {
720 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
722 const requestService = new MetricsRequestService(metricsApi, {
727 requestService.report(getMetricsRequest('Request 1'));
728 await jest.advanceTimersByTimeAsync(1);
729 requestService.report(getMetricsRequest('Request 2'));
730 await jest.advanceTimersByTimeAsync(1);
732 expect(metricsApi.fetch).toHaveBeenCalledTimes(1);
735 it('uses default if jailMax is 0', async () => {
736 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
737 const requestService = new MetricsRequestService(metricsApi, {
742 requestService.report(getMetricsRequest('Request 1'));
743 await jest.advanceTimersByTimeAsync(1);
744 requestService.report(getMetricsRequest('Request 2'));
745 await jest.advanceTimersByTimeAsync(1);
746 requestService.report(getMetricsRequest('Request 3'));
747 await jest.advanceTimersByTimeAsync(1);
748 requestService.report(getMetricsRequest('Request 4'));
750 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
753 it('uses default if jailMax is < 0', async () => {
754 metricsApi.fetch = jest.fn().mockImplementation(() => Promise.reject());
755 const requestService = new MetricsRequestService(metricsApi, {
760 requestService.report(getMetricsRequest('Request 1'));
761 await jest.advanceTimersByTimeAsync(1);
762 requestService.report(getMetricsRequest('Request 2'));
763 await jest.advanceTimersByTimeAsync(1);
764 requestService.report(getMetricsRequest('Request 3'));
765 await jest.advanceTimersByTimeAsync(1);
766 requestService.report(getMetricsRequest('Request 4'));
768 expect(metricsApi.fetch).toHaveBeenCalledTimes(3);
771 it('resets jails if successful call is made', async () => {
772 metricsApi.fetch = jest
774 .mockRejectedValueOnce(undefined)
775 // Resolve second request
776 .mockResolvedValueOnce(undefined)
778 .mockRejectedValueOnce(undefined)
779 .mockRejectedValueOnce(undefined)
780 .mockRejectedValueOnce(undefined);
781 const requestService = new MetricsRequestService(metricsApi, {
786 requestService.report(getMetricsRequest('Request 1'));
787 await jest.advanceTimersByTimeAsync(1);
788 requestService.report(getMetricsRequest('Request 2'));
789 await jest.advanceTimersByTimeAsync(1);
790 requestService.report(getMetricsRequest('Request 3'));
791 await jest.advanceTimersByTimeAsync(1);
792 requestService.report(getMetricsRequest('Request 4'));
793 await jest.advanceTimersByTimeAsync(1);
794 requestService.report(getMetricsRequest('Request 5'));
795 await jest.advanceTimersByTimeAsync(1);
797 expect(metricsApi.fetch).toHaveBeenCalledTimes(4);