# track.js

> Komplet reference til sporingsscriptet – attributter, JavaScript-API, hvad der gemmes i browseren, samtykke og fejl.

track.js er Affilyflows sporingsscript til webshops og websites. Det gemmer klikket, når en kunde lander fra et affiliate-link, og melder salget, når kunden når kvitteringen. Hvordan det hele hænger sammen, står i [Sådan virker sporingen](https://www.affilyflow.com/docs/start/sadan-virker-sporingen).

## Indsæt scriptet

Indsæt linjen i `<head>` på alle sider – også kvitteringssiden.

```html title="<head> · alle sider"
<script src="https://app.affilyflow.com/track.js" data-store="DIN_BUTIK" async></script>
```

`DIN_BUTIK` er butikkens id. Den rigtige linje med jeres id står under Opsætning i butikkens dashboard – kopier den derfra.

Brug `async`: scriptet må aldrig holde siden tilbage. Undtagelsen er den kodefri lead-trigger (`data-lead-url`), hvor linjen skal være uden `async` – se [Lead uden kode](#lead-uden-kode).

`https://app.affilyflow.com/s.js` er den samme fil som `track.js` under et neutralt navn. Begge adresser virker, også fremover. Se [Neutrale adresser og egen proxy](#neutrale-adresser-og-egen-proxy).

## Attributter

| Attribut | Påkrævet | Betydning |
|---|---|---|
| `data-store` | Ja | Butikkens id. Sendes med hvert salg og skal passe til klikkets butik, ellers afvises salget som `store_mismatch`. |
| `data-consent="required"` | Nej | Intet gemmes i browseren, før den besøgende har givet samtykke til marketing. Se [Samtykke](#samtykke). |
| `data-lead-url` | Nej | Adressen på tak-siden for et lead-program. Når siden matcher, meldes et lead automatisk. Se [Lead uden kode](#lead-uden-kode). |
| `data-cookie-domain` | Nej | Domænet, cookien lægges på (fx `shop.dk`). Uden attributten vælger scriptet selv det bredeste domæne, browseren tillader. |
| `data-sale="ga4"` | Nej | Læs købet fra Google Analytics 4 (dataLayer eller gtag). Se [Google Analytics](https://www.affilyflow.com/docs/reference/google-analytics). |
| `data-sale-vat` | Nej | Kun med `data-sale="ga4"`: momsen i procent, som Google Analytics-beløbene indeholder (fx `25`). |
| `data-sale-shipping` | Nej | Kun med `data-sale="ga4"`: `incl` hvis fragten altid er med i `value`, `excl` hvis den aldrig er. Uden attributten gætter scriptet. |

## Hvad scriptet gemmer

Når en kunde lander med `?afid=` i adressen, gemmer scriptet klik-id'et både som cookie og i localStorage, så det overlever, at den ene af dem ryddes.

| Nøgle | Indhold |
|---|---|
| `af_click` | Klik-id'et fra `?afid=` |
| `af_store` | Butikkens id fra `data-store` |
| `af_aff` | Kun ved ældre direkte links (`?aff=` + `?store=`) |
| `af_t` | Kun cookie: klikkets tidspunkt i sekunder. Samme levetid som `af_click`. |

Scriptet læser også cookien `af_srv`, men sætter den aldrig. Den sættes kun af butikkens egen server (se [Sporing fra jeres server](https://www.affilyflow.com/docs/butik/server-sporing)) og betyder, at serveren har sat klik-cookien. Står `af_srv=1`, skriver scriptet aldrig `af_click` eller `af_t` igen – en cookie, som JavaScript skriver, ville Safari forkorte til cirka 7 dage, også selvom serveren satte den først. localStorage skrives stadig som reserve.

- **Levetid:** højst 90 dage fra klikket. Cookiens `max-age` er altid den resterende tid af de 90 dage.
- **Genbesøg forlænger aldrig vinduet.** Ved hvert besøg skrives cookie og localStorage igen med den resterende tid, så en slettet cookie genskabes fra localStorage – men tidspunktet er altid klikkets.
- **localStorage** gemmer værdien som `{ v, t }` – værdien og tidspunktet for klikket. En værdi over 90 dage gammel slettes ved læsning.
- **Cookien** sættes med `path=/` og `SameSite=Lax`, og med `Secure` på https.
- **Domænet:** cookien lægges på det bredeste domæne, browseren tillader (`shop.dk` frem for `www.shop.dk`), så klikket følger med til en kvittering på et andet subdomæne. Scriptet finder det med en kortvarig testcookie (`_af_d`), der slettes med det samme. På en IP-adresse eller `localhost` bruges den præcise host.
- **Hukommelsen:** klik-id'et fra adressen ligger også i sidens hukommelse, så et salg på samme side virker, selvom lageret er blokeret (fx et privat vindue).
- **`af_t`** gør det muligt for butikkens server at forny cookien ved et genbesøg uden at forlænge den ud over 90 dage fra klikket.

> [!NOTE]
> Serveren afgør det rigtige vindue. Hvert program har sit eget cookie-vindue, som låses på klikket. Er klikket ældre end programmets vindue, afvises salget som `expired` – også selvom browseren stadig har det.

### Kun Affilyflows klik-id'er

Kun et `afid`, der ligner vores (`c_` efterfulgt af 4–64 tegn: bogstaver, tal, `_` og `-`), gemmes. Bruger butikken også et andet netværk med en parameter, der hedder `afid`, kan den fremmede værdi derfor aldrig overskrive et Affilyflow-klik.

## Dækning

Lander en kunde med et klik-id, melder scriptet det til Affilyflow – og igen, når klikket er gemt i browseren, og hvis kunden aktivt siger nej til marketing. Meldingen indeholder kun klik-id'et, butikkens id og samtykkets tilstand – intet om den besøgende. Selve landingsmeldingen gemmer intet i browseren, så den sendes også, mens scriptet venter på samtykke.

Det er grundlaget for **Sporingens dækning** under Opsætning i butikkens dashboard: hvor mange klik fra affiliates der nåede en side med scriptet, blev gemt og blev til et salg. Se [Test og fejlfinding](https://www.affilyflow.com/docs/butik/test-og-fejlfinding#sporingens-daekning).

## affilyflow.conversion()

Meld et salg fra JavaScript. Kaldet virker kun, når track.js er indlæst på siden.

```js title="Kvitteringssiden"
affilyflow.conversion({ order: '1008', total: 499, currency: 'DKK' });
```

| Felt | Type | Betydning |
|---|---|---|
| `order` | tekst | Ordrenummeret. Bruges til at stoppe dubletter. |
| `total` | tal eller tekst | Beløbet, provisionen regnes af: varerne efter rabat, uden moms og fragt. Tekst som `"1.249,00"` læses, se [Beløb som tekst](#beloeb-som-tekst). |
| `currency` | tekst | Valutakode, fx `DKK` eller `EUR`. Gøres til store bogstaver. Standard er `DKK`. |
| `store` | tekst | Butikkens id. Bruges kun, hvis browseren ikke allerede har det fra `data-store`. |
| `coupon` | tekst eller liste | Kundens rabatkode(r). Flere i en tekst adskilles med komma. Se [Personlige rabatkoder](https://www.affilyflow.com/docs/butik/personlige-rabatkoder). |

Sådan opfører kaldet sig:

- **Uden et affiliateklik i browseren sendes intet** – og der skrives ingen fejl. Et almindeligt køb uden affiliate er ikke en fejl. Undtagelsen er en [personlig rabatkode](#personlige-rabatkoder).
- Salget sendes med `navigator.sendBeacon`, så det kommer frem, selvom siden skifter med det samme bagefter. Uden sendBeacon bruges en usynlig billed-anmodning.
- Med `data-consent="required"` venter kaldet, til samtykket er afklaret, og sendes først derefter.
- Til et lead-program: `affilyflow.conversion({ order: 'lead_' + Date.now() })` – intet beløb.

### Personlige rabatkoder

Med `coupon` kan et salg tilskrives en personlig rabatkode, som butikken har givet en affiliate:

```js title="Kvitteringssiden · med rabatkode"
affilyflow.conversion({ order: '1008', total: 499, currency: 'DKK', coupon: 'ANNA10' });
```

- **Med et klik** sendes salget som altid, og koden sendes med. Et gyldigt klik vinder altid over koden. Er klikket udløbet eller ukendt, kan en personlig kode stadig tilskrive salget.
- **Uden et klik** henter scriptet en liste over envejs-fingeraftryk (SHA-256) af butikkens personlige koder og sammenligner. Kun hvis en kode står på listen, og der er et ordrenummer, sendes salget – med de koder, der passede. En generel kode eller ingen kode sender intet.
- Koden kræver intet lager i browseren. Med `data-consent="required"` venter et salg med kode højst 3 sekunder på samtykket; uden samtykke kan koden stadig tilskrive salget.

Koderne sammenlignes uden mellemrum og med store bogstaver. Salgslinjen har samme mulighed med `data-affilyflow-coupon`, og med `data-sale="ga4"` læses `coupon` fra købet i Google Analytics.

> [!WARNING]
> Med `async` er `window.affilyflow` ikke sikkert klar, når en inline-kode på kvitteringen kører. Vent på den, eller brug [salgslinjen](https://www.affilyflow.com/docs/reference/salgslinje), som slet ikke kræver JavaScript.

```html title="Kvitteringssiden · vent på track.js"
<script>
  (function () {
    var tries = 0;
    (function send() {
      if (!window.affilyflow) { if (tries++ < 50) setTimeout(send, 200); return; }
      affilyflow.conversion({ order: '1008', total: 499, currency: 'DKK' });
    })();
  })();
</script>
```

### Beløb som tekst

Et tal bruges, som det er. Tekst, der er et gyldigt maskintal, bruges også, som den er. Anden tekst renses: alt andet end cifre, punktum, komma og minus fjernes, og den **sidste** separator er decimaltegnet. Står der kun en separator med præcis tre cifre efter, er den tusindtal.

| Sendt som `total` | Læses som |
|---|---|
| `499` | 499 |
| `"1249.5"` | 1249,5 |
| `"249.125"` | 249,125 |
| `"1.249,00"` | 1249 |
| `"1.249,-"` | 1249 |
| `"499,00 kr."` | 499 |
| `"kr. 499"` | 499 |
| `"1,234.56"` | 1234,56 |

> [!WARNING]
> Teksten `"1.249"` er et gyldigt maskintal og læses af `conversion()` som 1,249. Send hele kroner som tal (`1249`) eller med decimaler (`"1.249,00"`). [Salgslinjen](https://www.affilyflow.com/docs/reference/salgslinje) læser derimod `"1.249"` som 1249, fordi værdien kommer fra sidens tekst.

### Pladsholdere, der afvises

Står skabelonens pladsholder stadig i `order`, sendes salget **uden** ordrenummer – en pladsholder må aldrig optage et ordrenummer, så alle senere salg blev afvist som dubletter. Disse værdier regnes som pladsholdere (store og små bogstaver er ligegyldige):

- `DIN_ORDRE_ID`, `DIT_ORDRE_ID`, `ORDRE_ID`, `ORDER_ID`, `YOUR_ORDER_ID`, `ORDERID`
- `XX`, `XXX` osv. (to eller flere X)
- Alt i `{{ … }}`, `[ … ]`, `< … >` eller `% … %` – en skabelonvariabel, der ikke blev udfyldt

Serveren bruger samme regel.

### Dubletværn i browseren

Serveren tæller aldrig samme ordrenummer to gange i samme program. Browseren husker desuden, hvad den har sendt, så en genindlæst kvittering ikke sender igen:

- **Med ordrenummer:** gemt i localStorage (`af_sent`) i 30 dage.
- **Uden ordrenummer:** gemt i sessionStorage (`af_sent_s`) som side og beløb – samme kvittering med samme beløb sendes en gang pr. fane.

## affilyflow.consent()

Butikkens egen samtykkeløsning kan styre scriptet direkte. Det er kun relevant med `data-consent="required"`.

```js title="Efter den besøgendes valg"
affilyflow.consent(true);  // gem klikket fra adressen, og send ventende salg
affilyflow.consent(false); // slet af_click, af_store, af_aff, af_t, af_srv og af_sent
```

Siger den besøgende ja igen på samme side, gemmes klikket fra adressen igen.

## Samtykke

Med `data-consent="required"` gemmer scriptet intet i browseren – ingen cookie, intet localStorage eller sessionStorage, ingen testcookie – før der er givet samtykke til marketing. Indtil da ligger klikket kun i sidens hukommelse og bruges ikke. Uden attributten opfører scriptet sig som før.

```html title="<head> · alle sider"
<script src="https://app.affilyflow.com/track.js" data-store="DIN_BUTIK" data-consent="required" async></script>
```

Scriptet læser selv samtykket fra:

| Kilde | Hvad der tæller som ja |
|---|---|
| `affilyflow.consent(true)` | Kaldet |
| `window.affilyflowConsent` | Værdien `true` |
| Shoporamas banner | `shoporamaConsent.isAllowed('marketing')` |
| Cookie Information | Kategorien `cookie_cat_marketing` |
| Cookiebot | `Cookiebot.consent.marketing` |
| IAB TCF v2 | Publisher-samtykke til formål 1 og 7 (eller GDPR gælder ikke) |

- Et ja virker, uanset om banneret svarer før eller efter scriptet. Cookie Informations, Cookiebots og Shoporamas hændelser lyttes på hele tiden. `window.affilyflowConsent` tjekkes ved indlæsning og de første 10 sekunder, og TCF bruges, når dens API findes inden for de 10 sekunder.
- Et nej sletter kun ved en aktiv handling (Cookie Informations og Cookiebots hændelser, TCF's `useractioncomplete` eller `affilyflow.consent(false)`) – aldrig fordi banneret endnu ikke har læst sin egen cookie.
- Sætter I `window.affilyflowConsent` senere end 10 sekunder efter indlæsning, så kald `affilyflow.consent(true)` i stedet.

Se [Samtykke og cookies](https://www.affilyflow.com/docs/butik/samtykke-og-cookies) for opsætningen.

## Neutrale adresser og egen proxy

Annonceblokkere fjerner gerne filer og kald med "track" i navnet. Derfor sender scriptet sine kald til neutrale adresser ved siden af sig selv:

| Adresse | Bruges til |
|---|---|
| `/e/l` | Landingen og dækningen |
| `/e/c` | Salget (samme som `/api/track/conversion`) |
| `/e/k` | Listen over fingeraftryk af butikkens personlige rabatkoder |
| `/s.js` | Scriptet selv – samme fil som `/track.js` |

De gamle adresser virker fortsat. Serverer butikken scriptet fra sit eget domæne gennem en proxy – fx `https://shop.dk/af/s.js` sendt videre til `https://app.affilyflow.com/` – går alle kaldene også gennem butikkens domæne, så en annonceblokker kun ser butikkens egne adresser. Kodestumper til nginx, Apache og Next.js står i [Sporing fra jeres server](https://www.affilyflow.com/docs/butik/server-sporing#egen-proxy-valgfri).

## Lead uden kode

Til lead-programmer kan scriptet selv melde et lead, når kunden når tak-siden. Sæt adressen i `data-lead-url`, og fjern `async`, så leadet sendes, før tak-siden eventuelt viderestiller.

```html title="<head> · alle sider · lead-program"
<script src="https://app.affilyflow.com/track.js" data-store="DIN_BUTIK" data-lead-url="/tak"></script>
```

- `/tak` rammer `/tak` og `/tak/…`, men ikke `/takeaway`.
- En fuld adresse (`https://shop.dk/tak`) læses som stien.
- Står der parametre (`/tak?type=lead`), skal de alle findes i sidens adresse.
- Forsiden alene (`/`) matcher kun, når der også står parametre.
- Leadet meldes en gang pr. session og kun med et affiliateklik i browseren.

> [!NOTE]
> Et lead (`order` der begynder med `lead_`) på et program, der betaler pr. salg, afvises som `stale_lead_trigger`. Fjern lead-triggeren, når programmet er skiftet til salg.

## Fejl i konsollen

track.js skriver kun i browserens konsol, når et salg ikke kunne meldes fra [salgslinjen](https://www.affilyflow.com/docs/reference/salgslinje) eller [Google Analytics](https://www.affilyflow.com/docs/reference/google-analytics):

| Besked | Årsag |
|---|---|
| `Affilyflow: data-affilyflow-order must be the order number – the sale was not recorded.` | Salgslinjens ordrenummer mangler eller er en pladsholder. |
| `Affilyflow: data-affilyflow-total must be the amount (goods excl. VAT and shipping) – the sale was not recorded.` | Salgslinjens beløb er tomt eller 0. |
| `Affilyflow: the purchase event has no amount – the sale was not recorded.` | Google Analytics-købet havde intet beløb. |

Se [Test og fejlfinding](https://www.affilyflow.com/docs/butik/test-og-fejlfinding) for at teste hele vejen med testlinket.

## Safari og iPhone

Safari (ITP) sletter cookies og localStorage, der er sat fra JavaScript, efter cirka 7 dage uden besøg på siden. track.js genskriver lageret ved hvert besøg, men kan og skal ikke omgå grænserne. Kun en cookie sat af butikkens egen server – fx WooCommerce-pluginnet eller [jeres egen server](https://www.affilyflow.com/docs/butik/server-sporing) – lever længere. Det gælder alle netværk, ikke kun Affilyflow.

## Ældre direkte links

Links med `?aff=` (eller `?aff_id=`) og `?store=` i stedet for `?afid=` virker stadig. Scriptet tæller klikket og gemmer `af_aff` og `af_store`. `data-store` vinder altid over `?store=` i adressen, så en butik, der selv bruger `?store=`, ikke får sine salg afvist.

Kilde: https://www.affilyflow.com/docs/reference/track-js
