Skip to content

Commit

Permalink
feat: 'Resume' needs to apply missed logs (#89)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jwallet authored Sep 24, 2024
1 parent e423079 commit 0ecd3e8
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 45 deletions.
63 changes: 44 additions & 19 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import NetworkLogger, {
startNetworkLogging,
stopNetworkLogging,
} from 'react-native-network-logger';
import { getRates } from './apolloClient';
import { getHero, getRates, getUser } from './apolloClient';

export default function App() {
const formData = new FormData();
formData.append('test', 'hello');
const makeRequest = () => {
const formData = new FormData();
formData.append('test', 'hello');

const requests = [
async () =>
fetch(
`https://postman-echo.com/post?query=${'some really long query that goes onto multiple lines so we can test what happens'.repeat(
5
Expand All @@ -29,31 +30,55 @@ export default function App() {
method: 'POST',
body: JSON.stringify({ test: 'hello' }),
}
);
),
async () =>
fetch('https://postman-echo.com/post?formData', {
method: 'POST',
body: formData,
});
fetch('https://httpstat.us/200', { method: 'HEAD' });
}),
async () => fetch('https://httpstat.us/200', { method: 'HEAD' }),
async () =>
fetch('https://postman-echo.com/put', {
method: 'PUT',
body: JSON.stringify({ test: 'hello' }),
});
fetch('https://httpstat.us/302');
fetch('https://httpstat.us/400');
fetch('https://httpstat.us/500');
// Non JSON response
fetch('https://postman-echo.com/stream/2');

getRates();
// Test requests that fail
// fetch('https://failingrequest');
}),
async () => fetch('https://httpstat.us/200?sleep=300'),
async () => fetch('https://httpstat.us/204?sleep=200'),
async () => fetch('https://httpstat.us/302?sleep=200'),
async () => fetch('https://httpstat.us/400?sleep=200'),
async () => fetch('https://httpstat.us/401?sleep=200'),
async () => fetch('https://httpstat.us/403?sleep=200'),
async () => fetch('https://httpstat.us/404?sleep=400'),
async () => fetch('https://httpstat.us/500?sleep=5000'),
async () => fetch('https://httpstat.us/503?sleep=200'),
async () => fetch('https://httpstat.us/504?sleep=10000'),

// Non JSON response
async () => fetch('https://postman-echo.com/stream/2'),

async () => getRates(), // 405
async () => getHero(), // 400
async () => getUser(), // 200
// Test requests that fail
// async () => fetch('https://failingrequest'),
];

export default function App() {
const maxRequests = 500;

// randomly make requests
const makeRequest = async () => {
Promise.all(
Array.from({ length: Math.min(maxRequests, 10) }).map((_) =>
requests[Math.floor(Math.random() * requests.length)]()
)
);
};

const start = useCallback(() => {
startNetworkLogging({
ignoredHosts: ['127.0.0.1'],
maxRequests: 20,
maxRequests,
ignoredUrls: ['https://httpstat.us/other'],
ignoredPatterns: [/^POST http:\/\/(192|10)/, /\/logs$/, /\/symbolicate$/],
});
Expand Down
38 changes: 36 additions & 2 deletions example/src/apolloClient.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
const sandboxClient = new ApolloClient({
uri: 'https://48p1r2roz4.sse.codesandbox.io',
cache: new InMemoryCache(),
});

export const getRates = async () => {
return client
return sandboxClient
.query({
query: gql`
query GetRates {
Expand All @@ -19,3 +19,37 @@ export const getRates = async () => {
})
.catch((e: any) => console.log(e.message));
};

const gqlZeroClient = new ApolloClient({
uri: 'https://graphqlzero.almansi.me/api',
cache: new InMemoryCache(),
});

export const getUser = async () => {
return gqlZeroClient
.query({
query: gql`
query getUser {
user(id: 1) {
id
name
}
}
`,
})
.catch((e: any) => console.log(e.message));
};

export const getHero = async () => {
return gqlZeroClient
.query({
query: gql`
query getHero {
hero(class: "Human") {
health
}
}
`,
})
.catch((e: any) => console.log(e.message));
};
46 changes: 35 additions & 11 deletions src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ type XHR = {

export default class Logger {
private requests: NetworkRequestInfo[] = [];
private pausedRequests: NetworkRequestInfo[] = [];
private xhrIdMap: Map<number, () => number> = new Map();
private maxRequests: number = LOGGER_MAX_REQUESTS;
private refreshRate: number = LOGGER_REFRESH_RATE;
private latestRequestUpdatedAt: number = 0;
private ignoredHosts: Set<string> | undefined;
private ignoredUrls: Set<string> | undefined;
private ignoredPatterns: RegExp[] | undefined;
private paused = false;
public enabled = false;
public paused = false;

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

isPaused = this.paused;

setCallback = (callback: any) => {
this.callback = callback;
};
Expand All @@ -46,7 +49,7 @@ export default class Logger {
if (xhrIndex === undefined) return undefined;
if (!this.xhrIdMap.has(xhrIndex)) return undefined;
const index = this.xhrIdMap.get(xhrIndex)!();
return this.requests[index];
return (this.paused ? this.pausedRequests : this.requests)[index];
};

private updateRequest = (
Expand All @@ -59,10 +62,6 @@ export default class Logger {
};

private openCallback = (method: RequestMethod, url: string, xhr: XHR) => {
if (this.paused) {
return;
}

if (this.ignoredHosts) {
const host = extractHost(url);
if (host && this.ignoredHosts.has(host)) {
Expand All @@ -84,7 +83,9 @@ export default class Logger {

xhr._index = nextXHRId++;
this.xhrIdMap.set(xhr._index, () => {
return this.requests.findIndex((r) => r.id === `${xhr._index}`);
return (this.paused ? this.pausedRequests : this.requests).findIndex(
(r) => r.id === `${xhr._index}`
);
});

const newRequest = new NetworkRequestInfo(
Expand All @@ -94,11 +95,19 @@ export default class Logger {
url
);

if (this.requests.length >= this.maxRequests) {
this.requests.pop();
if (this.paused) {
const logsLength = this.pausedRequests.length + this.requests.length;
if (logsLength > this.maxRequests) {
if (this.requests.length > 0) this.requests.pop();
else this.pausedRequests.pop();
}
this.pausedRequests.push(newRequest);
} else {
this.requests.unshift(newRequest);
if (this.requests.length > this.maxRequests) {
this.requests.pop();
}
}

this.requests.unshift(newRequest);
};

private requestHeadersCallback = (
Expand Down Expand Up @@ -230,10 +239,25 @@ export default class Logger {

clearRequests = () => {
this.requests = [];
this.pausedRequests = [];
this.latestRequestUpdatedAt = 0;
this.debouncedCallback();
};

onPausedChange = (paused: boolean) => {
if (!paused) {
this.pausedRequests.forEach((request) => {
this.requests.unshift(request);
if (this.requests.length > this.maxRequests) {
this.requests.pop();
}
});
this.pausedRequests = [];
this.debouncedCallback();
}
this.paused = paused;
};

disableXHRInterception = () => {
if (!this.enabled) return;

Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/Logger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,46 @@ describe('openCallback', () => {

logger.disableXHRInterception();
});

it('should retrieve missed requests when it is restricted by maxRequests after resuming a paused network logging', () => {
const logger = new Logger();
logger.enableXHRInterception({
maxRequests: 2,
});

const url = 'http://example.com/1';

// @ts-expect-error
logger.openCallback('POST', url, { _index: 0 });

// @ts-expect-error
logger.paused = true;

// @ts-expect-error
logger.openCallback('GET', url, { _index: 1 });

expect(logger.getRequests()[0].method).toEqual('POST');
expect(logger.getRequests()).toHaveLength(1);

// @ts-expect-error
logger.paused = false;

// @ts-expect-error
logger.openCallback('HEAD', url, { _index: 2 });
// @ts-expect-error
logger.openCallback('PUT', url, { _index: 3 });

// Requests should be in reverse order
expect(logger.getRequests()[0].method).toEqual('PUT');
expect(logger.getRequests()[1].method).toEqual('HEAD');
expect(logger.getRequests()).toHaveLength(2);

// @ts-expect-error
expect(logger.getRequest(0)?.method).toBeUndefined();
const first = logger.getRequests()[0];
// @ts-expect-error
expect(logger.getRequest(3)?.method).toBe(first?.method);

logger.disableXHRInterception();
});
});
2 changes: 1 addition & 1 deletion src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('singleton logger', () => {
expect(XHRInterceptor.setResponseCallback).toHaveBeenCalledTimes(2);

expect(logger.enabled).toBe(false);
expect(logger.paused).toBe(false);
expect(logger.isPaused).toBe(false);
// @ts-ignore
expect(logger.requests).toEqual([]);
// @ts-ignore
Expand Down
36 changes: 31 additions & 5 deletions src/components/NetworkLogger.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import NetworkLogger, { NetworkLoggerProps } from './NetworkLogger';
import logger from '../loggerSingleton';
import NetworkRequestInfo from '../NetworkRequestInfo';

jest.mock('../loggerSingleton', () => ({
isPaused: false,
enabled: true,
setCallback: jest.fn(),
clearRequests: jest.fn(),
onPausedChange: jest.fn(),
getRequests: jest.fn().mockReturnValue([]),
enableXHRInterception: jest.fn(),
disableXHRInterception: jest.fn(),
}));
jest.mock('react-native/Libraries/Blob/FileReader', () => ({}));
jest.mock('react-native/Libraries/Network/XHRInterceptor', () => ({
isInterceptorEnabled: jest.fn(),
Expand Down Expand Up @@ -69,32 +79,48 @@ describe('max rows', () => {
});

describe('options', () => {
it('should toggle the display of the paused banner when paused', () => {
const { getByText, queryByText, getByTestId } = render(<MyNetworkLogger />);
it('should toggle the display of the paused banner when paused', async () => {
const spyOnLoggerPauseRequests = jest.spyOn(logger, 'onPausedChange');
const { getByText, queryByText, getByTestId, unmount } = render(
<MyNetworkLogger />
);

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

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

expect(spyOnLoggerPauseRequests).toHaveBeenCalledTimes(1);

fireEvent.press(getByTestId('options-menu'));
fireEvent.press(getByText(/^resume$/i));

expect(queryByText(/^paused$/i)).toBeFalsy();

spyOnLoggerPauseRequests.mockRestore();

unmount();
});

it('should clear the logs on demand', () => {
const spyOnLoggerClearRequests = jest.spyOn(logger, 'clearRequests');
it('should clear the logs on demand', async () => {
const spyOnLoggerClearRequests = jest
.spyOn(logger, 'clearRequests')
.mockImplementationOnce(() => null);

const { getByText, getByTestId } = render(<MyNetworkLogger />);
const { getByText, queryByText, getByTestId, unmount } = render(
<MyNetworkLogger />
);
expect(spyOnLoggerClearRequests).toHaveBeenCalledTimes(0);

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

expect(spyOnLoggerClearRequests).toHaveBeenCalledTimes(1);

spyOnLoggerClearRequests.mockRestore();

unmount();
});
});

Expand Down
Loading

0 comments on commit 0ecd3e8

Please sign in to comment.