Но мы нашли проблему, которая еще не была решена. Так родился EventNative open-source сервис аналитики. O том, почему мы пошли на разработку собственного сервиса, что нам это дало и что в итоге получилось (с кусками кода), читайте под катом
Зачем нам разрабатывать собственный сервис?
Как устроен сервис
Сервис состоит из трех частей: javascript пиксель (который мы впоследствии переписали на typescript), серверная часть реализована на языке GO и в качестве in-house базы данных планировалось использовать Redshift и BigQuery (позже добавили поддержку Postgres, ClickHouse и Snowflake).
Структуру событий GA и Segment решили оставить без изменения. Все, что было нужно, это дублировать все события с web ресурса, где установлен пиксель, в наш бекенд. Как оказалось, это сделать несложно. Javascript пиксель переопределял оригинальный метод библиотеки GA на новый, который дублировал событие в нашу систему.
//'ga' - стандартное название переменной Google Analyticsif (window.ga) { ga(tracker => { var originalSendHitTask = tracker.get('sendHitTask'); tracker.set('sendHitTask', (model) => { var payLoad = model.get('hitPayload'); //отправка оригинального события в GA originalSendHitTask(model); let jsonPayload = this.parseQuery(payLoad); //отправка события в наш сервис this.send3p('ga', jsonPayload); }); });}
С пикселем Segment все проще, он имеет middleware методы, одним из них мы и воспользовались.
//'analytics' - стандартное название переменной Segmentif (window.analytics) { if (window.analytics.addSourceMiddleware) { window.analytics.addSourceMiddleware(chain => { try {//дублирование события в наш сервис this.send3p('ajs', chain.payload); } catch (e) { LOG.warn('Failed to send an event', e) } //отправка оригинального события в Segment chain.next(chain.payload); }); } else { LOG.warn("Invalid interceptor state. Analytics js initialized, but not completely"); }} else { LOG.warn('Analytics.js listener is not set.');}
Помимо копирования событий мы добавили возможность отправлять произвольный json:
//Отправка событий с произвольным json объектомeventN.track('product_page_view', { product_id: '1e48fb70-ef12-4ea9-ab10-fd0b910c49ce', product_price: 399.99, price_currency: 'USD' product_release_start: '2020-09-25T12:38:27.763000Z'});
Далее поговорим про серверную часть. Backend должен принимать http запросы, наполнять их дополнительной информацией, к примеру, гео данными (спасибо maxmind за это) и записывать в базу данных. Мы хотели сделать сервис максимально удобным, чтобы его можно было использовать с минимальной конфигурацией. Мы реализовали функционал определения схемы данных на основе структуры входящего json события. Типы данных определяются по значениям. Вложенные объекты раскладываются и приводятся к плоской структуре:
//входящий json{ "field_1": { "sub_field_1": "text1", "sub_field_2": 100 }, "field_2": "text2", "field_3": { "sub_field_1": { "sub_sub_field_1": "2020-09-25T12:38:27.763000Z" } }}//результат{ "field_1_sub_field_1": "text1", "field_1_sub_field_2": 100, "field_2": "text2", "field_3_sub_field_1_sub_sub_field_1": "2020-09-25T12:38:27.763000Z"}
Однако массивы на данный момент просто конвертируются в строку т.к. не все реляционные базы данных поддерживают повторяющиеся поля (repeated fields). Также есть возможность изменять названия полей или удалять их с помощью опциональных правил маппинга. Они позволяют менять схему данных, если это потребуется или приводить один тип данных к другому. К примеру, если в json поле находится строка с timestamp (field_3_sub_field_1_sub_sub_field_1 из примера выше) то для того чтобы создать поле в базе данных с типом timestamp, необходимо написать правило маппинга в конфигурации. Другими словами, тип данных поля определяется сначала по json значению, а затем применяется правило приведения типов (если оно сконфигурировано). Мы выделили 4 основных типа данных: STRING, FLOAT64, INT64 и TIMESTAMP. Правила маппинга и приведения типов выглядят следующим образом:
rules: - "/field_1/subfield_1 -> " #правило удаления поля - "/field_2/subfield_1 -> /field_10/subfield_1" #правило переноса поля - "/field_3/subfield_1/subsubfield_1 -> (timestamp) /field_20" #правило переноса поля и приведения типа
Алгоритм определения типа данных:
- преобразование json структуры в плоскую структуру
- определение типа данных полей по значениям
- применение правил маппинга и приведения типов
Тогда из входящей json структуры:
{ "product_id": "1e48fb70-ef12-4ea9-ab10-fd0b910c49ce", "product_price": 399.99, "price_currency": "USD", "product_type": "supplies", "product_release_start": "2020-09-25T12:38:27.763000Z", "images": { "main": "picture1", "sub": "picture2" }}
будет получена схема данных:
"product_id" character varying,"product_price" numeric (38,18),"price_currency" character varying,"product_type" character varying,"product_release_start" timestamp,"images_main" character varying,"images_sub" character varying
Также мы подумали, что пользователь должен иметь возможность настроить партиционирование или разделять данные в БД по другим критериям и реализовали возможность задавать имя таблицы константой или выражением в конфигурации. В примере ниже событие будет сохранено в таблицу с именем, вычисленным на основе значений полей product_type и _timestamp (например supplies_2020_10):
tableName: '{{.product_type}}_{{._timestamp.Format "2006_01"}}'
Однако структура входящих событий может изменяться в runtime. Мы реализовали алгоритм проверки разницы между структурой существующей таблицы и структурой входящего события. Если разница найдена таблица будет обновлена новыми полями. Для этого используется patch SQL запрос:
#Пример для PostgresALTER TABLE "schema"."table" ADD COLUMN new_column character varying
Архитектура
Зачем нужно записывать события на файловую систему, а не просто писать их сразу в БД? Базы данных не всегда демонстрируют высокую производительность при большом количестве вставок (рекомендации Postgres). Для этого Logger записывает входящие события в файл и уже в отдельной горутине (потоке) File reader читает файл, далее происходит преобразование и определение схемы данных. После того как Table manager убедится, что схема таблицы актуальна данные будут записаны в БД одним батчем. Впоследствии мы добавили возможность записывать данные напрямую в БД, но применяем такой режим для событий, которых не много например конверсии.
Open Source и планы на будущее
В какой-то момент сервис стал похож на полноценный продукт и мы решили выложить его в Open Source. На текущий момент реализованы интеграции с Postgres, ClickHouse, BigQuery, Redshift, S3, Snowflake. Все интеграции поддерживают как batch, так и streaming режимы загрузки данных. Добавлена поддержка запросов через API.
Текущая интеграционная схема выглядит следующим образом:
Несмотря на то что сервис можно использовать самостоятельно (например с помощью Docker), у нас также есть hosted версия, в которой можно настроить интеграцию с хранилищем данных, добавить CNAME на свой домен и посмотреть статистику по количеству событий. Наши ближайшие планы добавление возможности агрегировать не только статистику с веб ресурса, но и данные из внешних источников данных и сохранять их в любое хранилище на выбор!
GitHub
Документация
Slack
Будем рады если EventNative поможет решить ваши задачи!