ลิงก์ติดตามผลของ Campaign (Redirect Mapping / Encrypted Token)
ภาพรวม
ทุก URL และรูปภาพในข้อความ campaign ต้องถูกแทนที่ด้วย "ลิงก์ติดตาม" ก่อนส่งออก เพื่อให้รู้ว่าใครคลิกอะไรตอนไหน ระบบรองรับ 2 โหมด:
- legacy — เรียก universal-redirect-service สร้าง short URL ล่วงหน้า (ต้องเรียก API รายผู้ใช้)
- builtin — สร้าง token เข้ารหัสในตัวเอง (AES-256-GCM) ที่บรรจุปลายทางและบริบท tracking ครบ ไม่ต้องเรียก API ภายนอกและไม่ต้องสร้างแถวล่วงหน้าเลย
โหมด builtin แก้ปัญหา scale โดยตรง: campaign ที่ส่ง 2 ล้านคนเดิมต้องยิง redirect API 2–4 ล้านครั้ง และสร้างแถวราว 8 ล้านแถวซึ่งราว 85% ไม่เคยถูกคลิก
Business Flow
- ตอน delivery (broadcast/multicast) เรียก
extendService.createRedirectMappings(ctx, campaign, userID) - ตรวจ
CAMPAIGN_REDIRECT_MODE- ไม่ใช่
builtin→ เส้นทางเดิม: ยิง universal-redirect-service สร้าง mapping - เป็น
builtinและ ตั้งCAMPAIGN_LINK_KEYS/CAMPAIGN_LINK_ACTIVE_KEY_IDครบ →buildBuiltinMappings(ถ้า config พังจะ log ครั้งเดียวแล้ว fallback กลับ legacy ไม่ทำให้การส่งล้ม)
- ไม่ใช่
- ดึงรายการ URL (
ExtractUrls) และ image URL (ExtractImageUrls) จากเนื้อหา rich message - ถ้า OA เปิด Google Analytics identity จะต่อพารามิเตอร์ระบุตัวตน (เช่น
`?ga_id=<lineUid>`) ต่อท้าย destination ก่อน เข้ารหัส (พฤติกรรมเดียวกับappendGaIdentityParamเดิม) - ถ้าปลายทางเป็น
liff.line.meจะ mint ลิงก์ใต้ LIFF base ของ OA และตั้ง flagfเพื่อให้เปิดเป็นหน้าต่าง LIFF แทนที่จะเปิด in-app browser เปล่า - สร้าง token: bytes เรียงเป็น
keyId (1 byte)+nonce (12 byte)+ciphertext+tag (16 byte)เข้ารหัส AES-256-GCM ด้วย AAD"campaign-link-v1"แล้ว encode เป็น base64url ไม่มี padding payload คือ JSON กระชับ ที่มีคีย์c, o, l, m, i, u, t, d, eและf(ถ้ามี) หมายถึง campaignId, orgId, lineOaId, richMessageId, index, lineUid, type url/asset, destination, expiry - ผลลัพธ์คือ URL รูปแบบ
`https://<CAMPAIGN_LINK_HOST>/c/<token>`แล้วส่งกลับเป็น mapping (URL เดิม → URL ติดตาม) ให้TransformMessageObjectsไปแทนที่ในข้อความ - เวลาผู้ใช้กด: client-web proxy เส้นทาง
`/c/[token]`→ client-api-go ถอดรหัส → เขียนtracking_logแบบ async → ตอบ 302 ไปปลายทาง (หรือ stream รูป) — worker ไม่เกี่ยวข้องตอนคลิก - หลังหมดอายุ (ค่า
eในโครงข้อมูล, default 30 วัน) endpoint ยัง redirect ให้ปกติแต่หยุดเขียน tracking → ลิงก์ไม่ตาย และ key เก่าปลดระวางได้
ไฟล์และฟังก์ชันหลัก
internal/campaignlink/token.go—Payload,seal(),Builder.Build(),marshalPayload()(ปิด HTML escaping เพื่อให้ byte ตรงกับJSON.stringifyของ Node)internal/campaignlink/config.go—Mode(),IsBuiltin(),ExpiryDays(),NewBuilderFromEnv()internal/campaignlink/token_test.go— cross-language test vector (ต้องตรงกับฝั่ง client-api)internal/linemessageapi/extend.go—extendService.createRedirectMappings(),buildBuiltinMappings(),campaignLinkBuilder(),getGaSettings(),getLineLiffId(),isLiffURL()internal/redirectx/redirectx.go— client ของ universal-redirect-service (เส้นทาง legacy)- เอกสารสเปกเต็ม:
docs/campaign-link-token-spec.mdและdocs/campaign-delivery-tracking-redesign.mdใน repo worker
จุดเชื่อมต่อกับ Service อื่น
- ENV ฝั่ง worker:
CAMPAIGN_REDIRECT_MODE,CAMPAIGN_LINK_KEYS(JSON map keyId ไปยัง base64 32 bytes),CAMPAIGN_LINK_ACTIVE_KEY_ID,CAMPAIGN_LINK_HOST,CAMPAIGN_LINK_EXP_DAYS - client-api-go — ต้องถือ
CAMPAIGN_LINK_KEYSชุดเดียวกัน byte-for-byte เพื่อถอดรหัส (endpoint`/api/c/:token`); คีย์ห้ามลบตราบใดที่ยังมีลิงก์มีชีวิต - client-web — route
`/c/[token]`เป็น proxy บาง ๆ ไม่ถือคีย์ - universal-redirect-service — ยังคงอยู่สำหรับ legacy links และ consumer อื่น
- ตาราง: อ่าน
line_oa(GA setting, LIFF id); การคลิกจะไปเขียนtracking_log(ที่ client-api) - cms-api ไม่ต้องแก้อะไร — สร้าง campaign และอ่านรายงานจาก
tracking_logเหมือนเดิมทั้งสองโหมด