Skip to content

Commit c6fab88

Browse files
authored
feat: implementing request timeouts in the node sdk (#182)
* feat: first pass on fetch timeouts * test: removing timeout tests for now * chore: docs * fix: timeout is no longer user-configurable
1 parent dda036a commit c6fab88

File tree

6 files changed

+53
-7
lines changed

6 files changed

+53
-7
lines changed

packages/node/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ app.use(readme.metrics('', req => ({
5757

5858
| Option | Use |
5959
| :--- | :--- |
60-
| development | **default: false** If true, the log will be separate from normal production logs. This is great for separating staging or test data from data coming from customers |
61-
| bufferLength | **default: 10** By default, we only send logs to ReadMe after 10 requests are made. Depending on the usage of your API it make make sense to send logs more or less frequently |
60+
| development | **default: false** If true, the log will be separate from normal production logs. This is great for separating staging or test data from data coming from customers. |
61+
| bufferLength | **default: 10** By default, we only send logs to ReadMe after 10 requests are made. Depending on the usage of your API it make make sense to send logs more or less frequently. |
6262
| blacklist | **optional** An array of keys from your API requests and responses headers and bodies that you wish to blacklist from sending to ReadMe.<br /><br />If you configure a blacklist, it will override any whitelist configuration. |
6363
| whitelist | **optional** An array of keys from your API requests and responses headers and bodies that you only wish to send to ReadMe. |
64-
| baseLogUrl | **optional** This is the base URL for your ReadMe project. Normally this would be `https://projectName.readme.io` or `https://docs.yourdomain.com`, however if this value is not supplied, a request to the ReadMe API will be made once a day to retrieve it. This data is cached into `node_modules/.cache/readmeio`.
64+
| baseLogUrl | **optional** This is the base URL for your ReadMe project. Normally this would be `https://projectName.readme.io` or `https://docs.yourdomain.com`, however if this value is not supplied, a request to the ReadMe API will be made once a day to retrieve it. This data is cached into `node_modules/.cache/readmeio`. |
6565

6666
### Limitations
6767
- Currently only supports JSON request bodies. Adding a whitelist/blacklist for non-JSON bodies will not work (unless they're added to `req.body`) the same way that `body-parser` does it. The properties will be passed into [`postData`](http://www.softwareishard.com/blog/har-12-spec/#postData) as a `params` array.

packages/node/__tests__/index.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ describe('#metrics', () => {
160160
});
161161
});
162162

163+
describe('#timeout', () => {
164+
it.todo('should silently fail metrics requests if they take longer than the timeout');
165+
166+
it.todo('should silently fail baseLogUrl requests if they take longer than the timeout');
167+
});
168+
163169
describe('#bufferLength', () => {
164170
it('should send requests when number hits `bufferLength` size', async function test() {
165171
const apiMock = getReadMeApiMock(1);

packages/node/config/default.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"host": "https://metrics.readme.io",
33
"readmeApiUrl": "https://dash.readme.io/api",
4-
"bufferLength": 1
4+
"bufferLength": 1,
5+
"timeout": 2000
56
}

packages/node/index.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const fetch = require('node-fetch');
2+
const timeoutSignal = require('timeout-signal');
23
const { v4: uuidv4 } = require('uuid');
34
const crypto = require('crypto');
45
const config = require('./config');
@@ -31,7 +32,7 @@ function patchResponse(res) {
3132
};
3233
}
3334

34-
async function getProjectBaseUrl(encodedApiKey) {
35+
async function getProjectBaseUrl(encodedApiKey, requestTimeout) {
3536
const cacheDir = findCacheDir({ name: pkg.name, create: true });
3637
const fsSafeApikey = crypto.createHash('md5').update(encodedApiKey).digest('hex');
3738

@@ -49,13 +50,16 @@ async function getProjectBaseUrl(encodedApiKey) {
4950
lastUpdated === undefined ||
5051
(lastUpdated !== undefined && Math.abs(lastUpdated - Math.round(Date.now() / 1000)) >= 86400)
5152
) {
53+
const signal = timeoutSignal(requestTimeout);
54+
5255
let baseUrl;
5356
await fetch(`${config.readmeApiUrl}/v1/`, {
5457
method: 'get',
5558
headers: {
5659
Authorization: `Basic ${encodedApiKey}`,
5760
'User-Agent': `${pkg.name}/${pkg.version}`,
5861
},
62+
signal,
5963
})
6064
.then(res => {
6165
if (res.status >= 400 && res.status <= 599) {
@@ -75,6 +79,9 @@ async function getProjectBaseUrl(encodedApiKey) {
7579
// now yesterday so that in 2 minutes we'll automatically make another attempt.
7680
cache.setKey('baseUrl', null);
7781
cache.setKey('lastUpdated', Math.round(Date.now() / 1000) - 86400 + 120);
82+
})
83+
.finally(() => {
84+
timeoutSignal.clear(signal);
7885
});
7986

8087
cache.save();
@@ -90,13 +97,14 @@ module.exports.metrics = (apiKey, group, options = {}) => {
9097
if (!group) throw new Error('You must provide a grouping function');
9198

9299
const bufferLength = options.bufferLength || config.bufferLength;
100+
const requestTimeout = config.timeout;
93101
const encodedApiKey = Buffer.from(`${apiKey}:`).toString('base64');
94102
let baseLogUrl = options.baseLogUrl || undefined;
95103
let queue = [];
96104

97105
return async (req, res, next) => {
98106
if (baseLogUrl === undefined) {
99-
baseLogUrl = await getProjectBaseUrl(encodedApiKey);
107+
baseLogUrl = await getProjectBaseUrl(encodedApiKey, requestTimeout);
100108
}
101109

102110
const startedDateTime = new Date();
@@ -117,6 +125,9 @@ module.exports.metrics = (apiKey, group, options = {}) => {
117125
if (queue.length >= bufferLength) {
118126
const json = queue.slice();
119127
queue = [];
128+
129+
const signal = timeoutSignal(requestTimeout);
130+
120131
fetch(`${config.host}/v1/request`, {
121132
method: 'post',
122133
body: JSON.stringify(json),
@@ -125,9 +136,15 @@ module.exports.metrics = (apiKey, group, options = {}) => {
125136
'Content-Type': 'application/json',
126137
'User-Agent': `${pkg.name}/${pkg.version}`,
127138
},
139+
signal,
128140
})
129141
.then(() => {})
130-
.catch(() => {});
142+
.catch(() => {
143+
// Silently discard errors and timeouts.
144+
})
145+
.finally(() => {
146+
timeoutSignal.clear(signal);
147+
});
131148
}
132149

133150
cleanup(); // eslint-disable-line no-use-before-define

packages/node/package-lock.json

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/node/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"flat-cache": "^3.0.4",
2626
"lodash": "^4.17.15",
2727
"node-fetch": "^2.6.0",
28+
"timeout-signal": "^1.1.0",
2829
"uuid": "^8.2.0"
2930
},
3031
"devDependencies": {

0 commit comments

Comments
 (0)