Din lille webapp ved mere om folk end du tror

Din lille webapp ved mere om folk end du tror

– Du logger hele requesten i console, “for en sikkerheds skyld”
– Du har smidt Google Analytics på, fordi “det har alle andre”
– Du har aldrig besluttet, hvornår dine logs skal slettes
– Du har ingen idé om, hvad din error-tracker gemmer om brugere

Hvis du nikkede til mindst et punkt, så er den her artikel til dig.

Lad mig starte med en disclaimer: Jeg er udvikler, ikke jurist. Det du får her, er et teknisk perspektiv på GDPR for udviklere til små webapps. Altså hvordan du designer kode, logs og dataflows, så du ikke laver helt skøre ting med persondata uden at opdage det.

Hvis du på et tidspunkt står med et rigtigt produkt, rigtige kunder og rigtige penge involveret, bør du tale med nogen der laver jura til daglig. Men du kan komme overraskende langt med lidt teknisk omtanke.

1. De stille persondata der sniger sig ind i din app

Mange tænker “persondata” og forestiller sig CPR-numre og pas. I virkeligheden er det langt mere lavpraktisk i små webapps: IP-adresser, e-mails i logs, user agents, request bodies.

Så lad os være konkrete. Her er nogle typiske steder, hvor dine hobbyprojekter allerede håndterer persondata uden at du har tænkt over det.

IP-adresser og user agent i server-logs

Standard webserver-logs indeholder ofte:

  • IP-adresse
  • Timestamp
  • URL
  • User agent (browser, OS osv.)

IP-adresse + timestamp + URL er nok til at blive set som persondata. Det er ikke specielt dramatisk, men det betyder, at du skal tænke over hvor længe du gemmer det, og om du kan nøjes med mindre.

Et simpelt mønster er at have to niveauer af logs:

  • En kortlivet “access log” til driftsfejl (få dage eller uger)
  • En mere anonym statistiklog uden fulde IP-adresser

Hvis du bruger en reverse proxy eller hosting der logger automatisk, kan du ofte skrue på formatet. Fx i Nginx kan du maskere IP-adresser delvist, så du kun beholder de første par oktetter.

E-mails og navne i fejlbeskeder

En klassiker: Du har en form der fejler, og i panik smider du hele payloaden i en log:

// Express eksempel
app.post("/signup", async (req, res) => {
  try {
    await createUser(req.body);
    res.status(201).send("ok");
  } catch (err) {
    console.error("Signup failed", { body: req.body, error: err });
    res.status(500).send("error");
  }
});

Hvis req.body indeholder email, name, måske endda adresse, så gemmer du pludselig persondata i dine logs. Måske også i en tredjeparts error-tracker.

Et bedre mønster er at logge et “request id” eller en genereret reference, og så nøjes med at gemme de felter, du faktisk har brug for til debugging.

Stacktraces der indeholder tokens

En anden fæl ting er, når fejlbeskeder indeholder hele JWT tokens eller session IDs. Kombineret med IP og tid kan det være mere følsomt end du tror.

Hvis du logger headers, så sørg for aldrig at logge hele Authorization-headeren. Mask ét eller andet:

const safeHeadersForLog = (headers) => ({
  "user-agent": headers["user-agent"],
  "content-type": headers["content-type"],
  // Authorization ryger helt ud eller maskeres
});

Det samme gælder cookies. Du behøver ikke logge folks sessions-cookie for at debugge din 500-fejl.

Form data i analytics og heatmaps

Hvis du kaster tredjeparts-scripts ind, som fx analytics eller “session replay”, så kan de nogle gange opsnappe:

  • Indtastede e-mails i formularer
  • Brugernavne
  • Andre tekstfelter, som helt klart er persondata

Gode værktøjer giver dig mulighed for at “maskere” felter, men kun hvis du går ind og gør det. Hvis du bare smider scriptet ind og går videre, kan du ende med at sende alt muligt, du ikke havde forventet.

Hvis du er i den helt lette ende af full stack webapps, er den mest fredelige løsning ofte at vælge analytics der slet ikke bruger cookies eller persondata.

2. Dataflowet på 15 minutter – tegn det og find lækagerne

Inden vi går ned i logs vs analytics vs retention, er der én øvelse, der gør resten meget nemmere: Tegn dataflowet for din app. Ikke fancy arkitekturdiagram, bare en simpel tegning.

Tag et stykke papir (eller et simpelt diagramværktøj) og tegn:

  • Klient (browser, mobil osv.)
  • Server(e): API, backend, funktioner i skyen
  • Database(r) og køer
  • Tredjepartsservices: analytics, mail, error tracking, hosting

Så stiller du ét spørgsmål ved hver pil: Hvilke persondata kan bevæge sig her?

Klient til server

Her er det ret åbenlyst:

  • Forms: email, password, navn, beskeder
  • Upload: billeder, dokumenter
  • Login: tokens, session-id

Skriv ned hvilke felter du sender. Ikke bare “brugerobjekt” men reelt: { email, name, avatarUrl }. Det gør det lettere senere at beslutte, hvad du vil kunne eksportere/slette.

Server til tredjepart

Her sker de mere skjulte ting:

  • Sender du e-mails via et mail-API (Mailgun, SendGrid, etc.)?
  • Sender du events til analytics fra backend?
  • Bruger du en ekstern error-tracker (Sentry, etc.)?

Hvis ja, så flytter du persondata videre til et andet firma. Det er ikke ulovligt, men det betyder, at du skal vide nogenlunde, hvad du sender, og kunne forklare det i en simpel tekst til brugeren.

Server til database og backups

Her er de data, du normalt godt ved du har. Men to ting bliver ofte glemt:

  • Automatiske backups fra din database-host
  • Evt. egne dumps du tager manuelt og gemmer i et random cloud-drive

Hvis du vil kunne slette eller anonymisere data “rigtigt”, skal du have en idé om, hvor længe backups lever, og om de er krypteret. Mange managed databaser dokumenterer ret tydeligt deres retention. Læs den side. Gerne mens du ikke er alt for træt.

3. Logs og error tracking – det du faktisk bør ændre i koden

Logs er dér, hvor små apps ofte går helt amok med persondata. Mest fordi det er så fristende at logge alt, når man fejlsøger. Jeg kender det alt for godt, og ja, jeg har også haft “console.log(user)” liggende i produktion for længe.

Design et “safe log event” format

I stedet for at smide hele objekter ind i logs, kan du definere en lille funktion, der renser dem.

// pseudo-eksempel på et "safe log" helper
const redactUser = (user) => user && {
  id: user.id,
  role: user.role,
};

const safeLog = (msg, context = {}) => {
  const cleaned = {
    ...context,
    user: redactUser(context.user),
    // Fjerner følsomme felter fra body
    body: context.body && {
      action: context.body.action,
    },
  };

  console.log(msg, cleaned);
};

Pointen er: Du vælger aktivt, hvilke felter der må ende i logs, i stedet for at lade JavaScript lave et dump af hele verden.

Hvis du bruger en error-tracker, så giv den det samme “rensede” objekt, ikke rå request- eller user-objekter.

Maskér kendte følsomme felter

Passwords må aldrig i logs. Men også ting som:

  • Reset tokens
  • API-nøgler
  • Adgangslinks der indeholder hemmelige tokens

Et mønster jeg godt kan lide, er at have en lille liste af “forbudte” feltnavne, og så lave en generel redaktør:

const SENSITIVE_KEYS = ["password", "token", "secret", "authorization"];

function redactObject(obj) {
  if (!obj || typeof obj !== "object") return obj;
  const result = Array.isArray(obj) ? [] : {};

  for (const [key, value] of Object.entries(obj)) {
    if (SENSITIVE_KEYS.includes(key.toLowerCase())) {
      result[key] = "[REDACTED]";
    } else if (typeof value === "object") {
      result[key] = redactObject(value);
    } else {
      result[key] = value;
    }
  }

  return result;
}

Så kan du køre redactObject på dine bodies, før du logger dem. Det redder dig fra en del “hov, det var hele password-reset-linket” øjeblikke.

Begræns retention for logs

Selv hvis dine logs er rensede, behøver du ikke gemme dem for evigt. En simpel model er:

  • Applikationslogs: 7-30 dage, afhængigt af behov
  • Aggregater/metrics uden persondata: længere

På filbaserede logs kan du bruge log-rotation. På hosted løsninger kan du ofte vælge retention i UI. Vælg noget aktivt, i stedet for at lade standarden være “indtil disken er fuld”.

Hvis du er interesseret i debugging som vane, så er fejlfinding og debugging i øvrigt en kategori, der er værd at dykke ned i. Logs er kun én del af det.

4. Analytics uden at drukne i cookies og bannere

Analytics er næste hotspot. Mange smider Google Analytics på per refleks, og så har de pludselig brug for cookie-banner, databehandleraftaler og en del tekst.

Til små webapps kan du spørge dig selv: Hvad vil jeg faktisk vide?

Cookie-baseret analytics vs cookieless

Groft forsimplet har du to verdener:

  • Cookie-baseret analytics: fx Google Analytics, der typisk tracker på individniveau og kræver samtykke.
  • Cookieless / privacy-venlige løsninger: der måler pageviews og simple events uden at identificere brugere på samme måde.

Hvis du bare vil se “hvor mange besøg om dagen” og “hvilke sider bliver vist”, så er den anden kategori ofte nok. Og du slipper for at bygge halve cookie-bannere som i artiklen om rigtige cookie bannere.

Beslutningsmodel for små sites

Jeg plejer at tænke sådan her:

  • Portfolio-side, blog, hobbyprojekt: vælg cookieless eller slet ingen analytics.
  • Seriøst produkt med marketingfolk involveret: forvent cookie-baseret analytics og et rigtigt samtykke-setup.

Hvis du vælger noget i den “lette” ende, så læs deres privacy-dokumentation. Mange privacy-fokuserede værktøjer forklarer ret pænt, hvilke data de indsamler, og hvornår de mener, at det er uden for cookie-reglerne.

5. Retention der kan implementeres på en eftermiddag

“Retention” lyder tungt, men for en lille app handler det mest om: Hvornår sletter du ting igen, og gør du det automatisk, eller kun når du får dårlig samvittighed?

Start simpelt: Aldersbaseret sletning

Et mønster du kan implementere uden at skrive 300 linjer cron-job, er en simpel aldersregel på data. Eksempel: Brugere der har været inaktive i X måneder, markeres til sletning.

Hvis du bruger en database med timestamps, kan du lave et script, der fjerner eller anonymiserer gamle rækker:

-- pseudo-SQL til at anonymisere gamle brugere
UPDATE users
SET email = CONCAT("deleted-", id, "@example.com"),
    name = NULL,
    deleted_at = NOW()
WHERE last_login < NOW() - INTERVAL '12 months'
  AND deleted_at IS NULL;

Du beholder måske nogle tekniske nøgler (id, created_at) til statistik, men fjerner det, der direkte siger “det her er Anders fra Århus”.

Backups – hvad gør du realistisk?

Backups er tricky, for du kan sjældent gå ind og slette én bruger i en gammel backup. Her er det mere realistisk at:

  • Vide hvor længe din udbyder gemmer backups
  • Undgå at tage ekstra ustrukturerede dumps og glemme dem

Hvis din database-host siger “vi gemmer backups i 30 dage”, så ved du, at en sletning i dag i praksis kan ligge i gamle backups i op til en måned. Det kan du beskrive ærligt i din privacy-tekst.

Hvis du manuelt tager exports til udvikling, så sørg for at:

  • Bruge minimalt datasæt
  • Anonymisere e-mails og navne
  • Gem dem et sted med adgangskontrol, ikke i en tilfældig offentlig bucket

6. Export og sletning – en minimal API-kontrakt

GDPR taler om retten til indsigt (export) og retten til sletning. Det lyder voldsomt, men i en lille webapp kan du komme langt med en “minimal kontrakt” du bygger ind i din backend fra starten.

Definér din “brugerdata-model” ét sted

Før du kan eksportere eller slette, skal du vide, hvor du gemmer ting. Tegn relationerne:

  • users: id, email, name, created_at
  • orders: id, user_id, amount, created_at
  • audit_logs: id, user_id, action, timestamp

Lav gerne en lille kommentar i koden eller i README, der siger: “Brugerdata er primært i disse tabeller”. Det gør sletning mindre gætteri senere.

Simpelt export-endpoint

Et minimal JSON-export kan se sådan ud:

// Pseudo-Express route med auth tjek
app.get("/me/export", requireAuth, async (req, res) => {
  const userId = req.user.id;

  const user = await db.user.findById(userId);
  const orders = await db.order.findMany({ userId });

  res.json({
    user,
    orders,
  });
});

Du skal selvfølgelig sikre, at kun den autentificerede bruger kan hente sin egen data. Men pointen er: Du sampler de tabeller, der er relevante, og returnerer dem samlet.

Hvis du vil være ekstra flink mod fremtidige dig, kan du først lave en lille service-funktion:

async function exportUserData(userId) {
  const user = await db.user.findById(userId);
  const orders = await db.order.findMany({ userId });
  return { user, orders };
}

Så kan du genbruge den både til API og fx en admin-funktion.

Simpelt delete-flow

For sletning er du nødt til at beslutte, om du vil:

  • Slette rækker helt
  • Eller anonymisere dem (fx for at beholde statistik)

En blanding er almindelig: Du fjerner kontaktdata, men beholder anonymiserede poster.

async function deleteUserAccount(userId) {
  // Anonymiser ordredata
  await db.order.updateMany({ userId }, {
    userId: null,
  });

  // Anonymiser selve brugeren
  await db.user.update(userId, {
    email: `deleted-${userId}@example.com`,
    name: null,
    deletedAt: new Date(),
  });
}

Det er ikke perfekt i juridisk forstand, men det er stærkt meget bedre end ingenting, og det er noget, du kan implementere i en lille app uden at miste overblikket.

7. Tredjeparts-SDKs – checklisten før du npm install

Hver gang du smider et tredjeparts-script eller SDK ind, inviterer du et nyt firma ind i dit dataflow. Det er ikke farligt i sig selv, men det er godt at gøre bevidst.

Tre spørgsmål du bør stille hver gang

Næste gang du overvejer et nyt SDK, så tjek:

  • Sender det data videre til en vendor (analytics, logs, chat, video osv.)?
  • Hvilke data samler det op som standard? Er der “masking” muligheder?
  • Har de en nogenlunde forståelig privacy-side, så du kan forklare brugere hvad der sker?

Hvis meget af dokumentationen føles som at læse gammel græsk, er det ofte et tegn på, at værktøjet ikke er målrettet små hobbyprojekter men store virksomheder med jurister.

Frontend vs backend-SDKs

Det er også værd at skelne mellem:

  • Frontend-SDKs som kører i brugerens browser og typisk kan se mere (DOM, inputfelter, kliks).
  • Backend-SDKs som du selv kalder og har mere kontrol over, hvilke felter du sender.

Hvis du kan vælge en backend-integration frem for et stort frontend-script, har du ofte bedre styr på, hvad du sender, og hvornår.

8. Dit lille “privacy README” – så du ikke glemmer dine valg

Til sidst vil jeg anbefale noget, der ligner en blanding af dokumentation og huskeseddel: En lille “privacy README” i dit repo.

Den kan hedde PRIVACY.md eller være en sektion i din almindelige README som du måske allerede har styr på efter at have læst om gode READMEs.

Hvad den bør indeholde

Jeg ville mindst skrive:

  • Hvilke data du gemmer om brugere (felter, tabeller, services)
  • Hvorfor du gemmer dem (login, statistik, fejlretning)
  • Hvornår du sletter eller anonymiserer dem (fx inaktivitet, manuelle sletninger)
  • Hvordan en bruger i teorien kunne få sine data (export-endpoint) og blive slettet
  • Liste over tredjepartsservices du sender data til

Det gør to ting:

  • Hjælper fremtidige dig med at huske, hvad du har besluttet.
  • Gør det muligt at skrive en simpel offentlig privacy-tekst til brugere, hvis appen vokser.

Og ja, det giver faktisk også et pluspunkt i din portfolio, fordi det viser, at du tænker sikkerhed og ansvarlighed ind i dine projekter, ikke bare features.

Eksempel på kort privacy README for en lille app

# Privacy-noter for "Habit Tracker" app

## Hvilke data gemmer vi
- Bruger: email, hashed password, timestamps
- Habits: titel, frekvens, tilknyttet bruger-id
- Logs: korte API-logs med delvist anonymiseret IP (xx.xx.*.*)

## Formål
- Email + password: login og adgangsstyring
- Habits: gemme brugerens data om vaner
- Logs: fejlsøgning og misbrugsdetektering

## Sletning og retention
- Bruger kan anmode om sletning (pt. manuel via admin-script)
- Ved sletning anonymiseres email og navn, habits bevares uden reference til bruger
- Logs roteres automatisk efter 14 dage

## Tredjepartsservices
- Error tracking: <navn> (sender anonymiseret fejl-info uden email)
- Hosting: <navn> (database-backups i 30 dage, krypteret)

Det er ikke jura-perfekt, men det er ærligt og teknisk sandt. Det er et godt sted at starte, og noget en jurist kan bygge videre på, hvis det en dag bliver nødvendigt.

9. Sådan kommer du i gang i dag uden at omskrive hele din app

Hvis du er lidt mentalt mæt nu, giver det mening. GDPR føles ofte som en mur af tekst. Jeg vil slutte med et helt lavpraktisk forslag til, hvad du kan gøre i din næste lille webapp, uden at det bliver et forskningsprojekt.

En realistisk to-do liste

Vælg 3 ting at gøre først:

  • Stop med at logge hele request bodies og headers. Lav en lille safeLog-helper.
  • Tegn dit dataflow på et stykke papir og skriv ned, hvilke tredjepartsservices får hvilke data.
  • Lav en minimal /me/export-route i din backend, så du kan hente en samlet JSON for en bruger.

Når det spiller, kan du senere:

  • Tilføje et simpelt slette-flow
  • Skifte til et mere privacy-venligt analytics-setup eller droppe det helt i små projekter
  • Skrive et kort “privacy README” i dit repo

Det store skift er at gå fra “ingen idé om hvor data ender” til “jeg kan nogenlunde forklare, hvad der sker”. Når først du har den vane, føles det gradvist mindre skræmmende, også i større softwareprojekter i praksis.

Jeg er spændt på, hvornår vi som udviklere begynder at se på privacy på samme måde som automatiske tests: Ikke som pynt, men som en naturlig del af at bygge noget, man kan stå inde for om et par år.

Der er ikke ét rigtigt svar, men som praktisk udgangspunkt kan du bruge korte, konkrete perioder: 7-30 dage for rå access-logs, 30-90 dage for fejl- og performance-logs og 6-12 måneder for aggregeret statistik hvis du har behov. Vælg kortere retention hvis data indeholder IP, e-mail eller andre identificerende felter, dokumentér beslutningen og lav en automatisk sletningsjob så du undgår manuel opsamling.
Slå IP-logging fra eller anonymisér den, undlad at sende user-id eller e-mail som event-parametre, og brug de redigeringshooks tjenesten tilbyder (fx before-send) til at scrubbe request bodies og headers. Tjek leverandørens databehandleraftale, slå funktioner fra der gemmer fulde payloads, og overvej server-side proxying hvis du vil kontrollere hvad der sendes.
Generér en unik id (fx UUID v4) per request, sæt den i en header som X-Request-ID og log kun id’et sammen med nødvendige metadata, ikke hele payloaden. Propager id’et til downstream services og fejltrackers så du kan korrelere hændelser uden at skulle lække brugeres e-mails eller tokens.
Plan for sletning fra starten: anonymisér eller pseudonymisér data hvor muligt, brug soft-delete i databasen med en baggrundsproces til endelig sletning, og sørg for at logs og backups har egne retention- og sletteprocedurer. For tredjepartstjenester indsend sletteanmodninger til leverandøren, og dokumentér hvilke steder data kan være for at kunne svare hurtigt.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed