Процесс атрибуции рефералов
Полный процесс отслеживания, атрибуции и начисления рефералов партнерам.
Обзор
Система атрибуции рефералов отслеживает:
- Клики по реферальным ссылкам и управление cookie
- Захват атрибуции при регистрации
- Захват атрибуции при покупках (включая гостевые)
- Окно атрибуции и правила
- Соображения мультиточечной атрибуции
- Хранение данных и отчетность
Основная диаграмма процесса
Детали этапов
1. Структура реферальной ссылки
Стандартный формат ссылки:
https://platform.com/{targetPath}?ref={partnerCode}&utm_source={source}&utm_medium={medium}&utm_campaign={campaign}Компоненты:
| Компонент | Обязательный | Пример | Описание |
|---|---|---|---|
| ref | Да | ABC123XY | Реферальный код партнера |
| utm_source | Нет | Источник трафика | |
| 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/ABC123XY2. Управление Cookie
Cookie, устанавливаемые при клике по реферальной ссылке:
| Имя Cookie | Значение | Длительность | HttpOnly | Secure |
|---|---|---|---|---|
iwm_ref_code | Код партнера | 30 дней | Нет | Да |
iwm_ref_link_id | UUID ссылки | 30 дней | Нет | Да |
iwm_first_touch | ISO timestamp | 30 дней | Нет | Да |
iwm_last_touch | ISO timestamp | Сессия | Нет | Да |
iwm_utm | JSON (base64) | Сессия | Нет | Да |
Логика установки Cookie:
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
Запрос:
{
"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"
}
}Запись анонимной атрибуции:
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. Захват атрибуции при регистрации
Процесс:
- Чтение attribution cookies из запроса
- Проверка существования и активности партнера по коду
- Создание записи пользователя
- Создание referral_attribution, связывающей пользователя с партнером
- Обновление денормализованных счетчиков
Создаваемая запись атрибуции:
{
"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"
}Обновление счетчиков:
-- Обновление статистики реферальной ссылки
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. Захват атрибуции при покупке
Для зарегистрированных пользователей:
async function getPartnerForOrder(userId: string): Promise<string | null> {
// Проверить атрибуцию реферала пользователя
const attribution = await db.referralAttribution.findUnique({
where: { userId }
});
return attribution?.partnerId ?? null;
}Для гостевых покупок:
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;
}Хранение атрибуции заказа:
-- Заказ хранит реферального партнера
CREATE TABLE product.orders (
...
referring_partner_id UUID REFERENCES mlm.partners(id),
...
);6. Правила окна атрибуции
Конфигурация по умолчанию:
| Настройка | Значение | Описание |
|---|---|---|
| Длительность Cookie | 30 дней | Время действия cookie атрибуции |
| Модель атрибуции | First-touch | Первый реферер получает кредит |
| Окно клика | 30 дней | Время от клика до конверсии |
| Множественные покупки | Пожизненно | Пользователь всегда атрибутирован исходному партнеру |
First-Touch vs Last-Touch:
| Модель | Описание | Когда используется |
|---|---|---|
| First-Touch | Первый реферер получает весь кредит | По умолчанию для регистраций |
| Last-Touch | Последний реферер получает весь кредит | Может быть настроен |
| Linear | Кредит делится между всеми точками касания | Корпоративная функция |
Логика атрибуции:
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. Мультиточечная атрибуция
Отслеживание множественных точек касания:
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)
);Запрос точек касания (для отчетности):
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
Сценарий: Пользователь кликает по реферальной ссылке, cookie истекает, затем регистрируется.
Обработка:
- Атрибуция не создается
- Пользователь считается "органическим"
- Может быть вручную атрибутирован администратором при наличии доказательств
Множественные рефереры
Сценарий: Пользователь кликает по ссылке Партнера A, затем по ссылке Партнера B.
First-Touch (по умолчанию):
- Партнер A получает атрибуцию
- Клик Партнера B записывается, но не атрибутируется
Last-Touch (альтернатива):
- Партнер B получает атрибуцию
- Партнер A теряет потенциальную комиссию
Предотвращение само-реферала
Правила:
- Партнер не может получать комиссию со своих покупок
- Партнер не может использовать собственную реферальную ссылку
Реализация:
function validateNotSelfReferral(
userId: string,
partnerCode: string
): boolean {
const userPartner = await db.partner.findUnique({
where: { userId }
});
if (userPartner?.referralCode === partnerCode) {
return false; // Попытка само-реферала
}
return true;
}Предотвращение реферала вышестоящих
Правило: Нельзя атрибутировать партнеру из нижестоящей линии (циклическая ссылка)
Реализация:
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.
Разрешение:
- Если у пользователя есть существующая атрибуция, использовать ее (постоянная)
- Атрибуция корзины вторична
- Записать конфликт для анализа
9. Хранение данных атрибуции
Таблицы базы данных:
-- Постоянная атрибуция пользователя
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
Ответ:
{
"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 истек | Атрибуция потеряна | Нет реферального кредита |
| Попытка само-реферала | Атрибуция отклонена | Пользователь зарегистрирован без реферала |
| Попытка циклической ссылки | Атрибуция отклонена | Показывается сообщение об ошибке |