Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: increase github rate limit with multiple PATs #58

Merged
merged 2 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ module.exports = async (req, res) => {
} = req.query;
let stats;

res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");

try {
stats = await fetchStats(username);
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions api/pin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = async (req, res) => {
} = req.query;

let repoData;

res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");

try {
Expand Down
29 changes: 18 additions & 11 deletions src/fetchRepo.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
const { request } = require("./utils");
const retryer = require("./retryer");

async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}

const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
fragment RepoInfo on Repository {
name
stargazers {
Expand All @@ -33,11 +31,20 @@ async function fetchRepo(username, reponame) {
}
}
`,
variables: {
login: username,
repo: reponame,
variables,
},
});
{
Authorization: `bearer ${token}`,
}
);
};

async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}

let res = await retryer(fetcher, { login: username, repo: reponame });

const data = res.data.data;

Expand Down
40 changes: 25 additions & 15 deletions src/fetchStats.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
const { request } = require("./utils");
const retryer = require("./retryer");
const calculateRank = require("./calculateRank");
require("dotenv").config();

async function fetchStats(username) {
if (!username) throw Error("Invalid username");

const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
query userInfo($login: String!) {
user(login: $login) {
name
login
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
contributionsCollection {
totalCommitContributions
}
pullRequests(first: 100) {
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
pullRequests(first: 1) {
totalCount
}
issues(first: 100) {
issues(first: 1) {
totalCount
}
followers {
Expand All @@ -36,9 +36,17 @@ async function fetchStats(username) {
}
}
}
`,
variables: { login: username },
});
`,
variables,
},
{
Authorization: `bearer ${token}`,
}
);
};

async function fetchStats(username) {
if (!username) throw Error("Invalid username");

const stats = {
name: "",
Expand All @@ -47,12 +55,14 @@ async function fetchStats(username) {
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: "C",
rank: { level: "C", score: 0 },
};

let res = await retryer(fetcher, { login: username });

if (res.data.errors) {
console.log(res.data.errors);
throw Error("Could not fetch user");
throw Error(res.data.errors[0].message || "Could not fetch user");
}

const user = res.data.data.user;
Expand Down
43 changes: 43 additions & 0 deletions src/retryer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const retryer = async (fetcher, variables, retries = 0) => {
if (retries > 7) {
throw new Error("Maximum retries exceeded");
}
try {
console.log(`Trying PAT_${retries + 1}`);

// try to fetch with the first token since RETRIES is 0 index i'm adding +1
let response = await fetcher(
variables,
process.env[`PAT_${retries + 1}`],
retries
);

// prettier-ignore
const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";

// if rate limit is hit increase the RETRIES and recursively call the retryer
// with username, and current RETRIES
if (isRateExceeded) {
console.log(`PAT_${retries + 1} Failed`);
retries++;
// directly return from the function
return retryer(fetcher, variables, retries);
}

// finally return the response
return response;
} catch (err) {
// prettier-ignore
// also checking for bad credentials if any tokens gets invalidated
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";

if (isBadCredential) {
console.log(`PAT_${retries + 1} Failed`);
retries++;
// directly return from the function
return retryer(fetcher, variables, retries);
}
}
};

module.exports = retryer;
12 changes: 9 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ function isValidHexColor(hexColor) {
).test(hexColor);
}

function request(data) {
function request(data, headers) {
return new Promise((resolve, reject) => {
axios({
url: "https://api.github.com/graphql",
method: "post",
headers: {
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
...headers,
},
data,
})
Expand All @@ -48,4 +48,10 @@ function request(data) {
});
}

module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request };
module.exports = {
renderError,
kFormatter,
encodeHTML,
isValidHexColor,
request,
};
2 changes: 1 addition & 1 deletion tests/fetchStats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("Test fetchStats", () => {
mock.onPost("https://api.github.com/graphql").reply(200, error);

await expect(fetchStats("anuraghazra")).rejects.toThrow(
"Could not fetch user"
"Could not resolve to a User with the login of 'noname'."
);
});
});
50 changes: 50 additions & 0 deletions tests/retryer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require("@testing-library/jest-dom");
const retryer = require("../src/retryer");

const fetcher = jest.fn((variables, token) => {
console.log(variables, token);
return new Promise((res, rej) => res({ data: "ok" }));
});

const fetcherFail = jest.fn(() => {
return new Promise((res, rej) =>
res({ data: { errors: [{ type: "RATE_LIMITED" }] } })
);
});

const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
return new Promise((res, rej) => {
// faking rate limit
if (retries < 1) {
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
}
return res({ data: "ok" });
});
});

describe("Test Retryer", () => {
it("retryer should return value and have zero retries on first try", async () => {
let res = await retryer(fetcher, {});

expect(fetcher).toBeCalledTimes(1);
expect(res).toStrictEqual({ data: "ok" });
});

it("retryer should return value and have 2 retries", async () => {
let res = await retryer(fetcherFailOnSecondTry, {});

expect(fetcherFailOnSecondTry).toBeCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
});

it("retryer should throw error if maximum retries reached", async () => {
let res;

try {
res = await retryer(fetcherFail, {});
} catch (err) {
expect(fetcherFail).toBeCalledTimes(8);
expect(err.message).toBe("Maximum retries exceeded");
}
});
});