Skip to content

v0.10.1 — Per-campaign reward attribution

Choose a tag to compare

@Shudesu Shudesu released this 07 Apr 11:31

Behavior Change — Per-campaign reward attribution

v0.10.0 では reward template の解決を friends.first_tracked_link_id(最初のキャンペーンに永久 pin)に固定していたため、既存友だちが新しいキャンペーンの tracked link を踏んでも reward が古いキャンペーンのままになる問題がありました。本リリースでこれを反転します。

How it works

  • LIFF が ?ref= (= 当該キャンペーンの tracked link id) をフォーム送信時の trackedLinkId として乗せる
  • サーバー (apps/worker/src/services/reward-resolver.ts) は trackedLinkIdとして優先解決:
    1. trackedLinkId 指定 + 該当 link が DB に存在 → その link の reward_template_id を採用(NULL の場合は null を返してフォームの on_submit_message_* に委譲)
    2. trackedLinkId 不明 / 該当 link なし → 旧来の first_tracked_link_id フォールバック(後方互換)
  • DB マイグレーションなし、追加カラムなし
  • リプレイ防止は意図的に入れない。本機能はオプトイン誘導が目的で、エンゲージメントゲートが上流の真のアンチフラウドを担う想定

v0.10.0 → v0.10.1 の挙動差

シナリオ v0.10.0 v0.10.1
既存友だちが新キャンペーンの link → 同じフォームを再 submit 古い (first-touch) キャンペーンの reward 新しいキャンペーンの reward
新キャンペーンの link で reward_template_id=NULL → submit 古いキャンペーンの reward が漏れる(バグ) フォームの on_submit_message_* にフォールバック
?ref= なし(古いリンク経由) first-touch reward first-touch reward(後方互換)

Trade-off

v0.10.0 は friends.first_tracked_link_id の 1 回 pin によって URL 改ざんによる reward 奪取を防いでいました。v0.10.1 はその境界を意図的に緩めます — /api/forms/:id/submitbody.trackedLinkId を信用するため、攻撃者が手動でリクエストを書き換えると別キャンペーンの reward を取得し得ます。本プロジェクトの想定利用文脈ではエンゲージメントゲートで担保するため許容しています。

Implementation

  • apps/worker/src/services/reward-resolver.ts — DB アクセサを引数注入する純粋関数。Vitest で 5 ケース単体テスト同梱
  • apps/worker/src/routes/forms.ts — inline reward 解決を resolveRewardTemplate() 呼び出しに置換
  • apps/worker/src/client/form.ts — LIFF が ?ref= を読み取り、通常経路と Webhook 経路の両方で trackedLinkId を submit body に乗せる

Docs

詳細は docs/wiki/10-Tracked-Links.md の「キャンペーンメッセージ」セクションを参照。