Come Funziona#
Il meccanismo e semplice, ma i dettagli contano per la precisione e la conformita.
sequenceDiagram
participant S as Mittente (server Go)
participant MTA as Mail Transfer Agent
participant C as Client Email
participant T as Tracking Server
S->>S: Genera UUID per il destinatario
S->>S: Costruisce email HTML con img src=URL di tracking
S->>MTA: Invia email via SMTP
MTA->>C: Consegna email alla casella di posta
C->>C: L'utente apre l'email
C->>T: GET /pixel/{uuid}.png
T->>T: Cerca l'UUID, registra l'evento di apertura
T->>C: 200 OK con PNG trasparente 1x1
Il tracking server deve servire il pixel in ogni caso, incluso quando l’UUID non e riconosciuto. Restituire un 404 fa si che alcuni client email ritentino indefinitamente, gonfiando i conteggi di apertura e sovraccaricando il server.
L’Implementazione Naive (e i Suoi Difetti)#
La versione originale di questo articolo aveva un errore sintattico critico: http.ListenAndServe era posizionato all’interno del corpo della closure del handler, il che significa che viene eseguito solo quando arriva una richiesta – e il handler non ritorna mai, bloccandosi indefinitamente sulla prima richiesta. Ecco prima la versione minima corretta:
package main
import (
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/pixel.png", pixelHandler)
// ListenAndServe deve essere al livello principale di main, non dentro un handler.
log.Fatal(http.ListenAndServe(":8080", nil))
}
func pixelHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("open event from %s user-agent=%s", r.RemoteAddr, r.UserAgent())
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", time.Now().UTC().Format(http.TimeFormat))
// Serve il PNG trasparente 1x1 inline (decodificato da base64 per evitare dipendenze dal filesystem).
w.Write(transparentPNG)
}
// transparentPNG e il PNG trasparente 1x1 valido piu piccolo (67 byte).
var transparentPNG = []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9c, 0x62, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
0x42, 0x60, 0x82,
}Questo funziona, ma e inutile per la produzione: ogni richiesta appare identica. Non puoi sapere quale destinatario ha aperto l’email, quante aperture uniche ci sono state, o se un singolo utente l’ha aperta piu volte.
URL Univoci per Destinatario#
L’intero valore dei tracking pixel deriva dal dare a ciascun destinatario un URL univoco. Il server mappa l’UUID dell’URL al destinatario al momento dell’apertura.
package tracker
import (
"sync"
"time"
)
// TrackingRecord memorizza i metadati su un singolo invio di email.
type TrackingRecord struct {
RecipientEmail string
Subject string
SentAt time.Time
FirstOpenAt *time.Time
OpenCount int
}
// Store e un in-memory open tracking store.
// In produzione, sostituisci con un database (PostgreSQL, Redis, DynamoDB).
type Store struct {
mu sync.RWMutex
records map[string]*TrackingRecord // chiave: UUID
}
func NewStore() *Store {
return &Store{records: make(map[string]*TrackingRecord)}
}
// Register crea un nuovo tracking record e restituisce il suo UUID.
func (s *Store) Register(uuid, recipient, subject string) {
s.mu.Lock()
defer s.mu.Unlock()
s.records[uuid] = &TrackingRecord{
RecipientEmail: recipient,
Subject: subject,
SentAt: time.Now(),
}
}
// RecordOpen segna il tracking record come aperto. Thread-safe.
// Restituisce il record, o nil se l'UUID e sconosciuto.
func (s *Store) RecordOpen(uuid string) *TrackingRecord {
s.mu.Lock()
defer s.mu.Unlock()
rec, ok := s.records[uuid]
if !ok {
return nil
}
now := time.Now()
if rec.FirstOpenAt == nil {
rec.FirstOpenAt = &now
}
rec.OpenCount++
return rec
}Tracking Server Completo#
package tracker
import (
"log/slog"
"net/http"
"strings"
"time"
)
type Server struct {
store *Store
mux *http.ServeMux
}
func NewServer(store *Store) *Server {
s := &Server{store: store, mux: http.NewServeMux()}
s.mux.HandleFunc("/pixel/", s.handlePixel)
return s
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}
// handlePixel gestisce GET /pixel/{uuid}.png
func (s *Server) handlePixel(w http.ResponseWriter, r *http.Request) {
// Estrai UUID dal path: /pixel/550e8400-e29b-41d4-a716-446655440000.png
path := strings.TrimPrefix(r.URL.Path, "/pixel/")
uuid := strings.TrimSuffix(path, ".png")
rec := s.store.RecordOpen(uuid)
if rec != nil {
slog.Info("email aperta",
"uuid", uuid,
"recipient", rec.RecipientEmail,
"subject", rec.Subject,
"open_count", rec.OpenCount,
"user_agent", r.UserAgent(),
"remote_addr", r.RemoteAddr,
)
} else {
slog.Warn("UUID di tracking sconosciuto", "uuid", uuid)
}
// Serve sempre il pixel -- anche per UUID sconosciuti.
// Restituire 404 causa storm di retry da client email aggressivi.
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", time.Now().UTC().Format(http.TimeFormat))
w.WriteHeader(http.StatusOK)
w.Write(transparentPNG)
}Generare un UUID per invio e registrare il tracking record:
package mailer
import (
"crypto/rand"
"fmt"
"tracker"
)
func newUUID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
func SendTrackedEmail(store *tracker.Store, recipient, subject, baseURL string) {
uuid := newUUID()
store.Register(uuid, recipient, subject)
pixelURL := fmt.Sprintf("%s/pixel/%s.png", baseURL, uuid)
html := buildEmailHTML(subject, pixelURL)
// Invia html via la tua libreria SMTP qui.
_ = html
}Template HTML con Tracking Pixel#
Il pixel deve essere l’ultimo elemento all’interno del <body> per massimizzare la possibilita che venga caricato anche quando l’utente scorre fino in fondo e il client email carica le immagini progressivamente.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.Subject}}</title>
</head>
<body>
<p>Ciao {{.RecipientName}},</p>
<p>{{.Body}}</p>
<p>Cordiali saluti,<br>Il Team</p>
<!--
Tracking pixel: PNG trasparente 1x1.
width/height=1 previene layout shift.
alt="" impedisce agli screen reader di annunciarlo.
Posizionato alla fine del body per evitare di bloccare il rendering.
-->
<img src="{{.PixelURL}}"
width="1" height="1"
alt=""
style="display:block;width:1px;height:1px;border:0;" />
</body>
</html>Non usare mai il trucco CSS display:none per nascondere il pixel. Diversi client email non caricano affatto le immagini nascoste, vanificando l’intero scopo. Usa invece width="1" height="1" con uno stile esplicito.
La Realta dei Client Email: il Blocco delle Immagini#
I tracking pixel non sono indicatori affidabili di apertura. La maggior parte dei principali client email blocca il caricamento delle immagini remote per impostazione predefinita.
| Client | Caricamento immagini predefinito | Note |
|---|---|---|
| Gmail (web) | Bloccato, proxied via Google | Le aperture vengono proxy; l’IP e il data center di Google, non del destinatario |
| Outlook (desktop) | Bloccato per impostazione predefinita | L’utente deve cliccare “Scarica immagini” |
| Apple Mail (iOS/macOS 15+) | Pre-caricato da Apple | Apple Mail Privacy Protection recupera le immagini per conto dell’utente, anche se non apre mai l’email |
| Outlook.com (web) | Bloccato per impostazione predefinita | Si renderizza tramite proxy Microsoft quando consentito |
| Thunderbird | Bloccato per impostazione predefinita | Controllato dall’utente per ogni messaggio |
L’implicazione pratica: i tracking pixel catturano circa il 40-60% delle aperture effettive in una tipica lista email B2B. Apple Mail Privacy Protection (AMPP), introdotta in iOS 15 e macOS Monterey, rompe attivamente il tracking delle aperture per gli utenti Apple pre-caricando tutte le immagini remote immediatamente alla consegna. Questo gonfia i tassi di apertura per gli utenti Apple Mail rendendoli privi di significato come segnali di coinvolgimento.
Non usare i tassi di apertura come metrica primaria per le decisioni sulle campagne email. Usa invece i tassi di click-through (link con tag UTM) – non sono influenzati dal blocco delle immagini e forniscono un segnale di intento molto piu forte.
Conformita GDPR#
I tracking pixel raccolgono dati personali: il timestamp di un evento di apertura collegato a un indirizzo email, piu l’indirizzo IP e lo user agent del dispositivo che ha caricato l’immagine. Ai sensi dell’Articolo 4 del GDPR, questi sono dati personali. Il loro trattamento richiede una base giuridica.
Applicabile per: email transazionali (ricevute, conferme di consegna, reset password) dove tracciare se l’email e stata ricevuta e un’esigenza operativa legittima.
Requisiti: documentare la valutazione dell’interesse legittimo (LIA), assicurarsi che il tracking sia proporzionato (non usato per profilazione), fornire chiara divulgazione nella privacy policy, e rispettare le richieste di opt-out.
Non applicabile per: email di marketing dove lo scopo principale e misurare il coinvolgimento della campagna.
Applicabile per: email di marketing e qualsiasi situazione in cui l’interesse legittimo non si applica.
Requisiti: ottenere il consenso esplicito e informato prima di inviare email tracciate. Il consenso deve essere specifico per il tracking (non raggruppato con l’accettazione generale dei termini di servizio). Mantenere un audit trail del consenso.
Implementazione pratica: includere un centro preferenze dove gli iscritti possono rinunciare al tracking pur continuando a ricevere email.
Minima divulgazione nella privacy policy:
Utilizziamo tracking pixel nelle nostre email per misurare se le email vengono aperte e per raccogliere la data, l’ora e il tipo di dispositivo dell’evento di apertura. Questi dati vengono utilizzati per [scopo specifico]. Puoi rinunciare al tracking email [meccanismo specifico – es. disabilitando il caricamento delle immagini nel tuo client email, o aggiornando le tue preferenze a questo link].
Considerazioni per la Produzione#
CDN per il pixel server. Se invii milioni di email e il 40% dei destinatari le apre entro la prima ora, il tuo pixel server ricevera un picco di centinaia di migliaia di richieste entro pochi minuti dall’invio. Metti il pixel server dietro un CDN (CloudFront, Fastly) o un load balancer con scaling orizzontale. Il handler stesso e stateless con lettura da database, facile da scalare.
Database invece di in-memory store. L’implementazione Store mostrata sopra perde tutti i dati al riavvio. In produzione, usa un database. PostgreSQL con una semplice tabella email_opens e una primary key uuid e sufficiente. Per volumi molto elevati, Redis con INCR e SETNX fornisce latenza di scrittura sub-millisecondo.
Probabilita di collisione UUID. Un UUID v4 ha 122 bit di entropia. La probabilita di collisione in 10^12 (un trilione) di UUID generati e circa 10^-13. Per il tracking email a qualsiasi scala realistica, le collisioni non sono un problema pratico. Usa crypto/rand (come mostrato sopra), non math/rand.
Deduplicazione per aperture multiple. Un singolo utente che apre un’email cinque volte genera cinque richieste pixel. La tua analytics dovrebbe distinguere “aperture uniche” (prima apertura per UUID) da “aperture totali” (somma di tutti gli eventi di apertura per UUID). La struct TrackingRecord mostrata sopra tiene traccia di entrambe tramite FirstOpenAt e OpenCount.
Traffico bot. I gateway di sicurezza email (Proofpoint, Mimecast, Barracuda) scansionano i link e le immagini delle email prima della consegna per rilevare malware. Questa pre-scansione attiva il tuo tracking pixel prima che qualsiasi umano veda l’email. Identifica le aperture bot verificando se lo user agent corrisponde a pattern di scanner noti e filtrali dalla tua analytics.
Se vuoi approfondire questi temi, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.