Skip to content
This repository was archived by the owner on Sep 2, 2022. It is now read-only.

Commit 2fa6cd8

Browse files
committed
Send request and response body size information with metrics
Just have to rely on Content-Length headers here since there really didn't seem to be any other reliable way of getting request or response body sizes.
1 parent 39bfbc1 commit 2fa6cd8

8 files changed

Lines changed: 155 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Send request and response body size information with metrics.
1213
- Thoroughly document all exported symbols with JSDoc.
1314
- Enable `inlineSources` for source maps.
1415

packages/core/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const myApilyticsMiddleware = async (req, handler) => {
5050
query: req.queryString,
5151
method: req.method,
5252
statusCode: res.statusCode,
53+
requestSize: req.bodyBytes.length,
54+
responseSize: res.bodyBytes.length,
5355
timeMillis: timer(),
5456
});
5557
return res;

packages/core/__tests__/index.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@ describe('sendApilyticsMetrics()', () => {
142142
expect(data).not.toHaveProperty('query');
143143
});
144144

145+
it('should handle empty values correctly', async () => {
146+
sendApilyticsMetrics({
147+
apiKey,
148+
path: '',
149+
method: '',
150+
timeMillis: 0,
151+
query: '',
152+
statusCode: null,
153+
requestSize: undefined,
154+
responseSize: undefined,
155+
apilyticsIntegration: undefined,
156+
integratedLibrary: undefined,
157+
});
158+
159+
expect(requestSpy).toHaveBeenCalledTimes(1);
160+
161+
const data = JSON.parse(clientRequestMock.write.mock.calls[0]);
162+
expect(data).toStrictEqual({
163+
path: '',
164+
method: '',
165+
timeMillis: 0,
166+
});
167+
});
168+
145169
it('should hide HTTP errors in production', async () => {
146170
// @ts-ignore: Assigning to a read-only property.
147171
process.env.NODE_ENV = 'production';

packages/core/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface Params {
99
timeMillis: number;
1010
query?: string;
1111
statusCode?: number | null;
12+
requestSize?: number;
13+
responseSize?: number;
1214
apilyticsIntegration?: string;
1315
integratedLibrary?: string;
1416
}
@@ -29,6 +31,8 @@ interface Params {
2931
* @param params.statusCode - Status code for the sent HTTP response.
3032
* Can be omitted (or null) if the middleware could not get the status code
3133
* for the response. E.g. if the inner request handling threw an exception.
34+
* @param params.requestSize - Size of the user's HTTP request's body in bytes.
35+
* @param params.responseSize - Size of the sent HTTP response's body in bytes.
3236
* @param params.apilyticsIntegration - Name of the Apilytics integration that's
3337
* calling this, e.g. 'apilytics-node-express'.
3438
* No need to pass this when calling from user code.
@@ -46,6 +50,8 @@ interface Params {
4650
* query: req.queryString,
4751
* method: req.method,
4852
* statusCode: res.statusCode,
53+
* requestSize: req.bodyBytes.length,
54+
* responseSize: res.bodyBytes.length,
4955
* timeMillis: timer(),
5056
* });
5157
*/
@@ -56,6 +62,8 @@ export const sendApilyticsMetrics = ({
5662
timeMillis,
5763
query,
5864
statusCode,
65+
requestSize,
66+
responseSize,
5967
apilyticsIntegration,
6068
integratedLibrary,
6169
}: Params): void => {
@@ -64,6 +72,8 @@ export const sendApilyticsMetrics = ({
6472
query: query || undefined,
6573
method,
6674
statusCode: statusCode ?? undefined,
75+
requestSize,
76+
responseSize,
6777
timeMillis,
6878
});
6979
let apilyticsVersion = `${

packages/express/__tests__/index.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,17 @@ describe('apilyticsMiddleware()', () => {
3939
throw new Error();
4040
}
4141

42+
if (req.url?.includes('empty')) {
43+
res.status(200).end();
44+
return;
45+
}
46+
4247
if (req.method === 'POST') {
43-
res.status(201).end();
48+
res.status(201).send('created');
4449
return;
4550
}
4651

47-
res.status(200).end();
52+
res.status(200).send('ok');
4853
};
4954

5055
const createAgent = ({
@@ -94,6 +99,7 @@ describe('apilyticsMiddleware()', () => {
9499
path: '/',
95100
method: 'GET',
96101
statusCode: 200,
102+
responseSize: 2,
97103
timeMillis: expect.any(Number),
98104
});
99105
expect(data['timeMillis']).toEqual(Math.trunc(data['timeMillis']));
@@ -129,10 +135,34 @@ describe('apilyticsMiddleware()', () => {
129135
query: '?param=foo&param2=bar',
130136
method: 'POST',
131137
statusCode: 201,
138+
requestSize: 0,
139+
responseSize: 7,
132140
timeMillis: expect.any(Number),
133141
});
134142
});
135143

144+
it('should handle zero request and response sizes', async () => {
145+
const agent = createAgent({ apiKey });
146+
const response = await agent.post('/empty');
147+
expect(response.status).toEqual(200);
148+
149+
expect(requestSpy).toHaveBeenCalledTimes(1);
150+
const data = JSON.parse(clientRequestMock.write.mock.calls[0]);
151+
expect(data.requestSize).toEqual(0);
152+
expect(data.responseSize).toEqual(0);
153+
});
154+
155+
it('should handle non zero request and response sizes', async () => {
156+
const agent = createAgent({ apiKey });
157+
const response = await agent.post('/dummy').send({ hello: 'world' });
158+
expect(response.status).toEqual(201);
159+
160+
expect(requestSpy).toHaveBeenCalledTimes(1);
161+
const data = JSON.parse(clientRequestMock.write.mock.calls[0]);
162+
expect(data.requestSize).toEqual(17);
163+
expect(data.responseSize).toEqual(7);
164+
});
165+
136166
it('should be disabled if API key is unset', async () => {
137167
const agent = createAgent({ apiKey: undefined });
138168
const response = await agent.get('/');
@@ -154,7 +184,19 @@ describe('apilyticsMiddleware()', () => {
154184
path: '/error',
155185
method: 'GET',
156186
statusCode: 500,
187+
responseSize: expect.any(Number),
157188
timeMillis: expect.any(Number),
158189
});
159190
});
191+
192+
it('should handle undefined content lengths', async () => {
193+
const agent = createAgent({ apiKey });
194+
const numberSpy = jest
195+
.spyOn(global, 'Number')
196+
.mockImplementation(() => NaN);
197+
const response = await agent.get('/empty');
198+
numberSpy.mockRestore();
199+
expect(response.status).toEqual(200);
200+
expect(requestSpy).toHaveBeenCalledTimes(1);
201+
});
160202
});

packages/express/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,25 @@ export const apilyticsMiddleware = (
3636
req.originalUrl,
3737
'http://_', // Cannot parse a relative URL, so make it absolute.
3838
);
39+
40+
const _requestSize = Number(req.headers['content-length']);
41+
const requestSize = isNaN(_requestSize) ? undefined : _requestSize;
42+
43+
const _responseSize = Number(
44+
// @ts-ignore: `_contentLength` is not typed, but it does exist sometimes
45+
// when the header doesn't. Even if it doesn't this won't fail at runtime.
46+
res.getHeader('content-length') ?? res._contentLength,
47+
);
48+
const responseSize = isNaN(_responseSize) ? undefined : _responseSize;
49+
3950
sendApilyticsMetrics({
4051
apiKey,
4152
path,
4253
query,
4354
method: req.method,
4455
statusCode: res.statusCode,
56+
requestSize,
57+
responseSize,
4558
timeMillis: timer(),
4659
apilyticsIntegration: 'apilytics-node-express',
4760
integratedLibrary: `express/${EXPRESS_VERSION}`,

packages/next/__tests__/index.test.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,23 @@ describe('withApilytics()', () => {
4040
}
4141

4242
if (req.url?.includes('empty')) {
43+
res.status(200).end();
44+
return;
45+
}
46+
47+
if (req.url?.includes('no-url-or-method')) {
4348
req.url = undefined;
4449
req.method = undefined;
45-
res.end();
50+
res.status(200).end();
4651
return;
4752
}
4853

4954
if (req.method === 'POST') {
50-
res.status(201).end();
55+
res.status(201).send('created');
5156
return;
5257
}
5358

54-
res.status(200).end();
59+
res.status(200).send('ok');
5560
};
5661

5762
const createAgent = ({
@@ -114,6 +119,7 @@ describe('withApilytics()', () => {
114119
path: '/',
115120
method: 'GET',
116121
statusCode: 200,
122+
responseSize: 2,
117123
timeMillis: expect.any(Number),
118124
});
119125
expect(data['timeMillis']).toEqual(Math.trunc(data['timeMillis']));
@@ -149,10 +155,34 @@ describe('withApilytics()', () => {
149155
query: '?param=foo&param2=bar',
150156
method: 'POST',
151157
statusCode: 201,
158+
requestSize: 0,
159+
responseSize: 7,
152160
timeMillis: expect.any(Number),
153161
});
154162
});
155163

164+
it('should handle zero request and response sizes', async () => {
165+
const agent = createAgent({ apiKey });
166+
const response = await agent.post('/empty');
167+
expect(response.status).toEqual(200);
168+
169+
expect(requestSpy).toHaveBeenCalledTimes(1);
170+
const data = JSON.parse(clientRequestMock.write.mock.calls[0]);
171+
expect(data.requestSize).toEqual(0);
172+
expect(data.responseSize).toEqual(0);
173+
});
174+
175+
it('should handle non zero request and response sizes', async () => {
176+
const agent = createAgent({ apiKey });
177+
const response = await agent.post('/dummy').send({ hello: 'world' });
178+
expect(response.status).toEqual(201);
179+
180+
expect(requestSpy).toHaveBeenCalledTimes(1);
181+
const data = JSON.parse(clientRequestMock.write.mock.calls[0]);
182+
expect(data.requestSize).toEqual(17);
183+
expect(data.responseSize).toEqual(7);
184+
});
185+
156186
it('should be disabled if API key is unset', async () => {
157187
const agent = createAgent({ apiKey: undefined });
158188
const response = await agent.get('/');
@@ -174,21 +204,34 @@ describe('withApilytics()', () => {
174204
expect(data).toStrictEqual({
175205
path: '/error',
176206
method: 'GET',
207+
responseSize: 0,
177208
timeMillis: expect.any(Number),
178209
});
179210
});
180211

181212
it('should use correct default values', async () => {
182213
const agent = createAgent({ apiKey });
183-
const response = await agent.get('/empty');
214+
const response = await agent.get('/no-url-or-method');
184215
expect(response.status).toEqual(200);
185216

186217
const data = JSON.parse(clientRequestMock.write.mock.calls[0]);
187218
expect(data).toStrictEqual({
188219
path: '',
189220
method: '',
190221
statusCode: 200,
222+
responseSize: 0,
191223
timeMillis: expect.any(Number),
192224
});
193225
});
226+
227+
it('should handle undefined content lengths', async () => {
228+
const agent = createAgent({ apiKey });
229+
const numberSpy = jest
230+
.spyOn(global, 'Number')
231+
.mockImplementation(() => NaN);
232+
const response = await agent.get('/empty');
233+
numberSpy.mockRestore();
234+
expect(response.status).toEqual(200);
235+
expect(requestSpy).toHaveBeenCalledTimes(1);
236+
});
194237
});

packages/next/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const withApilytics = <T>(
3535
res: NextApiResponse<T>,
3636
): Promise<void> => {
3737
let statusCode: number | undefined;
38+
let responseSize: number | undefined;
3839
const timer = milliSecondTimer();
3940

4041
try {
@@ -50,12 +51,25 @@ export const withApilytics = <T>(
5051
'http://_', // Cannot parse a relative URL, so make it absolute.
5152
));
5253
}
54+
55+
const _requestSize = Number(req.headers['content-length']);
56+
const requestSize = isNaN(_requestSize) ? undefined : _requestSize;
57+
58+
const _responseSize = Number(
59+
// @ts-ignore: `_contentLength` is not typed, but it does exist sometimes
60+
// when the header doesn't. Even if it doesn't this won't fail at runtime.
61+
res.getHeader('content-length') ?? res._contentLength,
62+
);
63+
responseSize = isNaN(_responseSize) ? undefined : _responseSize;
64+
5365
sendApilyticsMetrics({
5466
apiKey,
5567
path: path ?? '',
5668
query,
5769
method: req.method ?? '',
5870
statusCode,
71+
requestSize,
72+
responseSize,
5973
timeMillis: timer(),
6074
apilyticsIntegration: 'apilytics-node-next',
6175
integratedLibrary: `next/${NEXT_VERSION}`,

0 commit comments

Comments
 (0)