Show tracker type in cookie tracking policy

Trackers sharing a display name can differ in type, so the generated
cookie and tracking technologies policy was ambiguous without it. Carry
the tracker type through the banner version snapshot and surface it as a
dedicated column in the policy table.

Stop the snapshot from dropping non-cookie trackers so storage, IndexedDB
and cache technologies appear in the policy and served banner config with
their real type. Duration now reflects the type when no max-age applies:
session storage clears with the tab, the remaining storage technologies
persist. Legacy snapshots predate the field and only ever held cookies,
so GetSnapshot backfills an empty type as COOKIE, keeping the non-null
GraphQL enum and policy output valid without a migration.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-02 09:53:01 +02:00
parent 346ca94d38
commit 73854f98cb
10 changed files with 59 additions and 16 deletions

View File

@@ -43,7 +43,7 @@ func TestSnapshotsEqual(t *testing.T) {
Description: "Analytics cookies", Description: "Analytics cookies",
Kind: coredata.CookieCategoryKindNormal, Kind: coredata.CookieCategoryKindNormal,
Cookies: coredata.CookieItems{ Cookies: coredata.CookieItems{
{Name: "_ga", MaxAgeSeconds: &maxAge, Description: "Google Analytics"}, {Name: "_ga", TrackerType: coredata.TrackerTypeCookie, MaxAgeSeconds: &maxAge, Description: "Google Analytics"},
}, },
GCMConsentTypes: []string{"analytics_storage"}, GCMConsentTypes: []string{"analytics_storage"},
PostHogConsent: false, PostHogConsent: false,

View File

@@ -80,14 +80,11 @@ func buildSnapshot(
cookiesByCategory := make(map[gid.GID]coredata.CookieItems) cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
for _, p := range allPatterns { for _, p := range allPatterns {
if p.TrackerType != coredata.TrackerTypeCookie {
continue
}
cookiesByCategory[p.CookieCategoryID] = append( cookiesByCategory[p.CookieCategoryID] = append(
cookiesByCategory[p.CookieCategoryID], cookiesByCategory[p.CookieCategoryID],
coredata.CookieItem{ coredata.CookieItem{
Name: p.DisplayName, Name: p.DisplayName,
TrackerType: p.TrackerType,
MaxAgeSeconds: p.MaxAgeSeconds, MaxAgeSeconds: p.MaxAgeSeconds,
Description: p.Description, Description: p.Description,
}, },

View File

@@ -126,6 +126,18 @@ func (v *CookieBannerVersion) GetSnapshot() (CookieBannerVersionSnapshot, error)
return snapshot, fmt.Errorf("cannot unmarshal cookie banner version snapshot: %w", err) return snapshot, fmt.Errorf("cannot unmarshal cookie banner version snapshot: %w", err)
} }
// Snapshots created before tracker types were captured only ever held
// cookie-type trackers, so their cookie items carry an empty tracker
// type. Backfill them as cookies so downstream consumers (policy
// generation, GraphQL, served banner config) see a valid type.
for i := range snapshot.Categories {
for j := range snapshot.Categories[i].Cookies {
if snapshot.Categories[i].Cookies[j].TrackerType == "" {
snapshot.Categories[i].Cookies[j].TrackerType = TrackerTypeCookie
}
}
}
return snapshot, nil return snapshot, nil
} }

View File

@@ -33,9 +33,10 @@ import (
type ( type (
CookieItem struct { CookieItem struct {
Name string `json:"name"` Name string `json:"name"`
MaxAgeSeconds *int `json:"max_age_seconds"` TrackerType TrackerType `json:"tracker_type"`
Description string `json:"description"` MaxAgeSeconds *int `json:"max_age_seconds"`
Description string `json:"description"`
} }
CookieItems []CookieItem CookieItems []CookieItem
@@ -80,13 +81,22 @@ var cookieDurationUnits = [...]cookieDurationUnit{
{1, "second", 0}, {1, "second", 0},
} }
// HumanizedDuration renders the cookie's max-age into a human-readable lifetime // HumanizedDuration renders the tracker's max-age into a human-readable
// using the same snapping and composition rules as the banner's // lifetime using the same snapping and composition rules as the banner's
// humanizeDuration helper. A nil or non-positive max-age denotes a session // humanizeDuration helper. A nil or non-positive max-age has no fixed
// cookie that is cleared when the browser closes. // expiry, so the lifetime is described by the tracker type: session cookies
// are cleared when the browser closes, session storage is cleared when the
// tab closes, and the remaining storage technologies persist until cleared.
func (c CookieItem) HumanizedDuration() string { func (c CookieItem) HumanizedDuration() string {
if c.MaxAgeSeconds == nil || *c.MaxAgeSeconds <= 0 { if c.MaxAgeSeconds == nil || *c.MaxAgeSeconds <= 0 {
return "Session" switch c.TrackerType {
case TrackerTypeSessionStorage:
return "Until the tab is closed"
case TrackerTypeLocalStorage, TrackerTypeIndexedDB, TrackerTypeCacheStorage:
return "Persistent"
default:
return "Session"
}
} }
remaining := *c.MaxAgeSeconds remaining := *c.MaxAgeSeconds

View File

@@ -63,6 +63,26 @@ func (v TrackerType) String() string {
return string(v) return string(v)
} }
// Label returns a human-readable name for the tracker type, suitable for
// display in visitor-facing documents such as the cookie and tracking
// technologies policy.
func (v TrackerType) Label() string {
switch v {
case TrackerTypeCookie:
return "Cookie"
case TrackerTypeLocalStorage:
return "Local storage"
case TrackerTypeSessionStorage:
return "Session storage"
case TrackerTypeIndexedDB:
return "IndexedDB"
case TrackerTypeCacheStorage:
return "Cache storage"
default:
return string(v)
}
}
func (v TrackerType) MarshalText() ([]byte, error) { func (v TrackerType) MarshalText() ([]byte, error) {
return []byte(v.String()), nil return []byte(v.String()), nil
} }

View File

@@ -489,6 +489,7 @@ type (
TrackerPolicyTracker struct { TrackerPolicyTracker struct {
Name string Name string
Type string
Purpose string Purpose string
Duration string Duration string
} }

View File

@@ -25,9 +25,9 @@ The tables below describe the categories of trackers we use on the Website, the
{{ with .Description }}{{ . }} {{ with .Description }}{{ . }}
{{ else }}Trackers in this category support the functionality described by its name. {{ else }}Trackers in this category support the functionality described by its name.
{{ end }}{{ if .Trackers }} {{ end }}{{ if .Trackers }}
| Tracker | Purpose | Duration | | Tracker | Type | Purpose | Duration |
| --- | --- | --- | | --- | --- | --- | --- |
{{ range .Trackers }}| {{ .Name }} | {{ .Purpose }} | {{ .Duration }} | {{ range .Trackers }}| {{ .Name }} | {{ .Type }} | {{ .Purpose }} | {{ .Duration }} |
{{ end }}{{ else }} {{ end }}{{ else }}
We are not currently using any trackers in this category. We are not currently using any trackers in this category.
{{ end }}{{ end }} {{ end }}{{ end }}

View File

@@ -191,6 +191,7 @@ func (s *GeneratedDocumentService) buildTrackerPolicyDocumentData(
for _, cookie := range c.Cookies { for _, cookie := range c.Cookies {
trackers = append(trackers, docgen.TrackerPolicyTracker{ trackers = append(trackers, docgen.TrackerPolicyTracker{
Name: sanitizeTrackerCell(cookie.Name), Name: sanitizeTrackerCell(cookie.Name),
Type: cookie.TrackerType.Label(),
Purpose: trackerPurpose(cookie.Description), Purpose: trackerPurpose(cookie.Description),
Duration: cookie.HumanizedDuration(), Duration: cookie.HumanizedDuration(),
}) })

View File

@@ -416,6 +416,7 @@ func (r *cookieBannerVersionResolver) Categories(ctx context.Context, obj *types
for j, c := range cat.Cookies { for j, c := range cat.Cookies {
cookies[j] = &types.CookieBannerVersionCookie{ cookies[j] = &types.CookieBannerVersionCookie{
Name: c.Name, Name: c.Name,
TrackerType: c.TrackerType,
MaxAgeSeconds: c.MaxAgeSeconds, MaxAgeSeconds: c.MaxAgeSeconds,
Description: c.Description, Description: c.Description,
} }

View File

@@ -543,6 +543,7 @@ type CookieBannerVersionCategory {
type CookieBannerVersionCookie { type CookieBannerVersionCookie {
name: String! name: String!
trackerType: TrackerType!
maxAgeSeconds: Int maxAgeSeconds: Int
description: String! description: String!
} }