From f1fc2dc0e0f699e3aad8cf7e636fb9d4feaa969e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 19 Jun 2026 10:35:19 +0200 Subject: [PATCH] Show persistent for local-storage trackers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local storage, IndexedDB, and cache storage have no expiry yet persist until explicitly cleared, so a missing max-age should read as "persistent", not "session" (the latter only fits cookies and session storage, which end with the session or tab). Thread the tracker type through humanizeSeconds (helpers) and humanizeDuration (cookie-banner, with a localized persistent label) and pass it at every console and banner call site. The consent record query now selects trackerType so its duration column can make the same distinction. This mirrors the Go HumanizedDuration helper that already renders these types as persistent. Signed-off-by: Émile Ré --- .../CookieBannerConsentRecordPage.tsx | 3 ++- .../display/_components/CategorySection.tsx | 2 +- .../TrackerPatternPropertiesSection.tsx | 2 +- .../_components/TrackerPatternRow.tsx | 16 +++--------- .../src/components/cookie-list.ts | 4 +-- packages/cookie-banner/src/cookie-utils.ts | 26 +++++++++++++++++-- packages/helpers/src/duration.ts | 20 ++++++++++++-- 7 files changed, 51 insertions(+), 22 deletions(-) diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx index abd6033db..c0e6036b0 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx @@ -49,6 +49,7 @@ export const cookieBannerConsentRecordPageQuery = graphql` kind cookies { name + trackerType maxAgeSeconds description } @@ -235,7 +236,7 @@ export default function CookieBannerConsentRecordPage({ {cookie.name} - {humanizeSeconds(cookie.maxAgeSeconds ?? null)} + {humanizeSeconds(cookie.maxAgeSeconds ?? null, cookie.trackerType)} {cookie.description} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/display/_components/CategorySection.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/display/_components/CategorySection.tsx index dd8911da9..506a16fd1 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/display/_components/CategorySection.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/display/_components/CategorySection.tsx @@ -795,7 +795,7 @@ export function CategorySection({ categoryKey, connectionId }: CategorySectionPr })()} - {humanizeSeconds(pattern.maxAgeSeconds ?? null)} + {humanizeSeconds(pattern.maxAgeSeconds ?? null, pattern.trackerType)}
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternPropertiesSection.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternPropertiesSection.tsx index 7a71c4a56..f342cbf03 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternPropertiesSection.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternPropertiesSection.tsx @@ -161,7 +161,7 @@ export function TrackerPatternPropertiesSection({ - {humanizeSeconds(pattern.maxAgeSeconds ?? null)} + {humanizeSeconds(pattern.maxAgeSeconds ?? null, pattern.trackerType)} {pattern.description && ( diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx index 54ed0c506..f6539abe3 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx @@ -28,7 +28,6 @@ import { } from "@probo/ui"; import { useState } from "react"; import { graphql, useFragment, useMutation } from "react-relay"; -import { ConnectionHandler } from "relay-runtime"; import type { TrackerPatternRowDeleteMutation } from "#/__generated__/core/TrackerPatternRowDeleteMutation.graphql"; import type { TrackerPatternRowFragment$key } from "#/__generated__/core/TrackerPatternRowFragment.graphql"; @@ -94,6 +93,8 @@ const movePatternMutation = graphql` id cookieCategory { id + name + kind } } cookieBanner { @@ -214,17 +215,6 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo targetCookieCategoryId: targetCategoryId, }, }, - updater(store) { - const payload = store.getRootField("moveTrackerPatternToCategory"); - if (!payload?.getLinkedRecord("trackerPattern")) { - return; - } - - const conn = store.get(connectionId); - if (conn) { - ConnectionHandler.deleteNode(conn, pattern.id); - } - }, onCompleted(_, errors) { if (errors?.length) { toast({ title: __("Error"), description: errors[0].message, variant: "error" }); @@ -392,7 +382,7 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
- {humanizeSeconds(pattern.maxAgeSeconds ?? null)} + {humanizeSeconds(pattern.maxAgeSeconds ?? null, pattern.trackerType)} {pattern.lastMatchedAt diff --git a/packages/cookie-banner/src/components/cookie-list.ts b/packages/cookie-banner/src/components/cookie-list.ts index ed318a04c..fe833458a 100644 --- a/packages/cookie-banner/src/components/cookie-list.ts +++ b/packages/cookie-banner/src/components/cookie-list.ts @@ -52,8 +52,8 @@ export class ProboCookieList extends ProboElement { if (!this.template) return; const duration = cookie.max_age_seconds != null - ? humanizeDuration(cookie.max_age_seconds, lang) - : humanizeDuration(0, lang); + ? humanizeDuration(cookie.max_age_seconds, lang, cookie.tracker_type) + : humanizeDuration(0, lang, cookie.tracker_type); const type = getTrackerTypeLabel(cookie.tracker_type); diff --git a/packages/cookie-banner/src/cookie-utils.ts b/packages/cookie-banner/src/cookie-utils.ts index de94a09eb..31c479d2d 100644 --- a/packages/cookie-banner/src/cookie-utils.ts +++ b/packages/cookie-banner/src/cookie-utils.ts @@ -13,6 +13,16 @@ // PERFORMANCE OF THIS SOFTWARE. import { interpolate } from "./i18n"; +import type { TrackerType } from "./types"; + +// Tracker types whose data persists until explicitly cleared. When such a +// tracker has no max-age, its lifetime is "persistent" rather than "session" +// (cookies and session storage are cleared when the session/tab ends). +const PERSISTENT_TRACKER_TYPES: ReadonlySet = new Set([ + "LOCAL_STORAGE", + "INDEXED_DB", + "CACHE_STORAGE", +]); interface DurationTexts { [key: string]: string; @@ -35,6 +45,7 @@ const durationTextsByLanguage: Record = { duration_second_one: "{{count}} second", duration_second_other: "{{count}} seconds", duration_session: "session", + duration_persistent: "persistent", }, fr: { duration_year_one: "{{count}} an", @@ -52,6 +63,7 @@ const durationTextsByLanguage: Record = { duration_second_one: "{{count}} seconde", duration_second_other: "{{count}} secondes", duration_session: "session", + duration_persistent: "persistant", }, de: { duration_year_one: "{{count}} Jahr", @@ -69,6 +81,7 @@ const durationTextsByLanguage: Record = { duration_second_one: "{{count}} Sekunde", duration_second_other: "{{count}} Sekunden", duration_session: "Sitzung", + duration_persistent: "dauerhaft", }, es: { duration_year_one: "{{count}} año", @@ -86,6 +99,7 @@ const durationTextsByLanguage: Record = { duration_second_one: "{{count}} segundo", duration_second_other: "{{count}} segundos", duration_session: "sesión", + duration_persistent: "persistente", }, }; @@ -107,9 +121,17 @@ const DURATION_UNITS: [number, string, number][] = [ [1, "duration_second", 0], ]; -export function humanizeDuration(seconds: number, lang?: string): string { +export function humanizeDuration( + seconds: number, + lang?: string, + trackerType?: TrackerType, +): string { const texts = getDurationTexts(lang); - if (seconds <= 0) return texts.duration_session; + if (seconds <= 0) { + return trackerType && PERSISTENT_TRACKER_TYPES.has(trackerType) + ? texts.duration_persistent + : texts.duration_session; + } let remaining = seconds; const parts: string[] = []; diff --git a/packages/helpers/src/duration.ts b/packages/helpers/src/duration.ts index f8d5cf229..2f1c2cf77 100644 --- a/packages/helpers/src/duration.ts +++ b/packages/helpers/src/duration.ts @@ -22,8 +22,24 @@ export const DURATION_UNITS: { value: string; label: string; singular: string; s { value: "years", label: "years", singular: "year", seconds: 31536000, snap: 21 * 24 * 3600 }, ] as const; -export function humanizeSeconds(seconds: number | null): string { - if (seconds === null || seconds <= 0) return "session"; +// Tracker types whose data persists until explicitly cleared. When such a +// tracker has no max-age, its lifetime is "persistent" rather than "session" +// (cookies and session storage are cleared when the session/tab ends). +const PERSISTENT_TRACKER_TYPES = new Set([ + "LOCAL_STORAGE", + "INDEXED_DB", + "CACHE_STORAGE", +]); + +export function humanizeSeconds( + seconds: number | null, + trackerType?: string | null, +): string { + if (seconds === null || seconds <= 0) { + return trackerType && PERSISTENT_TRACKER_TYPES.has(trackerType) + ? "persistent" + : "session"; + } let remaining = seconds; const parts: string[] = [];