From 73854f98cb43c4e2d2af7da7fe548dc3bda93bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 2 Jun 2026 09:53:01 +0200 Subject: [PATCH] Show tracker type in cookie tracking policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- pkg/cookiebanner/service_test.go | 2 +- pkg/cookiebanner/snapshot.go | 5 +--- pkg/coredata/cookie_banner_version.go | 12 +++++++++ pkg/coredata/cookie_category.go | 26 +++++++++++++------ pkg/coredata/tracker_type.go | 20 ++++++++++++++ pkg/docgen/generator.go | 1 + pkg/probo/templates/tracker_policy.md.tmpl | 6 ++--- pkg/probo/tracker_policy_document.go | 1 + .../api/console/v1/cookie_banner_resolvers.go | 1 + .../console/v1/graphql/cookie_banner.graphql | 1 + 10 files changed, 59 insertions(+), 16 deletions(-) diff --git a/pkg/cookiebanner/service_test.go b/pkg/cookiebanner/service_test.go index 2776b2283..fa4853dd6 100644 --- a/pkg/cookiebanner/service_test.go +++ b/pkg/cookiebanner/service_test.go @@ -43,7 +43,7 @@ func TestSnapshotsEqual(t *testing.T) { Description: "Analytics cookies", Kind: coredata.CookieCategoryKindNormal, Cookies: coredata.CookieItems{ - {Name: "_ga", MaxAgeSeconds: &maxAge, Description: "Google Analytics"}, + {Name: "_ga", TrackerType: coredata.TrackerTypeCookie, MaxAgeSeconds: &maxAge, Description: "Google Analytics"}, }, GCMConsentTypes: []string{"analytics_storage"}, PostHogConsent: false, diff --git a/pkg/cookiebanner/snapshot.go b/pkg/cookiebanner/snapshot.go index 624eff6f3..cb4e34460 100644 --- a/pkg/cookiebanner/snapshot.go +++ b/pkg/cookiebanner/snapshot.go @@ -80,14 +80,11 @@ func buildSnapshot( cookiesByCategory := make(map[gid.GID]coredata.CookieItems) for _, p := range allPatterns { - if p.TrackerType != coredata.TrackerTypeCookie { - continue - } - cookiesByCategory[p.CookieCategoryID] = append( cookiesByCategory[p.CookieCategoryID], coredata.CookieItem{ Name: p.DisplayName, + TrackerType: p.TrackerType, MaxAgeSeconds: p.MaxAgeSeconds, Description: p.Description, }, diff --git a/pkg/coredata/cookie_banner_version.go b/pkg/coredata/cookie_banner_version.go index 2ad26cea1..379e6a91b 100644 --- a/pkg/coredata/cookie_banner_version.go +++ b/pkg/coredata/cookie_banner_version.go @@ -126,6 +126,18 @@ func (v *CookieBannerVersion) GetSnapshot() (CookieBannerVersionSnapshot, error) 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 } diff --git a/pkg/coredata/cookie_category.go b/pkg/coredata/cookie_category.go index 699039628..a15028817 100644 --- a/pkg/coredata/cookie_category.go +++ b/pkg/coredata/cookie_category.go @@ -33,9 +33,10 @@ import ( type ( CookieItem struct { - Name string `json:"name"` - MaxAgeSeconds *int `json:"max_age_seconds"` - Description string `json:"description"` + Name string `json:"name"` + TrackerType TrackerType `json:"tracker_type"` + MaxAgeSeconds *int `json:"max_age_seconds"` + Description string `json:"description"` } CookieItems []CookieItem @@ -80,13 +81,22 @@ var cookieDurationUnits = [...]cookieDurationUnit{ {1, "second", 0}, } -// HumanizedDuration renders the cookie's max-age into a human-readable lifetime -// using the same snapping and composition rules as the banner's -// humanizeDuration helper. A nil or non-positive max-age denotes a session -// cookie that is cleared when the browser closes. +// HumanizedDuration renders the tracker's max-age into a human-readable +// lifetime using the same snapping and composition rules as the banner's +// humanizeDuration helper. A nil or non-positive max-age has no fixed +// 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 { 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 diff --git a/pkg/coredata/tracker_type.go b/pkg/coredata/tracker_type.go index 58a80ee3d..832534bff 100644 --- a/pkg/coredata/tracker_type.go +++ b/pkg/coredata/tracker_type.go @@ -63,6 +63,26 @@ func (v TrackerType) String() string { 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) { return []byte(v.String()), nil } diff --git a/pkg/docgen/generator.go b/pkg/docgen/generator.go index fdb05c87b..67d34620a 100644 --- a/pkg/docgen/generator.go +++ b/pkg/docgen/generator.go @@ -489,6 +489,7 @@ type ( TrackerPolicyTracker struct { Name string + Type string Purpose string Duration string } diff --git a/pkg/probo/templates/tracker_policy.md.tmpl b/pkg/probo/templates/tracker_policy.md.tmpl index bc40944c6..efea2f8d4 100644 --- a/pkg/probo/templates/tracker_policy.md.tmpl +++ b/pkg/probo/templates/tracker_policy.md.tmpl @@ -25,9 +25,9 @@ The tables below describe the categories of trackers we use on the Website, the {{ with .Description }}{{ . }} {{ else }}Trackers in this category support the functionality described by its name. {{ end }}{{ if .Trackers }} -| Tracker | Purpose | Duration | -| --- | --- | --- | -{{ range .Trackers }}| {{ .Name }} | {{ .Purpose }} | {{ .Duration }} | +| Tracker | Type | Purpose | Duration | +| --- | --- | --- | --- | +{{ range .Trackers }}| {{ .Name }} | {{ .Type }} | {{ .Purpose }} | {{ .Duration }} | {{ end }}{{ else }} We are not currently using any trackers in this category. {{ end }}{{ end }} diff --git a/pkg/probo/tracker_policy_document.go b/pkg/probo/tracker_policy_document.go index 3ee31af8b..84184629a 100644 --- a/pkg/probo/tracker_policy_document.go +++ b/pkg/probo/tracker_policy_document.go @@ -191,6 +191,7 @@ func (s *GeneratedDocumentService) buildTrackerPolicyDocumentData( for _, cookie := range c.Cookies { trackers = append(trackers, docgen.TrackerPolicyTracker{ Name: sanitizeTrackerCell(cookie.Name), + Type: cookie.TrackerType.Label(), Purpose: trackerPurpose(cookie.Description), Duration: cookie.HumanizedDuration(), }) diff --git a/pkg/server/api/console/v1/cookie_banner_resolvers.go b/pkg/server/api/console/v1/cookie_banner_resolvers.go index b7fdd2fba..486ba0467 100644 --- a/pkg/server/api/console/v1/cookie_banner_resolvers.go +++ b/pkg/server/api/console/v1/cookie_banner_resolvers.go @@ -416,6 +416,7 @@ func (r *cookieBannerVersionResolver) Categories(ctx context.Context, obj *types for j, c := range cat.Cookies { cookies[j] = &types.CookieBannerVersionCookie{ Name: c.Name, + TrackerType: c.TrackerType, MaxAgeSeconds: c.MaxAgeSeconds, Description: c.Description, } diff --git a/pkg/server/api/console/v1/graphql/cookie_banner.graphql b/pkg/server/api/console/v1/graphql/cookie_banner.graphql index 2aeb7cf2c..bab67e6f6 100644 --- a/pkg/server/api/console/v1/graphql/cookie_banner.graphql +++ b/pkg/server/api/console/v1/graphql/cookie_banner.graphql @@ -543,6 +543,7 @@ type CookieBannerVersionCategory { type CookieBannerVersionCookie { name: String! + trackerType: TrackerType! maxAgeSeconds: Int description: String! }