Following diagram explain how metrics code is structured:
This section explain how metrics is structured.
source/app/metrics/
contains metrics engine filessource/app/action/
contains GitHub action filesindex.mjs
contains GitHub action entry pointaction.yml
contains GitHub action templated descriptor
source/app/web/
contains web instance filesindex.mjs
contains web instance entry pointinstance.mjs
contains web instance source codesettings.example.json
contains web instance settings examplestatics/
contains web instance static filesapp.js
contains web instance client source codeapp.placeholder.js
contains web instance placeholder mocked data
source/plugins/*/
contains source code of pluginsREADME.md
contains plugin documentationmetadata.yml
contains plugin metadataexamples.yml
contains plugin workflow examplesindex.mjs
contains plugin source codequeries/
contains plugin GraphQL queries
source/templates/*/
contains templates filesREADME.md
contains template documentationmetadata.yml
contains template metadataexamples.yml
contains template workflow examplesimage.svg
contains template image used to render metricsstyle.css
contains style used to render metricsfonts.css
contains additional fonts used to render metricstemplate.mjs
contains template source code
tests/
contains testsmetrics.test.js
contains metrics testerssource/app/mocks/
contains mocked data filesapi/
contains mocked api dataaxios/
contains external REST APIs mocked datagithub/
contains mocked GitHub api data
index.mjs
contains mockers
Dockerfile
contains docker instructions used to build metrics imagepackage.json
contains dependencies and command line aliases
This section explore some topics which explain globally how metrics was designed and how it works.
metrics actually exploit the possibility of integrating HTML and CSS into SVGs, so basically creating these images is as simple as designing static web pages. It can even handle animations and transparency.
SVGs are templated through EJS framework to make the whole rendering process easier thanks to variables, conditional and loop statements. Only drawback is that it tends to make syntax coloration a bit confused because templates are often misinterpreted as HTML tags markers (<%= "EJS templating syntax" %>
).
Images (and custom fonts) are encoded into base64 to prevent cross-origin requests, while also removing any external dependencies, although it tends to increase files sizes.
Since SVG renders differently depending on OS and browsers (system fonts, CSS support, ...), it's pretty hard to compute dynamically height. Previously, it was computed with ugly formulas, but as it wasn't scaling really well (especially since the introduction of variable content length plugins). It was often resulting in large empty blank spaces or really badly cropped image.
To solve this, metrics now spawns a puppeteer instance and directly render SVG in a browser environment (with all animations disabled). An hidden "marker" element is placed at the end of the image, and is used to resize image through its Y-offset.
Additional bonus of using puppeteer is that it can take screenshots, making it easy to convert SVGs to PNG output.
metrics mostly use GitHub APIs since it is its primary target. Most of the time, data are retrieved through GraphQL to save APIs requests, but it sometimes fallback on REST for other features. Octokit SDKs are used to make it easier.
As for other external services (Twitter, Spotify, PageSpeed, ...), metrics use their respective APIs, usually making https requests through axios and by following their documentation. It would be overkill to install entire SDKs for these since plugins rarely uses more than 2/3 calls.
In last resort, puppeteer is seldom used to scrap websites, though its use tends to make things slow and unstable (as it'll break upon HTML structural changes).
Historically, metrics used to be only a web service without any customization possible. The single input was a GitHub username, and was composed of what is now base
content (along with languages
and followup
plugin, which is why they can be computed without any additional queries). That's why base
content is handled a bit differently from plugins.
As it gathered more and more plugins over time, generating a single user's metrics was becoming costly both in terms of resources but also in APIs requests. It was thus decided to switch to GitHub Action. At first, it was just a way to explore possibilities of this GitHub feature, but now it's basically the full-experience of metrics (unless you use your own self-hosted instance).
Both web instance and Action actually use the same entrypoint so they basically have the same features. Action just format inputs into a query-like object (similarly to when url params are parsed by web instance), from which metrics compute the rendered image. It also makes testing easier, as test cases can be reused since only inputs differs.
Below is a list of used packages.
- express/express.js and expressjs/compression
- To serve, compute and render a GitHub user's metrics
- nfriedly/express-rate-limit
- To apply rate limiting on server and avoid spams and hitting GitHub API's own rate limit
- octokit/graphql.js and octokit/rest.js
- To perform request to GitHub GraphQL API and GitHub REST API
- mde/ejs
- To render SVG images
- ptarjan/node-cache
- To cache generated content
- lovell/sharp, foliojs/png.js and eugeneware/gifencoder
- To process images transformations
- svg/svgo
- To optimize generated SVG
- axios/axios
- To make HTTP/S requests
- actions/toolkit
- To build the GitHub Action
- vuejs/vue, egoist/vue-prism-component, prismjs/prism and zenorocha/clipboard.js
- To display server application
- puppeteer/puppeteer
- To scrape the web
- marudor/libxmljs2 and chrisbottin/xml-formatter
- To format, test and verify SVG validity
- facebook/jest and nodeca/js-yaml
- For unit testing
- faker-js/faker
- For mocking data
- steveukx/git-js
- For simple git operations
- twitter/twemoji-parser and IonicaBizau/emoji-name-map
- To parse and handle emojis/twemojis
- jshemas/openGraphScraper
- To retrieve open graphs metadata
- rbren/rss-parser
- To parse RSS streams
- Nixinova/Linguist
- To analyze used languages
- markedjs/marked and apostrophecms/sanitize-html
- To render markdown blocks
- css/csso and FullHuman/purgecss
- To optimize and purge unused CSS
- isaacs/minimatch
- For file traversal
- node-fetch/node-fetch
- For
fetch
polyfill
- For
- eslint/eslint
- As linter