Store cookie durations as max_age_seconds

Replace the free-form duration TEXT column with a nullable
max_age_seconds INTEGER on both cookies and cookie_patterns
tables. The SDK detector now sends raw seconds instead of
humanized strings, eliminating locale-dependent comparisons
in the pattern merge worker. Humanization happens at display
time in the widget and console UI.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-29 18:54:36 +04:00
parent b8ff3c66e1
commit 5cdaddf8b1
21 changed files with 308 additions and 151 deletions

View File

@@ -31,7 +31,7 @@ import { getOrCreateVisitorId } from "./visitor";
export interface CookieItem {
name: string;
duration: string;
max_age_seconds: number | null;
description: string;
}

View File

@@ -13,6 +13,7 @@
// PERFORMANCE OF THIS SOFTWARE.
import type { CookieItem } from "../client";
import { humanizeDuration } from "../cookie-utils";
import { getCookieDetailLabels } from "../i18n";
import { ProboElement } from "./base";
import type { ProboCategory } from "./category";
@@ -43,24 +44,28 @@ export class ProboCookieList extends ProboElement {
const cookies = this.category.cookies;
for (const cookie of cookies) {
this.stampCookie(cookie, labels);
this.stampCookie(cookie, labels, lang);
}
}
private stampCookie(cookie: CookieItem, labels: Record<string, string>): void {
private stampCookie(cookie: CookieItem, labels: Record<string, string>, lang: string): void {
if (!this.template) return;
const duration = cookie.max_age_seconds != null
? humanizeDuration(cookie.max_age_seconds, lang)
: humanizeDuration(0, lang);
const wrapper = document.createElement("probo-cookie");
wrapper.setAttribute("name", cookie.name);
const clone = this.template.content.cloneNode(true) as DocumentFragment;
this.fillSlots(clone, {
name: cookie.name,
duration: cookie.duration,
duration,
description: cookie.description,
});
this.fillLabels(clone, labels, {
description: cookie.description,
duration: cookie.duration,
duration,
});
wrapper.appendChild(clone);

View File

@@ -98,7 +98,7 @@ const DURATION_UNITS: [number, string, number][] = [
[60, "duration_minute", 15],
];
function humanizeDuration(seconds: number, lang?: string): string {
export function humanizeDuration(seconds: number, lang?: string): string {
const texts = getDurationTexts(lang);
if (seconds <= 0) return texts.duration_session;
@@ -133,17 +133,15 @@ export function parseCookieName(raw: string): string {
return raw.substring(0, eqIdx).trim();
}
export function parseDuration(raw: string, lang?: string): string {
const texts = getDurationTexts(lang);
const session = texts.duration_session;
export function parseMaxAgeSeconds(raw: string): number | null {
const parts = raw.split(";").map((s) => s.trim());
for (const part of parts) {
const lower = part.toLowerCase();
if (lower.startsWith("max-age=")) {
const val = parseInt(part.substring(8), 10);
if (isNaN(val) || val <= 0) return session;
return humanizeDuration(val, lang);
if (isNaN(val) || val <= 0) return null;
return val;
}
}
@@ -152,16 +150,16 @@ export function parseDuration(raw: string, lang?: string): string {
if (lower.startsWith("expires=")) {
const dateStr = part.substring(8);
const expires = new Date(dateStr);
if (isNaN(expires.getTime())) return session;
if (isNaN(expires.getTime())) return null;
const deltaSeconds = Math.round(
(expires.getTime() - Date.now()) / 1000,
);
if (deltaSeconds <= 0) return session;
return humanizeDuration(deltaSeconds, lang);
if (deltaSeconds <= 0) return null;
return deltaSeconds;
}
}
return session;
return null;
}
export function isDeletion(raw: string): boolean {

View File

@@ -12,12 +12,12 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { isDeletion, parseCookieName, parseDuration } from "./cookie-utils";
import { isDeletion, parseCookieName, parseMaxAgeSeconds } from "./cookie-utils";
import { fetchJSON } from "./http";
interface DetectedCookieEntry {
name: string;
duration: string;
max_age_seconds: number | null;
source: "script" | "pre-existing";
}
@@ -93,10 +93,10 @@ export class CookieDetector {
const name = parseCookieName(raw);
if (!name || this.knownNames.has(name) || this.reported.has(name)) return;
const duration = parseDuration(raw);
const maxAgeSeconds = parseMaxAgeSeconds(raw);
this.reported.add(name);
this.pending.set(name, { name, duration, source: "script" });
this.pending.set(name, { name, max_age_seconds: maxAgeSeconds, source: "script" });
this.scheduleFlush();
}
@@ -110,7 +110,7 @@ export class CookieDetector {
continue;
}
this.reported.add(name);
this.pending.set(name, { name, duration: "session", source: "pre-existing" });
this.pending.set(name, { name, max_age_seconds: null, source: "pre-existing" });
}
if (this.pending.size > 0) {