ผมสร้างระบบรายงานข้อผิดพลาดฝั่งไคลเอนต์ขึ้นมา เพราะอยากเห็นปัญหาที่เกิดกับผู้ใช้จริง โดยเฉพาะปัญหาที่ทำซ้ำในเครื่องตัวเองได้ยาก ตัวรายงานก็ทำงานตรงตามที่สั่ง: จับความผิดพลาดแล้วส่งมาให้ผม
ปัญหาคือแทบทุกอย่างดูสำคัญเท่ากันหมด
สคริปต์วิเคราะห์จากบริการภายนอกโหลดไม่สำเร็จ? แจ้งเตือนสีแดง สคริปต์โฆษณาถูกบล็อก? แจ้งเตือนสีแดง โปรแกรมรวบรวมข้อมูลโหลด Google Analytics ไม่ได้? แจ้งเตือนสีแดง วิดีโอตัวอย่างเรียก play() แล้วหยุดก่อน Promise จะเสร็จ? แจ้งเตือนสีแดง มี Script error. ที่ไม่บอกแหล่งที่มาหรือสแตกที่ใช้ได้? ก็ยังแจ้งเตือนสีแดง
ในกระแสเดียวกันยังมีความผิดพลาดที่สมควรแก้จริง ๆ: URL ของแอปผมเองถูกประกอบผิดจนเป็น https://example.comhttps://example.com/... และเบราว์เซอร์โหลดไฟล์ Next.js ของแอปใต้ /_next/static/chunks/... ไม่สำเร็จ
ระบบเก็บข้อมูลทำงาน แต่ชั้นมอนิเตอร์ไม่ได้ทำงานในความหมายที่ผมต้องการ
ความต่างนี้เปลี่ยนวิธีที่ผมมองการสังเกตการณ์ฝั่งฟรอนต์เอนด์ เหตุการณ์ข้อผิดพลาดเป็นเพียงหลักฐานว่ามีบางอย่างเกิดขึ้น มันยังไม่ใช่การวินิจฉัย ระดับความรุนแรง หรือเหตุขัดข้อง
ความผิดพลาดแรกคือการตีความคำว่า “ข้อผิดพลาด” ว่า “ด่วน” โดยอัตโนมัติ
กรอบความคิดแรกของผมประมาณนี้:
browser รายงาน error
↓
ส่ง CLIENT ERROR
↓
developer ต้องรีบดู
แต่โมเดลนี้เอาคำถามหลายอย่างมารวมกัน: เป็นโค้ดของผมหรือไม่? เส้นทางที่ผู้ใช้กำลังเปิดพังจริงหรือเปล่า? เป็นการยกเลิกที่คาดไว้หรือไม่? เบราว์เซอร์มีข้อมูลพอระบุแหล่งที่มาหรือไม่? แอปกู้คืนแล้วหรือยัง? ข้อความสิบอันคือสิบเหตุขัดข้อง หรือสิบอาการของเหตุเดียว?
ถ้ายังตอบคำถามเหล่านี้ไม่ได้ เหตุการณ์ก็ไม่ควรถูกยกระดับเป็นการแจ้งเตือนโดยอัตโนมัติ
ในการตรวจสอบช่วงแรกครั้งหนึ่ง ผมมีข้อความราวสิบแปดอัน ส่วนใหญ่เป็นสัญญาณรบกวนจากบริการภายนอกหรือวงจรชีวิตปกติ มีเพียงสองกรณีที่ต่างชัดเจน: URL ของแอปที่ผิดรูปแบบเป็นบั๊กแน่นอน และชังก์ JavaScript ของแอปที่โหลดไม่สำเร็จอาจทำให้หน้าไม่มีโค้ดที่จำเป็น แต่ตัวรายงานกลับให้ความเร่งด่วนแทบเท่ากับกรณีโฆษณาถูกบล็อก
ตรงนั้นเองที่ผมเลิกมอง “เก็บข้อผิดพลาดเบราว์เซอร์ทั้งหมด” กับ “สร้างระบบมอนิเตอร์ในโปรดักชัน” ว่าเป็นเรื่องเดียวกัน ชั้นเก็บข้อมูลมีหน้าที่รักษาหลักฐาน ส่วนชั้นมอนิเตอร์ต้องเปลี่ยนหลักฐานให้เป็นการตัดสินใจ
เบราว์เซอร์ไม่มีช่องทางข้อผิดพลาดเดียวที่มีความหมายเหมือนกันสำหรับทุกความล้มเหลว
ความล้มเหลวแต่ละชนิดฝั่งไคลเอนต์ไม่ได้มาพร้อมความหมายเดียวกัน
เหตุการณ์ error บน window ใช้กับข้อผิดพลาดแบบซิงโครนัสของสคริปต์ และยังเกี่ยวข้องกับการโหลดทรัพยากรล้มเหลวด้วย ส่วน Promise ที่ถูกปฏิเสธโดยไม่มีตัวจัดการจะไปอีกช่องทางหนึ่ง โดยเบราว์เซอร์ส่ง unhandledrejection ออกมา องค์ประกอบที่โหลดสคริปต์ รูปภาพ หรือสื่อก็ส่ง error ของตัวเองได้ และ React/Next.js ยังเพิ่มสัญญาณจากขอบเขตดักข้อผิดพลาดของเฟรมเวิร์กอีกชั้น
window.error
→ synchronous script error อาจหลุดออกมา
unhandledrejection
→ rejected Promise ยังไม่ได้รับการ handle ในขณะนั้น
element error
→ resource โหลดหรือใช้งานไม่ได้
framework boundary
→ rendering หรือ execution ไปถึง error boundary
ฮุกส่วนกลางเห็นอาการตรงขอบของระบบ ไม่ได้แปลว่าจะรู้สายเหตุและผลทั้งหมด
เมื่อยอมรับความต่างนี้ ผมก็เลิกรีบแปลงทุกเหตุการณ์ให้เป็น Error แบบเดียวและระดับความรุนแรงเดียวกัน
การดูว่าใครเป็นเจ้าของคือการกรองขั้นแรกที่มีประโยชน์จริง
ตัวกรองแรกที่ช่วยได้จริงคือดูว่าโค้ดหรือทรัพยากรที่ล้มเหลวเป็นของใคร
การโหลด /_next/static/chunks/app/... ไม่สำเร็จไม่เหมือน SDK โฆษณาบนออริจินอื่น URL ที่ตัวสร้างของผมประกอบผิดก็ไม่เหมือนคำขอวิเคราะห์ที่ถูกบล็อก และข้อผิดพลาดจากส่วนขยายเบราว์เซอร์ก็เป็นอีกหมวดหนึ่ง
- ตัวแอปเอง: JavaScript, CSS, API, สื่อ และ URL ที่โค้ดของผมสร้าง;
- เฟรมเวิร์กและรันไทม์: Next.js หรือ React ที่อยู่ในเส้นทางการทำงานของแอป;
- การเชื่อมต่อภายนอก: การวิเคราะห์ โฆษณา วิดเจ็ต และ SDK ภายนอก;
- สภาพแวดล้อม: ส่วนขยายเบราว์เซอร์ โปรแกรมรวบรวมข้อมูล สถานะเครือข่าย เครื่องมือความเป็นส่วนตัว และพฤติกรรมเฉพาะเบราว์เซอร์
นี่ไม่ได้หมายความว่าบริการภายนอกไม่สำคัญ ผู้ให้บริการชำระเงินหรือยืนยันตัวตนอาจสำคัญมาก และความล้มเหลวของโฆษณาอาจกระทบรายได้ แต่สถานะของการเชื่อมต่อภายนอกไม่ควรถูกตีความเป็นแอปล่มโดยอัตโนมัติ
ถ้าส่งทุกอย่างเข้าช่องแจ้งเตือนด่วนเดียวกัน ช่องนั้นก็จะหมดความหมาย
ความล้มเหลวของแอปสองกรณีสอนผมว่า “แก้แล้วเกิดผล” หมายถึงอะไร
กรณี URL ผิดนั้นตรงไปตรงมา:
https://example.comhttps://example.com/resource
กรณีนี้ไม่ต้องเดาเรื่อง AdBlock, VPN หรือนโยบายเบราว์เซอร์ URL เองผิดอยู่แล้ว ที่ไหนสักแห่งโค้ดกำลังเอาออริจินไปต่อหน้าค่าที่เป็น URL แบบสมบูรณ์อยู่แล้ว
เหตุการณ์นี้ลงมือแก้ได้ทันที เพราะหลักฐานเฉพาะเจาะจง ทรัพยากรเป็นของผม และปัญหาชี้ไปยังเส้นทางโค้ดที่ผมควบคุม
แต่ชังก์ของ Next.js ที่โหลดไม่สำเร็จเป็นอีกเรื่อง:
Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js
มันก็เป็นทรัพยากรของแอปและอาจทำให้หน้าพัง แต่เหตุการณ์นั้นไม่ได้พิสูจน์สาเหตุ ไคลเอนต์เก่าอาจขอไฟล์จากการดีพลอยก่อนหน้า คำขออาจหมดเวลา รีเวิร์สพร็อกซีหรือ CDN อาจมีปัญหา การเชื่อมต่ออาจหาย หรือไฟล์อาจไม่มีจริง ๆ
ดังนั้นคำตอบที่ถูกต้องไม่ใช่ “ผมรู้สาเหตุแล้ว” แต่คือ “ข้อผิดพลาดกลุ่มนี้สำคัญและต้องการบริบทเพิ่ม”
ความรุนแรงอาจสูงได้ แม้ความมั่นใจในสาเหตุจะต่ำ
Script error. เป็นเบาะแส ไม่ใช่สแตกเทรซ
Error: Script error.
filename: unknown
line: 0
column: 0
ดูร้ายแรง แต่แทบไม่มีข้อมูล
เบราว์เซอร์จำกัดรายละเอียดของข้อผิดพลาดสคริปต์ข้ามออริจินโดยตั้งใจ MDN อธิบายว่า หากไม่มีการตั้งค่า CORS ที่เหมาะสม window.onerror จะเห็นข้อมูลจำกัด และพฤติกรรม crossorigin ของ <script> มีผลโดยตรงต่อการเข้าถึงรายละเอียดข้อผิดพลาดเต็มรูปแบบ
ดังนั้นผมจะไม่ตีความ Script error. ที่คลุมเครือว่า “แอปผมล่ม” โดยอัตโนมัติ แหล่งที่มาอาจเป็นโค้ดของผม โค้ดภายนอก โค้ดที่ถูกฉีดเข้ามา หรือข้อผิดพลาดที่เบราว์เซอร์เปิดเผยรายละเอียดไม่ได้เพราะข้อจำกัดข้ามออริจิน
ผมเก็บเหตุการณ์ไว้และเชื่อมกับหน้า บิลด์ เบราว์เซอร์ และเหตุการณ์ใกล้เคียง แต่ 0:0 เพียงครั้งเดียวไม่พอสำหรับการแจ้งเตือนด่วน หากรูปแบบเดียวกันเริ่มกระจุกตัวรอบรีลีสหรือเส้นทางใดเส้นทางหนึ่ง ลำดับความสำคัญจึงค่อยเปลี่ยน
ไม่รู้สาเหตุไม่ได้แปลว่าปลอดภัย และก็ไม่ได้แปลว่าร้ายแรง
AbortError อาจเป็นข้อผิดพลาดจริง และยังเป็นส่วนปกติของวงจรชีวิตได้
วิดีโอตัวอย่างเป็นตัวอย่างที่ชัดที่สุดของเรื่องนี้:
AbortError:
The play() request was interrupted by a call to pause()
HTMLMediaElement.play() คืน Promise และ Promise นั้นอาจถูกปฏิเสธได้ การทำงานในวงจรชีวิตของสื่อยังอาจยกเลิกการเล่นที่กำลังรออยู่โดยตั้งใจ; MDN ระบุชัดว่า load() จะยุติ Promise ของ play() ที่ยังค้างด้วย AbortError
ในตารางพรีวิว เรื่องนี้เกิดได้ง่ายโดยที่ผู้ใช้ไม่เห็นอะไรเสียเลย รายการเข้ามาใน viewport โค้ดเรียก play(); ผู้ใช้เลื่อนต่อ รายการออกจาก viewport; ก่อนเริ่มเล่นเสร็จ โค้ดก็หยุดหรือเปลี่ยนสื่อ
การปฏิเสธ Promise เป็นเรื่องจริง แต่เหตุขัดข้องสำหรับผู้ใช้อาจไม่มีอยู่เลย
วิธีแก้ที่ถูกมักอยู่ใกล้จุดเรียก: จัดการ Promise ตรงนั้นและแยกการยกเลิกที่คาดไว้จากความล้มเหลวของการเล่นจริง unhandledrejection ควรเป็นตาข่ายนิรภัย ไม่ใช่ที่แรกที่เราเพิ่งเริ่มทำความเข้าใจวงจรชีวิตปกติของคอมโพเนนต์
ความล้มเหลวจากบริการภายนอกต้องมีโมเดลสุขภาพของตัวเอง
ล็อกช่วงแรกของผมมีความล้มเหลวจากโดเมนวิเคราะห์และโฆษณาจำนวนมาก บางส่วนมาจากเบราว์เซอร์ที่เน้นความเป็นส่วนตัว บางส่วนมาจากโปรแกรมรวบรวมข้อมูล ตัวอย่างที่แทบไม่มีค่าในฐานะการแจ้งเตือนด่วนคือ โปรแกรมรวบรวมข้อมูลโหลด Google Analytics ไม่ได้
สิ่งนี้พิสูจน์ได้แค่ว่าคำขอเครือข่ายหนึ่งรายการล้มเหลว แทบไม่ได้บอกเลยว่าผู้ใช้จริงยังใช้แอปได้หรือไม่
ปัญหาไม่ใช่การเก็บเหตุการณ์นั้น ปัญหาคือผมเอามันไปอยู่ในกระแสเหตุขัดข้องเดียวกับชังก์ JavaScript ของแอปที่โหลดไม่สำเร็จ
- แอปพังสำหรับผู้ใช้จริงหรือไม่?
- การเชื่อมต่อภายนอกทำงานปกติหรือไม่?
สคริปต์โฆษณาที่ถูกบล็อกอาจไปอยู่ในเมตริกการส่งโฆษณา ความล้มเหลวของระบบวิเคราะห์อาจใช้วัดความครอบคลุมของการวิเคราะห์ แต่ไม่ควรปลุกผมด้วยคำว่า “ฟรอนต์เอนด์ล่ม” เว้นแต่มีหลักฐานว่าฟังก์ชันหลักพึ่งพามันจริง ๆ
การแยกยังทำให้ปัญหาภายนอกเห็นง่ายขึ้น เพราะรวมตามผู้ให้บริการ เบราว์เซอร์ และภูมิภาคได้ แทนที่จะเป็นสัญญาณรบกวนสีแดงแบบสุ่ม
navigator.onLine คือบริบท ไม่ใช่หลักฐานว่าปลายทางเข้าถึงได้
ต่อมาผมเริ่มบันทึกว่าเบราว์เซอร์คิดว่าตัวเองออนไลน์หรือไม่ ข้อมูลนี้มีประโยชน์ แต่ควรใช้เป็นเพียงเบาะแส
ผมมีเหตุการณ์ที่ลอจิกเสริมจัดเป็นความล้มเหลวของเครือข่าย ทั้งที่ในล็อกเดียวกันเขียนว่า:
Online: true
เรื่องนี้ไม่ขัดแย้งกัน MDN เตือนชัดว่า navigator.onLine อิงการคาดเดาของเบราว์เซอร์และระบบปฏิบัติการ เครื่องอาจต่อ LAN อยู่แต่เข้าถึงออริจินของผมไม่ได้ VPN, ไฟร์วอลล์, DNS และการล่มบางส่วนของเครือข่ายทำให้ภาพซับซ้อนขึ้นอีก
online === false
→ strong hint ว่าปัญหาอาจมาจาก environment
online === true
→ ไม่ได้พิสูจน์ว่า origin หรือ resource reachable
ความต่างเล็ก ๆ นี้ช่วยไม่ให้ระบบมอนิเตอร์เอาเบาะแสไปแต่งเป็นคำวินิจฉัยที่มั่นใจแต่ผิด
ความล้มเหลวต้นเหตุหนึ่งครั้งอาจสร้างหลายเหตุการณ์ในเบราว์เซอร์
หลังปรับการเก็บข้อมูล อีกปัญหาหนึ่งก็ชัดขึ้น: เหตุขัดข้องเดียวอาจสร้างหลายข้อความ
ชังก์ JavaScript อาจส่ง resource.error ก่อน จากนั้นตัวโหลดโมดูลโยน ChunkLoadError, React หรือ Next.js ข้ามขอบเขตดักข้อผิดพลาด และลอจิกกู้คืนกำหนดการรีโหลด หากแต่ละชั้นส่งการแจ้งเตือนแยกกัน การกระทำของผู้ใช้ครั้งเดียวจะดูเหมือนหลายความล้มเหลวในโปรดักชัน
ข้อความห้าอันให้ความรู้สึกเหมือนผู้ใช้ห้าคนได้รับผลกระทบ ทั้งที่อาจมาจากเซสชันเดียวและทรัพยากรเดียว
การตัดรายการซ้ำด้วยข้อความอย่างเดียวไม่พอ ต้องเชื่อมเหตุการณ์เป็นเหตุขัดข้องเดียว:
session
+ time window สั้น
+ normalized error class
+ first-party resource
+ client build
+ route
ผมยังเก็บเหตุการณ์ดิบไว้ แต่สิ่งที่ส่งให้คนควรเป็นตัวแทนเหตุขัดข้องที่ให้สัญญาณชัดที่สุด ถ้าขอบเขตดักข้อผิดพลาดมีสแตกจากโค้ดของผมและ URL ชังก์ที่แน่นอนอยู่แล้ว ข้อผิดพลาดทรัพยากรทั่วไปที่มาก่อนก็ไม่ต้องสร้างการแจ้งเตือนด่วนอันที่สอง
แจ้งเตือนที่เหตุขัดข้อง เก็บเหตุการณ์ไว้
บริบทรอบข้อผิดพลาดมีค่ามากกว่าข้อความข้อผิดพลาดเองในภายหลัง
เทเลเมทรีรุ่นหลังของผมมีโครงสร้างมากขึ้น:
clientBuildId
resource URL
resourceResponseStatus
resourceTransferSize
resourceDurationMs
serviceWorkerVersion
serviceWorkerController
serviceWorkerState
chunkRecoveryScheduled
online
stack / component stack
สำหรับสื่อ ผมยังเก็บรหัสข้อผิดพลาดของสื่อ สถานะ HTTP เมื่อสังเกตได้แยกต่างหาก Content-Type ที่ตอบกลับจริง และระบุว่าความล้มเหลวดูเป็น HTTP หรือการส่งผ่านเครือข่าย
ฟิลด์เหล่านี้ทำให้ถามสิ่งที่ข้อความข้อยกเว้นทั่วไปตอบไม่ได้: ปัญหาเริ่มจากบิลด์เดียวหรือไม่? เบราว์เซอร์ได้รับการตอบกลับ HTTP หรือไม่? Service Worker ควบคุมหน้าอยู่หรือเปล่า? ลอจิกกู้คืนทำงานแล้วหรือยัง? หลายเหตุการณ์ชี้ไปที่ทรัพยากรเดียวกันหรือไม่? เส้นทางปัจจุบันพังจริงหรือเปล่า?
Resource Timing API ให้ระยะเวลาของทรัพยากร ข้อมูลการส่ง และสถานะการตอบกลับในกรณีที่รองรับและอนุญาต แต่มีข้อจำกัด: ข้อมูลเวลาแบบข้ามออริจินถูกจำกัด ทรัพยากรที่มาจากแคชอาจมี transferSize: 0 และ responseStatus ไม่ได้มีทุกเบราว์เซอร์ ดังนั้น null และ 0 ต้องคงเป็นสถานะที่มีความหมาย ไม่ใช่ถูกแปลงเป็นความแน่นอนปลอม ๆ
ChunkLoadError เป็นอาการ ไม่ใช่เครื่องตรวจ 404
เหตุการณ์ภายหลังครั้งหนึ่งเปลี่ยนวิธีที่ผมตีความความล้มเหลวของชังก์
เบราว์เซอร์รายงาน ChunkLoadError สำหรับชังก์ layout ของ Next.js และเทเลเมทรีที่เพิ่มบริบทยังมี:
resourceResponseStatus: 200
resourceDurationMs: 170523
serviceWorkerState: activated
chunkRecoveryScheduled: true
เวลาที่บันทึกได้อยู่ราว 170 วินาที ไม่ว่าสาเหตุรากของเหตุการณ์นั้นจะเป็นอะไร ข้อมูลนี้ก็พอจะหักล้างกฎที่ง่ายเกินไปได้แล้ว:
ChunkLoadError === server ตอบ 404
ความล้มเหลวของชังก์อื่นไม่มีสถานะการตอบกลับที่มองเห็นได้เลย บางอันเป็นการหมดเวลา บางอันมีข้อมูลการส่ง ประเภทข้อผิดพลาดเหมือนกัน แต่หลักฐานรอบข้างไม่เหมือนกัน
เรื่องนี้สำคัญเป็นพิเศษกับ Next.js เพราะไฟล์ใต้ /_next/static/ โดยทั่วไปมีแฮชของเนื้อหาและออกแบบให้แคชแบบ immutable เอกสาร การโฮสต์ Next.js ด้วยตัวเอง ในปัจจุบันอธิบายแคชเฮดเดอร์ระยะยาวสำหรับไฟล์ประเภทนี้ ดังนั้น ChunkLoadError อาจเกี่ยวกับเวอร์ชันไม่ตรงกันตอนดีพลอย ไคลเอนต์เก่า เครือข่าย รีเวิร์สพร็อกซี CDN แคช Service Worker หรือไฟล์บิลด์ที่หายจริง ๆ
ผมไม่ต้องการให้ชั้นแจ้งเตือนเดาสาเหตุเอง ผมต้องการให้มันรักษาหลักฐานที่จำเป็นสำหรับการตรวจสอบ
ข้อผิดพลาดของสื่อสอนบทเรียนเดียวกันจากอีกชั้นหนึ่ง
ในบางเหตุการณ์ องค์ประกอบสื่อรายงานว่า:
MEDIA_ELEMENT_ERROR: Format error
ถ้าดูข้อความอย่างเดียว มันฟังเหมือนโคเดกไม่รองรับ
แต่การตรวจสอบการส่งข้อมูลเพิ่มในบางเหตุการณ์แสดงว่า:
HTTP status: 410
Content-Type: text/html; charset=utf-8
failure kind: http
เบราว์เซอร์ขอวิดีโอ แต่กลับได้รับการตอบกลับข้อผิดพลาด HTTP ที่มี HTML องค์ประกอบสื่อถอดรหัส HTML เป็นวิดีโอไม่ได้ อาการภายนอกจึงออกมาเป็น “Format error” การวินิจฉัยที่มีประโยชน์จริงอยู่ที่ชั้นการส่งข้อมูล
ชั้นที่เห็นความล้มเหลวก่อน ไม่จำเป็นต้องเป็นชั้นที่ทำให้เกิดปัญหา
“โคเดกล้มเหลว”, “เครือข่ายล่ม”, “บั๊กแคช” และ “ชังก์หาย” ล้วนเป็นข้อสรุป เทเลเมทรีควรเริ่มจากสิ่งที่สังเกตได้
ผมประเมินความล้มเหลวของเบราว์เซอร์ด้วยห้ามิติ
1. เจ้าของ
แหล่งที่มาเป็นตัวแอปเอง เฟรมเวิร์ก/รันไทม์ บริการภายนอก หรือสภาพแวดล้อม?
2. ผลกระทบต่อผู้ใช้
เส้นทางที่ใช้งาน การเรนเดอร์ การยืนยันตัวตน แชต เช็กเอาต์ หรือโฟลว์หลักอื่นพังหรือไม่? หรือแค่โฆษณา การวิเคราะห์ พรีโหลด หรือพรีวิวที่เป็นตัวเลือกโหลดไม่สำเร็จ แต่หน้ายังใช้งานได้?
3. คุณภาพของหลักฐาน
ผมมีสแตกจากโค้ดของตัวเอง URL ทรัพยากร สถานะ HTTP รหัสบิลด์ และสแตกคอมโพเนนต์หรือไม่? หรือมีแค่ Script error. ที่ 0:0?
4. การเกิดซ้ำและการกระจาย
เป็นเหตุการณ์จากเซสชันเดียว หรือรูปแบบเดียวกันเกิดกับผู้ใช้ เส้นทาง และเบราว์เซอร์หลายแบบหลังรีลีสเดียวกัน?
5. การกู้คืน
แอปกู้คืนเองหรือไม่? กำหนดให้รีโหลดชังก์แล้วหรือยัง? ทางสำรองทำงานไหม? ผู้ใช้ยังติดอยู่หรือเปล่า?
first-party ชัด
+ user impact สูง
+ evidence แข็งแรง
+ หลาย session
+ ไม่มี recovery
= urgent incident
third-party
+ optional
+ evidence อ่อน
+ isolated
+ user ไม่ได้รับผลกระทบ
= metric หรือ low priority
การตัดเหตุการณ์ทิ้งต้องทำอย่างระมัดระวัง
เมื่อสัญญาณรบกวนเริ่มน่ารำคาญ วิธีที่ล่อตาล่อใจคือเขียนนิพจน์ปกติเป็นสิบ ๆ ตัวแล้วทิ้งทุกอย่างที่กวนใจ นั่นอันตราย
ถ้าปิด AbortError ทุกตัว ผมอาจซ่อนคำขอ API จริงที่ถูกยกเลิก ถ้าทิ้ง Script error. ทุกตัว อาจพลาดกลุ่มปัญหาเฉพาะเบราว์เซอร์ที่มีความหมายเมื่อรวมหลายเหตุการณ์ และถ้าเมินความล้มเหลวภายนอกทั้งหมด อาจพลาดผู้ให้บริการชำระเงิน ยืนยันตัวตน หรือจัดการความยินยอมที่เสีย
ALERT
→ strong incident ที่ต้อง action
RETAIN / AGGREGATE
→ เก็บและนับ; alert เมื่อกลายเป็น cluster
METRIC / SAMPLE
→ noise ที่คาดไว้หรือ impact ต่ำ; เก็บ trend และตัวอย่าง
ระบบควรเงียบลงโดยไม่ตาบอด
ตัวจำแนกที่ดีจริง ๆ คือการเอานโยบายมาเขียนเป็นโค้ด
โค้ดด้านล่างไม่ได้คัดมาจากโปรดักชันของผม เป็นเพียงตัวอย่างสั้น ๆ ของนโยบายที่ผมอยากให้มีตั้งแต่แรก:
function classifyClientEvent(event) {
const owner = classifyOwner(event);
if (isExpectedMediaCancellation(event)) {
return { severity: "metric", reason: "expected-cancellation" };
}
if (owner === "first-party" && breaksActiveRoute(event)) {
return { severity: "alert", reason: "first-party-user-impact" };
}
if (isActiveFirstPartyChunkFailure(event)) {
return { severity: "alert", reason: "application-chunk" };
}
if (owner === "third-party") {
return { severity: "aggregate", reason: "integration-health" };
}
if (isOpaqueScriptError(event)) {
return { severity: "aggregate", reason: "insufficient-evidence" };
}
return { severity: "aggregate", reason: "needs-correlation" };
}
งานยากซ่อนอยู่ในฟังก์ชันอย่าง breaksActiveRoute() ชื่อข้อยกเว้นอย่างเดียวไม่พอ ต้องมีบริบทของเส้นทาง เจ้าของทรัพยากร ข้อมูลจากขอบเขตดักข้อผิดพลาด และบางครั้งต้องรู้รายละเอียดของผลิตภัณฑ์ด้วย
ลายนิ้วมือควรติดตามเหตุขัดข้อง ไม่ใช่ข้อความ
การเทียบข้อความแบบตรงตัวเป็นวิธีตัดรายการซ้ำที่ไม่ดี ออฟเซ็ตในสแตกที่ถูกย่อเปลี่ยนตามบิลด์ แฮชชังก์เปลี่ยน URL มีตัวระบุแบบไดนามิก และข้อความของแต่ละเบราว์เซอร์ก็ไม่เหมือนกัน
{
errorClass,
normalizedFirstPartyResource,
routeFamily,
clientBuild,
sessionId,
shortTimeBucket
}
ถ้าแอปล่มทั้งระบบ เฟรมบนสุดจากโค้ดของผมในสแตกอาจมีประโยชน์กว่า สำหรับชังก์ล้มเหลว ทรัพยากรชังก์ที่ปรับรูปแบบแล้วสำคัญกว่า ส่วนเหตุขัดข้องด้านการส่งสื่อ ประเภทความล้มเหลว HTTP และเส้นทางสื่ออาจมีค่ากว่าข้อความข้อผิดพลาดชั้นนอกของเบราว์เซอร์
resource.error
→ ChunkLoadError
→ framework boundary
→ recovery scheduled
จากนั้นลำดับเหตุการณ์ที่เกี่ยวข้องกันสามารถกลายเป็นเหตุขัดข้องเดียวที่มีข้อสังเกตสี่รายการ แทนที่จะเป็นความล้มเหลวด่วนสี่ครั้ง
ช่องแจ้งเตือนด่วนควรมีหน้าที่แคบกว่านี้มาก
ผมจะเก็บการแจ้งเตือนทันทีไว้สำหรับกรณีเช่น: ข้อผิดพลาดรันไทม์ของแอปที่มีสแตกใช้ได้และทำให้เส้นทางปัจจุบันพัง; ขอบเขตดักข้อผิดพลาด React/Next.js ที่กระทบการใช้งานจริง; JavaScript หรือ CSS ของแอปที่เส้นทางปัจจุบันต้องใช้แต่โหลดไม่สำเร็จ; ChunkLoadError ที่เกิดซ้ำในหลายเซสชันหรือหลายบิลด์; API หรือข้อมูลหลักล้มเหลวจนผู้ใช้ไปต่อไม่ได้; หรือกฎของโค้ดตัวเองถูกละเมิดชัดเจน เช่นสร้าง URL ผิดรูปแบบ
ส่วน Script error. ที่คลุมเครือครั้งเดียว ทรัพยากรของแอปล้มเหลวแต่กู้คืนสำเร็จ ข้อผิดพลาดสื่อที่ยังไม่รู้ชั้นต้นเหตุ หรือความผิดปกติเฉพาะเบราว์เซอร์ที่ต้องรวมกลุ่มก่อน ผมจะเก็บไว้แต่ไม่แจ้งเตือนทันที
และโดยปกติจะส่งไปเป็นเมตริกหรือการวินิจฉัยแบบสุ่มตัวอย่าง: ความล้มเหลวที่รู้แล้วของทรัพยากรโฆษณาหรือการวิเคราะห์; AbortError ของสื่อที่คาดไว้; ความล้มเหลวภายนอกที่เกิดกับครอว์เลอร์เท่านั้น; ความล้มเหลวที่มีสัญญาณออฟไลน์ชัด; และทรัพยากรคาดเดาแบบไม่จำเป็นที่ไม่กระทบเส้นทางปัจจุบัน
การแจ้งเตือนด่วนควรหมายถึงผลกระทบต่อผู้ใช้ที่ลงมือแก้ได้ ไม่ใช่จำนวนคำบ่นดิบ ๆ จากเบราว์เซอร์
ผมอยากวัดเหตุขัดข้องมากกว่านับ “ข้อผิดพลาด” รวมก้อนเดียว
- เหตุขัดข้องของแอปต่อ 1,000 เซสชัน;
- เซสชันที่ได้รับผลกระทบแยกตามรหัสบิลด์;
- เหตุขัดข้องจากขอบเขตดักข้อผิดพลาดแยกตามเส้นทาง;
- ชังก์ล้มเหลวแยกตามทรัพยากรและการดีพลอย;
- อัตราความล้มเหลวของการเชื่อมต่อภายนอกแยกตามผู้ให้บริการ;
- ปริมาณการยกเลิกที่คาดไว้ เพื่อให้เห็นการเพิ่มขึ้นผิดปกติ;
- อัตราการกู้คืนสำเร็จ;
- จำนวนเหตุขัดข้องไม่ซ้ำ แยกจากจำนวนเหตุการณ์ดิบ
“เกิดเหตุการณ์หนึ่งครั้ง” แทบไม่ใช่เกณฑ์แจ้งเตือนในโปรดักชันที่ดี “เหตุขัดข้องเดียวกันของแอปกำลังกระทบหลายเซสชันอิสระบนบิลด์ใหม่ และการกู้คืนก็ล้มเหลว” เป็นสัญญาณที่มีประโยชน์กว่ามาก
การมอนิเตอร์ฝั่งไคลเอนต์ยังพิสูจน์สาเหตุรากไม่ได้ด้วยตัวเอง
เทเลเมทรีจากเบราว์เซอร์มีข้อจำกัดชัดเจน
การไม่มีสถานะ HTTP อาจหมายถึง API ไม่ได้ให้ข้อมูล เบราว์เซอร์ไม่รองรับฟิลด์นั้น มีข้อจำกัดข้ามออริจิน คำขอถูกยกเลิก หรือยังมีช่องว่างในการสังเกตการณ์อื่น Service Worker ทำงานอยู่ไม่ได้พิสูจน์ว่ามันเป็นตัวส่งทรัพยากรเก่า ChunkLoadError หลังดีพลอยไม่ได้พิสูจน์ว่าเวอร์ชันไม่ตรงกัน และ online: true ก็ไม่ได้พิสูจน์ว่าเข้าถึงออริจินได้
การมอนิเตอร์ฝั่งไคลเอนต์ช่วยลดขอบเขตของสมมติฐาน แต่การพิสูจน์สาเหตุยังอาจต้องใช้ล็อกเซิร์ฟเวอร์ ล็อกรีเวิร์สพร็อกซี รายการไฟล์ของการดีพลอย สถานะแคช และการทำซ้ำจริง
ผมก็ไม่ต้องการให้การสังเกตการณ์กลายเป็นการเก็บข้อมูลผู้ใช้อย่างไร้ขอบเขต ทุกฟิลด์ควรมีอยู่เพราะมันช่วยแยกชนิดของความล้มเหลวได้จริง
เทเลเมทรีที่ดีกว่าไม่ได้แปลว่ามีข้อมูลมากกว่า แต่แปลว่าแยกความล้มเหลวได้ดีกว่า
กฎที่ผมใช้ตอนนี้: เก็บเหตุการณ์ ตรวจสอบเหตุขัดข้อง แจ้งเตือนเมื่อมีผลกระทบ
ตอนแรกผมอยากให้ตัวรายงานตอบแค่คำถามว่า “มีอะไรพังไหม?” ในโปรดักชันคำถามนี้กว้างจนแทบไม่มีประโยชน์ มีโปรแกรมรวบรวมข้อมูลที่เข้า Analytics ไม่ได้ เครื่องมือความเป็นส่วนตัวบล็อกโฆษณา Promise ของสื่อถูกยกเลิกโดยตั้งใจ ผู้ใช้หลุดเครือข่าย หรือ SDK ภายนอกทำงานผิดอยู่เสมอ
เป็นของเราหรือไม่?
user สูญเสีย functionality หรือไม่?
evidence แข็งแรงแค่ไหน?
เกิดซ้ำหรือไม่?
application recover แล้วหรือยัง?
นี่คือหลาย event หรือ incident เดียว?
เมื่อผมสร้างการมอนิเตอร์รอบคำถามที่แคบลง กระแสข้อความสีแดงก็เปลี่ยนจากสัญญาณรบกวนเป็นเครื่องมือทางวิศวกรรม
ข้อผิดพลาดของเบราว์เซอร์คือข้อสังเกต เหตุขัดข้องคือคำอธิบายที่เชื่อมหลักฐานเข้ากับผลกระทบต่อผู้ใช้ ส่วนการแจ้งเตือนคือการตัดสินใจว่าถึงเวลาที่คนควรลงมือแล้ว
ผมไม่อยากให้สามสิ่งนี้มีความหมายเหมือนกันอีก