- 可观测性
- 后端
【免费下载链接】highlight
highlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.
本文围绕 Highlight 的浏览器端会话录制隐私控制展开:如何用最少的 CSS 类名实现元素屏蔽(highlight-block)、文本混淆(highlight-mask)与输入忽略(highlight-ignore),以及如何通过H.init()的privacySetting选项切换 strict / default / none 三级隐私策略。读完本文,你可以对照 Highlight SDK 与 rrweb 的源码实现,理解每一处脱敏发生在客户端序列化的哪个环节,并据此为自己的产品配置一套可复现、可验证的隐私保护方案。
隐私控制的两种入口:CSS 类名与 privacySetting
Highlight 录制会话时,所有脱敏都发生在客户端序列化阶段——敏感数据在离开浏览器之前就已经被移除或替换,服务端(Highlight 后端)从未接收过原始数据。官方文档(privacy.md)给出的控制手段可以归纳为两条路径:
- 元素级标注:在 HTML 上添加
highlight-block、highlight-mask、highlight-ignore三个 CSS 类,分别控制"屏蔽内容"、"混淆文本"和"忽略输入"; - 全局策略:在调用
H.init()时传入privacySetting(取值为'strict' | 'default' | 'none'),设定整页的脱敏强度。
从源码结构看,SDK 在启动录制时把这两类配置合并传入底层录制器(rrweb):sdk/highlight-run/src/client/index.tsx 中,record()调用显式传入ignoreClass: 'highlight-ignore'、blockClass: 'highlight-block'以及privacySetting、maskAllInputs、maskInputOptions;而highlight-mask则是 rrweb 侧的默认值,见 __generated/rr/rrweb/rr.js 中record()的默认参数maskTextClass = "highlight-mask"。
privacySetting的可选值定义在 sdk/highlight-run/src/client/types/types.ts:
export type PrivacySettingOption = 'strict' | 'default' | 'none'其官方文档语义(docs-content/sdk/client.md)为:
'strict':屏蔽页面上所有文本和图片,是无需手动标注即可保证不录制任何个人信息的做法;'default':只屏蔽匹配常见 PII 正则表达式以及常见输入名的文本和输入数据,不屏蔽图片和媒体内容;'none':按页面显示原样记录所有文本和内容。
privacySetting的默认值在 SDK 中是'default':sdk/highlight-run/src/client/index.tsx 中this.privacySetting = options.privacySetting ?? 'default'。
Masking Elements:用 highlight-block 屏蔽元素内容
最直接的内容清洗方式是给需要忽略的元素加上highlight-blockCSS 类:
<div class="highlight-block">Super secret sauce</div>Highlight 片段会测量被忽略元素的尺寸,回放时用占位符替换其内容——即用户能看到该区域"有东西"(布局、尺寸不变),但内容不可见。
对应实现分两层:
- 判定层:rrweb 序列化时用
_isBlockedElement检查节点是否携带 blockClass,匹配规则是element.classList.contains(blockClass),见 __generated/rr/rrweb/rr.js; - 默认绑定:
record()与snapshot()的默认参数均为blockClass = "highlight-block"(rr.js#L14220、rr.js#L1513),因此不需要任何额外配置,只要 HTML 里加了这个类名即可生效。
注意:highlight-block作用于元素及其整个子树,适合整块区域(如账单明细面板、内部工单卡片)级别的屏蔽。
Obfuscating Elements:用 highlight-mask 混淆文本
如果不想完全隐藏区域、只希望把文本"打码"成乱码,可以给元素加highlight-mask类:
<div class="highlight-mask">This is some sensitive data <button>Important Button</button></div>其效果等同于privacySetting: 'strict'的文本混淆(即前文图片中看到的随机字符串),但只作用于被标记的具体元素。源码中:
snapshot()的默认参数maskTextClass = "highlight-mask"(rr.js#L1515);- 判定函数
needMaskingText对元素节点检查classList.contains(maskTextClass),对文本节点则检查其父元素(rr.js#L775-L805); - 命中后文本会被
maskTextFn替换,record()的默认maskTextFn = obfuscateText(rr.js#L14231),最终在序列化节点时执行textContent2 = maskTextFn ? maskTextFn(...) : textContent2.replace(/[\S]/g, "*")(rr.js#L971-L973)。
obfuscateText的实现(rr.js#L576-L580)值得注意:它先剔除非 ASCII 字符,再按空格拆词,对每个词用随机数生成等长的随机字符串,因此回放时文本长度和排版基本保持原样,观感更接近"真实但不可读"的内容,而不是简单的星号:
function obfuscateText(text) { text = text.replace(/[^ -~]+/g, ""); text = (text?.split(" ").map((word) => Math.random().toString(20).substring(2, word.length)).join(" ")) || ""; return text; }Ignoring Input:用 highlight-ignore 忽略输入内容
注意:
highlight-ignore只适用于<input>元素。如果想屏蔽其他 HTML 元素的采集,请使用highlight-block。
对敏感输入框(如身份证号、卡号),团队往往希望保留输入框本身的外观与交互轨迹(光标移动、焦点变化),但不记录用户敲入的值。给<input>加上highlight-ignore即可:
<input class="highlight-ignore" name="social security number" />源码中该机制对应 rrweb 的输入监听逻辑:record()默认ignoreClass = "highlight-ignore"(rr.js#L14222),输入事件处理器在捕获到目标元素后先做短路判断——target.classList.contains(ignoreClass)命中则直接return,连value都不会读取(rr.js#L12164-L12166)。SDK 侧同样显式传入该配置:record({ ignoreClass: 'highlight-ignore', blockClass: 'highlight-block', ... })(index.tsx#L764-L766)。
这与highlight-mask处理输入的区别在于:highlight-ignore是"输入事件不采集",而 mask/privacy 策略是对已序列化值的替换。
Network Request Redaction:网络层脱敏
DOM 之外的另一个数据出口是网络请求。Highlight 开箱即会屏蔽若干已知携带密钥的请求头,并提供多级自定义能力。完整配置见 Recording Network Requests and Responses,核心要点如下:
- 开启请求头/响应体录制:
networkRecording.recordHeadersAndBody: true; - 默认脱敏的请求头:
Authorization、Cookie、Proxy-Authorization; - 追加脱敏请求头:
networkRecording.networkHeadersToRedact; - URL 黑名单:
urlBlocklist(命中后不记录 header 与 body),Highlight 默认不记录https://www.googleapis.com/identitytoolkit与https://securetoken.googleapis.com; - 白名单与键级脱敏:
networkRecording.headerKeysToRecord/bodyKeysToRecord(白名单)、networkRecording.networkBodyKeysToRedact(键级脱敏),需highlight.run高于4.1.0; - 自定义 sanitizer:
networkRecording.requestResponseSanitizer接收 Request/Response pair,返回同类型对象即完成改写,返回null则整条请求被丢弃(官方不建议滥用丢弃,以免调试时缺少请求上下文),需highlight.run高于8.1.0。
示例(来自上述文档):
H.init('<YOUR_PROJECT_ID>', { networkRecording: { enabled: true, recordHeadersAndBody: true, requestResponseSanitizer: (pair) => { if (pair.request.url.toLowerCase().indexOf('ignore') !== -1) { // 丢弃整条请求/响应(不会产生网络日志) return null } // 其余请求正常返回 pair return pair }, }, })Default Privacy Mode:默认隐私模式与 PII 正则
默认情况下(即privacySetting: 'default'),Highlight 会混淆所有输入以及匹配常见个人信息(PII)正则的文本。这为地址、电话号码、社保号等数据提供了基线保护;图片和媒体内容不受影响。代价是可能"误伤":与长数字、联系方式模式吻合的非 PII 文本也可能被混淆。如需关闭,调用H.init()时将privacySetting设为'none'。
注意:
default模式仅在 SDK 8.0.0 及以后版本中可用(早期版本仅 strict / none 二选一)。
使用的正则表达式清单
官方文档列出了默认隐私模式使用的正则(源自 rrweb-snapshot 的utils.ts):
Email: '[a-zA-Z0-9.!#$%&'*+=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*' SSN: '[0-9]{3}-?[0-9]{2}-?[0-9]{4}' Phone number: '[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}' Credit card: '[0-9]{4}-?[0-9]{4}-?[0-9]{4}-?[0-9]{4}' Unformatted SSN, phone number, credit card: '[0-9]{9,16}' Address: '[0-9]{1,5}.?[0-9]{0,3}\s[a-zA-Z]{2,30}\s[a-zA-Z]{2,15}' IP address: '(?:[0-9]{1,3}.){3}[0-9]{1,3}'当前仓库内置的 rrweb 构建中同样可以找到这份正则清单,定义在 __generated/rr/rrweb/rr.js#L584-L605:EMAIL_REGEX、LONG_NUMBER_REGEX(对应[0-9]{9,16})、SSN_REGEX、PHONE_NUMBER_REGEX、CREDIT_CARD_REGEX、ADDRESS_REGEX、IP_REGEX,由DEFAULT_OBFUSCATE_REGEXES数组聚合,命中判定函数为:
function shouldObfuscateTextByDefault(text) { if (!text) return false; return DEFAULT_OBFUSCATE_REGEXES.some((regex) => regex.test(text)); }(见 rr.js#L606-L609。)
输入框的脱敏规则从何而来
静态文本靠正则,动态输入靠"整类屏蔽"。SDK 通过 sdk/highlight-run/src/client/utils/privacy.ts 中的determineMaskInputOptions把隐私策略翻译成 rrweb 的maskAllInputs/maskInputOptions:
export const determineMaskInputOptions = ( privacyPolicy: PrivacySettingOption, ): [maskAllOptions: boolean, maskOptions?: MaskInputOptions] => { switch (privacyPolicy) { case 'strict': return [true, undefined] // 屏蔽所有输入 case 'default': return [true, undefined] // 同样屏蔽所有输入 case 'none': { return [false, { password: true }] // 仅屏蔽 password 类型 } }rrweb 侧的兜底逻辑(rr.js#L14278-L14295):maskAllInputs === true时构造包含text、email、tel、number、textarea、select、password等全部类型的屏蔽表;否则取调用方传入的maskInputOptions;再否则默认{ password: true }。这也解释了为什么none模式下密码框依然会被打码——密码内容属于绝对红线。
被判定应屏蔽的输入,其value会被替换为与原文等长的*串(maskInputValue中text = "*".repeat(text.length),rr.js#L342-L361),保持回放时的宽度稳定。官方博客 Revamping Privacy Mode 还提到:默认模式会额外搜索带有常见name/id/autocomplete值的输入框并从输入第一刻就对其进行混淆,以解决"用户正在输入社保号、但正则要凑够位数才命中"的窗口期问题。
同时该文也点明了默认模式的两类已知局限:一是会过度混淆(如用户创建的UserId长数字命中手机号正则),因为算法不区分上下文;二是跨元素拆分的文本可能漏检(如<div>spencer@<b>highlight</b>.io</div>被<b>切断后整体不再命中邮箱正则)。
覆盖混淆:data-hl-record="true"
默认模式下,一些无害但被误混淆的文本(例如用户自设的名称、公司地址输入框)可以用data-hl-record="true"属性放行。注意两点约束:该属性必须写在被录制的 HTML 标签本身上,且其子元素仍可能各自被脱敏。
源码印证:文本节点混淆前先读取父元素的属性(rr.js#L974-L990):
const enableStrictPrivacy = privacySetting === "strict"; const highlightOverwriteRecord = n2.parentElement?.getAttribute("data-hl-record"); const obfuscateDefaultPrivacy = privacySetting === "default" && shouldObfuscateTextByDefault(textContent2); if ((enableStrictPrivacy || obfuscateDefaultPrivacy) && !highlightOverwriteRecord && parentTagName) { // 忽略 HEAD/TITLE/STYLE/SCRIPT 等标签后执行 obfuscateText }输入事件路径同样如此:initInputObserver中overwriteRecord = target.getAttribute("data-hl-record"),而maskedInputType在overwriteRecord === "true"时直接返回false(rr.js#L12167-L12188、rr.js#L610-L618),即输入值不再打码。因此该属性对 strict 与 default 两种模式都有效,且作用于元素自身而非其子孙——这与highlight-mask的整树混淆形成互补。
Strict Privacy Mode:最严格的全页混淆
如果不想逐元素标注,可调用H.init()时设置privacySetting: 'strict':
H.init('<YOUR_PROJECT_ID>', { privacySetting: 'strict' })Strict 模式会混淆所有文本和图片。文档给出的效果示例:
<h1>Hello World</h1>会被记录为<h1>1f0eqo jw02d</h1>;<img src="https://my-secrets.com/secret.png" />会被记录为<img src="" />。
两点性质需要强调:混淆不可逆(随机文本由客户端生成,原文从未上传);混淆发生在客户端(序列化阶段即替换,见上节obfuscateText与maskTextFn的调用链)。
从源码结构看,strict 的判定集中在两条链路:初始快照中enableStrictPrivacy = privacySetting === "strict"时对全部文本执行obfuscateText(rr.js#L974-L990),后续 DOM 文本变更则在 MutationObserver 的文本变更分支中做同样的判定与替换(rr.js#L11524-L11528),保证回放时后续出现的动态内容同样被混淆。此外,SDK 在初始化会话时会把策略上报给后端,供回放侧渲染使用:initializeSession请求携带enable_strict_privacy: this.privacySetting === 'strict'与privacy_setting: this.privacySetting(index.tsx#L633-L634)。
三级策略对照与配置建议
| 维度 | strict | default(默认) | none |
|---|---|---|---|
| 页面文本 | 全部混淆(随机化) | 仅命中 PII 正则的文本混淆 | 原样记录 |
| 图片/媒体 | 全部屏蔽(src置空) | 不屏蔽 | 原样记录 |
| 所有输入框 | 全部打码(等长*) | 全部打码(等长*) | 仅password类型打码 |
| 是否需要手动标注 | 不需要 | 可选(data-hl-record可放行误伤) | 不需要 |
| 可用版本 | 所有版本 | SDK 8.0.0+ | 所有版本 |
配置时的决策路径建议:
- 合规要求高、不想逐元素标注→
strict;配合data-hl-record="true"放行确需查看的少量元素; - 常规生产环境→ 保持
default;对误混淆的元素用data-hl-record="true"精确放行,对整块敏感区域叠加highlight-block,对"保留外观但不录值"的输入框用highlight-ignore,对"保留元素但打码文本"的区域用highlight-mask; - 调试/内网环境→
none,此时仍需记住password输入与网络层默认脱敏(Authorization/Cookie等)依然生效。
小结
Highlight 的会话录制隐私控制是一套分层设计:CSS 类名(highlight-block/highlight-mask/highlight-ignore)提供元素级、免配置的精确控制,privacySetting(strict / default / none)提供整页级策略,data-hl-record="true"提供白名单式放行,网络层另有请求头/URL/键级脱敏与自定义 sanitizer。所有脱敏均在客户端序列化阶段完成(rrweb 的 snapshot 与 MutationObserver 两条链路),敏感原文不会离开浏览器。相关实现可追溯至 sdk/highlight-run/src/client/index.tsx、sdk/highlight-run/src/client/utils/privacy.ts 与内置录制器 __generated/rr/rrweb/rr.js;官方说明见 Privacy 文档 与 H.init 参考。
- 可观测性
- 后端
【免费下载链接】highlight
highlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.
相关推荐
用 Highlight 接入 Next.js:@highlight-run/next 的会话回放、错误监控与分布式追踪完整实践
用 Highlight 接入 Next.js:@highlight run/next 的会话回放、错误监控与分布式追踪完整实践 本文基于 Highlight 仓
可观测性后端Highlight iframe 会话录制:同源与跨域 iframe 的捕获原理与配置实践
Highlight iframe 会话录制:同源与跨域 iframe 的捕获原理与配置实践 本文基于 Highlight 官方文档 iframe Recordi
可观测性后端Highlight 会话回放中的 Console 消息录制:disableConsoleRecording 与 consoleMethodsToRecord 配置详解
Highlight 会话回放中的 Console 消息录制:disableConsoleRecording 与 consoleMethodsToRecord 配
可观测性后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考