Skip to content

Commit e6e506e

Browse files
rickstaaanuraghazra
authored andcommitted
feat: add PAT monitoring functions (anuraghazra#2178)
* feat: add PAT monitoring functions This commit adds two monitoring functions that can be used to check whether the PATs are functioning correctly: - status/up: Returns whether the PATs are rate limited. - status/pat-info: Returns information about the PATs. * feat: add shields.io dynamic badge json response This commit adds the ability to set the return format of the `/api/status/up` cloud function. When this format is set to `shields` a dynamic shields.io badge json is returned. * feat: add 'json' type to up monitor * feat: cleanup status functions * ci: decrease pat-info rate limiting time * feat: decrease monitoring functions rate limits * refactor: pat code * feat: add PAT monitoring functions This commit adds two monitoring functions that can be used to check whether the PATs are functioning correctly: - status/up: Returns whether the PATs are rate limited. - status/pat-info: Returns information about the PATs. * feat: add shields.io dynamic badge json response This commit adds the ability to set the return format of the `/api/status/up` cloud function. When this format is set to `shields` a dynamic shields.io badge json is returned. * feat: add 'json' type to up monitor * feat: cleanup status functions * ci: decrease pat-info rate limiting time * feat: decrease monitoring functions rate limits * refactor: pat code * test: fix pat-info tests * Update api/status/pat-info.js Co-authored-by: Anurag Hazra <[email protected]> * test: fix broken tests * chore: fix suspended account * chore: simplify and refactor * chore: fix test * chore: add resetIn field --------- Co-authored-by: Anurag <[email protected]>
1 parent 1fdeb27 commit e6e506e

File tree

7 files changed

+685
-2
lines changed

7 files changed

+685
-2
lines changed

api/status/pat-info.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @file Contains a simple cloud function that can be used to check which PATs are no
3+
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
4+
*
5+
* @description This function is currently rate limited to 1 request per 10 minutes.
6+
*/
7+
8+
import { logger, request, dateDiff } from "../../src/common/utils.js";
9+
export const RATE_LIMIT_SECONDS = 60 * 10; // 1 request per 10 minutes
10+
11+
/**
12+
* Simple uptime check fetcher for the PATs.
13+
*
14+
* @param {import('axios').AxiosRequestHeaders} variables
15+
* @param {string} token
16+
*/
17+
const uptimeFetcher = (variables, token) => {
18+
return request(
19+
{
20+
query: `
21+
query {
22+
rateLimit {
23+
remaining
24+
resetAt
25+
},
26+
}`,
27+
variables,
28+
},
29+
{
30+
Authorization: `bearer ${token}`,
31+
},
32+
);
33+
};
34+
35+
const getAllPATs = () => {
36+
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
37+
};
38+
39+
/**
40+
* Check whether any of the PATs is expired.
41+
*/
42+
const getPATInfo = async (fetcher, variables) => {
43+
const details = {};
44+
const PATs = getAllPATs();
45+
46+
for (const pat of PATs) {
47+
try {
48+
const response = await fetcher(variables, process.env[pat]);
49+
const errors = response.data.errors;
50+
const hasErrors = Boolean(errors);
51+
const errorType = errors?.[0]?.type;
52+
const isRateLimited =
53+
(hasErrors && errorType === "RATE_LIMITED") ||
54+
response.data.data?.rateLimit?.remaining === 0;
55+
56+
// Store PATs with errors.
57+
if (hasErrors && errorType !== "RATE_LIMITED") {
58+
details[pat] = {
59+
status: "error",
60+
error: {
61+
type: errors[0].type,
62+
message: errors[0].message,
63+
},
64+
};
65+
continue;
66+
} else if (isRateLimited) {
67+
const date1 = new Date();
68+
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
69+
details[pat] = {
70+
status: "exhausted",
71+
remaining: 0,
72+
resetIn: dateDiff(date2, date1) + " minutes",
73+
};
74+
} else {
75+
details[pat] = {
76+
status: "valid",
77+
remaining: response.data.data.rateLimit.remaining,
78+
};
79+
}
80+
} catch (err) {
81+
// Store the PAT if it is expired.
82+
const errorMessage = err.response?.data?.message?.toLowerCase();
83+
if (errorMessage === "bad credentials") {
84+
details[pat] = {
85+
status: "expired",
86+
};
87+
} else if (errorMessage === "sorry. your account was suspended.") {
88+
details[pat] = {
89+
status: "suspended",
90+
};
91+
} else {
92+
throw err;
93+
}
94+
}
95+
}
96+
97+
const filterPATsByStatus = (status) => {
98+
return Object.keys(details).filter((pat) => details[pat].status === status);
99+
};
100+
101+
return {
102+
validPATs: filterPATsByStatus("valid"),
103+
expiredPATs: filterPATsByStatus("expired"),
104+
exhaustedPATS: filterPATsByStatus("exhausted"),
105+
errorPATs: filterPATsByStatus("error"),
106+
details,
107+
};
108+
};
109+
110+
/**
111+
* Cloud function that returns information about the used PATs.
112+
*/
113+
export default async (_, res) => {
114+
res.setHeader("Content-Type", "application/json");
115+
try {
116+
// Add header to prevent abuse.
117+
const PATsInfo = await getPATInfo(uptimeFetcher, {});
118+
if (PATsInfo) {
119+
res.setHeader(
120+
"Cache-Control",
121+
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
122+
);
123+
}
124+
res.send(JSON.stringify(PATsInfo, null, 2));
125+
} catch (err) {
126+
// Throw error if something went wrong.
127+
logger.error(err);
128+
res.setHeader("Cache-Control", "no-store");
129+
res.send("Something went wrong: " + err.message);
130+
}
131+
};

api/status/up.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @file Contains a simple cloud function that can be used to check if the PATs are still
3+
* functional.
4+
*
5+
* @description This function is currently rate limited to 1 request per 10 minutes.
6+
*/
7+
8+
import retryer from "../../src/common/retryer.js";
9+
import { logger, request } from "../../src/common/utils.js";
10+
11+
export const RATE_LIMIT_SECONDS = 60 * 10; // 1 request per 10 minutes
12+
13+
/**
14+
* Simple uptime check fetcher for the PATs.
15+
*
16+
* @param {import('axios').AxiosRequestHeaders} variables
17+
* @param {string} token
18+
*/
19+
const uptimeFetcher = (variables, token) => {
20+
return request(
21+
{
22+
query: `
23+
query {
24+
rateLimit {
25+
remaining
26+
}
27+
}
28+
`,
29+
variables,
30+
},
31+
{
32+
Authorization: `bearer ${token}`,
33+
},
34+
);
35+
};
36+
37+
/**
38+
* Creates Json response that can be used for shields.io dynamic card generation.
39+
*
40+
* @param {*} up Whether the PATs are up or not.
41+
* @returns Dynamic shields.io JSON response object.
42+
*
43+
* @see https://shields.io/endpoint.
44+
*/
45+
const shieldsUptimeBadge = (up) => {
46+
const schemaVersion = 1;
47+
const isError = true;
48+
const label = "Public Instance";
49+
const message = up ? "up" : "down";
50+
const color = up ? "brightgreen" : "red";
51+
return {
52+
schemaVersion,
53+
label,
54+
message,
55+
color,
56+
isError,
57+
};
58+
};
59+
60+
/**
61+
* Cloud function that returns whether the PATs are still functional.
62+
*/
63+
export default async (req, res) => {
64+
let { type } = req.query;
65+
type = type ? type.toLowerCase() : "boolean";
66+
67+
res.setHeader("Content-Type", "application/json");
68+
69+
try {
70+
let PATsValid = true;
71+
try {
72+
await retryer(uptimeFetcher, {});
73+
} catch (err) {
74+
PATsValid = false;
75+
}
76+
77+
if (PATsValid) {
78+
res.setHeader(
79+
"Cache-Control",
80+
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
81+
);
82+
} else {
83+
res.setHeader("Cache-Control", "no-store");
84+
}
85+
86+
switch (type) {
87+
case "shields":
88+
res.send(shieldsUptimeBadge(PATsValid));
89+
break;
90+
case "json":
91+
res.send({ up: PATsValid });
92+
break;
93+
default:
94+
res.send(PATsValid);
95+
break;
96+
}
97+
} catch (err) {
98+
// Return fail boolean if something went wrong.
99+
logger.error(err);
100+
res.setHeader("Cache-Control", "no-store");
101+
res.send("Something went wrong: " + err.message);
102+
}
103+
};

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"@testing-library/dom": "^8.17.1",
4040
"@testing-library/jest-dom": "^5.16.5",
4141
"@uppercod/css-to-object": "^1.1.1",
42-
"axios-mock-adapter": "^1.18.1",
42+
"axios-mock-adapter": "^1.21.2",
4343
"color-contrast-checker": "^2.1.0",
4444
"hjson": "^3.2.2",
4545
"husky": "^8.0.0",

src/common/utils.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,19 @@ const parseEmojis = (str) => {
424424
});
425425
};
426426

427+
/**
428+
* Get diff in minutes
429+
* @param {Date} d1
430+
* @param {Date} d2
431+
* @returns {number}
432+
*/
433+
const dateDiff = (d1, d2) => {
434+
const date1 = new Date(d1);
435+
const date2 = new Date(d2);
436+
const diff = date1.getTime() - date2.getTime();
437+
return Math.round(diff / (1000 * 60));
438+
};
439+
427440
export {
428441
ERROR_CARD_LENGTH,
429442
renderError,
@@ -447,4 +460,5 @@ export {
447460
lowercaseTrim,
448461
chunkArray,
449462
parseEmojis,
463+
dateDiff,
450464
};

0 commit comments

Comments
 (0)