v0.10.1 — Per-campaign reward attribution
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を正として優先解決:trackedLinkId指定 + 該当 link が DB に存在 → その link のreward_template_idを採用(NULL の場合はnullを返してフォームのon_submit_message_*に委譲)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/submit の body.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 の「キャンペーンメッセージ」セクションを参照。