Skip to content
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
127 changes: 127 additions & 0 deletions api/combined.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// @ts-check

import { renderCombinedCard } from "../src/cards/combined.js";
import { guardAccess } from "../src/common/access.js";
import {
CACHE_TTL,
resolveCacheSeconds,
setCacheHeaders,
setErrorCacheHeaders,
} from "../src/common/cache.js";
import {
MissingParamError,
retrieveSecondaryMessage,
} from "../src/common/error.js";
import { parseArray, parseBoolean } from "../src/common/ops.js";
import { renderError } from "../src/common/render.js";
import { fetchOverview } from "../src/fetchers/overview.js";
import { fetchStreak } from "../src/fetchers/streak.js";
import { fetchTopLanguages } from "../src/fetchers/top-languages.js";

// @ts-ignore
export default async (req, res) => {
const {
username,
hide_border,
card_width,
title_color,
icon_color,
text_color,
bg_color,
theme,
cache_seconds,
custom_title,
border_radius,
border_color,
disable_animations,
langs_count,
exclude_repo,
hide,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");

const access = guardAccess({
res,
id: username,
type: "username",
colors: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
});
if (!access.isPassed) {
return access.result;
}

try {
// Fetch all three data sources in parallel.
const [overview, streak, langs] = await Promise.all([
fetchOverview(username),
fetchStreak(username),
fetchTopLanguages(username, parseArray(exclude_repo)),
]);

const cacheSeconds = resolveCacheSeconds({
requested: parseInt(cache_seconds, 10),
def: CACHE_TTL.STATS_CARD.DEFAULT,
min: CACHE_TTL.STATS_CARD.MIN,
max: CACHE_TTL.STATS_CARD.MAX,
Comment on lines +69 to +71

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint renders top-languages data but resolves cache seconds using CACHE_TTL.STATS_CARD bounds (min 12h). That can significantly increase GitHub GraphQL load compared to /api/top-langs (min 2d), increasing rate-limit/latency risk for a more expensive fetcher. Consider using CACHE_TTL.TOP_LANGS_CARD bounds (or at least min/max aligned to top-langs) for /api/combined.

Suggested change
def: CACHE_TTL.STATS_CARD.DEFAULT,
min: CACHE_TTL.STATS_CARD.MIN,
max: CACHE_TTL.STATS_CARD.MAX,
def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
min: CACHE_TTL.TOP_LANGS_CARD.MIN,
max: CACHE_TTL.TOP_LANGS_CARD.MAX,

Copilot uses AI. Check for mistakes.
});

setCacheHeaders(res, cacheSeconds);

return res.send(
renderCombinedCard(
{ overview, streak, langs },
{
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
title_color,
icon_color,
text_color,
bg_color,
theme,
custom_title,
border_radius,
border_color,
disable_animations: parseBoolean(disable_animations),
langs_count: parseInt(langs_count, 10) || 8,
hide: parseArray(hide),
},
Comment on lines +90 to +93

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hide is parsed from the query and passed into renderCombinedCard, but renderCombinedCard doesn't read or apply a hide option. This is dead/unimplemented API surface and can confuse users. Either implement hide handling in the combined card (and document supported keys) or remove it from the endpoint options.

Copilot uses AI. Check for mistakes.
),
);
Comment on lines +59 to +95

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no automated test coverage added for the new /api/combined route (query parsing, caching headers, and error rendering). Similar endpoints like /api/top-langs, /api/pin, and /api/gist have dedicated Jest tests, so adding a tests/combined.test.js would help prevent regressions.

Copilot uses AI. Check for mistakes.
} catch (err) {
setErrorCacheHeaders(res);
if (err instanceof Error) {
return res.send(
renderError({
message: err.message,
secondaryMessage: retrieveSecondaryMessage(err),
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
show_repo_link: !(err instanceof MissingParamError),
},
}),
);
}
return res.send(
renderError({
message: "An unknown error occurred",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
};
Loading
Loading