В даний час практично кожна компанія у світі збирає статистику про дії користувача на веб-ресурсі. Мотивація зрозуміла — компанії хочуть знати як використовується їхній продукт/веб сайт і краще розуміти своїх користувачів. Звичайно, на ринку існує велика кількість інструментів для вирішення цієї проблеми — від систем аналітики, які надають дані у вигляді дашбордів та графіків (наприклад
Але ми знайшли проблему, яку ще не було вирішено. Так народився
Навіщо нам розробляти власний сервіс?
Це були дев'яності, ми виживали як могли. 2019 рік ми розробляли API First Customer Data Platform kSense, яка дозволяла агрегувати дані з різних джерел (Facebook ads, Stripe, Salesforce, Google Play, Google Analytics та ін.) для зручнішого аналізу даних, виявлення залежностей і т.д. Ми помітили, що багато користувачів використовують нашу платформу для аналізу даних Google Analytics (далі GA). З деякими користувачами ми поговорили і з'ясували, що їм потрібні дані аналітики їхнього продукту, які вони отримують за допомогою GA, але
Вони встановлювали Segment javascript піксель на свій web ресурс і дані про поведінку користувачів завантажувалися в зазначену базу даних (наприклад Postgres). Але й Segment має свій мінус — ціна. Наприклад, якщо у інтернет ресурсу 90,000 MTU (monthly tracked users) то потрібно оплатити в касу ~1,000 $ на місяць. Також була і третя проблема - деякі розширення для браузера (такі як AdBlock) блокували збір аналітики. http запити з браузера надсилалися на домени GA та Segment. З бажання наших клієнтів, ми зробили сервіс аналітики, який збирає повний набір даних (без семплінгу), безкоштовний і може працювати на власній інфраструктурі.
Як влаштований сервіс
Сервіс складається з трьох частин: javascript піксель (який ми згодом переписали на typescript), серверна частина реалізована мовою GO і як in-house бази даних планувалося використовувати Redshift та BigQuery (пізніше додали підтримку Postgres, ClickHouse та Snowflake).
Структуру подій GA та Segment вирішили залишити без зміни. Все, що було потрібно, це дублювати всі події з web ресурсу, де встановлений піксель, у наш бекенд. Як виявилось, це зробити нескладно. Javascript пікселів перевизначав оригінальний метод бібліотеки GA на новий, який дублював подію в нашу систему.
//'ga' - стандартное название переменной Google Analytics
if (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' - стандартное название переменной Segment
if (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 запити, наповнювати їх додатковою інформацією, наприклад, гео даними (спасибі)
//входящий 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
Також ми подумали, що користувач повинен мати можливість налаштувати партиціонування або розділяти дані в БД за іншими критеріями та реалізували можливість задавати ім'я таблиці константою або
tableName: '{{.product_type}}_{{._timestamp.Format "2006_01"}}'
Однак структура вхідних подій може змінюватися в runtime. Ми реалізували алгоритм перевірки різниці між структурою існуючої таблиці та структурою вхідної події. Якщо різниця знайдено, таблиця буде оновлена новими полями. Для цього використовується patch SQL запит:
#Пример для Postgres
ALTER TABLE "schema"."table" ADD COLUMN new_column character varying
Архітектура
Навіщо потрібно записувати події на файлову систему, а не просто писати їх одразу в БД? Бази даних не завжди демонструють високу продуктивність при великій кількості вставок (
Open Source та плани на майбутнє
Якогось моменту сервіс став схожим на повноцінний продукт і ми вирішили викласти його в Open Source. На даний момент реалізовані інтеграції з Postgres, ClickHouse, BigQuery, Redshift, S3, Snowflake. Усі інтеграції підтримують як batch, і streaming режими завантаження даних. Додано підтримку запитів через API.
Поточна інтеграційна схема виглядає так:
Незважаючи на те, що сервіс можна використовувати самостійно (наприклад за допомогою Docker), у нас також є
→
→
→
Будемо раді, якщо EventNative допоможе вирішити ваші завдання!
Тільки зареєстровані користувачі можуть брати участь в опитуванні.
Яка система збору статистики використовується у вашій компанії
-
48,0%Google Analytics12
-
4,0%Сегмент1
-
16,0%Іншу (напишіть у коментарях)4
-
32,0%Реалізували свій сервіс8
Проголосували 25 користувачів. Утрималися 6 користувачів.
Джерело: habr.com