feat(core): add insights output (#575)
This commit is contained in:
@@ -5,6 +5,7 @@ import octokit from "@octokit/graphql"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import paths from "path"
|
import paths from "path"
|
||||||
import sgit from "simple-git"
|
import sgit from "simple-git"
|
||||||
|
import processes from "child_process"
|
||||||
import metrics from "../metrics/index.mjs"
|
import metrics from "../metrics/index.mjs"
|
||||||
import setup from "../metrics/setup.mjs"
|
import setup from "../metrics/setup.mjs"
|
||||||
import mocks from "../mocks/index.mjs"
|
import mocks from "../mocks/index.mjs"
|
||||||
@@ -96,8 +97,8 @@ async function wait(seconds) {
|
|||||||
...config
|
...config
|
||||||
} = metadata.plugins.core.inputs.action({core})
|
} = metadata.plugins.core.inputs.action({core})
|
||||||
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
|
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
|
||||||
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template].formats[0] ?? null
|
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template].formats[0] ?? null
|
||||||
const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf"}[_output] ?? _output)
|
const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf", insights:"html"}[_output] ?? _output)
|
||||||
|
|
||||||
//Docker image
|
//Docker image
|
||||||
if (_image)
|
if (_image)
|
||||||
@@ -248,6 +249,22 @@ async function wait(seconds) {
|
|||||||
Object.assign(q, config)
|
Object.assign(q, config)
|
||||||
if (/markdown/.test(convert))
|
if (/markdown/.test(convert))
|
||||||
info("Markdown cache", _markdown_cache)
|
info("Markdown cache", _markdown_cache)
|
||||||
|
if (/insights/.test(convert)) {
|
||||||
|
try {
|
||||||
|
await new Promise(async (solve, reject) => {
|
||||||
|
let stdout = ""
|
||||||
|
setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5*60*1000)
|
||||||
|
const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env:{...process.env, NO_SETTINGS: true }})
|
||||||
|
web.stdout.on("data", data => (console.debug(`web > ${data}`), stdout += data, /Server ready !/.test(stdout) ? solve() : null))
|
||||||
|
web.stderr.on("data", data => console.debug(`web > ${data}`))
|
||||||
|
})
|
||||||
|
info("Insights webserver", "ok")
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
info("Insights webserver", "(failed to initialize)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Base content
|
//Base content
|
||||||
info.break()
|
info.break()
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
|
|||||||
if (conf.settings["debug.headless"])
|
if (conf.settings["debug.headless"])
|
||||||
imports.puppeteer.headless = false
|
imports.puppeteer.headless = false
|
||||||
|
|
||||||
|
//Metrics insights
|
||||||
|
if (convert === "insights")
|
||||||
|
return metrics.insights.output({login, imports, conf}, {graphql, rest, Plugins, Templates})
|
||||||
|
|
||||||
//Partial parts
|
//Partial parts
|
||||||
{
|
{
|
||||||
data.partials = new Set([
|
data.partials = new Set([
|
||||||
@@ -209,3 +213,65 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Metrics insights
|
||||||
|
metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) {
|
||||||
|
const q = {
|
||||||
|
template:"classic",
|
||||||
|
achievements:true,
|
||||||
|
"achievements.threshold":"X",
|
||||||
|
isocalendar:true,
|
||||||
|
"isocalendar.duration":"full-year",
|
||||||
|
languages:true,
|
||||||
|
"languages.limit":0,
|
||||||
|
activity:true,
|
||||||
|
"activity.limit":100,
|
||||||
|
"activity.days":0,
|
||||||
|
notable:true,
|
||||||
|
followup:true,
|
||||||
|
"followup.sections":"repositories, user",
|
||||||
|
habits:true,
|
||||||
|
"habits.from":100,
|
||||||
|
"habits.days":7,
|
||||||
|
"habits.facts":false,
|
||||||
|
"habits.charts":true,
|
||||||
|
introduction:true
|
||||||
|
}
|
||||||
|
const plugins = {achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true, extras:false}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}, followup:{enabled:true}, habits:{enabled:true, extras:false}, introduction:{enabled:true}}
|
||||||
|
return metrics({login, q}, {graphql, rest, plugins, conf, convert:"json"}, {Plugins, Templates})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Metrics insights static render
|
||||||
|
metrics.insights.output = async function ({login, imports, conf}, {graphql, rest, Plugins, Templates}) {
|
||||||
|
//Server
|
||||||
|
console.debug(`metrics/compute/${login} > insights`)
|
||||||
|
const server = `http://localhost:${conf.settings.port}`
|
||||||
|
console.debug(`metrics/compute/${login} > insights > server on port ${conf.settings.port}`)
|
||||||
|
|
||||||
|
//Data processing
|
||||||
|
const browser = await imports.puppeteer.launch()
|
||||||
|
const page = await browser.newPage()
|
||||||
|
console.debug(`metrics/compute/${login} > insights > generating data`)
|
||||||
|
const json = JSON.stringify(await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates}))
|
||||||
|
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
|
||||||
|
await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef
|
||||||
|
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
|
||||||
|
await page.waitForSelector(".container .user", {timeout:10*60*1000})
|
||||||
|
|
||||||
|
//Rendering
|
||||||
|
console.debug(`metrics/compute/${login} > insights > rendering data`)
|
||||||
|
const rendered = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Metrics insights: ${login}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${await page.evaluate(() => document.querySelector("main").outerHTML)}
|
||||||
|
${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data:style}) => `<style>${style}</style>`).join("\n")}
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
await browser.close()
|
||||||
|
return {mime:"text/html", rendered}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ export default async function({log = true, nosettings = false, community = {}} =
|
|||||||
authenticated:null,
|
authenticated:null,
|
||||||
templates:{},
|
templates:{},
|
||||||
queries:{},
|
queries:{},
|
||||||
settings:{},
|
settings:{port:3000},
|
||||||
metadata:{},
|
metadata:{},
|
||||||
paths:{
|
paths:{
|
||||||
statics:__statics,
|
statics:__statics,
|
||||||
|
|||||||
@@ -174,34 +174,7 @@ export default async function({mock, nosettings} = {}) {
|
|||||||
}
|
}
|
||||||
//Compute metrics
|
//Compute metrics
|
||||||
console.debug(`metrics/app/${login}/insights > compute insights`)
|
console.debug(`metrics/app/${login}/insights > compute insights`)
|
||||||
const json = await metrics(
|
const json = await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates})
|
||||||
{
|
|
||||||
login,
|
|
||||||
q:{
|
|
||||||
template:"classic",
|
|
||||||
achievements:true,
|
|
||||||
"achievements.threshold":"X",
|
|
||||||
isocalendar:true,
|
|
||||||
"isocalendar.duration":"full-year",
|
|
||||||
languages:true,
|
|
||||||
"languages.limit":0,
|
|
||||||
activity:true,
|
|
||||||
"activity.limit":100,
|
|
||||||
"activity.days":0,
|
|
||||||
notable:true,
|
|
||||||
followup:true,
|
|
||||||
"followup.sections":"repositories, user",
|
|
||||||
habits:true,
|
|
||||||
"habits.from":100,
|
|
||||||
"habits.days":7,
|
|
||||||
"habits.facts":false,
|
|
||||||
"habits.charts":true,
|
|
||||||
introduction:true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}, followup:{enabled:true}, habits:{enabled:true}, introduction:{enabled:true}}, conf, convert:"json"},
|
|
||||||
{Plugins, Templates},
|
|
||||||
)
|
|
||||||
//Cache
|
//Cache
|
||||||
if ((!debug) && (cached)) {
|
if ((!debug) && (cached)) {
|
||||||
const maxage = Math.round(Number(req.query.cache))
|
const maxage = Math.round(Number(req.query.cache))
|
||||||
@@ -286,7 +259,7 @@ export default async function({mock, nosettings} = {}) {
|
|||||||
conf,
|
conf,
|
||||||
die:q["plugins.errors.fatal"] ?? false,
|
die:q["plugins.errors.fatal"] ?? false,
|
||||||
verify:q.verify ?? false,
|
verify:q.verify ?? false,
|
||||||
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
|
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null,
|
||||||
}, {Plugins, Templates})
|
}, {Plugins, Templates})
|
||||||
//Cache
|
//Cache
|
||||||
if ((!debug) && (cached)) {
|
if ((!debug) && (cached)) {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||||
}
|
}
|
||||||
catch (error) {}
|
catch (error) {}
|
||||||
|
//Embed
|
||||||
|
this.embed = !!(new URLSearchParams(location.search).get("embed"))
|
||||||
|
//From local storage
|
||||||
|
this.localstorage = !!(new URLSearchParams(location.search).get("localstorage"))
|
||||||
//User
|
//User
|
||||||
const user = location.pathname.split("/").pop()
|
const user = location.pathname.split("/").pop()
|
||||||
if ((user) && (user !== "about")) {
|
if ((user) && (user !== "about")) {
|
||||||
@@ -18,8 +22,6 @@
|
|||||||
else {
|
else {
|
||||||
this.searchable = true
|
this.searchable = true
|
||||||
}
|
}
|
||||||
//Embed
|
|
||||||
this.embed = !!(new URLSearchParams(location.search).get("embed"))
|
|
||||||
//Init
|
//Init
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
//GitHub limit tracker
|
//GitHub limit tracker
|
||||||
@@ -80,6 +82,10 @@
|
|||||||
this.error = null
|
this.error = null
|
||||||
this.metrics = null
|
this.metrics = null
|
||||||
this.pending = true
|
this.pending = true
|
||||||
|
if (this.localstorage) {
|
||||||
|
this.metrics = JSON.parse(localStorage.getItem("local.metrics") ?? "null")
|
||||||
|
return
|
||||||
|
}
|
||||||
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
|
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -143,6 +149,7 @@
|
|||||||
hosted: null,
|
hosted: null,
|
||||||
user: "",
|
user: "",
|
||||||
embed: false,
|
embed: false,
|
||||||
|
localstorage: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
|
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
|
||||||
palette: "light",
|
palette: "light",
|
||||||
|
|||||||
@@ -272,6 +272,21 @@ It is possible to convert output to PDF when using a markdown template by settin
|
|||||||
config_output: markdown-pdf
|
config_output: markdown-pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ✨ Render `Metrics insights` statically
|
||||||
|
|
||||||
|
It is possible to generate an HTML file containing `✨ Metrics insights` output by setting `config_output` to `insights`. Resulting output will already be pre-rendered and not contain any external sources (i.e. no JavaScript and style sheets).
|
||||||
|
|
||||||
|
> Note that like `✨ Metrics insights` content is not configurable.
|
||||||
|
|
||||||
|
#### ℹ️ Examples workflows
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: lowlighter/metrics@latest
|
||||||
|
with:
|
||||||
|
# ... other options
|
||||||
|
config_output: insights
|
||||||
|
```
|
||||||
|
|
||||||
### 🐳 Faster execution with prebuilt docker images
|
### 🐳 Faster execution with prebuilt docker images
|
||||||
|
|
||||||
If you're using the official release `lowlighter/metrics` as a GitHub Action (either a specific version, `@latest` or `@master`), it'll pull a prebuilt docker container image from [GitHub Container Registry](https://github.com/users/lowlighter/packages/container/package/metrics) which contains already installed dependencies which will cut execution time from ~5 minutes to ~1 minute.
|
If you're using the official release `lowlighter/metrics` as a GitHub Action (either a specific version, `@latest` or `@master`), it'll pull a prebuilt docker container image from [GitHub Container Registry](https://github.com/users/lowlighter/packages/container/package/metrics) which contains already installed dependencies which will cut execution time from ~5 minutes to ~1 minute.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
|
|||||||
pending.push((async () => {
|
pending.push((async () => {
|
||||||
try {
|
try {
|
||||||
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
|
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
|
||||||
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, {...plugins[name], extras:conf.settings?.extras?.features ?? conf.settings?.extras?.default ?? false})
|
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, {extras:conf.settings?.extras?.features ?? conf.settings?.extras?.default ?? false, ...plugins[name]})
|
||||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
|
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
@@ -197,13 +197,14 @@ inputs:
|
|||||||
type: string
|
type: string
|
||||||
default: auto
|
default: auto
|
||||||
values:
|
values:
|
||||||
- auto # Defaults to template default
|
- auto # Defaults to template default
|
||||||
- svg
|
- svg
|
||||||
- png # Does not support animations
|
- png # Does not support animations
|
||||||
- jpeg # Does not support animations and transparency
|
- jpeg # Does not support animations and transparency
|
||||||
- json # Outputs a JSON file instead of an image
|
- json # Outputs a JSON file instead of an image
|
||||||
- markdown # Outputs a Markdown file instead of an image
|
- markdown # Outputs a Markdown file instead of an image
|
||||||
- markdown-pdf # Outputs a Markdown file as PDF instead of an image
|
- markdown-pdf # Outputs a Markdown file as PDF instead of an image
|
||||||
|
- insights # Outputs a rendered HTML file of Metrics Insights
|
||||||
|
|
||||||
# Number of retries in case rendering fail
|
# Number of retries in case rendering fail
|
||||||
retries:
|
retries:
|
||||||
|
|||||||
Reference in New Issue
Block a user