From 29e6333e5c291766bba77e76ea3056134d6b0486 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Mon, 4 Nov 2024 12:19:42 -0500 Subject: [PATCH] [Feature] Add CSV header name mapping --- .mocharc.yml | 1 + package.json | 1 + .../analytics_data_processor.js | 4 +- src/process_results/result_formatter.js | 22 +++++- test/process_results/result_formatter.test.js | 75 +++++++++---------- test/support/fixtures/data.js | 18 ++++- test/support/fixtures/report.js | 8 +- 7 files changed, 81 insertions(+), 48 deletions(-) diff --git a/.mocharc.yml b/.mocharc.yml index d0504279..bb78f462 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -1,5 +1,6 @@ diff: true extension: ['js'] +full-trace: true package: './package.json' slow: '75' spec: diff --git a/package.json b/package.json index 6eb50148..b0698c28 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "pretest": "NODE_ENV=test npm run migrate", "start": "./bin/analytics", "test": "NODE_ENV=test mocha", + "test:debug": "NODE_ENV=test node --inspect-brk node_modules/mocha/bin/mocha", "coverage": "nyc --reporter html --reporter text -t coverage --report-dir coverage/summary npm run test", "prepare": "npm run snyk-protect", "snyk-protect": "snyk-protect", diff --git a/src/process_results/analytics_data_processor.js b/src/process_results/analytics_data_processor.js index 7b1bdd48..d219b027 100644 --- a/src/process_results/analytics_data_processor.js +++ b/src/process_results/analytics_data_processor.js @@ -214,8 +214,8 @@ class AnalyticsDataProcessor { } /** - * @param {String} value a yearMonth dimension from GA. - * @returns {String} the yearMonth converted to readable format e.g '202410' + * @param {string} value a yearMonth dimension from GA. + * @returns {string} the yearMonth converted to readable format e.g '202410' * converts to 'October 2024'. */ #formatYearMonth(value) { diff --git a/src/process_results/result_formatter.js b/src/process_results/result_formatter.js index 1109a123..dd905f74 100644 --- a/src/process_results/result_formatter.js +++ b/src/process_results/result_formatter.js @@ -33,7 +33,27 @@ const _formatJSON = (result, { slim }) => { }; const _formatCSV = (result) => { - return csv.writeToString(result.data, { headers: true }); + const mappedData = _mapCSVHeaders(result.data); + return csv.writeToString(mappedData, { headers: true }); +}; + +function _mapCSVHeaders(dataArray) { + return dataArray.map((dataItem) => { + Object.keys(dataItem).forEach((key) => { + if (_keyMappings[key]) { + dataItem[_keyMappings[key]] = dataItem[key]; + delete dataItem[key]; + } + }); + + return dataItem; + }); +} + +const _keyMappings = { + yearMonth: "Month-Year", + totalUsers: "Total Users", + activeUsers: "Active Users", }; module.exports = { formatResult }; diff --git a/test/process_results/result_formatter.test.js b/test/process_results/result_formatter.test.js index 71f1de7b..eb8c4a2a 100644 --- a/test/process_results/result_formatter.test.js +++ b/test/process_results/result_formatter.test.js @@ -1,4 +1,5 @@ -const expect = require("chai").expect; +const chai = require("chai"); +const expect = chai.expect; const proxyquire = require("proxyquire"); const reportFixture = require("../support/fixtures/report"); const dataFixture = require("../support/fixtures/data"); @@ -24,74 +25,68 @@ describe("ResultFormatter", () => { }); }); - it("should format results into JSON if the format is 'json'", (done) => { + it("should format results into JSON if the format is 'json'", () => { const result = analyticsDataProcessor.processData({ report, data }); - ResultFormatter.formatResult(result, { format: "json" }) - .then((formattedResult) => { + return ResultFormatter.formatResult(result, { format: "json" }).then( + (formattedResult) => { const object = JSON.parse(formattedResult); expect(object).to.deep.equal(object); - done(); - }) - .catch(done); + }, + ); }); - it("should remove the data attribute for JSON if options.slim is true", (done) => { + it("should remove the data attribute for JSON if options.slim is true", () => { const result = analyticsDataProcessor.processData({ report, data }); - ResultFormatter.formatResult(result, { format: "json", slim: true }) - .then((formattedResult) => { - const object = JSON.parse(formattedResult); - expect(object.data).to.be.undefined; - done(); - }) - .catch(done); + return ResultFormatter.formatResult(result, { + format: "json", + slim: true, + }).then((formattedResult) => { + const object = JSON.parse(formattedResult); + expect(object.data).to.be.undefined; + }); }); - it("should reject if the data cannot be JSON stringified", (done) => { + it("should reject if the data cannot be JSON stringified", () => { const array = []; array[0] = array; - ResultFormatter.formatResult(array) - .catch((e) => { - expect(e).to.equal(""); - }) - .finally(() => { - done(); - }); + return ResultFormatter.formatResult(array).catch((e) => { + expect(e).to.match(/TypeError: Converting circular structure to JSON/); + }); }); - it("should format results into CSV if the format is 'csv'", (done) => { + it("should format results into CSV if the format is 'csv'", () => { const result = analyticsDataProcessor.processData({ report, data }); - ResultFormatter.formatResult(result, { + return ResultFormatter.formatResult(result, { format: "csv", slim: true, - }) - .then((formattedResult) => { - const lines = formattedResult.split("\n"); - const [header, ...rows] = lines; + }).then((formattedResult) => { + const lines = formattedResult.split("\n"); + const [header, ...rows] = lines; - expect(header).to.equal("date,hour,visits"); - rows.forEach((row) => { - // Each CSV row should match 2017-01-30,00,100 - expect(row).to.match(/[0-9]{4}-[0-9]{2}-[0-9]{2},[0-9]{2},100/); - }); - }) - .finally(() => { - done(); + expect(header).to.equal( + "date,hour,visits,Month-Year,Total Users,Active Users", + ); + rows.forEach((row) => { + // Each CSV row should match 2017-01-30,00,100,January 2017,100,100 + expect(row).to.match( + /[0-9]{4}-[0-9]{2}-[0-9]{2},[0-9]{2},100,January 2017,100,100/, + ); }); + }); }); - it("should throw an error if the format is unsupported", (done) => { + it("should throw an error if the format is unsupported", () => { const result = analyticsDataProcessor.processData({ report, data }); - ResultFormatter.formatResult(result, { + return ResultFormatter.formatResult(result, { format: "xml", slim: true, }).catch((e) => { expect(e).to.equal("Unsupported format: xml"); - done(); }); }); }); diff --git a/test/support/fixtures/data.js b/test/support/fixtures/data.js index 52a8e61a..6854b473 100644 --- a/test/support/fixtures/data.js +++ b/test/support/fixtures/data.js @@ -14,12 +14,16 @@ module.exports = { * @param Object[] Describes dimension columns. The number of DimensionHeaders * and ordering of DimensionHeaders matches the dimensions present in rows. */ - dimensionHeaders: [{ name: "date" }, { name: "hour" }], + dimensionHeaders: [{ name: "date" }, { name: "hour" }, { name: "yearMonth" }], /** * @param Object[] Describes metric columns. The number of MetricHeaders and * ordering of MetricHeaders matches the metrics present in rows. */ - metricHeaders: [{ name: "sessions", type: "TYPE_INTEGER" }], + metricHeaders: [ + { name: "sessions", type: "TYPE_INTEGER" }, + { name: "totalUsers" }, + { name: "activeUsers" }, + ], /** * @param Row[] Rows of dimension value combinations and metric values in the * report. @@ -35,8 +39,16 @@ module.exports = { value: `${index}`.length < 2 ? `0${index}` : `${index}`, oneValue: "value", }, + { + value: "201701", + oneValue: "value", + }, + ], + metricValues: [ + { value: `100`, oneValue: "value" }, + { value: `100`, oneValue: "value" }, + { value: `100`, oneValue: "value" }, ], - metricValues: [{ value: `100`, oneValue: "value" }], }; }), /** diff --git a/test/support/fixtures/report.js b/test/support/fixtures/report.js index d6a70ee1..cf9bbe8f 100644 --- a/test/support/fixtures/report.js +++ b/test/support/fixtures/report.js @@ -2,8 +2,12 @@ module.exports = { name: "today", frequency: "hourly", query: { - dimensions: [{ name: "date" }, { name: "hour" }], - metrics: [{ name: "sessions" }], + dimensions: [{ name: "date" }, { name: "hour" }, { name: "yearMonth" }], + metrics: [ + { name: "sessions" }, + { name: "totalUsers" }, + { name: "activeUsers" }, + ], dateRanges: [{ startDate: "today", endDate: "today" }], }, meta: {