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.goRegister(r, deps)
Handlerinternal/trackingredirect/handler.go(*Handler).Resolve
Serviceinternal/trackingredirect/service.goNew(...), ResolveAndTrack, recordClick, writeClick, fieldsFromBuiltin, fieldsFromRow และ type Destination, clickFields
Token ringinternal/trackingtoken/token.goNewRing(env), (Ring).Decrypt, Payload และ aad = "tracking-link-v1"
Helperinternal/trackingredirect/util.goparseMetadata, jsonNumberToInt64, jsInt64, jsNullInt64, rawMeta, nowFn
Cacheinternal/cacheSet ซึ่งถูกใช้เป็น 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 ซ้อนกัน)