Skip to content

Commit

Permalink
[Feature] Add CSV header name mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
levinmr committed Nov 4, 2024
1 parent 922d67c commit 29e6333
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 48 deletions.
1 change: 1 addition & 0 deletions .mocharc.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
diff: true
extension: ['js']
full-trace: true
package: './package.json'
slow: '75'
spec:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/process_results/analytics_data_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 21 additions & 1 deletion src/process_results/result_formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
75 changes: 35 additions & 40 deletions test/process_results/result_formatter.test.js
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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();
});
});
});
Expand Down
18 changes: 15 additions & 3 deletions test/support/fixtures/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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" }],
};
}),
/**
Expand Down
8 changes: 6 additions & 2 deletions test/support/fixtures/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down

0 comments on commit 29e6333

Please sign in to comment.