Version 2.1

- No plugins are enabled by default
- Logs from setup can be hidden
- Number of repositories to inspect can be configured (default to 100)
- Default events for habits plugin is now 100
- Number of events for habits can be overriden in query
- Server app improvments
- Test improvements
- Better test
This commit is contained in:
lowlighter
2020-10-23 13:56:15 +02:00
parent 543c6f8f98
commit 2cf152d7d1
16 changed files with 311 additions and 200 deletions

86
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,86 @@
# 📊 GitHub metrics
## 💪 Interested in contributing ?
Nice ! Read the few sections below to understand how this project is structured.
### 👨‍💻 General informations
#### Adding new metrics through GraphQL API, REST API or Third-Party service
To use [GitHub GraphQL API](https://docs.github.com/en/graphql), update the GraphQL query from `templates/*/query.graphql`.
Raw queried data should be exposed in `data.user` whereas computed data should be in `data.computed`, and code should be updated through `templates/*/template.mjs`.
To use [GitHub Rest API](https://docs.github.com/en/rest) or a third-party service instead, create a new plugin in `src/plugins`.
Plugins should be self-sufficient and re-exported from [src/plugins/index.mjs](https://github.com/lowlighter/metrics/blob/master/src/plugins/index.mjs), to be later included in the `//Plugins` section of `templates/*/template.mjs`.
Data generated should be exposed in `data.computed.plugins[plugin]` where `plugin` is your plugin's name.
#### Updating the SVG template
The SVG template is located in `templates/*/image.svg` and include the CSS from `templates/*/style.css`.
It is rendered with [EJS](https://github.com/mde/ejs) so you can actually include variables (e.g. `<%= user.name %>`) and execute simple code, like control statements.
#### Metrics server and GitHub action
Most of the time, you won't need to edit these, unless you're integrating features directly tied to them.
Remember that SVG image is actually generated from `src/metrics.mjs`, independently from metrics server and GitHub action.
Metrics server code is located in `src/app.mjs` and instantiates an `express` server app, `octokit`s instances, middlewares (like rate-limiter) and routes.
GitHub action code is located in `action/index.mjs` and instantiates `octokit`s instances and retrieves action parameters.
It then use directly `src/metrics.mjs` to generate the SVG image and commit them to user's repository.
You must run `npm run build` to rebuild the GitHub action.
#### Testing new features
To test new features, setup a metrics server with a test token and `debug` mode enabled.
This way you'll be able to rapidly test SVG renders with your browser.
### 🗂️ Project structure
#### Metrics generator
* `src/setup.mjs` contains the configuration setup
* `src/metrics.mjs` contains the metrics renderer
* `src/templates/*` contains templates files
* `src/templates/*/image.svg` contains the template used by the generated SVG image
* `src/templates/*/query.graphql` is the GraphQL query sent to GitHub GraphQL API
* `src/templates/*/style.css` contains the style used by the generated SVG image
* `src/templates/*/template.mjs` contains the code which prepares data for rendering
* `src/plugins/*` contains the source code of metrics plugins
#### Metrics server instance
* `index.mjs` contains the metrics server entry point
* `src/app.mjs` contains the metrics server code which serves, renders, restricts/rate limit, etc.
#### GitHub action
* `action.yml` contains the GitHub action descriptor
* `action/index.mjs` contains the GitHub action code
* `action/dist/index.js` contains compiled the GitHub action code
* `utils/build.mjs` contains the GitHub action builder
### 📦 Used packages
* [express/express.js](https://github.com/expressjs/express) and [expressjs/compression](https://github.com/expressjs/compression)
* To serve, compute and render a GitHub user's metrics
* [nfriedly/express-rate-limit](https://github.com/nfriedly/express-rate-limit)
* To apply rate limiting on server and avoid spams and hitting GitHub API's own rate limit
* [octokit/graphql.js](https://github.com/octokit/graphql.js/) and [octokit/rest.js](https://github.com/octokit/rest.js)
* To perform request to GitHub GraphQL API and GitHub REST API
* [mde/ejs](https://github.com/mde/ejs)
* To render SVG images
* [ptarjan/node-cache](https://github.com/ptarjan/node-cache)
* To cache generated content
* [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64)
* To generate base64 representation of users' avatars
* [svg/svgo](https://github.com/svg/svgo)
* To optimize generated SVG
* [axios/axios](https://github.com/axios/axios)
* To make HTTP/S requests
* [actions/toolkit](https://github.com/actions/toolkit/tree/master) and [vercel/ncc](https://github.com/vercel/ncc)
* To build the GitHub Action
* [vuejs/vue](https://github.com/vuejs/vue)
* To display server application

102
README.md
View File

@@ -3,13 +3,19 @@
![Build](https://github.com/lowlighter/metrics/workflows/Build/badge.svg) ![Analysis](https://github.com/lowlighter/metrics/workflows/Analysis/badge.svg) ![Build](https://github.com/lowlighter/metrics/workflows/Build/badge.svg) ![Analysis](https://github.com/lowlighter/metrics/workflows/Analysis/badge.svg)
Generates your own GitHub metrics as an SVG image to put them on your profile page or elsewhere ! Generates your own GitHub metrics as an SVG image to put them on your profile page or elsewhere !
See what it looks like below :
![GitHub metrics](https://github.com/lowlighter/lowlighter/blob/master/github-metrics-alt.svg)
Still not enough data for you ? You can enable additional plugins for even more metrics !
![GitHub metrics](https://github.com/lowlighter/lowlighter/blob/master/github-metrics.svg) ![GitHub metrics](https://github.com/lowlighter/lowlighter/blob/master/github-metrics.svg)
### 🦑 Interested to get your own ? ### 🦑 Interested to get your own ?
Try it now at [metrics.lecoq.io](https://metrics.lecoq.io/) with your GitHub username ! Try it now at [metrics.lecoq.io](https://metrics.lecoq.io/) with your GitHub username !
For a fully-featured experience, setup this as a [GitHub Action](https://github.com/marketplace/actions/github-metrics-as-svg-image) !
## 📜 How to use ? ## 📜 How to use ?
### ⚙️ Using GitHub Action on your profile repo (~5 min setup) ### ⚙️ Using GitHub Action on your profile repo (~5 min setup)
@@ -150,9 +156,10 @@ Since GitHub API has rate limitations, the shared instance has a few limitations
* Your generated metrics won't be updated during this amount of time * Your generated metrics won't be updated during this amount of time
* If you enable or disable plugins in url parameters, you'll need to wait for cache expiration before these changes are applied * If you enable or disable plugins in url parameters, you'll need to wait for cache expiration before these changes are applied
* The rate limiter is enabled, although it won't affect already cached users metrics * The rate limiter is enabled, although it won't affect already cached users metrics
* Plugins which consume additional requests or require elevated token rights are disabled * Plugins which consume additional requests or require elevated token rights are disabled. The following plugins are available :
* PageSpeed plugin can still be enabled by passing `?pagespeed=1`, but metrics generation can take up some time when it has not been cached yet * PageSpeed plugin can be enabled by passing `?pagespeed=1`, but metrics generation can take up some time when it has not been cached yet
* Languages and Follow-up plugins are enabled by default by can be disabled respectively with `?languages=0` and `?followup=0` * Languages plugin can be enabled by passing `?languages=1`
* Follow-up plugin can be enabled by passing `?followup=1`
To ensure maximum availability, consider deploying your own instance or use the GitHub Action. To ensure maximum availability, consider deploying your own instance or use the GitHub Action.
@@ -267,7 +274,7 @@ Open and edit `settings.json` to configure your instance using a text editor of
//Enable or disable this plugin. Pass "?habits=1" in url to generate coding habits based on your recent activity //Enable or disable this plugin. Pass "?habits=1" in url to generate coding habits based on your recent activity
"enabled":true, "enabled":true,
//Number of events used to compute coding habits (capped at 100 by GitHub API) //Number of events used to compute coding habits (capped at 100 by GitHub API)
"from":50, "from":100,
} }
} }
} }
@@ -511,8 +518,6 @@ The *follow-up* plugin allows you to compute the ratio of opened/closed issues a
<details> <details>
<summary>💬 About</summary> <summary>💬 About</summary>
This plugin is enabled by default. To disable it, explicitly opt-out.
##### Setup with GitHub actions ##### Setup with GitHub actions
Add the following to your workflow : Add the following to your workflow :
@@ -545,8 +550,6 @@ The *languages* plugin allows you to compute which languages you use the most in
<details> <details>
<summary>💬 About</summary> <summary>💬 About</summary>
This plugin is enabled by default. To disable it, explicitly opt-out.
##### Setup with GitHub actions ##### Setup with GitHub actions
Add the following to your workflow : Add the following to your workflow :
@@ -591,69 +594,13 @@ Add the following to your workflow :
</details> </details>
### 🗂️ Project structure
#### Metrics generator
* `src/setup.mjs` contains the configuration setup
* `src/metrics.mjs` contains the metrics renderer
* `src/templates/*` contains templates files
* `src/templates/*/image.svg` contains the template used by the generated SVG image
* `src/templates/*/query.graphql` is the GraphQL query sent to GitHub GraphQL API
* `src/templates/*/style.css` contains the style used by the generated SVG image
* `src/templates/*/template.mjs` contains the code which prepares data for rendering
* `src/plugins/*` contains the source code of metrics plugins
#### Metrics server instance
* `index.mjs` contains the metrics server entry point
* `src/app.mjs` contains the metrics server code which serves, renders, restricts/rate limit, etc.
#### GitHub action
* `action.yml` contains the GitHub action descriptor
* `action/index.mjs` contains the GitHub action code
* `action/dist/index.js` contains compiled the GitHub action code
* `utils/build.mjs` contains the GitHub action builder
### 💪 Contributing and customizing ### 💪 Contributing and customizing
If you would like to suggest a new feature, find a bug or need help, you can fill an [issue](https://github.com/lowlighter/metrics/issues) describing your problem. If you would like to suggest a new feature, find a bug or need help, you can fill an [issue](https://github.com/lowlighter/metrics/issues) describing your problem.
If you're motivated enough, you can submit a [pull request](https://github.com/lowlighter/metrics/pulls) to integrate new features or to solve open issues. If you're motivated enough, you can submit a [pull request](https://github.com/lowlighter/metrics/pulls) to integrate new features or to solve open issues.
Read the few sections below to get started with project structure. Read [contributing.md](https://github.com/lowlighter/metrics/blob/master/CONTRIBUTING.md) for more information about this.
#### Adding new metrics through GraphQL API, REST API or Third-Party service
To use [GitHub GraphQL API](https://docs.github.com/en/graphql), update the GraphQL query from `templates/*/query.graphql`.
Raw queried data should be exposed in `data.user` whereas computed data should be in `data.computed`, and code should be updated through `templates/*/template.mjs`.
To use [GitHub Rest API](https://docs.github.com/en/rest) or a third-party service instead, create a new plugin in `src/plugins`.
Plugins should be self-sufficient and re-exported from [src/plugins/index.mjs](https://github.com/lowlighter/metrics/blob/master/src/plugins/index.mjs), to be later included in the `//Plugins` section of `templates/*/template.mjs`.
Data generated should be exposed in `data.computed.plugins[plugin]` where `plugin` is your plugin's name.
#### Updating the SVG template
The SVG template is located in `templates/*/image.svg` and include the CSS from `templates/*/style.css`.
It is rendered with [EJS](https://github.com/mde/ejs) so you can actually include variables (e.g. `<%= user.name %>`) and execute simple code, like control statements.
#### Metrics server and GitHub action
Most of the time, you won't need to edit these, unless you're integrating features directly tied to them.
Remember that SVG image is actually generated from `src/metrics.mjs`, independently from metrics server and GitHub action.
Metrics server code is located in `src/app.mjs` and instantiates an `express` server app, `octokit`s instances, middlewares (like rate-limiter) and routes.
GitHub action code is located in `action/index.mjs` and instantiates `octokit`s instances and retrieves action parameters.
It then use directly `src/metrics.mjs` to generate the SVG image and commit them to user's repository.
You must run `npm run build` to rebuild the GitHub action.
#### Testing new features
To test new features, setup a metrics server with a test token and `debug` mode enabled.
This way you'll be able to rapidly test SVG renders with your browser.
### 📖 Useful references ### 📖 Useful references
@@ -661,29 +608,6 @@ This way you'll be able to rapidly test SVG renders with your browser.
* [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) * [GitHub Rest API](https://docs.github.com/en/rest)
### 📦 Used packages
* [express/express.js](https://github.com/expressjs/express) and [expressjs/compression](https://github.com/expressjs/compression)
* To serve, compute and render a GitHub user's metrics
* [nfriedly/express-rate-limit](https://github.com/nfriedly/express-rate-limit)
* To apply rate limiting on server and avoid spams and hitting GitHub API's own rate limit
* [octokit/graphql.js](https://github.com/octokit/graphql.js/) and [octokit/rest.js](https://github.com/octokit/rest.js)
* To perform request to GitHub GraphQL API and GitHub REST API
* [mde/ejs](https://github.com/mde/ejs)
* To render SVG images
* [ptarjan/node-cache](https://github.com/ptarjan/node-cache)
* To cache generated content
* [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64)
* To generate base64 representation of users' avatars
* [svg/svgo](https://github.com/svg/svgo)
* To optimize generated SVG
* [axios/axios](https://github.com/axios/axios)
* To make HTTP/S requests
* [actions/toolkit](https://github.com/actions/toolkit/tree/master) and [vercel/ncc](https://github.com/vercel/ncc)
* To build the GitHub Action
* [vuejs/vue](https://github.com/vuejs/vue)
* To display server application
All icons were ripped across GitHub's site, but still remains the intellectual property of GitHub. All icons were ripped across GitHub's site, but still remains the intellectual property of GitHub.
See [GitHub Logos and Usage](https://github.com/logos) for more information. See [GitHub Logos and Usage](https://github.com/logos) for more information.

View File

@@ -43,10 +43,10 @@ inputs:
default: no default: no
plugin_languages: plugin_languages:
description: Enable most used languages metrics description: Enable most used languages metrics
default: yes default: no
plugin_followup: plugin_followup:
description: Enable owned repositories issues and pull requests metrics description: Enable owned repositories issues and pull requests metrics
default: yes default: no
debug: debug:
description: Enable debug logs description: Enable debug logs
default: no default: no

File diff suppressed because one or more lines are too long

View File

@@ -28,7 +28,7 @@
} }
//Load configuration //Load configuration
const conf = await setup() const conf = await setup({log:false})
console.log(`Configuration | loaded`) console.log(`Configuration | loaded`)
//Load svg template, style and query //Load svg template, style and query

78
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "metrics", "name": "metrics",
"version": "1.9.0", "version": "2.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -37,9 +37,9 @@
} }
}, },
"@octokit/core": { "@octokit/core": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.1.3.tgz",
"integrity": "sha512-AInOFULmwOa7+NFi9F8DlDkm5qtZVmDQayi7TUgChE3yeIGPq0Y+6cAEXPexQ3Ea+uZy66hKEazR7DJyU+4wfw==", "integrity": "sha512-s5UyENGUQBB+ocEOulXq6UH5J16fxuKY2J7ZYrIu9oJYAn0nCwM8hC8o4L23HEzU0SFzNEX86+ffc1T3Vr2ybg==",
"requires": { "requires": {
"@octokit/auth-token": "^2.4.0", "@octokit/auth-token": "^2.4.0",
"@octokit/graphql": "^4.3.1", "@octokit/graphql": "^4.3.1",
@@ -78,9 +78,9 @@
} }
}, },
"@octokit/plugin-request-log": { "@octokit/plugin-request-log": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.1.tgz",
"integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==" "integrity": "sha512-d8vmiGAUGswxErdIGfpd0I2UHo2Cs7EaBDpFUZQ9UqYmA0s5/4XoMO4HBld73xGpCj2BvyVyQe2qd9e+/nvKwQ=="
}, },
"@octokit/plugin-rest-endpoint-methods": { "@octokit/plugin-rest-endpoint-methods": {
"version": "4.2.0", "version": "4.2.0",
@@ -136,9 +136,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "14.11.10", "version": "14.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.2.tgz",
"integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA==" "integrity": "sha512-jeYJU2kl7hL9U5xuI/BhKPZ4vqGM/OmK6whiFAXVhlstzZhVamWhDSmHyGLIp+RVyuF9/d0dqr2P85aFj4BvJg=="
}, },
"@types/q": { "@types/q": {
"version": "1.5.4", "version": "1.5.4",
@@ -1421,21 +1421,63 @@
} }
}, },
"string.prototype.trimend": { "string.prototype.trimend": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz",
"integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==",
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
}
} }
}, },
"string.prototype.trimstart": { "string.prototype.trimstart": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz",
"integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==",
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
}
} }
}, },
"string_decoder": { "string_decoder": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "metrics", "name": "metrics",
"version": "2.0.0", "version": "2.1.0",
"description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else", "description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {

View File

@@ -7,6 +7,7 @@
"port":3000, "//":"Listening port", "port":3000, "//":"Listening port",
"optimize":true, "//":"Optimize SVG image", "optimize":true, "//":"Optimize SVG image",
"debug":false, "//":"Debug mode", "debug":false, "//":"Debug mode",
"repositories":100, "//":"Number of repositories to use to compute metrics",
"templates":{ "//":"Template configuration", "templates":{ "//":"Template configuration",
"default":"classic", "//":"Default template", "default":"classic", "//":"Default template",
@@ -15,24 +16,24 @@
"plugins":{ "//":"Additional plugins (optional)", "plugins":{ "//":"Additional plugins (optional)",
"pagespeed":{ "//":"Pagespeed plugin", "pagespeed":{ "//":"Pagespeed plugin",
"enabled":false, "//":"Enable or disable PageSpeed metrics", "enabled":true, "//":"Enable or disable PageSpeed metrics",
"token":"******", "//":"Pagespeed token" "token":null, "//":"Pagespeed token"
}, },
"traffic":{ "//":"Traffic plugin (GitHub API token must be RW for this to work)", "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" "enabled":false, "//":"Enable or disable repositories total page views is last two weeks"
}, },
"lines":{ "//":"Lines plugin", "lines":{ "//":"Lines plugin",
"enabled":true, "//":"Enable or disable repositories total lines added/removed" "enabled":false, "//":"Enable or disable repositories total lines added/removed"
}, },
"habits":{ "//":"Habits plugin", "habits":{ "//":"Habits plugin",
"enabled":true, "//":"Enable or disable coding habits metrics", "enabled":false, "//":"Enable or disable coding habits metrics",
"from":50, "//":"Number of activity events to base habits on (up to 100)" "from":100, "//":"Number of activity events to base habits on (up to 100)"
}, },
"languages":{ "//":"Languages plugins", "languages":{ "//":"Languages plugins",
"enabled":true, "//":"Enable or disable most used languages metrics (*this plugin is enabled by default)" "enabled":true, "//":"Enable or disable most used languages metrics"
}, },
"followup":{ "//":"Follow-up plugin", "followup":{ "//":"Follow-up plugin",
"enabled":true, "//":"Enable owned repositories issues and pull requests metrics (*this plugin is enabled by default)" "enabled":true, "//":"Enable owned repositories issues and pull requests metrics"
} }
} }
} }

View File

@@ -13,6 +13,8 @@
<h1><a href="https://github.com/lowlighter/metrics">Generate your metrics !</a></h1> <h1><a href="https://github.com/lowlighter/metrics">Generate your metrics !</a></h1>
<template>
<div class="step"> <div class="step">
<h2>1. Enter your GitHub username</h2> <h2>1. Enter your GitHub username</h2>
<label> <label>
@@ -21,7 +23,7 @@
</div> </div>
<div class="step"> <div class="step">
<h2>2. Select a template and enable additional plugins</h2> <h2>2. Select a template {{ plugins.list.length ? "and enable additional plugins" : "" }}</h2>
<div class="templates"> <div class="templates">
<label v-for="template in templates.list" :key="template"> <label v-for="template in templates.list" :key="template">
<input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending"> <input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending">
@@ -34,7 +36,7 @@
{{ plugins.descriptions[plugin] || plugin }} {{ plugins.descriptions[plugin] || plugin }}
</label> </label>
</div> </div>
<div class="cache-notice"> <div class="cache-notice" v-if="plugins.list.length">
*To reduce server overhead, metrics are cached. Changes may not be reflected until cache expiration. *To reduce server overhead, metrics are cached. Changes may not be reflected until cache expiration.
</div> </div>
<div class="palette"> <div class="palette">
@@ -74,6 +76,8 @@
For even more features, setup <a href="https://github.com/lowlighter/metrics">lowlighter/metrics</a> as a <a href="https://github.com/marketplace/actions/github-metrics-as-svg-image">GitHub action</a> ! For even more features, setup <a href="https://github.com/lowlighter/metrics">lowlighter/metrics</a> as a <a href="https://github.com/marketplace/actions/github-metrics-as-svg-image">GitHub action</a> !
</div> </div>
</template>
</main> </main>
<script src="/axios.min.js"></script> <script src="/axios.min.js"></script>
@@ -91,7 +95,7 @@
palette:"light", palette:"light",
plugins:{ plugins:{
list:(await axios.get("/plugins.list")).data, list:(await axios.get("/plugins.list")).data,
enabled:{languages:true, followup:true}, enabled:{},
descriptions:{ descriptions:{
pagespeed:"Website performances", pagespeed:"Website performances",
languages:"Most used languages", languages:"Most used languages",
@@ -122,7 +126,7 @@
}, },
url() { url() {
const plugins = Object.entries(this.plugins.enabled) const plugins = Object.entries(this.plugins.enabled)
.filter(([key, value]) => /^(?:languages|followup)$/.test(key) ? !value : value) .filter(([key, value]) => value)
.map(([key, value]) => `${key}=${+value}`) .map(([key, value]) => `${key}=${+value}`)
.join("&") .join("&")
return `${window.location.href}${this.user}${plugins.length ? `?${plugins}` : ""}` return `${window.location.href}${this.user}${plugins.length ? `?${plugins}` : ""}`

View File

@@ -14,6 +14,7 @@
console.debug(`metrics/compute/${login} > start`) console.debug(`metrics/compute/${login} > start`)
console.debug(JSON.stringify(q)) console.debug(JSON.stringify(q))
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 pending = [] const pending = []
const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end
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))))
@@ -24,6 +25,7 @@
console.debug(`metrics/compute/${login} > query`) console.debug(`metrics/compute/${login} > query`)
const data = await graphql(query const data = await graphql(query
.replace(/[$]login/, `"${login}"`) .replace(/[$]login/, `"${login}"`)
.replace(/[$]repositories/, `${repositories}`)
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`) .replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
.replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`) .replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`)
) )

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default function ({login, data, computed, pending, q}, {enabled = true} = {}) { export default function ({login, data, computed, pending, q}, {enabled = false} = {}) {
//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.followup = null return computed.plugins.followup = null

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default function ({login, rest, computed, pending, q}, {enabled = false, from = 50} = {}) { export default function ({login, rest, computed, pending, q}, {enabled = false, from = 100} = {}) {
//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.habits = null return computed.plugins.habits = null
@@ -8,6 +8,12 @@
console.debug(`metrics/compute/${login}/plugins > habits`) console.debug(`metrics/compute/${login}/plugins > habits`)
computed.svg.height += 70 computed.svg.height += 70
//Parameter override
if (typeof q["habits.from"] === "number") {
from = Math.max(0, Math.min(from, q["habits.from"]))
console.debug(`metrics/compute/${login}/plugins > habits > events = ${from}`)
}
//Plugin execution //Plugin execution
pending.push(new Promise(async solve => { pending.push(new Promise(async solve => {
try { try {

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default function ({login, data, computed, pending, q}, {enabled = true} = {}) { export default function ({login, data, computed, pending, q}, {enabled = false} = {}) {
//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.languages = null return computed.plugins.languages = null

View File

@@ -3,10 +3,11 @@
import path from "path" import path from "path"
/** Setup */ /** Setup */
export default async function () { export default async function ({log = true} = {}) {
//Init //Init
console.debug(`metrics/setup > setup`) const logger = log ? console.debug : () => null
logger(`metrics/setup > setup`)
const templates = "src/templates" const templates = "src/templates"
const conf = { const conf = {
templates:{}, templates:{},
@@ -16,17 +17,19 @@
} }
//Load settings //Load settings
console.debug(`metrics/setup > load settings.json`) logger(`metrics/setup > load settings.json`)
if (fs.existsSync(path.resolve("settings.json"))) { if (fs.existsSync(path.resolve("settings.json"))) {
conf.settings = JSON.parse(`${await fs.promises.readFile(path.resolve("settings.json"))}`) conf.settings = JSON.parse(`${await fs.promises.readFile(path.resolve("settings.json"))}`)
console.debug(`metrics/setup > load settings.json > success`) logger(`metrics/setup > load settings.json > success`)
} }
else else
console.debug(`metrics/setup > load settings.json > (missing)`) logger(`metrics/setup > load settings.json > (missing)`)
if (!conf.settings.templates) if (!conf.settings.templates)
conf.settings.templates = {default:"classic", enabled:[]} conf.settings.templates = {default:"classic", enabled:[]}
if (!conf.settings.plugins)
conf.settings.plugins = {}
if (conf.settings.debug) if (conf.settings.debug)
console.debug(conf.settings) logger(conf.settings)
//Load templates //Load templates
if (fs.existsSync(path.resolve(templates))) { if (fs.existsSync(path.resolve(templates))) {
@@ -34,7 +37,7 @@
//Cache templates //Cache templates
if (/^index.mjs$/.test(name)) if (/^index.mjs$/.test(name))
continue continue
console.debug(`metrics/setup > load template [${name}]`) logger(`metrics/setup > load template [${name}]`)
const files = [ const files = [
`${templates}/${name}/query.graphql`, `${templates}/${name}/query.graphql`,
`${templates}/${name}/image.svg`, `${templates}/${name}/image.svg`,
@@ -43,14 +46,14 @@
] ]
const [query, image, placeholder, style] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`)) const [query, image, placeholder, style] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`))
conf.templates[name] = {query, image, placeholder, style} conf.templates[name] = {query, image, placeholder, style}
console.debug(`metrics/setup > load template [${name}] > success`) logger(`metrics/setup > load template [${name}] > success`)
//Debug //Debug
if (conf.settings.debug) { if (conf.settings.debug) {
Object.defineProperty(conf.templates, name, { Object.defineProperty(conf.templates, name, {
get() { get() {
console.debug(`metrics/setup > reload template [${name}]`) logger(`metrics/setup > reload template [${name}]`)
const [query, image, placeholder, style] = files.map(file => `${fs.readFileSync(path.resolve(file))}`) const [query, image, placeholder, style] = files.map(file => `${fs.readFileSync(path.resolve(file))}`)
console.debug(`metrics/setup > reload template [${name}] > success`) logger(`metrics/setup > reload template [${name}] > success`)
return {query, image, placeholder, style} return {query, image, placeholder, style}
} }
}) })
@@ -58,12 +61,12 @@
} }
} }
else { else {
console.debug(`metrics/setup > load templates from build`) logger(`metrics/setup > load templates from build`)
conf.templates = JSON.parse(Buffer.from(`<#assets>`, "base64").toString("utf8")) conf.templates = JSON.parse(Buffer.from(`<#assets>`, "base64").toString("utf8"))
} }
//Conf //Conf
console.debug(`metrics/setup > setup > success`) logger(`metrics/setup > setup > success`)
return conf return conf
} }

View File

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

View File

@@ -20,23 +20,66 @@
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}) const rest = new OctokitRest.Octokit({auth:token})
//Perform tests
await test.build()
for (const template of [
"classic"
]) {
for (const q of [
{},
{followup:1},
{languages:1},
{followup:1, languages:1},
{habits:1, "habits.events":1},
{lines:1},
{traffic:1},
{selfskip:1},
{pagespeed:1},
{followup:1, languages:1, habits:1, "habits.events":1, lines:1, traffic:1, selfskip:1, pagespeed:1}
]) {
await test.metrics({graphql, rest, q:{template, repositories:1, ...q}})
}
}
}
/** Metrics tests */
test.metrics = async function ({graphql, rest, q}) {
//Preparation
console.log(`### Checking metrics with plugins [${Object.keys(q).filter(key => /^\w+$/.test(key)).join(", ")}]`)
const plugins = {
lines:{enabled:true},
traffic:{enabled:true},
pagespeed:{enabled:true},
habits:{enabled:true},
selfskip:{enabled:true},
languages:{enabled:true},
followup:{enabled:true},
}
//Compute render //Compute render
const conf = await setup() console.log("#### Checking that SVG can be generated")
const rendered = await metrics({login:"lowlighter", q:{}}, {graphql, rest, plugins:{}, conf}) const conf = await setup({log:false})
const rendered = await metrics({login:"lowlighter", q}, {graphql, rest, plugins, conf})
//Ensure it's a well-formed SVG image //Ensure it's a well-formed SVG image
console.log("#### Checking that generated SVG can be parsed")
const parsed = libxmljs.parseXml(rendered) const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length) if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`) throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
}
/** Build test */
test.build = async function () {
//Ensure that action has been rebuild //Ensure that action has been rebuild
console.log("### Checking that code has been rebuild")
const action = `${await fs.promises.readFile(`${__dirname}/dist/index.js`)}` const action = `${await fs.promises.readFile(`${__dirname}/dist/index.js`)}`
const code = await build() const code = await build()
if (action !== code) if (action !== code)
throw new Error(`GitHub Action has not been rebuild. Run "npm run build" to solve this issue`) throw new Error(`GitHub Action has not been rebuild. Run "npm run build" to solve this issue`)
} }
//Main
//Main
if (/metrics.mjs/.test(process.argv[1])) { if (/metrics.mjs/.test(process.argv[1])) {
//Test //Test
await test() await test()