Перед вами новий пост і знов піде мова про розробку ботів для Google Chat’ів. Сьогодні ми навчимося додавати статистику використання до нашого бота.
Відразу невеликий спойлер — ми будемо вести статистику в боті, не виходячи за межі нашого коду та не використовуючи інші сервіси. Ми познайомимось з таким функціоналом як Properties.
Спочатку я хочу розповісти що я хочу отримати у якості результату. За моїм задумом, бот буде рахувати скільки та які команди використовувалася, а також віддавати цю статистику за спеціальним запитом до бота, крім цього, я хочу щоб користувачі могли теж отримувати свою статистику використання бота.
Тож ось вам перелік даних які будемо збирати, не питайте чому саме ці дані, бо у самурая немає цілі, є лише шлях:
- основні події: повідомлення, кліки, додавання та вилучення зі спейсу
- виклик slash-команд – які команди використовували та скільки разів
- виклик функцій, які прив’язані до кнопок на картках
Збір та організація статистики
Перше питання, звідкіля брати потрібну інформацію? Вся інформація про події в нас доступна у об’єкті event
який нам приходить до 4-х ключових функцій які в нас лежать у файлі Code.gs
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /** * Responds to a MESSAGE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onMessage(event) { // ... } /** * Responds to a CARD_CLICKED event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onCardClick(event) { // ... } /** * Responds to an ADDED_TO_SPACE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onAddToSpace(event) { // ... } /** * Responds to a REMOVED_FROM_SPACE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onRemoveFromSpace(event) { // ... } |
Щоб зібрати дані для статистики нам треба лише вбудувати виклик функції, яка і буде збирати дані:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** * Responds to a MESSAGE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onMessage(event) { collectStatisticData(event) // ... } /** * Responds to a CARD_CLICKED event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onCardClick(event) { collectStatisticData(event) // ... } /** * Responds to an ADDED_TO_SPACE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onAddToSpace(event) { collectStatisticData(event) // ... } /** * Responds to a REMOVED_FROM_SPACE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onRemoveFromSpace(event) { collectStatisticData(event) // ... } |
А ось саму функцію треба ще написати, вона повинна аналізувати наступні дані:
01 02 03 04 05 06 07 08 09 10 11 12 13 | /** * Collect event data and count statistics for functions and commands * * @param {Object} event the event object from Google Chat */ function collectStatisticData(event) { // MESSAGE or CARD_CLICKED or ADDED_TO_SPACE or REMOVED_FROM_SPACE event.type // if user used a slash command event.message.slashCommand.commandId // if user clicked something on the card and call function event.common.invokedFunction } |
Тож наче все зрозуміло, тепер нам би розібратися де зберігати цю інформацію, і нам в цьому допоможуть Properties
Робота з Properties
Apps Script мають можливість використовувати 3 типи сховищ для службових даних:
ScriptProperties
— сховище даних вашого додатку, воно потрібно щоб зберігати загальні налаштування скрипта на кшталт ключів доступу, кредів, тощоUserProperties
— сховище даних поточного користувача, воно прив’язано до вашого додатку, його використовують щоб зберігати налаштування користувача відносно вашого додатку, наприклад — вибір метричної системи або якоїсь іншоїDocumentProperties
— це сховище пов’язане з відкритим документом, це не наш випадок, але і не розповісти про цього я не міг
При використанні слід пам’ятити про обмеження при використанні Properties Service — це 9KB на значення, та 500KB загалом на сховище. Також слід врахувати, що є обмеження на кількість read/write операцій — 50 000 на день для gmail акаунтів там 500 000 для Google Workspace.
Для реалізації мого задуму нам знадобляться наступні методи:
PropertiesService.getScriptProperties()
— сховище даних вашого додатку, тут будемо зберігати загальну статистику використанняPropertiesService.getUserProperties()
— сховище даних поточного користувача, тут будемо зберігати статистику користувача
Це key-value сховища, для роботи з якими є декілька методів, серед яких нам наразі потрібні лише getProperty(key)
та setProperty(key, value)
.
Давайте додамо використання сховища ScriptProperties
до нашої функції collectStatisticData(event)
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /** * Collect event data and count statistics for functions and commands * * @param {Object} event the event object from Google Chat */ function collectStatisticData(event) { const scriptProperties = PropertiesService.getScriptProperties(); let eventCounter = scriptProperties.getProperty(event.type) || 0; scriptProperties.setProperty(event.type, parseInt(eventCounter) + 1); // Increment slash command counter if applicable if (event.message && event.message.slashCommand) { let slashCommandCounter = scriptProperties.getProperty( 'SLASH_' + event.message.slashCommand.commandId) || 0 scriptProperties.setProperty( 'SLASH_' + event.message.slashCommand.commandId, parseInt(slashCommandCounter) + 1) } // Increment invoked function counter if applicable if (event.common && event.common.invokedFunction) { let functionCounter = scriptProperties.getProperty( 'FNC_' + event.common.invokedFunction) || 0 scriptProperties.setProperty( 'FNC_' + event.common.invokedFunction, parseInt(functionCounter) + 1) } } |
Виглядає досить «дивно», та ми ще повернемося до цього функціоналу, давайте подивимось де ми можемо подивитися на дані які ми зберігаємо. Для цього треба перейти до вкладки Project Settings
, і проскроліть до розділу Script Properties
:
А тепер трошки оптимізуємо наявний спосіб збереження, я пропоную зберігати дані у JSON форматі:
01 02 03 04 05 06 07 08 09 10 | { events: { MESSAGE: 0, CARD_CLICKED: 0, ADDED_TO_SPACE: 0, REMOVED_FROM_SPACE: 0, }, commands: {}, functions: {}, } |
Тепер внесемо зміни до функції збору статистики:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | /** * Collect event data and count statistics for functions and commands * * @param {Object} event the event object from Google Chat */ function collectStatisticData(event) { const scriptProperties = PropertiesService.getScriptProperties(); let statistics = scriptProperties.getProperty( 'STATS' ); if (statistics) { statistics = JSON.parse(statistics) } else { statistics = { events: { MESSAGE: 0, CARD_CLICKED: 0, ADDED_TO_SPACE: 0, REMOVED_FROM_SPACE: 0, }, commands: {}, functions: {}, } } statistics[event.type]++ // Increment slash command counter if applicable if (event.message && event.message.slashCommand) { const { commandId } = event.message.slashCommand statistics.commands[commandId] = (statistics.commands[commandId] || 0) + 1 } // Increment invoked function counter if applicable if (event.common && event.common.invokedFunction) { const functionName = event.common.invokedFunction statistics.functions[functionName] = (statistics.functions[functionName] || 0) + 1 } scriptProperties.setProperty( 'STATS' , JSON.stringify(statistics)) } |
Зверніть увагу в цьому прикладі, що дані які ми отримуємо зі сховища то є текст, і нам ще треба його розпарсити. Звісно перед зберіганням треба JSON знов перетворити на текст. І це ще досить непогано, бо у попередній версії цієї функції нам треба була кожного разу парсити текст, щоб працювати з цифрами 😯
Додайте такий самий функціонал, але вже в якості сховища використайте userProperties
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | /** * Collect event data and count statistics for functions and commands * * @param {Object} event the event object from Google Chat */ function collectStatisticData(event) { const userProperties = PropertiesService.getScriptProperties(); let userStatistics = userProperties.getProperty( 'STATS' ); // // ... // userProperties.setProperty( 'STATS' , JSON.stringify(userStatistics)) } |
На цьому збір статистики ми закінчимо, та будемо переходити до команди відображення.
Розширення slash-команд
Для відображення статистики я не хочу створювати окрему команду для бота, т.к. тоді вона буде з’являтися у переліку з іншими slash-командами, а це не дуже зручно, та ще буде збивати користувачів з пантелику. Тому я хочу сховати команду як аргумент до вже існуючої команди /bender stats
. Для цього слід внести зміни до функції slashBender()
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | /** * @param {Object} event the event object from Google Chat */ function slashBender (event) { if (event.message.argumentText && event.message.argumentText.length) { switch (event.message.argumentText.trim()) { case 'stats' : return slashBenderStats(event) } } // // ... // } |
Статистика
Нарешті перейдемо до відображення статистики, за це в нас якраз буде відповідати функція slashBenderStats()
:
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 | /** * @param {Object} event the event object from Google Chat */ function slashBenderStats(event) { let defaultStats = { events: { MESSAGE: 0, CARD_CLICKED: 0, ADDED_TO_SPACE: 0, REMOVED_FROM_SPACE: 0, }, commands: {}, functions: {}, } let userStats = PropertiesService.getUserProperties().getProperty( 'STATS' ) ? JSON.parse(PropertiesService.getUserProperties().getProperty( 'STATS' )) : defaultStats let scriptStats = PropertiesService.getScriptProperties().getProperty( 'STATS' ) ? JSON.parse(PropertiesService.getScriptProperties().getProperty( 'STATS' )) : defaultStats let widgets = [] widgets.push( widgetDivider() ) widgets.push( widgetTextParagraph( '<b>Scripts statistics</b>' ) ) widgets.push( widgetTextParagraph( 'Events' ) ) Object.keys(scriptStats.events).forEach((key) => { widgets.push( widgetTextParagraph(`${key}: ${scriptStats.events[key]}`) ) }) widgets.push( widgetTextParagraph( 'Commands' ) ) Object.keys(scriptStats.commands).forEach((key) => { widgets.push( widgetTextParagraph(`ID ${key}: ${scriptStats.commands[key]}`) ) }) widgets.push( widgetTextParagraph( 'Functions' ) ) Object.keys(scriptStats.functions).forEach((key) => { widgets.push( widgetTextParagraph(`${key}: ${scriptStats.functions[key]}`) ) }) widgets.push( widgetDivider() ) widgets.push( widgetTextParagraph( '<b>User statistics</b>' ) ) widgets.push( widgetTextParagraph( 'Commands' ) ) Object.keys(userStats.commands).forEach((key) => { widgets.push( widgetTextParagraph(`ID ${key}: ${userStats.commands[key]}`) ) }) widgets.push( widgetTextParagraph( 'Functions' ) ) Object.keys(userStats.functions).forEach((key) => { widgets.push( widgetTextParagraph(`${key}: ${userStats.functions[key]}`) ) }) return { 'cardsV2' : [{ 'cardId' : 'bender' , 'card' : { 'sections' : [ { 'header' : 'Stats' , 'collapsible' : false , 'widgets' : widgets } ] } }] } } |
Таким чином за запитом /bender stats
ми отримаємо ось таку статистику:
Для оформлення картки зі статистикою я використовував функції-помічники
widgetTextParagraph()
таwidgetDivider()
, окрім цього у коді на GitHub я ще додав декілька подібних функцій, може вони і вам стануть у нагоді ;)
Deployment
З тестовим деплойментом ми вже знайомились у першій частині, а ось щоб вашим ботом могли користуватися інші користувачі, то вам треба буде вже продакшен деплоймент робити:
Після цього вас запитають, щоб ви скрипт перемкнули на використання проєкту у GCP:
Для цього слід зайти на сторінку проєкту у консолі Google та скопіювати project number
:
Тепер поверніться та внесіть цей номер у налаштування проєкту:
Так, тепер знов спробуйте створіть деплоймент:
Після створення скопіюйте Deployment ID, та пропишіть його у налаштуваннях Google Chat API у вашому проєкті. Все, тепер вашим ботом зможуть користуватися інші користувачі з тих кому ви надали доступ.
Пам’ятайте, що після цього кроку, для додавання кожної нової функції до бота вам треба буде створювати новий деплоймент, і лише після цього новий функціонал буде доступним у боті.
Домашнє завдання
Цього разу завдання буде на «покурити код та розібратися», тож давайте по порядку.
Перше — це знов таки хочу звернути вашу увагу на файл Widgets.gs
, перелік віджетів у ньому збільшився, бо мені кортіло оформити статистику трохи цікавіше, ніж на скрині вище.
І друге — для роботи зі статистикою я зробив окремий клас StatisticsManager
, щоб функція collectStatisticData()
мала наступний вигляд:
01 02 03 04 05 06 07 08 09 10 | /** * Collect event data and count statistics for functions and commands * * @param {Object} event the event object from Google Chat */ function collectStatisticData(event) { const statistics = new StatisticsManager() statistics.collectData(event) statistics.save() } |
Тож дякую за увагу, не ігноруйте завдання, та чекайте на наступні статті з цієї серії.
Source Code
Код бота доступний на GitHub, реліз 4.0.0 відповідає коду з цієї статті.
