From 6b15a1d929d9a7aaf0e3654080dcde12b26c74b8 Mon Sep 17 00:00:00 2001 From: Aleksei Simatov Date: Tue, 29 Oct 2024 16:13:23 +0800 Subject: [PATCH] Feature: Made charts optional (#48) * Feature: Added anonymous tracking * Feature: Replace Total merge in reviews table with total reviews conducted * Feature: Added events and use_charts variable * Feature: Added percentage in table * Feature: Added sorting * Feature: Update readme * Feature: Update readme --- README.md | 15 +- action.yml | 4 + build/index.js | 192 +++++++++++++++----- package.json | 2 +- src/analytics/index.ts | 1 + src/analytics/sendActionError.ts | 3 + src/analytics/sendActionRun.ts | 3 + src/analytics/sendDiscussionUsage.ts | 41 +++++ src/index.ts | 7 +- src/view/utils/createConfigParamsCode.ts | 1 + src/view/utils/createDiscussionsPieChart.ts | 88 +++++++-- src/view/utils/createReviewTable.ts | 7 +- src/view/utils/createTimelineContent.ts | 7 +- src/view/utils/createTimelinePieChart.ts | 80 ++++++-- 14 files changed, 358 insertions(+), 93 deletions(-) create mode 100644 src/analytics/sendDiscussionUsage.ts diff --git a/README.md b/README.md index 5c04b53..7296bca 100644 --- a/README.md +++ b/README.md @@ -171,18 +171,19 @@ To integrate **pull-request-analytics-action** into your GitHub repository, use runs-on: ubuntu-latest steps: - name: "Run script for analytics" - uses: AlexSim93/pull-request-analytics-action@v3 + uses: AlexSim93/pull-request-analytics-action@v4 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # In the case of a personal access token, it needs to be added to the repository's secrets and used in this field. - GITHUB_REPO_FOR_ISSUE: "repo" # Make sure to specify the name of the repository where the issue will be created - GITHUB_OWNER_FOR_ISSUE: "owner" # Make sure to specify the owner of the repository where the issue will be created - GITHUB_OWNERS_REPOS: "owner/repo" # Be sure to list the owner and repository name in the format owner/repo + GITHUB_REPO_FOR_ISSUE: # Make sure to specify the name of the repository where the issue will be created + GITHUB_OWNER_FOR_ISSUE: # Make sure to specify the owner of the repository where the issue will be created + GITHUB_OWNERS_REPOS: # Be sure to list the owner and repository name in the format owner/repo CORE_HOURS_START: "9:00" CORE_HOURS_END: "19:00" TIMEZONE: "Europe/Berlin" REPORT_DATE_START: ${{ inputs.report_date_start }} REPORT_DATE_END: ${{ inputs.report_date_end }} ``` + 4. Check your repository settings if you want to publish reports in issues. Go to the repository's **Settings**, and under the **Features** section, make sure the **Issues** checkbox is selected. Additionally, if you are collecting statistics for an organization's repository using a **personal access token**, ensure that the token has the necessary permissions. To do this, go to the organization's **Settings** and navigate to the **Personal access token** tab. Verify that the tokens (classic) have permission to access the repository. 5. Decide on which GitHub event you want to trigger the report generation. You can refer to the [GitHub Events Documentation](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows) for a detailed understanding of different events. In this example, the `workflow_dispatch` event is selected to allow the action to be manually triggered multiple times with different parameters. `report_date_start` and `report_date_end` can be set before running the action without modifying the code. 6. Depending on your needs, you can use either the `GITHUB_TOKEN` or a generated **Personal Access Token (classic)**. In this example, we are using the `GITHUB_TOKEN`, but keep in mind that it won't allow you to collect data from multiple repositories or organizations, nor will it provide data segmented by GitHub teams. If these features are critical for you, create a token with the **repo** and **read:org** scopes selected on [tokens page](https://github.com/settings/tokens). You can read more about tokens in the [GitHub Documentation](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28). @@ -238,13 +239,14 @@ Below is a table outlining the various configuration parameters available for ** | `ISSUE_TITLE` | Title for the created/updated issue with report | `Pull requests report(d/MM/yyyy HH:mm)` | | `LABELS` | Labels for the created/updated issue with report separated by commas. Example: `Report` | - | | `ASSIGNEES` | Assignees for the created/updated issue with report separated by commas. Example: `AlexSim93` | - | +| `USE_CHARTS` | Primarily uses charts and diagrams instead of tables to display data. Set the value to `true` to use charts instead of tables | `false` | | `HIDE_USERS` | Hides selected users from reports, while still including their data in the analytics. Use `total` to hide total stats. Users should be separated by commas. | - | | `SHOW_USERS` | Displays only specified users in reports, but includes all users in the background analytics. Use `total` to show total stats. Users should be separated by commas. | - | | `EXCLUDE_LABELS` | PRs with mentioned labels will be excluded from the report . Values should be separated by commas. Example: `bugfix, enhancement` | - | | `INCLUDE_LABELS` | Only PRs with mentioned labels will be included in the report. Values should be separated by commas. Example: `bugfix, enhancement` | - | | `EXECUTION_OUTCOME` | This parameter allows you to specify the format in which you wish to receive the report. Options include creating a new issue, updating an existing one, obtaining markdown, or JSON. Markdown and JSON will be available in outputs. Can take mulitple values separated by commas: `new-issue`, `markdown`, `collection`, `existing-issue`. This parameter is **required** Example: `existing-issue` | `new-issue` | | `ISSUE_NUMBER` | Issue number to update. Add `existing-issue` to `EXECUTION_OUTCOME` for updating existing issue. The specified issue must already exist at the time the action is executed. This parameter is mandatory if the `EXECUTION_OUTCOME` input includes `existing-issue` value | - | -| `ALLOW_ANALYTICS` | Allows sending non-sensitive inputs to mixpanel for better understanding user's needs. Set the value to `false` to disable data transmission to Mixpanel | `true` | +| `ALLOW_ANALYTICS` | Allows sending non-sensitive inputs to mixpanel for better understanding user's needs. Set the value to `false` to disable sending action parameter data | `true` | Use these parameters to tailor the **pull-request-analytics-action** to your project's specific requirements. @@ -267,6 +269,7 @@ Below is a table describing the possible outputs of **pull-request-analytics-act - You can filter pull requests using labels with the `EXCLUDE_LABELS` and `INCLUDE_LABELS` parameters. ## Troubleshooting + If you encounter a `Not Found` error: - Check the scopes of your **personal access token** if you're using one. @@ -278,7 +281,7 @@ You can read more about this in the [GitHub documentation](https://docs.github.c ## Privacy and Data Handling -**pull-request-analytics-action** is stateless; it does not send or store any of the collected data. However, to better understand user needs, fix bugs, and efficiently develop the project, some non-sensitive input parameters are sent to Mixpanel. These data are anonymous and do not provide any information that could identify the project or its data. If you wish to disable any data transmission, set `ALLOW_ANALYTICS` to `false`. +**pull-request-analytics-action** is stateless; it does not send or store any of the collected data. However, to better understand user needs, fix bugs, and efficiently develop the project, some non-sensitive input parameters are sent to Mixpanel. These data are anonymous and do not provide any information that could identify the project or its data. If you wish to disable parameter data transmission, set `ALLOW_ANALYTICS` to `false`. ## Usage Limitations diff --git a/action.yml b/action.yml index de32464..01c7878 100644 --- a/action.yml +++ b/action.yml @@ -106,6 +106,10 @@ inputs: description: "Allows sending non-sensitive inputs to mixpanel" required: false default: true + USE_CHARTS: + descriptions: "Primarily uses charts and diagrams instead of tables to display data" + required: false + default: false outputs: JSON_COLLECTION: diff --git a/build/index.js b/build/index.js index a52c10f..6442b36 100644 --- a/build/index.js +++ b/build/index.js @@ -7,11 +7,13 @@ "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.sendActionError = exports.sendActionRun = void 0; +exports.sendDiscussionUsage = exports.sendActionError = exports.sendActionRun = void 0; var sendActionRun_1 = __nccwpck_require__(7293); Object.defineProperty(exports, "sendActionRun", ({ enumerable: true, get: function () { return sendActionRun_1.sendActionRun; } })); var sendActionError_1 = __nccwpck_require__(59827); Object.defineProperty(exports, "sendActionError", ({ enumerable: true, get: function () { return sendActionError_1.sendActionError; } })); +var sendDiscussionUsage_1 = __nccwpck_require__(19208); +Object.defineProperty(exports, "sendDiscussionUsage", ({ enumerable: true, get: function () { return sendDiscussionUsage_1.sendDiscussionUsage; } })); /***/ }), @@ -74,8 +76,12 @@ const sendActionError = (error) => { REVIEW_TIME_INTERVALS: (0, utils_1.getMultipleValuesInput)("REVIEW_TIME_INTERVALS"), APPROVAL_TIME_INTERVALS: (0, utils_1.getMultipleValuesInput)("APPROVAL_TIME_INTERVALS"), MERGE_TIME_INTERVALS: (0, utils_1.getMultipleValuesInput)("MERGE_TIME_INTERVALS"), + USE_CHARTS: (0, utils_1.getValueAsIs)("USE_CHARTS"), }); } + else { + mixpanel_1.mixpanel.track("Anonymous action error", { distinct_id: "anonymous" }); + } }; exports.sendActionError = sendActionError; @@ -122,12 +128,57 @@ const sendActionRun = () => { REVIEW_TIME_INTERVALS: (0, utils_1.getMultipleValuesInput)("REVIEW_TIME_INTERVALS"), APPROVAL_TIME_INTERVALS: (0, utils_1.getMultipleValuesInput)("APPROVAL_TIME_INTERVALS"), MERGE_TIME_INTERVALS: (0, utils_1.getMultipleValuesInput)("MERGE_TIME_INTERVALS"), + USE_CHARTS: (0, utils_1.getValueAsIs)("USE_CHARTS"), }); } + else { + mixpanel_1.mixpanel.track("Anomymous action run", { distinct_id: "anonymous" }); + } }; exports.sendActionRun = sendActionRun; +/***/ }), + +/***/ 19208: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.sendDiscussionUsage = void 0; +const utils_1 = __nccwpck_require__(41002); +const mixpanel_1 = __nccwpck_require__(88238); +const sendDiscussionUsage = (data) => { + if ((0, utils_1.getValueAsIs)("ALLOW_ANALYTICS") === "true") { + mixpanel_1.mixpanel.track("Discussion usage", { + distinct_id: (0, utils_1.encrypt)((0, utils_1.getMultipleValuesInput)("ORGANIZATIONS")[0] || + (0, utils_1.getMultipleValuesInput)("GITHUB_OWNERS_REPOS")[0].split("/")[0]), + GITHUB_OWNERS_REPOS: (0, utils_1.getMultipleValuesInput)("GITHUB_OWNERS_REPOS").length, + ORGANIZATIONS: (0, utils_1.getMultipleValuesInput)("ORGANIZATIONS").length, + SHOW_STATS_TYPES: (0, utils_1.getMultipleValuesInput)("SHOW_STATS_TYPES"), + AMOUNT: (0, utils_1.getValueAsIs)("AMOUNT"), + REPORT_DATE_START: (0, utils_1.getValueAsIs)("REPORT_DATE_START"), + REPORT_DATE_END: (0, utils_1.getValueAsIs)("REPORT_DATE_END"), + REPORT_PERIOD: (0, utils_1.getValueAsIs)("REPORT_PERIOD"), + EXECUTION_OUTCOME: (0, utils_1.getMultipleValuesInput)("EXECUTION_OUTCOME"), + HOLIDAYS: (0, utils_1.getMultipleValuesInput)("HOLIDAYS").length, + DISCUSSION_USAGE: data.total.total.discussions?.received?.total && + Object.entries(data.total.total?.discussionsTypes || {}).reduce((acc, type) => acc + (type[1].received?.total || 0), 0) / (data.total.total.discussions?.received?.total || 0), + DISCUSSION_AGREED: data.total.total.discussions?.received?.agreed && + data.total.total.discussions?.received?.total && + data.total.total.discussions?.received?.agreed / + data.total.total.discussions?.received?.total, + DISCUSSION_DISAGREED: data.total.total.discussions?.received?.disagreed && + data.total.total.discussions?.received?.total && + data.total.total.discussions?.received?.disagreed / + data.total.total.discussions?.received?.total, + }); + } +}; +exports.sendDiscussionUsage = sendDiscussionUsage; + + /***/ }), /***/ 11140: @@ -2077,6 +2128,7 @@ async function main() { comments: [], }); const preparedData = (0, converters_1.collectData)(mergedData, teams); + (0, analytics_1.sendDiscussionUsage)(preparedData); console.log("Calculation complete. Generating markdown."); await (0, createOutput_1.createOutput)(preparedData); const rateLimitAtEnd = await (0, getRateLimit_1.getRateLimit)(); @@ -3004,6 +3056,7 @@ ${[ "REPORT_DATE_END", "AMOUNT", "PERIOD_SPLIT_UNIT", + "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", "EXECUTION_OUTCOME", @@ -3028,23 +3081,51 @@ exports.createConfigParamsCode = createConfigParamsCode; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createDiscussionsPieChart = void 0; const common_1 = __nccwpck_require__(64682); +const utils_1 = __nccwpck_require__(41002); const createDiscussionsPieChart = (data, users, date) => { - return users - .map((user) => ({ user, values: data[user][date]?.discussionsTypes })) - .filter((types) => types.values && - Object.values(types.values).some((value) => value.received?.total)) - .map((data) => { - const values = Object.entries(data.values) - .filter(([key, value]) => value.received?.total) - .reduce((acc, value) => { - return { - ...acc, - [value[0]]: value[1].received?.total, - }; - }, {}); - return (0, common_1.createPieChart)(`Discussion's types ${data.user} ${date}`, values); - }) - .join("\n"); + if ((0, utils_1.getValueAsIs)("USE_CHARTS") === "true") { + return users + .map((user) => ({ user, values: data[user][date]?.discussionsTypes })) + .filter((types) => types.values && + Object.values(types.values).some((value) => value.received?.total)) + .map((data) => { + const values = Object.entries(data.values) + .filter(([key, value]) => value.received?.total) + .reduce((acc, value) => { + return { + ...acc, + [value[0]]: value[1].received?.total, + }; + }, {}); + return (0, common_1.createPieChart)(`Discussion's types ${data.user} ${date}`, values); + }) + .join("\n"); + } + const headers = Object.keys(data.total[date]?.discussionsTypes || {}).sort((a, b) => (data.total[date]?.discussionsTypes?.[b]?.received?.total || 0) - + (data.total[date]?.discussionsTypes?.[a]?.received?.total || 0)); + if (headers.length === 0) + return ""; + const userRows = users + .filter((user) => data[user][date]?.discussionsTypes && + Object.values(data[user][date]?.discussionsTypes).some((value) => value.received?.total)) + .map((user) => { + const total = headers.reduce((acc, header) => acc + + (data[user][date]?.discussionsTypes?.[header]?.received?.total || 0), 0); + return [ + `**${user}**`, + ...headers.map((header) => data[user][date]?.discussionsTypes?.[header]?.received?.total + ? `${data[user][date]?.discussionsTypes?.[header]?.received?.total?.toString() || "0"}(${Math.round(((data[user][date]?.discussionsTypes?.[header]?.received + ?.total || 0) / + total) * + 1000) / 10}%)` + : "0"), + ]; + }); + return (0, common_1.createTable)({ + title: `Discussion's types ${date}`, + description: "", + table: { headers: ["users", ...headers], rows: userRows }, + }); }; exports.createDiscussionsPieChart = createDiscussionsPieChart; @@ -3202,14 +3283,13 @@ const common_1 = __nccwpck_require__(64682); const createReviewTable = (data, users, date) => { const sizes = ["xs", "s", "m", "l", "xl"]; const tableRowsTotal = users - .filter((user) => data[user]?.[date]?.merged || - data[user]?.[date]?.reviewsConducted?.total?.total || + .filter((user) => data[user]?.[date]?.reviewsConducted?.total?.total || data[user]?.[date]?.commentsConducted || data[user]?.[date]?.discussions?.conducted?.total) .map((user) => { return [ `**${user}**`, - data[user]?.[date]?.merged?.toString() || "0", + data[user]?.[date]?.reviewsConducted?.total?.total?.toString() || "0", `${data[user]?.[date]?.discussions?.conducted?.agreed?.toString() || "0"} / ${data[user]?.[date]?.discussions?.conducted?.disagreed?.toString() || "0"} / ${data[user]?.[date]?.discussions?.conducted?.total?.toString() || "0"}`, data[user]?.[date]?.commentsConducted?.toString() || "0", @@ -3226,7 +3306,7 @@ const createReviewTable = (data, users, date) => { table: { headers: [ "user", - constants_1.totalMergedPrsHeader, + constants_1.reviewConductedHeader, constants_1.discussionsConductedHeader, constants_1.commentsConductedHeader, constants_1.prSizesHeader, @@ -3280,8 +3360,9 @@ const createTimelineContent = (data, users, date) => { const pullRequestTimelineTable = (0, createTimelineTable_1.createTimelineTable)(data, type, users, date); const pullRequestTimelineBar = (0, createTimelineGanttBar_1.createTimelineGanttBar)(data, type, users, date); return ` -${pullRequestTimelineTable} -${pullRequestTimelineBar} +${(0, utils_1.getValueAsIs)("USE_CHARTS") === "true" + ? pullRequestTimelineBar + : pullRequestTimelineTable} `; }) .join("\n"); @@ -3451,30 +3532,59 @@ exports.createTimelineMonthsGanttBar = createTimelineMonthsGanttBar; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createTimelinePieChart = void 0; const common_1 = __nccwpck_require__(64682); +const utils_1 = __nccwpck_require__(41002); const titleMap = { reviewTimeIntervals: "Review time", approvalTimeIntervals: "Approval time", mergeTimeIntervals: "Merge time", }; const createTimelinePieChart = (data, users, date, key) => { - return users - .map((user) => ({ - user, - values: data[user][date]?.[key], - })) - .filter((types) => types.values && Object.values(types.values).some((value) => value)) - .map((data) => { - const values = Object.entries(data.values) - .filter(([key, value]) => value) - .reduce((acc, value) => { - return { - ...acc, - [`${value[0].replace("-Infinity", "+")} hours`]: value[1], - }; - }, {}); - return (0, common_1.createPieChart)(`${titleMap[key]} ${data.user} ${date}`, values); - }) - .join("\n"); + if ((0, utils_1.getValueAsIs)("USE_CHARTS") === "true") { + return users + .map((user) => ({ + user, + values: data[user][date]?.[key], + })) + .filter((types) => types.values && Object.values(types.values).some((value) => value)) + .map((data) => { + const values = Object.entries(data.values) + .filter(([key, value]) => value) + .reduce((acc, value) => { + return { + ...acc, + [`${value[0].replace("-Infinity", "+")} hours`]: value[1], + }; + }, {}); + return (0, common_1.createPieChart)(`${titleMap[key]} ${data.user} ${date}`, values); + }) + .join("\n"); + } + const headers = Object.keys(data.total[date]?.[key] || {}); + if (headers.length === 0) + return ""; + const userRows = users + .filter((user) => data[user][date]?.[key] && + Object.values(data[user][date]?.[key]).some((value) => value)) + .map((user) => { + const total = headers.reduce((acc, header) => acc + (data[user][date]?.[key]?.[header] || 0), 0); + return [ + `**${user}**`, + ...headers.map((header) => data[user][date]?.[key]?.[header] + ? `${data[user][date]?.[key]?.[header]?.toString() || "0"}(${Math.round(((data[user][date]?.[key]?.[header] || 0) / total) * 1000) / 10}%)` + : "0"), + ]; + }); + return (0, common_1.createTable)({ + title: `${titleMap[key]} ${date}`, + description: "", + table: { + headers: [ + "users", + ...headers.map((header) => `${header.replace("-Infinity", "+")}h`), + ], + rows: userRows, + }, + }); }; exports.createTimelinePieChart = createTimelinePieChart; diff --git a/package.json b/package.json index 9d5767b..13262fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pull-request-analytics-action", - "version": "3.2.0", + "version": "4.0.0", "description": "Generates detailed PR analytics reports within GitHub, focusing on review efficiency and team performance.", "main": "build/index.js", "scripts": { diff --git a/src/analytics/index.ts b/src/analytics/index.ts index 371ba37..dec6447 100644 --- a/src/analytics/index.ts +++ b/src/analytics/index.ts @@ -1,2 +1,3 @@ export { sendActionRun } from "./sendActionRun"; export { sendActionError } from "./sendActionError"; +export { sendDiscussionUsage } from "./sendDiscussionUsage"; diff --git a/src/analytics/sendActionError.ts b/src/analytics/sendActionError.ts index e31f998..12458a8 100644 --- a/src/analytics/sendActionError.ts +++ b/src/analytics/sendActionError.ts @@ -40,6 +40,9 @@ export const sendActionError = (error: Error) => { "APPROVAL_TIME_INTERVALS" ), MERGE_TIME_INTERVALS: getMultipleValuesInput("MERGE_TIME_INTERVALS"), + USE_CHARTS: getValueAsIs("USE_CHARTS"), }); + } else { + mixpanel.track("Anonymous action error", { distinct_id: "anonymous" }); } }; diff --git a/src/analytics/sendActionRun.ts b/src/analytics/sendActionRun.ts index 893d365..58d06de 100644 --- a/src/analytics/sendActionRun.ts +++ b/src/analytics/sendActionRun.ts @@ -38,6 +38,9 @@ export const sendActionRun = () => { "APPROVAL_TIME_INTERVALS" ), MERGE_TIME_INTERVALS: getMultipleValuesInput("MERGE_TIME_INTERVALS"), + USE_CHARTS: getValueAsIs("USE_CHARTS"), }); + } else { + mixpanel.track("Anomymous action run", { distinct_id: "anonymous" }); } }; diff --git a/src/analytics/sendDiscussionUsage.ts b/src/analytics/sendDiscussionUsage.ts new file mode 100644 index 0000000..2a2d385 --- /dev/null +++ b/src/analytics/sendDiscussionUsage.ts @@ -0,0 +1,41 @@ +import { encrypt, getMultipleValuesInput, getValueAsIs } from "../common/utils"; +import { Collection } from "../converters"; +import { mixpanel } from "./mixpanel"; + +export const sendDiscussionUsage = ( + data: Record> +) => { + if (getValueAsIs("ALLOW_ANALYTICS") === "true") { + mixpanel.track("Discussion usage", { + distinct_id: encrypt( + getMultipleValuesInput("ORGANIZATIONS")[0] || + getMultipleValuesInput("GITHUB_OWNERS_REPOS")[0].split("/")[0] + ), + GITHUB_OWNERS_REPOS: getMultipleValuesInput("GITHUB_OWNERS_REPOS").length, + ORGANIZATIONS: getMultipleValuesInput("ORGANIZATIONS").length, + SHOW_STATS_TYPES: getMultipleValuesInput("SHOW_STATS_TYPES"), + AMOUNT: getValueAsIs("AMOUNT"), + REPORT_DATE_START: getValueAsIs("REPORT_DATE_START"), + REPORT_DATE_END: getValueAsIs("REPORT_DATE_END"), + REPORT_PERIOD: getValueAsIs("REPORT_PERIOD"), + EXECUTION_OUTCOME: getMultipleValuesInput("EXECUTION_OUTCOME"), + HOLIDAYS: getMultipleValuesInput("HOLIDAYS").length, + DISCUSSION_USAGE: + data.total.total.discussions?.received?.total && + Object.entries(data.total.total?.discussionsTypes || {}).reduce( + (acc, type) => acc + (type[1].received?.total || 0), + 0 + ) / (data.total.total.discussions?.received?.total || 0), + DISCUSSION_AGREED: + data.total.total.discussions?.received?.agreed && + data.total.total.discussions?.received?.total && + data.total.total.discussions?.received?.agreed / + data.total.total.discussions?.received?.total, + DISCUSSION_DISAGREED: + data.total.total.discussions?.received?.disagreed && + data.total.total.discussions?.received?.total && + data.total.total.discussions?.received?.disagreed / + data.total.total.discussions?.received?.total, + }); + } +}; diff --git a/src/index.ts b/src/index.ts index 0c04918..f9495e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,11 @@ import { validate, } from "./common/utils"; import { getRateLimit } from "./requests/getRateLimit"; -import { sendActionError, sendActionRun } from "./analytics"; +import { + sendActionError, + sendActionRun, + sendDiscussionUsage, +} from "./analytics"; async function main() { try { @@ -87,6 +91,7 @@ async function main() { } ); const preparedData = collectData(mergedData, teams); + sendDiscussionUsage(preparedData); console.log("Calculation complete. Generating markdown."); await createOutput(preparedData); diff --git a/src/view/utils/createConfigParamsCode.ts b/src/view/utils/createConfigParamsCode.ts index 67a869b..470473d 100644 --- a/src/view/utils/createConfigParamsCode.ts +++ b/src/view/utils/createConfigParamsCode.ts @@ -29,6 +29,7 @@ ${[ "REPORT_DATE_END", "AMOUNT", "PERIOD_SPLIT_UNIT", + "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", "EXECUTION_OUTCOME", diff --git a/src/view/utils/createDiscussionsPieChart.ts b/src/view/utils/createDiscussionsPieChart.ts index 2dd708f..2da6926 100644 --- a/src/view/utils/createDiscussionsPieChart.ts +++ b/src/view/utils/createDiscussionsPieChart.ts @@ -1,28 +1,80 @@ -import { createPieChart } from "./common"; +import { createPieChart, createTable } from "./common"; import { Collection } from "../../converters/types"; +import { getValueAsIs } from "../../common/utils"; export const createDiscussionsPieChart = ( data: Record>, users: string[], date: string ) => { - return users - .map((user) => ({ user, values: data[user][date]?.discussionsTypes })) + if (getValueAsIs("USE_CHARTS") === "true") { + return users + .map((user) => ({ user, values: data[user][date]?.discussionsTypes })) + .filter( + (types) => + types.values && + Object.values(types.values).some((value) => value.received?.total) + ) + .map((data) => { + const values = Object.entries(data.values!) + .filter(([key, value]) => value.received?.total) + .reduce((acc, value) => { + return { + ...acc, + [value[0]]: value[1].received?.total, + }; + }, {}); + return createPieChart( + `Discussion's types ${data.user} ${date}`, + values + ); + }) + .join("\n"); + } + const headers = Object.keys(data.total[date]?.discussionsTypes || {}).sort( + (a, b) => + (data.total[date]?.discussionsTypes?.[b]?.received?.total || 0) - + (data.total[date]?.discussionsTypes?.[a]?.received?.total || 0) + ); + if (headers.length === 0) return ""; + const userRows = users .filter( - (types) => - types.values && - Object.values(types.values).some((value) => value.received?.total) + (user) => + data[user][date]?.discussionsTypes && + Object.values(data[user][date]?.discussionsTypes!).some( + (value) => value.received?.total + ) ) - .map((data) => { - const values = Object.entries(data.values!) - .filter(([key, value]) => value.received?.total) - .reduce((acc, value) => { - return { - ...acc, - [value[0]]: value[1].received?.total, - }; - }, {}); - return createPieChart(`Discussion's types ${data.user} ${date}`, values); - }) - .join("\n"); + .map((user) => { + const total = headers.reduce( + (acc, header) => + acc + + (data[user][date]?.discussionsTypes?.[header]?.received?.total || 0), + 0 + ); + return [ + `**${user}**`, + ...headers.map((header) => + data[user][date]?.discussionsTypes?.[header]?.received?.total + ? `${ + data[user][date]?.discussionsTypes?.[ + header + ]?.received?.total?.toString() || "0" + }(${ + Math.round( + ((data[user][date]?.discussionsTypes?.[header]?.received + ?.total || 0) / + total) * + 1000 + ) / 10 + }%)` + : "0" + ), + ]; + }); + return createTable({ + title: `Discussion's types ${date}`, + description: "", + table: { headers: ["users", ...headers], rows: userRows }, + }); }; diff --git a/src/view/utils/createReviewTable.ts b/src/view/utils/createReviewTable.ts index 20e8634..cb7904f 100644 --- a/src/view/utils/createReviewTable.ts +++ b/src/view/utils/createReviewTable.ts @@ -3,8 +3,8 @@ import { commentsConductedHeader, discussionsConductedHeader, prSizesHeader, + reviewConductedHeader, reviewTypesHeader, - totalMergedPrsHeader, } from "./constants"; import { createTable } from "./common"; @@ -17,7 +17,6 @@ export const createReviewTable = ( const tableRowsTotal = users .filter( (user) => - data[user]?.[date]?.merged || data[user]?.[date]?.reviewsConducted?.total?.total || data[user]?.[date]?.commentsConducted || data[user]?.[date]?.discussions?.conducted?.total @@ -25,7 +24,7 @@ export const createReviewTable = ( .map((user) => { return [ `**${user}**`, - data[user]?.[date]?.merged?.toString() || "0", + data[user]?.[date]?.reviewsConducted?.total?.total?.toString() || "0", `${ data[user]?.[date]?.discussions?.conducted?.agreed?.toString() || "0" } / ${ @@ -63,7 +62,7 @@ export const createReviewTable = ( table: { headers: [ "user", - totalMergedPrsHeader, + reviewConductedHeader, discussionsConductedHeader, commentsConductedHeader, prSizesHeader, diff --git a/src/view/utils/createTimelineContent.ts b/src/view/utils/createTimelineContent.ts index 1fcc3b1..b0f1866 100644 --- a/src/view/utils/createTimelineContent.ts +++ b/src/view/utils/createTimelineContent.ts @@ -56,8 +56,11 @@ export const createTimelineContent = ( ); return ` -${pullRequestTimelineTable} -${pullRequestTimelineBar} +${ + getValueAsIs("USE_CHARTS") === "true" + ? pullRequestTimelineBar + : pullRequestTimelineTable +} `; }) .join("\n"); diff --git a/src/view/utils/createTimelinePieChart.ts b/src/view/utils/createTimelinePieChart.ts index 4d1cd81..e8c8928 100644 --- a/src/view/utils/createTimelinePieChart.ts +++ b/src/view/utils/createTimelinePieChart.ts @@ -1,5 +1,6 @@ -import { createPieChart } from "./common"; +import { createPieChart, createTable } from "./common"; import { Collection } from "../../converters/types"; +import { getValueAsIs } from "../../common/utils"; const titleMap = { reviewTimeIntervals: "Review time", @@ -13,25 +14,64 @@ export const createTimelinePieChart = ( date: string, key: "reviewTimeIntervals" | "approvalTimeIntervals" | "mergeTimeIntervals" ) => { - return users - .map((user) => ({ - user, - values: data[user][date]?.[key], - })) + if (getValueAsIs("USE_CHARTS") === "true") { + return users + .map((user) => ({ + user, + values: data[user][date]?.[key], + })) + .filter( + (types) => + types.values && Object.values(types.values).some((value) => value) + ) + .map((data) => { + const values = Object.entries(data.values!) + .filter(([key, value]) => value) + .reduce((acc, value) => { + return { + ...acc, + [`${value[0].replace("-Infinity", "+")} hours`]: value[1], + }; + }, {}); + return createPieChart(`${titleMap[key]} ${data.user} ${date}`, values); + }) + .join("\n"); + } + const headers = Object.keys(data.total[date]?.[key] || {}); + if (headers.length === 0) return ""; + const userRows = users .filter( - (types) => - types.values && Object.values(types.values).some((value) => value) + (user) => + data[user][date]?.[key] && + Object.values(data[user][date]?.[key]!).some((value) => value) ) - .map((data) => { - const values = Object.entries(data.values!) - .filter(([key, value]) => value) - .reduce((acc, value) => { - return { - ...acc, - [`${value[0].replace("-Infinity", "+")} hours`]: value[1], - }; - }, {}); - return createPieChart(`${titleMap[key]} ${data.user} ${date}`, values); - }) - .join("\n"); + .map((user) => { + const total = headers.reduce( + (acc, header) => acc + (data[user][date]?.[key]?.[header] || 0), + 0 + ); + return [ + `**${user}**`, + ...headers.map((header) => + data[user][date]?.[key]?.[header] + ? `${data[user][date]?.[key]?.[header]?.toString() || "0"}(${ + Math.round( + ((data[user][date]?.[key]?.[header] || 0) / total) * 1000 + ) / 10 + }%)` + : "0" + ), + ]; + }); + return createTable({ + title: `${titleMap[key]} ${date}`, + description: "", + table: { + headers: [ + "users", + ...headers.map((header) => `${header.replace("-Infinity", "+")}h`), + ], + rows: userRows, + }, + }); };