diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index d1a1f9a6f..87a84c8be 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -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, } diff --git a/pkg/cookiebanner/worker.go b/pkg/cookiebanner/worker.go index 89b63e75c..b2aab0625 100644 --- a/pkg/cookiebanner/worker.go +++ b/pkg/cookiebanner/worker.go @@ -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) diff --git a/pkg/coredata/cookie.go b/pkg/coredata/cookie.go index ffe09f76f..96cc4a34b 100644 --- a/pkg/coredata/cookie.go +++ b/pkg/coredata/cookie.go @@ -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, } diff --git a/pkg/coredata/cookie_pattern.go b/pkg/coredata/cookie_pattern.go index 76333de97..195896bc8 100644 --- a/pkg/coredata/cookie_pattern.go +++ b/pkg/coredata/cookie_pattern.go @@ -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, diff --git a/pkg/coredata/migrations/20260504T114755Z.sql b/pkg/coredata/migrations/20260504T114755Z.sql new file mode 100644 index 000000000..cbf979dec --- /dev/null +++ b/pkg/coredata/migrations/20260504T114755Z.sql @@ -0,0 +1,26 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- 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;