如何用履约建模法设计埋点平台数据结构

一句话先讲清楚

如果我要设计的不是某个业务系统里的埋点,而是埋点平台本身,我不会先从 click 或 pageview 开始建,而会先用履约建模法把平台自己的承诺关系找出来,也就是谁定义事件、谁提交数据、平台怎么验收、最后又怎么把数据交付给下游。

这句话是核心。


30 秒版本

我会把埋点平台看成一个数据履约平台,而不是简单的日志收集系统。因为它真正要解决的,不是“收到了多少事件”,而是事件有没有定义、有没有按约定提交、平台有没有确认接收、数据质量合不合格、下游能不能稳定消费。所以我会先按履约建模法拆上下文,再去落事件定义、采集请求、验收结果和数据交付这些核心结构。


1 分钟版本

如果让我用履约建模法来设计埋点平台,我不会先问“埋哪些按钮”,而会先问平台里到底有哪些履约关系。因为埋点平台本身不是业务系统,它更像一个中间平台,核心是管理数据的定义、接入、验收和消费。

所以我会先拆四个上下文。第一是事件定义上下文,也就是谁定义了什么事件、字段和口径;第二是采集接入上下文,也就是 SDK 或客户端把什么数据提交进来;第三是数据验收上下文,也就是平台怎么校验、接收、拒收或者隔离这些数据;第四是数据消费上下文,也就是报表、分析、告警这些下游怎么稳定消费平台交付的数据。

这样拆完以后,数据结构就不会只是一个 event 表,而会变成一条完整的数据履约链路,比如事件定义合同、上报请求凭证、事件事实、校验确认、交付记录。这样的平台结构会更适合做治理、审计和追责。


我在面试里会怎么口语化讲这个建模过程

我通常会按下面这个顺序讲,而不是一上来就甩表结构。

第一步:先澄清我建模的对象到底是什么

我先区分了一下,我要建模的不是业务系统里的“订单埋点”或者“任务埋点”,而是埋点平台本身。也就是说,我关心的是这个平台怎么定义事件、怎么接收数据、怎么做质量控制、怎么给下游交付数据。对象一旦变了,核心结构就不应该再从按钮点击开始,而应该从平台自己的履约关系开始。

这一步其实是在找合同上下文。

第二步:先找平台里有哪些“合同关系”

按履约建模法,我会先看平台里到底谁和谁之间存在承诺关系。比如业务方和平台之间,承诺的是“我按约定给你提交事件”;平台和业务方之间,承诺的是“我按照定义帮你接收、校验和保存”;平台和下游分析系统之间,承诺的是“我给你交付可消费的数据能力”。

所以我会先拆出四个上下文:

  • 事件定义上下文
  • 采集接入上下文
  • 数据验收上下文
  • 数据消费上下文

第三步:从履约凭证而不是从字段开始

我不会先说 event 表有哪些字段,而会先找每个上下文里的凭证。因为履约建模法强调的是证据链。对于埋点平台来说,真正重要的不是“平台里有条数据”,而是“这条数据是按什么定义来的、谁提交的、平台是否确认接收、最后有没有被交付出去”。

所以我会先抽这几类核心凭证:

  • 事件定义凭证:EventSpec
  • 采集请求凭证:IngestionRequest
  • 事件事实凭证:EventFact
  • 验收确认凭证:ValidationResult
  • 数据交付凭证:DeliveryRecord

第四步:再回到数据项,问每个字段从哪里来

到这一步我才会开始落字段。这里我会用履约建模法里一个很重要的思路,就是每个关键数据项都要能回答来源。来源只可能有三种:用户输入、前序凭证提供、算法计算。

比如 eventKey 来自事件定义合同,requestId 来自采集请求凭证,status 可能来自平台校验结果,rawHash 是平台算法计算出来的去重依据。这样建出来的结构不是拍脑袋补字段,而是能顺着证据链往回追。

第五步:最后再补角色和标的物

等凭证流有了,我再去补参与方。比如谁是 Producer,也就是 SDK / App / 服务端;谁是 Platform;谁是 Consumer;谁是治理方。然后再补标的物,比如 EventSpec、EventBatch、EventFact、Dataset。

这样最后得到的不是孤零零的一张事件表,而是一整套围绕“定义 - 提交 - 验收 - 交付”的平台模型。


我最终会落下来的核心数据结构

1. 事件定义合同 EventSpec

type EventSpec = {
  specId: string
  eventKey: string
  eventName: string
  category: string
  ownerTeam: string
  ownerUser?: string
  version: number
  status: draft | reviewing | active | deprecated
  fields: EventField[]
  contextFields: string[]
  triggers?: string[]
  createdAt: number
  updatedAt: number
}
 
type EventField = {
  fieldKey: string
  fieldName: string
  type: string | number | boolean | object | array
  required: boolean
  description?: string
  enumValues?: string[]
}

这个结构解决的是“埋什么”和“按什么口径埋”。

2. 采集请求凭证 IngestionRequest

type IngestionRequest = {
  requestId: string
  appId: string
  sdkVersion: string
  tenantId?: string
  source: web | mobile | server | miniapp
  batchId: string
  eventCount: number
  receivedAt: number
  transport: beacon | xhr | fetch | kafka
  traceId?: string
  payloadRef?: string
}

这个结构解决的是“谁在什么时候提交了一批什么数据”。

3. 事件事实 EventFact

type EventFact = {
  factId: string
  requestId: string
  specId?: string
  eventKey: string
  eventTime: number
  ingestTime: number
  userId?: string
  anonymousId?: string
  sessionId?: string
  deviceId?: string
  page?: string
  payload: Record<string, unknown>
  rawHash: string
}

这个结构保存真正发生过的事件,但它必须能回挂到定义合同和采集请求。

4. 验收确认 ValidationResult

type ValidationResult = {
  validationId: string
  factId: string
  specId?: string
  status: accepted | rejected | quarantined
  errors: ValidationIssue[]
  checkedAt: number
}
 
type ValidationIssue = {
  code: string
  level: warn | error
  fieldKey?: string
  message: string
}

这个结构解决的是“平台是否确认接收,以及为什么拒收”。

5. 数据交付记录 DeliveryRecord

type DeliveryRecord = {
  deliveryId: string
  consumerId: string
  datasetId: string
  mode: dashboard | export | stream | alert
  windowStart?: number
  windowEnd?: number
  deliveredAt: number
  status: success | partial | failed
}

这个结构解决的是“平台最后把什么数据交付给了谁”。


如果面试官追问“为什么不是一个大 event 表就够了”

因为一个大 event 表只能回答“发生了什么”,但回答不了“这条数据按哪个定义来的、是不是合法提交、为什么被拒收、最后有没有被交付给下游”。埋点平台如果只建 event 表,本质上还是日志收集系统;但如果按履约建模法去建,它才会变成一个真正可治理、可审计、可追责的数据平台。


如果面试官追问“履约建模法在这里最有价值的点是什么”

我觉得最有价值的点,是它逼着我先去找平台自己的承诺关系和凭���流,而不是一开始就沉进技术字段里。这样最后建出来的数据结构天然更稳定,也更适合长期治理。


最后一句收尾

所以如果让我总结,我不是用履约建模法去描述某个业务事件,而是用它先把埋点平台自己的定义、提交、验收和交付关系建清楚。最后得到的数据结构也不是单张事件表,而是一整套围绕证据链展开的平台模型。