前端老鸟血泪史:搞定那些让人头秃的报错,让线上稳如老狗

前端老鸟血泪史:搞定那些让人头秃的报错,让线上稳如老狗

前端老鸟血泪史:搞定那些让人头秃的报错,让线上稳如老狗

前端老鸟血泪史:搞定那些让人头秃的报错,让线上稳如老狗

别在那硬扛了,聊聊咱们天天跟JS错误斗智斗勇那点破事

说真的,干前端这些年,我算是看明白了——咱们这行就是个和错误谈恋爱的过程。刚开始你看见控制台飘红就慌得一批,到后来看见报错甚至能笑出声,这种心态转变没个三五年的深夜加班根本练不出来。

谁还没在凌晨三点被Sentry报警电话叫醒过?那感觉,心脏直接从胸腔蹦到嗓子眼,比当年初恋对象发"在吗"还刺激。你迷迷糊糊摸过手机,屏幕亮起的瞬间看到"Production Error: TypeError: Cannot read property ‘map’ of undefined",整个人瞬间就清醒了。这时候你脑子里闪过一万个念头:是不是昨天那个hotfix没测全?还是某个接口突然改了返回格式?又或者,是老板正在看着监控大屏准备杀人?

我见过最离谱的一次,大促期间某个核心链路突然崩了,报错信息就一句话:“undefined is not a function”。就这一句,没堆栈,没上下文,跟猜谜似的。团队五个人盯着日志看了两小时,最后发现是某个CDN节点缓存了个旧版本的polyfill,新代码调用了一个不存在的方法。你说这上哪说理去?浏览器它也不告诉你"嘿兄弟,你加载的脚本版本不对",它就给你扔个undefined,剩下的自己悟去吧。

还有很多人觉得,不就是报错吗,加个try-catch不就完了?太天真了。我见过太多这种"保险主义者",代码里try-catch套得跟俄罗斯套娃似的,结果用户该白屏还是白屏。为啥?因为try-catch只能抓同步错误,那些藏在setTimeout里的、躲在Promise里的、埋在async/await里的错误,该漏还是漏。更坑的是,有时候你catch住了错误,但后续逻辑依赖了前面失败的数据,程序没崩,但跑出来的结果全是错的,用户看着满屏的乱码比直接白屏还崩溃。

所以咱们今天不整那些虚头巴脑的理论,就唠唠怎么把那些神出鬼没的Bug按在地上摩擦。都是血泪换来的经验,看完能让你少熬几个大夜。

扒一扒浏览器到底是怎么"发疯"报错的

要搞定错误监控,你得先知道敌人长啥样。浏览器的错误机制说复杂也复杂,说简单也简单,但里面的坑是真的多。

window.onerror这个老六,到底靠不靠谱

全局错误捕获,大家第一个想到的都是window.onerror。这玩意儿确实能抓不少东西,但你要是以为有了它就高枕无忧,那等着被坑吧。

// 最基础的用法,但说实话,这代码写了跟没写差不多 window.onerror=function(message, source, lineno, colno, error){ console.log('抓到错误了:', message);returntrue;// 返回true阻止浏览器默认报错(就是控制台不飘红)};// 稍微像样点的版本,至少能把信息攒一攒 window.onerror=function(message, source, lineno, colno, error){// 有些老浏览器可能不给error对象,得做个兼容const errorInfo ={message: message,source: source,line: lineno,column: colno,stack: error && error.stack ? error.stack :'no stack trace',userAgent: navigator.userAgent,timestamp:newDate().toISOString(),// 加个页面URL,不然你都不知道用户在哪个页面炸的pageUrl: window.location.href };// 发送到监控服务,注意这里要用try-catch,别监控代码自己崩了try{sendToMonitoring(errorInfo);}catch(e){// 实在发不出去,至少存localStorage,下次有机会再发 localStorage.setItem('pending_error_'+ Date.now(),JSON.stringify(errorInfo));}returnfalse;// 建议返回false,让控制台继续飘红,方便开发时看到};

但window.onerror有个巨坑——它抓不到跨域脚本的错误。啥意思呢?你引了个CDN上的jQuery,那里面出错了,onerror收到的信息就是"Script error",其他啥也没有。这是浏览器的安全策略,怕你别有用心地探测其他域的脚本内容。要解决这个问题,得两边配合:CDN那边要加Access-Control-Allow-Origin头,你这边script标签要加crossorigin="anonymous"

<!-- 不加crossorigin,报错信息会被浏览器吞掉 --><scriptsrc="https://cdn.example.com/app.js"></script><!-- 加了crossorigin,配合CDN的CORS头,才能拿到详细错误 --><scriptsrc="https://cdn.example.com/app.js"crossorigin="anonymous"></script>

console.error就是个摆设,别指望它

很多人喜欢在代码里console.error一下,觉得这样就算处理了错误。兄弟,console.error只是打印到控制台,它既不阻止程序崩溃,也不通知你去处理。更关键的是,生产环境谁看控制台啊?用户的浏览器控制台你看得见吗?

而且异步错误这东西,真的是个幽灵。你看这段代码:

// 这种错误,window.onerror根本抓不到setTimeout(()=>{thrownewError('我在异步里爆炸了');},1000);// Promise里的错误,onerror也看不见 Promise.resolve().then(()=>{thrownewError('Promise内部错误');});// async/await如果没包try-catch,也是悄无声息地挂asyncfunctionfetchData(){const response =awaitfetch('/api/data');// 如果这里网络错误,整个函数就停了const data =await response.json();// 如果返回的不是JSON,这里又炸return data;}

这些异步错误,window.onerror一个都抓不到。它们就像深夜里的刺客,悄无声息地把你的程序干掉,然后消失在事件循环的黑暗中。

Promise.reject这个定时炸弹

Promise如果没处理rejection,那简直就是埋了个地雷。而且这地雷还有个特性——专门挑你上线那天炸。

// 这种写法,如果fetch失败,Promise就处于rejected状态,但没人管functiongetUserInfo(userId){returnfetch(`/api/user/${userId}`).then(res=> res.json());}// 调用的时候没catch,错误就漏了getUserInfo(123).then(data=>{renderUser(data);// 如果前面reject了,这行根本不会执行});// 没有.catch(),错误就被"吞"了(其实浏览器会报unhandledrejection,但很多人忽略这个)// 正确的姿势应该是这样getUserInfo(123).then(data=>renderUser(data)).catch(err=>{ console.error('获取用户信息失败:', err);showErrorToast('用户信息加载失败,请重试');// 甚至可以上报错误reportError({type:'API_ERROR',api:'/api/user/123',error: err.message });});

而且现代浏览器对unhandled rejection的处理越来越严格。以前可能只是个警告,现在有些环境直接给你把进程杀了。所以一定得加上全局的unhandledrejection监听:

window.addEventListener('unhandledrejection',function(event){// event.reason就是reject的原因(通常是Error对象)const errorInfo ={type:'unhandled_promise_rejection',message: event.reason?.message ||'Unknown promise rejection',stack: event.reason?.stack ||'No stack',timestamp:newDate().toISOString()};// 上报错误reportError(errorInfo);// 阻止浏览器默认行为(比如在控制台打印) event.preventDefault();});// 还有个rejectionhandled事件,用来监听"迟到"的catch window.addEventListener('rejectionhandled',function(event){// 这个很少用,但知道有这个东西就行 console.log('Promise rejection was handled later:', event.reason);});

资源加载失败,浏览器的提示有多敷衍

图片挂了、CSS 404了、脚本加载失败,这些错误window.onerror也抓不到。你得用专门的error事件监听:

// 监听图片加载失败 document.addEventListener('error',function(event){const target = event.target;// 判断是不是图片if(target.tagName ==='IMG'){ console.error('图片加载失败:', target.src);// 可以搞个兜底图,别让页面丑着 target.src ='/assets/fallback-image.png';// 上报错误,带上图片URLreportError({type:'RESOURCE_ERROR',resourceType:'image',url: target.src,pageUrl: window.location.href });}// 判断是不是脚本if(target.tagName ==='SCRIPT'){reportError({type:'RESOURCE_ERROR',resourceType:'script',url: target.src,// 脚本错误比较严重,可能需要立即通知severity:'high'});}},true);// 注意这里要用捕获阶段,因为error事件不冒泡// 还有个专门的window.addEventListener('error')可以抓资源错误 window.addEventListener('error',function(event){// event.target可以判断是哪种资源 console.log('资源错误详情:', event.target);},true);

但说实话,资源错误的堆栈信息基本为零,你就只能知道"某个URL加载失败了",至于为啥失败(是404?还是超时?还是被CORS拦截了?),得靠猜。这时候network条件的上下文就特别重要,后面会讲到怎么收集这些信息。

给代码穿上防弹衣,这些监控手段必须安排上

知道了浏览器怎么"发疯",咱们就得给代码穿上防弹衣。但防弹衣也不能乱穿,穿太厚影响行动(性能),穿太薄又防不住子弹(漏报)。

网络错误这块,很多人其实没监控到位

大家通常只关心JS逻辑错误,但线上问题一大半其实是网络相关的。接口超时、返回500、DNS解析失败,这些"外家功夫"不防,用户该骂娘还是骂娘。

// 封装一个带监控的fetch,别直接用原生fetchclassMonitoredFetch{constructor(options ={}){this.timeout = options.timeout ||10000;// 默认10秒超时this.retries = options.retries ||1;// 默认重试1次this.baseURL = options.baseURL ||'';}asyncrequest(url, options ={}){const fullURL =this.baseURL + url;const startTime = performance.now();let attemptCount =0;constexecuteRequest=async()=>{ attemptCount++;try{// 创建AbortController用于超时控制const controller =newAbortController();const timeoutId =setTimeout(()=> controller.abort(),this.timeout);const response =awaitfetch(fullURL,{...options,signal: controller.signal });clearTimeout(timeoutId);// 记录性能数据,不管成功失败都要记const duration = performance.now()- startTime;this.logPerformance(fullURL, duration, response.status);// HTTP错误状态也要当作错误处理if(!response.ok){thrownewHTTPError(`HTTP ${response.status}`, response.status, fullURL);}return response;}catch(error){// 区分错误类型if(error.name ==='AbortError'){thrownewTimeoutError(`Request timeout after ${this.timeout}ms`, fullURL);}// 网络错误(断网、DNS失败等)if(error.message.includes('fetch')|| error.message.includes('network')){thrownewNetworkError(error.message, fullURL);}throw error;}};// 重试逻辑let lastError;for(let i =0; i <this.retries; i++){try{returnawaitexecuteRequest();}catch(error){ lastError = error;// 只有网络错误和超时才重试,4xx错误重试也没用if(error instanceofHTTPError&& error.status >=400&& error.status <500){throw error;}// 延迟重试,别猛冲if(i <this.retries -1){awaitthis.delay(1000*(i +1));// 指数退避}}}// 重试用完了还是失败,上报错误this.reportError(lastError,{url: fullURL,method: options.method ||'GET', attemptCount,duration: performance.now()- startTime });throw lastError;}logPerformance(url, duration, status){// 发送到性能监控,可以用来做SLIif(window.performanceObserver){// 或者用自己的上报逻辑 console.log(`[Performance] ${url}: ${duration.toFixed(2)}ms, status: ${status}`);}}reportError(error, context){const errorInfo ={type:'NETWORK_ERROR',errorType: error.constructor.name,message: error.message,url: context.url,method: context.method,attemptCount: context.attemptCount,duration: context.duration,userAgent: navigator.userAgent,// 网络状况,这个很有用connection: navigator.connection ?{effectiveType: navigator.connection.effectiveType,// 4g/3g/2gdownlink: navigator.connection.downlink,rtt: navigator.connection.rtt }:'unknown'};reportError(errorInfo);}delay(ms){returnnewPromise(resolve=>setTimeout(resolve, ms));}}// 自定义错误类型,方便区分classHTTPErrorextendsError{constructor(message, status, url){super(message);this.name ='HTTPError';this.status = status;this.url = url;}}classTimeoutErrorextendsError{constructor(message, url){super(message);this.name ='TimeoutError';this.url = url;}}classNetworkErrorextendsError{constructor(message, url){super(message);this.name ='NetworkError';this.url = url;}}// 使用示例const api =newMonitoredFetch({baseURL:'https://api.example.com',timeout:5000,retries:2});// 调用的时候就像普通fetch一样,但背后有监控和重试 api.request('/user/profile').then(res=> res.json()).then(data=> console.log(data)).catch(err=>{// 这里拿到的错误已经是分类好的,可以针对性处理if(err instanceofTimeoutError){showToast('网络太慢了,请检查网络或稍后再试');}elseif(err instanceofNetworkError){showToast('网络好像断了,看看WiFi是不是没连');}});

看到没,网络监控不只是记个错,还得把当时的网络环境、重试次数、耗时都记下来。这样你排查问题的时候才能还原现场,而不是对着"请求失败"四个字发呆。

SourceMap这玩意儿,真的是救命稻草

生产环境的代码都是压缩过的,报错信息长这样:at t.e (app.abc123.js:1:2345)。你看着这一堆a.b.c,根本不知道是哪个文件哪行代码。这时候SourceMap就是你的救命稻草。

但SourceMap怎么用,这里面有讲究。你不能直接把SourceMap文件扔到生产环境,因为那样等于把源码公开了(虽然可以配置只映射到行不映射到列,但还是能反推个大概)。所以通常的做法是:

  1. 构建时生成SourceMap,但不上传到CDN
  2. 把SourceMap文件存到内部服务器或者Sentry这种错误监控平台
  3. 线上报错时,用监控平台保存的SourceMap来还原堆栈
// webpack配置示例,怎么生成SourceMap// webpack.config.js module.exports ={// 生产环境用这个,生成单独的map文件,但不包含源码(只映射行列)devtool:'hidden-source-map',// 或者用这个,更安全,只映射到行,不映射到列,反编译难度大一些// devtool: 'nosources-source-map',output:{filename:'[name].[contenthash].js',// 很重要:sourceMapFilename要配置好sourceMapFilename:'sourcemaps/[name].[contenthash].js.map'}};// 如果你用Vite// vite.config.jsexportdefault{build:{sourcemap:'hidden',// 生成但不引用// 或者// sourcemap: true, // 开发时用}};

然后在错误监控服务里,你需要实现一个堆栈还原的功能。如果你用Sentry,它自动就做了。如果自己实现,大概长这样:

// 简化的堆栈还原逻辑,实际要用source-map库const{ SourceMapConsumer }=require('source-map');asyncfunctionunminifyStackTrace(stackTrace, sourceMapContent){const consumer =awaitnewSourceMapConsumer(sourceMapContent);const lines = stackTrace.split('\n');const unminifiedLines = lines.map(line=>{// 解析压缩后的位置,比如 "at t.e (app.abc123.js:1:2345)"const match = line.match(/at\s+(.+)\s+\((.+):(\d+):(\d+)\)/);if(!match)return line;const[, funcName, fileName, lineNum, colNum]= match;// 用SourceMap还原const originalPosition = consumer.originalPositionFor({line:parseInt(lineNum),column:parseInt(colNum)});if(originalPosition.source){return`at ${originalPosition.name || funcName} (${originalPosition.source}:${originalPosition.line}:${originalPosition.column})`;}return line;}); consumer.destroy();return unminifiedLines.join('\n');}// 使用示例const minifiedStack ="at t.e (app.abc123.js:1:2345)\nat n.r (app.abc123.js:1:5678)";const sourceMap = fs.readFileSync('./app.abc123.js.map','utf8');unminifyStackTrace(minifiedStack, sourceMap).then(originalStack=> console.log(originalStack));// 输出: at handleClick (src/components/Button.tsx:45:12)\nat render (src/App.tsx:123:8)

还原报错现场,得像侦探小说一样精彩

光知道哪行代码错了还不够,你得知道当时用户干了啥、页面啥状态、网络啥情况。这就需要"录屏"功能——不是真的录视频,而是记录用户操作和页面变化。

// 简化的用户行为录屏实现,用rrweb的思路classSessionRecorder{constructor(options ={}){this.events =[];this.maxEvents = options.maxEvents ||1000;// 限制数量,别撑爆内存this.isRecording =false;}start(){if(this.isRecording)return;this.isRecording =true;// 记录初始DOM状态this.recordEvent({type:'init',timestamp: Date.now(),// 这里应该序列化DOM,简化示例就记个URLurl: window.location.href,viewport:{width: window.innerWidth,height: window.innerHeight },userAgent: navigator.userAgent });// 监听点击this.clickHandler=(e)=>{this.recordEvent({type:'click',timestamp: Date.now(),target:this.describeElement(e.target),x: e.clientX,y: e.clientY });}; document.addEventListener('click',this.clickHandler,true);// 监听输入this.inputHandler=(e)=>{if(e.target.tagName ==='INPUT'|| e.target.tagName ==='TEXTAREA'){this.recordEvent({type:'input',timestamp: Date.now(),target:this.describeElement(e.target),// 注意:密码框不能记值,隐私保护value: e.target.type ==='password'?'***': e.target.value.substring(0,100)// 截断,别记太长});}}; document.addEventListener('input',this.inputHandler,true);// 监听页面跳转this.popstateHandler=()=>{this.recordEvent({type:'navigation',timestamp: Date.now(),url: window.location.href });}; window.addEventListener('popstate',this.popstateHandler);// 定时记录性能数据this.performanceInterval =setInterval(()=>{this.recordEvent({type:'performance',timestamp: Date.now(),memory: performance.memory ?{usedJSHeapSize: performance.memory.usedJSHeapSize,totalJSHeapSize: performance.memory.totalJSHeapSize }:null,// 长任务(阻塞主线程超过50ms的任务)longTasks:this.getLongTasks()});},5000);}describeElement(element){// 给元素一个可读描述,比如 "button#submit.primary-btn"const tag = element.tagName.toLowerCase();const id = element.id ?`#${element.id}`:'';const classes = element.className &&typeof element.className ==='string'?`.${element.className.split(' ').join('.')}`:'';const text = element.textContent ? element.textContent.substring(0,50):'';return`${tag}${id}${classes}${text ?` "${text}"`:''}`;}getLongTasks(){// 需要PerformanceObserver支持if(!window.PerformanceObserver)return[];// 实际实现要维护一个列表,这里简化return[];}recordEvent(event){if(this.events.length >=this.maxEvents){this.events.shift();// 满了就丢最老的,保持滑动窗口}this.events.push(event);}// 报错时调用这个,拿到最近的上下文getContextForError(){return{recentEvents:this.events.slice(-50),// 报错前最近50个操作timestamp: Date.now()};}stop(){this.isRecording =false; document.removeEventListener('click',this.clickHandler,true); document.removeEventListener('input',this.inputHandler,true); window.removeEventListener('popstate',this.popstateHandler);clearInterval(this.performanceInterval);}}// 使用示例const recorder =newSessionRecorder({maxEvents:500}); recorder.start();// 报错时 window.addEventListener('error',(e)=>{const context = recorder.getContextForError();reportError({message: e.message,stack: e.error?.stack,// 带上用户操作上下文userActions: context.recentEvents,// 还可以带上DOM快照(如果用了rrweb的话)domSnapshot:captureDOMSnapshot()});});

这种"录屏"数据配合错误堆栈,你排查问题的时候就跟看监控录像似的,用户点了哪、输入了啥、页面怎么变的,一目了然。但要注意隐私合规,密码、身份证号这些敏感信息一定要脱敏。

埋点策略得有讲究,别把用户浏览器卡死

监控代码本身不能成为性能瓶颈。我见过有团队为了"全量监控",在每个函数开头结尾都插桩,结果主线程直接堵死,页面卡顿得要命。

// 错误的示范:这种写法,函数多了直接卡爆functionheavyFunction(){ monitor.startTrack('heavyFunction');// 这里可能有DOM操作,阻塞渲染// ... 业务逻辑 monitor.endTrack('heavyFunction');}// 正确的姿势:采样 + 异步上报classSmartMonitor{constructor(options ={}){this.sampleRate = options.sampleRate ||0.1;// 只监控10%的请求this.batchSize = options.batchSize ||10;// 批量上报,攒够10条发一次this.buffer =[];this.flushInterval = options.flushInterval ||5000;// 最多5秒发一次this.isSending =false;// 定时刷新setInterval(()=>this.flush(),this.flushInterval);// 页面关闭前强制发送(用sendBeacon,可靠) window.addEventListener('beforeunload',()=>this.flush(true));}track(event){// 采样:不是所有事件都记if(Math.random()>this.sampleRate)return;this.buffer.push({...event,timestamp: Date.now()});// 攒够一批就发,但用requestIdleCallback,别卡主线程if(this.buffer.length >=this.batchSize){if(window.requestIdleCallback){requestIdleCallback(()=>this.flush(),{timeout:2000});}else{setTimeout(()=>this.flush(),0);// 降级}}}flush(isBeacon =false){if(this.buffer.length ===0||this.isSending)return;this.isSending =true;const data =[...this.buffer];// 拷贝一份this.buffer =[];// 清空缓存if(isBeacon && navigator.sendBeacon){// 页面关闭时用sendBeacon,可靠但大小有限制(通常64KB)const blob =newBlob([JSON.stringify(data)],{type:'application/json'}); navigator.sendBeacon('/monitor/collect', blob);this.isSending =false;}else{// 普通fetch上报fetch('/monitor/collect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data),keepalive:true// 页面关闭后也尽量发完}).catch(err=>{// 上报失败,存回buffer下次再试,但别无限增长if(this.buffer.length <100){this.buffer.unshift(...data);}}).finally(()=>{this.isSending =false;});}}}// 使用:性能开销极小const monitor =newSmartMonitor({sampleRate:0.1});functionbusinessLogic(){ monitor.track({type:'function_start',name:'businessLogic'});// ... 业务代码 monitor.track({type:'function_end',name:'businessLogic',duration:123});}

关键点:采样(别全量)、批量(别一条条发)、异步(别卡主线程)、降级(sendBeacon)。这样监控代码对性能的影响可以忽略不计。

这招好用是好用,但副作用你也得心里有数

监控这东西,用好了是神器,用不好就是给自己挖坑。我见过太多团队监控搞得太激进,结果问题没解决,先制造了一堆新问题。

监控代码太猛,主线程直接堵死

前面说了性能优化,但还有些更隐蔽的坑。比如有些监控库为了"精确计时",用performance.now()在每次DOM操作前后打戳,结果频繁的时钟查询本身就成为性能瓶颈。

还有些库为了获取"完整的错误上下文",在报错时遍历整个DOM树,序列化成字符串。如果页面很大(比如那种无限滚动的长列表),这一下就能卡死好几秒。

// 危险操作:报错时遍历整个DOMfunctiongetFullDOMContext(){return document.documentElement.outerHTML;// 大页面这里直接爆炸,内存和CPU双杀}// 相对安全的做法:只取关键元素functiongetSafeContext(){const context ={url: window.location.href,viewport:{width: window.innerWidth,height: window.innerHeight },// 只取body的直接子元素数量,判断页面复杂度bodyChildrenCount: document.body.children.length,// 当前聚焦的元素activeElement: document.activeElement?.tagName,// 滚动位置scrollY: window.scrollY };return context;}

另外,监控代码本身也可能报错。比如你想记录navigator.connection的网络状态,但某些浏览器不支持这个API,直接抛错。所以监控代码里要再套try-catch,形成"监控的监控":

// 防御性编程:监控代码也要被保护functionsafeMonitor(fn){returnfunction(...args){try{returnfn.apply(this, args);}catch(e){// 监控代码出错,至少别影响业务 console.error('Monitor error:', e);// 甚至可以上报"监控器故障",但小心死循环if(window.__monitorFailed){ window.__monitorFailed =true;// 简化版的上报,避免再用复杂的监控逻辑fetch('/monitor/self-error',{method:'POST',body:JSON.stringify({error: e.message }),keepalive:true});}}};}// 包装所有监控方法const originalTrack = monitor.track; monitor.track =safeMonitor(originalTrack);

海量报错数据,服务器账单比你工资涨得还快

如果你不做采样和聚合,生产环境的海量报错能把你的监控服务冲垮。想象一下,一个bug影响了10万用户,每人触发10次,这就是100万条错误日志。存起来、索引起来、查询起来,都是钱。

所以一定要有分级和聚合策略:

// 错误聚合:相同的错误只记一次,但记次数classErrorAggregator{constructor(){this.errorMap =newMap();// 用Map做内存中的聚合this.flushInterval =60000;// 1分钟同步一次setInterval(()=>this.syncToServer(),this.flushInterval);}// 生成错误指纹,用来判断是不是同一个错误getFingerprint(error){// 取堆栈的前几行,去掉行号(因为每次编译行号会变)const stack = error.stack ||'';const stackLines = stack.split('\n').slice(0,3);// 用正则去掉具体的行号列号,只保留函数名和文件名const normalizedStack = stackLines.map(line=> line.replace(/:\d+:\d+/g,'')// 去掉:12:34这种).join('|');// 加上错误类型和消息return`${error.name}:${error.message}:${normalizedStack}`;}report(error, context ={}){const fingerprint =this.getFingerprint(error);const now = Date.now();if(this.errorMap.has(fingerprint)){// 已存在的错误,增加计数const record =this.errorMap.get(fingerprint); record.count++; record.lastTime = now;// 保留最近几个用户的上下文,用于复现if(record.samples.length <5){ record.samples.push(context);}}else{// 新错误this.errorMap.set(fingerprint,{ fingerprint,name: error.name,message: error.message,stack: error.stack,firstTime: now,lastTime: now,count:1,samples:[context]});}}syncToServer(){if(this.errorMap.size ===0)return;const errors = Array.from(this.errorMap.values());this.errorMap.clear();// 清空内存// 批量上报聚合后的错误fetch('/monitor/aggregated-errors',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({timestamp: Date.now(),errors: errors,// 加上聚合统计totalErrors: errors.reduce((sum, e)=> sum + e.count,0),uniqueErrors: errors.length })});}}// 使用const aggregator =newErrorAggregator(); window.addEventListener('error',(e)=>{ aggregator.report(e.error,{url: window.location.href,userId:getCurrentUserId()// 如果有的话});});

这样你存的不是100万条重复记录,而是几百条聚合记录,每条带个计数器。查询的时候也能一眼看出哪个错误影响面最大。

误报率太高,狼来了的故事听多了

如果监控太敏感,一堆无关紧要的警告会把真正的P0级错误淹没。比如第三方脚本的小错误、浏览器插件注入的代码错误、用户网络不稳定导致的请求失败,这些如果都报上来,团队很快就麻木了。

// 错误过滤策略functionshouldReportError(error){// 1. 过滤掉已知的无害错误(比如某些浏览器扩展的锅)const knownHarmlessPatterns =[/ResizeObserver loop limit exceeded/,// Chrome的 benign error/Script error\.?/,// 跨域脚本错误,信息太少,通常处理不了/Non-Error promise rejection captured/// 某些框架的警告];for(const pattern of knownHarmlessPatterns){if(pattern.test(error.message))returnfalse;}// 2. 过滤掉第三方脚本的错误(如果你确定不管)if(error.stack){const isThirdParty = error.stack.split('\n').some(line=> line.includes('chrome-extension://')|| line.includes('moz-extension://')|| line.includes('https://third-party-analytics.com'));if(isThirdParty)returnfalse;}// 3. 只报自己域名的错误const isOwnDomain = error.stack && error.stack.includes(window.location.hostname);if(!isOwnDomain)returnfalse;// 谨慎开启,可能会漏掉CDN资源的错误// 4. 采样:高频错误降低采样率const fingerprint =getFingerprint(error);const count =getRecentErrorCount(fingerprint);// 需要维护一个计数if(count >100){// 这个错误已经出现100次以上了,只采样10%return Math.random()<0.1;}returntrue;}// 上报前检查 window.addEventListener('error',(e)=>{if(!shouldReportError(e.error))return;reportError(e.error);});

过滤策略要持续维护,根据线上实际情况调整。定期review错误列表,把那些"无害但 noisy"的错误加入黑名单。

真到了生产环境,这套组合拳该怎么打

监控搭好了,怎么用是个学问。不能所有错误都一视同仁,得有分级处理机制。

分级处理才是王道

错误得分级,就像医院分急诊和门诊一样。有些错误记下来就行,有些得立即通知,有些得直接触发回滚。

// 错误分级系统classErrorSeverityClassifier{classify(error, context ={}){// P0:核心流程阻断,必须立即处理if(this.isP0Error(error, context)){return{level:'P0',action:'immediate_alert',// 立即电话/短信通知autoRollback:true,// 考虑自动回滚notifyChannels:['pagerduty','slack','email']};}// P1:重要功能受损,15分钟内响应if(this.isP1Error(error, context)){return{level:'P1',action:'urgent_alert',responseTime:15*60*1000,// 15分钟notifyChannels:['slack','email']};}// P2:次要功能问题,当天处理if(this.isP2Error(error, context)){return{level:'P2',action:'daily_digest',// 日报汇总notifyChannels:['email']};}// P3:轻微问题,周会reviewreturn{level:'P3',action:'weekly_review',notifyChannels:[]};}isP0Error(error, context){// 支付流程报错if(context.pageType ==='checkout'&& error.message.includes('payment')){returntrue;}// 核心API持续500错误if(context.apiPath?.includes('/api/core')&& context.httpStatus >=500){// 而且错误率超过阈值if(context.errorRate >0.1)returntrue;// 10%的请求失败}// 白屏错误(渲染层崩溃)if(error.message?.includes('render')|| error.message?.includes('mount')){if(context.isWhiteScreen)returntrue;}returnfalse;}isP1Error(error, context){// 登录注册流程if(context.pageType ==='login'|| context.pageType ==='signup'){returntrue;}// 用户数据加载失败if(error.message?.includes('user profile'))returntrue;returnfalse;}isP2Error(error, context){// 非核心功能,比如推荐算法挂了if(context.pageType ==='recommendation')returntrue;// 单个图片加载失败if(error.type ==='RESOURCE_ERROR'&& error.resourceType ==='image'){returntrue;}returnfalse;}}// 自动回滚逻辑(需要配合CI/CD)asyncfunctionautoRollback(errorInfo){// 只有P0错误且持续5分钟以上才触发if(errorInfo.level !=='P0')return;const recentErrors =awaitgetRecentP0Errors(5*60*1000);// 最近5分钟if(recentErrors.length <10)return;// 错误数不够,可能是偶发// 触发回滚 console.error('🚨 触发自动回滚!');// 调用CI/CD API回滚到上一个版本fetch('/deploy/rollback',{method:'POST',headers:{'Authorization':'Bearer '+DEPLOY_TOKEN},body:JSON.stringify({reason:`Auto-rollback due to P0 error: ${errorInfo.message}`,targetVersion:'previous-stable'})});// 通知团队notifyTeam({type:'AUTO_ROLLBACK_TRIGGERED',error: errorInfo,timestamp: Date.now()});}

分级策略要根据业务特点定制。电商网站的支付错误是P0,内容网站的推荐算法错误可能就是P2。这个得和团队一起梳理清楚。

优雅的降级方案,让页面"瘸腿"也能跑

现代前端都是SPA,一个组件挂了可能导致整个页面白屏。这时候得有降级策略,让页面"瘸腿"也能跑。

// React的错误边界示例,但思路适用于所有框架import React from'react';classErrorBoundaryextendsReact.Component{constructor(props){super(props);this.state ={hasError:false,error:null};}staticgetDerivedStateFromError(error){// 下次渲染显示降级UIreturn{hasError:true, error };}componentDidCatch(error, errorInfo){// 上报错误reportError({error: error,componentStack: errorInfo.componentStack,componentName:this.props.componentName ||'unknown'});// 根据错误类型决定降级策略if(this.props.critical){// 关键组件,整个页面降级this.setState({fallbackType:'fullPage'});}else{// 非关键组件,只降级这个组件this.setState({fallbackType:'component'});}}render(){if(this.state.hasError){// 根据fallbackType显示不同的降级UIif(this.state.fallbackType ==='fullPage'){return(<div className="error-page"><h1>页面出了点小问题</h1><p>我们的技术团队已经收到通知,正在紧急修复</p><button onClick={()=> window.location.reload()}> 刷新试试 </button>{/* 甚至可以提供"简化版"入口 */}<a href="/lite-version">访问简化版</a></div>);}// 组件级降级return(<div className="component-error-fallback"><p>这部分内容加载失败</p><button onClick={()=>this.setState({hasError:false})}> 重试 </button></div>);}returnthis.props.children;}}// 使用:给关键页面包一层functionProductPage(){return(<ErrorBoundary critical componentName="ProductPage"><ProductDetail /><RelatedProducts /></ErrorBoundary>);}// 给非关键组件单独包functionSidebar(){return(<ErrorBoundary componentName="Sidebar"><Recommendations /></ErrorBoundary>);}

对于非React项目,可以用类似的思路:

// Vue3的错误处理 app.config.errorHandler=(err, instance, info)=>{ console.error('Vue Error:', err);// 上报reportError({error: err,component: instance?.$options?.name ||'anonymous',info: info // 比如 "render function"});// 根据严重程度决定要不要toast提示用户if(isCriticalError(err)){showGlobalError('页面出现严重错误,建议刷新');}};// 或者更细粒度的,在组件内exportdefault{name:'SafeComponent',methods:{riskyOperation(){try{// 可能出错的操作}catch(e){// 捕获并处理,不让错误冒泡this.handleError(e);}}},errorCaptured(err, instance, info){// 捕获子组件错误 console.log('子组件出错:', err);returnfalse;// 返回false阻止错误继续传播}};

配合CI/CD,把低级错误扼杀在摇篮里

监控是最后一道防线,更好的办法是在代码提交前就发现问题。配合CI/CD流水线,可以自动跑异常模拟测试。

// Cypress E2E测试示例,模拟各种异常情况describe('异常场景测试',()=>{it('应该优雅处理API 500错误',()=>{// 拦截API请求,强制返回500 cy.intercept('GET','/api/user/profile',{statusCode:500,body:{error:'Internal Server Error'}}).as('getProfile'); cy.visit('/profile'); cy.wait('@getProfile');// 验证页面没有白屏,显示了错误提示 cy.get('[data-testid="error-toast"]').should('be.visible'); cy.get('[data-testid="retry-button"]').should('be.enabled');});it('应该处理网络超时',()=>{// 让请求永远pending,模拟超时 cy.intercept('GET','/api/slow-endpoint',(req)=>{// 不回复,让请求超时}).as('slowRequest'); cy.visit('/slow-page');// 等待超时时间(比如5秒) cy.wait(5000);// 验证显示了超时提示 cy.contains('请求超时').should('be.visible');});it('应该处理JS运行时错误',()=>{// 故意制造一个JS错误 cy.visit('/test-page'); cy.window().then((win)=>{ win.eval('setTimeout(() => { throw new Error("Test error"); }, 100)');});// 验证错误边界捕获了错误,页面没有崩溃 cy.get('body').should('not.have.class','white-screen'); cy.get('[data-testid="error-boundary"]').should('be.visible');});it('应该处理资源加载失败',()=>{// 拦截图片请求,返回404 cy.intercept('GET','**/*.jpg',{statusCode:404}).as('image404'); cy.visit('/gallery'); cy.wait('@image404');// 验证显示了兜底图或者错误占位符 cy.get('img').each(($img)=>{// 检查onerror是否被触发,图片是否被替换为兜底图 cy.wrap($img).should('have.attr','src').and('include','fallback');});});});// 在CI中跑这些测试// .github/workflows/test.yml/* name: E2E Tests on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Cypress run uses: cypress-io/github-action@v4 with: start: npm start wait-on: 'http://localhost:3000' */

这些自动化测试能帮你发现"明显会崩"的场景。虽然不能覆盖所有错误,但至少能保证核心流程在异常情况下不挂。

遇到那种"在我本地明明是好的"玄学问题咋整

最烦人的就是这种玄学问题。本地好好的,一上线就崩;测试环境没问题,生产环境就报错。这时候怎么排查?

复现不了是最耍流氓的,教你几招顺藤摸瓜

首先,得有足够的上下文。用户ID、时间戳、浏览器版本、操作系统,这些基础信息必须有。

// 增强版错误上报,带上丰富的上下文functiongetRichContext(){return{// 用户标识(脱敏)userId:getUserId(),// 或者匿名IDsessionId:getSessionId(),// 时间信息timestamp: Date.now(),timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,// 浏览器环境userAgent: navigator.userAgent,viewport:{width: window.innerWidth,height: window.innerHeight },screen:{width: screen.width,height: screen.height,colorDepth: screen.colorDepth },devicePixelRatio: window.devicePixelRatio,// 网络状况connection: navigator.connection ?{effectiveType: navigator.connection.effectiveType,downlink: navigator.connection.downlink,rtt: navigator.connection.rtt,saveData: navigator.connection.saveData // 用户是否开启了省流量模式}:null,online: navigator.onLine,// 页面状态url: window.location.href,referrer: document.referrer,visibilityState: document.visibilityState,// 报错时页面是否在前台// 性能指标(如果可用)performance:{// 页面加载时间loadTime: performance.timing ? performance.timing.loadEventEnd - performance.timing.navigationStart :null,// DOM内容加载时间domContentLoaded: performance.timing ? performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart :null,// 内存使用(Chrome only)memory: performance.memory ?{usedJSHeapSize: performance.memory.usedJSHeapSize,totalJSHeapSize: performance.memory.totalJSHeapSize,jsHeapSizeLimit: performance.memory.jsHeapSizeLimit }:null},// 已加载的资源(用来排查资源加载顺序问题)resources: performance.getEntriesByType ? performance.getEntriesByType('resource').slice(-20)// 最近20个资源.map(r=>({name: r.name,duration: r.duration,initiatorType: r.initiatorType })):[],// 全局状态(如果你用Redux/Vuex)storeState: getReduxState ?sanitizeState(getReduxState()):null,// 最近的console日志(需要提前劫持console方法)recentLogs:getRecentConsoleLogs()};}// 劫持console,记录最近的日志const recentLogs =[];constMAX_LOGS=50;['log','warn','error','info'].forEach(method=>{const original = console[method]; console[method]=function(...args){ recentLogs.push({ method,args: args.map(arg=>{// 简化序列化,避免循环引用try{returntypeof arg ==='object'?JSON.stringify(arg).substring(0,200):String(arg);}catch(e){return'[unserializable]';}}),timestamp: Date.now()});if(recentLogs.length >MAX_LOGS){ recentLogs.shift();}returnoriginal.apply(this, args);};});functiongetRecentConsoleLogs(){return recentLogs;}// 上报时带上这些上下文 window.addEventListener('error',(e)=>{reportError({error:{message: e.message,stack: e.error?.stack },context:getRichContext()});});

有了这些信息,你可以按用户ID查日志,看他在报错前都干了啥。也可以按时间戳聚合,看是不是某个时间点集中爆发(可能是CDN节点问题、服务器发布问题)。

偶发的内存泄漏,怎么像抓鬼一样揪出来

内存泄漏最难搞,因为它不会立即报错,而是让页面越来越卡,最后崩溃。而且本地开发时通常不会长时间运行,很难发现。

// 内存监控工具classMemoryLeakDetector{constructor(){this.measurements =[];this.checkInterval =30000;// 30秒检查一次this.growthThreshold =1.5;// 内存增长超过50%就报警this.startMonitoring();}startMonitoring(){if(!performance.memory){ console.warn('当前浏览器不支持内存监控');return;}setInterval(()=>this.checkMemory(),this.checkInterval);}checkMemory(){const memory = performance.memory;const usedMB = memory.usedJSHeapSize /1048576;const totalMB = memory.totalJSHeapSize /1048576;this.measurements.push({timestamp: Date.now(),usedMB: usedMB,totalMB: totalMB,url: window.location.href // 记录当前页面,SPA可能切换路由});// 只保留最近20个测量点if(this.measurements.length >20){this.measurements.shift();}// 分析趋势this.analyzeTrend();}analyzeTrend(){if(this.measurements.length <5)return;// 计算最近5个点的平均增长率const recent =this.measurements.slice(-5);const first = recent[0].usedMB;const last = recent[4].usedMB;const growth = last / first;if(growth >this.growthThreshold){// 内存快速增长,可能泄漏this.reportLeak({growthFactor: growth,startMB: first,endMB: last,measurements: recent,// 尝试获取堆快照(需要Chrome DevTools协议,生产环境通常不行)// 但可以记录当前的DOM节点数作为参考domNodes: document.getElementsByTagName('*').length,eventListeners:this.getEventListenerCount()// 粗略估计});}}getEventListenerCount(){// 这个很难精确获取,但可以通过hack的方式估计// 实际生产环境建议用WeakRef和FinalizationRegistry来跟踪对象return'estimated';}reportLeak(details){reportError({type:'MEMORY_LEAK_DETECTED',severity:'warning',details: details,suggestion:'页面内存持续增长,可能存在泄漏,建议检查定时器、事件监听、DOM引用'});// 如果增长太夸张,提示用户刷新(避免崩溃)if(details.growthFactor >3){showToast('页面运行时间较长,建议刷新以获得最佳体验');}}}// 使用const leakDetector =newMemoryLeakDetector();

更专业的做法是用Chrome的Performance面板手动分析,但生产环境你没法让用户帮你开DevTools。所以这种自动监控是必要的补充。

第三方脚本挂了怎么办?沙箱隔离和超时熔断

现在的网站都依赖一堆第三方脚本:统计、广告、客服、支付……这些脚本如果挂了,可能拖慢甚至拖垮你的页面。

// 第三方脚本加载管理器classThirdPartyManager{constructor(){this.scripts =newMap();this.timeouts ={'analytics':5000,// 统计脚本5秒超时'chat-widget':10000,// 客服组件10秒'payment':15000// 支付脚本15秒,这个重要可以多等会儿};}asyncloadScript(name, url, options ={}){const timeout = options.timeout ||this.timeouts[name]||5000;returnnewPromise((resolve, reject)=>{const script = document.createElement('script'); script.src = url; script.async =true;// 超时熔断const timer =setTimeout(()=>{ script.remove();this.scripts.set(name,{status:'timeout',loadedAt:null});reject(newError(`Script ${name} load timeout after ${timeout}ms`));}, timeout); script.onload=()=>{clearTimeout(timer);this.scripts.set(name,{status:'loaded',loadedAt: Date.now()});resolve(script);}; script.onerror=()=>{clearTimeout(timer);this.scripts.set(name,{status:'error',loadedAt:null});reject(newError(`Script ${name} load failed`));};// 沙箱隔离:用iframe加载特别危险的脚本(可选,看安全需求)if(options.sandbox){this.loadInSandbox(name, url, resolve, reject);}else{ document.head.appendChild(script);}}).catch(err=>{// 第三方脚本失败不影响主流程 console.warn(`第三方脚本 ${name} 加载失败:`, err);reportError({type:'THIRD_PARTY_ERROR',scriptName: name,error: err.message });returnnull;// 返回null表示加载失败,但业务代码继续跑});}loadInSandbox(name, url, resolve, reject){// 创建隐藏iframe作为沙箱const iframe = document.createElement('iframe'); iframe.style.display ='none'; iframe.sandbox ='allow-scripts allow-same-origin';// 限制权限 iframe.onload=()=>{const iframeDoc = iframe.contentDocument;const script = iframeDoc.createElement('script'); script.src = url; script.onload=()=>resolve(iframe); script.onerror=()=>reject(newError('Sandboxed script failed')); iframeDoc.head.appendChild(script);}; document.body.appendChild(iframe);}// 检查某个脚本是否可用isAvailable(name){const script =this.scripts.get(name);return script && script.status ==='loaded';}}// 使用示例const tpm =newThirdPartyManager();// 加载非关键脚本,失败也无所谓 tpm.loadScript('analytics','https://analytics.example.com/track.js',{timeout:3000// 统计脚本3秒没加载完就放弃,别拖慢页面});// 加载关键脚本(比如支付),但也要有超时 tpm.loadScript('payment','https://payment.example.com/sdk.js',{timeout:10000}).then(()=>{// 初始化支付组件initPayment();}).catch(()=>{// 支付脚本加载失败,显示降级提示showPaymentFallback();});// 业务代码里检查可用性functiontrackEvent(event){if(tpm.isAvailable('analytics')){ window.analytics.track(event);// 假设第三方脚本全局暴露了方法}else{// 脚本不可用,存到本地队列,等它加载了再补发,或者直接丢弃 console.log('Analytics not ready, dropping event:', event);}}

关键点:超时熔断(别无限等)、失败降级(别阻塞主流程)、可选加载(非关键脚本可以没有)。这样第三方脚本挂了,你的页面还能正常跑。

面对用户截图里那个模糊不清的报错,怎么引导他们提供有价值的信息

有时候用户反馈问题,就给你一张模糊的截图,上面半个错误信息。这时候怎么引导他们提供有用的信息?

首先,你的错误页面本身要设计得能提供信息:

<!-- 友好的错误页面,带诊断信息 --><divclass="error-container"><h1>哎呀,出错了</h1><p>我们已经记录了这个问题,技术团队正在处理</p><!-- 给高级用户的诊断信息 --><detailsclass="diagnostics"><summary>技术详情(点击展开,反馈给客服时有用)</summary><divclass="error-code"><p>错误ID: <spanid="error-id">ERR-20240315-ABC123</span></p><p>时间: <spanid="error-time">2024-03-15 14:32:01</span></p><p>页面: <spanid="error-page">/checkout/payment</span></p><buttononclick="copyDiagnostics()">复制诊断信息</button></div></details><divclass="actions"><buttononclick="location.reload()">刷新页面</button><buttononclick="contactSupport()">联系客服</button></div></div><script>// 生成错误ID,方便后续追踪functiongenerateErrorId(){return'ERR-'+newDate().toISOString().slice(0,10).replace(/-/g,'')+'-'+ Math.random().toString(36).substring(2,8).toUpperCase();}// 把诊断信息复制到剪贴板,方便用户粘贴给客服functioncopyDiagnostics(){const info ={errorId: document.getElementById('error-id').textContent,time: document.getElementById('error-time').textContent,page: document.getElementById('error-page').textContent,userAgent: navigator.userAgent,// 还可以加更多}; navigator.clipboard.writeText(JSON.stringify(info,null,2)).then(()=>alert('诊断信息已复制,请粘贴给客服')).catch(()=>alert('复制失败,请手动截图'));}// 上报时带上这个errorIdreportError({errorId: document.getElementById('error-id').textContent,// ... 其他信息});</script>

其次,客服话术要培训好。别问"你遇到什么问题"这种开放式问题,要问"错误ID是多少"、“报错时你在哪个页面”、“有没有看到红色的错误提示”。

几个让你少加班的野路子技巧

说了这么多监控和排查的,最好的错误处理是不出错。这里分享几个从源头减少错误的野路子。

TypeScript把类型检查做到极致

别只是把TS当JS用,要利用它的严格模式。

// tsconfig.json 严格配置{"compilerOptions":{"strict":true,// 开启所有严格选项"noImplicitAny":true,// 禁止隐式any"strictNullChecks":true,// 严格检查null/undefined"noUncheckedIndexedAccess":true,// 索引访问可能undefined"exactOptionalPropertyTypes":true,// 区分undefined和可选属性"noImplicitReturns":true,// 函数所有分支必须有返回值"noFallthroughCasesInSwitch":true,// switch case必须有break"noUncheckedSideEffectImports":true// 检查副作用导入}}// 实际例子:严格类型能避免一堆运行时错误// 不好的写法:隐式any,后面调用时不知道有没有这个方法functionprocessData(data){return data.map(item => item.name);// 如果data不是数组,这里就炸}// 好的写法:明确类型,编译时就发现问题interfaceUser{ id:number; name:string; email:string;}functionprocessData(data: User[]):string[]{// TS会保证data是数组,item有name属性return data.map(item => item.name);}// 更严格的:处理边界情况functionsafeProcessData(data: User[]|undefined|null):string[]{// 开启strictNullChecks后,必须处理undefined/nullif(!data){return[];// 或者抛出错误,但至少显式处理了}// 开启noUncheckedIndexedAccess后,连数组索引都要检查const first = data[0];if(first){// 必须检查,因为data[0]可能是undefinedconsole.log(first.name);}return data.map(item => item.name);}// 用 branded type 防止ID混淆typeUserId=string&{ __brand:'UserId'};typeOrderId=string&{ __brand:'OrderId'};functiongetUser(id: UserId){...}functiongetOrder(id: OrderId){...}const userId ='123'as UserId;const orderId ='456'as OrderId;getUser(userId);// OKgetUser(orderId);// 编译错误!不能混用

TS的学习曲线是陡,但用好了能消灭一半的低级错误。特别是严格null检查,能把"Cannot read property of undefined"这种错误在编译期就干掉。

单元测试别偷懒,边界条件才是重灾区

写测试别只测"正常情况",要测边界:空数组、undefined、极大值、网络超时。

// Jest测试示例,重点测边界import{ processOrder, calculateDiscount }from'./order';describe('processOrder',()=>{// 正常情况it('应该正确处理正常订单',()=>{const order ={items:[{price:100,quantity:2}],coupon:'SAVE20'};expect(processOrder(order)).toEqual({total:200,discount:40,final:160});});// 边界:空数组it('应该处理空购物车',()=>{const order ={items:[]};expect(processOrder(order)).toEqual({total:0,discount:0,final:0});});// 边界:undefined输入it('应该处理undefined输入',()=>{expect(processOrder(undefined)).toThrow('Invalid order data');// 或者返回默认值,看业务需求});// 边界:价格或数量为0/负数it('应该拒绝无效的价格',()=>{const order ={items:[{price:-100,quantity:1}]};expect(()=>processOrder(order)).toThrow('Invalid price');});// 边界:极大值(防止溢出)it('应该处理极大金额',()=>{const order ={items:[{price: Number.MAX_SAFE_INTEGER,quantity:2}]};// 应该抛出溢出错误或者用BigInt处理expect(()=>processOrder(order)).toThrow('Amount overflow');});// 异步错误it('应该处理API超时',async()=>{// 模拟超时 jest.useFakeTimers();const promise =processOrderWithAPI({items:[]}); jest.advanceTimersByTime(10000);// 快进10秒awaitexpect(promise).rejects.toThrow('Request timeout'); jest.useRealTimers();});});// 用属性测试(Property-based testing)发现边界情况// 用fast-check库import fc from'fast-check';describe('calculateDiscount',()=>{it('折扣不应该超过原价',()=>{ fc.assert( fc.property( fc.integer({min:0,max:100000}),// 随机价格 fc.float({min:0,max:1}),// 随机折扣率(price, rate)=>{const discount =calculateDiscount(price, rate);return discount <= price && discount >=0;}));});});

测试覆盖率要追求,但更重要的是覆盖场景。100%覆盖率但只测了正常路径,不如80%覆盖率但边界都测到了。

团队规矩:谁搞出P0事故谁请喝奶茶

技术之外,管理手段也很重要。我们团队有个规矩:谁提交的代码导致线上P0事故(核心功能挂了、大量用户受影响),谁请全组喝奶茶。

这招比Code Review好使多了。Code Review大家还容易敷衍,“LGTM”(Looks Good To Me)就过了。但想到可能要请20个人喝奶茶, Review的时候眼睛都瞪大了,每一行都仔细看。

而且出了事故不惩罚,只是请喝奶茶,氛围比较轻松。大家复盘的时候也不会互相甩锅,而是真的想怎么避免下次再犯。毕竟谁都有手滑的时候,但同一个错误犯两次就真说不过去了。

我们还配合一个"事故日记",每次P0事故都记录下来:什么时候发生的、什么原因、怎么修复、怎么预防。新员工入职先看这个日记,了解团队的"黑历史",也学习排查问题的思路。

下次再看到红色报错别心慌,甚至想笑

说了这么多,其实想传达一个心态:bug是写出来的,不是测出来的。只要代码在变,bug就在路上。重要的不是追求零bug(那不可能),而是建立一套体系,让bug的影响可控、可快速修复、可从中学习。

我现在看到Sentry的报警邮件,第一反应已经不是"卧槽又出事了",而是"让我看看这次是什么新鲜玩意儿"。毕竟处理过的错误类型越多,排查新问题的速度就越快。而且很多错误看着吓人,其实就是个边界条件没处理好,改两行代码就完事。

记住,没有完美的系统,只有永远在修Bug的路上狂奔的我们。今晚能不能睡个安稳觉,就看你这波监控搭得稳不稳、降级方案设计得合理不合理、团队配合默不默契了。

不过说真的,自从我们把监控体系搭完善之后,凌晨三点的电话确实少多了。现在偶尔响一次,我甚至有点怀念那种肾上腺素飙升的感觉——毕竟,这才是前端工程师的"浪漫"啊(不是)。

好了,该说的都说了,去检查下你的Sentry配置吧,别等会儿电话真响了。☕

在这里插入图片描述

Read more

彻底弄懂Web Storage与Cookie:从机制到应用的全方位对比

彻底弄懂Web Storage与Cookie:从机制到应用的全方位对比

彻底弄懂Web Storage与Cookie:从机制到应用的全方位对比 * 引言 * 1. 什么是Cookie? * Cookie 的核心特性: * Cookie 流程图 * 2. 什么是Web Storage? * Web Storage 流程图 * 3. 核心区别深度解析(对标选项逐一解读) * a. 存储容量 * b. 网络流量(带宽浪费) * c. 作用域与跨域 * d. API 易用性 * e. 设计初衷 * f. 历史兼容与封装 * 4. 总结对比表 * 5. 应用场景建议 * 什么时候选 Cookie? * 什么时候选 Web Storage? * 6. 结语 🌺The Begin🌺点点关注,收藏不迷路🌺 引言 在前端开发中,

用 Web 技术构建跨平台应用:Capacitor 完全指南

开篇的碎碎念:自从发现了capacitor,我就一直在用!不用像 Flutter 那样配置一堆环境,也不用学习新的 UI 写法,直接用前端三件套就能打包原生 App。简直是降维打击!那么接下来就开始capacitor的学习吧!!! 目录 速成版 1. 什么是 Capacitor?         1.1 定义与背景         1.2 为什么叫 Capacitor?         1.3 发展历程         1.4 核心理念 2. Capacitor vs 其他跨平台方案         2.1 横评对比         2.2 为什么选择 Capacitor? 3. Capacitor 核心架构         3.1 架构图         3.2

Altium Designer导入DXF/DWG文件常见问题与实战解决方案

1. 导入失败:版本兼容性与文件损坏问题 我在使用Altium Designer导入DXF/DWG文件时,最常遇到的就是导入失败的情况。软件弹窗提示"由于文件版本不兼容或文件损坏而无法打开",这种情况特别让人头疼,尤其是赶项目的时候。 根本原因在于CAD和Altium Designer之间的版本鸿沟。AutoCAD每年都会推出新版本,而Altium Designer的更新节奏跟不上,这就导致了高版本的DWG文件在AD中无法识别。我实测过,AD 16.1版本最高只能兼容到AutoCAD 2013格式,再新的版本就会报错。 解决方案其实很简单:在AutoCAD中另存为低版本格式。我建议保存为2004或2007版本的DXF文件,这两个版本在兼容性方面表现最稳定。具体操作:在AutoCAD中打开文件后,点击"另存为",在文件类型中选择"AutoCAD 2004/LT2004 DXF (*.dxf)"。这个办法我用了十年,几乎能解决90%的导入失败问题。 如果保存为低版本后仍然无法导入,可能是文件本身损坏了。这时候可以在AutoCAD中使用RECOVER命令修复文件,然后再重新保存为低版

快学快用系列:一文学会java后端WebApi开发

快学快用系列:一文学会java后端WebApi开发

文章目录 * 第一部分:Web API开发基础概念 * 1.1 什么是Web API * 1.2 RESTful API设计原则 * 第二部分:开发环境搭建 * 2.1 环境要求 * 2.2 创建Spring Boot项目 * 2.3 配置文件 * 第三部分:项目架构设计 * 3.1 分层架构 * 3.2 包结构设计 * 第四部分:数据模型设计 * 4.1 实体类设计 * 4.2 DTO设计 * 第五部分:数据访问层实现 * 5.1 Repository接口 * 5.2 自定义Repository实现 * 第六部分:业务逻辑层实现