Skip to content

Процесс атрибуции рефералов

Полный процесс отслеживания, атрибуции и начисления рефералов партнерам.

Обзор

Система атрибуции рефералов отслеживает:

  1. Клики по реферальным ссылкам и управление cookie
  2. Захват атрибуции при регистрации
  3. Захват атрибуции при покупках (включая гостевые)
  4. Окно атрибуции и правила
  5. Соображения мультиточечной атрибуции
  6. Хранение данных и отчетность

Основная диаграмма процесса


Детали этапов

1. Структура реферальной ссылки

Стандартный формат ссылки:

https://platform.com/{targetPath}?ref={partnerCode}&utm_source={source}&utm_medium={medium}&utm_campaign={campaign}

Компоненты:

КомпонентОбязательныйПримерОписание
refДаABC123XYРеферальный код партнера
utm_sourceНетinstagramИсточник трафика
utm_mediumНетbioМаркетинговый канал
utm_campaignНетwinter_2024Идентификатор кампании

Примеры ссылок:

# Стандартный реферал
https://platform.com/?ref=ABC123XY

# С UTM-отслеживанием
https://platform.com/products?ref=ABC123XY&utm_source=instagram&utm_medium=post&utm_campaign=winter_sale

# Кастомная посадочная страница
https://platform.com/lp/special-offer?ref=ABC123XY

# Короткая ссылка (через сервис сокращения)
https://iwm.link/r/ABC123XY

Cookie, устанавливаемые при клике по реферальной ссылке:

Имя CookieЗначениеДлительностьHttpOnlySecure
iwm_ref_codeКод партнера30 днейНетДа
iwm_ref_link_idUUID ссылки30 днейНетДа
iwm_first_touchISO timestamp30 днейНетДа
iwm_last_touchISO timestampСессияНетДа
iwm_utmJSON (base64)СессияНетДа

Логика установки Cookie:

typescript
interface AttributionCookies {
  refCode: string;
  linkId?: string;
  firstTouch: string;  // ISO date
  lastTouch: string;   // ISO date
  utm: {
    source?: string;
    medium?: string;
    campaign?: string;
    content?: string;
    term?: string;
  };
  landingPage: string;
}

function setAttributionCookies(params: URLSearchParams): void {
  const refCode = params.get('ref');
  if (!refCode) return;

  const existing = getExistingCookies();
  const now = new Date().toISOString();

  // Атрибуция первого клика: Не перезаписывать если существует
  if (!existing.refCode) {
    setCookie('iwm_ref_code', refCode, { days: 30 });
    setCookie('iwm_first_touch', now, { days: 30 });
  }

  // Всегда обновлять последнее касание
  setCookie('iwm_last_touch', now, { session: true });

  // Сохранить UTM-параметры
  const utm = {
    source: params.get('utm_source'),
    medium: params.get('utm_medium'),
    campaign: params.get('utm_campaign'),
    content: params.get('utm_content'),
    term: params.get('utm_term'),
  };
  setCookie('iwm_utm', btoa(JSON.stringify(utm)), { session: true });
}

Обновление Cookie:

  • iwm_ref_code: Продлевается до 30 дней при каждом визите с ref-параметром
  • iwm_last_touch: Обновляется при каждом визите (если cookie существует)

3. Отслеживание кликов

Эндпоинт: POST /attribution/track-click

Запрос:

json
{
  "partnerCode": "ABC123XY",
  "linkId": "uuid-if-known",
  "landingPage": "/products/featured",
  "referrer": "https://instagram.com",
  "utm": {
    "source": "instagram",
    "medium": "post",
    "campaign": "winter_sale"
  },
  "fingerprint": {
    "userAgent": "Mozilla/5.0...",
    "screenResolution": "1920x1080",
    "timezone": "Europe/Moscow",
    "language": "ru-RU"
  }
}

Запись анонимной атрибуции:

sql
INSERT INTO mlm.anonymous_attributions (
    cookie_id,
    partner_id,
    link_id,
    first_touch_at,
    last_touch_at,
    landing_page,
    referrer_url,
    utm_source,
    utm_medium,
    utm_campaign,
    ip_address,
    user_agent,
    fingerprint_hash
) VALUES (...);

Дедупликация кликов:

  • Тот же IP + User Agent в течение 1 часа = игнорируется
  • Хеш fingerprint используется для более точной дедупликации

4. Захват атрибуции при регистрации

Процесс:

  1. Чтение attribution cookies из запроса
  2. Проверка существования и активности партнера по коду
  3. Создание записи пользователя
  4. Создание referral_attribution, связывающей пользователя с партнером
  5. Обновление денормализованных счетчиков

Создаваемая запись атрибуции:

json
{
  "id": "uuid",
  "userId": "user-uuid",
  "partnerId": "partner-uuid",
  "linkId": "link-uuid-or-null",
  "attributionType": "FIRST_TOUCH",
  "firstTouchAt": "2024-01-10T10:00:00Z",
  "lastTouchAt": "2024-01-15T14:30:00Z",
  "convertedAt": "2024-01-15T14:30:00Z",
  "cookieId": "anon-cookie-id",
  "ipAddress": "192.168.1.1",
  "userAgent": "Mozilla/5.0...",
  "utmSource": "instagram",
  "utmMedium": "post",
  "utmCampaign": "winter_sale"
}

Обновление счетчиков:

sql
-- Обновление статистики реферальной ссылки
UPDATE mlm.referral_links
SET registrations_count = registrations_count + 1
WHERE id = link_id;

-- Обновление статистики партнера
UPDATE mlm.partners
SET direct_referrals_count = direct_referrals_count + 1
WHERE id = partner_id;

5. Захват атрибуции при покупке

Для зарегистрированных пользователей:

typescript
async function getPartnerForOrder(userId: string): Promise<string | null> {
  // Проверить атрибуцию реферала пользователя
  const attribution = await db.referralAttribution.findUnique({
    where: { userId }
  });

  return attribution?.partnerId ?? null;
}

Для гостевых покупок:

typescript
async function getPartnerForGuestOrder(
  sessionId: string,
  cookies: AttributionCookies
): Promise<string | null> {
  // Попробовать найти из cookies
  if (cookies.refCode) {
    const partner = await db.partner.findUnique({
      where: { referralCode: cookies.refCode }
    });
    return partner?.id ?? null;
  }

  // Попробовать найти из корзины
  const cart = await db.cart.findFirst({
    where: { sessionId }
  });
  return cart?.referringPartnerId ?? null;
}

Хранение атрибуции заказа:

sql
-- Заказ хранит реферального партнера
CREATE TABLE product.orders (
    ...
    referring_partner_id UUID REFERENCES mlm.partners(id),
    ...
);

6. Правила окна атрибуции

Конфигурация по умолчанию:

НастройкаЗначениеОписание
Длительность Cookie30 днейВремя действия cookie атрибуции
Модель атрибуцииFirst-touchПервый реферер получает кредит
Окно клика30 днейВремя от клика до конверсии
Множественные покупкиПожизненноПользователь всегда атрибутирован исходному партнеру

First-Touch vs Last-Touch:

МодельОписаниеКогда используется
First-TouchПервый реферер получает весь кредитПо умолчанию для регистраций
Last-TouchПоследний реферер получает весь кредитМожет быть настроен
LinearКредит делится между всеми точками касанияКорпоративная функция

Логика атрибуции:

typescript
function determineAttribution(
  existingCookie: string | null,
  newRefCode: string | null,
  model: 'FIRST_TOUCH' | 'LAST_TOUCH'
): string | null {
  if (model === 'FIRST_TOUCH') {
    // Сохранить оригинальную атрибуцию
    return existingCookie ?? newRefCode;
  } else {
    // Переопределить последней
    return newRefCode ?? existingCookie;
  }
}

7. Мультиточечная атрибуция

Отслеживание множественных точек касания:

sql
CREATE TABLE mlm.attribution_touchpoints (
    id UUID PRIMARY KEY,
    anonymous_id VARCHAR(100),  -- До регистрации
    user_id UUID,               -- После регистрации
    partner_id UUID NOT NULL,
    link_id UUID,
    touched_at TIMESTAMP NOT NULL,
    touchpoint_type VARCHAR(20),  -- CLICK, VISIT, IMPRESSION
    utm_source VARCHAR(100),
    utm_medium VARCHAR(100),
    utm_campaign VARCHAR(100),
    landing_page VARCHAR(500)
);

Запрос точек касания (для отчетности):

sql
SELECT
    ra.user_id,
    COUNT(*) as touchpoint_count,
    MIN(at.touched_at) as first_touch,
    MAX(at.touched_at) as last_touch,
    ARRAY_AGG(DISTINCT p.referral_code) as partner_codes
FROM mlm.attribution_touchpoints at
JOIN mlm.partners p ON p.id = at.partner_id
LEFT JOIN mlm.referral_attributions ra ON ra.user_id = at.user_id
GROUP BY ra.user_id;

8. Пограничные случаи

Сценарий: Пользователь кликает по реферальной ссылке, cookie истекает, затем регистрируется.

Обработка:

  • Атрибуция не создается
  • Пользователь считается "органическим"
  • Может быть вручную атрибутирован администратором при наличии доказательств

Множественные рефереры

Сценарий: Пользователь кликает по ссылке Партнера A, затем по ссылке Партнера B.

First-Touch (по умолчанию):

  • Партнер A получает атрибуцию
  • Клик Партнера B записывается, но не атрибутируется

Last-Touch (альтернатива):

  • Партнер B получает атрибуцию
  • Партнер A теряет потенциальную комиссию

Предотвращение само-реферала

Правила:

  1. Партнер не может получать комиссию со своих покупок
  2. Партнер не может использовать собственную реферальную ссылку

Реализация:

typescript
function validateNotSelfReferral(
  userId: string,
  partnerCode: string
): boolean {
  const userPartner = await db.partner.findUnique({
    where: { userId }
  });

  if (userPartner?.referralCode === partnerCode) {
    return false;  // Попытка само-реферала
  }
  return true;
}

Предотвращение реферала вышестоящих

Правило: Нельзя атрибутировать партнеру из нижестоящей линии (циклическая ссылка)

Реализация:

typescript
async function validateNotDownline(
  newUserPartnerId: string,
  referringPartnerId: string
): Promise<boolean> {
  // Проверить, является ли реферальный партнер нижестоящим нового пользователя
  const isDownline = await db.partnerTreePath.findFirst({
    where: {
      ancestorId: newUserPartnerId,
      descendantId: referringPartnerId,
      depth: { gt: 0 }
    }
  });

  return !isDownline;  // Валидно, если НЕ в нижестоящей линии
}

Конфликты атрибуции корзины

Сценарий: Гостевая корзина имеет атрибуцию Партнера A, пользователь входит с атрибуцией Партнера B.

Разрешение:

  1. Если у пользователя есть существующая атрибуция, использовать ее (постоянная)
  2. Атрибуция корзины вторична
  3. Записать конфликт для анализа

9. Хранение данных атрибуции

Таблицы базы данных:

sql
-- Постоянная атрибуция пользователя
CREATE TABLE mlm.referral_attributions (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL UNIQUE,  -- Одна атрибуция на пользователя
    partner_id UUID NOT NULL,
    link_id UUID,
    attribution_type VARCHAR(20),  -- FIRST_TOUCH, LAST_TOUCH
    first_touch_at TIMESTAMP,
    last_touch_at TIMESTAMP,
    converted_at TIMESTAMP,
    -- Метаданные
    cookie_id VARCHAR(100),
    ip_address INET,
    user_agent TEXT,
    utm_source VARCHAR(100),
    utm_medium VARCHAR(100),
    utm_campaign VARCHAR(100)
);

-- Атрибуция по заказам (для поиска комиссии)
-- Хранится непосредственно в таблице orders как referring_partner_id

Эндпоинт отчета по атрибуции: GET /partners/me/attribution/report

Ответ:

json
{
  "period": {
    "start": "2024-01-01",
    "end": "2024-01-31"
  },
  "summary": {
    "totalClicks": 1250,
    "uniqueVisitors": 890,
    "registrations": 85,
    "conversions": 42,
    "clickToRegistration": 6.8,
    "registrationToConversion": 49.4
  },
  "byLink": [
    {
      "linkId": "uuid",
      "linkName": "Instagram Bio",
      "clicks": 500,
      "registrations": 35,
      "conversions": 18
    }
  ],
  "bySource": [
    {
      "source": "instagram",
      "clicks": 750,
      "registrations": 55,
      "conversions": 28
    }
  ],
  "topCampaigns": [
    {
      "campaign": "winter_sale",
      "clicks": 300,
      "conversions": 15,
      "revenue": 150000.00
    }
  ]
}

Сценарии ошибок

СценарийПоведениеВлияние на пользователя
Неверный код партнераТихий сбой, cookie не устанавливаетсяПользователь продолжает нормально
Партнер неактивен/заблокированАтрибуция не создаетсяПользователь зарегистрирован без реферала
Cookie заблокирован браузеромАтрибуция потерянаНет реферального кредита
Cookie истекАтрибуция потерянаНет реферального кредита
Попытка само-рефералаАтрибуция отклоненаПользователь зарегистрирован без реферала
Попытка циклической ссылкиАтрибуция отклоненаПоказывается сообщение об ошибке

Связанные документы