Перед вами новий пост і знов піде мова про розробку ботів для 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 відповідає коду з цієї статті.






