Extract co2 and hosting checks into separate module (#2899)

Co-authored-by: Peter Hedenskog <peter@soulgalore.com>
This commit is contained in:
Chris Adams 2020-02-29 09:29:22 +01:00 committed by GitHub
parent 835a57e437
commit 3fba6b2e01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 267 additions and 394 deletions

View File

@ -64,17 +64,12 @@ addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
firefox: latest
chrome: stable
before_install:
- firefox --version 2>/dev/null
- google-chrome --product-version
- python --version
env:
global:
- CXX=g++-4.8
notifications:
slack:
on_success: change

View File

@ -12,6 +12,7 @@ COPY docker/webpagereplay/LICENSE /webpagereplay/
RUN sudo apt-get update && sudo apt-get install libnss3-tools \
net-tools \
build-essential \
iproute2 -y && \
mkdir -p $HOME/.pki/nssdb && \
certutil -d $HOME/.pki/nssdb -N

View File

@ -1365,10 +1365,17 @@ module.exports.parseCommandLine = function parseCommandLine() {
.option('sustainable.disableHosting', {
type: 'boolean',
default: false,
describe: 'Disable the hosting check.',
describe:
'Disable the hosting check. Default we do a check to a local database of domains with green hosting provided by the Green Web Foundation',
group: 'Sustainable'
})
.option('sustainable.useGreenWebHostingAPI', {
type: 'boolean',
default: false,
describe:
'Instead of using the local copy of the hosting database, you can use the latest version through the Green Web Foundation API. This means sitespeed.io will make HTTP GET to the the hosting info.',
group: 'Sustainable'
})
.option('mobile', {
describe:
'Access pages as mobile a fake mobile device. Set UA and width/height. For Chrome it will use device Apple iPhone 6.',

View File

@ -1,170 +0,0 @@
'use strict';
const url = require('url');
// const log = require('intel').getLogger('sitespeedio.plugin.sustainable');
// Use the 1byte model for now from the Shift Project, and assume a US grid mix of around 519 g co2 for the time being.
const CO2_PER_KWH_IN_DC_GREY = 519;
const CO2_PER_KWH_IN_DC_GREEN = 33;
// the better way would be do to this with a weighted average of types of RE generation
// from solar, to wind, to biomass, and hydro and so on, based on how much they
// are used.
// For now, let's use a quoted figure from Ecotricity, which quotes OFGEM, the UK regulator
// and it's better than most quoted figures which pretend there is *no* footprint
// for RE.
// More here:
// https://www.ecotricity.co.uk/layout/set/popup/layout/set/print/for-your-home/britain-s-greenest-energy-company/lifecycle-carbon-emissions
// https://twitter.com/mrchrisadams/status/1227972969756086284
// https://en.wikipedia.org/wiki/Life-cycle_greenhouse-gas_emissions_of_energy_sources
// 33.4, but that's for the UK in 2014/15. Shouldn't be too off though. Probably lower now.
const CO2_PER_KWH_NETWORK_GREY = 495;
// these are the figures pulled from the 1byte model, there is a
// third figure to represent carbon emission from *making* the device used to access a site/app but we lieave it out as it relies on
// us knowing how long it's being used to read content
const KWH_PER_BYTE_IN_DC = 0.00000000072;
const KWH_PER_BYTE_FOR_NETWORK = 0.00000000152;
function getCO2PerByte(bytes, green) {
// return a CO2 figure for energy used to shift the corresponding
// the data transfer.
if (bytes < 1) {
return 0;
}
if (green) {
// if we have a green datacentre, use the lower figure for renewable energy
const Co2ForDC = bytes * KWH_PER_BYTE_IN_DC * CO2_PER_KWH_IN_DC_GREEN;
// but for the rest of the internet, we can't easily check, so assume
// grey for now
const Co2forNetwork =
bytes * KWH_PER_BYTE_FOR_NETWORK * CO2_PER_KWH_NETWORK_GREY;
return Co2ForDC + Co2forNetwork;
}
const KwHPerByte = KWH_PER_BYTE_IN_DC + KWH_PER_BYTE_FOR_NETWORK;
return bytes * KwHPerByte * CO2_PER_KWH_IN_DC_GREY;
}
function getCO2PerDomain(pageXray, greenDomains) {
const co2PerDomain = [];
for (let domain of Object.keys(pageXray.domains)) {
let co2;
if (greenDomains && greenDomains.indexOf(domain) > -1) {
co2 = getCO2PerByte(pageXray.domains[domain].transferSize, true);
} else {
co2 = getCO2PerByte(pageXray.domains[domain].transferSize);
}
co2PerDomain.push({
domain,
co2,
transferSize: pageXray.domains[domain].transferSize
});
}
co2PerDomain.sort(function(a, b) {
return b.co2 - a.co2;
});
return co2PerDomain;
}
function getCO2perPage(pageXray, green) {
// Accept an xray object, and if we receive a boolean as the second
// argument, we assume every request we make is sent to a server
// running on renwewable power.
// if we receive an array of domains, return a number accounting the
// reduced CO2 from green hosted domains
const domainCO2 = getCO2PerDomain(pageXray, green);
let totalCO2 = 0;
for (let domain of domainCO2) {
totalCO2 += domain.co2;
}
return totalCO2;
}
function getCO2PerContentType(pageXray, greenDomains) {
const co2PerContentType = {};
for (let asset of pageXray.assets) {
const domain = url.parse(asset.url).domain;
const transferSize = asset.transferSize;
const co2ForTransfer = getCO2PerByte(
transferSize,
greenDomains && greenDomains.indexOf(domain) > -1
);
const contentType = asset.type;
if (!co2PerContentType[contentType]) {
co2PerContentType[contentType] = { co2: 0, transferSize: 0 };
}
co2PerContentType[contentType].co2 += co2ForTransfer;
co2PerContentType[contentType].transferSize += transferSize;
}
// restructure and sort
const all = [];
for (let type of Object.keys(co2PerContentType)) {
all.push({
type,
co2: co2PerContentType[type].co2,
transferSize: co2PerContentType[type].transferSize
});
}
all.sort(function(a, b) {
return b.co2 - a.co2;
});
return all;
}
function dirtiestResources(pageXray, greenDomains) {
const allAssets = [];
for (let asset of pageXray.assets) {
const domain = url.parse(asset.url).domain;
const transferSize = asset.transferSize;
const co2ForTransfer = getCO2PerByte(
transferSize,
greenDomains && greenDomains.indexOf(domain) > -1
);
allAssets.push({ url: asset.url, co2: co2ForTransfer, transferSize });
}
allAssets.sort(function(a, b) {
return b.co2 - a.co2;
});
return allAssets.slice(0, allAssets.length > 10 ? 10 : allAssets.length);
}
function getCO2PerParty(pageXray, greenDomains) {
let firstParty = 0;
let thirdParty = 0;
// calculate co2 per first/third party
const firstPartyRegEx = pageXray.firstPartyRegEx;
for (let d of Object.keys(pageXray.domains)) {
if (!d.match(firstPartyRegEx)) {
thirdParty += getCO2PerByte(
pageXray.domains[d].transferSize,
greenDomains && greenDomains.indexOf(d) > -1
);
} else {
firstParty += getCO2PerByte(
pageXray.domains[d].transferSize,
greenDomains && greenDomains.indexOf(d) > -1
);
}
}
return { firstParty, thirdParty };
}
module.exports = {
perByte: getCO2PerByte,
perDomain: getCO2PerDomain,
perPage: getCO2perPage,
perParty: getCO2PerParty,
perContentType: getCO2PerContentType,
dirtiestResources
};

Binary file not shown.

View File

@ -1,61 +0,0 @@
'use strict';
const log = require('intel').getLogger('sitespeedio.plugin.sustainable');
const https = require('https');
async function getBody(url) {
// Return new promise
return new Promise(function(resolve, reject) {
// Do async job
const req = https.get(url, function(res) {
if (res.statusCode < 200 || res.statusCode >= 300) {
log.error(
'Could not get info from the Green Web Foundation API, %s for %s',
res.statusCode,
url
);
return reject(new Error(`Status Code: ${res.statusCode}`));
}
const data = [];
res.on('data', chunk => {
data.push(chunk);
});
res.on('end', () => resolve(Buffer.concat(data).toString()));
});
req.end();
});
}
async function greenDomains(pageXray) {
const domains = Object.keys(pageXray.domains);
try {
const allGreenCheckResults = JSON.parse(
await getBody(
`https://api.thegreenwebfoundation.org/v2/greencheckmulti/${JSON.stringify(
domains
)}`
)
);
const entries = Object.entries(allGreenCheckResults);
// TODO find the preferred way for assigning vars
// when making key value pairs , but only using the val
/* eslint-disable-next-line */
let greenEntries = entries.filter(function ([key, val]) {
return val.green;
});
/* eslint-disable-next-line */
return greenEntries.map(function ([key, val]) {
return val.url;
});
} catch (e) {
return [];
}
}
module.exports = {
greenDomains
};

View File

@ -3,10 +3,15 @@
const path = require('path');
const fs = require('fs');
const log = require('intel').getLogger('sitespeedio.plugin.sustainable');
const co2 = require('./co2');
const hosting = require('./hosting');
const Aggregator = require('./aggregator');
let tgwf;
try {
tgwf = require('@tgwf/co2');
} catch (e) {
tgwf = null;
}
const DEFAULT_METRICS_PAGE_SUMMARY = ['co2PerPageView', 'totalCO2'];
module.exports = {
open(context, options) {
@ -26,149 +31,175 @@ module.exports = {
);
},
async processMessage(message, queue) {
const make = this.make;
const aggregator = this.aggregator;
if (tgwf) {
const make = this.make;
const aggregator = this.aggregator;
switch (message.type) {
// When sitespeed.io starts, it sends a setup message on the queue
// That way, we can tell other plugins we exist (sustainable.setup)
// so others could build upon our data
// ... and we also register the pug file(s) for the HTML output
case 'sitespeedio.setup': {
queue.postMessage(make('sustainable.setup'));
// Add the HTML pugs
queue.postMessage(
make('html.pug', {
id: 'sustainable',
name: 'Sustainable Web',
pug: this.pug,
type: 'pageSummary'
})
);
queue.postMessage(
make('html.pug', {
id: 'sustainable',
name: 'Sustainable Web',
pug: this.pug,
type: 'run'
})
);
log.info('Sustainable is setup');
break;
}
case 'pagexray.run': {
// We got data for a URL, lets calculate co2, check green servers etc
const hostingGreenCheck =
this.sustainableOptions.disableHosting === true
? []
: await hosting.greenDomains(message.data);
const co2PerDomain = co2.perDomain(message.data, hostingGreenCheck);
const baseDomain = message.data.baseDomain;
const hostingInfo = {
green: false,
url: baseDomain
};
// is the base domain in our list of green domains?
if (hostingGreenCheck.indexOf(baseDomain) > -1) {
hostingInfo.green = true;
switch (message.type) {
// When sitespeed.io starts, it sends a setup message on the queue
// That way, we can tell other plugins we exist (sustainable.setup)
// so others could build upon our data
// ... and we also register the pug file(s) for the HTML output
case 'sitespeedio.setup': {
queue.postMessage(make('sustainable.setup'));
// Add the HTML pugs
queue.postMessage(
make('html.pug', {
id: 'sustainable',
name: 'Sustainable Web',
pug: this.pug,
type: 'pageSummary'
})
);
queue.postMessage(
make('html.pug', {
id: 'sustainable',
name: 'Sustainable Web',
pug: this.pug,
type: 'run'
})
);
log.info('Use the sustainable web plugin');
break;
}
const co2PerParty = co2.perParty(message.data, hostingGreenCheck);
// Fetch the resources with the largest CO2 impact. ie,
// the resources to optimise, host somewhere green, or contact
// a supplier about
const dirtiestResources = co2.dirtiestResources(
message.data,
hostingGreenCheck
);
case 'pagexray.run': {
// We got data for a URL, lets calculate co2, check green servers etc
const listOfDomains = Object.keys(message.data.domains);
const co2PerContentType = co2.perContentType(
message.data,
hostingGreenCheck
);
let hostingGreenCheck;
if (this.sustainableOptions.disableHosting === true) {
hostingGreenCheck = [];
} else {
hostingGreenCheck =
this.sustainableOptions.useGreenWebHostingAPI === true
? await tgwf.hosting.check(listOfDomains)
: await tgwf.hosting.check(
listOfDomains,
path.join(__dirname, 'data', 'green_urls_2020-01-20.db')
);
}
const co2PerPageView = co2.perPage(message.data, hostingGreenCheck);
const co2PerDomain = tgwf.co2.perDomain(
message.data,
hostingGreenCheck
);
const baseDomain = message.data.baseDomain;
const totalCO2 = this.sustainableOptions.pageViews
? this.sustainableOptions.pageViews * co2PerPageView
: co2PerPageView;
if (message.iteration === 1) {
this.firstRunsData[message.url] = {
co2PerDomain,
hostingGreenCheck,
dirtiestResources,
co2PerContentType
const hostingInfo = {
green: false,
url: baseDomain
};
}
// We get data per run, so we want to aggregate that per page (multiple runs per page)
// and per group/domain
aggregator.addStats(
{
co2PerPageView,
totalCO2,
hostingInfo,
co2PerDomain,
co2PerParty
},
message.group,
message.url
);
// We pass on the data we have, so the that HTML plugin can generate the HTML tab
// per run
queue.postMessage(
make(
'sustainable.run',
// is the base domain in our list of green domains?
if (hostingGreenCheck.indexOf(baseDomain) > -1) {
hostingInfo.green = true;
}
const co2PerParty = tgwf.co2.perParty(
message.data,
hostingGreenCheck
);
// Fetch the resources with the largest CO2 impact. ie,
// the resources to optimise, host somewhere green, or contact
// a supplier about
const dirtiestResources = tgwf.co2.dirtiestResources(
message.data,
hostingGreenCheck
);
const co2PerContentType = tgwf.co2.perContentType(
message.data,
hostingGreenCheck
);
const co2PerPageView = tgwf.co2.perPage(
message.data,
hostingGreenCheck
);
const totalCO2 = this.sustainableOptions.pageViews
? this.sustainableOptions.pageViews * co2PerPageView
: co2PerPageView;
if (message.iteration === 1) {
this.firstRunsData[message.url] = {
co2PerDomain,
hostingGreenCheck,
dirtiestResources,
co2PerContentType
};
}
// We get data per run, so we want to aggregate that per page (multiple runs per page)
// and per group/domain
aggregator.addStats(
{
co2PerPageView,
totalCO2,
hostingInfo,
co2PerDomain,
co2FirstParty: co2PerParty.firstParty,
co2ThirdParty: co2PerParty.thirdParty,
hostingGreenCheck,
dirtiestResources,
co2PerContentType
co2PerParty
},
{
url: message.url,
group: message.group,
runIndex: message.runIndex
}
)
); // Here we put the data that we got from that run
break;
}
case 'sitespeedio.summarize': {
// All URLs has been tested, now calculate the min/median/max c02 per page
// and push that info on the queue
const summaries = aggregator.summarize();
// Send each URL
for (let url of Object.keys(summaries.urls)) {
const extras = this.firstRunsData[url];
// Attach first run so we can show that extra data that we don't collect stats for
summaries.urls[url].firstRun = extras;
queue.postMessage(
make('sustainable.pageSummary', summaries.urls[url], {
url: url,
group: summaries.urlToGroup[url]
})
message.group,
message.url
);
// We pass on the data we have, so the that HTML plugin can generate the HTML tab
// per run
queue.postMessage(
make(
'sustainable.run',
{
co2PerPageView,
totalCO2,
hostingInfo,
co2PerDomain,
co2FirstParty: co2PerParty.firstParty,
co2ThirdParty: co2PerParty.thirdParty,
hostingGreenCheck,
dirtiestResources,
co2PerContentType
},
{
url: message.url,
group: message.group,
runIndex: message.runIndex
}
)
); // Here we put the data that we got from that run
break;
}
queue.postMessage(
make('sustainable.summary', summaries.groups['total'], {
group: 'total'
})
);
break;
case 'sitespeedio.summarize': {
// All URLs has been tested, now calculate the min/median/max c02 per page
// and push that info on the queue
const summaries = aggregator.summarize();
// Send each URL
for (let url of Object.keys(summaries.urls)) {
const extras = this.firstRunsData[url];
// Attach first run so we can show that extra data that we don't collect stats for
summaries.urls[url].firstRun = extras;
queue.postMessage(
make('sustainable.pageSummary', summaries.urls[url], {
url: url,
group: summaries.urlToGroup[url]
})
);
}
queue.postMessage(
make('sustainable.summary', summaries.groups['total'], {
group: 'total'
})
);
break;
}
}
} else {
log.info(
'Not using the sustainable web plugin since the dependencies is not installed'
);
}
}
};

View File

@ -1,26 +0,0 @@
## The plan
I totally had my head down before, this it the updated plan to work to, based on the original issue
### co2
- takes care of converting transfer to CO2
- mainly relies on a single function that works out emissions from transfer.
- if `green` is passed for transfer figure, we apply the green figure for transferring data
- add helper methods to breakdown by domain, and accounting for green/grey at a domain level
### hosting
- takes care of marking domains as green or not
- shields users from greenweb foundation API, returns only the domain list of green domains on a page, til we know we will use anything else.
- checks against an api to return the green/gray status, and maaaybe against a local sqlite database snapshot published if folk don't want to make reqs to an API
## Top resources
Use the page xray,and its list of assets. if we have hosting check enabled, account for emissios in list.
## Showing things in pug
How to present sensible figures
Checkout the helper for bytes https://github.com/sitespeedio/sitespeed.io/blob/green/lib/support/helpers/size.js

92
npm-shrinkwrap.json generated
View File

@ -206,6 +206,33 @@
}
}
},
"@tgwf/co2": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@tgwf/co2/-/co2-0.4.3.tgz",
"integrity": "sha512-mQKmyJ7IUIW0OdfPN8cTvtHK630C7AEEJLJ5zmBxwGe/tBMDzs66q3+Se2jqzTkYXL2SqMd8PC3RJ37Ye7vxrw==",
"optional": true,
"requires": {
"better-sqlite3": "^5.4.3",
"debug": "^4.1.1"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"optional": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
}
}
},
"@types/babel-types": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.2.tgz",
@ -660,6 +687,16 @@
"tweetnacl": "^0.14.3"
}
},
"better-sqlite3": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-5.4.3.tgz",
"integrity": "sha512-fPp+8f363qQIhuhLyjI4bu657J/FfMtgiiHKfaTsj3RWDkHlWC1yT7c6kHZDnBxzQVoAINuzg553qKmZ4F1rEw==",
"optional": true,
"requires": {
"integer": "^2.1.0",
"tar": "^4.4.10"
}
},
"bignumber.js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz",
@ -9445,6 +9482,12 @@
"xml-escape": "^1.0.0"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"optional": true
},
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@ -11013,6 +11056,15 @@
}
}
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"optional": true,
"requires": {
"minipass": "^2.6.0"
}
},
"fs-mkdirp-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz",
@ -11791,6 +11843,12 @@
}
}
},
"integer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/integer/-/integer-2.1.0.tgz",
"integrity": "sha512-vBtiSgrEiNocWvvZX1RVfeOKa2mCHLZQ2p9nkQkQZ/BvEiY+6CcUz0eyjvIiewjJoeNidzg2I+tpPJvpyspL1w==",
"optional": true
},
"intel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/intel/-/intel-1.2.0.tgz",
@ -12798,6 +12856,25 @@
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"optional": true,
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
@ -15008,6 +15085,21 @@
}
}
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"optional": true,
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
}
},
"teeny-request": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.2.1.tgz",

View File

@ -112,6 +112,9 @@
"webpagetest": "0.3.9",
"yargs": "15.1.0"
},
"optionalDependencies": {
"@tgwf/co2": "0.4.3"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-customizable"

View File

@ -1,6 +1,7 @@
'use strict';
const co2 = require('../lib/plugins/sustainable/co2');
const hosting = require('../lib/plugins/sustainable/hosting');
const tgwf = require('@tgwf/co2');
const co2 = tgwf.co2;
const hosting = tgwf.hosting;
const fs = require('fs');
const path = require('path');
@ -176,7 +177,7 @@ describe('sustainableWeb', function() {
const pageXrayRun = pages[0];
// TODO find a way to not hit the API each time
const greenDomains = await hosting.greenDomains(pageXrayRun);
const greenDomains = await hosting.checkPage(pageXrayRun);
expect(greenDomains)
.to.be.an('array')