Add last_detected_at and last_matched_at tracking

Track when cookies are last detected (last_detected_at on cookies)
and when patterns last matched a detected cookie
(last_matched_at on cookie_patterns). The cookie timestamp is
refreshed on every detection report; the pattern timestamp is
computed as MAX(last_detected_at) during pattern analysis.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-04 16:00:24 +04:00
parent c4a6a57789
commit b89707d9d2
5 changed files with 90 additions and 2 deletions

View File

@@ -540,6 +540,7 @@ func (s *Service) CreateCookieBanner(
Name: "probo_consent",
MaxAgeSeconds: &consentMaxAge,
Source: coredata.CookieSourceScript,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
@@ -2280,6 +2281,7 @@ func (s *Service) ReportDetectedCookies(
Name: dc.Name,
MaxAgeSeconds: dc.MaxAgeSeconds,
Source: dc.Source,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}

View File

@@ -170,6 +170,11 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co
return fmt.Errorf("cannot adopt uncategorised patterns: %w", err)
}
var patterns coredata.CookiePatterns
if err := patterns.RefreshLastMatchedAtByCookieBannerID(ctx, tx, scope, banner.ID); err != nil {
return fmt.Errorf("cannot refresh last_matched_at: %w", err)
}
if consentChanged {
if _, err := h.svc.ensureDraftVersionForBanner(ctx, tx, scope, banner.ID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)

View File

@@ -36,6 +36,7 @@ type (
Name string `db:"name"`
MaxAgeSeconds *int `db:"max_age_seconds"`
Source CookieSource `db:"source"`
LastDetectedAt time.Time `db:"last_detected_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
@@ -89,6 +90,7 @@ INSERT INTO cookies (
name,
max_age_seconds,
source,
last_detected_at,
created_at,
updated_at
) VALUES (
@@ -100,6 +102,7 @@ INSERT INTO cookies (
@name,
@max_age_seconds,
@source,
@last_detected_at,
@created_at,
@updated_at
)
@@ -114,6 +117,7 @@ INSERT INTO cookies (
"name": c.Name,
"max_age_seconds": c.MaxAgeSeconds,
"source": c.Source,
"last_detected_at": c.LastDetectedAt,
"created_at": c.CreatedAt,
"updated_at": c.UpdatedAt,
}
@@ -146,6 +150,7 @@ INSERT INTO cookies (
name,
max_age_seconds,
source,
last_detected_at,
created_at,
updated_at
) VALUES (
@@ -157,12 +162,14 @@ INSERT INTO cookies (
@name,
@max_age_seconds,
@source,
@last_detected_at,
@created_at,
@updated_at
)
ON CONFLICT (cookie_banner_id, name) DO UPDATE
SET source = EXCLUDED.source, updated_at = EXCLUDED.updated_at
WHERE cookies.source != @source_script AND EXCLUDED.source = @source_script
SET last_detected_at = EXCLUDED.last_detected_at,
source = CASE WHEN cookies.source != @source_script AND EXCLUDED.source = @source_script THEN EXCLUDED.source ELSE cookies.source END,
updated_at = EXCLUDED.updated_at
`
args := pgx.StrictNamedArgs{
@@ -175,6 +182,7 @@ ON CONFLICT (cookie_banner_id, name) DO UPDATE
"max_age_seconds": c.MaxAgeSeconds,
"source": c.Source,
"source_script": CookieSourceScript,
"last_detected_at": c.LastDetectedAt,
"created_at": c.CreatedAt,
"updated_at": c.UpdatedAt,
}

View File

@@ -41,6 +41,7 @@ type (
Description string `db:"description"`
Source CookieSource `db:"source"`
Excluded bool `db:"excluded"`
LastMatchedAt *time.Time `db:"last_matched_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
@@ -91,6 +92,7 @@ SELECT
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
FROM
@@ -144,6 +146,7 @@ SELECT
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
FROM
@@ -201,6 +204,7 @@ SELECT
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
FROM
@@ -269,6 +273,7 @@ SELECT
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
FROM
@@ -351,6 +356,7 @@ SELECT
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
FROM
@@ -403,6 +409,7 @@ INSERT INTO cookie_patterns (
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
) VALUES (
@@ -418,6 +425,7 @@ INSERT INTO cookie_patterns (
@description,
@source,
@excluded,
@last_matched_at,
@created_at,
@updated_at
)
@@ -436,6 +444,7 @@ INSERT INTO cookie_patterns (
"description": cp.Description,
"source": cp.Source,
"excluded": cp.Excluded,
"last_matched_at": cp.LastMatchedAt,
"created_at": cp.CreatedAt,
"updated_at": cp.UpdatedAt,
}
@@ -472,6 +481,7 @@ INSERT INTO cookie_patterns (
description,
source,
excluded,
last_matched_at,
created_at,
updated_at
) VALUES (
@@ -487,6 +497,7 @@ INSERT INTO cookie_patterns (
@description,
@source,
@excluded,
@last_matched_at,
@created_at,
@updated_at
)
@@ -506,6 +517,7 @@ ON CONFLICT (cookie_banner_id, pattern) DO NOTHING
"description": cp.Description,
"source": cp.Source,
"excluded": cp.Excluded,
"last_matched_at": cp.LastMatchedAt,
"created_at": cp.CreatedAt,
"updated_at": cp.UpdatedAt,
}
@@ -592,6 +604,41 @@ WHERE
return nil
}
func (cps *CookiePatterns) RefreshLastMatchedAtByCookieBannerID(
ctx context.Context,
tx pg.Tx,
scope Scoper,
cookieBannerID gid.GID,
) error {
q := `
UPDATE cookie_patterns
SET
last_matched_at = sub.max_detected
FROM (
SELECT cookie_pattern_id, MAX(last_detected_at) AS max_detected
FROM cookies
WHERE %[1]s AND cookie_banner_id = @cookie_banner_id
GROUP BY cookie_pattern_id
) sub
WHERE
cookie_patterns.id = sub.cookie_pattern_id
AND %[1]s
AND cookie_patterns.cookie_banner_id = @cookie_banner_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_banner_id": cookieBannerID}
maps.Copy(args, scope.SQLArguments())
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot refresh last_matched_at for banner patterns: %w", err)
}
return nil
}
func (cps *CookiePatterns) MoveToCategoryByCookieCategoryID(
ctx context.Context,
tx pg.Tx,

View File

@@ -0,0 +1,26 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
ALTER TABLE cookies ADD COLUMN last_detected_at TIMESTAMPTZ NOT NULL DEFAULT now();
UPDATE cookies SET last_detected_at = created_at;
ALTER TABLE cookies ALTER COLUMN last_detected_at DROP DEFAULT;
ALTER TABLE cookie_patterns ADD COLUMN last_matched_at TIMESTAMPTZ;
UPDATE cookie_patterns SET last_matched_at = sub.max_detected
FROM (
SELECT cookie_pattern_id, MAX(last_detected_at) AS max_detected
FROM cookies
GROUP BY cookie_pattern_id
) sub
WHERE cookie_patterns.id = sub.cookie_pattern_id;