
你在本地跑 Gradio 应用的时候界面流畅、交互顺手一切都很完美。一旦部署到公网你会发现自己瞬间回到“盲人摸象”的状态到底有多少人访问了你的应用、用户是从哪个渠道来的、他们在页面上停留了多久、最常点击的是哪个按钮、有没有反复触发某个生成逻辑这些小到运营大到产品迭代的关键信息全部是黑盒。我之前遇到过类似问题给一个基于 Gradio 搭建的在线工具做完部署后团队对“有没有人用、用得怎么样”完全没数只能靠服务器日志里那些乱七八糟的 GET 请求推测个大概。后来想给页面加上百度统计却发现事情没那么简单——Gradio 不是一个常规的多页面网站它是典型的单页应用直接把统计代码塞进 HTML 里往往只能统计到落地页后面的动态交互一概抓不到。这篇文章就把我踩过坑之后梳理出的完整方案分享出来内容包括统计代码如何注入、事件如何上报、碰到统计不生效怎么排查整体可以直接照抄。1. 为什么 Gradio 不能直接粘贴百度统计代码1.1 Gradio 前端是典型动态渲染的单页应用很多人第一次接触 Gradio 时会把它当成一个普通的 Flask 应用来看反正都是 Python 写的返回一个页面不就行了实际上 Gradio 的架构是完全不同的。它在浏览器端渲染的是一个基于 Svelte 的纯前端应用页面初始加载完成后后续的所有交互都通过 WebSocket 与后端通信而不是像传统网站那样每次点击都刷新页面或跳转 URL。这就带来一个很现实的问题百度统计的默认 JS 统计代码是按照“浏览器打开一个有独立 URL 的页面”这种模型设计的。它会在页面加载时自动上报一次 PV但如果页面内部发生了路由变化、弹窗切换或者局部内容更新它自己是感知不到的。Gradio 应用在用户点击按钮、拖动滑块、切换 Tab 时URL 大概率不会有任何变化默认统计代码只能记录第一次加载的 PV后续用户行为基本为零。另一个更隐蔽的坑是Gradio 自带一套前端资源加载机制页面中的script标签如果直接写在 HTML 模板里有可能会被它的缓存策略或组件渲染顺序影响导致统计脚本在部分版本中压根不会执行。我在 Gradio 3.x 的老版本项目里就遇到过这种情况代码粘进去了、页面源代码里也能看到脚本标签但百度统计后台就是没有数据排查半天才发现是渲染时序问题。1.2 “注入脚本 显式上报”才是正确姿势理解了上面两个原因解决方案就清晰了第一统计代码要在页面合适的时间点以合适的方式注入到 DOM 中第二不能依赖浏览器默认的 PV 上报机制而是通过百度统计的_hmt.push接口在关键交互节点手动上报 PV 和事件。这套思路并不复杂就是把你从“被动等统计”切换成“主动上报”。通俗一点说默认统计代码像是一个只会在进门时登记的保安而 Gradio 应用里有大量用户不走正门你需要自己安排保安在各个关键路口盯梢并且随时记录他们做了什么。2. 方案选型Gradio 页面注入统计代码的三种方式2.1 三种注入方式对比把统计代码放进 Gradio 应用大体有三条路可以走我这里直接放对比表注入方式实现难度稳定性适用场景直接用Blocks(head...)注入低高推荐新版 Gradio只统计应用主页面运行时动态创建script标签中高需要精细控制加载时机兼容老版本反向代理层 sub_filter 注入中高极高应用容器不方便改代码或者要统计登录页第一种方式最直接。新版 Gradio 的gr.Blocks支持head参数你可以传入一段 HTML 字符串它会被插入到页面head区域。百度统计的标准代码本质上就是一段script放进head参数里就能在页面加载时执行和普通网站无异。第二种方式是运行时动态注入适合你需要在特定时机比如用户登录之后、某个模块加载之前再初始化统计的情况。原理是先用 JavaScript 创建script节点设置src为百度统计的hm.js地址然后把它appendChild到页面中同时在全局初始化_hmt数组。这种方法兼容性最好也是我最终采用的方式后面会给出完整代码。第三种方式是在 Nginx 或 Caddy 这一层做内容替换。比如 Nginx 的sub_filter指令可以在返回 HTML 内容时将你指定的统计代码自动插入响应体。这种方式和应用代码完全解耦Gradio 本身不用做任何改动改的是部署层配置。优点是即使 Gradio 版本升级也不会影响统计逻辑缺点是需要对反向代理配置比较熟悉而且只能做静态注入事件上报逻辑还是得靠应用内的 JavaScript 完成。2.2 为什么最终推荐“运行时 JS 注入 事件上报”我个人更推荐第二种方式原因有两个一是兼容性足够好不管你的 Gradio 是 3.x 还是 5.x只要页面能执行 JavaScript这套方案就能跑起来二是它和服务端代码天然分离你可以把所有统计相关逻辑封装成一段独立的 JS 字符串甚至是单独的.js文件维护起来非常清晰。更关键的是Gradio 应用最值得统计的不是 PV而是事件。用户点了几次“生成结果”按钮、调整了几次参数滑块、最终有没有复制输出内容——这些行为直接反映了产品的核心价值有没有被用起来。而事件上报恰恰需要你主动写 JavaScript 监听逻辑运行时注入方案给了你最灵活的控制空间。3. 核心细节统计代码注入和事件上报的实现原理3.1 百度统计脚本的引导加载机制百度统计的代码看起来是一段比较长的script但核心就两步第一在全局作用域创建或复用一个名为_hmt的数组第二动态加载https://hm.baidu.com/hm.js?站点ID这个脚本。_hmt数组是百度统计的前端消息队列你在调用_hmt.push([...])时如果hm.js还没加载完它会先把命令暂存在数组里等脚本加载完成后浏览器会按顺序处理这些命令。理解这个机制非常重要因为它意味着你不需要担心“脚本还没加载完就上报事件”这种竞态问题。只要确保_hmt这个数组存在任何时间点调用push都是安全的。这也是为什么动态注入方案可行的底层原因。3.2 事件上报的_hmt.push标准写法百度统计除了自动记录 PV还支持自定义事件。事件用来记录用户在页面内的某个行为比如点击按钮、提交表单、播放视频等等。上报格式如下_hmt.push([_trackEvent, category, action, label, value]);四个参数的含义分别是事件类别、事件动作、事件标签、事件值。比如我想统计“用户点击了生成按钮”这个行为可以这样写_hmt.push([_trackEvent, button, click, generate_btn, 1]);其中button是事件类别表示这是按钮类行为click是动作generate_btn是这个按钮的唯一标识最后的1是数值可以理解为“发生了一次”。在百度统计后台的“事件分析”模块里你可以按这些维度交叉查看数据从而知道哪个按钮最受欢迎、哪个环节流失最严重。3.3 用户身份识别和 localStorage 的配合Gradio 应用和普通网站不同它没有一个天然的“访问会话”概念用户可以在不同时间反复打开页面。如果只想统计“访问次数”默认逻辑就够了但如果你想识别“同一个用户到底来几次”就需要自己打标记。我的做法是在统计脚本执行时先读取 localStorage 里的一个自定义字段比如gradio_user_id。如果没有就生成一个随机字符串写进去然后作为自定义维度随事件上报。这样即使百度统计的 Cookie 被清理了只要用户不手动清 localStorage你依然能在事件标签或自定义变量里看到同一个用户的多条行为记录。新版的百度统计还支持自定义变量可以通过_hmt.push([_setCustomVar, index, name, value])设置。可以把用户 ID 放在第一个自定义变量位这样整个会话期间的 PV 和事件都会带上用户标识后续做留存分析就方便多了。3.4 和 Gradio 身份验证搭配的注意事项Gradio 的Demo.queue().launch(auth...)只保护了应用页面本身认证界面是 Gradio 内部渲染的独立页面。如果你把统计代码通过head参数注入它只在认证成功后的主页面生效登录页是统计不到的。这在实际场景中问题不大因为产品关心的核心数据都在主页面里。但如果你的运营需求是“必须知道有多少人访问了登录页甚至尝试登录”那就得走部署层注入的方案在反向代理层把统计代码插到所有页面里。这个场景下推荐用 Nginx 的sub_filter它会把静态脚本插入到包括登录页在内的所有 HTML 响应中。代价是配置相对复杂而且要考虑 Gradio 页面内是否允许出现两个统计脚本执行需要做好去重判断。4. 实操过程完整实现一个带百度统计的 Gradio 应用4.1 环境准备和基础依赖开始之前需要准备好以下内容Python 3.9 及以上版本已安装gradio库建议 4.x 以上版本一个百度统计账号并在“网站列表”中新增站点拿到统计代码中hm.js?后面那串站点 ID确认一下自己的 Gradio 版本直接在终端执行pip show gradio或者写一行print(gr.__version__)即可。版本差异会影响的只是head参数是否可用我的示例代码会给出动态注入的兼容写法所以对版本没有硬性要求。4.2 实现一个可复用的统计注入模块我先把统计逻辑封装成一个独立的 Python 模块方便你在不同应用里复用。这里有一个关键设计我不把统计代码硬编码在业务代码里而是单独维护一份 JS 模板字符串通过render函数输出。# analytics.py import html import secrets STATS_JS_TEMPLATE script (function() { // 站点 ID从百度统计后台获取 var siteId __SITE_ID__; // 初始化统计队列 window._hmt window._hmt || []; // 动态加载 hm.js (function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js? siteId; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); })(); // 生成稳定的用户标识存到 localStorage var uidKey gradio_uid; var uid localStorage.getItem(uidKey); if (!uid) { uid u_ Math.random().toString(36).slice(2) Date.now().toString(36); localStorage.setItem(uidKey, uid); } _hmt.push([_setCustomVar, 1, user_id, uid, 1]); // 手动上报 PV意义在于刷新或 SPA 场景下主动跟踪 if (window.__gradioStatsInitialized) { return; } window.__gradioStatsInitialized true; _hmt.push([_trackPageview, /]); // 监听文档点击事件通过 elem_id 识别 Gradio 组件 document.addEventListener(click, function(e) { // 如果点击元素在某个指定 id 的组件内部就上报事件 var target e.target; var generateBtn target.closest(#generate_btn); if (generateBtn) { _hmt.push([_trackEvent, button, click, generate_btn, 1]); return; } var copyBtn target.closest(#copy_btn); if (copyBtn) { _hmt.push([_trackEvent, button, click, copy_btn, 1]); return; } }, true); // 监听滑块释放后的 change 事件 document.addEventListener(change, function(e) { var slider e.target.closest(#temperature_slider); if (slider) { _hmt.push([_trackEvent, slider, change, temperature_slider, parseFloat(slider.value) || 0]); } }, true); })(); /script def render_stats_js(site_id: str) - str: 根据站点 ID 渲染统计脚本。 safe_site_id html.escape(site_id) return STATS_JS_TEMPLATE.replace(__SITE_ID__, safe_site_id)这个模块里有几个值得注意的细节。首先是window._hmt window._hmt || []这一行它保证脚本无论执行多少遍都不会覆盖掉已有的统计队列。其次是window.__gradioStatsInitialized这个标志位防止 Gradio 内部某些机制导致脚本被重复执行从而产生多条重复 PV。再其次是事件监听使用了capture: true这样做是为了确保 Gradio 内部框架可能调用的stopPropagation不会阻止我们的事件捕捉。关于closest方法它可以从当前点击元素向上找父级节点。Gradio 渲染出来的按钮内部往往还包着一层span或div用户实际点到的不一定是按钮本身用closest是最稳妥的。4.3 创建带统计功能的 Gradio 应用有了上面的模块接下来就是把它组装到一个真实应用中。我以一个简单的“文本生成小工具”为例包含一个输入框、一个滑块、一个生成按钮、一个复制按钮。# app.py import gradio as gr from analytics import render_stats_js # 在百度统计后台创建站点后复制这里替换成你自己的 ID SITE_ID 你的百度统计站点ID def generate_text(prompt, temperature): # 这里替换成实际模型调用逻辑 result f这是根据“{prompt}”生成的内容温度参数为 {temperature:.2f} return result stats_js render_stats_js(SITE_ID) # 方式一新版 Gradio 直接用 head 参数注入 with gr.Blocks( headstats_js, titleAI 文本生成小工具, ) as demo: gr.Markdown(# AI 文本生成小工具) with gr.Row(): prompt_input gr.Textbox(label输入提示词, lines3) temperature_slider gr.Slider( 0.0, 1.0, value0.7, step0.1, label温度参数, elem_idtemperature_slider ) generate_btn gr.Button(生成文本, elem_idgenerate_btn) output_text gr.Textbox(label生成结果, lines5) generate_btn.click(generate_text, inputs[prompt_input, temperature_slider], outputsoutput_text) # 如果是老版本 Gradio支持 document 加载后再动态注入 # demo gr.Blocks(titleAI 文本生成小工具) # demo.load(None, jsstats_js) # 或使用 head 不可用的替代方案 if __name__ __main__: demo.queue().launch(server_name0.0.0.0, server_port7860, auth(admin, password))代码里的elem_id参数值得展开说一下。我给生成按钮指定的elem_idgenerate_btn这个 ID 会直接渲染到前端 HTML 的对应组件上。事件监听脚本就可以通过这些 ID 精确捕获目标组件而不是去猜测 Gradio 内部生成的一长串随机类名。这是整个方案中“指定行为记录”的核心操作。如果你的用户群体需要更细的维度比如要区分不同页面Tab里的点击行为可以给每个 Tab 内容里的按钮设置不同 ID然后在事件监听脚本里按 ID 分别上报。百度统计后台的事件分析可以按类别和标签做下钻这样你就能知道哪个 Tab 的功能用得更多。4.4 部署绑定域名和统计验证流程应用写好后部署方式可以是任意能访问到公网的方式比如云服务器、容器平台等。百度统计建议绑定真实域名因为它的访客来源分析会依赖 HTTP 请求头中的 Referer 和域名信息。如果你只在 IP 加端口的方式下测试统计后台虽然能看到 PV但很多来源相关的报表字段会缺失。部署完成后有一个必须做的验证动作打开浏览器开发者工具切到 Network 面板刷新一次页面。正常情况下你应该能看到一个名为hm.js?...的请求出现在网络列表里。如果看到了说明统计脚本加载成功看不到就检查站点 ID 是否正确、脚本是否被防火墙或 CSP 拦截。接着在页面上点几次“生成文本”按钮再切到百度统计后台的“实时访客”页面。如果一切正常几分钟内就能看到事件上报记录事件分析模块里也能看到按钮点击的次数。我第一次做这个验证的时候以为脚本加载成功就等于万事大吉结果发现点击事件上报没生效后来排查了半天才发现是冒泡阶段被 Gradio 自己的事件处理器打断了改成捕获阶段监听才解决。这个坑我放在下一节详细说。5. 常见问题与排查技巧实录5.1 脚本加载成功但 PV 不增加这是最常见的现象。如果你是直接把统计代码放在head参数里并且页面加载后能在 Network 里看到hm.js请求但后台没数据优先检查两点第一站点 ID 是否和当前域名匹配百度统计有时会因为域名白名单设置导致上报被丢弃第二是否在浏览器隐私模式下测试某些浏览器隐私模式会限制第三方脚本的 Cookie 写入这种情况下统计数据是偏少的。另外如果你用了js参数方式注入而不是head需要确认脚本执行的时序。Gradio 的js参数是在页面渲染完成之后才执行的如果脚本里有_trackPageview调用正常不会丢失。但要注意_trackPageview默认只接受一个字符串参数表示路径如果你不传参或者传空字符串后台可能会显示为“未知来源”。5.2 事件上报没有进入后台我之前踩过的坑可以整理成几个排查步骤在浏览器 Console 手动执行_hmt.push([_trackEvent, test, click, test, 1])然后去后台看事件分析。如果手动上报能显示但点击按钮不上报基本可以确定是事件监听代码没生效。检查脚本中的closest选择器是否和你设置的elem_id一致。注意 Gradio 在异步渲染某些组件时点击事件的绑定时机要放在事件冒泡或捕获阶段建议用capture: true来保证不被其他框架逻辑拦截。确认没有在页面其他地方误删了window._hmt数组。有些前端库会重置全局变量导致统计队列失效。这里有一个我从实战里总结出来的技巧不要只在页面加载时绑定一次事件监听而是做成一个initStats()函数在每次 DOM 结构可能发生变化后手动调用一次或者用MutationObserver监听页面变化。Gradio 的组件有动态渲染的情况第一次绑定拿到的元素可能在后续交互中被替换掉事件自然就丢了。5.3 本地 localhost 测试数据污染开发时开着localhost:7860测试百度统计后台会混入一堆本机访问记录。虽然不影响功能但会让你分析真实流量时很头疼。我惯用的做法是在统计脚本里加一个环境判断只允许正式域名初始化统计逻辑var allowedHost your-domain.com; if (location.hostname ! allowedHost !location.hostname.endsWith(. allowedHost)) { return; }这样一来开发环境里的访问不会污染统计数据只有正式域名下的用户行为会被上报。如果你还有内网测试环境可以把这个域名列表扩展成一个数组灵活配置。5.4 和 Gradio 身份验证的兼容性问题很多生产环境会用auth参数给 Gradio 加一层简单的密码保护。这时候你可能会发现统计代码注入到页面后登录页本身没有统计代码而且有些浏览器在带有认证的前端页面里对第三方 Cookie 的限制更严格导致统计脚本加载后无法正常发送数据。解决方案是优先使用部署层注入在我上面的方案里统计代码是跟随应用页面加载的登录页统计不到。如果你确实需要统计“有多少人访问了登录页、有多少人成功登录”可以把百度统计代码通过 Nginx 的sub_filter注入到所有返回的 HTML 响应中。大致配置思路如下server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:7860; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; sub_filter /head script/* 你的统计代码 *//script/head; sub_filter_once on; } }注意sub_filter默认只能替换一次如果 Gradio 的 HTML 结构里head标签出现多次可能需要调整匹配规则。另外如果页面已经通过 gzip 压缩sub_filter可能无法正常工作需要确保后端不开启压缩或者代理层先解压再替换。这一点在不同版本 Gradio 上表现不同实际操作时最好在浏览器里查看最终返回的 HTML 源码确认一下。5.5 团队多人共用站点 ID 导致数据混乱如果你的统计站点同时被多个 Gradio 应用使用事件数据会全部混在一起很难区分不同应用的表现。最省事的办法是每个应用创建一个独立的百度统计站点但站点多了管理麻烦。我目前用的是“事件标签 自定义变量”的方式区分每个应用在初始化时通过_setCustomVar写入一个固定的app_name维度后续分析时按维度过滤即可不需要拆出多个站点 ID。写在最后的经验这套方案上线后我最大的感受是统计工具本身不产生价值基于数据做产品判断才产生价值。给 Gradio 应用接入百度统计只是第一步真正花精力的地方是定义清楚“哪些事件对你来说意味着用户真的用上了产品”。我的建议是事件命名从第一天就规范化英文小写加下划线的格式最稳妥比如generate_btn_click、slider_adjust、copy_result避免出现中文、空格和大小写混用的情况否则后期做报表的时候光是清洗事件名就够你头疼的了。还有一点小技巧可以在统计脚本里增加一个内部测试标识比如让团队成员的浏览器 localStorage 里带一个is_staff1上报事件时把这类流量自动过滤掉能有效避免运营看数据时被自己人的测试行为干扰。到了产品迭代中期这些细节会直接影响你分析数据的效率。如果后续你在这个基础上还想做更精细的用户行为分析可以接着扩展现有的上报逻辑把 Python 端返回的耗时、生成结果长度这些维度也塞进事件参数里那就是另一个话题了。