Skip to main content

ลิงก์ติดตามการคลิก

ภาพรวม​

endpoint ที่แปลง tracking token เป็น URL ปลายทางพร้อมกับบันทึกสถิติการคลิกไปในคราวเดียว ใช้กับปุ่ม บน rich menu เป็นหลัก กล่าวคือผู้ใช้กดปุ่ม, LIFF เปิดขึ้น, เว็บเรียก endpoint นี้เพื่อขอ URL แล้วพา ผู้ใช้ไปต่อ ในขณะที่ระบบบันทึกว่าใครกดปุ่มไหน

หลักการสำคัญคือ endpoint นี้ต้องคืน destinationUrl เสมอ การบันทึกคลิกเป็นงานแบบ fire-through ที่กลืน error ทั้งหมด ไม่มีสิ่งใดทำให้ redirect พังได้ ยกเว้นกรณีเดียวคือหา token ไม่พบ ซึ่งตอบ 404

Business Flow​

GET /api/tracking/redirect/:token (header x-liff-token เป็น optional)​

token อาจถูก resolve ได้จาก 2 เส้นทาง

เส้นทาง built-in (ไม่แตะฐานข้อมูลเลย)​

  1. ถอดรหัส token ด้วย keyring AES-256-GCM ซึ่งมีโครงสร้าง keyId(1) ‖ nonce(12) ‖ ciphertext ‖ tag(16) encode เป็น base64url แบบไม่มี padding โดย key มาจาก config CAMPAIGN_LINK_KEYS
  2. AAD ของเส้นทางนี้คือ "tracking-link-v1" ซึ่งต่างจากของ campaign redirect โดยเจตนา token ของ campaign จึงถอดออกมาเป็น tracking token ไม่ได้ แม้จะใช้ key ชุดเดียวกัน
  3. เมื่อถอดสำเร็จ หาก token ยังไม่หมดอายุ (p.E) และ ct มีค่าเป็น rich_menu ระบบจะเขียนคลิก แบบ best-effort (ค่า lead_gen จะไม่ถูกนับคลิกตาม parity ของระบบเดิม) แล้วคืน p.D เป็น destination
  4. หาก keyring ว่างหรือถอดรหัสไม่ได้ ระบบจะ fail closed แบบเงียบแล้วไปใช้เส้นทาง legacy

เส้นทาง legacy (อ่านจากตาราง)​

  1. SELECT ... FROM tracking_token WHERE token = $1 LIMIT 1 หากไม่มีแถวตอบ 404 "Tracking token not found"
  2. บันทึกคลิกเฉพาะเมื่อ token ยังไม่หมดอายุ และ status เป็น active และ content_type เป็น rich_menu นอกเหนือจากนั้นระบบยังคงคืน destination ตามปกติ
  3. คำนวณ field ของคลิกจากแถวข้อมูลและ jsonb metadata ตามลำดับ fallback ของระบบเดิมอย่างเคร่งครัด ได้แก่ richMenuId (มาจาก metadata เท่านั้น), richMenuActionIndex (จาก row.action_index ก่อน แล้วจึง metadata.richMenuActionIndex), richMenuArchiveId (จาก metadata.richMenuArchiveId) และ trackingLabel (จาก row.tracking_label แล้วจึง metadata.trackingLabel แล้วจึงสตริงว่าง)

การเขียนคลิก (writeClick) — ทำงานเหมือนกันทั้งสองเส้นทาง​

  1. ระบุตัวผู้ใช้ ผ่าน liff.VerifyAndGetLineUser ซึ่ง auto-provision guest จึงทำให้ FK ถูกต้องเสมอ กรณีที่ไม่มี token, verifier ไม่ได้ตั้งค่า, verify ล้มเหลว หรือไม่ได้ user ระบบจะบันทึก เป็น anonymous โดย line_uid เป็น NULL พร้อม log ที่แยกแยะสาเหตุไว้ (ระหว่าง "ไม่มี token ส่งมา" กับ "token มาแต่ verify ไม่ผ่าน") เพื่อให้ diagnose แถว anonymous ได้ภายหลัง
  2. insert ลงตาราง tracking_log ด้วย service='redirect', action_type='click', type='uri' และ content_type='rich_menu' โดยคอลัมน์ title ทำหน้าที่เก็บ trackingLabel ไปด้วยเพราะเป็น NOT NULL
    • คอลัมน์ raw_data เป็น jsonb ที่ต้องส่งค่าเป็น string ไม่ใช่ []byte เนื่องจาก pool ใช้ pgx simple query protocol ซึ่ง text-encode []byte เป็น bytea แล้วคอลัมน์ jsonb จะปฏิเสธ จุดนี้เคยเป็นบั๊กที่ทำให้ tracking_log ไม่ถูกเขียนแบบเงียบๆ ในทั้งสองเส้นทาง
  3. upsert ตาราง tracking_line_users เพื่อ dedup ต่อคู่ผู้ใช้และจุดคลิก เฉพาะเมื่อรู้ตัวผู้ใช้และ มี richMenuId โดย tracking_key มีรูปแบบ {richMenuId}:{archiveId}:{actionIndex} (archiveId ที่ไม่มีค่าใช้ 0) รูปแบบนี้ต้องตรงกับที่ worker ใช้ และ upsert ใช้เงื่อนไข ON CONFLICT (tracking_key, line_user_id) DO NOTHING
  4. ตั้ง flag RICH_MENU_STAT_DIRTY:{id} ใน Redis ด้วย TTL 7200 วินาที เพื่อให้ cron คำนวณสถิติใหม่ โดย guard ของระบบเดิมเป็น JS truthy check ดังนั้น richMenuId ที่เป็น 0 จะไม่ตั้ง flag ซึ่งต่าง จาก upsert ในข้อก่อนหน้าที่ใช้เงื่อนไข not-null

ไฟล์และฟังก์ชันหลัก​

รายการค่า
RouteGET /api/tracking/redirect/:token
Registerinternal/trackingredirect/register.go → Register(r, deps)
Handlerinternal/trackingredirect/handler.go → (*Handler).Resolve
Serviceinternal/trackingredirect/service.go → New(...), ResolveAndTrack, recordClick, writeClick, fieldsFromBuiltin, fieldsFromRow และ type Destination, clickFields
Token ringinternal/trackingtoken/token.go → NewRing(env), (Ring).Decrypt, Payload และ aad = "tracking-link-v1"
Helperinternal/trackingredirect/util.go → parseMetadata, jsonNumberToInt64, jsInt64, jsNullInt64, rawMeta, nowFn
Cacheinternal/cache → Set ซึ่งถูกใช้เป็น DirtyMarker

หมายเหตุ: package นี้ใช้ raw SQL โดยไม่มี entity หรือ repository แยกออกมาโดยเจตนา เพื่อ mirror dataSource.query ของระบบเดิมแบบตัวต่อตัว ทั้ง placeholder $N, ชุดคอลัมน์ และรูปแบบ tracking_key

จุดเชื่อมต่อกับ Service อื่น​

  • ตารางฐานข้อมูล tracking_token, tracking_log, tracking_line_users
  • Redis ผ่าน internal/cache สำหรับ flag RICH_MENU_STAT_DIRTY:*
  • Config CAMPAIGN_LINK_KEYS ซึ่งใช้ร่วมกับ campaign-redirect แต่แยก namespace ออกจากกันด้วย AAD
  • liff-authentication ผ่าน VerifyAndGetLineUser ซึ่ง auto-provision guest
  • token ถูกสร้างโดย cms-api-go (internal/core/trackinglink) ส่วนสถิติถูกอ่านโดย CMS และ cron ฝั่ง worker ซึ่งต้องใช้รูปแบบ tracking_key เดียวกัน
  • ตรงกับ client-web feature: tracking-redirect (เส้นทาง /{hash}/r/{id} และการหลบการเปิด LIFF ซ้อนกัน)