diff --git a/src/co2.js b/src/co2.js index e2e2549..e3daa5a 100644 --- a/src/co2.js +++ b/src/co2.js @@ -152,11 +152,16 @@ class CO2 { * @return {CO2EstimateTraceResultPerByte} the amount of CO2 in grammes */ perByteTrace(bytes, green = false, options = {}) { - let adjustments = {}; - if (options) { - // If there are options, parse them and add them to the model. - adjustments = parseOptions(options); - } + const adjustments = parseOptions(options, this.model.version, green); + + // Filter out the trace items that aren't relevant to this function. + const { gridIntensity, ...traceVariables } = adjustments; + const { + dataReloadRatio, + firstVisitPercentage, + returnVisitPercentage, + ...otherVariables + } = traceVariables; return { co2: this.model.perByte( bytes, @@ -173,16 +178,9 @@ class CO2 { gridIntensity: { description: "The grid intensity (grams per kilowatt-hour) used to calculate this CO2 estimate.", - network: - adjustments?.gridIntensity?.network?.value ?? GLOBAL_GRID_INTENSITY, - dataCenter: green - ? RENEWABLES_GRID_INTENSITY - : adjustments?.gridIntensity?.dataCenter?.value ?? - GLOBAL_GRID_INTENSITY, - production: GLOBAL_GRID_INTENSITY, - device: - adjustments?.gridIntensity?.device?.value ?? GLOBAL_GRID_INTENSITY, + ...adjustments.gridIntensity, }, + ...otherVariables, }, }; } @@ -199,11 +197,8 @@ class CO2 { */ perVisitTrace(bytes, green = false, options = {}) { if (this.model?.perVisit) { - let adjustments = {}; - if (options) { - // If there are options, parse them and add them to the model. - adjustments = parseOptions(options); - } + const adjustments = parseOptions(options, this.model.version, green); + const { gridIntensity, ...variables } = adjustments; return { co2: this.model.perVisit( @@ -221,21 +216,9 @@ class CO2 { gridIntensity: { description: "The grid intensity (grams per kilowatt-hour) used to calculate this CO2 estimate.", - network: - adjustments?.gridIntensity?.network?.value ?? - GLOBAL_GRID_INTENSITY, - dataCenter: green - ? RENEWABLES_GRID_INTENSITY - : adjustments?.gridIntensity?.dataCenter?.value ?? - GLOBAL_GRID_INTENSITY, - production: GLOBAL_GRID_INTENSITY, - device: - adjustments?.gridIntensity?.device?.value ?? - GLOBAL_GRID_INTENSITY, + ...adjustments.gridIntensity, }, - dataReloadRatio: adjustments?.dataReloadRatio ?? 0.02, - firstVisitPercentage: adjustments?.firstVisitPercentage ?? 0.75, - returnVisitPercentage: adjustments?.returnVisitPercentage ?? 0.25, + ...variables, }, }; } else { diff --git a/src/co2.test.js b/src/co2.test.js index b6ed814..5ea635a 100644 --- a/src/co2.test.js +++ b/src/co2.test.js @@ -4,8 +4,10 @@ import { MILLION, SWDV3 } from "./constants/test-constants.js"; import CO2 from "./co2.js"; import { averageIntensity, marginalIntensity } from "./index.js"; +import { SWDV4 } from "./constants/index.js"; const TwnGridIntensityValue = averageIntensity.data["TWN"]; +const SWDM4_GLOBAL_GRID_INTENSITY = SWDV4.GLOBAL_GRID_INTENSITY; describe("co2", () => { let co2; @@ -265,6 +267,33 @@ describe("co2", () => { co2.perVisit(MILLION, true) ); }); + + it("returns the expected properties", () => { + expect(co2.perByteTrace(MILLION)).toHaveProperty("co2"); + expect(co2.perByteTrace(MILLION)).toHaveProperty("variables"); + expect(co2.perByteTrace(MILLION)).toHaveProperty("green"); + expect(co2.perByteTrace(MILLION)).toHaveProperty( + "variables.gridIntensity" + ); + expect(co2.perByteTrace(MILLION)).not.toHaveProperty( + "variables.firstVisitPercentage" + ); + expect(co2.perVisitTrace(MILLION)).toHaveProperty("co2"); + expect(co2.perVisitTrace(MILLION)).toHaveProperty("variables"); + expect(co2.perVisitTrace(MILLION)).toHaveProperty("green"); + expect(co2.perVisitTrace(MILLION)).toHaveProperty( + "variables.gridIntensity" + ); + expect(co2.perVisitTrace(MILLION)).toHaveProperty( + "variables.firstVisitPercentage" + ); + expect(co2.perVisitTrace(MILLION)).toHaveProperty( + "variables.returnVisitPercentage" + ); + expect(co2.perVisitTrace(MILLION)).toHaveProperty( + "variables.dataReloadRatio" + ); + }); }); describe("Using custom grid intensity", () => { @@ -748,9 +777,9 @@ describe("co2", () => { }); const { dataCenter, network, device } = perByteTraceResult.variables.gridIntensity; - expect(dataCenter).toBe(0); - expect(network).toBe(0); - expect(device).toBe(0); + expect(dataCenter).toStrictEqual({ value: 0 }); + expect(network).toStrictEqual({ value: 0 }); + expect(device).toStrictEqual({ value: 0 }); }); it("expects perVisitTrace to support values equal to 0", () => { @@ -771,9 +800,9 @@ describe("co2", () => { expect(dataReloadRatio).toBe(0); expect(firstVisitPercentage).toBe(0); expect(returnVisitPercentage).toBe(0); - expect(dataCenter).toBe(0); - expect(network).toBe(0); - expect(device).toBe(0); + expect(dataCenter).toStrictEqual({ value: 0 }); + expect(network).toStrictEqual({ value: 0 }); + expect(device).toStrictEqual({ value: 0 }); }); it("expects perByteTrace segments to be 0 when grid intensity is 0", () => { const perByteTraceResult = co2.perByteTrace(1000000, false, { @@ -880,4 +909,120 @@ describe("co2", () => { expect(co2SWDV4.model.version).toBe(4); }); }); + + describe("Using the perByteTrace method in SWDM v4", () => { + const co2 = new CO2({ model: "swd", version: 4 }); + it("returns the expected object", () => { + const res = co2.perByteTrace(MILLION); + expect(res).toHaveProperty("variables"); + expect(res).toHaveProperty("co2"); + expect(res).toHaveProperty("green"); + + expect(res.variables).toHaveProperty("bytes"); + expect(res.variables).toHaveProperty("gridIntensity"); + expect(res.variables).toHaveProperty("greenHostingFactor"); + expect(res.variables).not.toHaveProperty("dataReloadRatio"); + + expect(res.variables.gridIntensity).toHaveProperty("device"); + expect(res.variables.gridIntensity).toHaveProperty("dataCenter"); + expect(res.variables.gridIntensity).toHaveProperty("network"); + + expect(res.co2).toBeGreaterThan(0); + expect(res.variables.greenHostingFactor).toBe(0); + expect(res.green).toBe(false); + expect(res.variables.gridIntensity.device.value).toBe( + SWDM4_GLOBAL_GRID_INTENSITY + ); + expect(res.variables.gridIntensity.dataCenter.value).toBe( + SWDM4_GLOBAL_GRID_INTENSITY + ); + expect(res.variables.gridIntensity.network.value).toBe( + SWDM4_GLOBAL_GRID_INTENSITY + ); + }); + it("returns the expected object when adjustments are made", () => { + const res = co2.perByteTrace(MILLION, false, { + gridIntensity: { + dataCenter: 300, + network: 200, + device: { country: "TWN" }, + }, + greenHostingFactor: 0.5, + }); + + expect(res.variables.greenHostingFactor).toBe(0.5); + expect(res.green).toBe(false); + expect(res.variables.gridIntensity.device.country).toBe("TWN"); + expect(res.variables.gridIntensity.dataCenter.value).toBe(300); + expect(res.variables.gridIntensity.network.value).toBe(200); + }); + it("returns the expected greenHosting factor when green is set", () => { + const res = co2.perByteTrace(MILLION, true, { + greenHostingFactor: 0.5, + }); + + expect(res.variables.greenHostingFactor).toBe(1); + }); + }); + + describe("Using the perVisitTrace method in SWDM v4", () => { + const co2 = new CO2({ model: "swd", version: 4 }); + it("returns the expected object", () => { + const res = co2.perVisitTrace(MILLION); + // console.log(res); + expect(res).toHaveProperty("variables"); + expect(res).toHaveProperty("co2"); + expect(res).toHaveProperty("green"); + + expect(res.variables).toHaveProperty("bytes"); + expect(res.variables).toHaveProperty("gridIntensity"); + expect(res.variables).toHaveProperty("greenHostingFactor"); + expect(res.variables).toHaveProperty("firstVisitPercentage"); + expect(res.variables).toHaveProperty("returnVisitPercentage"); + expect(res.variables).toHaveProperty("dataReloadRatio"); + + expect(res.variables.gridIntensity).toHaveProperty("device"); + expect(res.variables.gridIntensity).toHaveProperty("dataCenter"); + expect(res.variables.gridIntensity).toHaveProperty("network"); + + expect(res.co2).toBeGreaterThan(0); + expect(res.variables.greenHostingFactor).toBe(0); + expect(res.green).toBe(false); + expect(res.variables.firstVisitPercentage).toBe(1); + expect(res.variables.returnVisitPercentage).toBe(0); + expect(res.variables.dataReloadRatio).toBe(0); + expect(res.variables.gridIntensity.device.value).toBe( + SWDM4_GLOBAL_GRID_INTENSITY + ); + expect(res.variables.gridIntensity.dataCenter.value).toBe( + SWDM4_GLOBAL_GRID_INTENSITY + ); + expect(res.variables.gridIntensity.network.value).toBe( + SWDM4_GLOBAL_GRID_INTENSITY + ); + }); + it("returns the expected object when adjustments are made", () => { + const res = co2.perByteTrace(MILLION, false, { + gridIntensity: { + dataCenter: 300, + network: 200, + device: { country: "TWN" }, + }, + greenHostingFactor: 0.5, + }); + + expect(res.variables.greenHostingFactor).toBe(0.5); + expect(res.green).toBe(false); + expect(res.variables.gridIntensity.device.country).toBe("TWN"); + expect(res.variables.gridIntensity.dataCenter.value).toBe(300); + expect(res.variables.gridIntensity.network.value).toBe(200); + }); + it("returns the expected greenHosting factor when green is set", () => { + const res = co2.perByteTrace(MILLION, true, { + greenHostingFactor: 0.5, + }); + + expect(res.variables.greenHostingFactor).toBe(1); + }); + }); }); diff --git a/src/helpers/index.js b/src/helpers/index.js index b009147..8ff63f9 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -1,6 +1,7 @@ import { averageIntensity } from "../index.js"; import { - GLOBAL_GRID_INTENSITY, + GLOBAL_GRID_INTENSITY as SWDM3_GLOBAL_GRID_INTENSITY, + SWDV4, PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD, FIRST_TIME_VIEWING_PERCENTAGE, RETURNING_VISITOR_PERCENTAGE, @@ -8,6 +9,7 @@ import { SWDMV4_RATINGS, } from "../constants/index.js"; +const SWDM4_GLOBAL_GRID_INTENSITY = SWDV4.GLOBAL_GRID_INTENSITY; // Shared type definitions to be used across different files /** @@ -21,7 +23,9 @@ const formatNumber = (num) => parseFloat(num.toFixed(2)); const lessThanEqualTo = (num, limit) => num <= limit; -function parseOptions(options) { +function parseOptions(options = {}, version = 3, green = false) { + const globalGridIntensity = + version === 4 ? SWDM4_GLOBAL_GRID_INTENSITY : SWDM3_GLOBAL_GRID_INTENSITY; // CHeck that it is an object if (typeof options !== "object") { throw new Error("Options must be an object"); @@ -39,7 +43,7 @@ function parseOptions(options) { `"${device.country}" is not a valid country. Please use a valid 3 digit ISO 3166 country code. \nSee https://developers.thegreenwebfoundation.org/co2js/data/ for more information. \nFalling back to global average grid intensity.` ); adjustments.gridIntensity["device"] = { - value: GLOBAL_GRID_INTENSITY, + value: globalGridIntensity, }; } adjustments.gridIntensity["device"] = { @@ -54,7 +58,7 @@ function parseOptions(options) { }; } else { adjustments.gridIntensity["device"] = { - value: GLOBAL_GRID_INTENSITY, + value: globalGridIntensity, }; console.warn( `The device grid intensity must be a number or an object. You passed in a ${typeof device}. \nFalling back to global average grid intensity.` @@ -68,7 +72,7 @@ function parseOptions(options) { `"${dataCenter.country}" is not a valid country. Please use a valid 3 digit ISO 3166 country code. \nSee https://developers.thegreenwebfoundation.org/co2js/data/ for more information. \nFalling back to global average grid intensity.` ); adjustments.gridIntensity["dataCenter"] = { - value: GLOBAL_GRID_INTENSITY, + value: SWDM3_GLOBAL_GRID_INTENSITY, }; } adjustments.gridIntensity["dataCenter"] = { @@ -83,7 +87,7 @@ function parseOptions(options) { }; } else { adjustments.gridIntensity["dataCenter"] = { - value: GLOBAL_GRID_INTENSITY, + value: globalGridIntensity, }; console.warn( `The data center grid intensity must be a number or an object. You passed in a ${typeof dataCenter}. \nFalling back to global average grid intensity.` @@ -97,7 +101,7 @@ function parseOptions(options) { `"${network.country}" is not a valid country. Please use a valid 3 digit ISO 3166 country code. \nSee https://developers.thegreenwebfoundation.org/co2js/data/ for more information. Falling back to global average grid intensity. \nFalling back to global average grid intensity.` ); adjustments.gridIntensity["network"] = { - value: GLOBAL_GRID_INTENSITY, + value: globalGridIntensity, }; } adjustments.gridIntensity["network"] = { @@ -112,13 +116,19 @@ function parseOptions(options) { }; } else { adjustments.gridIntensity["network"] = { - value: GLOBAL_GRID_INTENSITY, + value: globalGridIntensity, }; console.warn( `The network grid intensity must be a number or an object. You passed in a ${typeof network}. \nFalling back to global average grid intensity.` ); } } + } else { + adjustments.gridIntensity = { + device: { value: globalGridIntensity }, + dataCenter: { value: globalGridIntensity }, + network: { value: globalGridIntensity }, + }; } if (options?.dataReloadRatio || options.dataReloadRatio === 0) { @@ -127,18 +137,24 @@ function parseOptions(options) { adjustments.dataReloadRatio = options.dataReloadRatio; } else { adjustments.dataReloadRatio = - PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD; + version === 3 ? PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD : 0; console.warn( `The dataReloadRatio option must be a number between 0 and 1. You passed in ${options.dataReloadRatio}. \nFalling back to default value.` ); } } else { adjustments.dataReloadRatio = - PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD; + version === 3 ? PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD : 0; console.warn( `The dataReloadRatio option must be a number. You passed in a ${typeof options.dataReloadRatio}. \nFalling back to default value.` ); } + } else { + adjustments.dataReloadRatio = + version === 3 ? PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD : 0; + console.warn( + `The dataReloadRatio option must be a number. You passed in a ${typeof options.dataReloadRatio}. \nFalling back to default value.` + ); } if (options?.firstVisitPercentage || options.firstVisitPercentage === 0) { @@ -149,17 +165,25 @@ function parseOptions(options) { ) { adjustments.firstVisitPercentage = options.firstVisitPercentage; } else { - adjustments.firstVisitPercentage = FIRST_TIME_VIEWING_PERCENTAGE; + adjustments.firstVisitPercentage = + version === 3 ? FIRST_TIME_VIEWING_PERCENTAGE : 1; console.warn( `The firstVisitPercentage option must be a number between 0 and 1. You passed in ${options.firstVisitPercentage}. \nFalling back to default value.` ); } } else { - adjustments.firstVisitPercentage = FIRST_TIME_VIEWING_PERCENTAGE; + adjustments.firstVisitPercentage = + version === 3 ? FIRST_TIME_VIEWING_PERCENTAGE : 1; console.warn( `The firstVisitPercentage option must be a number. You passed in a ${typeof options.firstVisitPercentage}. \nFalling back to default value.` ); } + } else { + adjustments.firstVisitPercentage = + version === 3 ? FIRST_TIME_VIEWING_PERCENTAGE : 1; + console.warn( + `The firstVisitPercentage option must be a number. You passed in a ${typeof options.firstVisitPercentage}. \nFalling back to default value.` + ); } if (options?.returnVisitPercentage || options.returnVisitPercentage === 0) { @@ -170,17 +194,52 @@ function parseOptions(options) { ) { adjustments.returnVisitPercentage = options.returnVisitPercentage; } else { - adjustments.returnVisitPercentage = RETURNING_VISITOR_PERCENTAGE; + adjustments.returnVisitPercentage = + version === 3 ? RETURNING_VISITOR_PERCENTAGE : 0; console.warn( `The returnVisitPercentage option must be a number between 0 and 1. You passed in ${options.returnVisitPercentage}. \nFalling back to default value.` ); } } else { - adjustments.returnVisitPercentage = RETURNING_VISITOR_PERCENTAGE; + adjustments.returnVisitPercentage = + version === 3 ? RETURNING_VISITOR_PERCENTAGE : 0; console.warn( `The returnVisitPercentage option must be a number. You passed in a ${typeof options.returnVisitPercentage}. \nFalling back to default value.` ); } + } else { + adjustments.returnVisitPercentage = + version === 3 ? RETURNING_VISITOR_PERCENTAGE : 0; + console.warn( + `The returnVisitPercentage option must be a number. You passed in a ${typeof options.returnVisitPercentage}. \nFalling back to default value.` + ); + } + + if ( + options?.greenHostingFactor || + (options.greenHostingFactor === 0 && version === 4) + ) { + if (typeof options.greenHostingFactor === "number") { + if (options.greenHostingFactor >= 0 && options.greenHostingFactor <= 1) { + adjustments.greenHostingFactor = options.greenHostingFactor; + } else { + adjustments.greenHostingFactor = 0; + console.warn( + `The returnVisitPercentage option must be a number between 0 and 1. You passed in ${options.returnVisitPercentage}. \nFalling back to default value.` + ); + } + } else { + adjustments.greenHostingFactor = 0; + console.warn( + `The returnVisitPercentage option must be a number. You passed in a ${typeof options.returnVisitPercentage}. \nFalling back to default value.` + ); + } + } else if (version === 4) { + adjustments.greenHostingFactor = 0; + } + + if (green) { + adjustments.greenHostingFactor = 1; } return adjustments; @@ -202,7 +261,6 @@ function getApiRequestHeaders(comment = "") { * @param {number} swdmVersion - The version of the SWDM to use. Defaults to version 3. * @returns {string} The SWDM rating. */ - function outputRating(co2e, swdmVersion) { let { FIFTH_PERCENTILE, diff --git a/src/sustainable-web-design-v4.js b/src/sustainable-web-design-v4.js index 80ba577..eccb08b 100644 --- a/src/sustainable-web-design-v4.js +++ b/src/sustainable-web-design-v4.js @@ -58,6 +58,20 @@ function outputSegments(operationalEmissions, embodiedEmissions) { }; } +/** + * Determine the green hosting factor + * @param {boolean} green + * @param {object} options + * @returns {number} + */ +function getGreenHostingFactor(green, options) { + if (green) { + return 1.0; + } else if (options?.greenHostingFactor || options?.greenHostingFactor === 0) { + return options.greenHostingFactor; + } + return 0; +} class SustainableWebDesign { constructor(options) { this.allowRatings = true; @@ -172,7 +186,7 @@ class SustainableWebDesign { }; } - // NOTE: Setting green: true should result in a GREEN_HOSTING_FACTOR of 1.0 + // NOTE: Setting green: true should result in a greenHostingFactor of 1.0 perByte( bytes, green = false, @@ -186,20 +200,11 @@ class SustainableWebDesign { const operationalEmissions = this.operationalEmissions(bytes, options); const embodiedEmissions = this.embodiedEmissions(bytes); - let GREEN_HOSTING_FACTOR = 0; - - if (green) { - GREEN_HOSTING_FACTOR = 1.0; - } else if ( - options?.greenHostingFactor || - options?.greenHostingFactor === 0 - ) { - GREEN_HOSTING_FACTOR = options.greenHostingFactor; - } + const greenHostingFactor = getGreenHostingFactor(green, options); const totalEmissions = { dataCenter: - operationalEmissions.dataCenter * (1 - GREEN_HOSTING_FACTOR) + + operationalEmissions.dataCenter * (1 - greenHostingFactor) + embodiedEmissions.dataCenter, network: operationalEmissions.network + embodiedEmissions.network, device: operationalEmissions.device + embodiedEmissions.device, @@ -247,7 +252,7 @@ class SustainableWebDesign { let firstViewRatio = 1; let returnViewRatio = 0; let dataReloadRatio = 0; - let GREEN_HOSTING_FACTOR = 0; + const greenHostingFactor = getGreenHostingFactor(green, options); const operationalEmissions = this.operationalEmissions(bytes, options); const embodiedEmissions = this.embodiedEmissions(bytes); @@ -255,15 +260,6 @@ class SustainableWebDesign { return 0; } - if (green) { - GREEN_HOSTING_FACTOR = 1.0; - } else if ( - options?.greenHostingFactor || - options?.greenHostingFactor === 0 - ) { - GREEN_HOSTING_FACTOR = options.greenHostingFactor; - } - if (options.firstVisitPercentage || options.firstVisitPercentage === 0) { firstViewRatio = options.firstVisitPercentage; } @@ -279,7 +275,7 @@ class SustainableWebDesign { // NOTE: First visit emissions are calculated as the sum of all three segments without any caching. const firstVisitEmissions = - operationalEmissions.dataCenter * (1 - GREEN_HOSTING_FACTOR) + + operationalEmissions.dataCenter * (1 - greenHostingFactor) + embodiedEmissions.dataCenter + operationalEmissions.network + embodiedEmissions.network + @@ -289,7 +285,7 @@ class SustainableWebDesign { // NOTE: First visit emissions are calculated as the sum of all three segments with caching applied. const returnVisitEmissions = - (operationalEmissions.dataCenter * (1 - GREEN_HOSTING_FACTOR) + + (operationalEmissions.dataCenter * (1 - greenHostingFactor) + embodiedEmissions.dataCenter + operationalEmissions.network + embodiedEmissions.network +