Add tests and optimization/compression for rendered metrics

This commit is contained in:
lowlighter committed 2020-10-13 14:00:48 +02:00
1 parent 1b738d11d2
commit 39404a9acf
10 files changed
+1055 -32

No files matched your search

+15 -9
View File
@@ -6,7 +6,7 @@ See what it looks like below :
![GitHub metrics](https://github.com/lowlighter/lowlighter/blob/master/github-metrics.svg)
##### 🦑 Interested to get your own ?
##### 🦑 Interested to get your own ?
Try it now at [metrics.lecoq.io](https://metrics.lecoq.io/) with your GitHub username !
## 📜 How to use ?
@@ -482,17 +482,17 @@ And pass `?traffic=1` in url when generating metrics.
* `action/index.mjs` contains the GitHub action code
* `action/dist/index.js` contains compiled the GitHub action code
* `utils/*` contains various utilitaries for build
### 💪 Contributing
If you would like to suggest a new feature or find a bug, you can fill an [issue](https://github.com/lowlighter/metrics/issues) describing your problem.
If you're motivated enough, you can submit a [pull request](https://github.com/lowlighter/metrics/pulls) to integrate new features or to solve open issues.
Read the few sections below to get started with project structure.
If you're motivated enough, you can submit a [pull request](https://github.com/lowlighter/metrics/pulls) to integrate new features or to solve open issues.
Read the few sections below to get started with project structure.
#### Adding new metrics through GraphQL API, REST API or Third-Party service
If you want to gather additional metrics, update the GraphQL query from `src/query.graphql` to get additional data from [GitHub GraphQL API](https://docs.github.com/en/graphql).
If you want to gather additional metrics, update the GraphQL query from `src/query.graphql` to get additional data from [GitHub GraphQL API](https://docs.github.com/en/graphql).
Add additional computations and formatting in `src/metrics.mjs`.
Raw queried data should be exposed in `data.user` whereas computed data should be in `data.computed`.
@@ -517,7 +517,7 @@ It then use directly `src/metrics.mjs` to generate the SVG image and commit them
#### Testing new features
To test new features, you'll need to follow the first steps of the `Deploying your own instance` tutorial.
To test new features, you'll need to follow the first steps of the `Deploying your own instance` tutorial.
Basically you create a `settings.json` containing a test token and `debug` mode enabled.
You can then start the node with `npm start` and you'll be able to test how the SVG renders with your editions by opening the server url in your browser.
@@ -544,16 +544,22 @@ Below is a list of useful links :
Below is a list of primary dependencies :
* [express/express.js](https://github.com/expressjs/express)
* [express/express.js](https://github.com/expressjs/express) and [expressjs/compression](https://github.com/expressjs/compression)
* To serve, compute and render a GitHub user's metrics
* [nfriedly/express-rate-limit](https://github.com/nfriedly/express-rate-limit)
* To apply rate limiting on server and avoid spams and hitting GitHub API's own rate limit
* [octokit/graphql.js](https://github.com/octokit/graphql.js/)
* To perform request to GitHub GraphQL API
* [octokit/graphql.js](https://github.com/octokit/graphql.js/) and [octokit/rest.js](https://github.com/octokit/rest.js)
* To perform request to GitHub GraphQL API and GitHub REST API
* [ptarjan/node-cache](https://github.com/ptarjan/node-cache)
* To cache generated content
* [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64)
* To generate base64 representation of users' avatars
* [svg/svgo](https://github.com/svg/svgo)
* To optimize generated SVG
* [axios/axios](https://github.com/axios/axios)
* To make HTTP/S requests
* [actions/toolkit](https://github.com/actions/toolkit/tree/master) and [vercel/ncc](https://github.com/vercel/ncc)
* To build the GitHub Action
All icons were ripped across GitHub's site, but still remains the intellectual property of GitHub.
See [GitHub Logos and Usage](https://github.com/logos) for more information.
+5 -5
View File
@@ -1,4 +1,4 @@
module.exports=(()=>{var __webpack_modules__={6677:(e,t,r)=>{"use strict";r.r(t);var s=r(5622);var o=r.n(s);var n=r(270);var i=r(3584);var a=r(2882);var c=r.n(a);var u=r(9483);var l=r.n(u);(async function(){const[e,t,r,s]=[a,u,i,n].map(e=>e&&e.default?e.default:e);try{console.log(`GitHub metrics as SVG image`);console.log(`========================================================`);console.log(`Version | 1.5.0`);process.on("unhandledRejection",e=>{throw e});const n=`<svg xmlns="http://www.w3.org/2000/svg" width="480" height="\${536 + (computed.plugins.pagespeed ? 100 : 0)}">
module.exports=(()=>{var __webpack_modules__={76677:(e,t,r)=>{"use strict";r.r(t);var n=r(85622);var i=r.n(n);var a=r(60270);var o=r(3584);var s=r(32882);var l=r.n(s);var c=r(29483);var u=r.n(c);(async function(){const[e,t,r,n]=[s,c,o,a].map(e=>e&&e.default?e.default:e);try{console.log(`GitHub metrics as SVG image`);console.log(`========================================================`);console.log(`Version | 1.6.0`);process.on("unhandledRejection",e=>{throw e});const a=`<svg xmlns="http://www.w3.org/2000/svg" width="480" height="\${536 + (computed.plugins.pagespeed ? 100 : 0)}">
<style>
\${style}
</style>
@@ -255,7 +255,7 @@ module.exports=(()=>{var __webpack_modules__={6677:(e,t,r)=>{"use strict";r.r(t)
</div>
</section>
</div>
\` : \`
\` : \`
<section>
<div class="row fill-width">
<section class="categories">
@@ -280,7 +280,7 @@ module.exports=(()=>{var __webpack_modules__={6677:(e,t,r)=>{"use strict";r.r(t)
</div>
</foreignObject>
</svg>`,i=`/* SVG global context */
</svg>`,o=`/* SVG global context */
svg {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-size: 14px;
@@ -468,7 +468,7 @@ module.exports=(()=>{var __webpack_modules__={6677:(e,t,r)=>{"use strict";r.r(t)
to {
opacity: 1;
}
}`,a=`query Metrics {
}`,s=`query Metrics {
user(login: \$login) {
name
login
@@ -564,4 +564,4 @@ module.exports=(()=>{var __webpack_modules__={6677:(e,t,r)=>{"use strict";r.r(t)
}
}
}
`;console.log(`Templates | loaded`);const[c,u,l,p]=[e.getInput("token"),e.getInput("user"),e.getInput("filename",{default:"github-metrics.svg"}),e.getInput("debug",{default:false})];const d=e.getInput("committer_token",{default:null})||c;const f=o().join(l);console.log(`GitHub user | ${u}`);console.log(`Output file | ${f}`);console.log(`Github token | ${c?"provided":"missing"}`);if(!c)throw new Error("You must provide a valid GitHub token");const m=r.graphql.defaults({headers:{authorization:`token ${c}`}});const h=t.getOctokit(c);if(!p)console.debug=(()=>null);console.log(`Debug mode | ${p?"enabled":"disabled"}`);const g={lines:{enabled:e.getInput("plugin_lines",{default:false})},traffic:{enabled:e.getInput("plugin_traffic",{default:false})},pagespeed:{enabled:e.getInput("plugin_pagespeed",{default:false})}};if(e.getInput("pagespeed_token")){console.log(`Pagespeed token | provided`);g.pagespeed.token=e.getInput("pagespeed_token")}const b=Object.fromEntries(Object.entries(g).filter(([e,t])=>t.enabled).map(([e])=>[e,true]));console.log(`Plugins enabled | ${Object.entries(g).filter(([e,t])=>t.enabled).map(([e])=>e).join(", ")}`);const w=await s({login:u,q:b},{template:n,style:i,query:a,graphql:m,rest:h,plugins:g});console.log(`Render | complete`);{const e=t.getOctokit(d);console.log(`Committer | ${(await e.users.getAuthenticated()).data.login}`);let r=undefined;try{const{data:t}=await e.repos.getContent({owner:u,repo:u,path:l});r=t.sha}catch(e){}console.log(`Previous render sha | ${r||"none"}`);await e.repos.createOrUpdateFileContents({owner:u,repo:u,path:l,sha:r,message:`Update ${l} - [Skip GitHub Action]`,content:Buffer.from(w).toString("base64")});console.log(`Commit to repo | ok`)}console.log(`Success !`)}catch(t){console.error(t);e.setFailed(t.message);process.exit(1)}})().catch(e=>process.exit(1))},2541:function(e,t,r){"use strict";var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(Object.hasOwnProperty.call(e,r))t[r]=e[r];t["default"]=e;return t};Object.defineProperty(t,"__esModule",{value:true});const o=s(r(2087));const n=r(4332);function issueCommand(e,t,r){const s=new Command(e,t,r);process.stdout.write(s.toString()+o.EOL)}t.issueCommand=issueCommand;function issue(e,t=""){issueCommand(e,{},t)}t.issue=issue;const i="::";class Command{constructor(e,t,r){if(!e){e="missing.command"}this.command=e;this.properties=t;this.message=r}toString(){let e=i+this.command;if(this.properties&&Object.keys(this.properties).length>0){e+=" ";let t=true;for(const r in this.properties){if(this.properties.hasOwnProperty(r)){const s=this.properties[r];if(s){if(t){t=false}else{e+=","}e+=`${r}=${escapeProperty(s)}`}}}}e+=`${i}${escapeData(this.message)}`;return e}}function escapeData(e){return n.toCommandValue(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return n.toCommandValue(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A").replace(/:/g,"%3A").replace(/,/g,"%2C")}},2882:function(e,t,r){"use strict";var s=this&&this.__awaiter||function(e,t,r,s){function adopt(e){return e instanceof r?e:new r(function(t){t(e)})}return new(r||(r=Promise))(function(r,o){function fulfilled(e){try{step(s.next(e))}catch(e){o(e)}}function rejected(e){try{step(s["throw"](e))}catch(e){o(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((s=s.apply(e,t||[])).next())})};var o=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(Object.hasOwnProperty.call(e,r))t[r]=e[r];t["default"]=e;return t};Object.defineProperty(t,"__esModule",{value:true});const n=r(2541);const i=r(9582);const a=r(4332);const c=o(r(2087));const u=o(r(5622));var l;(function(e){e[e["Success"]=0]="Success";e[e["Failure"]=1]="Failure"})(l=t.ExitCode||(t.ExitCode={}));function exportVariable(e,t){const r=a.toCommandValue(t);process.env[e]=r;const s=process.env["GITHUB_ENV"]||"";if(s){const t="_GitHubActionsFileCommandDelimeter_";const s=`${e}<<${t}${c.EOL}${r}${c.EOL}${t}`;i.issueCommand("ENV",s)}else{n.issueCommand("set-env",{name:e},r)}}t.exportVariable=exportVariable;function setSecret(e){n.issueCommand("add-mask",{},e)}t.setSecret=setSecret;function addPath(e){const t=process.env["GITHUB_PATH"]||"";if(t){i.issueCommand("PATH",e)}else{n.issueCommand("add-path",{},e)}process.env["PATH"]=`${e}${u.delimiter}${process.env["PATH"]}`}t.addPath=addPath;function getInput(e,t){const r=process.env[`INPUT_${e.replace(/ /g,"_").toUpperCase()}`]||"";if(t&&t.required&&!r){throw new Error(`Input required and not supplied: ${e}`)}return r.trim()}t.getInput=getInput;function setOutput(e,t){n.issueCommand("set-output",{name:e},t)}t.setOutput=setOutput;function setCommandEcho(e){n.issue("echo",e?"on":"off")}t.setCommandEcho=setCommandEcho;function setFailed(e){process.exitCode=l.Failure;error(e)}t.setFailed=setFailed;functio Line truncated
`;console.log(`Templates | loaded`);const[l,c,u,d]=[e.getInput("token"),e.getInput("user"),e.getInput("filename",{default:"github-metrics.svg"}),e.getInput("debug",{default:false})];const p=e.getInput("committer_token",{default:null})||l;const m=i().join(u);console.log(`GitHub user | ${c}`);console.log(`Output file | ${m}`);console.log(`Github token | ${l?"provided":"missing"}`);if(!l)throw new Error("You must provide a valid GitHub token");const f=r.graphql.defaults({headers:{authorization:`token ${l}`}});const h=t.getOctokit(l);if(!d)console.debug=(()=>null);console.log(`Debug mode | ${d?"enabled":"disabled"}`);const g={lines:{enabled:e.getInput("plugin_lines",{default:false})},traffic:{enabled:e.getInput("plugin_traffic",{default:false})},pagespeed:{enabled:e.getInput("plugin_pagespeed",{default:false})}};if(e.getInput("pagespeed_token")){console.log(`Pagespeed token | provided`);g.pagespeed.token=e.getInput("pagespeed_token")}const y=Object.fromEntries(Object.entries(g).filter(([e,t])=>t.enabled).map(([e])=>[e,true]));console.log(`Plugins enabled | ${Object.entries(g).filter(([e,t])=>t.enabled).map(([e])=>e).join(", ")}`);const v=await n({login:c,q:y},{template:a,style:o,query:s,graphql:f,rest:h,plugins:g});console.log(`Render | complete`);{const e=t.getOctokit(p);console.log(`Committer | ${(await e.users.getAuthenticated()).data.login}`);let r=undefined;try{const{data:t}=await e.repos.getContent({owner:c,repo:c,path:u});r=t.sha}catch(e){}console.log(`Previous render sha | ${r||"none"}`);await e.repos.createOrUpdateFileContents({owner:c,repo:c,path:u,sha:r,message:`Update ${u} - [Skip GitHub Action]`,content:Buffer.from(v).toString("base64")});console.log(`Commit to repo | ok`)}console.log(`Success !`)}catch(t){console.error(t);e.setFailed(t.message);process.exit(1)}})().catch(e=>process.exit(1))},12541:function(e,t,r){"use strict";var n=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(Object.hasOwnProperty.call(e,r))t[r]=e[r];t["default"]=e;return t};Object.defineProperty(t,"__esModule",{value:true});const i=n(r(12087));const a=r(74332);function issueCommand(e,t,r){const n=new Command(e,t,r);process.stdout.write(n.toString()+i.EOL)}t.issueCommand=issueCommand;function issue(e,t=""){issueCommand(e,{},t)}t.issue=issue;const o="::";class Command{constructor(e,t,r){if(!e){e="missing.command"}this.command=e;this.properties=t;this.message=r}toString(){let e=o+this.command;if(this.properties&&Object.keys(this.properties).length>0){e+=" ";let t=true;for(const r in this.properties){if(this.properties.hasOwnProperty(r)){const n=this.properties[r];if(n){if(t){t=false}else{e+=","}e+=`${r}=${escapeProperty(n)}`}}}}e+=`${o}${escapeData(this.message)}`;return e}}function escapeData(e){return a.toCommandValue(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return a.toCommandValue(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A").replace(/:/g,"%3A").replace(/,/g,"%2C")}},32882:function(e,t,r){"use strict";var n=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r(function(t){t(e)})}return new(r||(r=Promise))(function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())})};var i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(Object.hasOwnProperty.call(e,r))t[r]=e[r];t["default"]=e;return t};Object.defineProperty(t,"__esModule",{value:true});const a=r(12541);const o=r(29582);const s=r(74332);const l=i(r(12087));const c=i(r(85622));var u;(function(e){e[e["Success"]=0]="Success";e[e["Failure"]=1]="Failure"})(u=t.ExitCode||(t.ExitCode={}));function exportVariable(e,t){const r=s.toCommandValue(t);process.env[e]=r;const n=process.env["GITHUB_ENV"]||"";if(n){const t="_GitHubActionsFileCommandDelimeter_";const n=`${e}<<${t}${l.EOL}${r}${l.EOL}${t}`;o.issueCommand("ENV",n)}else{a.issueCommand("set-env",{name:e},r)}}t.exportVariable=exportVariable;function setSecret(e){a.issueCommand("add-mask",{},e)}t.setSecret=setSecret;function addPath(e){const t=process.env["GITHUB_PATH"]||"";if(t){o.issueCommand("PATH",e)}else{a.issueCommand("add-path",{},e)}process.env["PATH"]=`${e}${c.delimiter}${process.env["PATH"]}`}t.addPath=addPath;function getInput(e,t){const r=process.env[`INPUT_${e.replace(/ /g,"_").toUpperCase()}`]||"";if(t&&t.required&&!r){throw new Error(`Input required and not supplied: ${e}`)}return r.trim()}t.getInput=getInput;function setOutput(e,t){a.issueCommand("set-output",{name:e},t)}t.setOutput=setOutput;function setCommandEcho(e){a.issue("echo",e?"on":"off")}t.setCommandEcho=setCommandEcho;function setFailed(e){process.exitCode=u.Failure;error(e)}t.setFailed=setFaile Line truncated
+976 -4
View File
File diff suppressed because it is too large. Load diff
+8 -5
View File
@@ -1,13 +1,13 @@
{
"name": "metrics",
"version": "1.5.0",
"version": "1.6.0",
"description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else",
"main": "index.mjs",
"scripts": {
"start": "node index.mjs",
"build": "node utils/build.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest @vercel/ncc@latest"
"test": "node tests/metrics.mjs",
"upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest compression@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest svgo@latest @vercel/ncc@latest libxmljs@latest"
},
"repository": {
"type": "git",
@@ -25,12 +25,15 @@
"@octokit/graphql": "^4.5.6",
"@octokit/rest": "^18.0.6",
"axios": "^0.20.0",
"compression": "^1.7.4",
"express": "^4.17.1",
"express-rate-limit": "^5.1.3",
"image-to-base64": "^2.1.1",
"memory-cache": "^0.2.0"
"memory-cache": "^0.2.0",
"svgo": "^1.3.2"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.24.1",
"libxmljs": "^0.19.7"
}
}
+2
View File
@@ -7,6 +7,7 @@
import cache from "memory-cache"
import ratelimit from "express-rate-limit"
import metrics from "./metrics.mjs"
import compression from "compression"
//Load svg template, style and query
async function load() {
@@ -28,6 +29,7 @@
//Setup server
const app = express()
app.use(compression())
const middlewares = []
//Rate limiter middleware
if (ratelimiter) {
+8 -5
View File
@@ -5,9 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="A SVG image generator which includes activity, community and repositories metrics about your GitHub account that you can includes on your profile">
<meta name="author" content="lowlighter">
<link rel="icon" href="data:,">
</head>
<body>
<h1><a href="https://github.com/lowlighter/metrics">GitHub metrics</a></h1>
<label>
@@ -24,9 +25,11 @@
<br>
<div class="code">
![<span class="md-alt">GitHub metrics</span>](https://metrics.lecoq.io/my-github-user)
</div>
</div>
<br>
And for even more metrics, setup this <a href="https://github.com/marketplace/actions/github-metrics-as-svg-image">GitHub action</a> on your repository !
</aside>
<script>
window.onload = function () {
//User updater
@@ -40,11 +43,11 @@
document.querySelector("#metrics .generated").style.opacity = 0
document.querySelector("aside").style.opacity = 0
//Update github user
document.querySelector(".code").innerText = `![GitHub metrics](https://metrics.lecoq.io/${user})`
document.querySelector(".code").innerHTML = `![<span class="md-alt">GitHub metrics</span>](https://metrics.lecoq.io/${user})`
document.querySelector("#user-repo").href = `https://github.com/${user}/${user}`
document.querySelectorAll(".user").forEach(node => node.innerText = user)
//Update metrics
if (event.key === "Enter")
if (event.key === "Enter")
metrics(user)
else
timeout = setTimeout(() => metrics(user), 2000)
+13 -2
View File
@@ -1,5 +1,6 @@
//Imports
import imgb64 from "image-to-base64"
import SVGO from "svgo"
import Plugins from "./plugins/index.mjs"
//Setup
@@ -78,9 +79,19 @@
//Wait for pending promises
await Promise.all(pending)
//Eval rendering and return
//Eval rendering
console.debug(`metrics/metrics/${login} > computed`)
return eval(`\`${template}\``)
const templated = eval(`\`${template}\``)
console.debug(`metrics/metrics/${login} > templated`)
//Optimize rendering
const svgo = new SVGO({plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
const {data:optimized} = await svgo.optimize(templated)
console.debug(`metrics/metrics/${login} > optimized`)
//Result
const rendered = optimized
return rendered
}
//Internal error
catch (error) { throw (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")) ? new Error("user not found") : error) }
+1 -1
View File
@@ -30,7 +30,7 @@
computed.plugins.traffic = {views}
console.debug(`metrics/plugins/traffic/${login} > ${JSON.stringify(computed.plugins.traffic)}`)
solve()
}
}
catch (error) {
//Thrown when token has unsufficient permissions
if (error.status === 403) {
+1 -1
View File
@@ -255,7 +255,7 @@
</div>
</section>
</div>
` : `
` : `
<section>
<div class="row fill-width">
<section class="categories">

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

+26
View File
@@ -0,0 +1,26 @@
//Imports
import path from "path"
import fs from "fs"
import metrics from "../src/metrics.mjs"
import octokit from "@octokit/graphql"
import OctokitRest from "@octokit/rest"
import libxmljs from "libxmljs"
//Die on unhandled rejections
process.on("unhandledRejection", error => { throw error })
//Load GitHub handlers
const token = process.argv.slice(2)[0] ?? ""
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
const rest = new OctokitRest.Octokit({auth:token})
//Load svg template, style and query
const [template, style, query] = await Promise.all(["template.svg", "style.css", "query.graphql"].map(async file => `${await fs.promises.readFile(path.join("src", file))}`))
//Compute render
const rendered = await metrics({login:"lowlighter", q:{}}, {template, style, query, graphql, rest, plugins:{}})
//Ensure it's a well-formed SVG image
const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)