Перед вами новий пост і знов піде мова про розробку ботів для Google Chat’ів. Сьогодні ми навчимося додавати статистику використання до нашого бота.
Відразу невеликий спойлер — ми будемо вести статистику в боті, не виходячи за межі нашого коду та не використовуючи інші сервіси. Ми познайомимось з таким функціоналом як Properties.
Спочатку я хочу розповісти що я хочу отримати у якості результату. За моїм задумом, бот буде рахувати скільки та які команди використовувалася, а також віддавати цю статистику за спеціальним запитом до бота, крім цього, я хочу щоб користувачі могли теж отримувати свою статистику використання бота.
Тож ось вам перелік даних які будемо збирати, не питайте чому саме ці дані, бо у самурая немає цілі, є лише шлях:
- основні події: повідомлення, кліки, додавання та вилучення зі спейсу
- виклик slash-команд – які команди використовували та скільки разів
- виклик функцій, які прив’язані до кнопок на картках
Збір та організація статистики
Перше питання, звідкіля брати потрібну інформацію? Вся інформація про події в нас доступна у об’єкті event
який нам приходить до 4-х ключових функцій які в нас лежать у файлі Code.gs
:
/** * 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) { // ... }
Щоб зібрати дані для статистики нам треба лише вбудувати виклик функції, яка і буде збирати дані:
/** * 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) // ... }
А ось саму функцію треба ще написати, вона повинна аналізувати наступні дані:
/** * 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)
:
/** * 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 форматі:
{ events: { MESSAGE: 0, CARD_CLICKED: 0, ADDED_TO_SPACE: 0, REMOVED_FROM_SPACE: 0, }, commands: {}, functions: {}, }
Тепер внесемо зміни до функції збору статистики:
/** * 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
:
/** * 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()
:
/** * @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()
:
/** * @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()
мала наступний вигляд:
/** * 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 відповідає коду з цієї статті.