diff --git a/api/collections/lighthouse-subpaths.yml b/api/collections/lighthouse-subpaths.yml new file mode 100644 index 0000000..d378cc6 --- /dev/null +++ b/api/collections/lighthouse-subpaths.yml @@ -0,0 +1,29 @@ +name: lighthouseSubpath + +meta: + label: Lighthouse Subpaths + muiIcon: web + views: + - type: table + columns: + - source: lighthouseSubpath + +permissions: + public: + methods: + get: false + post: false + put: false + delete: false + user: + methods: + get: true + post: true + put: true + delete: true + +fields: + - type: string + name: lighthouseSubpath + meta: + label: PagespeedPaths diff --git a/api/collections/lighthouse.yml b/api/collections/lighthouse.yml new file mode 100644 index 0000000..d76c5bd --- /dev/null +++ b/api/collections/lighthouse.yml @@ -0,0 +1,120 @@ +name: lighthouse + +meta: + label: Lighthouse + muiIcon: web + views: + - type: table + mediaQuery: "(min-width: 600px)" + columns: + - source: insertTime + filter: true + - source: perfomance + filter: true + - source: accessibility + filter: true + - source: bestPractices + filter: true + - source: seo + filter: true + - type: simpleList + mediaQuery: "(max-width: 599px)" + primaryText: insertTime + secondaryText: performance + tertiaryText: accessibility + +permissions: + public: + methods: + get: false + post: false + put: false + delete: false + user: + methods: + get: true + post: true + put: true + delete: true + +projections: + dashboard: + +hooks: + post: + create: + type: javascript + file: hooks/lighthouse/post_create.js + +fields: + - name: analyzedPaths + type: string[] + meta: + label: Analyzed Paths + - name: performance + type: number + meta: + label: Performance + - name: accessibility + type: number + meta: + label: Accessibility + - name: bestPractices + type: number + meta: + label: Best Practices + - name: seo + type: number + meta: + label: SEO + - name: lighthouseMetrics + + type: object + meta: + label: Lighthouse Metrics + subFields: + - name: FCPS + type: number + meta: + label: First Contentful Paint Score + - name: FCPV + type: number + meta: + label: First Contentful Paint Value + - name: FMPV + type: number + meta: + label: First Meaningful Paint Value + + - name: FMPS + type: number + meta: + label: First Meaningful Paint Score + + - name: SIS + type: number + meta: + label: Speed Index Score + + - name: SIV + type: number + meta: + label: Speed Index Value + - name: TTIS + type: number + meta: + label: Time to Interactive Score + + - name: TTIV + type: number + meta: + label: Time to Interactive Value + - name: FPIDS + type: number + meta: + label: First Potential Input Delay Score + + - name: FPIDV + type: number + meta: + label: First Potential Input Delay Value diff --git a/api/config.yml b/api/config.yml index 0d712fd..1e0f2d7 100644 --- a/api/config.yml +++ b/api/config.yml @@ -7,9 +7,248 @@ meta: servers: - url: https://tibi-admin-server.code.testversion.online/api/v1/_/demo description: code-server - dashboard: majorItems: + - type: "sectionTitle" + title: { de: "Website Perfomance", en: "Website Perfomance" } + appendix: + collection: lighthouse + eval: | + (function(){ + return " " + new Date($date).toLocaleDateString() + "" + })() + - type: graph + filter: false + graphType: radialBar + until: "lastYear" + value: total + containerProps: + #optional class prop + layout: + breakBefore: false + breakAfter: false + size: + default: "col-6" + small: "col-12" + large: "col-3" + options: + { + property: plotOptions, + value: + { + radialBar: + { + hollow: { margin: 0, size: "70%" }, + track: { dropShadow: { enabled: true, top: 2, left: 0, blur: 4, opacity: 0.15 } }, + dataLabels: + { + name: { offsetY: -10, color: "#000", fontSize: "13px" }, + value: { color: "#000", fontSize: "30px", show: true }, + }, + }, + }, + } + graphs: + - collection: lighthouse + field: performance + yAxis: latestValue + graphName: { de: "Perfomance Score", en: "Perfomance Score" } + dateTimeField: insertTime + + - type: graph + filter: false + graphType: radialBar + until: "lastYear" + value: total + containerProps: + #optional class prop + layout: + breakBefore: false + breakAfter: false + size: + default: "col-6" + small: "col-12" + large: "col-3" + options: + { + property: plotOptions, + value: + { + radialBar: + { + hollow: { margin: 0, size: "70%" }, + track: { dropShadow: { enabled: true, top: 2, left: 0, blur: 4, opacity: 0.15 } }, + dataLabels: + { + name: { offsetY: -10, color: "#000", fontSize: "13px" }, + value: { color: "#000", fontSize: "30px", show: true }, + }, + }, + }, + } + graphs: + - collection: lighthouse + field: accessibility + yAxis: latestValue + graphName: { de: "Accessibility Score", en: "Accessibility Score" } + dateTimeField: insertTime + + - type: graph + filter: false + graphType: radialBar + until: "lastYear" + value: total + containerProps: + #optional class prop + layout: + breakBefore: false + breakAfter: false + size: + default: "col-6" + small: "col-12" + large: "col-3" + options: + { + property: plotOptions, + value: + { + radialBar: + { + hollow: { margin: 0, size: "70%" }, + track: { dropShadow: { enabled: true, top: 2, left: 0, blur: 4, opacity: 0.15 } }, + dataLabels: + { + name: { offsetY: -10, color: "#000", fontSize: "13px" }, + value: { color: "#000", fontSize: "30px", show: true }, + }, + }, + }, + } + graphs: + - collection: lighthouse + field: bestPractices + yAxis: latestValue + graphName: { de: "Best Practices Score", en: "Best Practices Score" } + dateTimeField: insertTime + + - type: graph + filter: false + graphType: radialBar + until: "lastYear" + value: total + containerProps: + #optional class prop + layout: + breakBefore: false + breakAfter: false + size: + default: "col-6" + small: "col-12" + large: "col-3" + options: + { + property: plotOptions, + value: + { + radialBar: + { + hollow: { margin: 0, size: "70%" }, + track: { dropShadow: { enabled: true, top: 2, left: 0, blur: 4, opacity: 0.15 } }, + dataLabels: + { + name: { offsetY: -10, color: "#000", fontSize: "13px" }, + value: { color: "#000", fontSize: "30px", show: true }, + }, + }, + }, + } + graphs: + - collection: lighthouse + field: seo + yAxis: latestValue + graphName: { de: "SEO Score", en: "SEO Score" } + dateTimeField: insertTime + + - type: swiper # Art des Elements, hier ein Swiper + containerProps: + #optional class prop + layout: + breakBefore: false + breakAfter: false + size: + default: "col-12" + small: "col-12" + large: "col-6 row-2-4" + + elements: # Liste der Elemente in diesem Swiper + - type: graph + title: + value: { de: "Ladezeit (Score)", en: "Loadtime (Score)" } + xAxis: manual + until: "lastYear" + filter: false #deaktiviert die Filter möglichkeit für den Nutzer beim diagramm, normalerweise aktiviert. Hierbei sind alle kombinationen x >= until möglich + columns: + - name: { de: '["Erstes sichtbares", "Element"]', en: '["First Contentful", "Paint"]' } + field: lighthouseMetrics.FCPS + + - name: { de: '["Erstes bedeutsames", "Element"]', en: '["First Meaningful", "Paint"]' } + field: lighthouseMetrics.FMPS + + - name: + { + de: '["Maximale potenzielle", "erste", "ingabeverzögerung"]', + en: '["Max Potential", "First Input", "Delay"]', + } + field: lighthouseMetrics.FPIDS + + - name: { de: '["Zeit bis", "zur", "Interaktivität"]', en: '["Time to", "Interactive"]' } + field: lighthouseMetrics.TTIS + + - name: { de: '["Geschwindigkeitsindex"]', en: '["Speed Index"]' } + field: lighthouseMetrics.SIS + + graphType: "bar" + graphs: + - graphName: { de: "Lighthouse Metriken", en: "Lighthouse Metrics" } + yAxis: latestValue + collection: lighthouse + dateTimeField: insertTime + - type: graph + title: + value: { de: "Ladezeit (Sekunden)", en: "Loadtime (seconds)" } + xAxis: manual + until: "lastYear" + filter: false #deaktiviert die Filter möglichkeit für den Nutzer beim diagramm, normalerweise aktiviert. Hierbei sind alle kombinationen x >= until möglich + columns: + - name: { de: '["Erstes sichtbares", "Element"]', en: '["First Contentful", "Paint"]' } + field: lighthouseMetrics.FCPV + + - name: { de: '["Erstes bedeutsames", "Element"]', en: '["First Meaningful", "Paint"]' } + field: lighthouseMetrics.FMPV + + - name: + { + de: '["Maximale potenzielle", "erste", "ingabeverzögerung"]', + en: '["Max Potential", "First Input", "Delay"]', + } + field: lighthouseMetrics.FPIDV + + - name: { de: '["Zeit bis", "zur", "Interaktivität"]', en: '["Time to", "Interactive"]' } + field: lighthouseMetrics.TTIV + + - name: { de: '["Geschwindigkeitsindex"]', en: '["Speed Index"]' } + field: lighthouseMetrics.SIV + + graphType: "bar" + graphs: + - graphName: { de: "Lighthouse Metriken", en: "Lighthouse Metrics" } + yAxis: latestValue + collection: lighthouse + dateTimeField: insertTime + + - type: "sectionTitle" + title: { de: "Seiteninhalte", en: "Page content" } + - collection: navigation type: reference style: @@ -34,6 +273,25 @@ meta: upper: rgba(3, 50, 59, 0.7) lower: rgba(3, 50, 59) + - type: "sectionTitle" + title: { de: "Aktionen", en: "Actions" } + + - collection: lighthouse + type: action + action: "Lighthouse Durchlauf starten" + backgroundAction: true + modalText: + { + de: "Zur Analyse der Website werden einige Zeitintensive prozesse gestartet, daher wird dies im Hintergrund ausgeführt. Es kann einige Minuten dauern bis das Dashboard aktuallisiert wird, bitte haben Sie etwas Geduld.", + en: "To analyze the website, some time-intensive processes are started, so this is done in the background. It may take a few minutes for the dashboard to be updated, please be patient.", + } + properties: + url: https://www.fontis.de + type: post + style: + upper: rgba(3, 50, 59, 0.7) + lower: rgba(3, 50, 59) + minorItems: - collection: page subNavigation: 0 @@ -49,7 +307,14 @@ collections: - !include collections/medialib.yml - !include collections/backups.yml - !include collections/ssr.yml + - !include collections/lighthouse.yml + - !include collections/lighthouse-subpaths.yml assets: - name: img path: img + +jobs: + - cron: "0 0 * * 1" + type: javascript + file: jobs/lighthouse.js diff --git a/api/hooks/config.js b/api/hooks/config.js index e76391d..015606d 100644 --- a/api/hooks/config.js +++ b/api/hooks/config.js @@ -29,4 +29,5 @@ module.exports = { return -1 }, ssrPublishCheckCollections: ["page"], + LIGHTHOUSE_TOKEN: "AIzaSyC0UxHp3-MpJiDL3ws7pEV6lj57bfIc7GQ", } diff --git a/api/hooks/lib/utils.js b/api/hooks/lib/utils.js index 922ef24..65836f9 100644 --- a/api/hooks/lib/utils.js +++ b/api/hooks/lib/utils.js @@ -46,8 +46,103 @@ function clearSSRCache() { context.response.header("X-SSR-Cleared", info.removed) } +var { LIGHTHOUSE_TOKEN } = require("../config") +function calculateAverageDynamically(dbObjs) { + const sumObj = {} + let count = 0 + + dbObjs.forEach((obj) => { + accumulate(obj, sumObj) + count++ + }) + + function accumulate(sourceObj, targetObj) { + for (const key in sourceObj) { + if (typeof sourceObj[key] === "number") { + targetObj[key] = (targetObj[key] || 0) + sourceObj[key] + } else if (typeof sourceObj[key] === "object" && sourceObj[key] !== null) { + targetObj[key] = targetObj[key] || {} + accumulate(sourceObj[key], targetObj[key]) + } + } + } + + function average(targetObj) { + for (const key in targetObj) { + if (typeof targetObj[key] === "number") { + targetObj[key] = targetObj[key] / count + } else if (typeof targetObj[key] === "object") { + average(targetObj[key]) + } + } + } + + average(sumObj) + return sumObj +} + +function run(url) { + const response = context.http + .fetch(url, { + timeout: 300, + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .body.json() + // needs enough traffic to be collected + const cruxMetrics = { + "First Contentful Paint": response?.loadingExperience?.metrics?.FIRST_CONTENTFUL_PAINT_MS?.category, + "First Input Delay": response?.loadingExperience?.metrics?.FIRST_INPUT_DELAY_MS?.category, + } + const lighthouse = response.lighthouseResult + const lighthouseMetrics = { + FCPS: lighthouse.audits["first-contentful-paint"].score * 100, + FCPV: lighthouse.audits["first-contentful-paint"].numericValue / 1000, + FMPS: lighthouse.audits["first-meaningful-paint"].score * 100, + FMPV: lighthouse.audits["first-meaningful-paint"].numericValue / 1000, + + SIS: lighthouse.audits["speed-index"].score * 100, + SIV: lighthouse.audits["speed-index"].numericValue / 1000, + TTIS: lighthouse.audits["interactive"].score * 100, + TTIV: lighthouse.audits["interactive"].numericValue / 1000, + + FPIDS: lighthouse.audits["max-potential-fid"].score * 100, + FPIDV: lighthouse.audits["max-potential-fid"].numericValue / 1000, + } + + let dbObject = { + cruxMetrics, + lighthouseMetrics, + performance: Math.round(lighthouse.categories.performance.score * 100), + accessibility: Math.round(lighthouse.categories.accessibility.score * 100), + bestPractices: Math.round(lighthouse.categories["best-practices"].score * 100), + seo: Math.round(lighthouse.categories.seo.score * 100), + } + return dbObject +} +function setUpQuery(subPath = "/") { + const api = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed" + let params = `category=performance&category=accessibility&category=best-practices&category=seo` + + const parameters = { + url: encodeURIComponent(`https://www.fontis.de/${subPath}`), + key: LIGHTHOUSE_TOKEN, + } + + let query = `${api}?` + for (let key in parameters) { + query += `${key}=${parameters[key]}&` + } + query += params // Append other parameters without URL encoding + return query +} module.exports = { log, clearSSRCache, obj2str, + run, + setUpQuery, + calculateAverageDynamically, } diff --git a/api/hooks/lighthouse/post_create.js b/api/hooks/lighthouse/post_create.js new file mode 100644 index 0000000..e3f1038 --- /dev/null +++ b/api/hooks/lighthouse/post_create.js @@ -0,0 +1,16 @@ +var { setUpQuery, calculateAverageDynamically, run } = require("../lib/utils") +;(function () { + let subPaths = context.db.find("lighthouseSubpath") + let urls = [] + for (let i = 0; i < subPaths.length; i++) { + urls.push(setUpQuery(subPaths[i].lighthouseSubpath)) + } + let dbObjs = [] + urls.forEach((url) => { + console.log("URL:", url) + dbObjs.push(run(url)) + }) + let dbObject = calculateAverageDynamically(dbObjs) + dbObject.analyzedPaths = [...subPaths].map((subPath) => subPath.lighthouseSubpath) + return { data: dbObject } +})() diff --git a/api/jobs/lighthouse.js b/api/jobs/lighthouse.js new file mode 100644 index 0000000..c13a13d --- /dev/null +++ b/api/jobs/lighthouse.js @@ -0,0 +1,17 @@ +var { setUpQuery, calculateAverageDynamically, run } = require("../hooks/lib/utils") +;(function () { + console.log("Running lighthouse job") + let subPaths = context.db.find("lighthouseSubpath") + let urls = [] + for (let i = 0; i < subPaths.length; i++) { + urls.push(setUpQuery(subPaths[i].lighthouseSubpath)) + } + let dbObjs = [] + urls.forEach((url) => { + console.log("URL:", url) + dbObjs.push(run(url)) + }) + let dbObject = calculateAverageDynamically(dbObjs) + dbObject.analyzedPaths = [...subPaths].map((subPath) => subPath.lighthouseSubpath) + context.db.create("lighthouse", dbObject) +})()