Ни для кого не секрет как важна аналитика. Она не только дает информацию о паттернах поведения пользователей, но и может помочь отследить частоту использования той или иной фичи которую вы могли недавно интегрировали. Однако, регулярное посещение сайта аналитики и протыкивание нужных эвентов может превратиться в утомительную рутину, которую можно избежать. Для того чтобы упростить задачу я набросал python сервер который бы раз в сутки забирал необходимые данные из аналитики и транслировал бы в чат. В рамках статьи я опишу базовый пример такой интеграции, однако возможности для расширения функционала довольно большие. Например, рисование графики или интеграцией с вашим сервером, если аналитика ограничивается id объекта.

Приготовления

Сперва нам конечно же нужны данные которые мы хотим отслеживать. В качестве примера я буду использовать Custom definition означающее частоту нажатия на категорию продукта. Для создания таких определений вам нужна роль как минимум редактора и не стоит ожидать что сразу после создания у вас будут данные. К сожалению, аналитика по каждому определению начинает собираться только после создания даже если по выбранному параметру уже полно данных. Подробнее о них можно почитать здесь Custom dimensions and metrics.

Вкратце как создавать такие определения:

  1. Заходим в аналитику проекта.

  2. Идем в панель администратора.

  3. Выбираем нужный property.

  4. Жмем на Custom definitions.

  5. Создаем definition указав параметр события который хотим отслеживать.

Вторым шагом нам нужно получить credentials.json для нашего бота. Для этого нужно создать проект на Cloud Platform если у вас еще его нет и включить Google Analytics Data API v1 в нем. Чтобы максимально упростить этот процесс, можно нажать на кнопку подготовки проекта на страницы документации Google Analytics Data API (GA4).

Как описывается в выше указанной документации, нам нужно забрать email из полученного json-а и вставить его в панели администратора нашего проперти в аналитике. Viewer прав будет вполне достаточно.

С телеграмом все проще. Если у вас еще нет бота телеграм, то он создается у @BotFather который выдаст токен с доступом. Чтобы получить id чата в который вы хотите вещать, можно добавить бот в чат и написать ему что либо, потом запросить апдейты у API телеграма по токену и получить

curl https://api.telegram.org/<ваш_токен>/getUpdates

Сервер

Сервер будет написан на python и для интеграции с ними нужно две библиотеки:

  • google-analytics-data

  • python-telegram-bot

pip install google-analytics-data python-telegram-bot

В принципе можно было бы обойтись и без библиотеки телеграмма и обойтись REST-ом, но в перспективе хочется расширить функционал, посему почему бы и не добавить.

Чтобы изолировать конфигурацию проекта от кода, создадим папку data куда положим наш credentials.json и в последующем добавим конфигурацию самого проекта.

Google Аналитика

Сначала давайте получим данные с аналитики. Согласно примеру нам нужно выполнить что то вроде

from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange,
    Dimension,
    Metric,
    RunReportRequest,
)

def sample_run_report(property_id="YOUR-GA4-PROPERTY-ID"):
    """Runs a simple report on a Google Analytics 4 property."""
    client = BetaAnalyticsDataClient()

    request = RunReportRequest(
        property=f"properties/{property_id}",
        dimensions=[Dimension(name="city")],
        metrics=[Metric(name="activeUsers")],
        date_ranges=[DateRange(start_date="2020-03-31", end_date="today")],
    )
    response = client.run_report(request)

    print("Report result:")
    for row in response.rows:
        print(row.dimension_values[0].value, row.metric_values[0].value)

Однако, у нас есть свое dimension. Запрос на него выполняется немного по другому

dimensions=[Dimension(name="customEvent:<выш_сustom_definition>")],

Диапозон дат лучше выбрать более динамичный. Допустим за прошедшие 7 дней

date_format = "%Y-%m-%d"
week_ago = (date.today() - timedelta(days = 7)).strftime(date_format)

Ну и завернем все это в класс. Честно говоря я не так часто имею дело с python и не знаю основных практик, но что то подсказывает, что так будет лучше. Так как я планирую использовать различные property_id и не хочу таскать их толкать в функцию каждый раз. Результат выглядит так:

class Analytics:
    def __init__(self, config):
        self.property_id = config['property_id']

    def run_report(self, event_name, limit):
        """Runs a simple report on a Google Analytics 4 property."""
        client = BetaAnalyticsDataClient()
        date_format = "%Y-%m-%d"
        week_ago = (date.today() - timedelta(days = 7)).strftime(date_format)
        
        request = RunReportRequest(
            property=f"properties/{self.property_id}",
            dimensions=[Dimension(name="customEvent:" + event_name)],
            metrics=[Metric(name="activeUsers")],
            date_ranges=[DateRange(start_date=week_ago, end_date="today")], 
            limit=limit,
        )
        response = client.run_report(request)
        result = []
        for row in response.rows:
            demension = row.dimension_values[0].value
            value = row.metric_values[0].value
            if demension == "(not set)": 
                continue
            result.append((demension, value))
        return result

Здесь мы собираем в массив результаты (значение, количество) исключая такого значения которое не выбрано. Их бывает довольно много и оно не особо интересно.

Telegram

У нас уже есть id чата, токен для бота и массив данных. Теперь можно их отправить. Сперва давайте сформатирует сообщение.

def format_report(title, data): 
    message = f'*{title}:*\n'
    index = 1
    for row in data:
        message += f'{str(index)}. {row[0]} - _{row[1]}_\n'
        index += 1
    return message

Функция забирает заголовок и формирует пронумерованный список. Далее отправка

from telegram.constants import ParseMode
from telegram.ext import ApplicationBuilder, Defaults

application = (
    ApplicationBuilder()
    .token("<ваш_токен>")
    .defaults(Defaults(parse_mode=ParseMode.MARKDOWN))
    .build()
)

await application.bot.send_message(
    chat_id="<id_чата>",
    text=format_report("<заголовок>", report)
)

И так же завернем все это в класс

import logging
from telegram.constants import ParseMode
from telegram.ext import ApplicationBuilder, Defaults

logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)

class TelegramBot:
    def __init__(self, config):
        self.chat_id = config['chat_id']
        self.application = (
            ApplicationBuilder()
            .token(config['token'])
            .defaults(Defaults(parse_mode=ParseMode.MARKDOWN))
            .build()
        )

    def format_report(self, title, data): 
        message = f'*{title}:*\n'
        index = 1
        for row in data:
            message += f'{str(index)}. {row[0]} - _{row[1]}_\n'
            index += 1
        return message

    async def send_report(self, report, title):
        message = self.format_report(title, report)
        await self.application.bot.send_message(
            chat_id=self.chat_id,
            text=message
        )

HTTP Сервер

Может показаться странным, но для того чтобы запускать те или иные события сервера я решил использовать простой HTTP сервер. Его события можно легко дергать cron-ом через curl да и в ответ на каждое действие можно выдавать какой то статус, что в каких то случаях может помочь отследить сбои по каждому частному случаю.

В качестве HTTP сервера был выбран AIOHTTP

pip install aiohttp

Сервер является связующим звеном обоих классов аналитики и бота, посему он будет держать оба объекта при себе. А так же конфигурацию проперти для выдачи.

class AnalyticsProperty:
    def __init__(self, config):
        self.title = config['title']
        self.dimension = config['dimension']
        self.limit = config['limit']
        self.endpoint = config['endpoint']

class Server: 
    def __init__(self, config, telegram: TelegramBot, analytics: Analytics):
        self.telegram = telegram
        self.analytics = analytics
        self.host = config['host']
        self.port = config['port']
        self.app = web.Application()
        self.app.add_routes([
            web.get('/analytics/{property}', self.handle_analytics)
        ])
        self.properties = {}
    
    def add_routes(self, properties):
        for property in properties:
            item = AnalyticsProperty(property)
            self.properties[item.endpoint] = item
        
    async def handle_analytics(self, request):
        property_id = request.match_info['property']
        if property_id in self.properties:
            property = self.properties[property_id]
            report = self.analytics.run_report(property.dimension, property.limit)
            await self.telegram.send_report(report, title=property.title)
            return web.Response(text='ok')
        else:
            raise web.HTTPNotFound()
        
    def run(self):
        web.run_app(self.app, host=self.host, port=self.port)

По поводу конфигурации мы поговорим позже. Основное что здесь можно выделить, это инициализация сервера и определение метода который будет отвечать за забор аналитики и их отправку. Порядок использования будет такой.

  1. Мы создаем объект Server с указанием бота и аналитики.

  2. Добавляем в него нужны пути с информацией о параметрах которые сервер будет запрашивать и отправлять.

В результате, будет что то вроде

bot = TelegramBot(config['telegram'])
analytics = Analytics(config['analytics'])
server = Server(config['server'], bot, analytics)
server.add_routes(config['properties'])
server.run()

Конфигурация

Для конфигурации я выбрал yaml-файл.

pip install pyyaml

Конфигурация будет резделена на 4 основные части:

  • telegram - токен и id чата;

  • analytics - id проперти;

  • server - host и post для создания сервера;

  • properties - массив из аналитических данных:

    • title - заголовок измерения;

    • dimension - наш custom difinition;

    • limit - максимальное количество результатов;

    • endpoint - компонент пути по которому будет иницирована операция.

В результате получаем такой конфиг config.yaml который положим в папку data:

server:
  host: 0.0.0.0 # Server host
  port: 8080 # Server port
analytics:
  property_id: <your_google_analytics_view_id>
telegram:
  token: <your_telegram_bot_token>
  chat_id: <your_telegram_chat_id>
properties:
  - 
    title: 'Popular categories'
    dimension: 'category_name'
    limit: 15
    endpoint: 'top_categories'

Docker

Для запуска сервера было решено запускать его в докере и дергать тот или иной endpoint кроном раз в сутки. В качестве конфига был выбран вполне шаблонный Dockerfile:

FROM python:3.9-alpine

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ["app.py", "analytics.py", "server.py", "telegram_bot.py", "./"]
ENV GOOGLE_APPLICATION_CREDENTIALS="/app/data/credentials.json"

EXPOSE 8080
ENTRYPOINT ["python", "app.py"]

Для полноценной работы контейнер требует подключения папки data в /app/data где будет лежать файл для доступа к Google аналитке и наш конфиг.

Хочу заметить, что хорошо бы закрыть контейнер от внешнего интернета так как он не требует какой либо авторизации. Ну или же настроить cron не снаружи, а внутри контейнера и не пробрасывать порты наружу.

Cron

Стоит в кратце объяснить про запуск. Чтобы настроить запуск той или иной задачи в cron необходимо добавить его с помощью конфигурации файла который вызывается по команде

crontab -e

Такой конфигурацией раз в сутки в 00:00 будет вызывается GET запрос на локальный адрес нашего сервера:

0 0 * * * curl localhost:8060/analytics/top_categories >/dev/null 2>&1

Поиграться с различными конфигурациями можно здесь https://crontab.guru/ и найти наиболее удобный для себя

Заключение

Полный результат можно посмотреть в репозитории. Конечно же решение требует от себя доработок, например, рисование графиков или интеграцию с другими серверами для выдачи более детализированной информации. Но как прототип он показал себя очень хорошо.

Комментарии (3)


  1. ebt
    00.00.0000 00:00
    -1

    Какая мотивация стоит за выбором гугл-аналитики, легаси? Сегодня практически не уступающее по возможностям селф-хостед-решение разворачивается, грубо говоря, за полчаса.


    1. ftp27 Автор
      00.00.0000 00:00
      +1

      Firebase и не желание накручивать дополнительные зависимости в проект


    1. vadvol
      00.00.0000 00:00

      Ткните меня носом, пожалуйста, в такое селф-хостед решение.