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

Commit e1d819c

Browse files
authored
Merge pull request #14 from apilytics/request-response-sizes
Send request and response body size information with metrics
2 parents 71cec49 + b836fa8 commit e1d819c

11 files changed

Lines changed: 182 additions & 42 deletions

File tree

.github/workflows/cd.yml

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,43 @@ jobs:
3232
- name: "Install dependencies"
3333
run: yarn install --frozen-lockfile
3434

35-
- name: "Build the sources"
36-
run: yarn build
37-
38-
- name: "Publish to npm"
39-
id: publish
35+
- name: "Bump package versions"
36+
id: bump
4037
run: |
41-
echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}' > .npmrc
42-
yarn release ${{ github.event.inputs.release-type }}
38+
yarn bump ${{ github.event.inputs.release-type }}
4339
echo "::set-output name=version::$(node --print 'require("./lerna.json").version')"
4440
4541
- name: "Update the changelog"
4642
# Find the first line that starts with `###` or `## [<number>` from the CHANGELOG and insert the new version header before it.
47-
run: |
48-
current_date="$(date -u '+%Y-%m-%d')"
49-
sed -i "0,/^\(###\|## *\[[0-9]\).*/{s//## [${{ steps.publish.outputs.version }}] - ${current_date}\n\n&/}" CHANGELOG.md
43+
run: sed -i "0,/^\(###\|## *\[[0-9]\).*/{s//## [${{ steps.bump.outputs.version }}] - $(date -u '+%Y-%m-%d')\n\n&/}" CHANGELOG.md
5044

5145
- name: "Extract version's changelog for release notes"
5246
# 1. Find the lines between the first `## [<number>` and the second `## [<number>`.
5347
# 2. Remove all leading and trailing newlines from the output.
5448
run: sed '1,/^## *\[[0-9]/d;/^## *\[[0-9]/Q' CHANGELOG.md | sed -e :a -e '/./,$!d;/^\n*$/{$d;N;};/\n$/ba' > release_notes.txt
5549

5650
- name: "Commit and tag the changes"
57-
uses: EndBug/add-and-commit@8c12ff729a98cfbcd3fe38b49f55eceb98a5ec02 # v7.5.0
58-
with:
59-
add: '["lerna.json", "*package.json", "CHANGELOG.md"]'
60-
message: 'Release ${{ steps.publish.outputs.version }}'
61-
tag: 'v${{ steps.publish.outputs.version }} --annotate --file /dev/null'
62-
default_author: github_actions
63-
pathspec_error_handling: exitImmediately
51+
run: |
52+
git config user.name 'github-actions'
53+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
54+
git add lerna.json \*package.json CHANGELOG.md
55+
git commit --message='Release ${{ steps.bump.outputs.version }}'
56+
git tag --annotate --message='' v${{ steps.bump.outputs.version }}
57+
58+
- name: "Build the sources"
59+
run: yarn build
60+
61+
- name: "Publish to npm"
62+
run: |
63+
echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}' > .npmrc
64+
yarn release
65+
66+
- name: "Push the changes"
67+
run: git push --follow-tags
6468

6569
- name: "Create a GitHub release"
6670
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v1
6771
with:
68-
tag_name: v${{ steps.publish.outputs.version }}
69-
name: v${{ steps.publish.outputs.version }}
72+
tag_name: v${{ steps.bump.outputs.version }}
73+
name: v${{ steps.bump.outputs.version }}
7074
body_path: release_notes.txt

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

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"scripts": {
77
"build": "lerna run build",
88
"clean": "lerna run clean",
9-
"clean:git-head": "lerna exec node \\$LERNA_ROOT_PATH/scripts/clean-git-head.js",
10-
"release": "lerna publish --force-publish='*' --exact --no-git-tag-version --no-git-reset --no-verify-access --yes && yarn clean:git-head",
9+
"bump": "lerna version --force-publish='*' --exact --no-git-tag-version --yes",
10+
"release": "lerna publish --no-verify-access --yes from-package",
1111
"format": "prettier --write '**/*.{ts,js,json}' && eslint --fix --max-warnings=0 --ext=.ts,.js .",
1212
"lint": "prettier --check '**/*.{ts,js,json}' && eslint --max-warnings=0 --ext=.ts,.js .",
13-
"type-check": "yarn tsc --noEmit",
13+
"type-check": "tsc --noEmit",
1414
"test": "jest --verbose",
1515
"test:cov": "yarn test --coverage",
1616
"postinstall": "yarn --cwd=packages/core build"

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
});

0 commit comments

Comments
 (0)