Source: vscodestat.js

const fs = require("fs");

const Enum = require("enum");
const csv = require("csv-parser");
const createCsvWriter = require("csv-writer").createArrayCsvWriter;
const childProcess = require("child_process");

/**
 * Enum for statistics grouping (year, month, day, null)
 * @readonly
 * @enum {string|null}
 */
const StatPeriod = new Enum(
    ["year", "month", "day", null],
    { ignoreCase: true },
    { freeze: true }
);

/**
 * Class to collect, filter and save vscode statistics to csv files
 * @property {string|null} [outDir] - path of the directory where
 *     the gathered data will be saved into csv files
 * @property {StatPeriod} [datePeriod=year] - grouping of the statistics
 * @property {boolean} [writeExtensionName=false] - flag used to write the name of the extension into a csv column
 * @property {boolean} [mergeStoredData=true] - flag used to merge actual day's vscode statistics with previously stored
 */
class WriteVscodeStat {
    #extensionName;
    outDir;

    #datePeriod;
    #writeExtensionName;
    #mergeStoredData;

    #statisticsTypes = {
        downloadCount: null,
        install: null,
        updateCount: null,
        averagerating: null,
        ratingcount: null,
        weightedRating: null,
        trendingdaily: null,
        trendingweekly: null,
        trendingmonthly: null,
    };

    /**
     * Initialize WriteVscodeStat class
     * @param {string} extensionName - name of the target vscode extension
     * @param {string|null} [outDir] - path of the directory where
     *     the gathered data will be saved into csv files
     */
    constructor(extensionName, outDir) {
        if (!extensionName) {
            throw new Error("extensionName is a required argument");
        }

        this.#extensionName = extensionName;
        this.outDir = outDir;

        this.#datePeriod = StatPeriod.year;
        this.#writeExtensionName = false;
        this.#mergeStoredData = true;
    }

    get extensionName() {
        return this.#extensionName;
    }

    get datePeriod() {
        return this.#datePeriod;
    }

    set datePeriod(datePeriod) {
        this.#datePeriod = StatPeriod.get(datePeriod);
    }

    get writeExtensionName() {
        return this.#writeExtensionName;
    }

    set writeExtensionName(writeExtensionName) {
        this.#writeExtensionName = Boolean(writeExtensionName);
    }

    get mergeStoredData() {
        return this.#mergeStoredData;
    }

    set mergeStoredData(mergeStoredData) {
        this.#mergeStoredData = Boolean(mergeStoredData);
    }

    /**
     * Returns actual day's vscode statistics for an extension
     * @returns {Promise} Promise object represents the actual day's vscode statistics for an extension
     */
    getVscodeStat() {
        return new Promise((resolve) => {
            return resolve(this.#getStat(100));
        });
    }

    #getStat(retryLimit, retryCount) {
        retryLimit = retryLimit || Number.MAX_VALUE;
        retryCount = Math.max(retryCount || 0, 0);
        const command = "npx vsce show vizzuhq.code-viz-stat --json";
        return new Promise((resolve, reject) => {
            childProcess.exec(
                command,
                (error, standardOutput, standardError) => {
                    if (error) {
                        reject(error);
                        return;
                    }
                    if (standardError) {
                        reject(standardError);
                        return;
                    }
                    standardOutput = JSON.parse(standardOutput);
                    const responseObject = {};
                    const dayObj = new Date();
                    const day =
                        dayObj.getFullYear() +
                        "-" +
                        ("0" + (dayObj.getMonth() + 1)).slice(-2) +
                        "-" +
                        ("0" + dayObj.getDate()).slice(-2);
                    const statKey = day;
                    const statValues = [];
                    statValues.push(day);
                    if (this.writeExtensionName) {
                        statValues.push(this.#extensionName);
                    }
                    standardOutput.statistics.forEach((stat) => {
                        this.#statisticsTypes[stat.statisticName] = stat.value;
                    });
                    for (const [key, value] of Object.entries(
                        this.#statisticsTypes
                    )) {
                        statValues.push(value);
                    }
                    responseObject[statKey] = statValues;
                    resolve(responseObject);
                }
            );
        });
    }

    /**
     * Writes actual day's vscode statistics for an extension
     * @param {string|null} [postfix=vscodestat] - postfix of the csv file
     * @returns {Promise} Promise object represents the actual day's vscode statistics for an extension
     */
    writeVscodeStat(postfix = "vscodestat") {
        return new Promise((resolve) => {
            const stats = this.getVscodeStat();
            stats.then((stats) => {
                const dayObj = new Date();
                const day =
                    dayObj.getFullYear() +
                    "-" +
                    ("0" + (dayObj.getMonth() + 1)).slice(-2) +
                    "-" +
                    ("0" + dayObj.getDate()).slice(-2);
                const grouped = this.#groupStats(stats, day, postfix);
                this.#mergeStats(grouped).then((merged) => {
                    this.#writeStats(merged);
                    return resolve(merged);
                });
            });
        });
    }

    #groupStats(stats, day, postfix) {
        const grouped = {};
        if (this.datePeriod) {
            let substring;
            if (this.datePeriod === StatPeriod.year) {
                substring = 4;
            } else if (this.datePeriod === StatPeriod.month) {
                substring = 7;
            } else if (this.datePeriod === StatPeriod.month) {
                substring = 10;
            }
            const initialized = {};
            const prefix = day.substring(0, substring);
            if (!initialized[prefix]) {
                initialized[prefix] = true;
                grouped[prefix + "_" + postfix + ".csv"] = [];
            }
            grouped[prefix + "_" + postfix + ".csv"].push([day, stats[day]]);
        } else {
            grouped[postfix + ".csv"] = [];
            grouped[postfix + ".csv"].push([day, stats[day]]);
        }
        return grouped;
    }

    #mergeStats(stats) {
        return new Promise((resolve) => {
            if (!this.mergeStoredData) {
                return resolve(stats);
            }
            const csvFiles = {};
            const csvFilesReady = [];
            for (const [key, value] of Object.entries(stats)) {
                const csvFileReady = this.#readCsv(key, value[0]);
                csvFilesReady.push(csvFileReady);
                csvFileReady.then((csvData) => {
                    Object.assign(csvFiles, csvData);
                });
            }
            Promise.all(csvFilesReady).then(() => {
                for (const [key] of Object.entries(stats)) {
                    if (csvFiles[key]) {
                        stats[key] = csvFiles[key].concat(stats[key]);
                    }
                }
                return resolve(stats);
            });
        });
    }

    #readCsv(csvFile, firstNewLine) {
        return new Promise((resolve) => {
            const csvData = {};
            csvData[csvFile] = [];
            if (!this.outDir) {
                return resolve(csvData);
            }
            const csvFilePath = this.outDir + "/" + csvFile;
            const writeExtensionName = this.writeExtensionName;
            const statisticsTypes = this.#statisticsTypes;
            fs.stat(csvFilePath, function (err) {
                if (err != null) {
                    return resolve(csvData);
                }
                fs.createReadStream(csvFilePath)
                    .pipe(csv())
                    .on("data", (row) => {
                        if (firstNewLine) {
                            if (row.date < firstNewLine[0].substring(0, 10)) {
                                const statKey = row.date;
                                const statValues = [];
                                statValues.push(row.date);
                                if (writeExtensionName) {
                                    statValues.push(row.extension);
                                }
                                for (const [key] of Object.entries(
                                    statisticsTypes
                                )) {
                                    statValues.push(row[key]);
                                }
                                csvData[csvFile].push([statKey, statValues]);
                            }
                        }
                    })
                    .on("end", () => {
                        return resolve(csvData);
                    });
            });
        });
    }

    #writeStats(stats) {
        if (this.outDir) {
            fs.mkdir(this.outDir, { recursive: true }, (err) => {
                if (err) {
                    throw err;
                }
                for (const [key, value] of Object.entries(stats)) {
                    const csvFilePath = this.outDir + "/" + key;
                    const header = ["date"];
                    if (this.writeExtensionName) {
                        header.push("extension");
                    }
                    for (const [key] of Object.entries(this.#statisticsTypes)) {
                        header.push(key);
                    }
                    const csvWriter = createCsvWriter({
                        path: csvFilePath,
                        header,
                    });
                    const postProcessedStats = [];
                    value.forEach((stat) => {
                        postProcessedStats.push(stat[1]);
                    });
                    csvWriter.writeRecords(postProcessedStats).catch((err) => {
                        throw err;
                    });
                }
            });
        }
    }
}

module.exports = WriteVscodeStat;