Prooflytics + DV360: атрибуция programmatic-рекламы до закрытой B2B-сделки

Display & Video 360 (DV360) - корпоративная платформа Google для programmatic-закупки медиа: display, видео, Connected TV, audio, native. Enterprise B2B компании используют DV360 для ABM (Account-Based Marketing), ретаргетинга по CRM-спискам и brand awareness у целевой аудитории. Стандартная атрибуция DV360 - view-through и click-through конверсии через Floodlight пиксель Campaign Manager 360. Проблема: Floodlight фиксирует конверсию на уровне сайта (форма, страница), но не знает - стала ли эта форма закрытой сделкой 60 дней спустя.

Prooflytics соединяет DV360 click/impression data с CRM lifecycle. При закрытии сделки передаёт offline conversion в Campaign Manager 360 - и DV360 учитывает её в оптимизации алгоритма.

Floodlight Activity - тег (пиксель) Campaign Manager 360 для отслеживания конверсий. DV360 использует Floodlight данные для bidding optimization. DCLID (DoubleClick Click ID) - уникальный идентификатор клика из DV360/CM360, аналог gclid для Google Ads.

Архитектура атрибуции

Пользователь видит / кликает DV360 объявление
  -> DCLID добавляется к URL: yoursite.com/demo?dclid=AKEYu...
  -> Ваш Floodlight пиксель срабатывает при посещении
  -> JS захватывает dclid из URL и сохраняет в cookie/localStorage

Форма заявки
  -> dclid передаётся как hidden field
  -> CRM: сделка создана, dclid в custom field

Prooflytics
  -> При Closed Won: Campaign Manager 360 Offline Conversions API
  -> Загрузить: dclid + timestamp + value
  -> DV360 учитывает в algorithm optimization

Захват DCLID на сайте

(function() {
    var params = new URLSearchParams(window.location.search);
    var dclid  = params.get("dclid");
    if (dclid) {
        localStorage.setItem("dclid", dclid);
        document.cookie = "dclid=" + dclid + ";path=/;max-age=2592000";  // 30 дней
    }
    // Также захватить gbraid/wbraid для iOS (Google Ads)
    ["gbraid", "wbraid"].forEach(function(p) {
        var val = params.get(p);
        if (val) localStorage.setItem(p, val);
    });
})();

function getCapturedId() {
    return (
        new URLSearchParams(window.location.search).get("dclid") ||
        localStorage.getItem("dclid") ||
        getCookie("dclid") || ""
    );
}

function getCookie(name) {
    var m = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
    return m ? m[2] : "";
}

Загрузка offline conversion в Campaign Manager 360

import requests, os, time, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

# CM360 OAuth2 credentials (service account)
CM360_SERVICE_ACCOUNT_JSON = os.environ.get("CM360_SERVICE_ACCOUNT_JSON", "")
CM360_PROFILE_ID           = os.environ["CM360_PROFILE_ID"]
CM360_FLOODLIGHT_CONFIG_ID = os.environ["CM360_FLOODLIGHT_CONFIG_ID"]
CM360_ACTIVITY_ID          = os.environ["CM360_FLOODLIGHT_ACTIVITY_ID"]  # "deal_won"

KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN     = os.environ["KOMMO_ACCESS_TOKEN"]
CLOSED_WON_ID   = int(os.environ["KOMMO_CLOSED_WON_ID"])
KOMMO_CF_DCLID  = int(os.environ["KOMMO_CF_DCLID_ID"])

KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

def get_cm360_access_token() -> str:
    import json as json_mod
    from google.oauth2 import service_account
    from google.auth.transport.requests import Request as GRequest
    sa_info  = json_mod.loads(CM360_SERVICE_ACCOUNT_JSON)
    creds    = service_account.Credentials.from_service_account_info(
        sa_info,
        scopes=["https://www.googleapis.com/auth/dfatrafficking"],
    )
    creds.refresh(GRequest())
    return creds.token

def get_lead_dclid_email(lead_id: int) -> tuple[str, str, float]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts,custom_fields_values"},
    )
    lead  = r.json()
    dclid = ""
    for cf in lead.get("custom_fields_values", []) or []:
        if cf.get("field_id") == KOMMO_CF_DCLID:
            vals = cf.get("values", [])
            if vals:
                dclid = vals[0].get("value", "")
                break

    email = ""
    contacts = lead.get("_embedded", {}).get("contacts", [])
    if contacts:
        rc = requests.get(
            f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
            headers=KOMMO_HDR,
            params={"with": "custom_fields_values"},
        )
        for cf in rc.json().get("custom_fields_values", []) or []:
            if cf.get("field_code") == "EMAIL":
                vals = cf.get("values", [])
                if vals:
                    email = vals[0].get("value", "")
                    break

    revenue = float(lead.get("price") or 0)
    return dclid, email, revenue

def sha256_email(email: str) -> str:
    return hashlib.sha256(email.strip().lower().encode()).hexdigest()

def upload_offline_conversion(dclid: str, email: str, revenue: float):
    token = get_cm360_access_token()
    hdrs  = {
        "Authorization": f"Bearer {token}",
        "Content-Type":  "application/json",
    }
    timestamp_micros = int(time.time() * 1_000_000)

    # CM360 Offline Conversion
    body = {
        "kind":                    "dfareporting#conversionsBatchInsertRequest",
        "conversions": [{
            "dclid":                  dclid,
            "floodlightActivityId":   CM360_ACTIVITY_ID,
            "floodlightConfigurationId": CM360_FLOODLIGHT_CONFIG_ID,
            "ordinal":                str(timestamp_micros),
            "timestampMicros":        timestamp_micros,
            "value":                  revenue,
            "quantity":               1,
            "encryptedUserId":        sha256_email(email) if email else None,
        }],
    }
    r = requests.post(
        f"https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/{CM360_PROFILE_ID}/conversions/batchinsert",
        headers=hdrs,
        json=body,
    )
    return r.status_code, r.text

def add_note(lead_id: int, text: str):
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{
            "entity_id":   lead_id,
            "entity_type": "leads",
            "note_type":   "common",
            "params":      {"text": text},
        }],
    )

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
    data = request.json or {}
    for lead_data in data.get("leads", {}).get("status", []):
        lead_id    = lead_data.get("id")
        new_status = lead_data.get("status_id")
        if new_status != CLOSED_WON_ID:
            continue

        dclid, email, revenue = get_lead_dclid_email(lead_id)
        if not dclid:
            continue  # не DV360 источник

        status_code, resp = upload_offline_conversion(dclid, email, revenue)
        add_note(
            lead_id,
            f"CM360 Offline Conversion: загружена. DCLID: {dclid[:20]}... "
            f"Revenue: ${revenue:.2f}. Status: {status_code}",
        )

    return jsonify({"status": "ok"}), 200

Настройка Floodlight Activity в CM360

Campaign Manager 360 -> Floodlight Activities -> New Activity:

  • Тип: Custom
  • Tag string: deal_won (это будет floodlightActivityId)
  • Counting method: Unique

После создания - запишите Activity ID и Floodlight Configuration ID в env.

DV360 и оптимизация по offline conversions

После загрузки offline conversions в CM360 - DV360 автоматически учитывает их при оптимизации ставок. В DV360 Line Item -> Optimization -> выбрать Floodlight Activity “deal_won”. Алгоритм начнёт оптимизироваться под реальные закрытые сделки, а не под форм-подачи.

Это меняет экономику programmatic: вместо оптимизации по CTR или форм-конверсиям DV360 будет показывать рекламу аудиториям, которые статистически чаще превращаются в закрытые сделки.

Роль Prooflytics

Prooflytics автоматизирует этот pipeline для multi-channel: одновременно загружает offline conversions в CM360/DV360, Google Ads (gclid), Meta (fbclid), LinkedIn (li_fat_id) при каждом Closed Won. Без ручной настройки отдельного endpoint для каждого канала.

В примере выше - реализация для single-channel DV360. Подробнее о multi-channel атрибуции через Prooflytics в статье Prooflytics + Amazon DSP.

Для кого актуально

Enterprise B2B компании с DV360-кампаниями: ABM display против target account lists, programmatic видео для brand awareness. Особенно актуально если DV360-бюджет >$20k/мес и нет возможности измерить реальный ROI по каналу через стандартные метрики.

Часто задаваемые вопросы

Нужен ли CM360 отдельно или достаточно DV360?

DV360 использует CM360 как measurement layer. Offline конверсии загружаются через CM360 API, не DV360. CM360 (Campaign Manager) - бесплатно для DV360 клиентов, настраивается через Google Marketing Platform.

Как долго DV360 хранит DCLID для атрибуции?

DCLID валиден для атрибуции 30 дней с момента клика (окно атрибуции по умолчанию). Для B2B с длинным циклом (90+ дней) нужно расширить окно атрибуции в CM360: Floodlight Configuration -> Attribution -> увеличить click-through window до 90 дней. Сохраняйте DCLID в CRM бессрочно - даже если атрибуция не засчитается в CM360, данные полезны для внутренней аналитики.

Какие разрешения нужны service account для CM360 API?

Service Account в Google Cloud -> добавить в CM360 как User (User Profile) с ролью Traffic Manager или выше. Scope: https://www.googleapis.com/auth/dfatrafficking. Роль “Reporting” недостаточна - нужен Traffic Manager для записи конверсий.

Итог

Prooflytics + DV360 - offline conversion attribution для programmatic:

  • Захват DCLID из URL -> localStorage + cookie -> CRM custom field
  • При Closed Won: CM360 Offline Conversions API batchinsert с DCLID + revenue
  • Service Account OAuth2 для CM360 (google-auth библиотека)
  • DV360 оптимизирует ставки под deal_won Floodlight Activity
  • Окно атрибуции увеличить до 90 дней для длинного B2B цикла

Если нужна настройка DV360 offline conversions и Prooflytics attribution - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →