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

Commit 8f67e94

Browse files
committed
Send user's IP address with metrics
Used for visualization aggregate geolocation data. The IP is never stored, and it is never sent to 3rd parties. We read the IP from the `X-Forwarded-For` header since in production one's backend service is most likely behind a reverse proxy of some sort. We use the left-most value in case there are multiple IPs in it, since that's most likely not from "our" infra and we don't have to worry about spoofing too much because it's not used for anything security related. Some additional reading about getting the real IP address: https://adam-p.ca/blog/2022/03/x-forwarded-for/
1 parent 8ecfc00 commit 8f67e94

7 files changed

Lines changed: 72 additions & 0 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Send user's IP address with metrics. Used for visualization aggregate geolocation data.
13+
The IP is never stored, and it is never sent to 3rd parties.
14+
1015
## [1.4.1] - 2022-02-20
1116

1217
### Added

packages/core/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const myApilyticsMiddleware = async (req, handler) => {
4444

4545
const timer = milliSecondTimer();
4646
const res = await handler(req);
47+
4748
sendApilyticsMetrics({
4849
apiKey,
4950
path: req.path,
@@ -53,6 +54,7 @@ const myApilyticsMiddleware = async (req, handler) => {
5354
requestSize: req.bodyBytes.length,
5455
responseSize: res.bodyBytes.length,
5556
userAgent: req.headers['user-agent'],
57+
ip: req.headers['x-forwarded-for']?.split(',')[0].trim(),
5658
timeMillis: timer(),
5759
});
5860
return res;

packages/core/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface Params {
1414
requestSize?: number;
1515
responseSize?: number;
1616
userAgent?: string;
17+
ip?: string;
1718
apilyticsIntegration?: string;
1819
integratedLibrary?: string;
1920
}
@@ -38,6 +39,8 @@ interface Params {
3839
* @param params.responseSize - Size of the sent HTTP response's body in bytes.
3940
* @param params.userAgent - Value of the `User-Agent` header from the user's
4041
* HTTP request.
42+
* @param params.ip - User's IP address (used for geolocation,
43+
* never stored nor sent to 3rd parties).
4144
* @param params.apilyticsIntegration - Name of the Apilytics integration that's
4245
* calling this, e.g. 'apilytics-node-express'.
4346
* No need to pass this when calling from user code.
@@ -49,6 +52,7 @@ interface Params {
4952
*
5053
* const timer = milliSecondTimer();
5154
* const res = await handler(req);
55+
*
5256
* sendApilyticsMetrics({
5357
* apikey: "<your-api-key>",
5458
* path: req.path,
@@ -58,6 +62,7 @@ interface Params {
5862
* requestSize: req.bodyBytes.length,
5963
* responseSize: res.bodyBytes.length,
6064
* userAgent: req.headers['user-agent'],
65+
* ip: req.headers['x-forwarded-for']?.split(',')[0].trim(),
6166
* timeMillis: timer(),
6267
* });
6368
*/
@@ -71,6 +76,7 @@ export const sendApilyticsMetrics = ({
7176
requestSize,
7277
responseSize,
7378
userAgent,
79+
ip,
7480
apilyticsIntegration,
7581
integratedLibrary,
7682
}: Params): void => {
@@ -86,6 +92,7 @@ export const sendApilyticsMetrics = ({
8692
requestSize,
8793
responseSize,
8894
userAgent: userAgent || undefined,
95+
ip: ip || undefined,
8996
cpuUsage,
9097
memoryUsage,
9198
memoryTotal,

packages/express/__tests__/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,31 @@ describe('apilyticsMiddleware()', () => {
178178
expect(data.userAgent).toEqual('some agent');
179179
});
180180

181+
it('should send IP', async () => {
182+
const agent = createAgent({ apiKey });
183+
let response = await agent
184+
.get('/dummy')
185+
.set('X-Forwarded-For', '127.0.0.1');
186+
expect(response.status).toEqual(200);
187+
188+
await flushTimers();
189+
190+
expect(requestSpy).toHaveBeenCalledTimes(1);
191+
let data = JSON.parse(clientRequestMock.write.mock.calls[0]);
192+
expect(data.ip).toEqual('127.0.0.1');
193+
194+
response = await agent
195+
.get('/dummy')
196+
.set('X-Forwarded-For', '127.0.0.2,127.0.0.3');
197+
expect(response.status).toEqual(200);
198+
199+
await flushTimers();
200+
201+
expect(requestSpy).toHaveBeenCalledTimes(2);
202+
data = JSON.parse(clientRequestMock.write.mock.calls[1]);
203+
expect(data.ip).toEqual('127.0.0.2');
204+
});
205+
181206
it('should handle zero request and response sizes', async () => {
182207
const agent = createAgent({ apiKey });
183208
const response = await agent.post('/empty');

packages/express/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export const apilyticsMiddleware = (
5959
requestSize,
6060
responseSize,
6161
userAgent: req.headers['user-agent'],
62+
// Type assertion since the value cannot be an array.
63+
ip: (req.headers['x-forwarded-for'] as string | undefined)
64+
?.split(',')[0]
65+
.trim(),
6266
timeMillis: timer(),
6367
apilyticsIntegration: 'apilytics-node-express',
6468
integratedLibrary: EXPRESS_VERSION

packages/next/__tests__/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,31 @@ describe('withApilytics()', () => {
198198
expect(data.userAgent).toEqual('some agent');
199199
});
200200

201+
it('should send IP', async () => {
202+
const agent = createAgent({ apiKey });
203+
let response = await agent
204+
.get('/dummy')
205+
.set('X-Forwarded-For', '127.0.0.1');
206+
expect(response.status).toEqual(200);
207+
208+
await flushTimers();
209+
210+
expect(requestSpy).toHaveBeenCalledTimes(1);
211+
let data = JSON.parse(clientRequestMock.write.mock.calls[0]);
212+
expect(data.ip).toEqual('127.0.0.1');
213+
214+
response = await agent
215+
.get('/dummy')
216+
.set('X-Forwarded-For', '127.0.0.2,127.0.0.3');
217+
expect(response.status).toEqual(200);
218+
219+
await flushTimers();
220+
221+
expect(requestSpy).toHaveBeenCalledTimes(2);
222+
data = JSON.parse(clientRequestMock.write.mock.calls[1]);
223+
expect(data.ip).toEqual('127.0.0.2');
224+
});
225+
201226
it('should handle zero request and response sizes', async () => {
202227
const agent = createAgent({ apiKey });
203228
const response = await agent.post('/empty');

packages/next/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export const withApilytics = <T>(
7474
requestSize,
7575
responseSize,
7676
userAgent: req.headers['user-agent'],
77+
// Type assertion since the value cannot be an array.
78+
ip: (req.headers['x-forwarded-for'] as string | undefined)
79+
?.split(',')[0]
80+
.trim(),
7781
timeMillis: timer(),
7882
apilyticsIntegration: 'apilytics-node-next',
7983
integratedLibrary: NEXT_VERSION ? `next/${NEXT_VERSION}` : undefined,

0 commit comments

Comments
 (0)