最近,我有一个任务是学习一个从未使用过或了解过的全新工具,我觉得可以写一写我是怎么学这个工具的。
这样的文章能教你以不同的角度看问题。不是最好的方式,但你可以在这里借用一些想法。
目录表-
- 前言
-
- 基础知识
-
2.1 打破才能成
- 2.2 但是... 其实没那么简单
-
- 文档部分
-
- 源代码
-
- 网络面板
-
- 实现细节
-
- 总结
这是一张图片 
最近我和朋友们用 PHP 和 Laravel 开始了一个新项目(又一个普通的 SaaS 应用程序),项目本身不重要,但技术堆栈很重要。为什么呢?因为我们穷得连自托管的基础设施都买不起,更别提好的付费工具了……所以这一点挺重要的。
所以,如果我们打算构建任何具有集成的东西,它至少需要有一个免费层可使用,并且不能是实现起来像地狱一样困难。想到这里,我们遇到了一个问题:我们该怎么跟踪用户呢?用户跟踪/指标数据将如何进行?
我的目标是展示每个SaaS用户页面的分析。一般的开发者可能会想:我们可以用Google Analytics和Google Tag Manager吧?嗯……让我告诉你Chester说过的一句话:
我这么努力,走这么远的路。但到最后,这根本无所谓了 - Chester Bennington
兄弟,我一直都是讨厌Google界面的人。但这次不是为了工作,而是为了我自己,我没有时间可以浪费在这种事情上。所以,我在我的项目上花了4个小时试图搞清楚……理解如何创建一个简单的标签。结果发现时间都浪费了,因为啥都没搞定。
谷歌的产品怎么都这么难用,得像博士一样才操作得过来?信息量大得吓人!
那时候,我的朋友告诉我关于 PostHog,一个开源分析平台,它让你可以用自托管或云选项来跟踪用户行为(并且在实施上比 GTM/GA4 更胜一筹)。不过,希望你能看到最后,因为这背后可是花费了好几个小时和很多代码行才完成的研究。
2. 基础知识好的,这一点很明显,但还是得说一下:当你尝试一个新工具时,你必须玩转它的每一个角落。我知道我们是开发者,但是UI在告诉你一些东西。
2.1 打破它,直到你做出成果
这个 PostHog 过程的设置时间少得令人难以置信,这确实是一个优点。然而,UI 太杂乱,我的第一个冲动是尝试点击每个按钮,并以最快的速度打破我正在上的项目。
只是和我一起想想:如果你只是因为在云或“生产”环境中就不敢用某个平台或产品,到最后你啥也学不到。
在这个步骤的最后,我直接删除了混乱的项目,然后开始了一个新的项目。不过,我现在已经了解了界面的工作原理。
免责声明: 不要在公司的云账户上做这件事,自己创建一个环境然后随意折腾!把你的环境炸掉!
2.2 但是... 并没有那么简单
好的……我得跟你说实话,有时候打破一切并不是解决问题的方法,因为这甚至不是一个需要这样做的问题,取决于你正在使用的功能。在PostHog他们有类似“TrendsQuery”,“RetentionQuery”,“Web Vitals”这样的东西,每个查询类型都有一个非常酷的UI查询构建器。真的挺厉害的!但是……我一开始并不能立即使用它。
因为我有这样一个很棒的状况叫ADHD,当我看到屏幕上出现简单但带有条件组件的UI时,我就会开始感到失控,变得非常紧张。但这并不是放弃的理由,对吧?
这一部分并不是在批评这款产品,而是说说我的想法。数据科学是一个由无数条件规则组成的领域,信息越多,越准确,而我刚刚开始学习它。
3. 文档部分:
,这是一个可爱的猫咪图片。
当然,这份文档!我之前为什么没接触过呢?我是说,这里不就是开发者的天地吗?嗯,没错!我们是会使用产品的开发者,所以第一步必不可少。
我一般不看教程,虽然我知道大家都这么做。我只是不喜欢,对我来说,好的 API 参考文档应该涵盖所有信息。但在我的理解中,这所谓的“所有东西”具体指什么呢?
- 目录下的标题: 在目录下,这非常有帮助。
- 精简描述: 简洁明了,方便初次使用。
- 实现示例: 使用像 PHP、JS、Go、Rust,以及主要使用的 cURL 这样的工具。
- 预期响应: 根据响应状态(200、401、422、500)返回相应的数据载荷。
- 实现参考: 可以在哪里找到实际代码以阅读和理解客户端和服务器端的具体实现。
- 所需范围: 通常你需要为该端点配置相应的权限范围。
在大多数情况下,你会有许多这样的元素,就像我上面提到的,但这取决于你使用的API。在我的情况下,这个PostHog 特定实现方式 中,挑战是/query/create 端点具有多态的请求和响应方式,因此那里没有返回的数据。
所以我们就开始研究真正的代码了,因为我们得搞清楚这些端点在实际实现中是如何工作的。
为了避免这种情况,我只能写下 HogQL 查询 ,不过这对我来说需要更多时间来学习。
4. 源码解析如图所示
我们现在知道我们想要什么了吧?我们想了解每种请求发送的有效载荷是什么。就我而言,我想了解Retention and Trends Query或“留存和趋势查询”在PostHog中是如何运作的。而且由于这个产品是开源的,我可以在GitHub上直接查看源代码!
前端是用 TypeScript 写的,后端是用 Python 写的。作为一名后端开发者,你可能会认为我会选择深入研究 Python 那部分,对吧?不过,我其实有很多理由讨厌 Python。此外,TypeScript 的类型系统可以作为我搜索代码时的参考。
在TypeScript源码中,我找到了客户端所需的所有实现,但一看完,我立刻掉进了坑里,你就会知道为什么了。
看看这个代码片段(参考链接如下):
(https://github.com/PostHog/posthog/blob/0834410018bc2b7299d8169f828144c5e94b9f9e/frontend/src/queries/schema.ts#L50):
export interface TrendsQuery extends InsightsQueryBase<TrendsQueryResponse> {
kind: NodeKind.TrendsQuery
/**
* 可选的时间间隔。可以是 `hour`, `day`, `week` 或 `month` 中的一个。
*
* @default day
*/
interval?: IntervalType
/** 包含的事件和操作 */
series: AnyEntityNode[]
/** 趋势特有的属性 */
trendsFilter?: TrendsFilter
/** 事件和操作的细分维度 */
breakdownFilter?: BreakdownFilter
}
全屏模式。
退出全屏。
这里我们可以做一些假设。
- 存在一个继承的 InsightsQueryBase,所以其他查询也有这个基础。
- TrendsQueryResponse 可用,而且必须遵循某种模式,所以我们稍后可以检查它。
- 查询中的 'concerns' (可以把它看作一个 过滤器)不够明确,需要做更多研究。
首先,我们需要理解基础知识,并查找 InsightsQuery 关注点 :
/**
* 洞察查询节点的基类。不应直接使用。
*/
export interface InsightsQueryBase<R extends AnalyticsQueryResponseBase<any>> extends Node<R> {
/**
* 查询时间范围
*/
dateRange?: DateRange
/**
* 排除内部和测试用户,应用相应的过滤器
*
* @default false
*/
filterTestAccounts?: boolean
/**
* 所有序列的属性过滤器
*
* @default []
*/
properties?: AnyPropertyFilter[] | PropertyGroupFilter
/**
* 聚合分组索引类型
*/
aggregation_group_type_index?: integer | null
/**
.
* 采样率
*/
samplingFactor?: number | null
/**
* 洞见可视化颜色主题
*/
dataColorTheme?: number | null
/**
* 查询修饰符
*/
modifiers?: HogQLQueryModifiers
}
全屏 退出全屏
那就是你意识到自己掉进“兔子坑”的时刻,因为这些问题似乎成了你API中一些拥有不同功能的巨大元素。
经过几个小时后,我开始意识到这些担忧如何影响查询,例如,之类的。
- 分解: 更像是 GroupBy 子句的用法
- 属性: 多态过滤,完全依赖于查询条件
- 序列: 所检索数据的类型(例如 "浏览次数")
- 区间: 从/到日期范围的过滤
这只是冰山一角。围绕PostHog上的众多请求,还有很多其他问题。但下面是一些问题:
- 如果有一个InsightsQueryBase,这些担忧是否也会被其他类型的该类查询共享?
- 是否有可能让其他类型的基查询也抱有同样的担忧?
- 我是否需要理解每种担忧类型如何影响最终查询?
我的意思是,我们要明白。也许不需要那么深入,但是起码要有基本的了解。但是文档里没有例子,我们怎么证实呢?
那么,现在让我来讲讲我最喜欢浏览器的地方——网络标签。
5. 网络选项卡我刚刚通过直接在浏览器里对端点进行A/B测试,学到了如何“探索”网页应用。你可能在想:
"因为不同的服务,你可能建不了自用机器人,因为这可能会违反服务条款规定" - 一般用户
而且我会回答说我根本无所谓,因为我这么做是出于教育目的。如果我这么做,那是因为想了解事情是如何运作的,想自己动手做工具并使用这些工具。
只是一个随机的事实:从2014年开始,我就一直在检查请求。我的第一个“真正的(真)”GitHub项目基本上是检查浏览器游戏中所有的请求,该游戏名为“TribalWars”,并构建了一个命令行工具/机器人通过我的终端玩该游戏,最终目的是不再亲自玩游戏(创建一个自我机器人账户)。
回到PostHog的实现上,当我开始检查时,我发现每种查询都有不同类型的请求负载。这样我就可以提出更多假设,并用于下一步的考量。
{
"client_query_id": "54f620e7-fdfe-4749-af41-caed0a3fe671",
"query": {
"breakdownFilter": {
"breakdown": "$geoip_country_code",
"breakdown_type": "事件"
},
"conversionGoal": null,
"dateRange": {
"date_from": "-7d",
"date_to": null
},
"filterTestAccounts": false,
"kind": "趋势查询",
"properties": [],
"series": [
{
"event": "$pageview",
"kind": "事件节点",
"math": "dau",
"name": "页面浏览"
}
],
"trendsFilter": {
"display": "世界地图"
}
},
"refresh": "异步"
}
全屏查看 退出全屏
以防万一,第二个查询也备上吧 :p
{
"client_query_id": "25d4b82f-5bbe-4665-89a7-7aedcc7b103e",
"query": {
"dateRange": {
"date_from": "-7d",
"date_to": null
},
"filterTestAccounts": false,
"kind": "RetentionQuery",
"properties": [],
"retentionFilter": {
"period": "Week",
"retentionReference": "total",
"retentionType": "retention_first_time",
"totalIntervals": 8
}
},
"refresh": "async"
}
全屏模式 或 退出全屏
不过包含结果查询会涉及太多代码,这部分的重点已经讲明白了。现在是时候把这个流程收尾了。
第六步:实施阶段 这是一张可爱的狗狗的照片。
那就是你开始想到,“这些工具是否能满足我的需求?如果SDK不提供一个完全类型化的API,我宁愿自己从零开始。我的意思是,我喜欢从完全从零开始做事情!就是那种从头再来的意思,你知道的。
原因很简单:我是一名开发者,通过重建事物并观察它们怎么运作来学习。因此,我决定用PHP开发一个PostHog专用查询构建器,来了解这边的工作原理。
到目前为止,我就得到了这些。
- 我们支持多种查询类型;
- 这些查询可以继承一些属性和关注点,但不一定使用所有这些属性;
- 查询可以共享过滤器和关注点;
那么我们该怎么组织呢?而在源和网络标签里找了半天之后,我的答案是:
------
- 我在我的 PostHogSDK 中实现的 QueryBuilder 功能
------
查询
├── 构建者
│ ├── AbstractQueryBuilder.php
│ └── Insights
│ ├── AbstractInsightsQueryBuilder.php
│ ├── RetentionQueryBuilder.php
│ └── TrendsQueryBuilder.php
├── 过滤器
│ ├── 分组
│ │ ├── BreakdownFilter.php
│ │ ├── BreakdownTypeEnum.php
│ │ └── Concerns
│ │ └── InteractsWithBreakdown.php
│ ├── 比较
│ │ ├── CompareFilter.php
│ │ └── Concerns
│ │ └── InteractsWithCompare.php
│ ├── 转换目标
│ │ ├── ActionConversionGoal.php
│ │ ├── Concern
│ │ │ └── InteractsWithConversionGoal.php
│ │ ├── Contracts
│ │ │ └── ConversionGoalContract.php
│ │ └── CustomEventConversionGoal.php
│ ├── 时间区间
│ │ ├── Concerns
│ │ │ └── InteractsWithDateRange.php
│ │ └── DateRangeFilter.php
│ ├── 间隔
│ │ ├── Concerns
│ │ │ └── InteractsWithInterval.php
│ │ └── QueryIntervalEnum.php
│ ├── 节点
│ │ ├── ActionNode.php
│ │ ├── Concerns
│ │ │ ├── InteractsWithMath.php
│ │ │ └── InteractsWithSeries.php
│ │ ├── Contracts
│ │ │ └── EntityNodeContract.php
│ │ ├── EntityNodeKindEnum.php
│ │ ├── EntityNode.php
│ │ ├── EventsNode.php
│ │ └── MathEnum.php
│ ├── 属性
│ │ ├── BaseProperty.php
│ │ ├── Concerns
│ │ │ └── InteractsWithProperties.php
│ │ ├── Contracts
│ │ │ └── PropertyFilterContract.php
│ │ ├── Filters
│ │ │ ├── EventPropertyFilter.php
│ │ │ └── SessionPropertyFilter.php
│ │ ├── PropertyFilterKind.php
│ │ └── PropertyOperator.php
│ └── 留存
│ ├── Concerns
│ │ └── InteractsWithRetention.php
│ ├── Enums
│ │ ├── RetentionPeriodEnum.php
│ │ ├── RetentionReferenceEnum.php
│ │ └── RetentionTypeEnum.php
│ └── RetentionFilter.php
├── QueryBuilderInterface.php
├── QueryFactory.php
└── QueryKindEnum.php
全屏模式 退出全屏
每个过滤器都有自己专属的位置,可以被使用 PHP Traits 继承到任何类型的构建器。
带有交互日期范围特性的类
{
protected ?日期范围筛选器 $日期范围 = null;
public function 设置日期范围(日期范围筛选器 $日期范围): 返回 $this
{
$this->日期范围 = $日期范围;
return $this;
}
public function 获取日期范围(): ?日期范围筛选器
{
return $this->日期范围;
}
private function 构建日期范围信息(array &$payload): void
{
if ($this->日期范围 !== null) {
$payload['dateRange'] = [
'date_from' => $this->日期范围->开始日期,
'date_to' => $this->日期范围->结束日期,
];
}
}
}
全屏查看 退出全屏
所以查询构建器只需要定义我QueryBuilderContract 中的 'build()' 方法,这样我就可以确保至少在最后类型会被正确处理。下面类中的每次使用都代表一个不同的常见查询构建器问题。
class TrendsQueryBuilder extends AbstractInsightsQueryBuilder
{
use InteractsWithInterval,
InteractsWithDateRange,
InteractsWithBreakdown,
InteractsWithCompare,
InteractsWithConversionGoal,
InteractsWithProperties,
InteractsWithSeries;
protected QueryKindEnum $queryType = QueryKindEnum::TrendsQuery;
public static function make(): self
{
return new self();
}
public function build(): array
{
return $this->jsonSerialize();
}
public function jsonSerialize(): array
{
$payload = parent::jsonSerialize();
$payload['kind'] = $this->queryType->value;
$this->buildDateRange($payload);
$this->buildInterval($payload);
$this->buildSeries($payload);
$this->buildCompare($payload);
$this->buildProperties($payload);
$this->buildConversionGoal($payload);
$this->buildBreakdown($payload);
return $payload;
}
}
点击这里进入全屏,出全屏
在编码时,我们应该更加留心那些非常细微的细节。说实话,在构建这种工具时,我通常会想到有人会使用它,这使得我的关注度更高。
这项研究的最终成果是一个功能齐全的趋势和保留的QueryBuilder,用于PostHog API:
test('可以构建一个保留查询', function () {
$expected = [
"kind" => "保留查询",
"dateRange" => [
"date_from" => "-7d",
"date_to" => null
],
"filterTestAccounts" => false,
"retentionFilter" => [
"retentionType" => "首次保留",
"retentionReference" => "总计",
"totalIntervals" => 8,
"period" => "周"
]
];
$queryBuilder = 保留查询构建器::make()
->setDateRange(日期范围过滤器::from('-7d'))
->setRetention(保留过滤器::每周()
->设置保留类型(保留类型枚举::FIRST_TIME)
->设置保留参考(保留参考枚举::总计)
);
期望($queryBuilder)为实例于保留查询构建器
->and($queryBuilder->build())->toMatchArray($expected);
});
test('可以构建一个趋势查询', function () {
$expected = [
'kind' => '趋势查询',
'filterTestAccounts' => false,
'dateRange' => [
'date_from' => '-7d',
'date_to' => null,
],
'interval' => '天',
'series' => [
[
'event' => '$pageview',
'kind' => 'EventsNode',
'math' => 'dau',
'name' => '页面浏览量',
],
],
'compareFilter' => [
'compare' => true,
],
'properties' => [
[ 'type' => 'event',
'key' => '$host',
'operator' => 'exact',
'value' => 'api-main-ofjibb.laravel.cloud',
],
],
];
$actual = 趋势查询构建器::make()
->setDateRange(日期范围过滤器::from('-7d'))
->设置时间间隔(查询间隔枚举::天)
->添加比较过滤器()
->添加序列(EventsNode::make('$pageview', MathEnum::DAU, '页面浏览量'))
->添加属性(EventPropertyFilter::make('$host', PropertyOperator::Exact, 'api-main-ofjibb.laravel.cloud'));
期望($actual)为实例于趋势查询构建器
->and($actual->build())->toMatchArray($expected);
});
全屏模式 退出全屏
你可以通过点击这里查看请求页面:点击这里。也欢迎你为这个项目贡献。
7. 最后的总结这只是一个我非常喜欢并决定写下来的关于PostHog API的研究。从零开始构建并进行深入研究的过程可以让你更深层次地学习东西。
现在我可以告诉你了,我可以非常流畅地使用PostHog平台,没有了文章第一部分提到的那些担忧,因为我只拆分了一次,分别解决了问题并单独研究了它们。
这种方法——破坏性测试,查看文档,探索网络选项卡,以及从头开始重建——可以应用于任何技术工具。无论是PostHog,新的API,还是一个框架,了解内部逻辑会让你成为更好的开发者。
就这样!希望你喜欢,别忘了喝水!
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質文章