Skip to main content

แคมเปญติดตามการเพิ่มเพื่อน

ภาพรวม

ฟีเจอร์นี้ใช้วัดผลแคมเปญหาเพื่อนใหม่ให้กับ LINE OA หลักการคือเมื่อผู้ใช้แตะลิงก์แคมเปญ ระบบจะบันทึกการเข้าชมพร้อมระบุตัวตนผู้ใช้ผ่าน LINE แล้วจึงพาผู้ใช้ไปยังหน้าเพิ่มเพื่อนของ OA ผลลัพธ์คือฝั่ง CMS สามารถรายงานได้ว่าเพื่อนใหม่แต่ละรายมาจากแคมเปญไหน และมาจากช่องทางย่อยใดภายในแคมเปญนั้นผ่านค่า referral

ฟีเจอร์นี้มีสองเส้นทางเข้า คือแบบระบุ token ของแคมเปญมาตรง ๆ และแบบให้ระบบค้นหาแคมเปญที่กำลังใช้งานอยู่จาก hash ของ OA

Business Flow

เส้นทางที่ระบุแคมเปญ (/:hash/friend-track/:token)

  1. ระบบดึงข้อมูล OA จาก hash และข้อมูลแคมเปญจาก token ของแคมเปญ
  2. หน้านี้เรียก liff.init() เองโดยไม่ผ่าน hook กลาง เพื่อควบคุมลำดับการทำงานเอง หากผู้ใช้ยังไม่ได้ล็อกอิน ระบบจะพาไปยัง LIFF URL เมื่ออยู่ในแอป LINE หรือเรียกฟังก์ชันล็อกอินเมื่ออยู่นอกแอป
  3. ดึง ID token โดยลองซ้ำได้ 3 ครั้ง และดึง access token ไว้เป็นตัวสำรอง หากไม่ได้ทั้งสองค่า ระบบจะ logout แล้วเริ่มล็อกอินใหม่
  4. ดึงโปรไฟล์ผู้ใช้เพื่อเอาชื่อที่แสดงและรูปโปรไฟล์ ซึ่งเป็นข้อมูลเสริมไม่ใช่ข้อมูลบังคับ
  5. บันทึกการเข้าชมด้วย POST /friend-track/:token/visit โดยแนบทั้ง x-liff-token และ x-liff-access-token พร้อมส่งชื่อที่แสดง รูปโปรไฟล์ และค่า referral ไปใน body
  6. ตัดสินใจตามผลลัพธ์ที่ได้กลับมาว่าผู้ใช้เป็นเพื่อนกับ OA อยู่แล้วหรือยัง
    • เป็นเพื่อนอยู่แล้ว หน้าจอแจ้งให้ผู้ใช้ทราบ แล้วพาไปเปิดห้องแชทของ OA อัตโนมัติภายใน 2 วินาที โดยมีปุ่มเปิดแชทให้กดเองด้วยหากไม่ต้องการรอ
    • ยังไม่เป็นเพื่อน ระบบพาไปยังหน้าเพิ่มเพื่อนของ OA ทันที
  7. ค่า referral อ่านได้ทั้งจาก query parameter โดยตรง และจากภายใน liff.state เนื่องจาก query parameter เดิมหายไปในขั้นตอน redirect ของการล็อกอิน

เส้นทางที่ไม่ระบุแคมเปญ (/:hash/friend-track)

เส้นทางนี้ใช้ในกรณีที่ endpoint ของ LIFF app ถูกตั้งให้ชี้มาที่ path นี้ ขั้นตอนโดยรวมเหมือนกัน แต่มีความต่างสามข้อ

  • ระบบพยายามอ่าน token ของแคมเปญจาก liff.state ก่อน
  • หากไม่พบ ระบบจะเรียก GET /friend-track/by-hash/:hash เพื่อค้นหาแคมเปญที่กำลังใช้งานอยู่ของ OA นั้น
  • หลังบันทึกการเข้าชม ระบบจะพาไปหน้าเพิ่มเพื่อนทันที โดยไม่มีสถานะ "เป็นเพื่อนอยู่แล้ว"

จุดเข้าอีกทาง: ปุ่ม CTA ในบทความ

ปุ่ม CTA ที่แสดงในหน้าอ่านบทความจะเก็บ token ของแคมเปญไว้ใน sessionStorage แล้วพาผู้ใช้ไปยัง LIFF URL ของแคมเปญนั้นพร้อมค่า referral

การเลือกใช้โดเมน liff.line.me แทน custom protocol แบบ line://app/ เป็นการตัดสินใจโดยเจตนา เนื่องจาก protocol แบบหลังไม่ส่ง liff.state มาเป็น query parameter เมื่อเปิดจาก in-app browser ของ LINE ซึ่งจะทำให้ระบบสูญเสียข้อมูลแคมเปญและ referral ไป

หน้าจอและองค์ประกอบหลัก

หน้าเพจ

  • หน้าแคมเปญที่ระบุ token (src/app/[hash]/friend-track/[token]/page.tsx) จัดการสถานะครบทุกช่วง ตั้งแต่กำลังโหลด กำลังประมวลผล เป็นเพื่อนอยู่แล้ว กำลังพาไปปลายทาง และเกิดข้อผิดพลาด
  • หน้าแคมเปญแบบไม่ระบุ token (src/app/[hash]/friend-track/page.tsx) รองรับกรณีที่ LIFF app ชี้ endpoint มาที่ path นี้

Hook และ service

  • Hook ดึงข้อมูลแคมเปญ (src/hooks/use-friend-track.ts) มีหน้าที่พิเศษคือตัด query string ที่ LIFF ต่อท้าย token ออกก่อนนำไปใช้ โดยรองรับทั้งกรณีที่เครื่องหมายคำถามถูก encode และไม่ถูก encode
  • Friend track service (src/service/friend-track.service.ts) ให้ฟังก์ชันดึงแคมเปญตาม token ดึงแคมเปญตาม hash ของ OA และบันทึกการเข้าชม

คอมโพเนนต์ CTA

  • ปุ่ม CTA แบบลอย (src/components/content-viewer/FriendTrackCta.tsx) แสดงในหน้าอ่านบทความ พร้อมแอนิเมชันดึงความสนใจ

Endpoint ที่ใช้

MethodPathคืนค่า
GET/friend-track/:tokenข้อมูลแคมเปญ ได้แก่ ชื่อ คำอธิบาย รหัสบอทของ OA และสถานะใช้งาน
GET/friend-track/by-hash/:hashแคมเปญที่ใช้งานอยู่ของ OA นั้น พร้อม token
POST/friend-track/:token/visitสถานะว่าเป็นเพื่อนอยู่แล้วหรือยัง พร้อมข้อความประกอบ

จุดเชื่อมต่อกับฟีเจอร์อื่น

  • @line/liff ถูกเรียกใช้โดยตรงในหน้านี้ ทั้งการ init การล็อกอิน การดึง ID token การดึง access token และการดึงโปรไฟล์
  • ใช้ deep link ของ LINE สองรูปแบบ คือลิงก์หน้าเพิ่มเพื่อน และลิงก์เปิดห้องแชทของ OA
  • เชื่อมกับ หน้าอ่านบทความ ผ่านข้อมูลแคมเปญที่ผูกไว้กับบทความ
  • รหัสบอทของ OA ที่ใช้ประกอบ deep link มาจาก payload ของแคมเปญเอง ไม่ได้มาจาก endpoint ข้อมูล OA
  • ระบบส่งทั้ง ID token และ access token ไปพร้อมกัน เนื่องจากฝั่ง API จะตรวจ ID token ก่อนแล้วจึง fallback ไปใช้ access token (ดู การล็อกอิน LINE ผ่าน LIFF)

รายละเอียดฝั่ง Backend (Client API)

การ verify token ที่ต่างจากฟีเจอร์อื่น

  • backend ตรวจ ID token ก่อนเสมอ โดยตรวจกับ primary channel ของ OA — ถ้า OA ยังไม่ได้ตั้งค่า LINE Login จะได้ 401 LINE Login not configured for this channel
  • ถ้าไม่มี ID token จึงใช้ access token แทน; ไม่มีทั้งคู่ → 401 No authentication token provided (นี่คือเหตุผลที่หน้านี้ส่งทั้งสองค่ามาพร้อมกัน)
  • จุดที่ต่างจากฟีเจอร์อื่นและตั้งใจ: endpoint นี้เรียกตัว verify แบบดิบๆ ไม่ผ่านเส้นทางกลางที่ auto-provision guest ดังนั้น การเปิดหน้าแคมเปญจะไม่สร้างแถวผู้ใช้ใหม่ในระบบ ต่างจากหน้าฟอร์มหรือหน้าเมนู — เป็นการรักษาพฤติกรรมเดิมไว้ และมีผลตามมาที่สำคัญในเรื่อง custom attribute (ดูด้านล่าง)

กฎ dedup — ส่วนที่ละเอียดที่สุดของฟีเจอร์นี้

การยิง visit ซ้ำ ไม่ได้ถูกกันแบบตายตัว แต่มีเงื่อนไขที่ยอมให้นับใหม่ได้:

  1. backend หา event การเข้าชมครั้งล่าสุดของคู่ (แคมเปญ, ผู้ใช้)
  2. ถ้าเคยมี → หา event การเลิกติดตามครั้งล่าสุดของคู่ (ผู้ใช้, OA) แล้วเทียบเวลากัน
    • ไม่มีการเลิกติดตามหลังการเข้าชมครั้งนั้น → ถือเป็น funnel เดิม คืน alreadyTracked: true โดยไม่เขียนอะไรลงฐานข้อมูลเลย
    • มีการเลิกติดตามหลังการเข้าชม → ถือเป็น funnel ใหม่ ไปต่อตามปกติ เพราะคนที่เลิกติดตามแล้วกลับมาอีกครั้งควรถูกนับใหม่

ผลที่หน้าเว็บควรรู้: alreadyTracked: true ไม่ได้แปลว่าผิดพลาด แต่แปลว่าผู้ใช้อยู่ใน funnel เดิมอยู่แล้ว — การ refresh หน้าหรือกดลิงก์ซ้ำในวันเดียวกันจะได้ค่านี้เป็นปกติ และตัวเลขแคมเปญจะไม่ถูกปั่น

สิ่งที่บันทึกและผลข้างเคียง

  • บันทึก event การเข้าชม โดย ชื่อที่แสดงและรูปโปรไฟล์ใช้ค่าจาก body ก่อน แล้ว fallback ไปใช้ค่าจาก token — ถ้าไม่มีทั้งคู่จะเก็บเป็น null การส่งค่าจากโปรไฟล์มาใน body จึงช่วยให้รายงานฝั่ง CMS อ่านง่ายขึ้น แต่ไม่บังคับ
  • ค่า referral ที่ว่างจะถูกเก็บเป็น null ไม่ใช่สตริงว่าง
  • ส่งงาน trigger เข้าคิวแบบ best-effort เพื่อให้ worker ประเมินกฎอัตโนมัติต่อ — ล้มเหลวก็ถูกกลืน ไม่กระทบผลลัพธ์ที่หน้าเว็บได้รับ
  • merge custom attribute ให้ผู้ใช้ เมื่อแคมเปญตั้งค่า attribute ไว้ หรือมีการกำหนดคีย์สำหรับเก็บค่า referral พร้อมกับที่ผู้ใช้ส่ง referral มาจริง
    • ข้อสำคัญ: การ merge นี้ทำเฉพาะเมื่อมีแถวผู้ใช้อยู่แล้วเท่านั้น ไม่มีการสร้างแถวใหม่ ดังนั้นผู้ใช้ที่ยังไม่เคยมีปฏิสัมพันธ์กับ OA มาก่อน (ยังไม่มีแถวในระบบ) จะไม่ได้รับ attribute จากแคมเปญ แม้จะส่ง referral มาถูกต้อง — เป็น edge case ที่มีผลจริงกับการทำ segmentation จากแคมเปญ

การชดเชย event สำหรับคนที่เป็นเพื่อนอยู่แล้ว

  • backend เช็คว่าผู้ใช้เป็นเพื่อนกับ OA อยู่แล้วหรือไม่ แล้วส่งสถานะนี้กลับมาให้หน้าเว็บตัดสินใจ (พาไปหน้าเพิ่มเพื่อน หรือพาไปห้องแชท)
  • ถ้าเป็นเพื่อนอยู่แล้ว backend จะบันทึก event การติดตามให้เองด้วย (ถ้ายังไม่มี) เหตุผลคือ LINE จะไม่ยิง webhook การติดตามสำหรับคนที่เป็นเพื่อนอยู่แล้ว ถ้าไม่ชดเชยตรงนี้ funnel ของแคมเปญจะค้างอยู่ที่ขั้น "เข้าชม" ตลอดไปและดูเหมือนแคมเปญไม่มีผลลัพธ์เลย
  • event การติดตามและเลิกติดตามตัวจริงมาจาก service webhook แยกต่างหาก — endpoint นี้เขียนเฉพาะ event การเข้าชม และ event การติดตามในกรณีชดเชยนี้เท่านั้น

รายละเอียดของ endpoint ค้นแคมเปญ

  • GET /api/friend-track/by-hash/:hash ค้น OA จาก hash โดยไม่ตรวจสถานะหรือการลบเลย (parity กับระบบเดิม) ซึ่ง หลวมกว่าทุก endpoint อื่นในระบบ — OA ที่ถูกปิดหรือลบไปแล้วก็ยัง resolve ได้
  • แต่ถ้าไม่มีแคมเปญที่ active อยู่จะได้ 404 No active campaign found for this LINE OA และถ้าไม่พบ OA จะได้ 404 LINE OA not found
  • เมื่อ OA มีแคมเปญ active หลายตัว ระบบเลือกตัวล่าสุดเสมอ — เส้นทางที่ไม่ระบุ token จึงชี้ไปยังแคมเปญใหม่สุดโดยอัตโนมัติ ซึ่งเป็นสิ่งที่ควรรู้ตอนสร้างแคมเปญใหม่ทับของเดิม
  • GET /api/friend-track/:token คืน LIFF ID และ hash ของ OA มาด้วย หน้าเว็บจึงเริ่ม LIFF ได้จากข้อมูลชุดนี้ตรงๆ; description ที่ไม่มีค่าจะมาเป็น null ไม่ใช่ key ที่หายไป

Edge case และข้อควรระวัง

  • endpoint บันทึกการเข้าชมมี rate limit เฉพาะ route คือ 10 ครั้งต่อ 60 วินาทีต่อ IP เข้มกว่า rate limit ระดับแอป — ผู้ใช้หลายคนที่ออกเน็ตผ่าน IP เดียวกัน (เช่น Wi-Fi ในงานอีเวนต์ที่แจก QR แคมเปญ) อาจชนเพดานนี้ได้จริง เป็นสถานการณ์ที่ควรคำนึงเมื่อวางแผนแคมเปญ on-ground
  • ทุก field ใน body เป็น optional และ ข้อผิดพลาดจากการ parse body ถูกละเลยทั้งหมด เพราะตัวตนผู้ใช้มาจาก header เท่านั้น — body ที่รูปแบบผิดจะไม่ทำให้ request ล้ม แต่จะทำให้ชื่อ รูป และ referral หายไปเงียบๆ
  • แคมเปญที่ถูกปิดใช้งานจะได้ 404 Campaign not found แม้ token จะถูกต้อง — ลิงก์เก่าที่แจกไปแล้วจะหยุดทำงานทันทีที่แอดมินปิดแคมเปญ