Історія нашого open source: як ми зробили сервіс аналітики на Go та виклали його у відкритий доступ

В даний час практично кожна компанія у світі збирає статистику про дії користувача на веб-ресурсі. Мотивація зрозуміла — компанії хочуть знати як використовується їхній продукт/веб сайт і краще розуміти своїх користувачів. Звичайно, на ринку існує велика кількість інструментів для вирішення цієї проблеми — від систем аналітики, які надають дані у вигляді дашбордів та графіків (наприклад Google Analytics) до Customer Data Platform, які дозволяють збирати та агрегувати дані з різних джерел у будь-якому сховищі (наприклад Сегмент).

Але ми знайшли проблему, яку ще не було вирішено. Так народився EventNative - open-source сервіс аналітики. Про те, чому ми пішли на розробку свого сервісу, що нам це дало і що в результаті вийшло (з шматками коду), читайте під катом.

Історія нашого open source: як ми зробили сервіс аналітики на Go та виклали його у відкритий доступ

Навіщо нам розробляти власний сервіс?

Це були дев'яності, ми виживали як могли. 2019 рік ми розробляли API First Customer Data Platform kSense, яка дозволяла агрегувати дані з різних джерел (Facebook ads, Stripe, Salesforce, Google Play, Google Analytics та ін.) для зручнішого аналізу даних, виявлення залежностей і т.д. Ми помітили, що багато користувачів використовують нашу платформу для аналізу даних Google Analytics (далі GA). З деякими користувачами ми поговорили і з'ясували, що їм потрібні дані аналітики їхнього продукту, які вони отримують за допомогою GA, але Google семплює дані і для багатьох GA User interface не є еталоном зручності. Ми провели достатню кількість розмов з нашими користувачами і зрозуміли, що багато хто також використовував платформу Segment (яка, до речі, буквально днями була продано за 3.2 млрд$).

Вони встановлювали 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 події. Типи даних визначаються за значеннями. Вкладені об'єкти розкладаються та наводяться до плоскої структури:

//входящий 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 запит:

#Пример для Postgres
ALTER TABLE "schema"."table" ADD COLUMN new_column character varying

Архітектура

Історія нашого open source: як ми зробили сервіс аналітики на Go та виклали його у відкритий доступ

Навіщо потрібно записувати події на файлову систему, а не просто писати їх одразу в БД? Бази даних не завжди демонструють високу продуктивність при великій кількості вставок (рекомендації Postgres). Для цього Logger записує вхідні події у файл і вже в окремій горутині (потоку) File reader читає файл, далі відбувається перетворення та визначення схеми даних. Після того, як Table manager переконається, що схема таблиці актуальна - дані будуть записані в БД одним батчем. Згодом ми додали можливість записувати дані безпосередньо в БД, але застосовуємо такий режим для подій, яких небагато — наприклад, конверсії.

Open Source та плани на майбутнє

Якогось моменту сервіс став схожим на повноцінний продукт і ми вирішили викласти його в Open Source. На даний момент реалізовані інтеграції з Postgres, ClickHouse, BigQuery, Redshift, S3, Snowflake. Усі інтеграції підтримують як batch, і streaming режими завантаження даних. Додано підтримку запитів через API.

Поточна інтеграційна схема виглядає так:

Історія нашого open source: як ми зробили сервіс аналітики на Go та виклали його у відкритий доступ

Незважаючи на те, що сервіс можна використовувати самостійно (наприклад за допомогою Docker), у нас також є hosted версія, в якій можна налаштувати інтеграцію зі сховищем даних, додати CNAME на свій домен і переглянути статистику за кількістю подій. Наші найближчі плани – додавання можливості агрегувати не лише статистику з веб-ресурсу, але й дані із зовнішніх джерел даних та зберігати їх у будь-яке сховище на вибір!

→ GitHub
→ Документація
→ Млявий

Будемо раді, якщо EventNative допоможе вирішити ваші завдання!

Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.

Яка система збору статистики використовується у вашій компанії

  • 48,0%Google Analytics12

  • 4,0%Сегмент1

  • 16,0%Іншу (напишіть у коментарях)4

  • 32,0%Реалізували свій сервіс8

Проголосували 25 користувачів. Утрималися 6 користувачів.

Джерело: habr.com

Додати коментар або відгук