Wszystkie wpisy

17 czerwca 2026

POAS zamiast ROAS — marża brutto w tagach konwersji przez sGTM

ROAS 4× wygląda świetnie na dashboardzie. Problem w tym, że ROAS liczy obrót, nie zysk. Sprzedajesz koszulkę za 95 zł z marżą 35 zł i drugą za 95 zł z marżą 8 zł, a algorytm Google Ads i Meta widzi obie tak samo: 95 zł przychodu. Licytuje pod obrót, dosypuje budżet tam, gdzie sprzedaż największa, niekoniecznie tam, gdzie faktycznie zarabiasz.

POAS zamiast ROAS zmienia to, co platforma reklamowa dostaje jako wartość konwersji. Zamiast przychodu wysyłasz marżę brutto zamówienia. Algorytm zaczyna optymalizować pod rentowność, a nie pod sam obrót. I robisz to po stronie serwera — bez przebudowy dataLayer i bez dotykania struktury samych tagów.

To piąty wariant wzorca, który rozkładam w tej serii (sGTM + Stape Store jako mini-CDP). Cztery poprzednie wzbogacały zdarzenia danymi z magazynu. Ten nie dodaje danych identyfikujących. Podmienia jedną liczbę: value.

Dlaczego to robić na serwerze — a nie w dataLayer

Najprostsza myśl brzmi: „przeliczę marżę we front-endzie i wepchnę ją do dataLayer”. W praktyce to zła droga. Marże to dane wrażliwe biznesowo, których nie chcesz mieć w kodzie strony dostępnym dla każdego przez DevTools. Zmieniają się częściej niż katalog, więc każda korekta oznaczałaby deploy front-endu. A jeśli masz kilka platform reklamowych, logikę przeliczania musiałbyś utrzymywać w wielu miejscach naraz.

Server-side rozwiązuje wszystkie trzy problemy jednym ruchem. Marże leżą w magazynie po stronie serwera, niewidoczne dla przeglądarki. Aktualizujesz je niezależnie od kodu strony. A samo przeliczenie dzieje się raz — w jednym punkcie pipeline’u sGTM — tuż przed wysyłką do platform.

Skąd serwer bierze marżę — feed CSV w Stape Store

Punkt wyjścia to katalog marż per SKU. Stape udostępnia do tego dedykowany power-up POAS Data Feed, który ładuje plik CSV do Stape Store i synchronizuje go cyklicznie. Plik jest banalnie prosty: dwie kolumny, identyfikator produktu i marża jednostkowa brutto.

item_id,margin
SKU_12345,18.50
SKU_12346,42.00
SKU_99999,7.30

W panelu Stape (Power-ups → POAS Data Feed) wskazujesz publiczny URL tego pliku, ustawiasz separator na przecinek i mapujesz kolumny. Jedno ustawienie wymaga uwagi: Value Type. Wybierasz Absolute, gdy w CSV trzymasz gotową wartość marży w walucie. Tryb Percent każe Stape liczyć marżę jako procent ceny pozycji. Pomylenie tych dwóch trybów daje cichy błąd w wartościach — bez żadnego ostrzeżenia.

Feed musi być publicznie dostępny, bo Stape pobiera go sam, cyklicznie. W praktyce serwuję go z eksportu Google Sheets albo z feedu platformy e-commerce. Jeśli URL przestanie odpowiadać i poleci 404, power-up nie zaktualizuje katalogu i zmienna zacznie pracować na nieaktualnych danych albo zwracać zera.

Jeden lookup, który sumuje marżę całego zamówienia

Mając katalog w Store, dokładam zmienną typu Stape Store Margin Lookup (galeria szablonów, stape-io). Nazywam ją CDP 1. (Use case 5) margin (all items) — konfiguracja to dwa pola:

  • Items Array Source: GA4 items array from Event Data. Zmienna iteruje po tablicy items[] z bieżącego zdarzenia purchase.
  • Field to be used as the Item ID: item_id. Po tym polu szuka rekordu w katalogu.

Mechanizm jest taki: dla każdej pozycji w koszyku zmienna znajduje w katalogu marżę jednostkową, mnoży ją przez quantity tej pozycji i sumuje wszystkie pozycje. Wynikiem jest łączna marża brutto zamówienia jako pojedyncza liczba. Stąd dwa wnioski praktyczne. Po pierwsze — w CSV trzymasz marżę na jednostkę, nie na zamówienie, bo mnożenie przez ilość robi już sama zmienna. Po drugie — jeśli żadna pozycja nie znajdzie dopasowania w katalogu, zmienna zwraca 0, nie błąd.

Jedna transformacja, która podmienia wartość konwersji

Teraz najciekawszy element architektury. Zamiast wstawiać zmienną Margin Lookup wprost do każdego taga, używam jednej transformacji typu „Uzupełnij zdarzenie” o nazwie CDP 2. (Use case 5) Transformation Value. Ma ona jeden parametr:

PoleWartość
Nazwavalue
Wartość{{CDP 1. (Use case 5) margin (all items)}}
Tagi objętetag Google Ads i tag Meta z tej serii

Powód tego wyboru jest kosztowy. Każde odwołanie do zmiennej Margin Lookup to osobne zapytanie do Stape Store. Gdyby do zmiennej sięgał bezpośrednio tag Google Ads, a potem osobno tag Meta, byłyby to dwa płatne zapytania na jedno zamówienie. Transformacja przelicza wartość raz i nakłada ją na wszystkie objęte tagi. Jeden request, dowolna liczba platform.

Działa to, bo „Uzupełnij zdarzenie” modyfikuje dane zdarzenia w pipeline sGTM zanim którykolwiek tag odczyta swoje parametry. Sekwencja na pojedynczym zamówieniu wygląda tak:

  1. Zdarzenie purchase przychodzi z value = 72.05 (przychód z ecommerce schema).
  2. Transformacja nadpisuje w danych zdarzenia: value = 34.20 (suma marż z lookupu).
  3. Tag Google Ads i tag Meta czytają {{value}} i dostają już 34.20.

Tagi konwersji zostają prawie nietknięte

W finalnych tagach (Google Ads Conversion i Meta CAPI Purchase) pole wartości konwersji wskazuje na generyczną zmienną {{value}}. Dokładnie tak, jak wskazywało wcześniej, gdy wysyłałeś przychód. Nie zmieniasz tu nic. Transformacja podmienia zawartość value w tle, więc tag „nie wie”, że dostaje marżę zamiast obrotu. To jest sens całego podejścia: warstwa biznesowa (czym jest wartość konwersji) jest odseparowana od konfiguracji tagów.

Jedyna rzecz, którą warto rozważyć, to czy podmieniać wartość w istniejących tagach, czy zrobić ich dedykowane kopie pod POAS. Osobne tagi dają czystszą izolację, gdy chcesz przez jakiś czas porównać oba sygnały albo stopniowo przełączać kampanie. Podmiana w istniejących jest prostsza, gdy decyzja o przejściu na POAS jest ostateczna.

Gdzie to się sypie

Konfiguracja jest krótka, ale ma kilka miejsc, w których błąd jest cichy. Nie wywali buildu, nie zapali się na czerwono w GTM Preview, a zobaczysz go dopiero w raportach reklamowych. Dlatego rozpisuję je osobno.

Rozjazd item_id między CSV a dataLayer — najgroźniejszy. Jeśli w items[] jedzie item_id: "SKU_123", a w pliku marż klucz brzmi "123", lookup nie trafi w żaden wiersz i zwróci 0. Tag wyśle konwersję o wartości zero. W podglądzie wszystko wygląda poprawnie, bo transformacja odpala się i nakłada liczbę, tyle że tą liczbą jest zero. Przed wdrożeniem porównaj dosłowny string identyfikatora z dataLayer z pierwszą kolumną CSV. Spacja, prefiks, wiodące zero, wszystko ma znaczenie.

Marża per zamówienie zamiast per jednostka — zmienna mnoży wartość z CSV przez quantity. Jeśli wrzucisz do pliku marżę całego zamówienia, wynik napompuje się przy każdej ilości większej niż jeden.

Value Type ustawiony na Percent — opisany wyżej. Przy Percent Stape interpretuje liczbę z CSV jako procent ceny i sam dolicza marżę. Przy Absolute bierze ją jako gotową wartość.

Feed pod URL-em zwraca 404 — katalog przestaje się aktualizować. Marże zamrażają się na ostatniej udanej synchronizacji albo lecą zera. Panel Stape zgłasza ten błąd przy próbie odświeżenia, więc po każdej zmianie feedu sprawdź datę ostatniej synchronizacji.

Jak to zweryfikować przed produkcją

W GTM Preview odpal testowe zamówienie i sprawdź trzy rzeczy. Wartość zmiennej Margin Lookup ma być niezerowa i zgodna z ręcznie policzoną sumą marża × ilość dla pozycji z koszyka. W szczegółach taga, w sekcji parametrów do nadpisania, pole value ma pokazywać marżę, nie przychód. I na koniec banał, który potrafi zjeść godzinę debugowania: upewnij się, że tagi nie są wstrzymane. Wstrzymany tag nie odpali się mimo poprawnej reszty łańcucha.


To piąty i ostatni wariant tego samego wzorca (sGTM + magazyn jako mini-CDP), tym razem jako podmiana sygnału wartości zamiast wzbogacania danych. Wcześniejsze: wzbogacanie page_view e-mailem, które otwiera serię, oraz webhook Shopify jako siatka bezpieczeństwa. Szerszy kontekst odzyskiwania i kształtowania sygnałów po stronie serwera opisuję w case study o server-side taggingu.

Powiązane wpisy