feat(cookie): 2. Sicht Banner-Kategorie + Fehl-Einsortierung
CookieResultView bekommt einen Umschalter [Rechtliche Rolle] ↔ [Banner-Kategorie] (Notwendig/Funktional/Statistik/Marketing). In beiden Sichten zeigt jede Cookie-Zeile '→ sollte: Marketing', wenn die tatsächliche Kategorie laut Library von der deklarierten abweicht (rot bei Tracker als notwendig, § 25 TDDDG). Neue KPI 'Falsch einsortiert'. Backend liefert dazu cookie_categories (name→actual_category) aus big_lib im cookie-check-Output; Seite lädt cookie-check einmal und reicht es an beide Komponenten. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -139,11 +139,14 @@ export function CookieFindingList({ data }: { data: CheckData }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CookieLibraryPanel({ snapshotId }: { snapshotId: string }) {
|
export function CookieLibraryPanel(
|
||||||
const [data, setData] = useState<CheckData | null>(null)
|
{ snapshotId, data: provided }: { snapshotId: string; data?: CheckData },
|
||||||
const [loading, setLoading] = useState(true)
|
) {
|
||||||
|
const [data, setData] = useState<CheckData | null>(provided ?? null)
|
||||||
|
const [loading, setLoading] = useState(!provided)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (provided) { setData(provided); setLoading(false); return }
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/cookie-check`)
|
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/cookie-check`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -151,7 +154,7 @@ export function CookieLibraryPanel({ snapshotId }: { snapshotId: string }) {
|
|||||||
.catch(() => { if (!cancelled) setData({ findings: [] }) })
|
.catch(() => { if (!cancelled) setData({ findings: [] }) })
|
||||||
.finally(() => { if (!cancelled) setLoading(false) })
|
.finally(() => { if (!cancelled) setLoading(false) })
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [snapshotId])
|
}, [snapshotId, provided])
|
||||||
|
|
||||||
if (loading) return <div className="text-xs text-gray-400">Library-Abgleich läuft…</div>
|
if (loading) return <div className="text-xs text-gray-400">Library-Abgleich läuft…</div>
|
||||||
return <CookieFindingList data={data || {}} />
|
return <CookieFindingList data={data || {}} />
|
||||||
|
|||||||
@@ -4,9 +4,12 @@
|
|||||||
* CookieResultView — strukturierte Cookie-/Vendor-Auswertung aus einem
|
* CookieResultView — strukturierte Cookie-/Vendor-Auswertung aus einem
|
||||||
* gespeicherten Snapshot (cmp_vendors), OHNE Re-Crawl.
|
* gespeicherten Snapshot (cmp_vendors), OHNE Re-Crawl.
|
||||||
*
|
*
|
||||||
* KPIs + Empfänger-Gruppen (Eigene / Auftragsverarbeiter / Joint Controller —
|
* Zwei Sichten (Umschalter):
|
||||||
* wie im Audit-Mail-VVT) + aufklappbare Vendor→Cookie-Tabelle. Verarbeitet
|
* - Rechtliche Rolle: Eigene / Auftragsverarbeiter / Joint Controller (VVT)
|
||||||
* Mengen (780 Cookies bei BMW): Vendors gruppiert, Cookies on-demand.
|
* - Banner-Kategorie: Notwendig / Funktional / Statistik / Marketing — die im
|
||||||
|
* Consent-Banner implementierte Einteilung. Pro Cookie wird die tatsächliche
|
||||||
|
* Kategorie laut Library gegengeprüft → '→ sollte: Marketing' bei
|
||||||
|
* Fehl-Einsortierung (Tracker als notwendig = § 25 TDDDG-relevant).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
@@ -37,6 +40,9 @@ interface Snapshot {
|
|||||||
cmp_vendors?: SnapshotVendor[]
|
cmp_vendors?: SnapshotVendor[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// name_lower → tatsächliche Kategorie laut Library (aus /cookie-check).
|
||||||
|
export type LibCategories = Record<string, string>
|
||||||
|
|
||||||
const ROLE_LABEL: Record<string, string> = {
|
const ROLE_LABEL: Record<string, string> = {
|
||||||
unknown: 'Unbekannt', ad_pixel: 'Werbe-Pixel', auth_token: 'Auth-Token',
|
unknown: 'Unbekannt', ad_pixel: 'Werbe-Pixel', auth_token: 'Auth-Token',
|
||||||
preference: 'Präferenz', visitor_id: 'Besucher-ID', consent_state: 'Consent',
|
preference: 'Präferenz', visitor_id: 'Besucher-ID', consent_state: 'Consent',
|
||||||
@@ -57,6 +63,45 @@ const GROUPS = [
|
|||||||
{ key: 'other', label: 'Sonstige Empfänger', test: () => true },
|
{ key: 'other', label: 'Sonstige Empfänger', test: () => true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Banner-Kategorie-Sicht: kanonische Buckets + Labels.
|
||||||
|
const CAT_CANON: Record<string, string> = {
|
||||||
|
necessary: 'necessary', essential: 'necessary', notwendig: 'necessary',
|
||||||
|
essenziell: 'necessary', security: 'necessary', 'strictly necessary': 'necessary',
|
||||||
|
functional: 'functional', funktional: 'functional', preferences: 'functional',
|
||||||
|
preference: 'functional', präferenzen: 'functional',
|
||||||
|
statistics: 'statistics', statistik: 'statistics', analytics: 'statistics',
|
||||||
|
performance: 'statistics',
|
||||||
|
marketing: 'marketing', targeting: 'marketing', advertising: 'marketing',
|
||||||
|
werbung: 'marketing', social_media: 'marketing', social: 'marketing', ad: 'marketing',
|
||||||
|
}
|
||||||
|
const CANON_LABEL: Record<string, string> = {
|
||||||
|
necessary: 'Notwendig', functional: 'Funktional',
|
||||||
|
statistics: 'Statistik', marketing: 'Marketing', unknown: '—',
|
||||||
|
}
|
||||||
|
const CATEGORY_GROUPS = [
|
||||||
|
{ key: 'necessary', label: 'Notwendig (essenziell)' },
|
||||||
|
{ key: 'functional', label: 'Funktional' },
|
||||||
|
{ key: 'statistics', label: 'Statistik' },
|
||||||
|
{ key: 'marketing', label: 'Marketing' },
|
||||||
|
{ key: 'unknown', label: 'Ohne Kategorie' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function canonCat(c?: string): string {
|
||||||
|
return CAT_CANON[(c || '').toLowerCase().trim()] || 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tatsächliche Kategorie laut Library vs. deklarierte Banner-Kategorie.
|
||||||
|
function mismatch(name: string, declaredCanon: string, lib?: LibCategories) {
|
||||||
|
const raw = lib?.[name.toLowerCase()]
|
||||||
|
if (!raw) return null
|
||||||
|
const actual = canonCat(raw)
|
||||||
|
if (actual === 'unknown' || actual === declaredCanon) return null
|
||||||
|
// severe: als notwendig deklariert, laut Library einwilligungspflichtig.
|
||||||
|
const severe = declaredCanon === 'necessary'
|
||||||
|
&& (actual === 'marketing' || actual === 'statistics')
|
||||||
|
return { actual, severe }
|
||||||
|
}
|
||||||
|
|
||||||
function scoreColor(s?: number): string {
|
function scoreColor(s?: number): string {
|
||||||
if (s == null) return 'text-gray-400'
|
if (s == null) return 'text-gray-400'
|
||||||
return s >= 80 ? 'text-green-700' : s >= 50 ? 'text-amber-700' : 'text-red-700'
|
return s >= 80 ? 'text-green-700' : s >= 50 ? 'text-amber-700' : 'text-red-700'
|
||||||
@@ -71,10 +116,11 @@ function Tile({ label, value, tone }: { label: string; value: React.ReactNode; t
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function VendorRow({ v }: { v: SnapshotVendor }) {
|
function VendorRow({ v, lib }: { v: SnapshotVendor; lib?: LibCategories }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const cookies = v.cookies || []
|
const cookies = v.cookies || []
|
||||||
const cat = (v.category || '').toLowerCase()
|
const cat = (v.category || '').toLowerCase()
|
||||||
|
const declaredCanon = canonCat(v.category)
|
||||||
const drittland = !!v.country && !EEA.has((v.country || '').toUpperCase())
|
const drittland = !!v.country && !EEA.has((v.country || '').toUpperCase())
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -111,22 +157,35 @@ function VendorRow({ v }: { v: SnapshotVendor }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{cookies.map((c, i) => (
|
{cookies.map((c, i) => {
|
||||||
<tr key={i} className="border-t border-gray-100 align-top">
|
const mm = mismatch(c.name, declaredCanon, lib)
|
||||||
<td className="px-2 py-1 font-mono text-gray-700 break-all w-40">{c.name}</td>
|
return (
|
||||||
<td className="px-2 py-1 text-gray-500 w-24">
|
<tr key={i} className="border-t border-gray-100 align-top">
|
||||||
{c.functional_role && c.functional_role !== 'unknown'
|
<td className="px-2 py-1 font-mono text-gray-700 break-all w-40">
|
||||||
? (ROLE_LABEL[c.functional_role] || c.functional_role)
|
{c.name}
|
||||||
: <span className="text-gray-300">—</span>}
|
{mm && (
|
||||||
</td>
|
<span
|
||||||
<td className="px-2 py-1 text-gray-500 break-words">
|
className={`ml-1 inline-block px-1 py-0.5 rounded text-[9px] font-sans ${mm.severe ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}`}
|
||||||
{c.purpose
|
title="tatsächliche Kategorie laut Library"
|
||||||
? c.purpose
|
>
|
||||||
: <span className="text-amber-600 italic">kein Zweck</span>}
|
→ sollte: {CANON_LABEL[mm.actual]}
|
||||||
</td>
|
</span>
|
||||||
<td className="px-2 py-1 text-gray-400 w-24 whitespace-nowrap">{c.expiry || '—'}</td>
|
)}
|
||||||
</tr>
|
</td>
|
||||||
))}
|
<td className="px-2 py-1 text-gray-500 w-24">
|
||||||
|
{c.functional_role && c.functional_role !== 'unknown'
|
||||||
|
? (ROLE_LABEL[c.functional_role] || c.functional_role)
|
||||||
|
: <span className="text-gray-300">—</span>}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1 text-gray-500 break-words">
|
||||||
|
{c.purpose
|
||||||
|
? c.purpose
|
||||||
|
: <span className="text-amber-600 italic">kein Zweck</span>}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1 text-gray-400 w-24 whitespace-nowrap">{c.expiry || '—'}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,48 +194,96 @@ function VendorRow({ v }: { v: SnapshotVendor }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CookieResultView({ snapshot }: { snapshot: Snapshot }) {
|
export function CookieResultView(
|
||||||
|
{ snapshot, cookieCategories }:
|
||||||
|
{ snapshot: Snapshot; cookieCategories?: LibCategories },
|
||||||
|
) {
|
||||||
const vendors = snapshot.cmp_vendors || []
|
const vendors = snapshot.cmp_vendors || []
|
||||||
|
const [viewMode, setViewMode] = useState<'role' | 'category'>('role')
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
const cookies = vendors.reduce((n, v) => n + (v.cookies?.length || 0), 0)
|
const cookies = vendors.reduce((n, v) => n + (v.cookies?.length || 0), 0)
|
||||||
const marketing = vendors.filter(v => (v.category || '').toLowerCase() === 'marketing').length
|
const marketing = vendors.filter(v => (v.category || '').toLowerCase() === 'marketing').length
|
||||||
const drittland = vendors.filter(v => v.country && !EEA.has(v.country.toUpperCase())).length
|
const drittland = vendors.filter(v => v.country && !EEA.has(v.country.toUpperCase())).length
|
||||||
return { cookies, marketing, drittland }
|
let misplaced = 0
|
||||||
}, [vendors])
|
for (const v of vendors) {
|
||||||
|
const dc = canonCat(v.category)
|
||||||
|
for (const c of v.cookies || []) {
|
||||||
|
if (mismatch(c.name, dc, cookieCategories)?.severe) misplaced++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { cookies, marketing, drittland, misplaced }
|
||||||
|
}, [vendors, cookieCategories])
|
||||||
|
|
||||||
const grouped = useMemo(() => GROUPS.map(g => ({
|
const grouped = useMemo(() => {
|
||||||
...g,
|
const sortByScore = (a: SnapshotVendor, b: SnapshotVendor) =>
|
||||||
vendors: vendors
|
(a.compliance_score ?? 100) - (b.compliance_score ?? 100)
|
||||||
.filter(v => GROUPS.find(gg => gg.test((v.recipient_type || '').toUpperCase()))?.key === g.key)
|
if (viewMode === 'category') {
|
||||||
.sort((a, b) => (a.compliance_score ?? 100) - (b.compliance_score ?? 100)),
|
return CATEGORY_GROUPS
|
||||||
})).filter(g => g.vendors.length > 0), [vendors])
|
.map(g => ({ ...g, vendors: vendors.filter(v => canonCat(v.category) === g.key).sort(sortByScore) }))
|
||||||
|
.filter(g => g.vendors.length > 0)
|
||||||
|
}
|
||||||
|
return GROUPS
|
||||||
|
.map(g => ({
|
||||||
|
...g,
|
||||||
|
vendors: vendors
|
||||||
|
.filter(v => GROUPS.find(gg => gg.test((v.recipient_type || '').toUpperCase()))?.key === g.key)
|
||||||
|
.sort(sortByScore),
|
||||||
|
}))
|
||||||
|
.filter(g => g.vendors.length > 0)
|
||||||
|
}, [vendors, viewMode])
|
||||||
|
|
||||||
|
const toggleBtn = (mode: 'role' | 'category', label: string) => (
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode(mode)}
|
||||||
|
className={`px-2.5 py-1 rounded text-xs ${viewMode === mode ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<div>
|
||||||
Cookie-Auswertung — {snapshot.site_domain || 'Snapshot'}
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
</h2>
|
Cookie-Auswertung — {snapshot.site_domain || 'Snapshot'}
|
||||||
<p className="text-xs text-gray-500 mt-0.5">
|
</h2>
|
||||||
aus gespeichertem Snapshot (kein Re-Crawl) ·{' '}
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
{snapshot.created_at ? snapshot.created_at.slice(0, 19).replace('T', ' ') : ''}
|
aus gespeichertem Snapshot (kein Re-Crawl) ·{' '}
|
||||||
</p>
|
{snapshot.created_at ? snapshot.created_at.slice(0, 19).replace('T', ' ') : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-[11px] text-gray-500 mr-1">Gruppierung:</span>
|
||||||
|
{toggleBtn('role', 'Rechtliche Rolle')}
|
||||||
|
{toggleBtn('category', 'Banner-Kategorie')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
<Tile label="Anbieter" value={vendors.length} tone="text-gray-800" />
|
<Tile label="Anbieter" value={vendors.length} tone="text-gray-800" />
|
||||||
<Tile label="Cookies gesamt" value={stats.cookies} tone="text-gray-800" />
|
<Tile label="Cookies gesamt" value={stats.cookies} tone="text-gray-800" />
|
||||||
<Tile label="Marketing-Anbieter" value={stats.marketing} tone={stats.marketing > 0 ? 'text-red-700' : 'text-gray-800'} />
|
<Tile label="Marketing-Anbieter" value={stats.marketing} tone={stats.marketing > 0 ? 'text-red-700' : 'text-gray-800'} />
|
||||||
<Tile label="Drittland (außerhalb EWR)" value={stats.drittland} tone={stats.drittland > 0 ? 'text-amber-700' : 'text-gray-800'} />
|
<Tile label="Drittland (außerhalb EWR)" value={stats.drittland} tone={stats.drittland > 0 ? 'text-amber-700' : 'text-gray-800'} />
|
||||||
|
<Tile label="Falsch einsortiert (lt. Library)" value={stats.misplaced} tone={stats.misplaced > 0 ? 'text-red-700' : 'text-gray-800'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{viewMode === 'category' && (
|
||||||
|
<p className="text-[11px] text-gray-500 -mt-1">
|
||||||
|
Banner-Kategorie wie im Consent-Tool deklariert. Badge{' '}
|
||||||
|
<span className="px-1 py-0.5 rounded text-[9px] bg-red-100 text-red-700">→ sollte: …</span>{' '}
|
||||||
|
zeigt die tatsächliche Kategorie laut Library (Fehl-Einsortierung).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{grouped.map(g => (
|
{grouped.map(g => (
|
||||||
<div key={g.key} className="border rounded-lg overflow-hidden">
|
<div key={g.key} className="border rounded-lg overflow-hidden">
|
||||||
<div className="px-3 py-2 bg-slate-50 border-b text-xs font-semibold text-gray-700">
|
<div className="px-3 py-2 bg-slate-50 border-b text-xs font-semibold text-gray-700">
|
||||||
{g.label} <span className="text-gray-400 font-normal">({g.vendors.length})</span>
|
{g.label} <span className="text-gray-400 font-normal">({g.vendors.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-gray-100">
|
<div className="divide-y divide-gray-100">
|
||||||
{g.vendors.map((v, i) => <VendorRow key={i} v={v} />)}
|
{g.vendors.map((v, i) => <VendorRow key={i} v={v} lib={cookieCategories} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -50,4 +50,20 @@ describe('CookieResultView', () => {
|
|||||||
expect(screen.getByText('LSKey-c$Policy')).toBeInTheDocument()
|
expect(screen.getByText('LSKey-c$Policy')).toBeInTheDocument()
|
||||||
expect(screen.getByText(/kein Zweck/)).toBeInTheDocument() // leerer purpose
|
expect(screen.getByText(/kein Zweck/)).toBeInTheDocument() // leerer purpose
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('schaltet auf die Banner-Kategorie-Sicht um', () => {
|
||||||
|
render(<CookieResultView snapshot={SNAP} />)
|
||||||
|
fireEvent.click(screen.getByText('Banner-Kategorie'))
|
||||||
|
expect(screen.getByText(/Notwendig \(essenziell\)/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Salesforce')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Meta / Facebook')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('markiert falsch einsortierte Cookies (Tracker als notwendig)', () => {
|
||||||
|
// 'sid' ist als necessary deklariert, Library sagt marketing → § 25-relevant.
|
||||||
|
render(<CookieResultView snapshot={SNAP} cookieCategories={{ sid: 'marketing' }} />)
|
||||||
|
expect(screen.getByText('Falsch einsortiert (lt. Library)')).toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByText('Salesforce'))
|
||||||
|
expect(screen.getByText(/sollte: Marketing/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default function SnapshotDetail(
|
|||||||
) {
|
) {
|
||||||
const { snapshotId } = useUnwrap(params)
|
const { snapshotId } = useUnwrap(params)
|
||||||
const [snap, setSnap] = useState<any>(null)
|
const [snap, setSnap] = useState<any>(null)
|
||||||
|
const [check, setCheck] = useState<any>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -34,6 +35,16 @@ export default function SnapshotDetail(
|
|||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [snapshotId])
|
}, [snapshotId])
|
||||||
|
|
||||||
|
// Library-Abgleich einmal laden (Findings + cookie_categories für beide Views).
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/cookie-check`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => { if (!cancelled) setCheck(d) })
|
||||||
|
.catch(() => { if (!cancelled) setCheck(null) })
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [snapshotId])
|
||||||
|
|
||||||
const hasCookies = (snap?.cmp_vendors?.length ?? 0) > 0
|
const hasCookies = (snap?.cmp_vendors?.length ?? 0) > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,8 +58,8 @@ export default function SnapshotDetail(
|
|||||||
<div className="text-sm text-red-600">Snapshot nicht gefunden.</div>
|
<div className="text-sm text-red-600">Snapshot nicht gefunden.</div>
|
||||||
) : hasCookies ? (
|
) : hasCookies ? (
|
||||||
<>
|
<>
|
||||||
<CookieLibraryPanel snapshotId={snapshotId} />
|
<CookieLibraryPanel snapshotId={snapshotId} data={check ?? undefined} />
|
||||||
<CookieResultView snapshot={snap} />
|
<CookieResultView snapshot={snap} cookieCategories={check?.cookie_categories} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ def analyze_cookies(vendors: list[dict], big_lib: dict | None = None) -> dict:
|
|||||||
in_library = 0
|
in_library = 0
|
||||||
seen_third: set[str] = set()
|
seen_third: set[str] = set()
|
||||||
seen_alt: set[str] = set()
|
seen_alt: set[str] = set()
|
||||||
|
# name_lower → tatsächliche Kategorie laut Library (für die Banner-Sicht:
|
||||||
|
# zeigt, wo ein Cookie eigentlich hingehört, falls falsch einsortiert).
|
||||||
|
cookie_cats: dict[str, str] = {}
|
||||||
|
|
||||||
for v in vendors or []:
|
for v in vendors or []:
|
||||||
vcat = (v.get("category") or "").lower()
|
vcat = (v.get("category") or "").lower()
|
||||||
@@ -149,6 +152,8 @@ def analyze_cookies(vendors: list[dict], big_lib: dict | None = None) -> dict:
|
|||||||
})
|
})
|
||||||
rich = lookup_cookie(name) or {}
|
rich = lookup_cookie(name) or {}
|
||||||
big = big_lib.get(name.lower(), {})
|
big = big_lib.get(name.lower(), {})
|
||||||
|
if big.get("actual_category"):
|
||||||
|
cookie_cats[name.lower()] = big["actual_category"]
|
||||||
if not rich and not big:
|
if not rich and not big:
|
||||||
continue
|
continue
|
||||||
in_library += 1
|
in_library += 1
|
||||||
@@ -268,4 +273,5 @@ def analyze_cookies(vendors: list[dict], big_lib: dict | None = None) -> dict:
|
|||||||
"findings": len(findings),
|
"findings": len(findings),
|
||||||
},
|
},
|
||||||
"findings": findings,
|
"findings": findings,
|
||||||
|
"cookie_categories": cookie_cats,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,19 @@ def test_no_missing_retention_when_vendor_has_cookies():
|
|||||||
assert not [f for f in out["findings"] if f["type"] == "missing_retention"]
|
assert not [f for f in out["findings"] if f["type"] == "missing_retention"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookie_categories_exposes_actual_library_category():
|
||||||
|
# Für die Banner-Sicht: name_lower → tatsächliche Kategorie laut Library.
|
||||||
|
big = {"bmw_track_de": {
|
||||||
|
"actual_category": "marketing", "typical_max_age_seconds": 86400,
|
||||||
|
"purpose_de": "Tracking", "vendor_name": "BMW",
|
||||||
|
}}
|
||||||
|
out = analyze_cookies([{
|
||||||
|
"name": "BMW", "category": "necessary",
|
||||||
|
"cookies": [{"name": "bmw_track_de", "purpose": "x", "expiry": "1 Tag"}],
|
||||||
|
}], big)
|
||||||
|
assert out["cookie_categories"]["bmw_track_de"] == "marketing"
|
||||||
|
|
||||||
|
|
||||||
def test_big_library_covers_cookie_not_in_rich_db():
|
def test_big_library_covers_cookie_not_in_rich_db():
|
||||||
# Cookie nicht in der 35er rich-DB, aber in der grossen 2287er (big_lib).
|
# Cookie nicht in der 35er rich-DB, aber in der grossen 2287er (big_lib).
|
||||||
big = {"bmw_track_de": {
|
big = {"bmw_track_de": {
|
||||||
|
|||||||
Reference in New Issue
Block a user