Одна заявка может дважды попасть в отчёт: событие отправилось из браузера и сервера, тег сработал повторно после обновления страницы или аналитика посчитала каждое срабатывание как отдельную конверсию. До расчёта CPL разделите три вопроса: сколько событий принято, сколько уникальных лидов создано и по какому правилу площадка показывает конверсии.
Сначала проверяют сбор события, затем правило подсчёта и только после этого — CPL. Цифры на схеме условные.
Почему счётчик конверсий не равен числу заявок
Событие аналитики и заявка в CRM — разные сущности. Событие сообщает, что произошло действие; CRM создаёт запись лида. Один лид может породить несколько событий, а одно событие иногда не приводит к созданию лида из-за ошибки интеграции.
В Google Analytics 4 ключевое событие можно считать один раз за событие или один раз за сеанс. Google Ads для действия-конверсии предлагает варианты «каждая» и «одна» после рекламного взаимодействия. Эти настройки отвечают на вопрос, как показать уже полученные сигналы в отчёте. Они не исправляют двойную отправку тега и не доказывают, что две записи относятся к одному лиду.
Поэтому проверка начинается не с переключателя в кабинете, а с устойчивого идентификатора результата — lead_id из CRM или формы. Для технической доставки нужен отдельный event_row_id: он помогает заметить повтор одной и той же записи.
Четыре причины двойной конверсии
| Причина | Что видно в данных | Что проверить |
|---|---|---|
| Тег сработал дважды | два разных события с одним lead_id и почти одинаковым временем | обработчик кнопки, отправку при submit и переход на страницу «спасибо» |
| Событие отправили браузер и сервер | один lead_id, разные источники web и server | общий идентификатор события и правило дедупликации |
| Пользователь повторил отправку | события близки по времени, но lead_id отсутствует или различается | CRM, форму, защиту кнопки и фактический бизнес-результат |
| Выбрано другое правило подсчёта | сырые события те же, число конверсий в колонке меняется | «за событие / за сеанс» в GA4 или «каждая / одна» в Ads |
Одинаковое время само по себе не доказывает дубль. Пользователь вправе отправить две разные заявки, например по двум товарам. Без lead_id такую пару лучше пометить для проверки, а не удалять автоматически.
Воспроизводимый учебный пример
В примере восемь событий generate_lead. Две строки имеют подтверждённый признак повтора: один event_row_id встретился дважды, а один lead_id пришёл в двух разных событиях. Ещё две строки относятся к одной форме и одному сеансу в пределах десяти минут, но без lead_id; скрипт отправляет вторую на ручную проверку.
Сохраните данные как lead-events.csv:
event_row_id,event_time_utc,event_name,lead_id,session_id,form_id,source
evt-001,2026-10-08T07:00:00Z,generate_lead,L-1001,s-10,callback,web
evt-002,2026-10-08T07:00:01Z,generate_lead,L-1001,s-10,callback,web
evt-003,2026-10-08T07:05:00Z,generate_lead,L-1002,s-11,brief,web
evt-003,2026-10-08T07:05:00Z,generate_lead,L-1002,s-11,brief,server
evt-004,2026-10-08T07:10:00Z,generate_lead,,s-12,demo,web
evt-005,2026-10-08T07:12:00Z,generate_lead,,s-12,demo,web
evt-006,2026-10-08T08:00:00Z,generate_lead,L-1003,s-13,quote,web
evt-007,2026-10-08T09:00:00Z,generate_lead,L-1004,s-14,callback,webСкрипт использует только стандартную библиотеку Python. Он не меняет исходный CSV: рядом создаётся отчёт со статусом и причиной решения.
#!/usr/bin/env python3
import csv
import sys
from datetime import datetime, timedelta, timezone
def parse_time(value):
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
def main(source_path, report_path):
with open(source_path, encoding="utf-8", newline="") as source:
rows = list(csv.DictReader(source))
seen_event_ids = set()
first_by_lead = {}
recent_without_lead = {}
result = []
for row in sorted(rows, key=lambda item: (parse_time(item["event_time_utc"]), item["event_row_id"])):
event_id = row["event_row_id"].strip()
lead_id = row["lead_id"].strip()
key = (row["session_id"].strip(), row["form_id"].strip())
occurred_at = parse_time(row["event_time_utc"])
if event_id in seen_event_ids:
status = "DUPLICATE_EVENT_ID"
reason = "тот же идентификатор события уже встречался"
elif lead_id and lead_id in first_by_lead:
status = "DUPLICATE_LEAD_ID"
reason = f"lead_id уже был в строке {first_by_lead[lead_id]}"
elif not lead_id and key in recent_without_lead and occurred_at - recent_without_lead[key] <= timedelta(minutes=10):
status = "REVIEW_NEAR_REPEAT"
reason = "тот же сеанс и форма в пределах 10 минут; автоматического удаления нет"
else:
status = "KEEP"
reason = "подтверждённого признака дубля нет"
seen_event_ids.add(event_id)
if lead_id and lead_id not in first_by_lead:
first_by_lead[lead_id] = event_id
if not lead_id:
recent_without_lead[key] = occurred_at
result.append({**row, "audit_status": status, "audit_reason": reason})
with open(report_path, "w", encoding="utf-8", newline="") as target:
writer = csv.DictWriter(target, fieldnames=list(rows[0]) + ["audit_status", "audit_reason"])
writer.writeheader()
writer.writerows(result)
counts = {}
for row in result:
counts[row["audit_status"]] = counts.get(row["audit_status"], 0) + 1
print(f"raw_events={len(result)}")
for status in ("KEEP", "REVIEW_NEAR_REPEAT", "DUPLICATE_EVENT_ID", "DUPLICATE_LEAD_ID"):
print(f"{status.lower()}={counts.get(status, 0)}")
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("usage: audit_lead_events.py lead-events.csv lead-events-audit.csv")
main(sys.argv[1], sys.argv[2])Запуск:
python audit_lead_events.py lead-events.csv lead-events-audit.csvРезультат:
raw_events=8
keep=5
review_near_repeat=1
duplicate_event_id=1
duplicate_lead_id=1Порог десять минут — редакционное условие учебного примера, а не правило GA4 или Google Ads. В рабочем процессе его выбирают по поведению формы и скорости обработки заявок.
Как проверить причину в интерфейсах
1. Воспроизведите одну отправку
Откройте форму в тестовом режиме, отправьте её один раз и запишите время. В GA4 DebugView проверьте поток событий с точным временем и параметрами. Если generate_lead появился дважды до передачи в рекламный кабинет, причина находится в реализации события или параллельной отправке.
2. Сверьте событие с CRM
Для каждой тестовой заявки сохраните lead_id, время создания и форму. Свяжите его с событием аналитики. Не используйте email или телефон как открытый ключ в выгрузке: передавайте внутренний непрозрачный идентификатор и соблюдайте правила работы с персональными данными.
3. Проверьте правило подсчёта
Для лидогенерации часто важна одна уникальная заявка, а для продаж — каждая покупка. Однако выбор «одна» в Google Ads ограничивает подсчёт на уровне взаимодействия, а «один раз за сеанс» в GA4 — на уровне сеанса. Ни один вариант не равен автоматически уникальному lead_id из CRM.
Изменение способа подсчёта применяется к будущим данным. Оно не пересчитывает историю, поэтому дату настройки нужно сохранить рядом с отчётом.
4. Пересчитайте CPL на трёх уровнях
В учебном примере расход — 240 BYN. По восьми сырым событиям CPL равен 30 BYN. По пяти подтверждённым уникальным лидам — 48 BYN. Если событие со статусом REVIEW_NEAR_REPEAT после проверки окажется отдельной заявкой, итог будет шесть лидов и CPL 40 BYN.
В отчёте полезно держать три колонки: raw_events, confirmed_leads, review_events. Тогда спор о CPL превращается в проверку конкретных строк. Формулу и правила включения расходов можно сверить с ответом «Что такое CPL и как рассчитать стоимость лида?».
Что исправить после аудита
Если повтор появляется уже в браузере, уберите второй обработчик, запретите повторное нажатие кнопки до ответа сервера и проверьте страницу «спасибо». Если одно действие отправляют браузер и сервер, согласуйте единый идентификатор события и дедупликацию на стороне приёма.
Если событие одно, а число конверсий отличается только между отчётами, зафиксируйте способы подсчёта и сравнивайте одинаковые колонки. Для конверсии сайта также нужен единый знаменатель — клики, сеансы или пользователи; этот выбор разобран в материале «Как считать конверсию сайта».
Не удаляйте исторические строки без следа. Храните сырой слой, статус аудита, причину исключения и версию правила. Так можно восстановить исходный отчёт и объяснить, почему CPL изменился после проверки качества данных.
Источники
Материал подготовлен редакцией WiseLab с помощью AI. Источники и фактические утверждения проверены при подготовке.
Что думаете?
Загружаем комментарии…