Skip to main content

ลิงก์ติดตามผลของ 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

  1. ตอน delivery (broadcast/multicast) เรียก extendService.createRedirectMappings(ctx, campaign, userID)
  2. ตรวจ CAMPAIGN_REDIRECT_MODE
    • ไม่ใช่ builtin → เส้นทางเดิม: ยิง universal-redirect-service สร้าง mapping
    • เป็น builtin และ ตั้ง CAMPAIGN_LINK_KEYS / CAMPAIGN_LINK_ACTIVE_KEY_ID ครบ → buildBuiltinMappings (ถ้า config พังจะ log ครั้งเดียวแล้ว fallback กลับ legacy ไม่ทำให้การส่งล้ม)
  3. ดึงรายการ URL (ExtractUrls) และ image URL (ExtractImageUrls) จากเนื้อหา rich message
  4. ถ้า OA เปิด Google Analytics identity จะต่อพารามิเตอร์ระบุตัวตน (เช่น `?ga_id=<lineUid>`) ต่อท้าย destination ก่อน เข้ารหัส (พฤติกรรมเดียวกับ appendGaIdentityParam เดิม)
  5. ถ้าปลายทางเป็น liff.line.me จะ mint ลิงก์ใต้ LIFF base ของ OA และตั้ง flag f เพื่อให้เปิดเป็นหน้าต่าง LIFF แทนที่จะเปิด in-app browser เปล่า
  6. สร้าง 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
  7. ผลลัพธ์คือ URL รูปแบบ `https://<CAMPAIGN_LINK_HOST>/c/<token>` แล้วส่งกลับเป็น mapping (URL เดิม → URL ติดตาม) ให้ TransformMessageObjects ไปแทนที่ในข้อความ
  8. เวลาผู้ใช้กด: client-web proxy เส้นทาง `/c/[token]` → client-api-go ถอดรหัส → เขียน tracking_log แบบ async → ตอบ 302 ไปปลายทาง (หรือ stream รูป) — worker ไม่เกี่ยวข้องตอนคลิก
  9. หลังหมดอายุ (ค่า e ในโครงข้อมูล, default 30 วัน) endpoint ยัง redirect ให้ปกติแต่หยุดเขียน tracking → ลิงก์ไม่ตาย และ key เก่าปลดระวางได้

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

  • internal/campaignlink/token.goPayload, seal(), Builder.Build(), marshalPayload() (ปิด HTML escaping เพื่อให้ byte ตรงกับ JSON.stringify ของ Node)
  • internal/campaignlink/config.goMode(), IsBuiltin(), ExpiryDays(), NewBuilderFromEnv()
  • internal/campaignlink/token_test.go — cross-language test vector (ต้องตรงกับฝั่ง client-api)
  • internal/linemessageapi/extend.goextendService.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 เหมือนเดิมทั้งสองโหมด