REPUBLIKEN
dataLayer-kravspec för den nya köpfunneln
Vad vi har granskat och vad det betyder inför lanseringen av nya checkouten.
Vi har gått igenom hela den nya köpfunneln på utvecklingsmiljön (dev, ej staging) och kartlagt vad som skickas till dataLayer i varje steg, hela vägen fram till genomfört testköp. Krav/önskemål på spec kommer nedan, plus de några buggar vi hittade som behöver åtgärdas innan lansering.
Grunden är god: bygget skickar redan flera funnel-events från förra flödet och ett purchase-event, och hela valet kodas i URL-state. Men det finns fyra saker som behöver fixas innan vi går live.
Det mesta löses genom att pusha data som redan finns i appens state. Den enskilt viktigaste åtgärden är att se till att purchase-eventet bara pushas en gång och bär ett konsekvent transaction_id, så att Google och Meta kan deduplicera korrekt.
Events som faktiskt fyrar idag, observerat genom hela funneln på utvecklingsmiljön.
| Steg | Event idag | Data |
|---|---|---|
| Funnel laddas | purchase_funnel_loaded | user_data (email, telefon) |
| Gym valt | inget event | gym-id hamnar i URL, ingen push |
| Medlemskapstyp | inget event | Ordinarie / Student / Senior |
| Åldersgrupp | purchase_funnel_age_selection | age_group |
| Tier (Platinum/Premium) | inget event | syns i UI, ej i dataLayer |
| Tillval togglat | inget event | bindningstid, passerkort, förtur |
| Sammanfattning | purchase_funnel_summary | endast user_data, ingen valdata |
| Personnummer | purchase_funnel_personal_id | status, member_age_group |
| Kontakt + order | initiate_checkout, checkout_loaded | city, user_data (email + telefon) |
| Köp klart | purchase (×2) | rik data + items[], men club: null |
Inkonsekvent naming convension -> let's fixa det när vi ändå är igång!
Tre saker som behöver vara på plats utöver själva eventen. Alla påverkar mätdata direkt.
På bekräftelsesidan pushas purchase två gånger med identiskt innehåll. Det ser ut att vara en remount eller en effekt som körs två gånger (t.ex. React StrictMode). Eventet ska fyra exakt en gång per genomfört köp, lägg en idempotensvakt.
Köpet ska samtidigt bära ett stabilt, unikt transaction_id (och event_id), så att konverteringar kan dedupliceras nedströms mot Google och Meta. Utan det riskerar varje köp att räknas dubbelt trots att ett köp skickas (p.g.a. server side tracking).
Det viktigaste eventet saknar gym. Utan gym på köpet går det inte att segmentera konverteringar per klubb eller region, eller bygga lokala målgrupper. Gym ska följa med från gym-valet hela vägen till purchase.
På checkout-steget läggs ssn, email och phone som query-parametrar i URL:en. Det läcker personuppgifter till GTM:s history-events, referrer-headers och serverloggar.
Rekommendation: skicka uppgifterna i POST-body, eller håll dem i app-state (t.ex. React-state, sessionStorage eller localstorage), aldrig i query-strängen.
Gäller alla events i specen.
purchase_funnel_*. Köp-eventet heter purchase (standardnamn, rör ej). Döp om initiate_checkout och checkout_loaded enligt katalogen.data-nyckel (samma konvention som idag). Ta bort tomma {data: null}-pushar.Samma struktur på varje event, fylls på progressivt. Skicka null för det som inte är känt ännu. Idag tappas telefon på purchase och namn/kön/födelsedatum saknas helt, trots att de går att härleda ur personnumret.
user_data: { email_address: 'eva.svensson@example.se', // normaliserad, gemener phone_number: '+46701234567', // E.164 address: { first_name: 'eva', last_name: 'svensson', city: 'ängelholm', postal_code: '26234', country: 'se' }, gender: 'f', // härlett ur personnummer date_of_birth: '19570414' // YYYYMMDD }
Exakt vad varje steg ska pusha. Alla events bär även user_data enligt sidan innan.
När gym väljs. Viktigast av tilläggen, gym och tier ska följa med hela vägen till purchase.
dataLayer.push({
event: 'purchase_funnel_gym_selected',
data: { gym_id: '17920', gym_name: 'Alvesta Kyrkogatan- Practice',
gym_city: 'Alvesta', gym_tier: 'Platinum' }
});
När medlemskapstyp väljs (vi kan skippa age group här).
dataLayer.push({
event: 'purchase_funnel_membership_selected',
data: { membership_category: 'Ordinarie' }
});
När ett tillval togglas (bindningstid, passerkort, förtur gruppträning).
dataLayer.push({
event: 'purchase_funnel_addon_changed',
data: { addon_id: 'fortur_grupptraning', addon_name: 'Förtur Gruppträning',
addon_price: 29, action: 'added' } // added | removed
});
Idag tom på valdata. Fyll med hela valet.
dataLayer.push({
event: 'purchase_funnel_summary',
data: { gym_id: '17920', gym_name: '...', gym_city: 'Alvesta', gym_tier: 'Platinum',
membership_category: 'Ordinarie', age_group: 'Vuxen',
binding_months: 12, passcard: 'app',
addons: [{ id: 'fortur_grupptraning', price: 29 }],
monthly_price: 529, startup_fee: 299, value: 828, currency: 'SEK' }
});
Hette initiate_checkout. Berika med val + value.
dataLayer.push({
event: 'purchase_funnel_checkout_initiated',
data: { gym_id: '17920', gym_tier: 'Platinum', membership_category: 'Ordinarie',
age_group: 'Vuxen', value: 828, currency: 'SEK' }
});
Hette checkout_loaded. Samma data som ovan.
När användaren skickas till Netaxept.
dataLayer.push({
event: 'purchase_funnel_payment_started',
data: { payment_option: 'Kort', payment_type: 'Månadsvis', value: 828, currency: 'SEK' }
});
Behåll som idag. Lägg gärna till landing_path och user_data om inloggad.
Behåll. Lägg till membership_category så det matchar membership_selected.
Behåll. Lägg inte personnummer i klartext i data, använd härledd kön och födelsedatum i user_data.
Befintlig struktur är en bra grund. Behåll namnet, fixa det markerade.
dataLayer.push({
event: 'purchase',
event_id: '45515553', // NYTT: SAMMA värde som transaction_id (ordernr)
data: {
transaction_id: '45515553', // samma sträng som event_id ovan
value: 828, // NYTT: lägg till value (= total_revenue)
total_revenue: 828, currency: 'SEK', tax: 46.86, coupon: '',
payment_type: 'Månadsvis', payment_option: 'Kort',
subscription_period_months: 12,
gym_id: '17920', // FIXA: idag club: null
gym_name: 'Alvesta Kyrkogatan- Practice', gym_city: 'Alvesta', gym_tier: 'Platinum',
membership_category: 'Ordinarie', age_group: 'Vuxen',
items: [
{ item_id: '21519', item_name: 'Platinum Ordinarie', item_category: 'Medlemskap',
item_category2: 'Platinum', price: 529, quantity: 1 },
{ item_id: '15211', item_name: 'Startavgift', item_category: 'Avgift', price: 299, quantity: 1 }
]
},
user_id: '<hashad>', member_id: '<hashad>',
user_data: { /* KOMPLETT: email + telefon (null idag) + namn + kön + dob + postnr */ }
});
event_id och transaction_id ska sättas till exakt samma sträng (ordernumret). Meta deduplicerar pixel mot CAPI på event_id, Google och GA4 deduplicerar på transaction_id. Ett värde, två fältnamn, så båda plattformarna räknar köpet en gång.
gym_id istället för club: null.value, och event_id satt till samma värde som transaction_id.user_data (telefon tappas idag).Strukturera purchase-värdet så att löpande abonnemang inte undervärderas mot kontantkort.
Idag skickas ett enda totalbelopp, första betalningen. Det gör att kontantkort, där hela perioden betalas direkt, får ett högt värde, medan ett löpande abonnemang (AG/RCP) bara rapporterar första månaden plus startavgift. En bunden 12-månadersmedlem är ofta värd mer långsiktigt men rapporteras lågt, vilket gör värdebaserad optimering missvisande.
Lösningen är att dataLayer skickar rena värdekomponenter och att GTM räknar ut annonsvärdet (det sköter vi senare, behöver kunna bygga logik kring det iterativt). Då kan vi vikta löpande abonnemang efter bindningstid utan att ni rör koden när vi byter formel.
item: billing: 'recurring_monthly' eller 'one_time', plus kategori (membership / addon / fee).monthly_recurring_value: summan av allt löpande per månad (medlemskap + recurring-addons som förtur och flex/obundet).one_time_value: summan av engångsdelarna (startavgift, ev. fysiskt passerkort).binding_months (0 om obundet) och payment_type (recurring / prepaid).data: { /* ... gym, membership, items ... */ value: 828, // första betalningen, oförändrat monthly_recurring_value: 558, // medlemskap 529 + förtur 29 one_time_value: 299, // startavgift (+ ev. fysiskt kort) binding_months: 12, payment_type: 'recurring', // recurring | prepaid items: [ { item_id: '21519', item_name: 'Platinum Ordinarie', item_category: 'membership', billing: 'recurring_monthly', price: 529 }, { item_id: 'fortur', item_name: 'Förtur Gruppträning', item_category: 'addon', billing: 'recurring_monthly', price: 29 }, { item_id: '15211', item_name: 'Startavgift', item_category: 'fee', billing: 'one_time', price: 299 } ] }
Siten levererar rena ingredienser (löpande vs engångs, månadsbelopp, bindningstid). Vi räknar i GTM, till exempel monthly_recurring_value × binding_months + one_time_value, eller en LTV-viktad modell. Byter vi formel behöver vi inte uppdatera siten något.
Vad ni implementerar, och vad vi gör på GTM-sidan.
purchase push:en, stabilt transaction_id, gym_id ej null, lägg value + event_idpurchase_funnel_*Snabb sanity-check i konsolen under utveckling: window.dataLayer.filter(e => (e.event||'').startsWith('purchase')). Varje steg ska ge exakt ett event, och purchase ska bara förekomma en gång.