Atrybucja leadów B2B: przekazywanie parametrów UTM do CRM

Management Summary
W sprzedaży do firm zapytanie może pojawić się długo po pierwszym kontakcie z reklamą. Oznaczenia kampanii w linkach pomagają przekazać ten kontekst do CRM, czyli systemu zarządzania kontaktami i szansami sprzedaży. Przy ważnej zgodzie zapis pierwszej i ostatniej zaobserwowanej kampanii ułatwia interpretację późniejszych zapytań. Wspiera to decyzje marketingu i sprzedaży, ale pokazuje tylko fragment drogi klienta i nie dowodzi, że reklama spowodowała zakup.
Spis treści
B2B oznacza sprzedaż między firmami. Lead to kontakt z potencjalnym klientem. Taki kontakt może rozpocząć się od reklamy, a wysłanie formularza nastąpić dopiero po kilku wizytach. Parametrów kampanii ze strony wejściowej często nie ma już wtedy w bieżącym adresie URL. Oddzielny zapis pozwala przekazać takie wartości do formularza, a następnie do CRM, czyli systemu zarządzania kontaktami z klientami i szansami sprzedaży. Rozwiązanie uzupełnia integrację formularza; GA4 nie traci automatycznie zarejestrowanego źródła kampanii przy następnym przejściu na wewnętrzną podstronę.
1. Znaczenie First Touch i Last Touch
Przykład zachowuje najwcześniejszą i najnowszą zaobserwowaną wizytę z prawidłowymi parametrami UTM lub obsługiwanymi identyfikatorami kliknięć. Wizyty bez takich parametrów nie zmieniają zapisów. First Touch oznacza zatem najwcześniejszy kontakt z kampanią nadal dostępny w tej przeglądarce, a Last Touch ostatni zarejestrowany kontakt z kampanią. Atrybucja przypisuje kontakty lub sprzedaż do kanałów marketingowych. Dwa zapisy nie odtwarzają pełnej ścieżki zakupowej ani atrybucji wielokanałowej.
Parametry UTM to oznaczenia kampanii dodane do adresu URL, np. utm_source=newsletter. Pięć pól UTM opisuje źródło, medium, kampanię, słowo kluczowe i treść reklamy. gclid oraz li_fat_id są dodatkowo zapisywane oddzielnie z czasem rejestracji. Późniejsza kampania e-mail może dzięki temu zmienić Last Touch bez usunięcia identyfikatora kliknięcia Google. Inne identyfikatory, w tym wbraid i gbraid, wymagają odrębnie sprawdzonej integracji.
2. Zgoda i termin ważności danych

localStorage to pamięć przeglądarki, którą witryna może odczytywać i zapisywać przez JavaScript. Platforma zarządzania zgodami (CMP) przechowuje decyzje dotyczące zgody. W Niemczech marketingowy zapis atrybucji w localStorage co do zasady wymaga zgody na podstawie § 25 TDDDG. localStorage nie stanowi wyjątku od ochrony informacji na urządzeniu końcowym. Dostęp pozostaje zablokowany do potwierdzenia zgody. Po wycofaniu zgody integracja CMP musi zatrzymać dalsze wykorzystanie, usunąć zapis i wyczyścić uzupełnione wcześniej pola atrybucji w formularzach.
Moduł ES korzysta wyłącznie z localStorage. Przykładowy okres 30 dni biegnie oddzielnie od rejestracji każdego kontaktu lub identyfikatora kliknięcia; nie jest ogólnym zaleceniem retencji. Wygasłe wartości nie trafiają do wyniku, a fizyczne porządkowanie następuje przy kolejnym wywołaniu capture. Usunięcie danych przez przeglądarkę, ochrona przed śledzeniem i błędy pamięci mogą skrócić dostępność. Origin to połączenie protokołu, nazwy hosta i portu adresu internetowego. Zapis dotyczy tego samego originu: inne subdomeny i przeglądarki nie współdzielą tej pamięci.
export function attributionStore({ consent, storage, now = Date.now }) {
const key = 'b2b_attribution_v2';
const ttl = 30 * 86400000;
const utm = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
const ids = ['gclid', 'li_fat_id'];
const valid = value => typeof value === 'string' && value.trim().length > 0
&& value.length <= 500 && !/[\u0000-\u001f\u007f]/.test(value);
function permitted() { try { return consent() === true; } catch { return false; } }
function fresh(record, time) {
return record && Number.isFinite(record.at) && record.at <= time
&& time - record.at < ttl && record.values && typeof record.values === 'object';
}
function read(time) {
const data = JSON.parse(storage().getItem(key) || '{}');
const result = { first: null, last: null, ids: {} };
for (const name of ['first', 'last']) {
if (fresh(data?.[name], time)) {
const values = Object.fromEntries([...utm, ...ids]
.filter(k => valid(data[name].values[k]))
.map(k => [k, data[name].values[k]]));
if (Object.keys(values).length) result[name] = { at: data[name].at, values };
}
}
for (const name of ids) {
const item = data?.ids?.[name];
if (fresh(item, time) && valid(item.values[name])) result.ids[name] = item;
}
return result;
}
return {
capture(search) {
if (!permitted()) return false;
try {
const time = now();
let data;
try { data = read(time); } catch { data = { first: null, last: null, ids: {} }; }
const params = new URLSearchParams(search);
const values = Object.fromEntries([...utm, ...ids]
.filter(k => params.getAll(k).length === 1 && valid(params.get(k)))
.map(k => [k, params.get(k).trim()]));
if (Object.keys(values).length) {
const touch = { at: time, values };
data.first ||= touch;
data.last = touch;
for (const name of ids) if (values[name]) {
data.ids[name] = { at: time, values: { [name]: values[name] } };
}
}
storage().setItem(key, JSON.stringify(data));
return true;
} catch { return false; }
},
fields() {
if (!permitted()) return {};
try {
const data = read(now());
const fields = {};
for (const side of ['first', 'last']) {
for (const name of utm) fields[side + '_' + name] = data[side]?.values[name] || '';
fields[side + '_captured_at'] = data[side] ? new Date(data[side].at).toISOString() : '';
}
for (const name of ids) {
fields[name] = data.ids[name]?.values[name] || '';
fields[name + '_captured_at'] = data.ids[name] ? new Date(data.ids[name].at).toISOString() : '';
}
return fields;
} catch { return {}; }
},
clear() { try { storage().removeItem(key); } catch {} }
};
}
Moduł może być udostępniany jako /assets/utm-store.mjs. Niezmieniony tag Custom HTML nie wykona export/import jako klasycznego skryptu JavaScript. Adapter CMP musi zwracać rzeczywisty, aktualny stan zgody. Bezpieczna konfiguracja początkowa poniżej celowo nie zezwala na zapis.
import { attributionStore } from '/assets/utm-store.mjs';
const attribution = attributionStore({
consent: () => false, // Replace with the verified CMP adapter.
storage: () => window.localStorage
});
// After consent is confirmed, and on relevant SPA navigation:
attribution.capture(window.location.search);
// In the CMP withdrawal handler:
// attribution.clear();
capture wymaga ponownego wywołania po zgodzie, gdy parametry kampanii są jeszcze dostępne. Pojedyncze wywołanie przed zgodą nie rejestruje wizyty wstecznie. Aplikacje SPA zmieniają wyświetlaną treść bez wczytywania całego nowego dokumentu. Wymagają wywołania przy odpowiednich zmianach adresu URL podczas nawigacji. Odczyt parametrów URL nie potrzebuje zdarzenia DOMContentLoaded. Duplikaty, puste lub zbyt długie wartości oraz znaki sterujące są odrzucane. Powtórzenie wywołania dla tego samego oznaczonego adresu stanowi kolejną obserwację; czas rejestracji nie jest zweryfikowanym czasem kliknięcia reklamy.
3. Jawne mapowanie pól formularza
fields() zwraca nazwane pola bez zagnieżdżonych obiektów, takie jak first_utm_source, last_utm_campaign, gclid i gclid_captured_at. Brakujące wartości pozostają puste. Puste pole nie dowodzi wejścia bezpośredniego. Sama obecność gclid również nie uzupełnia automatycznie pól UTM. Wnioskowana klasyfikacja kanału wymaga opisanej reguły, oddzielonej od surowych wartości.
Pole ukryte jest wysyłane wraz z formularzem, ale nie jest widoczne jako pole do wpisywania. We własnym formularzu HTML każde przygotowane pole ukryte może otrzymać data-attribution-key. Funkcja aktualizuje tylko oznaczone pola i usuwa wcześniejsze wartości, gdy zgoda nie jest już dostępna. Obsługa wysłania formularza musi wywołać funkcję przed utworzeniem danych żądania; biblioteki asynchroniczne wymagają odpowiedniego, udokumentowanego mechanizmu integracji.
function fillAttribution(form) {
const values = attribution.fields();
for (const field of form.querySelectorAll('input[type=hidden][data-attribution-key]')) {
field.value = values[field.dataset.attributionKey] || '';
}
}
// Example field in a custom form:
// <input type="hidden" name="campaign_source" data-attribution-key="first_utm_source">
Gravity Forms od wersji 2.9 udostępnia natywne zdarzenie gform/post_render. Starsze gform_post_render jest zdarzeniem jQuery; wpisanie tej nazwy do document.addEventListener nie zapewnia równoważnej obsługi. Zdarzenia i mapowanie pól muszą odpowiadać zainstalowanej wersji. Nazwa właściwości CRM nie staje się automatycznie nazwą pola HTML Gravity Forms. Aktualne formularze HubSpot udostępniają instancje formularzy i setFieldValue. Iframe osadza inną stronę wewnątrz bieżącej strony. Ogólne wyszukiwanie w drzewie HTML głównego dokumentu nie dociera do iframe z innego originu. Contact Form 7 również wymaga skonfigurowanych pól i poprawnego przekazania wartości.
4. CRM i konwersje offline
Backend ponownie sprawdza otrzymane wartości: pola ukryte i parametry URL można modyfikować. Imiona, adresy e-mail i inne dane osobowe nie powinny trafiać do UTM. Właściwości CRM, pola formularza i pola API wymagają jawnego mapowania; zgodne etykiety nie tworzą połączenia między systemami.
Dla istniejących kontaktów potrzebne są reguły zachowania najwcześniejszego znanego kontaktu, zapisu późniejszych kampanii i łączenia rekordów. Powiązanie kontaktu z transakcją wymaga jawnej konfiguracji. Grupa kanałów Paid Social (płatne reklamy w mediach społecznościowych) opisuje pochodzenie. Lead, Opportunity i Closed-Won to etapy sprzedaży: potencjalny kontakt, konkretna szansa sprzedaży i zakończona sprzedaż. To odrębne wymiary.
Konwersja offline to wynik zarejestrowany poza witryną, np. sprzedaż potwierdzona w CRM. Obsługa duplikatów zapobiega wielokrotnemu policzeniu tego samego wyniku. Konwersje offline wymagają oddzielnej integracji z akcją konwersji, rzeczywistym czasem konwersji, odpowiednim identyfikatorem, wymaganą zgodą i obsługą duplikatów. Sam zapis ID w CRM nie uruchamia wysyłki. Google podaje maksymalnie 90 dni od powiązanego kliknięcia dla klasycznego importu offline oraz 63 dni dla konwersji rozszerzonych dotyczących potencjalnych klientów; obowiązuje również ustawione okno konwersji. Określa ono, jak długo po interakcji z reklamą można jeszcze przypisać do niej wynik. Okres zapisu w przeglądarce jest niezależny. Wcześniejsze kwalifikowane etapy sprzedaży mogą stanowić osobne akcje konwersji, jeśli definicje i wartości odpowiadają celowi optymalizacji.
W LinkedIn parametr li_fat_id w adresie strony docelowej wymaga włączenia Enhanced conversion tracking dla Insight Tag w Campaign Managerze. Conversions API przyjmuje identyfikator jako LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID. Dostępność identyfikatora dopasowania nie dowodzi jeszcze konwersji w oknie atrybucji.
5. Sprawdzenie przed wdrożeniem
Ścieżka testowa prowadzi od kampanii Google przez nieoznaczoną podstronę i późniejszą kampanię e-mail do formularza. First Touch pozostaje Google, Last Touch zmienia się na e-mail, a niewygasły gclid pozostaje zapisany oddzielnie. Kolejne próby obejmują odmowę i wycofanie zgody, wygasłe i uszkodzone dane, zablokowaną pamięć, formularze AJAX i rzeczywiste pola CRM. Dziesięć izolowanych testów JavaScript sprawdza logikę zapisu; testy nie obejmują produkcyjnej integracji CMP, formularza ani CRM.
Pytania i odpowiedzi
Czy Safari zachowa dane przez 30 dni?
Taki okres nie jest gwarantowany. WebKit może usunąć pamięć zapisywalną przez skrypty po siedmiu dniach korzystania z przeglądarki bez interakcji z witryną. Ciasteczka ustawiane przez serwer nie są uniwersalnym obejściem: ograniczenia mogą obejmować również śledzenie wykorzystujące CNAME lub adresy IP. Zgodnie z prawem przekazana kopia w CRM nie zależy już od pamięci przeglądarki, lecz nadal wymaga własnych zasad retencji i usuwania.
Czy First Touch i Last Touch oznaczają pełną atrybucję?
Dwa punkty styku nie tworzą pełnej sekwencji zdarzeń. Przykład zachowuje ponadto tylko ostatni zaobserwowany obsługiwany identyfikator kliknięcia danej platformy. Wiele kliknięć, zmiany urządzeń i łączenie kontaktów wymagają rozbudowanego modelu z odpowiednią ochroną danych. Wynik opisuje zaobserwowane sygnały kampanii, a nie udowodniony wpływ na przychód.