Add support for lines and traffic plugins

This commit is contained in:
lowlighter
2020-09-17 21:17:04 +02:00
parent 6710a1be6e
commit 2493e29ebc
15 changed files with 322 additions and 26 deletions

View File

@@ -15,7 +15,7 @@ A GitHub Action which is run periodically at your convenience which generates an
Assuming your username is `my-github-user`, you can embed your metrics in your personal repository's readme like below : Assuming your username is `my-github-user`, you can embed your metrics in your personal repository's readme like below :
```markdown ```markdown
![GitHub metrics](https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg) ![GitHub metrics](https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg)
# Or with a redirection : # Or with a redirection :
[![GitHub metrics](https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg)](https://github.com/my-github-user/my-github-user) [![GitHub metrics](https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg)](https://github.com/my-github-user/my-github-user)
``` ```
```html ```html
@@ -64,6 +64,10 @@ jobs:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
# Your GitHub user name # Your GitHub user name
user: my-github-user user: my-github-user
# If you own a website and you added it to your GitHub profile,
# You can provide a PageSpeed token to add your site's performance results on the metrics SVG image
# See https://developers.google.com/speed/docs/insights/v5/get-started to obtain a key
# pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }}
``` ```
On each run, a new SVG image will be generated and committed to your repository. On each run, a new SVG image will be generated and committed to your repository.
@@ -88,7 +92,7 @@ For conveniency, you can use the shared instance available at [metrics.lecoq.io]
Assuming your username is `my-github-user`, you can embed your metrics in your personal repository's readme like below : Assuming your username is `my-github-user`, you can embed your metrics in your personal repository's readme like below :
```markdown ```markdown
![GitHub metrics](https://metrics.lecoq.io/my-github-user) ![GitHub metrics](https://metrics.lecoq.io/my-github-user)
# Or with a redirection : # Or with a redirection :
[![GitHub metrics](https://metrics.lecoq.io/my-github-user)](https://github.com/my-github-user/my-github-user) [![GitHub metrics](https://metrics.lecoq.io/my-github-user)](https://github.com/my-github-user/my-github-user)
``` ```
@@ -99,6 +103,7 @@ Since GitHub API has rate limitations and to avoid abuse, the shared instance ha
* Images are cached for 1 day (meaning that your metrics won't be updated until the next day) * Images are cached for 1 day (meaning that your metrics won't be updated until the next day)
* A maximum of 1000 users can use this service * A maximum of 1000 users can use this service
* You're limited to 3 requests per hour (cached metrics are not counted) * You're limited to 3 requests per hour (cached metrics are not counted)
* Plugins are not available
You should consider deploying your own instance or use GitHub Action if you're planning using this service. You should consider deploying your own instance or use GitHub Action if you're planning using this service.
@@ -197,6 +202,34 @@ Open and edit `settings.json` to configure your instance.
//This is intendend for easier development which allows to see your changes quickly //This is intendend for easier development which allows to see your changes quickly
//Defaults to false //Defaults to false
"debug":false, "debug":false,
//Plugins configuration
//Most of plugins are disabled by default
//Enabling them can add additional informations and metrics about you, but increases response time
"plugins":{
//Pagespeed plugin
"pagespeed":{
//Enable or disable this plugin
//When enabled, pass "?pagespeed=1" in url to generate website's performances
"enabled":false,
//Pagespeed token
//See https://developers.google.com/speed/docs/insights/v5/get-started to obtain a key
"token":"****************************************"
},
//Lines plugin
"lines":{
//Enable or disable this plugin
//When enabled, pass "?lines=1" in url to compute total lines added/removed in your repositories by you
"enabled":true
},
//Traffic plugin
"traffic":{
//Enable or disable this plugin
//When enabled, pass "?traffic=1" in url to compute total page views in your repositories in last two weeks
//Note that this requires that the passed GitHub API token requires a push access
"enabled":true
}
}
} }
``` ```
@@ -289,6 +322,7 @@ Below is a list of useful documentations links :
* [GitHub GraphQL API](https://docs.github.com/en/graphql) * [GitHub GraphQL API](https://docs.github.com/en/graphql)
* [GitHub GraphQL Explorer](https://developer.github.com/v4/explorer/) * [GitHub GraphQL Explorer](https://developer.github.com/v4/explorer/)
* [GitHub Rest API](https://docs.github.com/en/rest)
## 📦 Used packages ## 📦 Used packages

View File

@@ -15,7 +15,10 @@ inputs:
description: Name of SVG image output description: Name of SVG image output
default: github-metrics.svg default: github-metrics.svg
pagespeed_token: pagespeed_token:
description: Pagespeed Personal Token (optional, will generate user's website performances if provided). See https://developers.google.com/speed/docs/insights/v5/get-started for more information. description: Pagespeed Personal Token (optional, see https://developers.google.com/speed/docs/insights/v5/get-started for more information)
plugins:
description: List of additional plugins to enabled. Supported values are "lines", "pagespeed" (requires "pagespeed_token") and "traffic" (require "token" with "repository" permissions)
default: []
runs: runs:
using: node12 using: node12
main: action/dist/index.js main: action/dist/index.js

122
action/dist/index.js vendored
View File

@@ -142,6 +142,12 @@ __webpack_require__.r(__webpack_exports__);
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>
\${data.user.packages.totalCount} Package\${data.user.packages.totalCount > 1 ? "s" : ""} \${data.user.packages.totalCount} Package\${data.user.packages.totalCount > 1 ? "s" : ""}
</div> </div>
\${computed.plugins.lines ? \`
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>
\${computed.plugins.lines.added} added, \${computed.plugins.lines.deleted} removed
</div>\` : ""
}
</section> </section>
<section> <section>
@@ -153,6 +159,12 @@ __webpack_require__.r(__webpack_exports__);
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
\${data.computed.repositories.watchers} Watcher\${data.computed.repositories.watchers > 1 ? "s" : ""} \${data.computed.repositories.watchers} Watcher\${data.computed.repositories.watchers > 1 ? "s" : ""}
</div> </div>
\${computed.plugins.traffic ? \`
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 1.75A.75.75 0 01.75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0111.006 1h4.245a.75.75 0 01.75.75v10.5a.75.75 0 01-.75.75h-4.507a2.25 2.25 0 00-1.591.659l-.622.621a.75.75 0 01-1.06 0l-.622-.621A2.25 2.25 0 005.258 13H.75a.75.75 0 01-.75-.75V1.75zm8.755 3a2.25 2.25 0 012.25-2.25H14.5v9h-3.757c-.71 0-1.4.201-1.992.572l.004-7.322zm-1.504 7.324l.004-5.073-.002-2.253A2.25 2.25 0 005.003 2.5H1.5v9h3.757a3.75 3.75 0 011.994.574z"></path></svg>
\${computed.plugins.traffic.views.count} views in last two weeks
</div>\` : ""
}
</section> </section>
</div> </div>
</section> </section>
@@ -440,6 +452,7 @@ __webpack_require__.r(__webpack_exports__);
repositories(last: 100, isFork: false, ownerAffiliations: OWNER) { repositories(last: 100, isFork: false, ownerAffiliations: OWNER) {
totalCount totalCount
nodes { nodes {
name
watchers { watchers {
totalCount totalCount
} }
@@ -528,11 +541,13 @@ __webpack_require__.r(__webpack_exports__);
const rest = github.getOctokit(token) const rest = github.getOctokit(token)
//Additional plugins //Additional plugins
const plugins = {}, q = {} const enabled = new Set(core.getInput("plugins", {default:[]}))
const plugins = {lines:{enabled:enabled.has("lines")}, traffic:{enabled:enabled.has("traffic")}, pagespeed:{enabled:enabled.has("pagespeed")}}
if (core.getInput("pagespeed_token")) { if (core.getInput("pagespeed_token")) {
plugins.pagespeed = {enabled:true, token:core.getInput("pagespeed_token")} console.log(`Pagespeed token | provided`)
q.pagespeed = true plugins.pagespeed.token = core.getInput("pagespeed_token")
} }
const q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true]))
//Render metrics //Render metrics
const rendered = await metrics({login:user, q}, {template, style, query, graphql, plugins}) const rendered = await metrics({login:user, q}, {template, style, query, graphql, plugins})
@@ -9656,17 +9671,18 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ }); /* harmony export */ });
/* harmony import */ var image_to_base64__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7192); /* harmony import */ var image_to_base64__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7192);
/* harmony import */ var image_to_base64__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(image_to_base64__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var image_to_base64__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(image_to_base64__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _plugins_index_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9859); /* harmony import */ var _plugins_index_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5820);
//Imports //Imports
//Setup //Setup
async function metrics({login, q}, {template, style, query, graphql, plugins}) { async function metrics({login, q}, {template, style, query, graphql, rest, plugins}) {
//Compute rendering //Compute rendering
try { try {
//Query data from GitHub API //Query data from GitHub API
console.debug(`metrics/metrics/${login} > query`)
const data = await graphql(query const data = await graphql(query
.replace(/[$]login/, `"${login}"`) .replace(/[$]login/, `"${login}"`)
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`) .replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
@@ -9682,6 +9698,8 @@ __webpack_require__.r(__webpack_exports__);
//Plugins //Plugins
if (data.user.websiteUrl) if (data.user.websiteUrl)
_plugins_index_mjs__WEBPACK_IMPORTED_MODULE_1__/* .default.pagespeed */ .Z.pagespeed({url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed) _plugins_index_mjs__WEBPACK_IMPORTED_MODULE_1__/* .default.pagespeed */ .Z.pagespeed({url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed)
_plugins_index_mjs__WEBPACK_IMPORTED_MODULE_1__/* .default.lines */ .Z.lines({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.lines)
_plugins_index_mjs__WEBPACK_IMPORTED_MODULE_1__/* .default.traffic */ .Z.traffic({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.traffic)
//Iterate through user's repositories //Iterate through user's repositories
for (const repository of data.user.repositories.nodes) { for (const repository of data.user.repositories.nodes) {
@@ -9728,6 +9746,7 @@ __webpack_require__.r(__webpack_exports__);
await Promise.all(pending) await Promise.all(pending)
//Eval rendering and return //Eval rendering and return
console.debug(`metrics/metrics/${login} > computed`)
return eval(`\`${template}\``) return eval(`\`${template}\``)
} }
//Internal error //Internal error
@@ -9736,7 +9755,7 @@ __webpack_require__.r(__webpack_exports__);
/***/ }), /***/ }),
/***/ 9859: /***/ 5820:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict"; "use strict";
@@ -9746,6 +9765,51 @@ __webpack_require__.d(__webpack_exports__, {
"Z": () => /* default */ E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_index "Z": () => /* default */ E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_index
}); });
// CONCATENATED MODULE: E:\Users\lecoq\Documents\GitHub\gitstats\src\plugins\lines\index.mjs
//Formatter
function format(n) {
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}])
if (n/v >= 1)
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
return n
}
//Setup
/* harmony default export */ function E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_lines_index({login, repositories = [], rest, computed, pending, q}, {enabled = false} = {}) {
//Check if plugin is enabled and requirements are met
if (!enabled)
return computed.plugins.lines = null
if (!q.lines)
return computed.plugins.lines = null
console.debug(`metrics/plugins/lines/${login} > started`)
//Plugin execution
pending.push(new Promise(async solve => {
//Get contributors stats from repositories
const lines = {added:0, deleted:0}
const response = await Promise.all(repositories.map(async repo => await rest.repos.getContributorsStats({owner:login, repo})))
//Compute changed lines
response.map(({data:repository}) => {
//Check if data are available
if (!repository)
return
//Extract author
const [contributor] = repository.filter(({author}) => author.login === login)
//Compute editions
if (contributor)
contributor.weeks.forEach(({a, d}) => (lines.added += a, lines.deleted += d))
})
//Format values
lines.added = format(lines.added)
lines.deleted = format(lines.deleted)
//Save results
computed.plugins = {lines}
console.debug(`metrics/plugins/lines/${login} > ${JSON.stringify(computed.plugins.lines)}`)
solve()
}))
}
// EXTERNAL MODULE: E:\Users\lecoq\Documents\GitHub\gitstats\node_modules\axios\index.js // EXTERNAL MODULE: E:\Users\lecoq\Documents\GitHub\gitstats\node_modules\axios\index.js
var E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index = __webpack_require__(2390); var E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index = __webpack_require__(2390);
var E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index_default = /*#__PURE__*/__webpack_require__.n(E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index); var E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index_default = /*#__PURE__*/__webpack_require__.n(E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index);
@@ -9755,14 +9819,17 @@ var E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index_default = /
//Setup //Setup
/* harmony default export */ function E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_pagespeed_index({url, computed, pending, q}, {enabled = false, token = null} = {}) { /* harmony default export */ function E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_pagespeed_index({login, url, computed, pending, q}, {enabled = false, token = null} = {}) {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
if (!enabled) if (!enabled)
return computed.plugins.pagespeed = null return computed.plugins.pagespeed = null
if (!token)
return computed.plugins.pagespeed = null
if (!url) if (!url)
return computed.plugins.pagespeed = null return computed.plugins.pagespeed = null
if (!q.pagespeed) if (!q.pagespeed)
return computed.plugins.pagespeed = null return computed.plugins.pagespeed = null
console.debug(`metrics/plugins/pagespeed/${login} > started`)
//Plugin execution //Plugin execution
pending.push(new Promise(async solve => { pending.push(new Promise(async solve => {
@@ -9777,16 +9844,55 @@ var E_Users_lecoq_Documents_GitHub_gitstats_node_modules_axios_index_default = /
})) }))
//Save results //Save results
computed.plugins.pagespeed = {url, scores:[scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]} computed.plugins.pagespeed = {url, scores:[scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]}
console.debug(`metrics/plugins/pagespeed/${login} > ${JSON.stringify(computed.plugins.pagespeed)}`)
solve()
}))
}
// CONCATENATED MODULE: E:\Users\lecoq\Documents\GitHub\gitstats\src\plugins\traffic\index.mjs
//Formatter
function E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_traffic_index_format(n) {
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}])
if (n/v >= 1)
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
return n
}
//Setup
/* harmony default export */ function E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_traffic_index({login, repositories = [], rest, computed, pending, q}, {enabled = false} = {}) {
//Check if plugin is enabled and requirements are met
if (!enabled)
return computed.plugins.traffic = null
if (!q.traffic)
return computed.plugins.traffic = null
console.debug(`metrics/plugins/traffic/${login} > started`)
//Plugin execution
pending.push(new Promise(async solve => {
//Get views stats from repositories
const views = {count:0, uniques:0}
const response = await Promise.all(repositories.map(async repo => await rest.repos.getViews({owner:login, repo})))
//Compute views
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
//Format values
views.count = E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_traffic_index_format(views.count)
views.uniques = E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_traffic_index_format(views.uniques)
//Save results
computed.plugins.traffic = {views}
console.debug(`metrics/plugins/traffic/${login} > ${JSON.stringify(computed.plugins.traffic)}`)
solve() solve()
})) }))
} }
// CONCATENATED MODULE: E:\Users\lecoq\Documents\GitHub\gitstats\src\plugins\index.mjs // CONCATENATED MODULE: E:\Users\lecoq\Documents\GitHub\gitstats\src\plugins\index.mjs
//Imports //Imports
//Exports //Exports
/* harmony default export */ const E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_index = ({ /* harmony default export */ const E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_index = ({
pagespeed: E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_pagespeed_index lines: E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_lines_index,
pagespeed: E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_pagespeed_index,
traffic: E_Users_lecoq_Documents_GitHub_gitstats_src_plugins_traffic_index,
}); });
/***/ }), /***/ }),

View File

@@ -30,11 +30,13 @@
const rest = github.getOctokit(token) const rest = github.getOctokit(token)
//Additional plugins //Additional plugins
const plugins = {}, q = {} const enabled = new Set(core.getInput("plugins", {default:[]}))
const plugins = {lines:{enabled:enabled.has("lines")}, traffic:{enabled:enabled.has("traffic")}, pagespeed:{enabled:enabled.has("pagespeed")}}
if (core.getInput("pagespeed_token")) { if (core.getInput("pagespeed_token")) {
plugins.pagespeed = {enabled:true, token:core.getInput("pagespeed_token")} console.log(`Pagespeed token | provided`)
q.pagespeed = true plugins.pagespeed.token = core.getInput("pagespeed_token")
} }
const q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true]))
//Render metrics //Render metrics
const rendered = await metrics({login:user, q}, {template, style, query, graphql, plugins}) const rendered = await metrics({login:user, q}, {template, style, query, graphql, plugins})

35
package-lock.json generated
View File

@@ -77,6 +77,11 @@
"@octokit/types": "^5.3.0" "@octokit/types": "^5.3.0"
} }
}, },
"@octokit/plugin-request-log": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz",
"integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw=="
},
"@octokit/plugin-rest-endpoint-methods": { "@octokit/plugin-rest-endpoint-methods": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.1.4.tgz",
@@ -111,6 +116,36 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"@octokit/rest": {
"version": "18.0.6",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.6.tgz",
"integrity": "sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w==",
"requires": {
"@octokit/core": "^3.0.0",
"@octokit/plugin-paginate-rest": "^2.2.0",
"@octokit/plugin-request-log": "^1.0.0",
"@octokit/plugin-rest-endpoint-methods": "4.2.0"
},
"dependencies": {
"@octokit/plugin-rest-endpoint-methods": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz",
"integrity": "sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw==",
"requires": {
"@octokit/types": "^5.5.0",
"deprecation": "^2.3.1"
}
},
"@octokit/types": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz",
"integrity": "sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ==",
"requires": {
"@types/node": ">= 8"
}
}
}
},
"@octokit/types": { "@octokit/types": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.4.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.4.1.tgz",

View File

@@ -24,6 +24,7 @@
"@actions/core": "^1.2.5", "@actions/core": "^1.2.5",
"@actions/github": "^4.0.0", "@actions/github": "^4.0.0",
"@octokit/graphql": "^4.5.4", "@octokit/graphql": "^4.5.4",
"@octokit/rest": "^18.0.6",
"axios": "^0.20.0", "axios": "^0.20.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",

View File

@@ -6,10 +6,17 @@
"ratelimiter":null, "//":"Rate limiter (see express-rate-limit documentation for options)", "ratelimiter":null, "//":"Rate limiter (see express-rate-limit documentation for options)",
"port":3000, "//":"Listening port", "port":3000, "//":"Listening port",
"debug":false, "//":"Debug mode", "debug":false, "//":"Debug mode",
"plugins":{ "//":"Additional plugins (optional)", "plugins":{ "//":"Additional plugins (optional)",
"pagespeed":{ "//":"Pagespeed configuration", "pagespeed":{ "//":"Pagespeed plugin",
"enabled":false, "//":"Enable or disable Pagespeed metrics", "enabled":false, "//":"Enable or disable Pagespeed metrics",
"token":"******", "//":"Pagespeed token" "token":"******", "//":"Pagespeed token"
},
"traffic":{ "//":"Traffic plugin (GitHub API token must be RW for this to work)",
"enabled":true, "//":"Enable or disable repositories total page views is last two weeks"
},
"lines":{ "//":"Lines plugin",
"enabled":true, "//":"Enable or disabled repositories total lines added/removed"
} }
} }
} }

View File

@@ -3,6 +3,7 @@
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import octokit from "@octokit/graphql" import octokit from "@octokit/graphql"
import OctokitRest from "@octokit/rest"
import cache from "memory-cache" import cache from "memory-cache"
import ratelimit from "express-rate-limit" import ratelimit from "express-rate-limit"
import metrics from "./metrics.mjs" import metrics from "./metrics.mjs"
@@ -19,10 +20,11 @@
const settings = JSON.parse((await fs.promises.readFile(path.join("settings.json"))).toString()) const settings = JSON.parse((await fs.promises.readFile(path.join("settings.json"))).toString())
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = settings const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = settings
if (debug) if (debug)
console.log(settings) console.debug(settings)
//Load svg template, style and query //Load svg template, style and query
let [template, style, query] = await load() let [template, style, query] = await load()
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}}) const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
const rest = new OctokitRest.Octokit({auth:token})
//Setup server //Setup server
const app = express() const app = express()
@@ -51,8 +53,10 @@
//Request params //Request params
const {login} = req.params const {login} = req.params
if ((restricted.length)&&(!restricted.includes(login))) if ((restricted.length)&&(!restricted.includes(login))) {
console.debug(`metrics/app/${login} > 403 (not in whitelisted users)`)
return res.sendStatus(403) return res.sendStatus(403)
}
//Read cached data if possible //Read cached data if possible
if ((!debug)&&(cached)&&(cache.get(login))) { if ((!debug)&&(cached)&&(cache.get(login))) {
@@ -61,15 +65,17 @@
return return
} }
//Maximum simultaneous users //Maximum simultaneous users
if ((maxusers)&&(cache.size()+1 > maxusers)) if ((maxusers)&&(cache.size()+1 > maxusers)) {
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
return res.sendStatus(503) return res.sendStatus(503)
}
//Compute rendering //Compute rendering
try { try {
//Render //Render
if (debug) if (debug)
[template, style, query] = await load() [template, style, query] = await load()
const rendered = await metrics({login, q:req.query}, {template, style, query, graphql, plugins}) const rendered = await metrics({login, q:req.query}, {template, style, query, graphql, rest, plugins})
//Cache //Cache
if ((!debug)&&(cached)) if ((!debug)&&(cached))
cache.put(login, rendered, cached) cache.put(login, rendered, cached)
@@ -80,8 +86,10 @@
//Internal error //Internal error
catch (error) { catch (error) {
//Not found user //Not found user
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
console.debug(`metrics/app/${login} > 404 (user not found)`)
return res.sendStatus(404) return res.sendStatus(404)
}
//General error //General error
console.error(error) console.error(error)
res.sendStatus(500) res.sendStatus(500)
@@ -95,6 +103,7 @@
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`, `Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time | ${cached} seconds`, `Cached time | ${cached} seconds`,
`Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`, `Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}` `Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled | ${Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).join(", ")}`
].join("\n"))) ].join("\n")))
} }

View File

@@ -3,11 +3,12 @@
import Plugins from "./plugins/index.mjs" import Plugins from "./plugins/index.mjs"
//Setup //Setup
export default async function metrics({login, q}, {template, style, query, graphql, plugins}) { export default async function metrics({login, q}, {template, style, query, graphql, rest, plugins}) {
//Compute rendering //Compute rendering
try { try {
//Query data from GitHub API //Query data from GitHub API
console.debug(`metrics/metrics/${login} > query`)
const data = await graphql(query const data = await graphql(query
.replace(/[$]login/, `"${login}"`) .replace(/[$]login/, `"${login}"`)
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`) .replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
@@ -23,6 +24,8 @@
//Plugins //Plugins
if (data.user.websiteUrl) if (data.user.websiteUrl)
Plugins.pagespeed({url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed) Plugins.pagespeed({url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed)
Plugins.lines({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.lines)
Plugins.traffic({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.traffic)
//Iterate through user's repositories //Iterate through user's repositories
for (const repository of data.user.repositories.nodes) { for (const repository of data.user.repositories.nodes) {
@@ -69,6 +72,7 @@
await Promise.all(pending) await Promise.all(pending)
//Eval rendering and return //Eval rendering and return
console.debug(`metrics/metrics/${login} > computed`)
return eval(`\`${template}\``) return eval(`\`${template}\``)
} }
//Internal error //Internal error

View File

@@ -1,7 +1,11 @@
//Imports //Imports
import lines from "./lines/index.mjs"
import pagespeed from "./pagespeed/index.mjs" import pagespeed from "./pagespeed/index.mjs"
import traffic from "./traffic/index.mjs"
//Exports //Exports
export default { export default {
pagespeed lines,
pagespeed,
traffic,
} }

View File

@@ -1 +1,43 @@
//Placeholder for https://docs.github.com/en/rest/reference/repos#get-all-contributor-commit-activity //Formatter
function format(n) {
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}])
if (n/v >= 1)
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
return n
}
//Setup
export default function ({login, repositories = [], rest, computed, pending, q}, {enabled = false} = {}) {
//Check if plugin is enabled and requirements are met
if (!enabled)
return computed.plugins.lines = null
if (!q.lines)
return computed.plugins.lines = null
console.debug(`metrics/plugins/lines/${login} > started`)
//Plugin execution
pending.push(new Promise(async solve => {
//Get contributors stats from repositories
const lines = {added:0, deleted:0}
const response = await Promise.all(repositories.map(async repo => await rest.repos.getContributorsStats({owner:login, repo})))
//Compute changed lines
response.map(({data:repository}) => {
//Check if data are available
if (!repository)
return
//Extract author
const [contributor] = repository.filter(({author}) => author.login === login)
//Compute editions
if (contributor)
contributor.weeks.forEach(({a, d}) => (lines.added += a, lines.deleted += d))
})
//Format values
lines.added = format(lines.added)
lines.deleted = format(lines.deleted)
//Save results
computed.plugins = {lines}
console.debug(`metrics/plugins/lines/${login} > ${JSON.stringify(computed.plugins.lines)}`)
solve()
}))
}

View File

@@ -2,14 +2,17 @@
import axios from "axios" import axios from "axios"
//Setup //Setup
export default function ({url, computed, pending, q}, {enabled = false, token = null} = {}) { export default function ({login, url, computed, pending, q}, {enabled = false, token = null} = {}) {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
if (!enabled) if (!enabled)
return computed.plugins.pagespeed = null return computed.plugins.pagespeed = null
if (!token)
return computed.plugins.pagespeed = null
if (!url) if (!url)
return computed.plugins.pagespeed = null return computed.plugins.pagespeed = null
if (!q.pagespeed) if (!q.pagespeed)
return computed.plugins.pagespeed = null return computed.plugins.pagespeed = null
console.debug(`metrics/plugins/pagespeed/${login} > started`)
//Plugin execution //Plugin execution
pending.push(new Promise(async solve => { pending.push(new Promise(async solve => {
@@ -24,6 +27,7 @@
})) }))
//Save results //Save results
computed.plugins.pagespeed = {url, scores:[scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]} computed.plugins.pagespeed = {url, scores:[scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]}
console.debug(`metrics/plugins/pagespeed/${login} > ${JSON.stringify(computed.plugins.pagespeed)}`)
solve() solve()
})) }))
} }

View File

@@ -1 +1,33 @@
//Placeholder for https://docs.github.com/en/rest/reference/repos#get-page-views //Formatter
function format(n) {
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}])
if (n/v >= 1)
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
return n
}
//Setup
export default function ({login, repositories = [], rest, computed, pending, q}, {enabled = false} = {}) {
//Check if plugin is enabled and requirements are met
if (!enabled)
return computed.plugins.traffic = null
if (!q.traffic)
return computed.plugins.traffic = null
console.debug(`metrics/plugins/traffic/${login} > started`)
//Plugin execution
pending.push(new Promise(async solve => {
//Get views stats from repositories
const views = {count:0, uniques:0}
const response = await Promise.all(repositories.map(async repo => await rest.repos.getViews({owner:login, repo})))
//Compute views
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
//Format values
views.count = format(views.count)
views.uniques = format(views.uniques)
//Save results
computed.plugins.traffic = {views}
console.debug(`metrics/plugins/traffic/${login} > ${JSON.stringify(computed.plugins.traffic)}`)
solve()
}))
}

View File

@@ -8,6 +8,7 @@ query Metrics {
repositories(last: 100, isFork: false, ownerAffiliations: OWNER) { repositories(last: 100, isFork: false, ownerAffiliations: OWNER) {
totalCount totalCount
nodes { nodes {
name
watchers { watchers {
totalCount totalCount
} }

View File

@@ -110,6 +110,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>
${data.user.packages.totalCount} Package${data.user.packages.totalCount > 1 ? "s" : ""} ${data.user.packages.totalCount} Package${data.user.packages.totalCount > 1 ? "s" : ""}
</div> </div>
${computed.plugins.lines ? `
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>
${computed.plugins.lines.added} added, ${computed.plugins.lines.deleted} removed
</div>` : ""
}
</section> </section>
<section> <section>
@@ -121,6 +127,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
${data.computed.repositories.watchers} Watcher${data.computed.repositories.watchers > 1 ? "s" : ""} ${data.computed.repositories.watchers} Watcher${data.computed.repositories.watchers > 1 ? "s" : ""}
</div> </div>
${computed.plugins.traffic ? `
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 1.75A.75.75 0 01.75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0111.006 1h4.245a.75.75 0 01.75.75v10.5a.75.75 0 01-.75.75h-4.507a2.25 2.25 0 00-1.591.659l-.622.621a.75.75 0 01-1.06 0l-.622-.621A2.25 2.25 0 005.258 13H.75a.75.75 0 01-.75-.75V1.75zm8.755 3a2.25 2.25 0 012.25-2.25H14.5v9h-3.757c-.71 0-1.4.201-1.992.572l.004-7.322zm-1.504 7.324l.004-5.073-.002-2.253A2.25 2.25 0 005.003 2.5H1.5v9h3.757a3.75 3.75 0 011.994.574z"></path></svg>
${computed.plugins.traffic.views.count} views in last two weeks
</div>` : ""
}
</section> </section>
</div> </div>
</section> </section>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB