Skip to content

Commit 0ecd3e8

Browse files
authored
feat: 'Resume' needs to apply missed logs (#89)
* maxRequests * resume apply missed logs * test * dispose to fix test * lint * random requests in example * added gql requests * lint * fixed merge * 89 prevent going over maxrequests
1 parent e423079 commit 0ecd3e8

File tree

10 files changed

+198
-45
lines changed

10 files changed

+198
-45
lines changed

example/src/App.tsx

+44-19
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import NetworkLogger, {
1515
startNetworkLogging,
1616
stopNetworkLogging,
1717
} from 'react-native-network-logger';
18-
import { getRates } from './apolloClient';
18+
import { getHero, getRates, getUser } from './apolloClient';
1919

20-
export default function App() {
21-
const formData = new FormData();
22-
formData.append('test', 'hello');
23-
const makeRequest = () => {
20+
const formData = new FormData();
21+
formData.append('test', 'hello');
22+
23+
const requests = [
24+
async () =>
2425
fetch(
2526
`https://postman-echo.com/post?query=${'some really long query that goes onto multiple lines so we can test what happens'.repeat(
2627
5
@@ -29,31 +30,55 @@ export default function App() {
2930
method: 'POST',
3031
body: JSON.stringify({ test: 'hello' }),
3132
}
32-
);
33+
),
34+
async () =>
3335
fetch('https://postman-echo.com/post?formData', {
3436
method: 'POST',
3537
body: formData,
36-
});
37-
fetch('https://httpstat.us/200', { method: 'HEAD' });
38+
}),
39+
async () => fetch('https://httpstat.us/200', { method: 'HEAD' }),
40+
async () =>
3841
fetch('https://postman-echo.com/put', {
3942
method: 'PUT',
4043
body: JSON.stringify({ test: 'hello' }),
41-
});
42-
fetch('https://httpstat.us/302');
43-
fetch('https://httpstat.us/400');
44-
fetch('https://httpstat.us/500');
45-
// Non JSON response
46-
fetch('https://postman-echo.com/stream/2');
47-
48-
getRates();
49-
// Test requests that fail
50-
// fetch('https://failingrequest');
44+
}),
45+
async () => fetch('https://httpstat.us/200?sleep=300'),
46+
async () => fetch('https://httpstat.us/204?sleep=200'),
47+
async () => fetch('https://httpstat.us/302?sleep=200'),
48+
async () => fetch('https://httpstat.us/400?sleep=200'),
49+
async () => fetch('https://httpstat.us/401?sleep=200'),
50+
async () => fetch('https://httpstat.us/403?sleep=200'),
51+
async () => fetch('https://httpstat.us/404?sleep=400'),
52+
async () => fetch('https://httpstat.us/500?sleep=5000'),
53+
async () => fetch('https://httpstat.us/503?sleep=200'),
54+
async () => fetch('https://httpstat.us/504?sleep=10000'),
55+
56+
// Non JSON response
57+
async () => fetch('https://postman-echo.com/stream/2'),
58+
59+
async () => getRates(), // 405
60+
async () => getHero(), // 400
61+
async () => getUser(), // 200
62+
// Test requests that fail
63+
// async () => fetch('https://failingrequest'),
64+
];
65+
66+
export default function App() {
67+
const maxRequests = 500;
68+
69+
// randomly make requests
70+
const makeRequest = async () => {
71+
Promise.all(
72+
Array.from({ length: Math.min(maxRequests, 10) }).map((_) =>
73+
requests[Math.floor(Math.random() * requests.length)]()
74+
)
75+
);
5176
};
5277

5378
const start = useCallback(() => {
5479
startNetworkLogging({
5580
ignoredHosts: ['127.0.0.1'],
56-
maxRequests: 20,
81+
maxRequests,
5782
ignoredUrls: ['https://httpstat.us/other'],
5883
ignoredPatterns: [/^POST http:\/\/(192|10)/, /\/logs$/, /\/symbolicate$/],
5984
});

example/src/apolloClient.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
22

3-
const client = new ApolloClient({
3+
const sandboxClient = new ApolloClient({
44
uri: 'https://48p1r2roz4.sse.codesandbox.io',
55
cache: new InMemoryCache(),
66
});
77

88
export const getRates = async () => {
9-
return client
9+
return sandboxClient
1010
.query({
1111
query: gql`
1212
query GetRates {
@@ -19,3 +19,37 @@ export const getRates = async () => {
1919
})
2020
.catch((e: any) => console.log(e.message));
2121
};
22+
23+
const gqlZeroClient = new ApolloClient({
24+
uri: 'https://graphqlzero.almansi.me/api',
25+
cache: new InMemoryCache(),
26+
});
27+
28+
export const getUser = async () => {
29+
return gqlZeroClient
30+
.query({
31+
query: gql`
32+
query getUser {
33+
user(id: 1) {
34+
id
35+
name
36+
}
37+
}
38+
`,
39+
})
40+
.catch((e: any) => console.log(e.message));
41+
};
42+
43+
export const getHero = async () => {
44+
return gqlZeroClient
45+
.query({
46+
query: gql`
47+
query getHero {
48+
hero(class: "Human") {
49+
health
50+
}
51+
}
52+
`,
53+
})
54+
.catch((e: any) => console.log(e.message));
55+
};

src/Logger.ts

+35-11
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ type XHR = {
1515

1616
export default class Logger {
1717
private requests: NetworkRequestInfo[] = [];
18+
private pausedRequests: NetworkRequestInfo[] = [];
1819
private xhrIdMap: Map<number, () => number> = new Map();
1920
private maxRequests: number = LOGGER_MAX_REQUESTS;
2021
private refreshRate: number = LOGGER_REFRESH_RATE;
2122
private latestRequestUpdatedAt: number = 0;
2223
private ignoredHosts: Set<string> | undefined;
2324
private ignoredUrls: Set<string> | undefined;
2425
private ignoredPatterns: RegExp[] | undefined;
26+
private paused = false;
2527
public enabled = false;
26-
public paused = false;
2728

2829
callback = (_: NetworkRequestInfo[]) => null;
2930

31+
isPaused = this.paused;
32+
3033
setCallback = (callback: any) => {
3134
this.callback = callback;
3235
};
@@ -46,7 +49,7 @@ export default class Logger {
4649
if (xhrIndex === undefined) return undefined;
4750
if (!this.xhrIdMap.has(xhrIndex)) return undefined;
4851
const index = this.xhrIdMap.get(xhrIndex)!();
49-
return this.requests[index];
52+
return (this.paused ? this.pausedRequests : this.requests)[index];
5053
};
5154

5255
private updateRequest = (
@@ -59,10 +62,6 @@ export default class Logger {
5962
};
6063

6164
private openCallback = (method: RequestMethod, url: string, xhr: XHR) => {
62-
if (this.paused) {
63-
return;
64-
}
65-
6665
if (this.ignoredHosts) {
6766
const host = extractHost(url);
6867
if (host && this.ignoredHosts.has(host)) {
@@ -84,7 +83,9 @@ export default class Logger {
8483

8584
xhr._index = nextXHRId++;
8685
this.xhrIdMap.set(xhr._index, () => {
87-
return this.requests.findIndex((r) => r.id === `${xhr._index}`);
86+
return (this.paused ? this.pausedRequests : this.requests).findIndex(
87+
(r) => r.id === `${xhr._index}`
88+
);
8889
});
8990

9091
const newRequest = new NetworkRequestInfo(
@@ -94,11 +95,19 @@ export default class Logger {
9495
url
9596
);
9697

97-
if (this.requests.length >= this.maxRequests) {
98-
this.requests.pop();
98+
if (this.paused) {
99+
const logsLength = this.pausedRequests.length + this.requests.length;
100+
if (logsLength > this.maxRequests) {
101+
if (this.requests.length > 0) this.requests.pop();
102+
else this.pausedRequests.pop();
103+
}
104+
this.pausedRequests.push(newRequest);
105+
} else {
106+
this.requests.unshift(newRequest);
107+
if (this.requests.length > this.maxRequests) {
108+
this.requests.pop();
109+
}
99110
}
100-
101-
this.requests.unshift(newRequest);
102111
};
103112

104113
private requestHeadersCallback = (
@@ -230,10 +239,25 @@ export default class Logger {
230239

231240
clearRequests = () => {
232241
this.requests = [];
242+
this.pausedRequests = [];
233243
this.latestRequestUpdatedAt = 0;
234244
this.debouncedCallback();
235245
};
236246

247+
onPausedChange = (paused: boolean) => {
248+
if (!paused) {
249+
this.pausedRequests.forEach((request) => {
250+
this.requests.unshift(request);
251+
if (this.requests.length > this.maxRequests) {
252+
this.requests.pop();
253+
}
254+
});
255+
this.pausedRequests = [];
256+
this.debouncedCallback();
257+
}
258+
this.paused = paused;
259+
};
260+
237261
disableXHRInterception = () => {
238262
if (!this.enabled) return;
239263

src/__tests__/Logger.spec.ts

+42
Original file line numberDiff line numberDiff line change
@@ -415,4 +415,46 @@ describe('openCallback', () => {
415415

416416
logger.disableXHRInterception();
417417
});
418+
419+
it('should retrieve missed requests when it is restricted by maxRequests after resuming a paused network logging', () => {
420+
const logger = new Logger();
421+
logger.enableXHRInterception({
422+
maxRequests: 2,
423+
});
424+
425+
const url = 'http://example.com/1';
426+
427+
// @ts-expect-error
428+
logger.openCallback('POST', url, { _index: 0 });
429+
430+
// @ts-expect-error
431+
logger.paused = true;
432+
433+
// @ts-expect-error
434+
logger.openCallback('GET', url, { _index: 1 });
435+
436+
expect(logger.getRequests()[0].method).toEqual('POST');
437+
expect(logger.getRequests()).toHaveLength(1);
438+
439+
// @ts-expect-error
440+
logger.paused = false;
441+
442+
// @ts-expect-error
443+
logger.openCallback('HEAD', url, { _index: 2 });
444+
// @ts-expect-error
445+
logger.openCallback('PUT', url, { _index: 3 });
446+
447+
// Requests should be in reverse order
448+
expect(logger.getRequests()[0].method).toEqual('PUT');
449+
expect(logger.getRequests()[1].method).toEqual('HEAD');
450+
expect(logger.getRequests()).toHaveLength(2);
451+
452+
// @ts-expect-error
453+
expect(logger.getRequest(0)?.method).toBeUndefined();
454+
const first = logger.getRequests()[0];
455+
// @ts-expect-error
456+
expect(logger.getRequest(3)?.method).toBe(first?.method);
457+
458+
logger.disableXHRInterception();
459+
});
418460
});

src/__tests__/index.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ describe('singleton logger', () => {
107107
expect(XHRInterceptor.setResponseCallback).toHaveBeenCalledTimes(2);
108108

109109
expect(logger.enabled).toBe(false);
110-
expect(logger.paused).toBe(false);
110+
expect(logger.isPaused).toBe(false);
111111
// @ts-ignore
112112
expect(logger.requests).toEqual([]);
113113
// @ts-ignore

src/components/NetworkLogger.spec.tsx

+31-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import NetworkLogger, { NetworkLoggerProps } from './NetworkLogger';
55
import logger from '../loggerSingleton';
66
import NetworkRequestInfo from '../NetworkRequestInfo';
77

8+
jest.mock('../loggerSingleton', () => ({
9+
isPaused: false,
10+
enabled: true,
11+
setCallback: jest.fn(),
12+
clearRequests: jest.fn(),
13+
onPausedChange: jest.fn(),
14+
getRequests: jest.fn().mockReturnValue([]),
15+
enableXHRInterception: jest.fn(),
16+
disableXHRInterception: jest.fn(),
17+
}));
818
jest.mock('react-native/Libraries/Blob/FileReader', () => ({}));
919
jest.mock('react-native/Libraries/Network/XHRInterceptor', () => ({
1020
isInterceptorEnabled: jest.fn(),
@@ -69,32 +79,48 @@ describe('max rows', () => {
6979
});
7080

7181
describe('options', () => {
72-
it('should toggle the display of the paused banner when paused', () => {
73-
const { getByText, queryByText, getByTestId } = render(<MyNetworkLogger />);
82+
it('should toggle the display of the paused banner when paused', async () => {
83+
const spyOnLoggerPauseRequests = jest.spyOn(logger, 'onPausedChange');
84+
const { getByText, queryByText, getByTestId, unmount } = render(
85+
<MyNetworkLogger />
86+
);
7487

7588
fireEvent.press(getByTestId('options-menu'));
7689
fireEvent.press(getByText(/^pause$/i));
7790

7891
expect(queryByText(/^paused$/i)).toBeTruthy();
7992

93+
expect(spyOnLoggerPauseRequests).toHaveBeenCalledTimes(1);
94+
8095
fireEvent.press(getByTestId('options-menu'));
8196
fireEvent.press(getByText(/^resume$/i));
8297

8398
expect(queryByText(/^paused$/i)).toBeFalsy();
99+
100+
spyOnLoggerPauseRequests.mockRestore();
101+
102+
unmount();
84103
});
85104

86-
it('should clear the logs on demand', () => {
87-
const spyOnLoggerClearRequests = jest.spyOn(logger, 'clearRequests');
105+
it('should clear the logs on demand', async () => {
106+
const spyOnLoggerClearRequests = jest
107+
.spyOn(logger, 'clearRequests')
108+
.mockImplementationOnce(() => null);
88109

89-
const { getByText, getByTestId } = render(<MyNetworkLogger />);
110+
const { getByText, queryByText, getByTestId, unmount } = render(
111+
<MyNetworkLogger />
112+
);
90113
expect(spyOnLoggerClearRequests).toHaveBeenCalledTimes(0);
91114

92115
fireEvent.press(getByTestId('options-menu'));
116+
expect(queryByText(/^options$/i)).toBeDefined();
93117
fireEvent.press(getByText(/^clear/i));
94118

95119
expect(spyOnLoggerClearRequests).toHaveBeenCalledTimes(1);
96120

97121
spyOnLoggerClearRequests.mockRestore();
122+
123+
unmount();
98124
});
99125
});
100126

0 commit comments

Comments
 (0)