资讯动态

OpenReplay 消息二进制协议与 MOBS 代码生成器:从 Schema DSL 到多语言产物的完整指南

发布时间:2026/9/23 6:37:05 来源:尧图企业网站定制
OpenReplay 消息二进制协议与 MOBS 代码生成器从 Schema DSL 到多语言产物的完整指南【免费下载链接】openreplaySession replay, cobrowsing and product analytics you can self-host. Best for reproducing issues and iterating on your product.项目地址: https://gitcode.com/gh_mirrors/op/openreplayMOBSMessage Object Binary Schema是 OpenReplay 仓库中负责定义会话录制消息二进制格式并从单一 Schema 定义生成 Go / TypeScript / Python / Swift 多语言代码的代码生成子系统。本文以 mobs/README.md 为主线结合 mobs/messages.rb、mobs/run.rb、mobs/templates 及三套语言原语实现系统讲解消息 Schema 的 DSL 语法、消息 ID 空间分配、类型映射、ERB 模板渲染原理与底层二进制编解码细节。读完本文你将掌握 OpenReplay 会话消息从定义到代码的完整链路并具备在仓库中定位、理解乃至新增一条消息的实战能力。一、MOBS 是什么一条 Schema 定义五套语言代码OpenReplay 的会话回放Session Replay体系横跨多个技术栈浏览器端 tracker/tracker 负责采集并序列化用户行为backend/pkg/messages 负责消息的解码、存储与管道处理frontend/app/player/web/messages 负责前端播放器还原页面ee/connectors/msgcodec 负责数据管道中的编解码。这些模块必须对同一种二进制消息格式保持严格一致——任何字段增删、类型调整或消息 ID 变更都必须在所有语言中同步。MOBS 正是为解决这一问题而存在的代码生成子系统开发者只需在 Ruby DSL 文件中声明消息结构再执行一行命令即可自动生成所有语言的消息定义、编码函数和解码分发逻辑。仓库根目录下的 mobs/README.md 对此给出最简说明Message Object Binary Schema and Code Generator from Templates To generate all necessary files for the project:sh generate.sh这条命令背后是一个由 Schema 文件、Ruby 生成器脚本、ERB 模板和三套原语实现构成的完整流水线其组成如下角色文件职责生成入口mobs/generate.sh调用 Ruby 生成器并格式化 Go 输出生成器核心mobs/run.rb解析 Schema、渲染 ERB 模板、写出产物Web 消息定义mobs/messages.rb声明全部 Web 端消息ID 0–127移动端消息定义mobs/mobile_messages.rb声明 iOS/Android 移动端消息ID 90 起模板集合mobs/templates/*.erb每种目标语言/模块对应一个 ERB 模板二进制原语Gomobs/primitives/primitives.gouint/int/boolean/string/size 的读写实现二进制原语Pythonmobs/primitives/primitives.py与 Go 对齐的 Python 版编解码原语二进制原语Swiftmobs/primitives/primitives.swiftiOS 端原语实现从架构上看这形成了一条Schema DSL → Ruby 元编程解析 → ERB 模板渲染 → 多语言代码落盘的经典代码生成链路保证协议的唯一事实来源Single Source of Truth始终是 Ruby 定义文件本身。二、消息 Schema DSL用 Ruby 声明二进制协议消息定义的核心是 mobs/messages.rbWeb与 mobs/mobile_messages.rb移动端两个文件。它们不是配置文件而是被 mobs/run.rb 直接require的 Ruby 代码通过一个极简的领域特定语言DSL描述每条消息。2.1 顶层 API 与消息声明run.rb中定义了全局函数message(id, name, opts, block)并在每次声明时做 ID 去重校验$ids [] $messages [] def message(id, name, opts {}, block) raise id duplicated #{name} if $ids.include? id $ids id opts[:id] id opts[:name] name msg Message.new(**opts, block) $messages msg end每条消息通过Message类持有元数据id消息类型 ID、name消息名、tracker、replayer、swift、pipeline管道分类以及attributes字段列表。声明字段时DSL 为int、uint、boolean、string、data五种基本类型各注册了一个同名方法把字段名与类型封装成Attribute对象%i(int uint boolean string data).each do |type| define_method type do |name, opts {}| opts.merge!(name: name, type: type) attributes Attribute.new(**opts) end end2.2 一个真实的消息声明示例以 Web 端最常用的SetNodeAttribute消息 ID 12为例message 12, SetNodeAttribute do uint ID string Name string Value end字段名遵循 PascalCase 命名约定。run.rb中内置了命名转换工具函数pascal_casePascalCase、camel_casecamelCase、snake_casesnake_case并做了Id → ID、Url → URL的缩写规范化。因此生成出的各语言标识符风格可以自动适配Go 里是ID uint64、Name stringTypeScript 里是id: number、name: stringPython 里是self.i_d、self.name。再看一个带选项的消息NetworkRequestID 83展示选项参数的用法message 83, NetworkRequest, :replayer :devtools, :pipeline d do string Type # fetch/xhr/anythingElse(axios,gql,fonts,image?) string Method string URL string Request string Response uint Status uint Timestamp uint Duration uint TransferredBodySize end2.3 消息选项的含义每条消息可携带如下选项默认值定义在Message#initialize中选项默认值作用:tracker当前上下文为 web 时为true控制该消息是否由 tracker 采集端生成false表示仅后端/服务端使用:replayer当前上下文为 web 时为true控制该消息是否参与前端回放false表示后端专用:devtools表示仅用于开发者工具面板:swift当前上下文为 ios 时为true标记是否为 iOS 端消息:pipelinenil管道分类aURL 基址重写类、b业务/后端聚合类、ddevtools 面板类等例如SessionStart声明为:tracker false, :replayer false因为它由后端在会话开始时合成JSException声明为:replayer false, :pipeline b表明它走后端聚合管道。这些标记在模板渲染时被大量用于过滤——例如 TS 播放器类型只挑选msg.replayer ! false的消息。2.4 双上下文加载Web 与移动端共享同一生成器run.rb通过切换全局上下文变量$context实现一套生成器同时服务 Web 与移动端$context :web require ./messages.rb $context :ios require ./mobile_messages.rb$context会作为默认值传入Message构造器tracker: $context :web、swift: $context :ios因此移动端消息天然标记为swift: true且tracker: false。移动端消息从 ID 90 开始mobs/mobile_messages.rb 顶部注释明确90-111 reserved iOS例如MobileSessionStart90、MobileScreenChanges96、MobileClickEvent100等。三、消息类型体系与 ID 空间从 mobs/messages.rb 的完整定义看Web 消息 ID 空间覆盖 0–127按功能可划分为几大类类别典型消息ID 示例会话生命周期Timestamp、SessionStart、SessionEnd、SessionSearch0 / 1 / 126 / 127DOM 树操作CreateElementNode、CreateTextNode、MoveNode、RemoveNode、SetNodeAttribute8 / 9 / 10 / 11 / 12视图与滚动SetViewportSize、SetViewportScroll、SetNodeScroll5 / 6 / 16用户交互MouseMove、MouseClick、InputEvent、SelectionChange20 / 68 / 32 / 113网络与性能NetworkRequest、ResourceTiming、WSChannel、PageLoadTiming、WebVitals83 / 85 / 84 / 23 / 124框架状态devtoolsRedux、Vuex、MobX、NgRx、Zustand、GraphQL121 / 45 / 46 / 47 / 79 / 123异常与错误JSException、CustomIssue、Incident78 / 64 / 87后端专用IssueEvent、SessionEnd、SessionSearch125 / 126 / 127移动端MobileSessionStart、MobileCrash、MobileViewComponentEvent等90–111文件中还存在大量协议演进痕迹注释精确标注了废弃时间线例如SetPageLocationDeprecated4# DEPRECATED since 14.0.0 - goto 122新版本为SetPageLocation122新增了DocumentTitle字段ResourceTimingDeprecatedDeprecated53# deprecated during 1.16.0 releaseStringDictDeprecated50# deprecated 10.2024 (v1.21) - removed 2025。这种旧 ID 永久保留、新消息申请新 ID的策略是二进制协议的行业惯例——已落盘的历史数据必须能按旧 ID 解码。文件末尾的# FREE 2, 34, 35, 36, 65, 85, 86, 87, 88则列出了一组空闲 ID注其中 85 实际已被ResourceTiming使用可视为注释与实现的细微出入供后续协议扩展使用。任何新增消息必须避开已占用 ID因为run.rb会在重复 ID 时直接抛出id duplicated异常。四、属性类型与多语言映射Attribute类定义了四种基本类型 两种扩展类型的映射规则这是保证多语言一致性的核心表类型Gotype_goTypeScripttype_jsCythontype_pyx长度编码lengh_encodedintint64numberlong否zigzag 变长uintuint64numberunsigned long否LEB128 变长stringstringstringstr是前置长度data[]byte不支持模板中抛异常str是前置长度booleanbool—默认透传bint否单字节 0/1jsoninterface{}string带 TODO 注释str视实现而定值得注意的细节data类型在 JS 模板中直接raise异常说明当前 Web/TS 模板不期望出现二进制块字段json类型在 JS 侧暂以string兜底源码中留有# TODO注释属于未完成的映射从源码结构看 json 字段目前主要用于 Go 侧的结构化承载生成 Go 编码缓冲区大小时messages.go.erb采用估算公式attributes.count * 10 1每条属性预留 10 字节变长编码空间 1 字节消息 ID并叠加所有长度编码字段的len(field)最终按实际写入位置buf[:p]截断兼顾性能与正确性。五、生成器工作原理run.rb 的模板渲染管线mobs/run.rb 是整个代码生成器的心脏完整流程如下# 1. 定义 String / Attribute / Message 等 DSL 基础设施 # 2. 定义 $ids / $messages 全局容器与 message() 注册函数 # 3. 加载 Web 消息定义 $context :web require ./messages.rb # 4. 加载移动端消息定义 $context :ios require ./mobile_messages.rb # 5. 遍历 templates/*.erb 渲染全部产物 Dir[templates/*.erb].each do |tpl| e ERB.new(File.read(tpl)) path tpl.split / t ../ path[1].gsub(~, /) # ~ → / t t[0..-5] # 去掉 .erb 后缀 File.write(t, e.result) puts tpl -- t end关键设计在于模板文件名即输出路径~被替换为目录分隔符/.erb被剥离再以mobs/为基准加../前缀从而映射到仓库根目录下的真实源码位置。例如模板文件mobs/templates 下生成产物仓库根目录相对backend~pkg~messages~messages.go.erbbackend/pkg/messages/messages.gobackend~pkg~messages~read-message.go.erbbackend/pkg/messages/read-message.gobackend~pkg~messages~filters.go.erbbackend/pkg/messages/filters.gofrontend~app~player~web~messages~message.gen.ts.erbfrontend/app/player/web/messages/message.gen.tstracker~tracker~src~common~messages.gen.ts.erbtracker/tracker/src/common/messages.gen.tsee~connectors~msgcodec~messages.py.erbee/connectors/msgcodec/messages.pyios/ASMessage.swiftmobs/templates/ios/ASMessage.swift模板本身Swift 端在 mobs 子目录内输出因此仓库中所有带Auto-generated, do not edit//* eslint-disable */头注释的消息代码文件都是本生成器的产物直接修改它们是无效的正确姿势是修改 Schema 或模板后重新运行sh generate.sh。六、生成流程实战一行命令生成全仓消息代码在仓库根目录执行sh mobs/generate.sh该脚本内容仅两行ruby run.rb gofmt -w ../backend/pkg/messages第一行执行完整的多语言生成第二行对 Go 输出统一执行gofmt格式化保证 backend/pkg/messages 下的生成代码始终符合 Go 官方格式规范这也是唯一需要外部工具链依赖的环节。运行前提需要 Ruby 环境生成器使用标准库erb无需额外 gem需要gofmtGo 工具链自带必须在mobs/目录内执行ruby run.rbrun.rb使用相对路径require ./messages.rb与Dir[templates/*.erb]若需仅格式化而不重新生成可只执行脚本第二行。从run.rb的输出逻辑puts tpl -- t看每次生成会在终端打印每个模板到产物的映射关系方便核对生成范围。七、二进制编码原语三种语言同一套字节语义所有消息的编解码最终都落到三套原语实现上它们必须逐字节兼容否则回放数据就会错乱。三套实现分布在Gomobs/primitives/primitives.go被生成代码引用Pythonmobs/primitives/primitives.py被 ee/connectors/msgcodec 引用Swiftmobs/primitives/primitives.swiftiOS 端7.1 uintLEB128 变长编码Go 侧WriteUint/ReadUint实现了标准的 LEB128每字节低 7 位为有效数据最高位为是否继续标志小端序累积位移。primitives.py中同步实现了相同逻辑其注释还点明了大端/小端在此处无关紧要因为按字节处理并给出了 uint64 最大占用 9 字节的边界检查if i 9 || i 9 b 1判定溢出。func WriteUint(v uint64, buf []byte, p int) int { for v 0x80 { buf[p] byte(v) | 0x80 v 7 p } buf[p] byte(v) return p 1 }7.2 intZigZag 编码有符号整数采用 ZigZag 映射uv uint64(v) 1负数取反把符号位挪到最低位从而让绝对值小的负数也占用极少字节。Python 实现里对应的还原逻辑是x -x - 1等价于 Go 的x ^x两处代码逐行对齐primitives.py甚至直接以 Go 源码注释的形式嵌入了参考实现。7.3 boolean单字节 0/1WriteBoolean将true写为0x01、false写为0x00读取端p[0] 1即为真。Python 侧b 1同样严格比对而不是用真值判断——这要求所有语言写入端严格遵循 0/1 约定。7.4 string / data长度前缀 原始字节字符串先以 LEB128 写入字节长度再紧跟 UTF-8 字节序列。Go 侧对超长字符串有保护ReadString中if l 10e6直接报错Too long string防止恶意长度字段导致内存耗尽。Python 侧在解码时使用errorsreplace容错并将空字节\x00替换为替换符\uFFFD。data[]byte字段同样采用长度前缀但不做字符串语义处理。7.5 size三字节小端ReadSize固定读取 3 个字节以小端序组合成uint64用于读取消息块的整体尺寸与消息级编解码配合构成外层帧格式。八、生成产物解析Go / TypeScript / Python 三视角8.1 Gobackend/pkg/messages模板 backend~pkg~messages~messages.go.erb 为每条消息生成四件套类型常量const ( MsgSetNodeAttribute 12 ... )结构体内嵌message基类 类型映射后的字段uint → uint64、string → string等Encode() []byte按前文缓冲区公式分配空间依次调用WriteXxx写入各字段首字节为消息 IDTypeID() int返回消息 ID配合接口Message使用。解码侧由 backend~pkg~messages~read-message.go.erb 生成每条消息一个DecodeXxx(reader)函数按声明顺序调用reader.ReadXxx()并通过ReadMessage(t uint64, reader)以switch t分发到对应解码函数未识别的 ID 返回unknown message code错误。该 switch 是完全由 Schema 驱动生成的——新增消息后无需手写任何分发逻辑。过滤器模板 backend~pkg~messages~filters.go.erb 生成三个布尔判断函数IsReplayerType(id)排除所有replayer false的消息链式取非判断IsMobileType(id)命中所有context :ios的消息||链IsDOMType(id)命中所有replayer true的消息。这些函数被后端用于按类型路由消息回放引擎只关心IsReplayerType为真的消息移动端专属消息通过IsMobileType快速识别。8.2 TypeScript播放器与 tracker模板按职责拆分成了多份 TS 产物frontend~app~player~web~messages~raw.gen.ts.erb底层原始消息元组类型frontend~app~player~web~messages~message.gen.ts.erb在RawMessage上叠加Timed导出Message RawMessage Timed并按replayer ! false过滤出播放器关心的消息类型frontend~app~player~web~messages~tracker.gen.ts.erb 与tracker-legacy.gen.ts.erb新旧 tracker 消息序列化tracker~tracker~src~common~messages.gen.ts.erbtracker 公共消息常量与类型定义tracker~tracker~src~main~app~messages.gen.ts.erb为每个tracker消息生成带类型的构造工厂函数将字段以camelCase参数传入并返回元组数组[Type.Xxx, ...fields]tracker~tracker~src~webworker~MessageEncoder.gen.ts.erbWeb Worker 中的编码器。其中 TS 类型映射的关键在type_jsint/uint均映射为numberstring保持string与 Go 侧形成宽类型对应——JS 数字实际按 IEEE 754 double 承载 64 位整数这是跨语言协议常见的取舍。8.3 Python消息编解码msgcodec模板 ee~connectors~msgcodec~messages.py.erb 为每条消息生成一个继承自抽象基类Message的 Python 类类属性__id__记录消息 ID__init__按 snake_case 接收全部字段配套的.pyx模板messages.pyx.erb、msgcodec.pyx.erb生成 Cython 加速的编解码实现用long/unsigned long/bint/str与 mobs/primitives/primitives.py 的Codec静态方法对接。这保证了消费侧如 ee/connectors/consumer.py 所在的数据管道能以接近原生速度解析与 Go 后端完全一致的二进制流。8.4 iOSSwift模板 mobs/templates/ios/ASMessage.swift 与 mobs/primitives/primitives.swift 覆盖 iOS 端移动端消息由mobile_messages.rb驱动生成与 tracker-reactnative / iOS SDK 侧对齐。九、实战推演如何新增一条消息综合上述机制在 OpenReplay 中新增一条消息的标准流程以仓库只读视角介绍不涉及实际修改仓库如下分配空闲 ID在 mobs/messages.rb或 mobs/mobile_messages.rb末尾的可用 ID 中选取未被占用的编号声明消息在文件末尾追加message id, Name, opts do ... end块按需设置:tracker、:replayer、:pipeline选项重新生成进入mobs/目录执行sh generate.sh即ruby run.rb gofmt -w ../backend/pkg/messages等待终端打印各模板 → 产物映射验证产物检查 backend/pkg/messages/messages.go 出现对应结构体与常量、read-message.go的switch出现新分支、TS/Python/Swift 产物同步更新注意兼容性对已有消息的字段修改遵循只增不改原则删除或改类型会破坏历史会话解码废弃消息应复制为新 ID 并在旧定义处保留注释如# DEPRECATED since 14.0.0 - goto 122的写法同时回写文件末尾的空闲 ID 清单。从 backend/pkg/messages 与 frontend/app/player/web/messages 中大量带 Auto-generated 头注释的文件可以看出这套手写 Schema 模板生成的机制贯穿全仓是 OpenReplay 会话协议演进的基础设施。十、总结MOBS 以一处定义、处处生成的设计把 OpenReplay 横跨 Go / TypeScript / Python / Swift 的会话消息协议收敛到两个 Ruby 定义文件和一组 ERB 模板中。其核心价值在于单一事实来源消息 ID、字段顺序、类型语义只维护在 mobs/messages.rb 与 mobs/mobile_messages.rb模板即产物目录mobs/templates 中的~命名规则把模板文件路径直接映射为仓库内生成代码路径目录结构即配置原语层严格对齐primitives.go、primitives.py、primitives.swift 三套实现逐字节兼容 LEB128 / ZigZag / 长度前缀协议演进有痕废弃消息通过注释与新增 ID 保留历史兼容性保证存量会话数据永远可解码。对于需要在 OpenReplay 仓库中排查消息格式问题、扩展新事件类型或理解端到端数据流的开发者MOBS 目录mobs/README.md就是协议的总纲从这一入口出发可以顺藤摸瓜地掌握整条会话数据链路的字节级细节。【免费下载链接】openreplaySession replay, cobrowsing and product analytics you can self-host. Best for reproducing issues and iterating on your product.项目地址: https://gitcode.com/gh_mirrors/op/openreplay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价