From ac5c6eb7c6f48b0e12253dd6cd511a81fd088570 Mon Sep 17 00:00:00 2001 From: Manuel de la Torre Date: Sat, 27 Aug 2022 13:46:04 -0500 Subject: [PATCH] v2.4.5: Split Slack message into chunks. --- CHANGELOG.md | 4 + dist/index.js | 94 ++++++++++++++++--- package.json | 2 +- src/config/index.js | 5 + .../postSlackMessage/__tests__/index.test.js | 19 +++- .../__tests__/splitInChunks.test.js | 49 ++++++++++ src/interactors/postSlackMessage/index.js | 36 ++++--- .../postSlackMessage/splitInChunks.js | 37 ++++++++ 8 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 src/config/index.js create mode 100644 src/interactors/postSlackMessage/__tests__/splitInChunks.test.js create mode 100644 src/interactors/postSlackMessage/splitInChunks.js diff --git a/CHANGELOG.md b/CHANGELOG.md index eecb11f..ad79b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.4.5] - 2022-08-27 +### Fixed +- [#38](https://github.com/flowwer-dev/pull-request-stats/issues/38#issuecomment-1171087421) Split Slack message to prevent hitting characters limit. + ## [2.4.4] - 2022-08-21 ### Fixed - [#46](https://github.com/flowwer-dev/pull-request-stats/issues/46) Filtering reviewers with `undefined` name. diff --git a/dist/index.js b/dist/index.js index de1f151..40f7fa9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -10996,6 +10996,18 @@ function setup(env) { module.exports = setup; +/***/ }), + +/***/ 508: +/***/ (function(module) { + +const getSlackCharsLimit = () => 39000; + +module.exports = { + getSlackCharsLimit, +}; + + /***/ }), /***/ 510: @@ -13045,6 +13057,50 @@ module.exports = function settle(resolve, reject, response) { }; +/***/ }), + +/***/ 569: +/***/ (function(module, __unusedexports, __webpack_require__) { + +const { getSlackCharsLimit } = __webpack_require__(508); +const { median } = __webpack_require__(353); + +const CHARS_LIMIT = getSlackCharsLimit(); + +const getSize = (obj) => JSON.stringify(obj).length; + +const getBlockLengths = (blocks) => blocks + .filter(({ type }) => type === 'section') // Ignoring "divider" blocks + .map((block) => getSize(block)); + +const getSizePerBlock = (blocks) => Math.round(median(getBlockLengths(blocks))); + +module.exports = (message) => { + const blockSize = Math.max(1, getSizePerBlock(message.blocks)); + + const getBlocksToSplit = (blocks) => { + const currentSize = getSize({ blocks }); + const diff = currentSize - CHARS_LIMIT; + if (diff < 0 || blocks.length === 1) return 0; + + const blocksSpace = Math.ceil(diff / blockSize); + const blocksCount = Math.max(1, Math.min(blocks.length - 1, blocksSpace)); + const firsts = blocks.slice(0, blocksCount); + return getBlocksToSplit(firsts) || blocksCount; + }; + + const getChunks = (prev, msg) => { + const blocksToSplit = getBlocksToSplit(msg.blocks); + if (!blocksToSplit) return [...prev, msg]; + const blocks = msg.blocks.slice(0, blocksToSplit); + const others = msg.blocks.slice(blocksToSplit); + return getChunks([...prev, { blocks }], { blocks: others }); + }; + + return getChunks([], message); +}; + + /***/ }), /***/ 578: @@ -14873,7 +14929,7 @@ module.exports = function bind(fn, thisArg) { /***/ 731: /***/ (function(module) { -module.exports = {"name":"pull-request-stats","version":"2.4.4","description":"Github action to print relevant stats about Pull Request reviewers","main":"dist/index.js","scripts":{"build":"ncc build src/index.js","test":"yarn run build && jest"},"keywords":[],"author":"Manuel de la Torre","license":"MIT","jest":{"testEnvironment":"node","testMatch":["**/?(*.)+(spec|test).[jt]s?(x)"]},"dependencies":{"@actions/core":"^1.5.0","@actions/github":"^5.0.0","@sentry/react-native":"^3.4.2","axios":"^0.26.1","dotenv":"^16.0.1","graphql":"^16.5.0","graphql-anywhere":"^4.2.7","humanize-duration":"^3.27.0","i18n-js":"^3.9.2","jsurl":"^0.1.5","lodash":"^4.17.21","lodash.get":"^4.4.2","lottie-react-native":"^5.1.3","markdown-table":"^2.0.0","mixpanel":"^0.13.0"},"devDependencies":{"@zeit/ncc":"^0.22.3","eslint":"^7.32.0","eslint-config-airbnb-base":"^14.2.1","eslint-plugin-import":"^2.24.1","eslint-plugin-jest":"^24.4.0","jest":"^27.0.6"},"funding":"https://github.com/sponsors/manuelmhtr"}; +module.exports = {"name":"pull-request-stats","version":"2.4.5","description":"Github action to print relevant stats about Pull Request reviewers","main":"dist/index.js","scripts":{"build":"ncc build src/index.js","test":"yarn run build && jest"},"keywords":[],"author":"Manuel de la Torre","license":"MIT","jest":{"testEnvironment":"node","testMatch":["**/?(*.)+(spec|test).[jt]s?(x)"]},"dependencies":{"@actions/core":"^1.5.0","@actions/github":"^5.0.0","@sentry/react-native":"^3.4.2","axios":"^0.26.1","dotenv":"^16.0.1","graphql":"^16.5.0","graphql-anywhere":"^4.2.7","humanize-duration":"^3.27.0","i18n-js":"^3.9.2","jsurl":"^0.1.5","lodash":"^4.17.21","lodash.get":"^4.4.2","lottie-react-native":"^5.1.3","markdown-table":"^2.0.0","mixpanel":"^0.13.0"},"devDependencies":{"@zeit/ncc":"^0.22.3","eslint":"^7.32.0","eslint-config-airbnb-base":"^14.2.1","eslint-plugin-import":"^2.24.1","eslint-plugin-jest":"^24.4.0","jest":"^27.0.6"},"funding":"https://github.com/sponsors/manuelmhtr"}; /***/ }), @@ -18246,6 +18302,7 @@ module.exports = require("tty"); const { t } = __webpack_require__(781); const { postToSlack } = __webpack_require__(162); const buildSlackMessage = __webpack_require__(337); +const splitInChunks = __webpack_require__(569); module.exports = async ({ org, @@ -18271,7 +18328,19 @@ module.exports = async ({ return; } - const message = buildSlackMessage({ + const send = (message) => { + const params = { + webhook, + channel, + message, + iconUrl: t('table.icon'), + username: t('table.title'), + }; + core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`); + return postToSlack(params); + }; + + const fullMessage = buildSlackMessage({ org, repos, reviewers, @@ -18281,19 +18350,14 @@ module.exports = async ({ displayCharts, }); - const params = { - webhook, - channel, - message, - iconUrl: t('table.icon'), - username: t('table.title'), - }; - core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`); - - await postToSlack(params).catch((error) => { - core.error(`Error posting Slack message: ${error}`); - throw error; - }); + const chunks = splitInChunks(fullMessage); + await chunks.reduce(async (promise, message) => { + await promise; + return send(message).catch((error) => { + core.error(`Error posting Slack message: ${error}`); + throw error; + }); + }, Promise.resolve()); core.debug('Successfully posted to slack'); }; diff --git a/package.json b/package.json index 9045743..4534d71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pull-request-stats", - "version": "2.4.4", + "version": "2.4.5", "description": "Github action to print relevant stats about Pull Request reviewers", "main": "dist/index.js", "scripts": { diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..4475e09 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,5 @@ +const getSlackCharsLimit = () => 39000; + +module.exports = { + getSlackCharsLimit, +}; diff --git a/src/interactors/postSlackMessage/__tests__/index.test.js b/src/interactors/postSlackMessage/__tests__/index.test.js index c8603e4..c2ec308 100644 --- a/src/interactors/postSlackMessage/__tests__/index.test.js +++ b/src/interactors/postSlackMessage/__tests__/index.test.js @@ -1,12 +1,14 @@ const Fetchers = require('../../../fetchers'); const { t } = require('../../../i18n'); const buildSlackMessage = require('../buildSlackMessage'); +const splitInChunks = require('../splitInChunks'); const postSlackMessage = require('../index'); -const MESSAGE = 'MESSAGE'; +const MESSAGE = { blocks: ['MESSAGE'] }; jest.mock('../../../fetchers', () => ({ postToSlack: jest.fn(() => Promise.resolve()) })); jest.mock('../buildSlackMessage', () => jest.fn(() => MESSAGE)); +jest.mock('../splitInChunks', () => jest.fn((message) => [message])); describe('Interactors | .postSlackMessage', () => { const debug = jest.fn(); @@ -68,17 +70,18 @@ describe('Interactors | .postSlackMessage', () => { }); describe('when integration is enabled', () => { - it('logs an error', async () => { + it('posts successfully to Slack', async () => { await postSlackMessage({ ...defaultOptions }); expect(error).not.toHaveBeenCalled(); - expect(buildSlackMessage).toHaveBeenCalledWith({ + expect(buildSlackMessage).toBeCalledWith({ reviewers: defaultOptions.reviewers, pullRequest: defaultOptions.pullRequest, periodLength: defaultOptions.periodLength, disableLinks: defaultOptions.disableLinks, displayCharts: defaultOptions.displayCharts, }); - expect(Fetchers.postToSlack).toHaveBeenCalledWith({ + expect(Fetchers.postToSlack).toBeCalledTimes(1); + expect(Fetchers.postToSlack).toBeCalledWith({ webhook: defaultOptions.slack.webhook, channel: defaultOptions.slack.channel, message: MESSAGE, @@ -86,5 +89,13 @@ describe('Interactors | .postSlackMessage', () => { username: t('table.title'), }); }); + + it('posts multiple times with divided in chunks', async () => { + splitInChunks.mockImplementationOnce((message) => [message, message, message]); + await postSlackMessage({ ...defaultOptions }); + expect(error).not.toHaveBeenCalled(); + expect(buildSlackMessage).toBeCalledTimes(1); + expect(Fetchers.postToSlack).toBeCalledTimes(3); + }); }); }); diff --git a/src/interactors/postSlackMessage/__tests__/splitInChunks.test.js b/src/interactors/postSlackMessage/__tests__/splitInChunks.test.js new file mode 100644 index 0000000..ea22d9a --- /dev/null +++ b/src/interactors/postSlackMessage/__tests__/splitInChunks.test.js @@ -0,0 +1,49 @@ +// const { getSlackCharsLimit } = require('../../../config'); +const splitInChunks = require('../splitInChunks'); + +const buildBlock = (str, length) => `${str}`.padEnd(length, '.'); + +jest.mock('../../../config', () => ({ + getSlackCharsLimit: () => 100, +})); + +describe('Interactors | .postSlackMessage | .splitInChunks', () => { + it('wraps block in array when length is ok', () => { + const block1 = buildBlock('BLOCK 1', 10); + const message = { + blocks: [block1], + }; + const expectedChunks = [message]; + const result = splitInChunks(message); + expect(result).toEqual(expectedChunks); + }); + + it('divides block in chunks when above length, keeping order', () => { + const block1 = buildBlock('BLOCK 1', 15); + const block2 = buildBlock('BLOCK 2', 15); + const block3 = buildBlock('BLOCK 3', 120); + const block4 = buildBlock('BLOCK 4', 60); + const block5 = buildBlock('BLOCK 5', 50); + const block6 = buildBlock('BLOCK 6', 10); + const block7 = buildBlock('BLOCK 7', 10); + const message = { + blocks: [ + block1, + block2, + block3, + block4, + block5, + block6, + block7, + ], + }; + const expectedChunks = [ + { blocks: [block1, block2] }, + { blocks: [block3] }, + { blocks: [block4] }, + { blocks: [block5, block6, block7] }, + ]; + const result = splitInChunks(message); + expect(result).toEqual(expectedChunks); + }); +}); diff --git a/src/interactors/postSlackMessage/index.js b/src/interactors/postSlackMessage/index.js index 0b1ddef..339104b 100644 --- a/src/interactors/postSlackMessage/index.js +++ b/src/interactors/postSlackMessage/index.js @@ -1,6 +1,7 @@ const { t } = require('../../i18n'); const { postToSlack } = require('../../fetchers'); const buildSlackMessage = require('./buildSlackMessage'); +const splitInChunks = require('./splitInChunks'); module.exports = async ({ org, @@ -26,7 +27,19 @@ module.exports = async ({ return; } - const message = buildSlackMessage({ + const send = (message) => { + const params = { + webhook, + channel, + message, + iconUrl: t('table.icon'), + username: t('table.title'), + }; + core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`); + return postToSlack(params); + }; + + const fullMessage = buildSlackMessage({ org, repos, reviewers, @@ -36,19 +49,14 @@ module.exports = async ({ displayCharts, }); - const params = { - webhook, - channel, - message, - iconUrl: t('table.icon'), - username: t('table.title'), - }; - core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`); - - await postToSlack(params).catch((error) => { - core.error(`Error posting Slack message: ${error}`); - throw error; - }); + const chunks = splitInChunks(fullMessage); + await chunks.reduce(async (promise, message) => { + await promise; + return send(message).catch((error) => { + core.error(`Error posting Slack message: ${error}`); + throw error; + }); + }, Promise.resolve()); core.debug('Successfully posted to slack'); }; diff --git a/src/interactors/postSlackMessage/splitInChunks.js b/src/interactors/postSlackMessage/splitInChunks.js new file mode 100644 index 0000000..cddfa2f --- /dev/null +++ b/src/interactors/postSlackMessage/splitInChunks.js @@ -0,0 +1,37 @@ +const { getSlackCharsLimit } = require('../../config'); +const { median } = require('../../utils'); + +const CHARS_LIMIT = getSlackCharsLimit(); + +const getSize = (obj) => JSON.stringify(obj).length; + +const getBlockLengths = (blocks) => blocks + .filter(({ type }) => type === 'section') // Ignoring "divider" blocks + .map((block) => getSize(block)); + +const getSizePerBlock = (blocks) => Math.round(median(getBlockLengths(blocks))); + +module.exports = (message) => { + const blockSize = Math.max(1, getSizePerBlock(message.blocks)); + + const getBlocksToSplit = (blocks) => { + const currentSize = getSize({ blocks }); + const diff = currentSize - CHARS_LIMIT; + if (diff < 0 || blocks.length === 1) return 0; + + const blocksSpace = Math.ceil(diff / blockSize); + const blocksCount = Math.max(1, Math.min(blocks.length - 1, blocksSpace)); + const firsts = blocks.slice(0, blocksCount); + return getBlocksToSplit(firsts) || blocksCount; + }; + + const getChunks = (prev, msg) => { + const blocksToSplit = getBlocksToSplit(msg.blocks); + if (!blocksToSplit) return [...prev, msg]; + const blocks = msg.blocks.slice(0, blocksToSplit); + const others = msg.blocks.slice(blocksToSplit); + return getChunks([...prev, { blocks }], { blocks: others }); + }; + + return getChunks([], message); +};