Repository: anuraghazra/github-readme-stats
Pull request #3255 (2023-09-19 09:08:46)

refactor(stats card fetcher): improve could not fetch total commits error message
@rickstaa We can also make text "file an issue at https://tiny.one/readme-stats" optional in the next pull request if your are up to approve these changes to show it only for unexpected errors to reduce count of new issues.

https://github.com/anuraghazra/github-readme-stats/blob/master/src/common/utils.js#L44

vercel[bot]: [vc]: #dP7zrWS5VYjjD8WEbB/jJmhIq1cxbp6GiNbP3QnKXRo=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJnaXRodWItcmVhZG1lLXN0YXRzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2dpdGh1Yi1yZWFkbWUtc3RhdHMtdGVhbS9naXRodWItcmVhZG1lLXN0YXRzLzUxSlRxS3lVZDc2cFo1bVMzb2lnSzMyeHJMQzMiLCJwcmV2aWV3VXJsIjoiZ2l0aHViLXJlYWRtZS1zdGF0cy1naXQtcmVmYWN0by1kYWVkMTYtZ2l0aHViLXJlYWRtZS1zdGF0cy10ZWFtLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **github-readme-stats** | ✅ Ready ([Inspect](https://vercel.com/github-readme-stats-team/github-readme-stats/51JTqKyUd76pZ5mS3oigK32xrLC3)) | [Visit Preview](https://github-readme-stats-git-refacto-daed16-github-readme-stats-team.vercel.app) | Sep 19, 2023 9:08am |


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/3255?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) Report
Patch coverage: **`100.00%`** and no project coverage change.
> Comparison is base [(`0780435`)](https://app.codecov.io/gh/anuraghazra/github-readme-stats/commit/078043585a8531d1a3329255eecd6774df298d85?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) 98.37% compared to head [(`1409d1c`)](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/3255?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) 98.37%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #3255   +/-   ##
=======================================
  Coverage   98.37%   98.37%           
=======================================
  Files          27       27           
  Lines        5915     5918    +3     
  Branches      526      525    -1     
=======================================
+ Hits         5819     5822    +3     
  Misses         93       93           
  Partials        3        3           
```


| [Files Changed](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/3255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra) | Coverage Δ | |
|---|---|---|
| [src/fetchers/stats-fetcher.js](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/3255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra#diff-c3JjL2ZldGNoZXJzL3N0YXRzLWZldGNoZXIuanM=) | `94.63% <100.00%> (+0.05%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/anuraghazra/github-readme-stats/pull/3255?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Anurag+Hazra).


Patch:
diff --git a/.eslintrc.json b/.eslintrc.json
index 7b2160f..d8c31c7 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -156,7 +156,7 @@
         // "no-class-assign": "error",
         // "no-const-assign": "error",
         // "no-dupe-class-members": "error",
-        // "no-this-before-super": "error",
+        "no-this-before-super": "error",
         // "no-var": "warn",
         "object-shorthand": [ "warn" ],
         // "prefer-arrow-callback": "warn",
@@ -203,7 +203,7 @@
         // "no-ternary": "off",
         // "no-trailing-spaces": "warn",
         // "no-underscore-dangle": "warn",
-        // "no-unneeded-ternary": "warn",
+        "no-unneeded-ternary": "warn",
         // "object-curly-spacing": [ "warn", "always" ],
         // "one-var": "off",
         // "operator-assignment": [ "warn", "never" ],
diff --git a/.github/labeler.yml b/.github/labeler.yml
index c8da8cf..5e0475a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -49,3 +49,4 @@ ranks: src/calculateRank.js
 ci:
   - .github/workflows/*
   - scripts/*
+infrastructure: .eslintrc.js
diff --git a/api/gist.js b/api/gist.js
index 3e7dca0..1dbc5ae 100644
--- a/api/gist.js
+++ b/api/gist.js
@@ -34,8 +34,8 @@ export default async (req, res) => {
     const gistData = await fetchGist(id);
 
     let cacheSeconds = clampValue(
-      parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
-      CONSTANTS.FOUR_HOURS,
+      parseInt(cache_seconds || CONSTANTS.SIX_HOURS, 10),
+      CONSTANTS.SIX_HOURS,
       CONSTANTS.ONE_DAY,
     );
     cacheSeconds = process.env.CACHE_SECONDS
@@ -52,7 +52,7 @@ export default async (req, res) => {
     const isBothOver1K = stars > 1000 && forks > 1000;
     const isBothUnder1 = stars < 1 && forks < 1;
     if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
-      cacheSeconds = CONSTANTS.FOUR_HOURS;
+      cacheSeconds = CONSTANTS.SIX_HOURS;
     }
 
     res.setHeader(
diff --git a/api/index.js b/api/index.js
index c513f7c..25e4151 100644
--- a/api/index.js
+++ b/api/index.js
@@ -58,7 +58,7 @@ export default async (req, res) => {
 
     let cacheSeconds = clampValue(
       parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
-      CONSTANTS.FOUR_HOURS,
+      CONSTANTS.SIX_HOURS,
       CONSTANTS.ONE_DAY,
     );
     cacheSeconds = process.env.CACHE_SECONDS
diff --git a/api/pin.js b/api/pin.js
index d505746..21ecf96 100644
--- a/api/pin.js
+++ b/api/pin.js
@@ -41,7 +41,7 @@ export default async (req, res) => {
 
     let cacheSeconds = clampValue(
       parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
-      CONSTANTS.FOUR_HOURS,
+      CONSTANTS.SIX_HOURS,
       CONSTANTS.ONE_DAY,
     );
     cacheSeconds = process.env.CACHE_SECONDS
@@ -58,7 +58,7 @@ export default async (req, res) => {
     const isBothOver1K = stars > 1000 && forks > 1000;
     const isBothUnder1 = stars < 1 && forks < 1;
     if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
-      cacheSeconds = CONSTANTS.FOUR_HOURS;
+      cacheSeconds = CONSTANTS.SIX_HOURS;
     }
 
     res.setHeader(
diff --git a/api/top-langs.js b/api/top-langs.js
index 5596751..d9bf6b0 100644
--- a/api/top-langs.js
+++ b/api/top-langs.js
@@ -64,7 +64,7 @@ export default async (req, res) => {
 
     let cacheSeconds = clampValue(
       parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
-      CONSTANTS.FOUR_HOURS,
+      CONSTANTS.SIX_HOURS,
       CONSTANTS.ONE_DAY,
     );
     cacheSeconds = process.env.CACHE_SECONDS
diff --git a/api/wakatime.js b/api/wakatime.js
index 65f026e..b2582ca 100644
--- a/api/wakatime.js
+++ b/api/wakatime.js
@@ -43,7 +43,7 @@ export default async (req, res) => {
 
     let cacheSeconds = clampValue(
       parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
-      CONSTANTS.FOUR_HOURS,
+      CONSTANTS.SIX_HOURS,
       CONSTANTS.ONE_DAY,
     );
     cacheSeconds = process.env.CACHE_SECONDS
diff --git a/readme.md b/readme.md
index f0f5fce..f42f79e 100644
--- a/readme.md
+++ b/readme.md
@@ -293,12 +293,12 @@ You can customize the appearance of all your cards however you wish with URL par
 *   `bg_color` - Card's background color *(hex color)* **or** a gradient in the form of *angle,start,end*. Default: `fffefe`
 *   `hide_border` - Hides the card's border *(boolean)*. Default: `false`
 *   `theme` - Name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme.
-*   `cache_seconds` - Sets the cache header manually *(min: 14400, max: 86400)*. Default: `14400 seconds (4 hours)`.
+*   `cache_seconds` - Sets the cache header manually *(min: 21600, max: 86400)*. Default: `21600 seconds (6 hours)`.
 *   `locale` - Sets the language in the card *(e.g. cn, de, es, etc.)*. Default: `en`.
 *   `border_radius` - Corner rounding on the card. Default: `4.5`.
 
 > [!WARNING]\
-> We use caching to decrease the load on our servers (see <https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425>). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
+> We use caching to decrease the load on our servers (see <https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425>). Our cards have a default cache of 6 hours (21600 seconds). Also, note that the cache is clamped to a minimum of 6 hours and a maximum of 24 hours. If you want the data on your statistics card to be updated more often you can [deploy your own instance](#deploy-on-your-own) and set [environment variable](#disable-rate-limit-protections) `CACHE_SECONDS` to a value of your choosing.
 
 ##### Gradient in bg\_color
 
diff --git a/src/common/utils.js b/src/common/utils.js
index f1f74b6..2be7267 100644
--- a/src/common/utils.js
+++ b/src/common/utils.js
@@ -373,21 +373,30 @@ const noop = () => {};
 const logger =
   process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop };
 
-// Cache settings.
-const CARD_CACHE_SECONDS = 14400;
-const ERROR_CACHE_SECONDS = 600;
+const ONE_MINUTE = 60;
+const FIVE_MINUTES = 300;
+const TEN_MINUTES = 600;
+const FIFTEEN_MINUTES = 900;
+const THIRTY_MINUTES = 1800;
+const TWO_HOURS = 7200;
+const FOUR_HOURS = 14400;
+const SIX_HOURS = 21600;
+const EIGHT_HOURS = 28800;
+const ONE_DAY = 86400;
 
 const CONSTANTS = {
-  ONE_MINUTE: 60,
-  FIVE_MINUTES: 300,
-  TEN_MINUTES: 600,
-  FIFTEEN_MINUTES: 900,
-  THIRTY_MINUTES: 1800,
-  TWO_HOURS: 7200,
-  FOUR_HOURS: 14400,
-  ONE_DAY: 86400,
-  CARD_CACHE_SECONDS,
-  ERROR_CACHE_SECONDS,
+  ONE_MINUTE,
+  FIVE_MINUTES,
+  TEN_MINUTES,
+  FIFTEEN_MINUTES,
+  THIRTY_MINUTES,
+  TWO_HOURS,
+  FOUR_HOURS,
+  SIX_HOURS,
+  EIGHT_HOURS,
+  ONE_DAY,
+  CARD_CACHE_SECONDS: SIX_HOURS,
+  ERROR_CACHE_SECONDS: TEN_MINUTES,
 };
 
 const SECONDARY_ERROR_MESSAGES = {
diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js
index f021a72..b4a8ca2 100644
--- a/src/fetchers/stats-fetcher.js
+++ b/src/fetchers/stats-fetcher.js
@@ -180,7 +180,10 @@ const totalCommitsFetcher = async (username) => {
 
   const totalCount = res.data.total_count;
   if (!totalCount || isNaN(totalCount)) {
-    throw new Error("Could not fetch total commits.");
+    throw new CustomError(
+      "Could not fetch total commits.",
+      CustomError.GRAPHQL_ERROR,
+    );
   }
   return totalCount;
 };


Test patch:
diff --git a/tests/api.test.js b/tests/api.test.js
index 1353f1f..6af40f8 100644
--- a/tests/api.test.js
+++ b/tests/api.test.js
@@ -162,22 +162,25 @@ describe("Test /api/", () => {
       ["Content-Type", "image/svg+xml"],
       [
         "Cache-Control",
-        `max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
-          CONSTANTS.FOUR_HOURS
+        `max-age=${CONSTANTS.SIX_HOURS / 2}, s-maxage=${
+          CONSTANTS.SIX_HOURS
         }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
       ],
     ]);
   });
 
   it("should set proper cache", async () => {
-    const { req, res } = faker({ cache_seconds: 15000 }, data_stats);
+    const cache_seconds = 35000;
+    const { req, res } = faker({ cache_seconds }, data_stats);
     await api(req, res);
 
     expect(res.setHeader.mock.calls).toEqual([
       ["Content-Type", "image/svg+xml"],
       [
         "Cache-Control",
-        `max-age=7500, s-maxage=${15000}, stale-while-revalidate=${
+        `max-age=${
+          cache_seconds / 2
+        }, s-maxage=${cache_seconds}, stale-while-revalidate=${
           CONSTANTS.ONE_DAY
         }`,
       ],
@@ -224,8 +227,8 @@ describe("Test /api/", () => {
         ["Content-Type", "image/svg+xml"],
         [
           "Cache-Control",
-          `max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
-            CONSTANTS.FOUR_HOURS
+          `max-age=${CONSTANTS.SIX_HOURS / 2}, s-maxage=${
+            CONSTANTS.SIX_HOURS
           }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
         ],
       ]);
@@ -239,8 +242,8 @@ describe("Test /api/", () => {
         ["Content-Type", "image/svg+xml"],
         [
           "Cache-Control",
-          `max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
-            CONSTANTS.FOUR_HOURS
+          `max-age=${CONSTANTS.SIX_HOURS / 2}, s-maxage=${
+            CONSTANTS.SIX_HOURS
           }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
         ],
       ]);
@@ -301,4 +304,22 @@ describe("Test /api/", () => {
       renderError("Something went wrong", "Language not found"),
     );
   });
+
+  it("should render error card when include_all_commits true and upstream API fails", async () => {
+    mock
+      .onGet("https://api.github.com/search/commits?q=author:anuraghazra")
+      .reply(200, { error: "Some test error message" });
+
+    const { req, res } = faker(
+      { username: "anuraghazra", include_all_commits: true },
+      data_stats,
+    );
+
+    await api(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
+    expect(res.send).toBeCalledWith(
+      renderError("Could not fetch total commits.", "Please try again later"),
+    );
+  });
 });
diff --git a/tests/gist.test.js b/tests/gist.test.js
index 7327b91..8654e3d 100644
--- a/tests/gist.test.js
+++ b/tests/gist.test.js
@@ -188,8 +188,8 @@ describe("Test /api/gist", () => {
     expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
     expect(res.setHeader).toBeCalledWith(
       "Cache-Control",
-      `max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
-        CONSTANTS.FOUR_HOURS
+      `max-age=${CONSTANTS.SIX_HOURS / 2}, s-maxage=${
+        CONSTANTS.SIX_HOURS
       }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
     );
   });
diff --git a/tests/pin.test.js b/tests/pin.test.js
index dec6a24..0dcb5aa 100644
--- a/tests/pin.test.js
+++ b/tests/pin.test.js
@@ -4,7 +4,7 @@ import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 import pin from "../api/pin.js";
 import { renderRepoCard } from "../src/cards/repo-card.js";
-import { renderError } from "../src/common/utils.js";
+import { renderError, CONSTANTS } from "../src/common/utils.js";
 import { expect, it, describe, afterEach } from "@jest/globals";
 
 const data_repo = {
@@ -199,4 +199,28 @@ describe("Test /api/pin", () => {
       ),
     );
   });
+
+  it("should have proper cache", async () => {
+    const req = {
+      query: {
+        username: "anuraghazra",
+        repo: "convoychat",
+      },
+    };
+    const res = {
+      setHeader: jest.fn(),
+      send: jest.fn(),
+    };
+    mock.onPost("https://api.github.com/graphql").reply(200, data_user);
+
+    await pin(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
+    expect(res.setHeader).toBeCalledWith(
+      "Cache-Control",
+      `max-age=${CONSTANTS.SIX_HOURS / 2}, s-maxage=${
+        CONSTANTS.SIX_HOURS
+      }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
+    );
+  });
 });
diff --git a/tests/top-langs.test.js b/tests/top-langs.test.js
index 36a8586..272ee7e 100644
--- a/tests/top-langs.test.js
+++ b/tests/top-langs.test.js
@@ -4,7 +4,7 @@ import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 import topLangs from "../api/top-langs.js";
 import { renderTopLanguages } from "../src/cards/top-languages-card.js";
-import { renderError } from "../src/common/utils.js";
+import { renderError, CONSTANTS } from "../src/common/utils.js";
 import { expect, it, describe, afterEach } from "@jest/globals";
 
 const data_langs = {
@@ -206,4 +206,27 @@ describe("Test /api/top-langs", () => {
       renderError("Something went wrong", "Locale not found"),
     );
   });
+
+  it("should have proper cache", async () => {
+    const req = {
+      query: {
+        username: "anuraghazra",
+      },
+    };
+    const res = {
+      setHeader: jest.fn(),
+      send: jest.fn(),
+    };
+    mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
+
+    await topLangs(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
+    expect(res.setHeader).toBeCalledWith(
+      "Cache-Control",
+      `max-age=${CONSTANTS.SIX_HOURS / 2}, s-maxage=${
+        CONSTANTS.SIX_HOURS
+      }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
+    );
+  });
 });