REPUBLIKEN

Checkout-
tracking

dataLayer-kravspec för den nya köpfunneln, observerade luckor och buggar att åtgärda inför lansering

Från Republiken
Till STC, utvecklingsteam
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 och kartlagt exakt vad som skickas till dataLayer i varje steg, hela vägen fram till genomfört testköp. Den här rapporten är ett konkret kravspec på vad den nya checkouten ska pusha, plus de buggar vi hittade som behöver åtgärdas innan lansering.

Grunden är god: bygget skickar redan flera funnel-events och ett purchase-event med en ecommerce-liknande struktur, och hela valet kodas i URL-state. Men det finns tre saker som behöver fixas, och en av dem dubbelräknar konverteringar i Google.

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

Notera namn-inkonsekvensen: de flesta events har prefixet purchase_funnel_*, men initiate_checkout och checkout_loaded följer inte mönstret. Vi rekommenderar att allt standardiseras under purchase_funnel_*, så att GTM-triggers blir förutsägbara.

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.

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. Håll dem i state eller POST-body, 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.

dataLayer.push({
  event: 'purchase_funnel_membership_selected',
  data: { membership_category: 'Ordinarie', age_group: 'Vuxen' }
});
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: unikt per köp, för Meta CAPI-dedup (= ordernr)
  data: {
    transaction_id: 45515553,        // samma som event_id, för Google-dedup
    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 */ }
});

Fixar på purchase

Nästa steg

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

Dev-checklista (STC)

  1. STC implementerar dataLayer enligt specenVi finns tillgängliga för avstämning under implementationen.
  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.