Skip to content

Commit

Permalink
Merge branch 'next-18051/implement-lighthouse-cli-to-pipeline' into '…
Browse files Browse the repository at this point in the history
…trunk'

NEXT-18051 - implement lighthouse performance tests to CI

See merge request shopware/6/product/platform!7323
  • Loading branch information
shyim committed Jan 27, 2022
2 parents edf5264 + b183b7c commit 121c3c5
Show file tree
Hide file tree
Showing 10 changed files with 1,589 additions and 28 deletions.
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Expand Up @@ -23,6 +23,7 @@ include:
# include library files
- local: .gitlab/lib/rules.yml
- local: .gitlab/lib/datadog.yml
- local: .gitlab/lib/scripts.yml
# defines basic stuff and base templates
- local: .gitlab/base.yml
# put your jobs into the matching stage file you want it to run
Expand Down
1 change: 1 addition & 0 deletions .gitlab/base.yml
Expand Up @@ -34,6 +34,7 @@ variables:
- !reference [.rules, run]
- when: always
before_script:
- cp public/.htaccess.dist public/.htaccess
- composer run setup
- chown -R application:application .
- echo 'LogFormat "[httpd:access] %V:%p %h %l %u %t \"%r\" %>s bytesIn:%I bytesOut:%O reqTime:%{ms}T" dockerlog' > /opt/docker/etc/httpd/conf.d/20-custom-log.conf
Expand Down
12 changes: 12 additions & 0 deletions .gitlab/lib/scripts.yml
@@ -0,0 +1,12 @@
.scripts:
# install node $NODE_VERSION
install-node:
script:
- apk add curl bash wget coreutils || true
- apt-update && apt-get install curl bash || true
- curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
- export NVM_DIR="$HOME/.nvm"
- '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"'
- export NODE_VERSION="${NODE_VERSION:-14}"
- nvm install $NODE_VERSION
- nvm use $NODE_VERSION
2 changes: 1 addition & 1 deletion .gitlab/stages/05-build.yml
Expand Up @@ -3,7 +3,7 @@
Component Library:
extends: .base
stage: build
image: node:10.8
image: node:14.18
before_script: []
variables:
PROJECT_ROOT: $CI_PROJECT_DIR
Expand Down
33 changes: 33 additions & 0 deletions .gitlab/stages/08-performance.yml
@@ -0,0 +1,33 @@
# requires /.gitlab/base.yml

# performance stage - This files contains all jobs belonging to the performance stage

Lighthouse (Administration):
extends: .base
stage: unit
needs: []
services:
- name: mariadb:10.3
alias: database
variables:
APP_ENV: prod
DD_API_KEY: "$DATADOG_API_KEY"
rules:
- !reference [ .rules, skip ]
- !reference [ .rules, run ]
# always execute in merge train. PHP could in theory affect the admin jest tests
- !reference [ .rules, long-running ]
- changes:
- 'src/Administration/Resources/app/administration/**/*'
- .gitlab/stages/02-unit.yml
script:
- export NODE_VERSION=14
- !reference [.scripts, install-node, script]
- APP_ENV=prod bin/console framework:demodata
- APP_ENV=prod bin/console dal:refresh:index
- npm --prefix $ADMIN_PATH run lighthouse
coverage: '/^\s?All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
when: always
paths:
- build/artifacts/lighthouse-results
@@ -0,0 +1,9 @@
---
title: Implement lighthouse performance tests to CI
issue: NEXT-18051
author: Jannis Leifeld
author_email: j.leifeld@shopware.com
author_github: @jleifeld
---
# Administration
* Added Lighthouse CI tests to the pipeline
42 changes: 42 additions & 0 deletions public/.htaccess.dist
Expand Up @@ -41,4 +41,46 @@ DirectoryIndex index.php
</FilesMatch>
</IfModule>

# Deflate Compression by FileType
<IfModule mod_deflate.c>
#Force compression for mangled headers.
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
# Compress all output labeled with one of the following MIME-types
# (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
# and can remove the `<IFModule mod_filter.c>` and `</IFModule>` lines
# as `AddOutputFilterByType` is still in the core directives).
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/atom_xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/x-shockwave-flash
AddOutputFilterByType DEFLATE application/gpx+xml
AddOutputFilterByType DEFLATE application/atom+xml
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/vnd.api+json
AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
AddOutputFilterByType DEFLATE application/x-font-ttf
AddOutputFilterByType DEFLATE application/x-web-app-manifest+json
AddOutputFilterByType DEFLATE font/opentype
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE image/x-icon
AddOutputFilterByType DEFLATE text/x-component
AddOutputFilterByType DEFLATE application/font-woff
AddOutputFilterByType DEFLATE font/woff2
</IfModule>
</IfModule>

# END Shopware
219 changes: 219 additions & 0 deletions src/Administration/Resources/app/administration/lighthouse-tests.js
@@ -0,0 +1,219 @@
/* eslint-disable no-console */
const fse = require('fs-extra');
const path = require('path');
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const axios = require('axios');
const _get = require('lodash/get');


const APP_URL = process.env.APP_URL;
const PROJECT_ROOT = process.env.PROJECT_ROOT;
const DD_API_KEY = process.env.DD_API_KEY;

if (!APP_URL) {
throw new Error('The environment variable "APP_URL" have to be defined.');
}

if (!PROJECT_ROOT) {
throw new Error('The environment variable "PROJECT_ROOT" have to be defined.');
}

if (!DD_API_KEY) {
// eslint-disable-next-line no-console
console.warn('' +
'WARNING: The environment variable "DD_API_KEY" have to defined. ' +
'Otherwise it can\' send metrics to datadog.');
}

/**
*
* @param browser Browser
* @returns {Promise<void>}
*/
async function login(browser) {
console.log('LOGIN');
const page = await browser.newPage();

await page.setViewport({ width: 1280, height: 768 });
await page.goto(`${APP_URL}/admin`);

const usernameInput = await page.$('#sw-field--username');
const passwordInput = await page.$('#sw-field--password');
const loginButton = await page.$('button.sw-login__login-action');

await usernameInput.type('admin');
await passwordInput.type('shopware');
await loginButton.click();

await page.waitForNavigation();
await page.waitForSelector('.sw-dashboard-index__welcome-message');

await page.close();
}

async function iterateAsync(arr, handler) {
await arr.reduce(async (promise, value) => {
// This line will wait for the last async function to finish.
// The first iteration uses an already resolved Promise
// so, it will immediately continue.
await promise;

await handler(value);
}, Promise.resolve());
}

function getTimeStamp() {
return `${Math.floor(new Date().getTime() / 1000)}`;
}

function getScriptsSize(jsReport) {
return jsReport.audits['network-requests'].details.items
.filter((asset) => asset.resourceType === 'Script')
.reduce((totalSize, asset) => {
return totalSize + asset.resourceSize;
}, 0);
}


async function sendMetrics(metrics) {
console.log('SEND METRICS');

const METRIC_SCORE_MAP = {
// General scores
performance: 'categories.performance.score',
accessibility: 'categories.accessibility.score',
seo: 'categories.seo.score',
best_practices: 'categories.["best-practices"].score',
pwa: 'categories.pwa.score',
// Performance breakdown
first_contentful_paint: 'audits["first-contentful-paint"].numericValue',
speed_index: 'audits["speed-index"].numericValue',
largest_contentful_paint: 'audits["largest-contentful-paint"].numericValue',
time_to_interactive: 'audits["interactive"].numericValue',
total_blocking_time: 'audits["total-blocking-time"].numericValue',
cumulative_layout_shift: 'audits["cumulative-layout-shift"].numericValue',
server_response_time: 'audits["server-response-time"].numericValue'
};
const timeStamp = getTimeStamp();

const series = metrics.reduce((acc, metric) => {
acc.push(...Object.entries(METRIC_SCORE_MAP).map(([metricName, scorePath]) => {
return {
host: 'lighthouse',
type: 'gauge',
metric: `lighthouse.${metricName}.${metric.testName}`,
points: [[timeStamp, _get(metric.result.lhr, scorePath)]],
};
}));
acc.push({
host: 'lighthouse',
type: 'gauge',
metric: `lighthouse.total_bundle_size.${metric.testName}`,
points: [[timeStamp, getScriptsSize(metric.result.lhr)]],
});

return acc;
}, []);

if (!DD_API_KEY) return undefined;

return axios({
method: 'post',
url: 'https://api.datadoghq.eu/api/v1/series',
headers: {
'Content-Type': 'application/json',
'DD-API-KEY': DD_API_KEY,
},
data: {
series,
},
});
}

async function main() {
// create folder for artifacts
fse.mkdirpSync(path.join(PROJECT_ROOT, '/build/artifacts/lighthouse-results/'));

const PORT = 8041;

const browser = await puppeteer.launch({
args: [
`--remote-debugging-port=${PORT}`,
'--no-sandbox',
'--disable-setuid-sandbox',
],
// For debugging:
// headless: false,
slowMo: 50,
});

// Login into the admin so that we don't get redirected to login page
await login(browser);

// Test cases for lighthouse
const testCases = {
dashboard: async () => `${APP_URL}/admin#/sw/dashboard/index`,
productListing: async () => `${APP_URL}/admin#/sw/product/index`,
productDetail: async () => {
const page = await browser.newPage();

await page.goto(`${APP_URL}/admin#/sw/product/index`);
await page.waitForNavigation();
await page.waitForFunction(() => !document.querySelector('.sw-loader'));

await page.waitForSelector('.sw-data-grid__row--0');
await page.click('.sw-data-grid__row--0 a');

const url = page.url();
await page.close();

return url;
},
};

// Execute lighthouse tests
const lighthouseTests = [];

await iterateAsync(Object.entries(testCases), async ([testName, getTestUrl]) => {
console.log('MEASURE ', testName);
const url = await getTestUrl();
const result = await lighthouse(url, {
port: PORT,
disableStorageReset: true,
output: 'html',
formFactor: 'desktop',
screenEmulation: {
mobile: false,
width: 1360,
height: 768,
},
});

lighthouseTests.push({
testName: testName,
result: result,
});
});

// Save the result in files
lighthouseTests.forEach(({ testName, result }) => {
fse.outputFileSync(
path.join(PROJECT_ROOT, `/build/artifacts/lighthouse-results/${testName}.html`),
result.report,
);

// Output the result
console.log('-----');
console.log(`Report is written for "${testName}"`);
console.log('Performance score was', result.lhr.categories.performance.score * 100);
});

// Send results to dataDog
await sendMetrics(lighthouseTests);

// Close browser when all tests are finished
await browser.close();
}

main();

0 comments on commit 121c3c5

Please sign in to comment.