Skip to content

Commit a1eb684

Browse files
Copilotmgifford
andcommitted
feat: add third-party JavaScript service detection and report section
- Add THIRD_PARTY_SERVICES detection patterns to tech-detector.js (17 services across analytics, advertising, social, CDN, fonts, maps, government, support) - Export getThirdPartyServiceMeta() for category/privacy metadata lookups - Add detectThirdPartyServices() helper wired into detectTechnologies() - Extend buildTechSummary() with third_party_service_counts/_urls aggregation - Add .tech-badge-3p CSS badge and .privacy-concern indicator to render-pages.js - Update renderTechBadges() with per-URL third-party count badge - Extend renderTechSummarySection() with Third-Party JavaScript Services table - Add 22 new tests covering detection, metadata, and summary aggregation Co-authored-by: mgifford <116832+mgifford@users.noreply.github.com> Agent-Logs-Url: https://github.com/mgifford/daily-dap/sessions/788063cb-1297-4ab9-bf15-cbbeafd1afb0
1 parent 23cf665 commit a1eb684

File tree

3 files changed

+549
-9
lines changed

3 files changed

+549
-9
lines changed

src/publish/render-pages.js

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AXE_TO_FPC, FPC_LABELS, FPC_SVGS, FPC_DESCRIPTIONS } from '../data/axe-
22
import { getFpcPrevalenceRates, CENSUS_DISABILITY_STATS } from '../data/census-disability-stats.js';
33
import { getPolicyNarrative, getHeuristicsForAxeRule } from '../data/axe-impact-loader.js';
44
import { NNG_HEURISTICS } from '../data/nng-heuristics.js';
5+
import { getThirdPartyServiceMeta } from '../scanners/tech-detector.js';
56

67
const GITHUB_URL = 'https://github.com/mgifford/daily-dap';
78
const DASHBOARD_URL = 'https://mgifford.github.io/daily-dap/docs/reports/index.html';
@@ -848,12 +849,25 @@ function renderSharedStyles() {
848849
}
849850
.tech-badge-cms { background-color: hsl(200 60% 88%); color: hsl(200 60% 25%); }
850851
.tech-badge-uswds { background-color: hsl(225 70% 88%); color: hsl(225 70% 25%); }
852+
.tech-badge-3p { background-color: hsl(0 0% 88%); color: hsl(0 0% 25%); }
851853
:root:not([data-color-scheme="light"]) .tech-badge-cms { background-color: hsl(200 40% 25%); color: hsl(200 40% 85%); }
852854
:root:not([data-color-scheme="light"]) .tech-badge-uswds { background-color: hsl(225 40% 25%); color: hsl(225 40% 85%); }
855+
:root:not([data-color-scheme="light"]) .tech-badge-3p { background-color: hsl(0 0% 25%); color: hsl(0 0% 85%); }
853856
html[data-color-scheme="dark"] .tech-badge-cms { background-color: hsl(200 40% 25%); color: hsl(200 40% 85%); }
854857
html[data-color-scheme="dark"] .tech-badge-uswds { background-color: hsl(225 40% 25%); color: hsl(225 40% 85%); }
858+
html[data-color-scheme="dark"] .tech-badge-3p { background-color: hsl(0 0% 25%); color: hsl(0 0% 85%); }
855859
html[data-color-scheme="light"] .tech-badge-cms { background-color: hsl(200 60% 88%); color: hsl(200 60% 25%); }
856860
html[data-color-scheme="light"] .tech-badge-uswds { background-color: hsl(225 70% 88%); color: hsl(225 70% 25%); }
861+
html[data-color-scheme="light"] .tech-badge-3p { background-color: hsl(0 0% 88%); color: hsl(0 0% 25%); }
862+
/* Privacy-concern indicator inside third-party service table */
863+
.privacy-concern {
864+
font-size: 0.75em;
865+
font-weight: 600;
866+
color: hsl(30 80% 35%);
867+
}
868+
:root:not([data-color-scheme="light"]) .privacy-concern { color: hsl(30 80% 70%); }
869+
html[data-color-scheme="dark"] .privacy-concern { color: hsl(30 80% 70%); }
870+
html[data-color-scheme="light"] .privacy-concern { color: hsl(30 80% 35%); }
857871
858872
/* ---------- URL cells ---------- */
859873
.url-cell {
@@ -1865,6 +1879,7 @@ function renderTechUrlTooltip(visibleText, urls, ariaLabel, preEscaped = false)
18651879
/**
18661880
* Render small inline technology badges for a single URL row.
18671881
* Shows CMS name and/or USWDS version if detected.
1882+
* Shows a count badge for third-party services if any are detected.
18681883
*
18691884
* @param {object|null} tech - detected_technologies object
18701885
* @returns {string} HTML string
@@ -1885,6 +1900,13 @@ function renderTechBadges(tech) {
18851900
parts.push(`<span class="tech-badge tech-badge-uswds" title="U.S. Web Design System${tech.uswds.version ? ` v${escapeHtml(tech.uswds.version)}` : ''}">${label}</span>`);
18861901
}
18871902

1903+
const services = tech.third_party_services ?? [];
1904+
if (services.length > 0) {
1905+
const serviceList = services.map(escapeHtml).join(', ');
1906+
const label = `${services.length} 3rd-party`;
1907+
parts.push(`<span class="tech-badge tech-badge-3p" title="Third-party services: ${serviceList}">${label}</span>`);
1908+
}
1909+
18881910
return parts.join(' ');
18891911
}
18901912

@@ -1900,10 +1922,20 @@ function renderTechSummarySection(report) {
19001922
return '';
19011923
}
19021924

1903-
const { cms_counts = {}, cms_urls = {}, uswds_count = 0, uswds_versions = [], uswds_version_urls = {}, total_scanned = 0 } = summary;
1925+
const {
1926+
cms_counts = {},
1927+
cms_urls = {},
1928+
uswds_count = 0,
1929+
uswds_versions = [],
1930+
uswds_version_urls = {},
1931+
total_scanned = 0,
1932+
third_party_service_counts = {},
1933+
third_party_service_urls = {}
1934+
} = summary;
19041935
const cmsEntries = Object.entries(cms_counts).sort((a, b) => b[1] - a[1]);
1936+
const thirdPartyEntries = Object.entries(third_party_service_counts).sort((a, b) => b[1] - a[1]);
19051937

1906-
if (cmsEntries.length === 0 && uswds_count === 0) {
1938+
if (cmsEntries.length === 0 && uswds_count === 0 && thirdPartyEntries.length === 0) {
19071939
return '';
19081940
}
19091941

@@ -1924,6 +1956,49 @@ function renderTechSummarySection(report) {
19241956
}).join(', ')}. The latest release is available at <a href="https://github.com/uswds/uswds/releases" target="_blank" rel="noreferrer">github.com/uswds/uswds/releases</a>.</p>`
19251957
: '';
19261958

1959+
const thirdPartySection = thirdPartyEntries.length > 0
1960+
? (() => {
1961+
const rows = thirdPartyEntries
1962+
.map(([name, count]) => {
1963+
const meta = getThirdPartyServiceMeta(name);
1964+
const category = meta ? escapeHtml(meta.category) : '';
1965+
const urls = third_party_service_urls[name] ?? [];
1966+
const countCell = renderTechUrlTooltip(String(count), urls, `${count} URL${count !== 1 ? 's' : ''} using ${name}`);
1967+
const shareCell = `${total_scanned > 0 ? Math.round((count / total_scanned) * 100) : 0}%`;
1968+
const privacyCell = meta?.privacy_concern
1969+
? `<span class="privacy-concern"><span aria-hidden="true">&#9888;</span> tracking</span>`
1970+
: '';
1971+
return `<tr>
1972+
<td data-label="Service">${escapeHtml(name)}</td>
1973+
<td data-label="Category">${category}</td>
1974+
<td data-label="URLs">${countCell}</td>
1975+
<td data-label="Share">${shareCell}</td>
1976+
<td data-label="Privacy">${privacyCell}</td>
1977+
</tr>`;
1978+
})
1979+
.join('\n');
1980+
const trackingCount = thirdPartyEntries.filter(([name]) => getThirdPartyServiceMeta(name)?.privacy_concern).length;
1981+
const trackingNote = trackingCount > 0
1982+
? `<p><strong>${trackingCount}</strong> service${trackingCount !== 1 ? 's' : ''} marked <span class="privacy-concern"><span aria-hidden="true">&#9888;</span> tracking</span> send user data to third-party servers and may have privacy implications for site visitors.</p>`
1983+
: '';
1984+
return `
1985+
<h3 id="third-party-js-heading">Third-Party JavaScript Services${renderAnchorLink('third-party-js-heading', 'Third-Party JavaScript Services')}</h3>
1986+
<p>Third-party scripts detected from network requests loaded by scanned pages. These services may affect page performance, user privacy, and security posture.</p>
1987+
${trackingNote}
1988+
${wrapTable(`<table>
1989+
<caption>Third-party services detected across ${total_scanned} successfully scanned URLs</caption>
1990+
<thead><tr>
1991+
<th scope="col">Service</th>
1992+
<th scope="col">Category</th>
1993+
<th scope="col">URLs</th>
1994+
<th scope="col">Share</th>
1995+
<th scope="col">Privacy</th>
1996+
</tr></thead>
1997+
<tbody>${rows}</tbody>
1998+
</table>`)}`;
1999+
})()
2000+
: '';
2001+
19272002
return `
19282003
<section aria-labelledby="tech-summary-heading">
19292004
<h2 id="tech-summary-heading">Detected Technologies${renderAnchorLink('tech-summary-heading', 'Detected Technologies')}</h2>
@@ -1935,6 +2010,7 @@ function renderTechSummarySection(report) {
19352010
</table>`) : ''}
19362011
<p>USWDS detected on <strong>${uswds_count}</strong> of <strong>${total_scanned}</strong> scanned URL${total_scanned !== 1 ? 's' : ''}${total_scanned > 0 ? ` (${Math.round((uswds_count / total_scanned) * 100)}%)` : ''}.</p>
19372012
${uswdsVersionList}
2013+
${thirdPartySection}
19382014
</section>`;
19392015
}
19402016

0 commit comments

Comments
 (0)