게임화 및 초대

Gamification 플러그인을 사용할 경우, 초대 코드를 사용한 사용자에게 포인트가 지급됩니다.
이는 가짜(복제) 계정을 만들어 초대를 보낸 후 해당 계정으로 코드를 사용함으로써 악용하기 쉽습니다.
관리자가 이러한 가짜 계정을 삭제할 때, Gamification 플러그인(및 초대 시스템)의 동작은 초대가 어떻게 보내졌는지에 따라 달라집니다.

  • 초대가 특정 이메일 주소에 연결되어 있었고 사용자가 삭제된 경우, invite 레코드와 user_invite 레코드가 모두 하드(완전) 삭제됩니다. 이후 Sidekiq의 UpdateScoresFor* 작업이 다음으로 실행될 때, Gamification 플러그인은 이전에 지급된 포인트를 점수에서 차감합니다.

  • 초대가 특정 이메일 주소에 연결되지 않았고 사용자가 삭제된 경우, invited_users 레코드는 삭제되지만 invite 레코드는 유지됩니다. redemption_count 값은 감소하지 않습니다. 이는 사용자가 나중에 삭제되었더라도 초대 사용(redemption)이 실제로 발생했기 때문에 한 가지 관점에서는 타당합니다. 그러나 Gamification 플러그인은 redemption_count를 기반으로 점수를 계산하므로, 포인트는 차감되지 않습니다.

이 문제를 디버깅하는 과정에서, 점수 작업이 10일 이내의 초대만 고려한다는 사실도 발견했습니다. 따라서 초대가 11일 이후에 사용되면 포인트가 전혀 지급되지 않습니다. '초대 사용 시 updated_at이 갱신되지 않으므로 created_at 대신 updated_at을 확인해야 할 것 같다’고 말하려 했으나, 실제로는 초대의 redemption_count가 증가할 때 updated_at 타임스탬프가 업데이트되지 않는 것을 확인했습니다.

11개의 좋아요

이런 방향으로 가는 것이 더 나을까요 (점수화 가능한 쿼리 형식에 맞게 조정했습니다 - 데이터 탐색기를 위한 테스트용 쿼리입니다 :slight_smile:):

-- [params]
-- date :start_date
-- date :end_date

SELECT 
    invited_by_id AS user_id,
    COUNT(*) AS user_invites,
    COUNT(*) * 10 AS invite_score
FROM invited_users iu
  JOIN invites i ON i.id = iu.invite_id
  JOIN users u ON u.id = iu.user_id
WHERE iu.redeemed_at::date BETWEEN :start_date AND :end_date
  AND iu.user_id <> i.invited_by_id 
  AND u.created_at > iu.redeemed_at
GROUP BY invited_by_id
ORDER BY user_invites DESC

사용자가 삭제되면 invited_users 테이블에서 해당 기록이 제거되므로, 더 이상 카운트에 포함되지 않습니다. 삭제가 10일 이내에 이루어진 경우 자동으로 교정되지만, 10일 이상 지난 경우 수동으로 점수를 새로고침해야 합니다.

redeemed_at 날짜를 사용하면 10일 전에 생성된 초대 코드도 정확히 계산할 수 있습니다.

AND iu.user_id <> i.invited_by_id 조건은 자기 자신에 대한 초대를 제외합니다.

users 테이블을 조인하고 AND u.created_at > iu.redeemed_at 조건을 추가하면 기존 사용자가 초대를 받은 경우를 제외할 수 있습니다.

3개의 좋아요

좋은 접근 방식이지만, 한 가지 예외가 있습니다:

이 방법은 잘 작동하지 않습니다. 가끔 사용자는 리디엠션(redeem)이 발생한 시점보다 약간 먼저 생성됩니다. 이유를 모르겠습니다. 대부분 0.1초 단위의 차이이지만, 수 초 단위의 차이도 몇 건 발견했습니다.

실제 데이터베이스에서 테스트했습니다.

select 
  iu.redeemed_at iu_AS redeemed_at, 
  u.created_at AS u_created_at, 
  u.created_at > iu.redeemed_at AS u_created_gt_iu_redeemed 
from invited_users iu 
left join users u on u.id = iu.user_id 
where iu.redeemed_at is not null
order by iu.id desc;
       iu_redeemed_at       |        u_created_at        | u_created_gt_iu_redeemed 
----------------------------+----------------------------+--------------------------
 2023-09-08 00:00:47.557057 | 2023-09-08 00:00:48.376446 | t
 2023-08-25 20:09:03.486362 | 2023-08-25 20:09:03.201357 | f
 2023-08-15 23:38:32.271709 | 2023-08-15 23:38:33.570299 | t
 2023-08-14 10:44:34.19912  | 2023-08-14 10:44:35.429371 | t
 2023-08-12 13:41:10.428013 | 2023-08-12 13:41:11.733973 | t
 2023-07-31 17:58:13.511289 | 2023-07-31 17:57:50.427111 | f
 2023-07-23 00:56:33.455185 | 2023-07-23 00:55:47.999263 | f
 2023-07-19 08:42:44.908096 | 2023-07-19 08:42:46.040201 | t
 2023-06-30 09:11:38.829692 | 2023-06-30 09:11:39.618586 | t
 2023-06-30 08:37:02.322192 | 2023-06-30 08:37:03.133769 | t
 2023-06-29 16:24:01.705616 | 2023-06-29 16:24:02.55067  | t
 2023-06-29 12:53:33.245688 | 2023-06-29 12:53:34.067159 | t

1개의 좋아요

흥미롭네요. 그 부분은 확실히 고려하지 못했네요. :slight_smile:

AND u.created_at + INTERVAL '1 SECOND' > iu.redeemed_at 와 같은 작은 버퍼를 추가하면, 큰 영향을 주지 않으면서 그 문제를 상쇄할 수 있을까요?

지연의 원인을 정확히 알 수 없는 상황에서, 이 방법은 다소 "블랙 매직"에 가깝다고 생각합니다.

지연의 원인이 대규모 sidekiq 큐나 비슷한 요인일 것으로 추정됩니다. 대부분의 지연은 1초 미만이지만, 약 3%는 0:00:01 ~ 0:15:00(1초에서 15분 사이) 범위에 분포합니다.

그리고 0.5%는 며칠 단위의 지연을 보이며, 이는 우리가 방지하려는 유형의 악용에 해당합니다. 따라서 이 방법은 효과적이긴 하지만, 오탐(false positives)이 너무 많아져서 해가 될 가능성이 더 큽니다.

2개의 좋아요

이것이 문제를 해결해야 합니다:

1개의 좋아요

이것은 버그보다는 다른 기능 요청처럼 느껴집니다. (cc @Falco)

이 주제는 3일 후 자동으로 닫혔습니다. 더 이상 새 답변을 남길 수 없습니다.