From 38f5e47da773bb2ad9ed2b7df937a52d3682039f Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Wed, 17 Feb 2021 15:49:48 +0800 Subject: [PATCH 1/8] Remove excessive comment --- src/webhook/handlers/__tests__/groupHandler.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webhook/handlers/__tests__/groupHandler.test.js b/src/webhook/handlers/__tests__/groupHandler.test.js index c5e3807d..942f90d9 100644 --- a/src/webhook/handlers/__tests__/groupHandler.test.js +++ b/src/webhook/handlers/__tests__/groupHandler.test.js @@ -223,7 +223,6 @@ it('should jobQueue failed with TimeoutError and add job to expiredQueue', done expect(e).toMatchInlineSnapshot(`[Error: Event expired]`); }); - // in real case this will not happen expiredJobQueue.on('failed', async (job, e) => { // console.log('expiredJobQueue.failed'); expect(job.id).toBe(jobId); From 1e5fdfe3e2ac01bd3c3b022d143573ac64a3f7d2 Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Sun, 21 Feb 2021 14:45:20 +0800 Subject: [PATCH 2/8] Add leave group command --- src/webhook/handlers/groupMessage.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/webhook/handlers/groupMessage.js b/src/webhook/handlers/groupMessage.js index f6fe6c46..ead1e350 100644 --- a/src/webhook/handlers/groupMessage.js +++ b/src/webhook/handlers/groupMessage.js @@ -4,9 +4,11 @@ import { t } from 'ttag'; import gql from 'src/lib/gql'; import { createGroupReplyMessages } from './utils'; import ga from 'src/lib/ga'; +import lineClient from 'src/webhook/lineClient'; const SIMILARITY_THRESHOLD = 0.95; const INTRO_KEYWORDS = ['hi cofacts', 'hi confacts']; +const LEAVE_KEYWORD = 'bye bye cofacts'; const VALID_CATEGORIES = [ 'medical', //'疾病、醫藥🆕', 'covid19', //'COVID-19 疫情🆕', @@ -49,6 +51,11 @@ export default async function processText(event, groupId) { return { event, groupId, replies }; } + if (event.input.toLowerCase() === LEAVE_KEYWORD) { + await lineClient.post(`/${event.source.type}/${groupId}/leave`); + return { event, groupId, replies }; + } + // skip if (event.input.length <= 10) { return { event, groupId, replies: undefined }; From c4b79b6b1181104cb2f48823d5315ca21294d273 Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Sun, 21 Feb 2021 14:57:08 +0800 Subject: [PATCH 3/8] [GA] Use custom metrics to record groupMembersCount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And fix bugs 1. Custom metrics isn’t sent 2. Room/Group member count should use their own api --- src/webhook/handlers/processGroupEvent.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/webhook/handlers/processGroupEvent.js b/src/webhook/handlers/processGroupEvent.js index 411b805f..f5d17b53 100644 --- a/src/webhook/handlers/processGroupEvent.js +++ b/src/webhook/handlers/processGroupEvent.js @@ -25,22 +25,17 @@ export default async function processGroupEvent({ if (type === 'join') { // https://developers.line.biz/en/reference/messaging-api/#get-members-group-count // Note: leave group cannot get the number - const { count: numberOfGroupMembers } = await lineClient.get( - `/group/${groupId}/members/count` - ); - console.log( - 'Joined group, numberOfGroupMembers: ' + - JSON.stringify(numberOfGroupMembers) + const { count: groupMembersCount } = await lineClient.get( + `/${otherFields.source.type}/${groupId}/members/count` ); const visitor = ga(groupId, 'N/A', '', otherFields.source.type); - + visitor.set('cm1', groupMembersCount); visitor.event({ ec: 'Group', ea: 'Join', ev: 1, }); - visitor.set('cd2', numberOfGroupMembers); visitor.send(); } else if (type === 'leave') { const visitor = ga(groupId, 'N/A', '', otherFields.source.type); From 51ab28ee7a5bedd615c6303e488365ab47111fd5 Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Mon, 22 Feb 2021 01:47:07 +0800 Subject: [PATCH 4/8] [GA] Add events `Article` / `Selected` / `` and `Reply` / `Selected` / `` --- src/webhook/handlers/groupMessage.js | 35 ++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/webhook/handlers/groupMessage.js b/src/webhook/handlers/groupMessage.js index ead1e350..75c5980f 100644 --- a/src/webhook/handlers/groupMessage.js +++ b/src/webhook/handlers/groupMessage.js @@ -82,6 +82,7 @@ export default async function processText(event, groupId) { } articleReplies(status: NORMAL) { reply { + id type text reference @@ -135,16 +136,30 @@ export default async function processText(event, groupId) { false ); - if (hasIdenticalDocs && hasValidCategory) { + if (hasIdenticalDocs) { const node = edgesSortedWithSimilarity[0].node; - const articleReply = getValidArticleReply(node); - if (articleReply) { - replies = createGroupReplyMessages( - event.input, - articleReply.reply, - node.articleReplies.length, - node.id - ); + visitor.event({ + ec: 'Article', + ea: 'Selected', + el: node.id, + }); + + if (hasValidCategory) { + const articleReply = getValidArticleReply(node); + if (articleReply) { + visitor.event({ + ec: 'Reply', + ea: 'Selected', + el: articleReply.reply.id, + }); + + replies = createGroupReplyMessages( + event.input, + articleReply.reply, + node.articleReplies.length, + node.id + ); + } } } @@ -168,7 +183,7 @@ export default async function processText(event, groupId) { * 3. candidate's positiveFeedbackCount > non-rumors' positiveFeedbackCount * * @param {object} articleReplies `Article.articleReplies` from rumors-api - * @returns {object} A article reply which type is rumor + * @returns {object} A reply which type is rumor */ export function getValidArticleReply({ articleReplies }) { let rumorCount = 0; From 82a7691c96f87692ba209f31e2998f098c044790 Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Mon, 22 Feb 2021 01:49:22 +0800 Subject: [PATCH 5/8] [GA] Update REAMDE, Group messages --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1d9bda61..ffd4eabf 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Other customizable env vars are: * `PORT`: Which port the line bot server will listen at. * `GOOGLE_DRIVE_IMAGE_FOLDER`: Google drive folder id is needed when you want to test uploading image. * `GOOGLE_CREDENTIALS`: will be populated by `authGoogleDrive.js`. See "Upload image/video" section below. -* `GA_ID`: Google analytics universal analytics tracking ID, for tracking events +* `GA_ID`: Google analytics tracking ID, for tracking events. You should also add custom dimensions and metrics, see "Google Analytics Custom dimensions and metrics" section below. * `IMAGE_MESSAGE_ENABLED`: Default disabled. To enable, please see "Process image message" section below. * `DEBUG_LIFF`: Disables external browser check in LIFF. Useful when debugging LIFF in external browser. Don't enable this on production. * `RUMORS_LINE_BOT_URL`: Server public url which is used to generate tutorial image urls and auth callback url of LINE Notify. @@ -233,6 +233,10 @@ To use Dialogflow, - `DAILOGFLOW_LANGUAGE` : Empty to agent's default language, or you can specify a [language](https://cloud.google.com/dialogflow/es/docs/reference/language). - `DAILOGFLOW_ENV` : Default to draft agent, or you can create different [versions](https://cloud.google.com/dialogflow/es/docs/agents-versions). +### Google Analytics Custom dimensions and metrics + +[Create](https://support.google.com/analytics/answer/2709829) a custom (user scope) dimemsion for `Message Source`, and a custom (hit scope) metrix for `Group Members Count`. Both of them default index is 1. If the indexes GA created are not 1, find `cd1` and `cm1` in the code and change them to `cd$theIndexGACreated` and `cm$theIndexGACreated` respectively. + --- ## Production Deployment @@ -245,7 +249,6 @@ You can deploy the line bot server to your own Heroku account by [creating a Her Despite the fact that we don't use `Procfile`, Heroku still does detection and installs the correct environment for us. - ### Prepare storage services #### Redis @@ -290,6 +293,12 @@ Consult `.env.sample` for other optional env vars. Sent event format: `Event category` / `Event action` / `Event label` +We use dimemsion `Message Source` (Custom Dimemsion1) to classify different event sources +- `user` for 1 on 1 messages +- `room` | `group` for group messages + +### 1 on 1 messages + 1. User sends a message to us - `UserInput` / `MessageType` / `` - For the time being, we only process message with "text" type. The following events only applies @@ -352,13 +361,42 @@ Sent event format: `Event category` / `Event action` / `Event label` - `LIFF` / `page_redirect` / `App` is sent on LIFF redirect, with value being redirect count. 12. Tutorial - - If it's triggered by follow event + - If it's triggered by follow event (Add friend evnet) - `Tutorial` / `Step` / `ON_BOARDING` - If it's triggered by rich menu - `Tutorial` / `Step` / `RICH_MENU` - Others - `Tutorial` / `Step` / `` +### Group messages + +1. When chatbot joined/leaved a group or a room + - Join + - `Group` / `Join` / `1` (`Event category` / `Event action` / `Event value`) + - And `Group Members Count` (Custom Metric1) to record group members count when chatbot joined. + - Leave + - `Group` / `Leave` / `-1` (`Event category` / `Event action` / `Event value`) + > Note: + > a. We set ga event value 1 as join, -1 as leave. + > To know total groups count chatbot currently joined, you can directly see the total event value (Details see [Implicit Count](https://support.google.com/analytics/answer/1033068?hl=en)). + > + > b. To know a group is currently joined or leaved, you should find the last `Join` or `Leave` action of the `Client Id`. + > + > c. Also, you should find the last `Join` action of the `Client Id` to get a more accurate `Group Members Count`. + > `Group Members Count` is only recorded when chatbot joined group, to know the exact count, you should directly get it from [line messaging-api](https://developers.line.biz/en/reference/messaging-api/#get-members-group-count). + +2. User sends a message to us + - If we found a articles in database that matches the message: + - `UserInput` / `ArticleSearch` / `ArticleFound` + - `Article` / `Search` / `
` for each article found + - If the article is identical + - `Article` / `Selected` / `` + - If the article has a valid category and the reply is valid (Details see [#238](https://github.com/cofacts/rumors-line-bot/pull/238)) + - `Reply` / `Selected` / `` + +3. User trigger chatbot to introduce itself: + - `UserInput` / `Intro` / + ## Legal `LICENSE` defines the license agreement for the source code in this repository. From 7ed7f36539e30eea03366d96294fc0b189b1bd47 Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Mon, 22 Feb 2021 02:29:36 +0800 Subject: [PATCH 6/8] Update test about ga events and leave group command --- .../handlers/__fixtures__/groupMessage.js | 9 +- .../handlers/__tests__/groupMessage.test.js | 134 ++++++++++++++++++ .../__tests__/processGroupEvent.test.js | 2 +- 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/webhook/handlers/__fixtures__/groupMessage.js b/src/webhook/handlers/__fixtures__/groupMessage.js index a0a7f1f9..0f5badb6 100644 --- a/src/webhook/handlers/__fixtures__/groupMessage.js +++ b/src/webhook/handlers/__fixtures__/groupMessage.js @@ -17,6 +17,7 @@ const validArticleWithOneCategory = { articleReplies: [ { reply: { + id: 'fake-reply-id', type: 'RUMOR', text: 'It`s rumor. It`s rumor. It`s rumor.', reference: 'www.who.int', @@ -55,6 +56,7 @@ const validArticleWithTwoCategories = { articleReplies: [ { reply: { + id: 'fake-reply-id1', type: 'RUMOR', text: '這是謠言!這是謠言!這是謠言!這是謠言!', reference: 'https://taiwanbar.cc/', @@ -68,7 +70,7 @@ const validArticleWithTwoCategories = { { node: { text: '請問黑啤愛吃什麼?黑啤愛吃蠶寶寶', - id: '3nbzf064ks60d', + id: '8nbzf064ks87g', articleCategories: [ { categoryId: 'covid19', @@ -79,6 +81,7 @@ const validArticleWithTwoCategories = { articleReplies: [ { reply: { + id: 'fake-reply-id2', type: 'RUMOR', text: '這是謠言!這是謠言!這是謠言!這是謠言!', reference: 'https://taiwanbar.cc/', @@ -105,6 +108,7 @@ const validArticleWithTwoCategories = { articleReplies: [ { reply: { + id: 'fake-reply-id3', type: 'RUMOR', text: '謠言說進口蘋果會上蠟,所以一定要削皮,但其實不用太擔心。蘋果自己本身就會產生蠟,為了增加保存期限,農家也會將蘋果上蠟。\n蘋果本身就會產生天然蠟來保護果肉,並不讓水分流失,這天然蠟還非常營養,富含花青素、槲皮素等等,能夠抵抗發炎、過敏等反應,而且不是只有蘋果會產生果蠟,還有許多水果,像是甘蔗、檸檬或是李子,也都會產生果蠟。', @@ -139,6 +143,7 @@ const invalidCategoryFeedback = { articleReplies: [ { reply: { + id: 'fake-reply-id', type: 'RUMOR', text: '這是謠言!這是謠言!這是謠言!這是謠言!', reference: 'https://taiwanbar.cc/', @@ -237,6 +242,7 @@ const invalidArticleCategory = { articleReplies: [ { reply: { + id: 'fake-reply-id', type: 'RUMOR', text: '謠言說進口蘋果會上蠟,所以一定要削皮,但其實不用太擔心。蘋果自己本身就會產生蠟,為了增加保存期限,農家也會將蘋果上蠟。\n蘋果本身就會產生天然蠟來保護果肉,並不讓水分流失,這天然蠟還非常營養,富含花青素、槲皮素等等,能夠抵抗發炎、過敏等反應,而且不是只有蘋果會產生果蠟,還有許多水果,像是甘蔗、檸檬或是李子,也都會產生果蠟。', @@ -271,6 +277,7 @@ const invalidArticleReply = { articleReplies: [ { reply: { + id: 'fake-reply-id', type: 'NOT_RUMOR', text: '沒錯!正確答案', reference: '我自己', diff --git a/src/webhook/handlers/__tests__/groupMessage.test.js b/src/webhook/handlers/__tests__/groupMessage.test.js index 819ebcd8..b0e16018 100644 --- a/src/webhook/handlers/__tests__/groupMessage.test.js +++ b/src/webhook/handlers/__tests__/groupMessage.test.js @@ -1,5 +1,6 @@ jest.mock('src/lib/ga'); jest.mock('src/lib/gql'); +jest.mock('src/webhook/lineClient'); import ga from 'src/lib/ga'; import gql from 'src/lib/gql'; @@ -10,6 +11,7 @@ import { apiResult, article as articleFixtures, } from '../__fixtures__/groupMessage'; +import lineClient from 'src/webhook/lineClient'; const event = { groupId: 'C904bb9fc2f4904b2facf8204b3f08c79', @@ -20,6 +22,7 @@ beforeEach(() => { event.input = undefined; ga.clearAllMocks(); gql.__reset(); + lineClient.post.mockClear(); }); describe('groupMessage', () => { @@ -47,12 +50,22 @@ describe('groupMessage', () => { result = await groupMessage(event); expect(result.replies).not.toBeUndefined(); expect(result).toMatchSnapshot(); + expect(gql.__finished()).toBe(false); // don't really care about this result :p event.input = 'cofacts'; expect(await groupMessage(event)).toMatchSnapshot(); }); + it('processes leave command bye bye cofacts', async () => { + gql.__push(apiResult.notFound); + event.input = 'bye bye cofacts'; + let result = await groupMessage(event); + expect(gql.__finished()).toBe(false); + expect(result.replies).toBeUndefined(); + expect(lineClient.post).toHaveBeenCalledTimes(1); + }); + it('should not reply and send ga if article not found', async () => { event.input = 'article_not_found'; gql.__push(apiResult.notFound); @@ -96,6 +109,20 @@ describe('groupMessage', () => { "ni": true, }, ], + Array [ + Object { + "ea": "Selected", + "ec": "Article", + "el": "3nbzf064ks60d", + }, + ], + Array [ + Object { + "ea": "Selected", + "ec": "Reply", + "el": "fake-reply-id", + }, + ], ] `); expect(ga.sendMock).toHaveBeenCalledTimes(1); @@ -108,6 +135,55 @@ describe('groupMessage', () => { expect(result.replies).not.toBeUndefined(); expect(result).toMatchSnapshot(); expect(gql.__finished()).toBe(true); + expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "ea": "ArticleSearch", + "ec": "UserInput", + "el": "ArticleFound", + }, + ], + Array [ + Object { + "ea": "Search", + "ec": "Article", + "el": "3nbzf064ks60d", + "ni": true, + }, + ], + Array [ + Object { + "ea": "Search", + "ec": "Article", + "el": "8nbzf064ks87g", + "ni": true, + }, + ], + Array [ + Object { + "ea": "Search", + "ec": "Article", + "el": "2zn1215x6e70v", + "ni": true, + }, + ], + Array [ + Object { + "ea": "Selected", + "ec": "Article", + "el": "3nbzf064ks60d", + }, + ], + Array [ + Object { + "ea": "Selected", + "ec": "Reply", + "el": "fake-reply-id1", + }, + ], + ] + `); expect(ga.sendMock).toHaveBeenCalledTimes(1); }); @@ -143,6 +219,13 @@ describe('groupMessage', () => { "ni": true, }, ], + Array [ + Object { + "ea": "Selected", + "ec": "Article", + "el": "3nbzf064ks60d", + }, + ], ] `); expect(ga.sendMock).toHaveBeenCalledTimes(1); @@ -180,6 +263,50 @@ describe('groupMessage', () => { "ni": true, }, ], + Array [ + Object { + "ea": "Selected", + "ec": "Article", + "el": "2zn1215x6e70v", + }, + ], + ] + `); + expect(ga.sendMock).toHaveBeenCalledTimes(1); + }); + + it('should handle input is not identical to article ', async () => { + event.input = '我知道黑啤愛吃蠶寶寶哦!'; + gql.__push(apiResult.invalidArticleReply); + expect((await groupMessage(event)).replies).toBeUndefined(); + expect(gql.__finished()).toBe(true); + expect(ga.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + undefined, + "__INIT__", + "我知道黑啤愛吃蠶寶寶哦!", + "group", + ], + ] + `); + expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "ea": "ArticleSearch", + "ec": "UserInput", + "el": "ArticleFound", + }, + ], + Array [ + Object { + "ea": "Search", + "ec": "Article", + "el": "3nbzf064ks60d", + "ni": true, + }, + ], ] `); expect(ga.sendMock).toHaveBeenCalledTimes(1); @@ -217,6 +344,13 @@ describe('groupMessage', () => { "ni": true, }, ], + Array [ + Object { + "ea": "Selected", + "ec": "Article", + "el": "3nbzf064ks60d", + }, + ], ] `); expect(ga.sendMock).toHaveBeenCalledTimes(1); diff --git a/src/webhook/handlers/__tests__/processGroupEvent.test.js b/src/webhook/handlers/__tests__/processGroupEvent.test.js index 33934e08..40f510f7 100644 --- a/src/webhook/handlers/__tests__/processGroupEvent.test.js +++ b/src/webhook/handlers/__tests__/processGroupEvent.test.js @@ -70,7 +70,7 @@ describe('processGroupEvent', () => { expect(ga.setMock.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "cd2", + "cm1", 55987, ], ] From e3fddfb7904ea6fe3bfa4cbe5b019adb231647d8 Mon Sep 17 00:00:00 2001 From: Nonumpa Date: Mon, 22 Feb 2021 17:52:55 +0800 Subject: [PATCH 7/8] Update test, simplify groupHandler test --- .../handlers/__fixtures__/groupHandler.js | 64 +++-- .../handlers/__tests__/groupHandler.test.js | 259 ++++++++---------- 2 files changed, 142 insertions(+), 181 deletions(-) diff --git a/src/webhook/handlers/__fixtures__/groupHandler.js b/src/webhook/handlers/__fixtures__/groupHandler.js index 1df40361..76945945 100644 --- a/src/webhook/handlers/__fixtures__/groupHandler.js +++ b/src/webhook/handlers/__fixtures__/groupHandler.js @@ -1,51 +1,49 @@ const joinGroup = { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'join', - mode: 'active', - timestamp: 1462629479859, - source: { - type: 'group', - groupId: 'C4a1', - }, -}; - -const leaveGroup = { - replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', - type: 'leave', - mode: 'active', - timestamp: 1462629479859, - source: { - type: 'group', - groupId: 'C4a1', + groupId: 'C4a1', + otherFields: { + mode: 'active', + timestamp: 1462629479859, + source: { + type: 'group', + groupId: 'C4a1', + }, }, }; const textMessage = { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'message', - mode: 'active', - timestamp: 1462629479859, - message: { - type: 'text', - }, - source: { - type: 'group', - groupId: 'C4a1', + groupId: 'C4a1', + otherFields: { + mode: 'active', + timestamp: 1462629479859, + message: { + type: 'text', + }, + source: { + type: 'group', + groupId: 'C4a1', + }, }, }; const expiredTextMessage = { replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA', type: 'message', - mode: 'active', - timestamp: 612921600000, - message: { - type: 'text', - }, - source: { - type: 'group', - groupId: 'C4a1', + groupId: 'C4a1', + otherFields: { + mode: 'active', + timestamp: 612921600000, + message: { + type: 'text', + }, + source: { + type: 'group', + groupId: 'C4a1', + }, }, }; -export default { joinGroup, leaveGroup, textMessage, expiredTextMessage }; +export default { joinGroup, textMessage, expiredTextMessage }; diff --git a/src/webhook/handlers/__tests__/groupHandler.test.js b/src/webhook/handlers/__tests__/groupHandler.test.js index 942f90d9..41371482 100644 --- a/src/webhook/handlers/__tests__/groupHandler.test.js +++ b/src/webhook/handlers/__tests__/groupHandler.test.js @@ -49,15 +49,9 @@ afterAll(async () => { }); it('should not reply if result.replies undefined', done => { - const { type, replyToken, ...otherFields } = jobData.joinGroup; - const param = { - replyToken, - type, - groupId: otherFields.source.groupId, - otherFields: { ...otherFields }, - }; + const param = jobData.joinGroup; processGroupEvent.mockImplementationOnce(() => ({ - replyToken, + replyToken: param.replyToken, result: { replies: undefined, }, @@ -96,15 +90,9 @@ it('should not reply if result.replies undefined', done => { it('should reply', done => { const jobId = 'testId'; - const { type, replyToken, ...otherFields } = jobData.textMessage; - const param = { - replyToken, - type, - groupId: otherFields.source.groupId, - otherFields: { ...otherFields }, - }; + const param = jobData.textMessage; processGroupEvent.mockImplementationOnce(() => ({ - replyToken, + replyToken: param.replyToken, result: { replies: { dd: 'a' }, }, @@ -151,13 +139,7 @@ it('should reply', done => { it('should jobQueue failed with TimeoutError and should not add job to expiredQueue', done => { const jobId = 'testId'; - const { type, replyToken, ...otherFields } = jobData.textMessage; - const param = { - replyToken, - type, - groupId: otherFields.source.groupId, - otherFields: { ...otherFields }, - }; + const param = jobData.textMessage; processGroupEvent.mockImplementationOnce(() => Promise.reject(new TimeoutError('mock processGroupEvent timeout error')) ); @@ -203,13 +185,7 @@ it('should jobQueue failed with TimeoutError and should not add job to expiredQu it('should jobQueue failed with TimeoutError and add job to expiredQueue', done => { const jobId = 'testId'; - const { type, replyToken, ...otherFields } = jobData.expiredTextMessage; - const param = { - replyToken, - type, - groupId: otherFields.source.groupId, - otherFields: { ...otherFields }, - }; + const param = jobData.expiredTextMessage; processGroupEvent.mockImplementationOnce(() => Promise.reject(new TimeoutError('mock processGroupEvent timeout error')) ); @@ -263,133 +239,131 @@ it('should jobQueue failed with TimeoutError and add job to expiredQueue', done }); }); -it('should pause expired job queue when there are events comming in', done => { - let failedCount = 0; - let drainedCount = 0; - let successCount = 0; - let expiredQueuePausedTimes = 0; - let expiredActiveCount = 0; - // event timestamp affects job queue - const { type, replyToken, ...otherFields } = jobData.expiredTextMessage; - const param = { - replyToken, - type, - groupId: otherFields.source.groupId, - otherFields: { ...otherFields }, - }; - +it('should pause expiredJobQueue when there are events comming in', done => { + const param = jobData.textMessage; processGroupEvent.mockImplementation(async () => { await sleep(100); - // console.log('processGroupEvent failed with timeout'); + return Promise.resolve({ + replyToken: param.replyToken, + result: { + replies: { text: 'it`s rumor.' }, + }, + }); + }); + + const fakeProcessor = jest.fn().mockImplementation(async () => { + await sleep(100); return Promise.reject( new TimeoutError('mock processGroupEvent timeout error') ); }); + + // set concurrency 0, because the defined concurrency for each process function stacks up for the Queue. + // details see https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueprocess + expiredJobQueue.process('fakeProcessor', 0, fakeProcessor); + expiredJobQueue.add('fakeProcessor', {}, { jobId: 'testExpiredId1' }); + expiredJobQueue.add('fakeProcessor', {}, { jobId: 'testExpiredId2' }); + expiredJobQueue.add('fakeProcessor', {}, { jobId: 'testExpiredId3' }); + expiredJobQueue.add('fakeProcessor', {}, { jobId: 'testExpiredId4' }); + const gh = new GroupHandler(jobQueue, expiredJobQueue, 1); - gh.addJob(param, { jobId: 'testExpiredId1' }); - gh.addJob(param, { jobId: 'testExpiredId2' }); - gh.addJob(param, { jobId: 'testExpiredId3' }); - gh.addJob(param, { jobId: 'testExpiredId4' }); - - // add a job while expiredJobQueue active - expiredJobQueue.on('active', () => { - // console.log('expiredJobQueue active ' + expiredActiveCount); - if (expiredActiveCount++ === 1) { - const { type, replyToken, ...otherFields } = jobData.textMessage; - const param = { - replyToken, - type, - groupId: otherFields.source.groupId, - otherFields: { ...otherFields }, - }; - gh.addJob(param, { jobId: 'successJobId' }); - - // To make sure this event will success(completed), - // mockImplementation to resolve a valid result. - // - // Note: this may make some expired job success(completed), - // but in real case it will not happen. And because we do - // nothing on expired job `completed`, make some expired job - // success won't affect the test result. - processGroupEvent.mockImplementation(async () => { - await sleep(100); - // console.log('processGroupEvent success'); - return Promise.resolve({ - replyToken, - result: { - replies: { text: 'it`s rumor.' }, - }, - }); - }); - } + // event timestamp affects job queue + gh.addJob(param, { jobId: 'successJobId' }); + jobQueue.on('active', async () => { + // delay for expiredJobQueue to become paused + await sleep(100); + expect(await expiredJobQueue.getJobCounts()).toMatchInlineSnapshot(` + Object { + "active": 1, + "completed": 0, + "delayed": 0, + "failed": 0, + "paused": 3, + "waiting": 0, + } + `); + done(); }); +}); + +it('should activate expiredJobQueue after jobQueue drained', done => { + const expiredJobData = jobData.expiredTextMessage; + const activeJobData = jobData.textMessage; - jobQueue.on('completed', async () => { - // console.log('jobQueue.completed'); - successCount++; - // mockImplementation back to reject by timeout - processGroupEvent.mockImplementation(async () => { + processGroupEvent + .mockImplementationOnce(async () => { + await sleep(100); + return Promise.resolve({ + replyToken: activeJobData.replyToken, + result: { + replies: { text: 'it`s rumor.' }, + }, + }); + }) + .mockImplementationOnce(async () => { await sleep(100); - // console.log('processGroupEvent failed with timeout'); return Promise.reject( new TimeoutError('mock processGroupEvent timeout error') ); }); - }); - jobQueue.on('failed', async () => { - // console.log('jobQueue.failed done'); - failedCount++; - }); - expiredJobQueue.on('paused', async () => { - expiredQueuePausedTimes++; + + expiredJobQueue.pause(); + expiredJobQueue.add(expiredJobData, { jobId: 'testExpiredId1' }); + + const gh = new GroupHandler(jobQueue, expiredJobQueue, 1); + gh.addJob(activeJobData, { jobId: 'successJobId' }); + + jobQueue.on('active', async () => { + expect(await expiredJobQueue.getJobCounts()).toMatchInlineSnapshot(` + Object { + "active": 0, + "completed": 0, + "delayed": 0, + "failed": 0, + "paused": 1, + "waiting": 0, + } + `); }); jobQueue.on('drained', async () => { - // console.log('jobQueue drained'); - drainedCount++; + // delay for expiredJobQueue to become active + await sleep(100); + expect(await expiredJobQueue.getJobCounts()).toMatchInlineSnapshot(` + Object { + "active": 1, + "completed": 0, + "delayed": 0, + "failed": 0, + "paused": 0, + "waiting": 0, + } + `); }); expiredJobQueue.on('drained', async () => { - // console.log('expiredJobQueue drained'); - if ((await isQueueIdle(jobQueue)) && (await isQueueIdle(expiredJobQueue))) { - // console.log('test done'); - - // one by jobQueue, four by expiredJobQueue - expect(processGroupEvent).toHaveBeenCalledTimes(5); - - // jobQueue - // - // console.log('success: ' + successCount + ', failed: ' + failedCount); - - // should have one job(`successJobId`) success - expect(successCount).toBe(1); - expect(lineClient.post).toHaveBeenCalledTimes(successCount); - // jobQueue should drained twice - // first time is by `testExpiredId`s, second time is by `successJobId` - expect(drainedCount).toBe(2); - expect(await jobQueue.getJobCounts()).toMatchInlineSnapshot(` - Object { - "active": 0, - "completed": 1, - "delayed": 0, - "failed": 4, - "paused": 0, - "waiting": 0, - } - `); - - // expiredJobQueue - // - // it should have four job failed with timeout and process by expiredJobQueue - const expiredJobCount = await getJobCounts(expiredJobQueue); - expect(failedCount).toBe(expiredJobCount); - expect(expiredJobCount).toBe(4); - // expiredQueue should pause two times: - // first time by testExpiredId1, second time by testId - expect(expiredQueuePausedTimes).toBe(2); - - done(); - } + expect(processGroupEvent).toHaveBeenCalledTimes(2); + expect(await jobQueue.getJobCounts()).toMatchInlineSnapshot(` + Object { + "active": 0, + "completed": 1, + "delayed": 0, + "failed": 0, + "paused": 0, + "waiting": 0, + } + `); + expect(await expiredJobQueue.getJobCounts()).toMatchInlineSnapshot(` + Object { + "active": 0, + "completed": 0, + "delayed": 0, + "failed": 1, + "paused": 0, + "waiting": 0, + } + `); + done(); }); }); @@ -439,15 +413,4 @@ const isQueueIdle = async q => { }, true); }; -/** - * - * @param {Bull.Queue} q - * @return {number} - */ -const getJobCounts = async q => { - const jobCount = await q.getJobCounts(); - // console.log(JSON.stringify(jobCount)); - return Object.values(jobCount).reduce((acc, v) => acc + v, 0); -}; - const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); From bef0400cb5f5bab733820dfbab1e782bdd56cf3f Mon Sep 17 00:00:00 2001 From: MrOrz Date: Wed, 24 Feb 2021 13:57:44 +0800 Subject: [PATCH 8/8] [README] fix typo and markdown format --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ffd4eabf..d72bc5c3 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ We use dimemsion `Message Source` (Custom Dimemsion1) to classify different even - `LIFF` / `page_redirect` / `App` is sent on LIFF redirect, with value being redirect count. 12. Tutorial - - If it's triggered by follow event (Add friend evnet) + - If it's triggered by follow event (a.k.a add-friend event) - `Tutorial` / `Step` / `ON_BOARDING` - If it's triggered by rich menu - `Tutorial` / `Step` / `RICH_MENU` @@ -377,12 +377,11 @@ We use dimemsion `Message Source` (Custom Dimemsion1) to classify different even - Leave - `Group` / `Leave` / `-1` (`Event category` / `Event action` / `Event value`) > Note: - > a. We set ga event value 1 as join, -1 as leave. - > To know total groups count chatbot currently joined, you can directly see the total event value (Details see [Implicit Count](https://support.google.com/analytics/answer/1033068?hl=en)). > - > b. To know a group is currently joined or leaved, you should find the last `Join` or `Leave` action of the `Client Id`. - > - > c. Also, you should find the last `Join` action of the `Client Id` to get a more accurate `Group Members Count`. + > 1. We set ga event value 1 as join, -1 as leave. + > To know total groups count chatbot currently joined, you can directly see the total event value (Details see [Implicit Count](https://support.google.com/analytics/answer/1033068?hl=en)). + > 2. To know a group is currently joined or leaved, you should find the last `Join` or `Leave` action of the `Client Id`. + > 3. Also, you should find the last `Join` action of the `Client Id` to get a more accurate `Group Members Count`. > `Group Members Count` is only recorded when chatbot joined group, to know the exact count, you should directly get it from [line messaging-api](https://developers.line.biz/en/reference/messaging-api/#get-members-group-count). 2. User sends a message to us