Feat miscelleanous 1 (#28)

* Improve logs and better handling of plugins errors

* Add support for timezones

* Prepare next release
This commit is contained in:
Simon Lecoq
2020-12-27 22:30:53 +01:00
committed by GitHub
parent 619113295c
commit 016ab9aca1
24 changed files with 242 additions and 148 deletions

View File

@@ -213,14 +213,19 @@ The `README.md` of it will be displayed on your user profile :
From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token. From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token.
No additional scopes are needed, unless you want to include your private repositories metrics or if you want to use the `traffic` plugin. No additional scopes are needed, unless you want to include your private repositories metrics.
![Setup a GitHub personal token](.github/readme/imgs/setup_personal_token.png) ![Setup a GitHub personal token](.github/readme/imgs/setup_personal_token.png)
Be sure to enable `Include private contributions on my profile` in your account settings if you want to include your private contributions : With a scope-less token, you can still display private contributions by enabling `Include private contributions on my profile` in your account settings :
![Enable "Include private contributions on my profile`"](.github/readme/imgs/setup_private_contributions.png) ![Enable "Include private contributions on my profile`"](.github/readme/imgs/setup_private_contributions.png)
Some plugins also require additional scopes, which is indicated in their respective documentation.
In case your token does not have the required scope (and `plugins_errors_fatal` is not enabled), it will be directly notified in the plugin render like below :
![Plugin error example](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.error.svg)
### 2. Set your GitHub token in your personal repository secrets ### 2. Set your GitHub token in your personal repository secrets
Go to the `Settings` of your personal repository to create a new secret and paste your freshly generated GitHub token there. Go to the `Settings` of your personal repository to create a new secret and paste your freshly generated GitHub token there.
@@ -258,8 +263,6 @@ When using a token with additional permissions, it is advised to fork this repos
``` ```
In this case, consider watching new releases of this repository to stay up-to-date and enjoy latest features ! In this case, consider watching new releases of this repository to stay up-to-date and enjoy latest features !
If you prefer examples rather than theory, check out this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml) file which generates metrics daily.
#### Preview vs release #### Preview vs release
It is possible to use `@master` instead of `@latest` to use new features before their official release. It is possible to use `@master` instead of `@latest` to use new features before their official release.
@@ -271,6 +274,12 @@ A new metrics image will be generated and committed to your repository on each r
![Action update example](.github/readme/imgs/example_action_update.png) ![Action update example](.github/readme/imgs/example_action_update.png)
#### Workflow examples
Check out this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml) file which generates metrics daily.
Note that most of steps presented there are illustrative examples for this readme and are actually not needed to generate your own metrics.
### 4. Embed the link into your README.md ### 4. Embed the link into your README.md
Edit your repository readme and add your metrics image : Edit your repository readme and add your metrics image :
@@ -867,6 +876,8 @@ Add the following to your workflow :
### 🗂️ Projects ### 🗂️ Projects
⚠️ This plugin requires a personal token with public_repo scope.
The *projects* plugin displays the progress of your profile projects. The *projects* plugin displays the progress of your profile projects.
![Projects plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.projects.svg) ![Projects plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.projects.svg)
@@ -876,6 +887,8 @@ The *projects* plugin displays the progress of your profile projects.
It will consume an additional GitHub request. It will consume an additional GitHub request.
Because of GitHub REST API limitation, provided token requires `public_repo` scope to access projects informations.
Add the following to your workflow : Add the following to your workflow :
```yaml ```yaml
- uses: lowlighter/metrics@latest - uses: lowlighter/metrics@latest
@@ -926,7 +939,7 @@ Add the following to your workflow :
### 🧮 Traffic ### 🧮 Traffic
⚠️ This plugin requires a personal token with full repo scope. ⚠️ This plugin requires a personal token with repo scope.
The repositories *traffic* plugin displays the number of pages views across your repositories. The repositories *traffic* plugin displays the number of pages views across your repositories.
@@ -955,8 +968,6 @@ Add the following to your workflow :
### 🐤 Tweets ### 🐤 Tweets
🚧 This plugin is available as pre-release on @master
The recent *tweets* plugin displays your latest tweets of the [twitter](https://twitter.com) attached mentioned on your account : The recent *tweets* plugin displays your latest tweets of the [twitter](https://twitter.com) attached mentioned on your account :
![Tweets plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.tweets.svg) ![Tweets plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.tweets.svg)
@@ -1042,8 +1053,6 @@ Add the following to your workflow :
plugin_habits_days: 14 plugin_habits_days: 14
``` ```
🚧 The following feature is available as pre-release on @master
You can display charts in this section : You can display charts in this section :
![Habits plugin (charts)](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.habits.charts.svg) ![Habits plugin (charts)](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.habits.charts.svg)
@@ -1063,6 +1072,15 @@ Add the following to your workflow instead :
plugin_habits_charts: yes plugin_habits_charts: yes
``` ```
By default, dates are based on the Greenwich meridian (England time). In order to these metrics to be accurate, be sure to set your timezone (see [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of supported timezones) :
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
config_timezone: Europe/Paris
```
</details> </details>
### 🎫 Gists ### 🎫 Gists

View File

@@ -40,6 +40,13 @@ inputs:
description: Optimize SVG image description: Optimize SVG image
default: yes default: yes
# Set timezone used by metrics
# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# Some plugins will use this setting to calibrate dates
config_timezone:
description: Timezone used by metrics
default: ""
# Number of repositories to use for metrics # Number of repositories to use for metrics
# A high number increase metrics accuracy, but will consume additional API requests when using plugins # A high number increase metrics accuracy, but will consume additional API requests when using plugins
repositories: repositories:

24
action/dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -37,7 +37,7 @@
console.log(`Template to use | ${template}`) console.log(`Template to use | ${template}`)
//Token for data gathering //Token for data gathering
const token = core.getInput("token") const token = core.getInput("token") || ""
console.log(`Github token | ${token ? "provided" : "missing"}`) console.log(`Github token | ${token ? "provided" : "missing"}`)
if (!token) if (!token)
throw new Error("You must provide a valid GitHub token to gather your metrics") throw new Error("You must provide a valid GitHub token to gather your metrics")
@@ -71,7 +71,7 @@
if (!debug) if (!debug)
console.debug = message => debugged.push(message) console.debug = message => debugged.push(message)
console.log(`Debug mode | ${debug}`) console.log(`Debug mode | ${debug}`)
const dflags = (core.getInput("debug_flags") ?? "").split(" ").filter(flag => flag) const dflags = (core.getInput("debug_flags") || "").split(" ").filter(flag => flag)
console.log(`Debug flags | ${dflags.join(" ")}`) console.log(`Debug flags | ${dflags.join(" ")}`)
//Base elements //Base elements
@@ -81,6 +81,12 @@
base[`base.${part}`] = parts.includes(part) base[`base.${part}`] = parts.includes(part)
console.log(`Base parts | ${parts.join(", ") || "(none)"}`) console.log(`Base parts | ${parts.join(", ") || "(none)"}`)
//Config
const config = {
"config.timezone":core.getInput("config_timezone") || ""
}
console.log(`Timezone | ${config.timezone || "(none)"}`)
//Additional plugins //Additional plugins
const plugins = { const plugins = {
lines:{enabled:bool(core.getInput("plugin_lines"))}, lines:{enabled:bool(core.getInput("plugin_lines"))},
@@ -102,7 +108,7 @@
//Additional plugins options //Additional plugins options
//Pagespeed //Pagespeed
if (plugins.pagespeed.enabled) { if (plugins.pagespeed.enabled) {
plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || ""
q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`)) q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`))
console.log(`Pagespeed token | ${plugins.pagespeed.token ? "provided" : "missing"}`) console.log(`Pagespeed token | ${plugins.pagespeed.token ? "provided" : "missing"}`)
console.log(`Pagespeed detailed | ${q["pagespeed.detailed"]}`) console.log(`Pagespeed detailed | ${q["pagespeed.detailed"]}`)
@@ -145,7 +151,7 @@
} }
//Isocalendar //Isocalendar
if (plugins.isocalendar.enabled) { if (plugins.isocalendar.enabled) {
q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") ?? "half-year" q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") || "half-year"
console.log(`Isocalendar duration | ${q["isocalendar.duration"]}`) console.log(`Isocalendar duration | ${q["isocalendar.duration"]}`)
} }
//Topics //Topics
@@ -179,7 +185,7 @@
console.log(`Plugin errors | ${die ? "die" : "ignore"}`) console.log(`Plugin errors | ${die ? "die" : "ignore"}`)
//Built query //Built query
q = {...q, base:false, ...base, repositories, template} q = {...q, base:false, ...base, ...config, repositories, template}
//Render metrics //Render metrics
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die}) const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die})
@@ -206,7 +212,7 @@
console.log(`Repository | ${github.context.repo.owner}/${github.context.repo.repo}`) console.log(`Repository | ${github.context.repo.owner}/${github.context.repo.repo}`)
console.log(`Branch | ${branch}`) console.log(`Branch | ${branch}`)
//Committer token //Committer token
const token = core.getInput("committer_token") || core.getInput("token") const token = core.getInput("committer_token") || core.getInput("token") || ""
console.log(`Committer token | ${token ? "provided" : "missing"}`) console.log(`Committer token | ${token ? "provided" : "missing"}`)
if (!token) if (!token)
throw new Error("You must provide a valid GitHub token to commit your metrics") throw new Error("You must provide a valid GitHub token to commit your metrics")
@@ -231,7 +237,7 @@
) )
sha = oid sha = oid
} catch (error) { console.debug(error) } } catch (error) { console.debug(error) }
console.log(`Previous render sha | ${sha || "none"}`) console.log(`Previous render sha | ${sha ?? "none"}`)
//Update file content through API //Update file content through API
await rest.repos.createOrUpdateFileContents({ await rest.repos.createOrUpdateFileContents({
...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`, ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
@@ -245,8 +251,9 @@
console.log(`Success !`) console.log(`Success !`)
process.exit(0) process.exit(0)
}
//Errors //Errors
} catch (error) { catch (error) {
console.error(error) console.error(error)
if (!bool(core.getInput("debug"))) if (!bool(core.getInput("debug")))
for (const log of ["_".repeat(64), "An error occured, logging debug message :", ...debugged]) for (const log of ["_".repeat(64), "An error occured, logging debug message :", ...debugged])

62
package-lock.json generated
View File

@@ -211,26 +211,26 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.12.10", "version": "7.12.12",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz",
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.12.11",
"@babel/generator": "^7.12.10", "@babel/generator": "^7.12.11",
"@babel/helper-function-name": "^7.10.4", "@babel/helper-function-name": "^7.12.11",
"@babel/helper-split-export-declaration": "^7.11.0", "@babel/helper-split-export-declaration": "^7.12.11",
"@babel/parser": "^7.12.10", "@babel/parser": "^7.12.11",
"@babel/types": "^7.12.10", "@babel/types": "^7.12.12",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0", "globals": "^11.1.0",
"lodash": "^4.17.19" "lodash": "^4.17.19"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.12.11", "version": "7.12.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz",
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.12.11", "@babel/helper-validator-identifier": "^7.12.11",
@@ -280,9 +280,9 @@
} }
}, },
"@octokit/openapi-types": { "@octokit/openapi-types": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.1.tgz",
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw==" "integrity": "sha512-9AuC04PUnZrjoLiw3uPtwGh9FE4Q3rTqs51oNlQ0rkwgE8ftYsOC+lsrQyvCvWm85smBbSc0FNRKKumvGyb44Q=="
}, },
"@octokit/plugin-paginate-rest": { "@octokit/plugin-paginate-rest": {
"version": "2.6.2", "version": "2.6.2",
@@ -343,18 +343,18 @@
} }
}, },
"@octokit/types": { "@octokit/types": {
"version": "6.1.1", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.2.tgz",
"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==", "integrity": "sha512-LPCpcLbcky7fWfHCTuc7tMiSHFpFlrThJqVdaHgowBTMS0ijlZFfonQC/C1PrZOjD4xRCYgBqH9yttEATGE/nw==",
"requires": { "requires": {
"@octokit/openapi-types": "^2.0.0", "@octokit/openapi-types": "^2.0.1",
"@types/node": ">= 8" "@types/node": ">= 8"
} }
}, },
"@types/node": { "@types/node": {
"version": "14.14.14", "version": "14.14.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz",
"integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==" "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw=="
}, },
"@types/q": { "@types/q": {
"version": "1.5.4", "version": "1.5.4",
@@ -371,9 +371,9 @@
} }
}, },
"@vercel/ncc": { "@vercel/ncc": {
"version": "0.25.1", "version": "0.26.1",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.25.1.tgz", "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.26.1.tgz",
"integrity": "sha512-dGecC5+1wLof1MQpey4+6i2KZv4Sfs6WfXkl9KfO32GED4ZPiKxRfvtGPjbjZv0IbqMl6CxtcV1RotXYfd5SSA==", "integrity": "sha512-iVhYAL/rpHgjO88GkDHNVNrp7WTfMFBbeWXNgwaDPMv5rDI4hNOAM0u+Zhtbs42XBQE6EccNaY6UDb/tm1+dhg==",
"dev": true "dev": true
}, },
"abbrev": { "abbrev": {
@@ -445,9 +445,9 @@
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
}, },
"axios": { "axios": {
"version": "0.21.0", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": { "requires": {
"follow-redirects": "^1.10.0" "follow-redirects": "^1.10.0"
} }
@@ -1416,9 +1416,9 @@
"dev": true "dev": true
}, },
"get-intrinsic": { "get-intrinsic": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz",
"integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==",
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "metrics", "name": "metrics",
"version": "2.9.0-beta", "version": "2.9.0",
"description": "An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !", "description": "An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {
@@ -24,7 +24,7 @@
"@actions/github": "^4.0.0", "@actions/github": "^4.0.0",
"@octokit/graphql": "^4.5.8", "@octokit/graphql": "^4.5.8",
"@octokit/rest": "^18.0.12", "@octokit/rest": "^18.0.12",
"axios": "^0.21.0", "axios": "^0.21.1",
"colors": "^1.4.0", "colors": "^1.4.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"ejs": "^3.1.5", "ejs": "^3.1.5",
@@ -39,7 +39,7 @@
"vue-prism-component": "^1.2.0" "vue-prism-component": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.25.1", "@vercel/ncc": "^0.26.1",
"babel-minify": "^0.5.1", "babel-minify": "^0.5.1",
"libxmljs": "^0.19.7" "libxmljs": "^0.19.7"
} }

View File

@@ -8,6 +8,7 @@
import setup from "./setup.mjs" import setup from "./setup.mjs"
import metrics from "./metrics.mjs" import metrics from "./metrics.mjs"
import Templates from "./templates/index.mjs" import Templates from "./templates/index.mjs"
import util from "util"
/** App */ /** App */
export default async function () { export default async function () {
@@ -107,7 +108,7 @@
//Compute rendering //Compute rendering
try { try {
//Render //Render
console.debug(`metrics/app/${login} > ${JSON.stringify(req.query)}`) console.debug(`metrics/app/${login} > ${util.inspect(req.query, {depth:Infinity, maxStringLength:256})}`)
const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf}) const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf})
//Cache //Cache
if ((!debug)&&(cached)&&(login !== "placeholder")) if ((!debug)&&(cached)&&(login !== "placeholder"))
@@ -140,7 +141,7 @@
`Debug mode | ${debug}`, `Debug mode | ${debug}`,
`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 ? util.inspect(req.query, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`, `Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled | ${enabled.join(", ")}` `Plugins enabled | ${enabled.join(", ")}`
].join("\n"))) ].join("\n")))

View File

@@ -11,6 +11,7 @@
import fs from "fs/promises" import fs from "fs/promises"
import os from "os" import os from "os"
import paths from "path" import paths from "path"
import util from "util"
//Setup //Setup
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false}) { export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false}) {
@@ -19,7 +20,7 @@
//Init //Init
console.debug(`metrics/compute/${login} > start`) console.debug(`metrics/compute/${login} > start`)
console.debug(JSON.stringify(q)) console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
const template = q.template || conf.settings.templates.default const template = q.template || conf.settings.templates.default
const repositories = Math.max(0, Number(q.repositories)) || conf.settings.repositories || 100 const repositories = Math.max(0, Number(q.repositories)) || conf.settings.repositories || 100
const pending = [] const pending = []
@@ -27,7 +28,7 @@
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template)))) if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
throw new Error("unsupported template") throw new Error("unsupported template")
const {query, image, style, fonts} = conf.templates[template] const {query, image, style, fonts} = conf.templates[template]
const data = {base:{}} const data = {base:{}, config:{}}
//Base parts //Base parts
{ {
@@ -53,13 +54,10 @@
//Compute metrics //Compute metrics
console.debug(`metrics/compute/${login} > compute`) console.debug(`metrics/compute/${login} > compute`)
const computer = Templates[template].default || Templates[template] const computer = Templates[template].default || Templates[template]
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, format, bytes, shuffle, htmlescape, urlexpand}}) await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}})
const promised = await Promise.all(pending) const promised = await Promise.all(pending)
//Check plugins errors //Check plugins errors
if (conf.settings.debug)
for (const {name, result = null} of promised)
console.debug(`plugin ${name} ${result ? result.error ? "failed" : "success" : "ignored"} : ${JSON.stringify(result).replace(/^(.{888}).+/, "$1...")}`)
if (die) { if (die) {
const errors = promised.filter(({result = null}) => !!result?.error).length const errors = promised.filter(({result = null}) => !!result?.error).length
if (errors) if (errors)

View File

@@ -23,7 +23,6 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -5,7 +5,8 @@
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.gists)) if ((!enabled)||(!q.gists))
return null return null
//Retrieve contribution calendar from graphql api //Retrieve gists from graphql api
console.debug(`metrics/compute/${login}/plugins > gists > querying api`)
const {user:{gists}} = await graphql(` const {user:{gists}} = await graphql(`
query Gists { query Gists {
user(login: "${login}") { user(login: "${login}") {
@@ -27,6 +28,7 @@
` `
) )
//Iterate through gists //Iterate through gists
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.nodes.length} gists`)
let stargazers = 0, forks = 0, comments = 0 let stargazers = 0, forks = 0, comments = 0
for (const gist of gists.nodes) { for (const gist of gists.nodes) {
//Skip forks //Skip forks
@@ -42,7 +44,6 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default async function ({login, rest, imports, q}, {enabled = false, from:defaults = 100} = {}) { export default async function ({login, rest, imports, data, q}, {enabled = false, from:defaults = 100} = {}) {
//Plugin execution //Plugin execution
try { try {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
@@ -14,23 +14,25 @@
//Initialization //Initialization
const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}} const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}}
const pages = Math.ceil(from/100) const pages = Math.ceil(from/100)
const offset = data.config.timezone?.offset ?? 0
//Get user recent activity //Get user recent activity
console.debug(`metrics/compute/${login}/plugins > habits > querying api`)
const events = [] const events = []
try { try {
for (let page = 0; page < pages; page++) { for (let page = 0; page < pages; page++) {
console.debug(`metrics/compute/${login}/plugins > habits > loaded page ${page}`) console.debug(`metrics/compute/${login}/plugins > habits > loading page ${page}`)
events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data) events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data)
} }
} catch { console.debug(`metrics/compute/${login}/plugins > habits > no more events to load`) } } catch { console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`) }
console.debug(`metrics/compute/${login}/plugins > habits > no more events to load (${events.length} loaded)`) console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`)
//Get user recent commits //Get user recent commits
const commits = events const commits = events
.filter(({type}) => type === "PushEvent") .filter(({type}) => type === "PushEvent")
.filter(({actor}) => actor.login === login) .filter(({actor}) => actor.login === login)
.filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000)) .filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000))
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} commits`) console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} push events over last ${days} days`)
const actor = commits[0]?.actor?.id ?? 0
//Retrieve edited files and filter edited lines (those starting with +/-) from patches //Retrieve edited files and filter edited lines (those starting with +/-) from patches
console.debug(`metrics/compute/${login}/plugins > habits > loading patches`)
const patches = [...await Promise.allSettled(commits const patches = [...await Promise.allSettled(commits
.flatMap(({payload}) => payload.commits).map(commit => commit.url) .flatMap(({payload}) => payload.commits).map(commit => commit.url)
.map(async commit => (await rest.request(commit)).data.files) .map(async commit => (await rest.request(commit)).data.files)
@@ -42,7 +44,8 @@
//Commit day //Commit day
{ {
//Compute commit days //Compute commit days
const days = commits.map(({created_at}) => (new Date(created_at)).getDay()) console.debug(`metrics/compute/${login}/plugins > habits > searching most active day of week`)
const days = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getDay())
for (const day of days) for (const day of days)
habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1 habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1
habits.commits.days.max = Math.max(...Object.values(habits.commits.days)) habits.commits.days.max = Math.max(...Object.values(habits.commits.days))
@@ -52,7 +55,8 @@
//Commit hour //Commit hour
{ {
//Compute commit hours //Compute commit hours
const hours = commits.map(({created_at}) => (new Date(created_at)).getHours()) console.debug(`metrics/compute/${login}/plugins > habits > searching most active time of day`)
const hours = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getHours())
for (const hour of hours) for (const hour of hours)
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1 habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours)) habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours))
@@ -62,6 +66,7 @@
//Indent style //Indent style
{ {
//Attempt to guess whether tabs or spaces are used in patches //Attempt to guess whether tabs or spaces are used in patches
console.debug(`metrics/compute/${login}/plugins > habits > searching indent style`)
patches patches
.map(({patch}) => patch.match(/((?:\t)|(?: )) /gm) ?? []) .map(({patch}) => patch.match(/((?:\t)|(?: )) /gm) ?? [])
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++) .forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
@@ -70,36 +75,38 @@
//Linguist //Linguist
if (charts) { if (charts) {
//Check if linguist exists //Check if linguist exists
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
const prefix = {win32:"wsl"}[process.platform] ?? "" const prefix = {win32:"wsl"}[process.platform] ?? ""
if ((patches.length)&&(await imports.run(`${prefix} which github-linguist`))) { if ((patches.length)&&(await imports.run(`${prefix} which github-linguist`))) {
//Setup for linguist //Setup for linguist
habits.linguist.available = true habits.linguist.available = true
const path = imports.paths.join(imports.os.tmpdir(), `${actor}`) const path = imports.paths.join(imports.os.tmpdir(), `${commits[0]?.actor?.id ?? 0}`)
//Create temporary directory and save patches //Create temporary directory and save patches
console.debug(`metrics/compute/${login}/plugins > habits > creating temp dir ${path} with ${patches.length} files`)
await imports.fs.mkdir(path, {recursive:true}) await imports.fs.mkdir(path, {recursive:true})
await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch))) await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
console.debug(`metrics/compute/${login}/plugins > habits > created temp dir ${path} with ${patches.length} files`)
//Create temporary git repository //Create temporary git repository
console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`)
await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "null@github.com" && git commit -m "linguist"`, {cwd:path}).catch(console.debug) await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "null@github.com" && git commit -m "linguist"`, {cwd:path}).catch(console.debug)
await imports.run(`git status`, {cwd:path}) await imports.run(`git status`, {cwd:path})
console.debug(`metrics/compute/${login}/plugins > habits > created temp git repository`)
//Spawn linguist process //Spawn linguist process
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`) console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
;(await imports.run(`${prefix} github-linguist`, {cwd:path})) ;(await imports.run(`${prefix} github-linguist --breakdown`, {cwd:path}))
//Parse linguist result //Parse linguist result
.split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>\w+)/)?.groups).filter(line => line) .split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>\w+)/)?.groups).filter(line => line)
.map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100) .map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100)
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([an, a], [bn, b]) => b - a) habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([an, a], [bn, b]) => b - a)
} }
else else
console.debug(`metrics/compute/${login}/plugins > habits > linguist is not available`) console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
} }
//Results //Results
return habits return habits
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) if (error.error?.message)
throw {error:{message:`An error occured`}} throw error
throw {error:{message:"An error occured", instance:error}}
} }
} }

View File

@@ -20,9 +20,10 @@
const padding = new Date(start) const padding = new Date(start)
padding.setHours(-14*24) padding.setHours(-14*24)
//Retrieve contribution calendar from graphql api //Retrieve contribution calendar from graphql api
console.debug(`metrics/compute/${login}/plugins > isocalendar > querying api`)
const calendar = {} const calendar = {}
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) { for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading "${name}" from "${from.toISOString()}" to "${to.toISOString()}"`) console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(` const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(`
query Calendar { query Calendar {
user(login: "${login}") { user(login: "${login}") {
@@ -44,11 +45,13 @@
calendar[name] = weeks calendar[name] = weeks
} }
//Apply padding //Apply padding
console.debug(`metrics/compute/${login}/plugins > isocalendar > applying padding`)
const firstweek = calendar.weeks[0].contributionDays const firstweek = calendar.weeks[0].contributionDays
const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date)) const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date))
while (firstweek.length < 7) while (firstweek.length < 7)
firstweek.unshift(padded.pop()) firstweek.unshift(padded.pop())
//Compute the highest contributions in a day, streaks and average commits per day //Compute the highest contributions in a day, streaks and average commits per day
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
let max = 0, streak = {max:0, current:0}, values = [], average = 0 let max = 0, streak = {max:0, current:0}, values = [], average = 0
for (const week of calendar.weeks) { for (const week of calendar.weeks) {
for (const day of week.contributionDays) { for (const day of week.contributionDays) {
@@ -60,6 +63,7 @@
} }
average = (values.reduce((a, b) => a + b, 0)/values.length).toFixed(2).replace(/[.]0+$/, "") average = (values.reduce((a, b) => a + b, 0)/values.length).toFixed(2).replace(/[.]0+$/, "")
//Compute SVG //Compute SVG
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`)
const size = 6 const size = 6
let i = 0, j = 0 let i = 0, j = 0
let svg = ` let svg = `
@@ -96,7 +100,6 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -12,6 +12,7 @@
//Skipped repositories //Skipped repositories
skipped = decodeURIComponent(skipped).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x) skipped = decodeURIComponent(skipped).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
//Iterate through user's repositories and retrieve languages data //Iterate through user's repositories and retrieve languages data
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
const languages = {colors:{}, total:0, stats:{}} const languages = {colors:{}, total:0, stats:{}}
for (const repository of data.user.repositories.nodes) { for (const repository of data.user.repositories.nodes) {
//Skip repository if asked //Skip repository if asked
@@ -33,6 +34,7 @@
} }
} }
//Compute languages stats //Compute languages stats
console.debug(`metrics/compute/${login}/plugins > languages > computing stats`)
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total) Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0})) languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
for (let i = 1; i < languages.favorites.length; i++) for (let i = 1; i < languages.favorites.length; i++)
@@ -42,7 +44,6 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -8,9 +8,11 @@
//Repositories //Repositories
const repositories = data.user.repositories.nodes.map(({name}) => name) ?? [] const repositories = data.user.repositories.nodes.map(({name}) => name) ?? []
//Get contributors stats from repositories //Get contributors stats from repositories
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
const lines = {added:0, deleted:0} const lines = {added:0, deleted:0}
const response = await Promise.all(repositories.map(async repo => await rest.repos.getContributorsStats({owner:login, repo}))) const response = await Promise.all(repositories.map(async repo => await rest.repos.getContributorsStats({owner:login, repo})))
//Compute changed lines //Compute changed lines
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
response.map(({data:repository}) => { response.map(({data:repository}) => {
//Check if data are available //Check if data are available
if (!Array.isArray(repository)) if (!Array.isArray(repository))
@@ -29,8 +31,7 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -56,13 +56,14 @@
//Limit //Limit
limit = Math.max(1, Math.min(100, Number(limit))) limit = Math.max(1, Math.min(100, Number(limit)))
//Handle mode //Handle mode
console.debug(`metrics/compute/${login}/plugins > music > processing mode ${mode} with provider ${provider}`)
switch (mode) { switch (mode) {
//Playlist mode //Playlist mode
case "playlist":{ case "playlist":{
//Start puppeteer and navigate to playlist //Start puppeteer and navigate to playlist
console.debug(`metrics/compute/${login}/plugins > music > starting browser`) console.debug(`metrics/compute/${login}/plugins > music > starting browser`)
const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]}) const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
console.debug(`metrics/compute/${login}/plugins > music > loaded ${await browser.version()}`) console.debug(`metrics/compute/${login}/plugins > music > started ${await browser.version()}`)
const page = await browser.newPage() const page = await browser.newPage()
console.debug(`metrics/compute/${login}/plugins > music > loading page`) console.debug(`metrics/compute/${login}/plugins > music > loading page`)
await page.goto(playlist) await page.goto(playlist)
@@ -103,7 +104,7 @@
if (Array.isArray(tracks)) { if (Array.isArray(tracks)) {
//Tracks //Tracks
console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`) console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`)
console.debug(JSON.stringify(tracks)) console.debug(imports.util.inspect(tracks, {depth:Infinity, maxStringLength:256}))
//Shuffle tracks //Shuffle tracks
tracks = imports.shuffle(tracks) tracks = imports.shuffle(tracks)
} }
@@ -120,17 +121,18 @@
//Prepare credentials //Prepare credentials
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim()) const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
if ((!client_id)||(!client_secret)||(!refresh_token)) if ((!client_id)||(!client_secret)||(!refresh_token))
throw {error:`Spotify token must contain client id/secret and refresh token`} throw {error:{message:`Spotify token must contain client id/secret and refresh token`}}
//API call and parse tracklist //API call and parse tracklist
try { try {
//Request access token //Request access token
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with refresh token for spotify`) console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`)
const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token", const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token",
`${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`, `${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`,
{headers:{"Content-Type":"application/x-www-form-urlencoded"}}, {headers:{"Content-Type":"application/x-www-form-urlencoded"}},
) )
console.debug(`metrics/compute/${login}/plugins > music > got new access token`) console.debug(`metrics/compute/${login}/plugins > music > got access token`)
//Retriev tracks //Retriev tracks
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
tracks = (await imports.axios(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{ tracks = (await imports.axios(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{
"Accept":"application/json", "Accept":"application/json",
"Content-Type":"application/json", "Content-Type":"application/json",
@@ -143,8 +145,13 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if ((error.response?.status)) if (error.isAxiosError) {
throw {error:{message:`API returned ${error.response.status}${error.response.data?.error_description ? ` (${error.response.data.error_description})` : ""}`}, ...raw} const status = error.response?.status
const description = error.response.data?.error_description ?? null
message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null
throw {error:{message, instance:error}, ...raw}
}
throw error throw error
} }
break break
@@ -173,7 +180,6 @@
track.artwork = await imports.imgb64(track.artwork) track.artwork = await imports.imgb64(track.artwork)
} }
//Save results //Save results
console.debug(`metrics/compute/${login}/plugins > music > success`)
return {...raw, tracks} return {...raw, tracks}
} }
//Unhandled error //Unhandled error
@@ -183,7 +189,6 @@
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -15,29 +15,37 @@
url = `https://${url}` url = `https://${url}`
const result = {url, detailed, scores:[], metrics:{}} const result = {url, detailed, scores:[], metrics:{}}
//Load scores from API //Load scores from API
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`)
const scores = new Map() const scores = new Map()
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => { await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
const {score, title} = (await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)).data.lighthouseResult.categories[category] console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)
console.debug(request.data)
const {score, title} = request.data.lighthouseResult.categories[category]
scores.set(category, {score, title}) scores.set(category, {score, title})
console.debug(`metrics/compute/${login}/plugins > pagespeed > ${category} audit performed`) console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
})) }))
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")] result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
//Detailed metrics //Detailed metrics
if (detailed) if (detailed) {
Object.assign(result.metrics, ...(await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}&key=${token}`)).data.lighthouseResult.audits.metrics.details.items) console.debug(`metrics/compute/${login}/plugins > pagespeed > performing detailed audit`)
//Integrity check const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}&key=${token}`)
if (result.scores.filter(score => score).length < 4) console.debug(request.data)
throw {error:{message:"Incomplete PageSpeed results"}, url} Object.assign(result.metrics, ...request.data.lighthouseResult.audits.metrics.details.items)
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed detailed audit (status code ${request.status})`)
}
//Results //Results
return result return result
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.response?.status) let message = "An error occured"
throw {error:{message:`PageSpeed token error (code ${error.response.status})`}, url} if (error.isAxiosError) {
if (error.error?.message) const status = error.response?.status
throw error const description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
console.debug(error) message = `API returned ${status}${description ? ` (${description})` : ""}`
throw {error:{message:`An error occured`}} error = error.response?.data ?? null
}
throw {error:{message, instance:error}}
} }
} }

View File

@@ -11,10 +11,12 @@
//Limit //Limit
limit = Math.max(1, Math.min(30, Number(limit))) limit = Math.max(1, Math.min(30, Number(limit)))
//Retrieve posts //Retrieve posts
console.debug(`metrics/compute/${login}/plugins > posts > processing with source ${source}`)
let posts = null let posts = null
switch (source) { switch (source) {
//Dev.to //Dev.to
case "dev.to":{ case "dev.to":{
console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${login}&state=fresh`)).data.map(({title, readable_publish_date:date}) => ({title, date})) posts = (await imports.axios.get(`https://dev.to/api/articles?username=${login}&state=fresh`)).data.map(({title, readable_publish_date:date}) => ({title, date}))
break break
} }
@@ -37,7 +39,6 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -10,6 +10,7 @@
//Limit //Limit
limit = Math.max(1, Math.min(100, Number(limit))) limit = Math.max(1, Math.min(100, Number(limit)))
//Retrieve contribution calendar from graphql api //Retrieve contribution calendar from graphql api
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
const {user:{projects}} = await graphql(` const {user:{projects}} = await graphql(`
query Projects { query Projects {
user(login: "${login}") { user(login: "${login}") {
@@ -31,6 +32,7 @@
` `
) )
//Iterate through projects and format them //Iterate through projects and format them
console.debug(`metrics/compute/${login}/plugins > posts > processing ${projects.nodes.length} projects`)
const list = [] const list = []
for (const project of projects.nodes) { for (const project of projects.nodes) {
//Format date //Format date
@@ -52,7 +54,9 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) let message = "An error occured"
throw {error:{message:`An error occured`}} if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
message = "Insufficient token rights"
throw {error:{message, instance:error}}
} }
} }

View File

@@ -14,10 +14,11 @@
//Limit //Limit
limit = Math.max(1, Math.min(20, Number(limit))) limit = Math.max(1, Math.min(20, Number(limit)))
//Start puppeteer and navigate to topics //Start puppeteer and navigate to topics
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
let topics = [] let topics = []
console.debug(`metrics/compute/${login}/plugins > topics > starting browser`) console.debug(`metrics/compute/${login}/plugins > topics > starting browser`)
const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]}) const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
console.debug(`metrics/compute/${login}/plugins > topics > loaded ${await browser.version()}`) console.debug(`metrics/compute/${login}/plugins > topics > started ${await browser.version()}`)
const page = await browser.newPage() const page = await browser.newPage()
//Iterate through pages //Iterate through pages
for (let i = 1; i <= 100; i++) { for (let i = 1; i <= 100; i++) {
@@ -32,17 +33,22 @@
description:li.querySelector(".f5").innerText, description:li.querySelector(".f5").innerText,
icon:li.querySelector("img")?.src ?? null, icon:li.querySelector("img")?.src ?? null,
}))) })))
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)
//Check if next page exists //Check if next page exists
if (!starred.length) if (!starred.length) {
console.debug(`metrics/compute/${login}/plugins > topics > no more page to load`)
break break
}
topics.push(...starred) topics.push(...starred)
} }
//Close browser //Close browser
console.debug(`metrics/compute/${login}/plugins > music > closing browser`) console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
await browser.close() await browser.close()
//Shuffle topics //Shuffle topics
if (shuffle) if (shuffle) {
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
topics = imports.shuffle(topics) topics = imports.shuffle(topics)
}
//Limit topics //Limit topics
if (limit > 0) { if (limit > 0) {
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`) console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
@@ -51,6 +57,7 @@
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null}) topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
} }
//Convert icons to base64 //Convert icons to base64
console.debug(`metrics/compute/${login}/plugins > topics > loading artworks`)
for (const topic of topics) { for (const topic of topics) {
if (topic.icon) { if (topic.icon) {
console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`) console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`)
@@ -64,7 +71,6 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) throw {error:{message:"An error occured", instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -8,9 +8,11 @@
//Repositories //Repositories
const repositories = data.user.repositories.nodes.map(({name}) => name) ?? [] const repositories = data.user.repositories.nodes.map(({name}) => name) ?? []
//Get views stats from repositories //Get views stats from repositories
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
const views = {count:0, uniques:0} const views = {count:0, uniques:0}
const response = await Promise.all(repositories.map(async repo => await rest.repos.getViews({owner:login, repo}))) const response = await Promise.all(repositories.map(async repo => await rest.repos.getViews({owner:login, repo})))
//Compute views //Compute views
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques)) response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
//Format values //Format values
views.count = imports.format(views.count) views.count = imports.format(views.count)
@@ -20,9 +22,9 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
let message = "An error occured"
if (error.status === 403) if (error.status === 403)
throw {error:{message:`Insufficient token rights`}} message = "Insufficient token rights"
console.debug(error) throw {error:{message, instance:error}}
throw {error:{message:`An error occured`}}
} }
} }

View File

@@ -14,7 +14,7 @@
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`) console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`)
const {data:{data:profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers:{Authorization:`Bearer ${token}`}}) const {data:{data:profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers:{Authorization:`Bearer ${token}`}})
//Load tweets //Load tweets
console.debug(`metrics/compute/${login}/plugins > tweets > loading tweets`) console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
const {data:{data:tweets = []}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at&expansions=entities.mentions.username`, {headers:{Authorization:`Bearer ${token}`}}) const {data:{data:tweets = []}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at&expansions=entities.mentions.username`, {headers:{Authorization:`Bearer ${token}`}})
//Load profile image //Load profile image
if (profile?.profile_image_url) { if (profile?.profile_image_url) {
@@ -50,7 +50,13 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
console.debug(error) let message = "An error occured"
throw {error:{message:`An error occured`}} if (error.isAxiosError) {
const status = error.response?.status
const description = error.response?.data?.errors?.[0]?.message ?? null
message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null
}
throw {error:{message, instance:error}}
} }
} }

View File

@@ -1,6 +1,7 @@
//Imports //Imports
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import util from "util"
/** Setup */ /** Setup */
export default async function ({log = true} = {}) { export default async function ({log = true} = {}) {
@@ -30,7 +31,7 @@
conf.settings.plugins = {} conf.settings.plugins = {}
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]} conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]}
if (conf.settings.debug) if (conf.settings.debug)
logger(conf.settings) logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
//Load package settings //Load package settings
logger(`metrics/setup > load package.json`) logger(`metrics/setup > load package.json`)

View File

@@ -13,7 +13,7 @@
+ (!!plugins.isocalendar)*192 + (plugins.isocalendar?.duration === 'full-year')*100 + (!!plugins.isocalendar)*192 + (plugins.isocalendar?.duration === 'full-year')*100
+ (!!plugins.gists)*68 + (!!plugins.gists)*68
+ (!!plugins.topics)*160 + (!!plugins.topics)*160
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
+ (!!plugins.tweets)*64 + (plugins.tweets?.list?.length ?? 0)*90 + (!!plugins.tweets)*64 + (plugins.tweets?.list?.length ?? 0)*90
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+((!!plugins.habits))+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.music)+(!!plugins.posts)+(!!plugins.isocalendar)+(!!plugins.gists)+(!!plugins.topics)+(!!plugins.projects))-1))*4 + Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+((!!plugins.habits))+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.music)+(!!plugins.posts)+(!!plugins.isocalendar)+(!!plugins.gists)+(!!plugins.topics)+(!!plugins.projects))-1))*4
%>"> %>">
@@ -774,7 +774,7 @@
<% if (base.metadata) { %> <% if (base.metadata) { %>
<footer> <footer>
<span>These metrics <%= !computed.token.scopes.includes("repo") ? "does not include" : "includes" %> private contributions</span> <span>These metrics <%= !computed.token.scopes.includes("repo") ? "does not include all" : "includes" %> private contributions<% if ((config.timezone?.name)&&(!config.timezone?.error)) { %>, timezone <%= config.timezone.name %><% } %></span>
<span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span> <span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span>
</footer> </footer>
<% } %> <% } %>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -5,18 +5,36 @@
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}} const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
const avatar = imports.imgb64(data.user.avatarUrl) const avatar = imports.imgb64(data.user.avatarUrl)
data.plugins = {} data.plugins = {}
console.debug(`metrics/compute/${login} > formatting common metrics`)
//Timezone config
if (q["config.timezone"]) {
const timezone = data.config.timezone = {name:q["config.timezone"], offset:0}
try {
timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset*60*60*1000) || 0
console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset/(60*60*1000))} hours)`)
} catch {
timezone.error = `Failed to use timezone "${timezone.name}"`
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
}
}
//Plugins //Plugins
for (const name of Object.keys(imports.plugins)) { for (const name of Object.keys(imports.plugins)) {
pending.push((async () => { pending.push((async () => {
try { try {
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql}, plugins[name]) data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql}, plugins[name])
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (${data.plugins[name] !== null ? "success" : "skipped"})`)
} }
catch (error) { catch (error) {
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (error)`)
data.plugins[name] = error data.plugins[name] = error
} }
finally { finally {
return {name, result:data.plugins[name]} const result = {name, result:data.plugins[name]}
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256}))
return result
} }
})()) })())
} }
@@ -30,14 +48,14 @@
computed.repositories.forks += repository.forkCount computed.repositories.forks += repository.forkCount
//License //License
if (repository.licenseInfo) if (repository.licenseInfo)
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] || 0) + 1 computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] ?? 0) + 1
} }
//Total disk usage //Total disk usage
computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}` computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}`
//Compute licenses stats //Compute licenses stats
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) || "" computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) ?? ""
//Compute total commits //Compute total commits
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
@@ -62,11 +80,11 @@
data.meta = {version:conf.package.version, author:conf.package.author} data.meta = {version:conf.package.version, author:conf.package.author}
//Debug flags //Debug flags
if (dflags.includes("--cakeday")||q["dflag.cakeday"]) { if ((dflags.includes("--cakeday"))||(q["dflag.cakeday"])) {
console.debug(`metrics/compute/${login} > applying dflag --cakeday`) console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
computed.cakeday = true computed.cakeday = true
} }
if (dflags.includes("--hireable")||q["dflag.hireable"]) { if ((dflags.includes("--hireable"))||(q["dflag.hireable"])) {
console.debug(`metrics/compute/${login} > applying dflag --hireable`) console.debug(`metrics/compute/${login} > applying dflag --hireable`)
data.user.isHireable = true data.user.isHireable = true
} }