REPUBLIKEN

Checkout-
tracking

dataLayer-kravspec för den nya köpfunneln

Från Republiken
Till Mattias
Datum Maj 2026

Sammanfattning

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.

Dubblerat purchase-event
purchase pushas två gånger. Det ska fyra exakt en gång och bära ett stabilt, unikt transaction_id.
Saknad valdata
Gym, tier och pris saknas på flera events, och gym (club) är null på själva köpet.
Ofullständig user_data
E-post, telefon, namn, kön och födelsedatum saknas eller tappas mellan steg, trots att de finns i flödet.
PII i URL
Personnummer, e-post och telefon läggs som query-parametrar i checkout-URL:en. Bör bort.

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.

Nuläge i flödet

Events som faktiskt fyrar idag, observerat genom hela funneln på utvecklingsmiljön.

StegEvent idagData
Funnel laddaspurchase_funnel_loadeduser_data (email, telefon)
Gym valtinget eventgym-id hamnar i URL, ingen push
Medlemskapstypinget eventOrdinarie / Student / Senior
Åldersgrupppurchase_funnel_age_selectionage_group
Tier (Platinum/Premium)inget eventsyns i UI, ej i dataLayer
Tillval togglatinget eventbindningstid, passerkort, förtur
Sammanfattningpurchase_funnel_summaryendast user_data, ingen valdata
Personnummerpurchase_funnel_personal_idstatus, member_age_group
Kontakt + orderinitiate_checkout, checkout_loadedcity, user_data (email + telefon)
Köp klartpurchase (×2)rik data + items[], men club: null

Inkonsekvent naming convension -> let's fixa det när vi ändå är igång!

Krav utöver eventkatalogen

Tre saker som behöver vara på plats utöver själva eventen. Alla påverkar mätdata direkt.

purchase ska fyra exakt en gång, med stabilt transaction_id

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).

Gym (club) får inte vara null på purchase

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.

Ingen PII i URL-query

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.

Generella principer

Gäller alla events i specen.

user_data-objektet (delas av alla events)

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
}

Eventkatalog

Exakt vad varje steg ska pusha. Alla events bär även user_data enligt sidan innan.

purchase_funnel_gym_selectedNy

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' }
});
purchase_funnel_membership_selectedNy

När medlemskapstyp väljs (vi kan skippa age group här).

dataLayer.push({
  event: 'purchase_funnel_membership_selected',
  data: { membership_category: 'Ordinarie' }
});
purchase_funnel_addon_changedNy

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
});
purchase_funnel_summaryBerika

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' }
});
purchase_funnel_checkout_initiatedByt namn

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' }
});
purchase_funnel_checkout_loadedByt namn

Hette checkout_loaded. Samma data som ovan.

purchase_funnel_payment_startedNy

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' }
});
purchase_funnel_loadedBehåll

Behåll som idag. Lägg gärna till landing_path och user_data om inloggad.

purchase_funnel_age_selectionBehåll

Behåll. Lägg till membership_category så det matchar membership_selected.

purchase_funnel_personal_idBehåll

Behåll. Lägg inte personnummer i klartext i data, använd härledd kön och födelsedatum i user_data.

purchase-eventet

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.

Fixar på purchase

Konverteringsvärde för löpande medlemskap

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.

Vad ni exponerar på purchase

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.

Nästa steg

Vad ni implementerar, och vad vi gör på GTM-sidan.

Dev-checklista (STC)

  • Fixa purchase push:en, stabilt transaction_id, gym_id ej null, lägg value + event_id
  • Standardisera alla funnel-events till purchase_funnel_*
  • Nya events: gym_selected, membership_selected, addon_changed, payment_started
  • Berika summary och checkout-events med hela valet + value/currency
  • Exponera värdekomponenter på purchase (monthly_recurring_value, one_time_value, binding_months, payment_type, billing per item)
  • Konsekvent, consent-gated user_data på alla events
  • Ta bort PII (ssn/email/phone) ur URL-query
  1. STC implementerar dataLayer enligt specenVi finns tillgängliga för avstämning under implementationen vid behov!
  2. Verifiering på devVi kontrollerar i Preview, DebugView och Test Events att varje steg ger ett event, att purchase bara fyrar en gång och att gym_id, value och transaction_id är satta.
  3. Klart för produktionNär dataLayer är verifierat på dev kan ändringen gå live.

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.