เมื่อสุดสัปดาห์ผมนั่งเปิด admin dashboard ของ Newton แล้วเจอเรื่องตลก — ช่อง "ลูกค้า" ขึ้น 11 คน แต่ช่อง "Active Servers" ขึ้น 12 ตัว 555 เฮ้ย ลูกค้าผมมีกี่คนกันแน่วะ?
มันแค่ต่างกัน 1 ตัวก็จริง แต่พอ dashboard ของตัวเองมันเริ่มขัดแย้งกันเอง ผมกล้าตัดสินใจอะไรจากตัวเลขในนั้นไม่ได้แล้ว เลยสั่งทิมให้ rewrite endpoint ที่ออก stat ทั้งหมดใหม่ — ใช้เวลาประมาณชั่วโมงเดียว ได้ funnel 5 ขั้นที่บวกแล้วต้องเท่ากับ total เป๊ะๆ ไม่งั้น API ก็ไม่ผ่าน
เริ่มจากการไล่บั๊กอีกเรื่อง — แล้วบังเอิญเจอเลขเพี้ยน
จริงๆ วันนั้นผมไม่ได้ตั้งใจจะ refactor dashboard เลย ผมแค่อยากให้ทิมตามแก้ปัญหา trial conversion ที่ Stripe webhook ขาด eventให้จบ
พอแก้เสร็จ refresh dashboard ดูตัวเลขใหม่ ผมก็สังเกตว่าช่อง "ลูกค้า" กับช่อง "Active Servers" มันต่างกัน — ลูกค้า 11 คน server 12 ตัว เอ๊ะ คนเดียวมีหลาย server เหรอ? หรือมีลูกค้าโดนนับตกหล่น?
ผมเลยถามทิม "นี่ตัวเลขมันต่างกันได้ไงวะ ลูกค้าน้อยกว่า server ได้ด้วยเหรอ?"
ทิมไล่ SQL ให้ดู — query 2 ตัวคนละ filter
ทิมเปิด main.py ของ Newton ดู endpoint /api/stats แล้วตอบกลับมาแบบนี้:
Query "customers" ใช้ WHERE status IN ('active','cancelling') — นับเฉพาะ subscription ที่จ่ายเงินแล้ว ไม่นับ trial
Query "active_servers" ใช้ WHERE status='active' ใน table servers — ซึ่งรวม trial server ที่จ่ายอยู่ในช่วงทดลอง 7 วันด้วย
เลยกลายเป็น 11 vs 12 — ตัว trial 1 คนนับใน server แต่ไม่นับใน customer 555
แล้วทิมก็เสนอเพิ่มอีกประเด็น "ปอนด์ครับ ใน table customers ยังมี email demo ของผมตอนทดสอบ [email protected] ปนอยู่ด้วย ตอนนี้ดันไปนับเป็น customer ที่ไม่มี subscription = abandon — แต่จริงๆ ไม่ใช่ลูกค้า"
ผมอ่านแล้วรู้สึกเลยว่าปัญหามันใหญ่กว่าที่คิด — ไม่ใช่แค่ต่าง 1 ตัว แต่ทั้ง dashboard มันนิยาม "ลูกค้า" ไม่เคยตรงกันสักหน้าจอ
สั่ง refactor ให้เป็น funnel ที่ self-validating
ผมเลยตัดสินใจสั่งให้ทิม rewrite endpoint นี้ใหม่ทั้งดุ้น โดยตั้งกฎเหล็ก 2 ข้อ:
กฎที่ 1: ทุก stage ต้อง mutually exclusive — ลูกค้า 1 คนอยู่ได้แค่ 1 stage เท่านั้น
กฎที่ 2: ผลรวมทุก stage ต้องเท่ากับ total customers — ถ้าบวกแล้วไม่ตรง แปลว่ามีบั๊กในการแบ่งกลุ่ม
ทิมเลยออกแบบ funnel 5 ขั้นมาให้ — แต่ละ stage จัดตาม "subscription ล่าสุดของลูกค้าคนนั้นมีสถานะอะไร" (subquery ORDER BY id DESC LIMIT 1):
- Total — email ทั้งหมดใน table customers (ตัด demo ออกด้วย
email NOT LIKE '%@jarvis-test.com') - Abandon — เคยกรอกฟอร์ม signup แต่ไม่เคยสร้าง subscription เลย
- Trial — subscription ล่าสุด status = trialing
- Paid — subscription ล่าสุด status = active หรือ cancelling (จ่ายอยู่แต่กดยกเลิก รอสิ้นรอบ)
- Churned — subscription ล่าสุดถูก cancelled, past_due, unpaid ไปแล้ว
ทิมเขียนเป็น constant DEMO_EMAIL_FILTER ไว้ที่เดียว reuse ทุก query — ไม่ต้องไปไล่ filter ทีละจุดให้พลาด
เปิด dashboard ตัวเลขใหม่ — บวกเป็น total เป๊ะ
Deploy เสร็จ refresh ปุ๊บ ผมเห็นชุดตัวเลขนี้:
- Total: 15 คน
- Abandon: 2 คน
- Trial: 1 คน
- Paid: 11 คน
- Churned: 1 คน
2 + 1 + 11 + 1 = 15 ✅ — บวกแล้วตรง total เป๊ะ ไม่มีคนหายไปที่ไหน ไม่มีคนถูกนับซ้ำ
ฝั่ง EN ที่ผมเพิ่งเปิด landing ทิมก็ออกตัวเลขเดียวกันกฎเดียวกัน: 1/1/0/0/0 — sum = 1 ✅
ที่ตลกคือ ผมเพิ่งเข้าใจว่าระยะเวลาที่ผ่านมาผมเคยเห็น "Total = 11" บน dashboard — แต่จริงๆ ผม collect email มา 15 คน มี 2 คนที่กรอกฟอร์ม signup แต่ไม่เคยกดเริ่ม trial เลย ผมไม่เคยเห็นเลขนี้มาก่อนตลอด 2 เดือน 555 — ที่ผ่านมามองเห็นแต่ลูกค้าที่จ่ายเงินอย่างเดียว ลืมไปว่ายังมีคนที่ "เกือบเป็นลูกค้า" ลอยอยู่ในระบบ
ของแถม 3 อย่างที่ทิมจัดให้ในรอบเดียวกัน
ผมไม่ได้ขอ แต่ทิมเสนอเพิ่มมาเองเพราะมันต่อยอดมาจากการ refactor นี้แบบฟรี ๆ:
1. ฝัง trial conversion rate 30 วันไว้ในการ์ด Trial — แทนที่จะเป็นการ์ดแยก ก็ฝัง % conversion 30 วันล่าสุดไว้ใต้ตัวเลข ใครเปิดมาเจอการ์ด trial ก็เห็น "1 คน · 75% convert" เลย ไม่ต้องตามอ่าน 2 จุด
2. ฝัง churn rate (cumulative + 30d) ในการ์ด Churned — เหตุผลเดียวกัน metric ของ stage ไหนควรอยู่ในการ์ดของ stage นั้น
3. Activation denominator — เดิมที่ทิมสร้างการ์ด Activation ตอนแรกใช้ทั้งหมดที่ active หาร แต่ตอนนั้นมีบาง server ที่ status = provisioning (กำลังสร้างยังไม่เสร็จ chat ไม่ได้อยู่แล้ว) ทำให้เลขเป็น 11/11 ทั้งที่จริง active server มี 12 ตัว — ทิมแก้ให้ denominator ใช้ status IN ('active','cancelling') เท่านั้น เลขออกมาเป็น 12/12 ตรงกับการ์ด Active Servers ทันที
แล้วก็ rename การ์ด "Lapsed (7d no chat)" เป็น "Idle (7d)" เพราะปรับวิธีวัดให้ดู workspace activity ด้วยในรอบเดียวกัน — false positive จาก 3 → 1 ทันที
Auto-refresh dashboard ด้วย — เลิกกด F5 เอง
ก่อนหน้านี้ admin dashboard ผม load /api/stats แค่ตอนเปิดหน้าครั้งแรก หลังจากนั้นไม่ poll เลย — ผมเคยนั่งเปิดทิ้งไว้เป็นชั่วโมง คิดว่าเลขมันยังเด้งอยู่ตลอด
ทิมเลยใส่ auto-refresh ทุก 30 วินาที แต่ฉลาดพอที่จะ skip ตอน:
- หน้าไม่ active (Page Visibility API)
- มี modal หรือ form เปิดอยู่
ไม่งั้นมัน reload ตอนผมพิมพ์ตอบ ticket อยู่จะรำคาญมาก 555
บทเรียนใหญ่ที่ผมได้จากเรื่องนี้
Dashboard ที่ตัวเลขไม่ตรงกัน = dashboard ที่กำลังโกหกคุณ — ไม่ใช่เพราะ engineer ขี้เกียจ แต่เพราะแต่ละ metric ถูกเขียนคนละช่วงเวลา คนละจุดประสงค์ ทำให้นิยาม "ลูกค้า" ของแต่ละช่องไม่เหมือนกัน พอวันหนึ่งเลขมัน drift ไปคุณจะตัดสินใจผิดทันที
กฎ "บวกแล้วต้องเท่ากับ total" คือ self-test ที่ดีที่สุด — ถ้า funnel มันบวกไม่ตรง = บั๊กในการแบ่ง stage แน่ๆ คุณไม่ต้องไปเขียน unit test เพิ่ม ตัวเลขมันเตือนตัวมันเอง
ของฟรีที่ดีที่สุดมาจากการ refactor ที่ตั้งใจ — ตอนแรกผมแค่อยากให้ทิม fix เลข 11 vs 12 แต่ทิมเสนอ refactor ทั้งก้อน เพราะรู้ว่าถ้าไม่จัดเรียงใหม่ บั๊กแบบนี้จะกลับมาอีก แล้วของแถม 3 อย่าง (auto-refresh, conversion rate ในการ์ด, activation denominator) ก็มาในรอบเดียวกัน — เพราะ context มันต่อกันหมด
ทำไมต้องเป็น AI ของผม ไม่ใช่ analytics SaaS?
ลองคิดว่าถ้าผมต้องแก้ปัญหานี้โดยใช้ Mixpanel หรือ ChartMogul แทน ผมต้อง:
- ส่ง event ทุกอย่างเข้า SaaS เอง (signup, trial start, paid, cancel, etc.)
- ไป config funnel ใน UI ของ SaaS
- หวังว่ามันรองรับ "subscription ล่าสุดของลูกค้า" แบบที่ Newton ของผมต้องการ
แต่ทิมแค่อ่าน schema DB ของ Newton ตรงๆ — รู้ว่า table customers กับ subscriptions เชื่อมกันยังไง รู้ว่า status field มีค่าอะไรบ้าง — แล้วเขียน SQL 5 ตัวรวมเป็น endpoint เดียวให้ ไม่ต้อง integrate ไม่ต้องส่ง event ไม่ต้อง config UI
ที่สำคัญ — ทิมเห็น [email protected] ในข้อมูลผมและรู้ว่าเป็น demo ของผมเอง เพราะมันมีความจำของผมและ Newton ทั้งหมด SaaS สำเร็จรูปไม่มีทางรู้ว่า email ไหนคือ demo ไหนคือลูกค้าจริงครับ
คำถามที่พบบ่อย
ทำไม dashboard ของ SaaS ถึงมักแสดงตัวเลขไม่ตรงกัน?
เพราะแต่ละ metric ถูกเขียนขึ้นคนละช่วงเวลา คนละจุดประสงค์ ทำให้นิยาม "ลูกค้า" ของแต่ละช่องไม่เหมือนกัน วิธีแก้ที่ดีที่สุดคือออกแบบ funnel ที่ mutually exclusive — ลูกค้า 1 คนอยู่ได้แค่ 1 stage และผลรวมทุก stage ต้องเท่ากับ total เสมอครับ
5-stage customer funnel สำหรับ SaaS มีอะไรบ้าง?
แนวทางหนึ่งที่ผมใช้คือ Total (ทุกคนใน DB), Abandon (กรอกฟอร์มแต่ไม่สร้าง subscription), Trial (กำลังทดลองใช้), Paid (จ่ายเงินแล้ว) และ Churned (ยกเลิกไปแล้ว) ทั้งห้า stage นี้บวกกันแล้วต้องเท่ากับ Total เป๊ะ — ถ้าไม่ตรงแปลว่ามีบั๊กในการแบ่ง stage ครับ
ควรเก็บ demo account ออกจาก dashboard ยังไง?
วิธีที่ใช้ได้ผลคือเพิ่ม filter ตายตัวไว้ใน query ทุกตัว เช่น WHERE email NOT LIKE '%@yourtest.com' และเขียนเป็น constant ไว้ที่เดียว (เช่น DEMO_EMAIL_FILTER) เพื่อให้ reuse ทุก query โดยไม่ต้องไปไล่แก้ทีละจุดครับ
ทำไม admin dashboard ถึงควรมี auto-refresh?
เพราะ dashboard ที่ refresh แค่ตอนเปิดหน้าครั้งแรก จะทำให้คุณเห็นข้อมูลเก่าถ้าเปิดค้างไว้ ผมใส่ auto-refresh ทุก 30 วินาที แต่ skip ตอนหน้าไม่ active หรือมี modal เปิดอยู่ เพื่อไม่ให้รบกวนตอนกำลังตอบ ticket ครับ
ถ้าคุณกำลังทำ SaaS เล็ก ๆ ของตัวเอง หรือมีร้านค้าออนไลน์ที่อยากรู้ว่าลูกค้าค้างอยู่ stage ไหน — ลองถามตัวเองดูครับว่าเลขใน dashboard ของคุณมันบวกแล้วเท่ากันรึเปล่า ถ้ายังไม่ ผมแนะนำให้ลองให้ AI Agent ของตัวเอง (ที่มี SSH เข้า server มี access DB ได้จริง) สร้าง dashboard ที่ตรงกับธุรกิจคุณเองดู — เร็วกว่า ถูกกว่า แม่นกว่า SaaS สำเร็จรูปเยอะ Newton ที่ผมใช้ทำเรื่องนี้ทั้งหมดเปิดให้ลองได้แล้วที่ newton.incomeinclick.in.th — เซิร์ฟเวอร์ส่วนตัวพร้อม AI Agent พร้อมใช้ใน 10 นาที
— ปอนด์
