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! }