Show persistent for local-storage trackers

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é <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-19 10:35:19 +02:00
parent a4cc82441a
commit f1fc2dc0e0
7 changed files with 51 additions and 22 deletions

View File

@@ -49,6 +49,7 @@ export const cookieBannerConsentRecordPageQuery = graphql`
kind
cookies {
name
trackerType
maxAgeSeconds
description
}
@@ -235,7 +236,7 @@ export default function CookieBannerConsentRecordPage({
{cookie.name}
</td>
<td className="py-1 pr-4 text-txt-secondary">
{humanizeSeconds(cookie.maxAgeSeconds ?? null)}
{humanizeSeconds(cookie.maxAgeSeconds ?? null, cookie.trackerType)}
</td>
<td className="py-1 text-txt-secondary">
{cookie.description}

View File

@@ -795,7 +795,7 @@ export function CategorySection({ categoryKey, connectionId }: CategorySectionPr
})()}
</Td>
<Td className="text-sm text-muted-foreground">
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
{humanizeSeconds(pattern.maxAgeSeconds ?? null, pattern.trackerType)}
</Td>
<Td>
<div className="flex items-center gap-1">

View File

@@ -161,7 +161,7 @@ export function TrackerPatternPropertiesSection({
</PropertyRow>
<PropertyRow label={__("Max Age")}>
<span className="text-sm">
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
{humanizeSeconds(pattern.maxAgeSeconds ?? null, pattern.trackerType)}
</span>
</PropertyRow>
{pattern.description && (

View File

@@ -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
</div>
</Td>
<Td>
<span className="pl-2">{humanizeSeconds(pattern.maxAgeSeconds ?? null)}</span>
<span className="pl-2">{humanizeSeconds(pattern.maxAgeSeconds ?? null, pattern.trackerType)}</span>
</Td>
<Td>
{pattern.lastMatchedAt

View File

@@ -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);

View File

@@ -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<TrackerType> = new Set([
"LOCAL_STORAGE",
"INDEXED_DB",
"CACHE_STORAGE",
]);
interface DurationTexts {
[key: string]: string;
@@ -35,6 +45,7 @@ const durationTextsByLanguage: Record<string, DurationTexts> = {
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<string, DurationTexts> = {
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<string, DurationTexts> = {
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<string, DurationTexts> = {
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[] = [];

View File

@@ -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[] = [];