我们开源的历史:我们如何在 Go 中创建分析服务并将其公开可用

目前,世界上几乎每家公司都会收集有关网络资源上的用户操作的统计数据。 动机很明确——公司想知道他们的产品/网站是如何使用的,并更好地了解他们的用户。 当然,市场上有大量工具可以解决这个问题 - 来自以仪表板和图表形式提供数据的分析系统(例如, Google Analytics)到客户数据平台,该平台允许您从任何存储中的不同来源收集和聚合数据(例如, 分割).

但我们发现一个问题还没有解决。 就这样诞生了 事件原生 — 开源分析服务。 关于我们为什么要开发自己的服务、它给我们带来了什么以及最终发生了什么(通过代码片段),请阅读下文。

我们开源的历史:我们如何在 Go 中创建分析服务并将其公开可用

我们为什么要开发自己的服务?

那是九十年代,我们尽力生存。 2019年,我们开发了第一个客户数据平台API 科感,它允许聚合来自不同来源(Facebook 广告、Stripe、Salesforce、Google play、Google Analytics 等)的数据,以便更方便地进行数据分析、识别依赖关系等。 我们注意到许多用户使用我们的数据分析平台,特别是Google Analytics(以下简称GA)。 我们与一些用户交谈,发现他们需要使用 GA 接收的产品分析数据,但是 谷歌样本数据 对于许多 GA 用户界面来说并不是一个方便的标准。 我们与用户进行了足够的对话,并意识到许多人也使用了 Segment 平台(顺便说一句,就在几天前) 以 3.2 亿美元出售).

他们在其 Web 资源上安装了 Segment javascript 像素,并将用户行为数据加载到指定的数据库(例如 Postgres)中。 但 Segment 也有其缺点——价格。 例如,如果某个 Web 资源有 90,000 MTU(每月跟踪用户),那么您每月需要向收银员支付约 1,000 美元。 还有第三个问题 - 某些浏览器扩展(例如 AdBlock)阻止了分析收集。 来自浏览器的 http 请求被发送到 GA 和 Segment 域。 根据客户的愿望,我们创建了一项分析服务,可以免费收集全套数据(无需采样),并且可以在我们自己的基础设施上运行。

服务如何运作

该服务由三部分组成:JavaScript Pixel(我们后来将其重写为 TypeScript)、用 GO 语言实现的服务器部分,并计划使用 Redshift 和 BigQuery 作为内部数据库(后来他们添加了对 Postgres 的支持) 、ClickHouse 和 Snowflake)。

事件 GA 和 Segment 的结构决定保持不变。 所需要做的就是将安装像素的网络资源中的所有事件复制到我们的后端。 事实证明,这很容易做到。 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 Pixel,一切都变得更简单,它有中间件方法,我们使用了其中之一。


//'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'
});

接下来我们来说一下服务器端。 后端应该接受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"
}

然而,数组目前只是简单地转换为字符串。 并非所有关系数据库都支持重复字段。 还可以使用可选的映射规则更改字段名称或删除它们。 它们允许您根据需要更改数据模式,或将一种数据类型转换为另一种数据类型。 例如,如果 json 字段包含带有时间戳的字符串 (field_3_sub_field_1_sub_sub_field_1 从上面的例子),那么为了在数据库中创建时间戳类型的字段,需要在配置中编写映射规则。 换句话说,字段的数据类型首先由 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字段的值计算的(例如 耗材_2020_10):

tableName: '{{.product_type}}_{{._timestamp.Format "2006_01"}}'

但是,传入事件的结构可能会在运行时发生变化。 我们实现了一种算法来检查现有表的结构与传入事件的结构之间的差异。 如果发现差异,表将使用新字段进行更新。 为此,请使用修补 SQL 查询:

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

建筑

我们开源的历史:我们如何在 Go 中创建分析服务并将其公开可用

为什么需要将事件写入文件系统,而不是直接写入数据库? 数据库并不总是在大量插入时表现出高性能(Postgres 建议)。 为此,Logger 将传入事件写入文件,并且已经在单独的 goroutine(线程)中的文件读取器读取该文件,然后进行数据模式的转换和定义。 表管理器确保表架构是最新的后,会将数据批量写入数据库。 随后,我们添加了直接将数据写入数据库的功能,但我们将这种模式用于事件不多的情况——例如转换。

开源和未来计划

在某种程度上,该服务变得像一个成熟的产品,我们决定将其开源。 目前,与 Postgres、ClickHouse、BigQuery、Redshift、S3、Snowflake 的集成已经实现。 所有集成都支持批处理和流数据加载模式。 添加了对通过 API 请求的支持。

当前的集成方案如下所示:

我们开源的历史:我们如何在 Go 中创建分析服务并将其公开可用

虽然服务可以独立使用(例如使用Docker),但我们也有 托管版本,您可以在其中设置与数据仓库的集成、向您的域添加 CNAME 以及查看事件数量的统计信息。 我们的近期计划是不仅能够聚合来自网络资源的统计数据,还能够聚合来自外部数据源的数据,并将它们保存到您选择的任何存储中!

→ GitHub上
→ Документация
→ 松弛

如果 EventNative 能够帮助您解决问题,我们将非常高兴!

只有注册用户才能参与调查。 登录拜托

贵公司使用什么统计系统

  • 48,0%Google Analytics12

  • 4,0%段 1

  • 16,0%其他(写在评论里) 4

  • 32,0%实施您的服务8

25 位用户投票。 6 名用户弃权。

来源: habr.com

添加评论