
线上环境出了问题用户操作到一半页面白屏后端日志干净得像刚擦过的黑板。你点开Sentry事件详情只有一条没头没尾的JavaScript Error堆栈指向一个压缩后的bundle文件连哪个模块炸的都看不出来。这种事碰到一次就够难受了。后来我把Sentry附件上传这套机制彻底玩明白之后再遇到类似问题直接让前端在报错时把用户当时的操作记录、接口请求参数、甚至是关键页面的DOM快照一起挂到事件上排查效率完全不是一个量级。Sentry的附件上传Attachments说白了就是允许你在上报错误事件的同时携带一些额外的文件内容比如日志、配置文件、截图或者序列化后的数据。它不是一个独立的文件存储服务而是跟具体的事件Issue/Event强关联的附属信息。这样做的价值在于错误发生时光有堆栈只能告诉你“哪里炸了”但带上的附件能告诉你“为什么炸”。这篇文章我会围绕Sentry附件文件上传的完整链路来写从整体设计思路讲起覆盖前端SDK集成、后端API直传、微信小程序这类特殊环境怎么处理最后把常见问题、权限配置、体积限制这些容易踩坑的地方全部过一遍。无论你是刚开始接触Sentry还是已经在用了但从来没碰过附件功能都能从里面找到可以直接抄作业的方案。1. 内容整体设计与方案选型1.1 这个功能到底解决了什么问题先说个最直观的场景。我做前端监控的时候经常收到一类错误某个按钮点击后页面卡死控制台报“Cannot read properties of undefined”。这种错误在Sentry里一抓一大把但99%的信息量等于零——堆栈是压缩后的一行变量值全部丢失。以前的做法是让用户复现、开DevTools、看Network面板一来一回一整天就没了。有了附件上传之后思路完全变了。我可以在用户点击按钮之前把当前的用户ID、路由信息、关键组件的props序列化结果、最近20个接口请求的URL和状态码、localStorage里的异常标记位统一打包成一个JSON文件。真正发生错误时在Sentry的beforeSend钩子里把这个文件挂到事件上几秒钟后我就能在Issue详情页的附件区域看到完整上下文。这么说吧之前要反复跟用户要“截图”“操作步骤”“浏览器版本”的事现在大部分都能自己带过来。而且附件不局限于文本。你可以传图片、传日志文件、传配置文件快照。我见过有团队在移动端上报的时候直接附带用户截屏错误一出现连UI长什么样都一目了然。Sentry对这些内容不会做过多的内容解析就是原样存储、原样展示、允许下载。正因为这个“原样”特性它能承载非常多样化的排查信息你完全可以根据业务场景自定义要带什么。1.2 前端直传、后端代传还是API直传三条路怎么选附件上传的实现路径我归纳下来有三条分别对应不同的接入深度和灵活性。选哪条路取决于你的架构和权限范围我逐个说下适用场景。第一条是前端SDK直传。这是最省事的方式在浏览器里通过Sentry.addAttachment()或beforeSend里返回attachments字段来实现SDK会随着错误事件一起打包发送。它适合典型的Web项目尤其是纯前端应用。优点是沾手即用不需要额外写服务端接口缺点是附件内容依赖运行时收集如果进程直接崩了可能来不及凑齐文件。第二条是后端代传。前端把文件先传到你的业务后端后端再通过Sentry的Envelope API把附件和错误事件发过去。这种方式适合你不想把DSN暴露给前端、或者要对附件内容做二次加工比如脱敏、过滤的场景。缺点是链路多了一层需要自己维护转发逻辑。第三条是纯API直传。不依赖任何SDK手动构建Envelope格式的请求体通过HTTP直接打到Sentry的/api/{project_id}/envelope/接口。这种方式最灵活尤其适合小程序、服务端脚本、或者你手里只有原始DSN没有对应语言SDK的情况。它也是我在接入微信小程序时最终采用的方案。三条路并不是互斥的。我现在这个项目里Web端用的是SDK直传服务端定时脚本用的是API直传小程序端因为SDK不完善也是API直传。核心原则就一个在能满足需求的前提下选侵入性最小的路径。2. 前端SDK实现把文件和事件绑在一起2.1 beforeSend 和 addAttachment两种姿势怎么选浏览器端接入附件上传大多数人第一反应是找“上传文件”的API。确实旧版本SDK里提供了Sentry.addAttachment()方法可以预先塞一个附件进去之后任何事件上报都会带着它。但我实际用下来发现这个方法在SDK新版本里行为有变化而且对“只有特定错误才需要附件”这种场景很不友好。你总不能每次页面加载就把附件准备了等真正出错时可能已经过期了。我推荐的方式是在beforeSend钩子里动态附加。beforeSend是Sentry事件上报前的一道闸门你可以拿到完整的事件对象把附件放进event.attachments数组里返回。这样既能精确控制哪些错误带附件、带什么附件又能顺便对事件本身做清洗。import * as Sentry from sentry/browser; Sentry.init({ dsn: https://your-dsnsentry.example.com/123, beforeSend(event, hint) { const { originalException } hint; // 只在捕获到异常时组装附件避免普通事件也带上大文件 if (originalException) { const context { url: window.location.href, user: getUserInfo(), localData: collectKeyState(), lastRequests: performance.getEntriesByType(resource).slice(-20) }; event.attachments [ { filename: error-context.json, data: JSON.stringify(context, null, 2), contentType: application/json } ]; } return event; } });关于Sentry.addAttachment()如果你用的是5.x到6.x的版本它确实能工作但放在beforeSend外意味着全局都带附件流量和存储成本都会升高。新版本里我更建议统一走attachments字段。还有一点要注意beforeSend里对event对象的任何改动都要小心某些字段改错了会导致上报失败或事件被丢弃。我习惯把附件相关的代码单独抽一个函数方便调试和回滚。2.2 文件内容怎么取、内存和体积怎么控前端要在报错瞬间拿到文件内容绕不开两个来源一是运行时产生的字符串数据二是用户已经选择的本地文件。字符串数据好办JSON.stringify一下就行但有个坏习惯我见过很多人踩坑——直接把整个localStorage序列化塞进去。有些业务系统在localStorage里存了token、用户全名甚至敏感配置这么干等于把隐私直接送到了Sentry服务器。我的原则是白名单收集只挑跟排查相关的字段比如当前路由、某个功能模块的开关状态、异常计数器等。千万别图省事一把梭。如果是用户上传的文件比如导入Excel报错时要把那个Excel带上来就要用到FileReader。但这里有一个大坑FileReader.readAsText是异步的而beforeSend是同步执行的。你不能在beforeSend里等异步读取完成那样事件早发出去了。正确做法是在用户选择文件后提前把内容读成字符串或ArrayBuffer缓存起来等出错时直接取缓存。let pendingAttachment null; function handleFileSelect(file) { const reader new FileReader(); reader.onload (e) { pendingAttachment { filename: file.name, data: e.target.result, contentType: file.type || application/octet-stream }; }; reader.readAsDataURL(file); } Sentry.init({ beforeSend(event) { if (pendingAttachment) { event.attachments [pendingAttachment]; } return event; } });体积控制方面Sentry默认对附件大小有限制前端SDK层面我记得大概在1MB左右超过的部分会被丢弃或导致上报失败。我个人的经验值是单个附件尽量控制在200KB以内毕竟附件是用来辅助定位问题的不是用来做日志备份的。碰到那种动辄几十MB的日志文件先在浏览器端做截断比如只保留最后500行效果比硬传好得多。3. 后端与API直传绕过SDK的灵活方案3.1 攒一个Envelope到底要几步如果你跟我一样接触过自托管Sentry或者需要在非标准环境上报附件最终都会走到手动构造Envelope这条路上。Envelope是Sentry的事件封装格式Copy得有点像是HTTP头加Body的组合一个JSON对象作为信封头一行一个JSON对象作为事件头最后跟着原始数据内容。{event_id:9ec79a4f8b0d4b6d9b4d1e2f3a4b5c6d,dsn:https://publicexample.com/123} {type:event,length:456} {message:自定义错误信息,level:error,extra:{foo:bar}} {type:attachment,length:1024,filename:context.json,content_type:application/json} 这里是原始附件内容这个格式看起来简单但细节特别容易错。每个JSON对象的字段顺序无所谓但必须保证每个对象占一行而且length字段要精确对应后面原始内容的字节数。如果长度对不上整个事件会被Sentry判定为畸形Envelope直接丢掉排查起来极其痛苦。再说一遍实现路径用代码拼字符串是最容易出错的。我建议用数组维护每个部分最后用换行符拼接而不是在循环里反复做字符串累加。每个JSON对象后面跟一个换行附件内容后面也要跟一个换行但要注意length只计算附件本身的字节长度不包含末尾那个换行。这是我最开始踩过的一个莫名其妙的问题长度算错了服务端只报400没有任何细节提示。3.2 用curl和Python把附件送上去附带鉴权细节手工验证Envelope格式的时候curl是最快的工具。我一般在写完拼接逻辑后先用curl打一发确认格式没问题再写代码。Sentry的Envelope接口地址是/api/{project_id}/envelope/project_id是数字ID在项目设置里能看到。认证方式用DSN里的公钥就够了格式是一个Header字段。curl -X POST https://sentry.example.com/api/42/envelope/ \ -H Content-Type: application/x-sentry-envelope \ -H X-Sentry-Auth: Sentry sentry_version7, sentry_clientcurl/1.0, sentry_keyYOUR_PUBLIC_KEY \ --data-binary envelope.txt把这个写成一个可复用的Python函数后端代传也顺手了。我用的是requests库构造一个元组列表最后用空行分隔编码注意所有字符串统一转成UTF-8字节再拼接。Envelope头里的event_id必须是一个合法的UUID格式字符串如果重复或缺失Sentry会当成新事件处理或者直接拒绝。我习惯用uuid.uuid4().hex生成一个32位字符串然后把中划线去掉再填进去。import json import uuid import requests def send_attachment_event(base_url, project_id, key, message, filename, content): event_id uuid.uuid4().hex envelope_header { event_id: event_id, dsn: fhttps://{key}localhost/{project_id} } event_payload { type: event, length: len(message.encode(utf-8)) } event_body { message: message, level: error } attachment_header { type: attachment, length: len(content), filename: filename, content_type: text/plain } envelope_body ( json.dumps(envelope_header) \n json.dumps(event_payload) \n json.dumps(event_body) \n json.dumps(attachment_header) \n content ) headers { Content-Type: application/x-sentry-envelope, X-Sentry-Auth: fSentry sentry_version7, sentry_clientmyapp/1.0, sentry_key{key} } resp requests.post( f{base_url}/api/{project_id}/envelope/, headersheaders, dataenvelope_body.encode(utf-8) ) return resp.status_code鉴权这里有个容易混淆的点用DSN里的公钥sentry_key只能上报不能做管理操作。如果你是想用API Token来上传附件需要在Token权限里勾选project:write。我测试过只勾event:read或project:read都会在接口层直接被拒掉返回403。自托管环境和Sentry SaaS的Token权限模型略有差异但大原则一致往项目里写数据需要写入权限。我一直建议单独建一个只用于上报的Token不要拿管理员Token到处用。4. 微信小程序与特殊场景被平台限制怎么破4.1 数据落地再上传wx.env.user_data_path 的正确用法微信小程序的运行环境和浏览器差别很大最突出的问题有两个一是不能直接操作DOM二是文件系统的API和Web标准完全不同。如果你想把某个文件作为Sentry附件传上去第一步得先把内容写到本地再用小程序的文件接口读取。这里就绕不开wx.env.user_data_path这个路径。wx.env.user_data_path是小程序为用户数据分配的专属目录每个用户在小程序里有自己独立的一套本地文件路径通常在微信客户端的管理目录下。你直接wx.getFileSystemManager().writeFile()写到这个目录下再readFile读出来数据才能稳定地打包进上报内容。我一开始试着把文件内容直接放在内存里拼Envelope结果发现小程序的请求库对超长URL和request header有很多隐性限制走本地文件反而更稳。const fs wx.getFileSystemManager(); const filePath ${wx.env.user_data_path}/my-attachment.json; fs.writeFile({ filePath: filePath, data: JSON.stringify({ time: Date.now(), page: getCurrentPages() }), encoding: utf8, success: () { fs.readFile({ filePath: filePath, encoding: utf8, success: (res) { // 把 res.data 作为附件内容走API直传 uploadAttachmentToSentry(res.data); } }); } });有一点要提醒wx.env.user_data_path在不同基础库版本上表现可能有细微差异个别低版本客户端甚至可能拿不到这个环境变量。我建议在初始化上报器的时候先打个日志确认路径非空再继续否则后续的读写全部白费。文件写完后要留意权限writeFile默认是覆盖写如果之前同名文件存在新的内容会直接替换不会追加报错。4.2 小程序里无法用XMLHttpRequest构造Envelope也能传小程序使用的网络接口是wx.request和wx.uploadFile和浏览器里的fetch/XMLHttpRequest不是一回事。wx.uploadFile走的是multipart/form-data而Sentry的Envelope接口要的是application/x-sentry-envelope格式的纯文本请求体。有人尝试把Envelope字符串塞进wx.uploadFile的formData里结果服务端根本不认。正确做法是使用wx.request手动指定Content-Type头为application/x-sentry-envelope然后把完整的Envelope字符串作为data传出去。这里有个细节wx.request的data如果直接传字符串某些版本会自动帮你做URL编码而这恰恰会破坏Envelope的换行结构。我的处理方式是把Envelope字符串转成ArrayBuffer再传绕开微信对字符串的自动处理。另外小程序的请求默认超时时间是60秒如果附件内容比较大建议在wx.request的timeout参数里显式调大一点不然传一半断掉很难排查。function uploadEnvelope(envelopeStr) { const buffer new TextEncoder().encode(envelopeStr).buffer; wx.request({ url: https://sentry.example.com/api/42/envelope/, method: POST, header: { Content-Type: application/x-sentry-envelope, X-Sentry-Auth: Sentry sentry_version7, sentry_clientweapp/1.0, sentry_keyYOUR_KEY }, data: buffer, timeout: 30000, success: (res) console.log(upload ok, res.statusCode), fail: (err) console.error(upload fail, err) }); }在真机调试时我最常碰到的问题不是格式错误而是证书和域名校验。Sentry如果用了自签名证书或者域名没有配到微信后台的request合法域名列表里请求会直接被微信拦截连状态码都看不到。所以小程序之前别急着怼代码先去微信公众平台把Sentry域名加进白名单不然调试半小时全花在排查这儿了。5. 常见问题与排查实录5.1 上传成功但UI里找不到附件先查这几处这个现象挺迷惑的接口返回200日志里也没报错但打开Sentry的Issue详情页看了一圈就是没有附件区域。我排查过几次之后总结出三个高频原因。第一个是看错地方。Sentry新版UI里附件不在错误事件的默认面板里需要点到事件详情后找“Attachments”相关的标签页或者折叠区。如果你只停留在Issue列表页当然什么也看不到。这个问题虽然低级但确实坑过不少刚上手的人。第二个是event_id不匹配。Envelope头里的event_id必须和事件内容的ID一致或者保证它们是同一个Envelope里同时提交的。如果你把附件单独用一个请求提交并且没有关联任何事件ID那附件会变成孤儿附件不会自动挂到某个事件下。我见过有人用SDK上报事件后又手动构造了一个只有附件的Envelope结果两边各传各的UI里当然找不到。第三个是大小限制。Sentry服务端有默认的附件体积限制超过最大允许值的附件会被静默丢弃返回的200只是“请求接收成功”并不代表“附件保存成功”。这个限制在不同版本里配置项可能不同自托管环境下建议去管理后台或配置文件里查一下max-attachment-size相关的设置。前端SDK通常有自己的体积限制两层叠加后实际能成功上传的附件大小往往比你预期的小。5.2 文件上传超时、404、403的定位思路先说404。如果你手动构造Envelope请求时用了错误的project_id返回的就是404。Sentry的Project ID通常是一串数字在项目设置的基础信息里能看到不要在URL里写项目的slug名称。另外自托管环境如果配置了路径前缀比如/sentry/那么完整接口路径是/sentry/api/42/envelope/漏掉前缀一样会404。403的问题比较集中基本都是鉴权信息不对。DSN格式里的sentry_key是公钥部分通常在https://public:secrethost/1这种格式里取的是public那一段。如果你把整个DSN字符串塞进X-Sentry-Auth里那就等着403吧。用API Token的话记得检查Token是否勾选了写入权限project:write是常见的最低要求。还有一个细节某些中间层会改写X-Sentry-Auth头比如经过API网关时头被剥离也会导致403排查时可以抓包确认请求头是否完整到达Sentry服务端。超时的情况我遇到最多的是自托管Sentry和Nginx配置共同导致的。Sentry默认对附件存储的后端可能是本地磁盘或对象存储写入慢的时候接口响应会拖长。如果Nginx配置了client_max_body_size且值太小大于这个体积的请求会在Nginx层直接断掉表现就是连接被重置或504。我的做法是在Nginx的location /块里显式调大上传限制同时注意Sentry容器内部还有一层自己的限制两层都放行才真正生效。5.3 文件名乱码、保留期这些碎碎念中文文件名乱码的问题我在后端代传时遇到过。Envelope头里的filename字段如果直接放中文某些中间链路会按Latin-1或GBK重新解码到Sentry那里就变成一串乱码。稳妥做法是统一用UTF-8编码如果实在有特殊字符就先用encodeURIComponent做一次编码文件名里存编码后的字符串UI上展示时再解码。虽然麻烦一点但兼容性最好。关于保留期Sentry的附件不是永久存储的。它跟事件数据的保留策略绑定你在项目设置里配置的事件保留天数附件也遵循同样的时间线。如果你需要更长时间的归档那就别指望Sentry当网盘用要么定期把附件导出到自己存储要么在上报前就把重要文件同时备份到公司内部服务。我在做安全审计类项目时特意做过一次导出任务每天把前一天的附件拉下来存到自己的对象存储里避免Sentry那边清数据后什么都找不回来。附件命名这件事也应该从一开始就养成好习惯。我建议统一采用环境-模块-时间戳的格式比如prod-checkout-20250115-1532.json这样在Issue详情里扫一眼就能看出附件属于哪个环节。不然几十个附件全部叫attachment.json翻起来想死的心都有。6. 实操心得与经验补充6.1 附件不是越多越好这几个原则我踩坑后才明白用久了之后我总结出一个很反直觉的经验附件并不是越全越好反而要讲究克制。刚接入时我也犯过“什么数据都往里塞”的毛病结果一个错误带上来几百KB的上下文排查时反而淹没在无关信息里。后来我把附件分成两类来使用一类是通用型固定带上用户标识、路由信息、环境版本这类对任何错误都有用的基础数据另一类是专用型只在特定错误分支里动态生成比如导入文件失败时附上文件格式解析的中间结果。还要考虑流量成本。SDK直传时如果全局每次请求都附带一个几百KB的附件用户的流量消耗会非常明显。移动端用户对流量很敏感这个要权衡。我的做法是给附件上报加一个采样率比如只有error级别的事件才带附件warning级别只带基础上下文。这样既保证关键问题有充分信息又不至于把用户的流量和Sentry存储都烧掉。另外提醒一句附件内容如果有用户隐私或敏感字段务必在上报前做好脱敏。Sentry服务器上存了敏感数据一旦泄露就是事故。我见过有团队把用户手机号直接打进附件结果那次Sentry账号被渗透客户信息全丢了教训极其惨痛。写一个sanitize函数对所有附件内容统一过滤一遍再做replace处理这个步骤不能省。6.2 测试时的小技巧用curl快速验证接口别反复改代码如果你正在调试Envelope格式或鉴权我强烈建议先用curl把整个流程验证通了再回去写业务代码。我一般会准备一个本地环境的测试脚本里面放一个构造好的Envelope文本文件然后用curl打一发。如果返回200再把这个Envelope文本拿给后端同事看格式如果返回4xx直接改文本重试完全不用动代码逻辑。测试的时候有个常用手段是故意构造一个超大的附件看服务端到底在哪一层拒绝。这样能快速确认client_max_body_size和Sentry内部限制分别是什么值。我之前排查一个线上问题前端传了一个2MB的附件结果被静默丢弃后来用curl一发5MB的测试请求很快就定位到Nginx的client_max_body_size配的是2m坑了我们好几天。最后再分享一个我自己的使用习惯无论用哪种方式接入我都会在Sentry的项目设置里打开“附件上传成功与否”的相关日志面板或者在本地做一个最小化的Mock服务先拦截SDK的上报请求检查Envelope结构对不对再放行到真实Sentry。这样即使SDK版本升级导致行为变化我也能在第一时间发现问题而不是等线上事故出来后才发现附件根本没传上去。