Skip to content

Commit 469ca2e

Browse files
authored
fix(privacy): redact diagnostics URL/title/text for public reporting (#28)
Co-authored-by: NogaUwU <NogaUwU@users.noreply.github.com>
1 parent 29aa5b1 commit 469ca2e

2 files changed

Lines changed: 73 additions & 12 deletions

File tree

api/_lib/reporting.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,43 @@ function sanitizeText(value, limit = MAX_TEXT) {
1010
return String(value || '').trim().replace(/\s+/g, ' ').slice(0, limit);
1111
}
1212

13+
function sanitizeUrlForDiagnostics(input) {
14+
try {
15+
const parsed = new URL(String(input || ''));
16+
const parts = parsed.pathname.split('/').filter(Boolean);
17+
const first = parts[0] || '';
18+
const second = parts[1] || '';
19+
20+
if (first === 'c') return `${parsed.origin}/c/:id`;
21+
if (first === 'g') return `${parsed.origin}/g/:id`;
22+
if (!first) return `${parsed.origin}/`;
23+
if (second) return `${parsed.origin}/${first}/${second}`;
24+
return `${parsed.origin}/${first}`;
25+
} catch (_) {
26+
return sanitizeText(input, 120);
27+
}
28+
}
29+
30+
function sanitizePageLabel(inputUrl, platform = '') {
31+
const base = sanitizeText(platform || 'unknown', 24) || 'unknown';
32+
try {
33+
const path = new URL(String(inputUrl || '')).pathname || '/';
34+
if (path.includes('/c/')) return `${base}:conversation`;
35+
if (path.startsWith('/g/')) return `${base}:project`;
36+
if (path.startsWith('/apps')) return `${base}:apps`;
37+
return `${base}:page`;
38+
} catch (_) {
39+
return `${base}:page`;
40+
}
41+
}
42+
1343
function sanitizeTurn(turn) {
1444
return {
1545
id: sanitizeText(turn?.id, 120),
1646
turnIndex: Number.isFinite(turn?.turnIndex) ? turn.turnIndex : null,
1747
branchIndex: Number.isFinite(turn?.branchIndex) ? turn.branchIndex : null,
1848
role: sanitizeText(turn?.role, 24),
19-
text: sanitizeText(turn?.text || turn?.textSig, 120),
49+
text: '',
2050
};
2151
}
2252

@@ -28,7 +58,7 @@ function sanitizeDomSummary(summary) {
2858
tag: sanitizeText(sample?.tag, 24),
2959
testid: sanitizeText(sample?.testid, 80),
3060
cls: sanitizeText(sample?.cls, 160),
31-
text: sanitizeText(sample?.text, 160),
61+
text: '',
3262
})),
3363
}));
3464
}
@@ -38,7 +68,7 @@ function sanitizeProbe(probe) {
3868
platform: sanitizeText(probe?.platform, 24),
3969
version: sanitizeText(probe?.version, 40),
4070
ts: Number.isFinite(probe?.ts) ? probe.ts : null,
41-
url: sanitizeText(probe?.url, MAX_TEXT),
71+
url: sanitizeUrlForDiagnostics(probe?.url),
4272
broken: (Array.isArray(probe?.broken) ? probe.broken : []).map(item => sanitizeText(item, 60)).filter(Boolean),
4373
hits: probe?.hits && typeof probe.hits === 'object' ? probe.hits : {},
4474
};
@@ -53,7 +83,7 @@ function sanitizeDiagnostics(diagnostics) {
5383
platformLabel: sanitizeText(diagnostics.platformLabel, 40),
5484
extensionVersion: sanitizeText(diagnostics.extensionVersion, 24),
5585
selectorVersion: sanitizeText(diagnostics.selectorVersion, 40),
56-
url: sanitizeText(diagnostics.url, MAX_TEXT),
86+
url: sanitizeUrlForDiagnostics(diagnostics.url),
5787
ts: Number.isFinite(diagnostics.ts) ? diagnostics.ts : Date.now(),
5888
turnCount: Number.isFinite(diagnostics.turnCount) ? diagnostics.turnCount : 0,
5989
probe: sanitizeProbe(diagnostics.probe),
@@ -93,8 +123,8 @@ function parseReportBody(body) {
93123
type: input.type === 'user_report' ? 'user_report' : 'auto_probe',
94124
description: sanitizeText(input.description, MAX_DESCRIPTION),
95125
source: sanitizeText(input.source, 40) || 'extension',
96-
tabUrl: sanitizeText(input.tabUrl, MAX_TEXT),
97-
pageTitle: sanitizeText(input.pageTitle, 120),
126+
tabUrl: sanitizeUrlForDiagnostics(input.tabUrl || diagnostics?.url),
127+
pageTitle: sanitizePageLabel(input.tabUrl || diagnostics?.url, diagnostics?.platform),
98128
reportedAt: new Date().toISOString(),
99129
diagnostics,
100130
metadata: {

background.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,37 @@ function sanitizeText(value, limit = 240) {
3939
return String(value || '').trim().replace(/\s+/g, ' ').slice(0, limit);
4040
}
4141

42+
function sanitizeUrlForDiagnostics(input) {
43+
try {
44+
const parsed = new URL(String(input || ''));
45+
const parts = parsed.pathname.split('/').filter(Boolean);
46+
const first = parts[0] || '';
47+
const second = parts[1] || '';
48+
49+
// Keep only route pattern, remove concrete identifiers/query/hash.
50+
if (first === 'c') return `${parsed.origin}/c/:id`;
51+
if (first === 'g') return `${parsed.origin}/g/:id`;
52+
if (!first) return `${parsed.origin}/`;
53+
if (second) return `${parsed.origin}/${first}/${second}`;
54+
return `${parsed.origin}/${first}`;
55+
} catch (_) {
56+
return sanitizeText(input || '', 120);
57+
}
58+
}
59+
60+
function sanitizePageLabel(inputUrl, platform = '') {
61+
const base = sanitizeText(platform || 'unknown', 24) || 'unknown';
62+
try {
63+
const path = new URL(String(inputUrl || '')).pathname || '/';
64+
if (path.includes('/c/')) return `${base}:conversation`;
65+
if (path.startsWith('/g/')) return `${base}:project`;
66+
if (path.startsWith('/apps')) return `${base}:apps`;
67+
return `${base}:page`;
68+
} catch (_) {
69+
return `${base}:page`;
70+
}
71+
}
72+
4273
function pruneReportedDiagnostics(now = Date.now()) {
4374
const ttl = Number(getReportingConfig().dedupeWindowMs) || (30 * 60 * 1000);
4475
for (const [key, ts] of reportedDiagnostics.entries()) {
@@ -81,7 +112,7 @@ function sanitizeTurn(turn) {
81112
turnIndex: Number.isFinite(turn?.turnIndex) ? turn.turnIndex : null,
82113
branchIndex: Number.isFinite(turn?.branchIndex) ? turn.branchIndex : null,
83114
role: sanitizeText(turn?.role, 24),
84-
text: sanitizeText(turn?.text || turn?.textSig, 120),
115+
text: '',
85116
};
86117
}
87118

@@ -94,14 +125,14 @@ function sanitizeDiagnostics(diagnostics) {
94125
platformLabel: sanitizeText(diagnostics.platformLabel, 40),
95126
extensionVersion: sanitizeText(diagnostics.extensionVersion, 24),
96127
selectorVersion: sanitizeText(diagnostics.selectorVersion, 40),
97-
url: sanitizeText(diagnostics.url, 240),
128+
url: sanitizeUrlForDiagnostics(diagnostics.url),
98129
ts: Number.isFinite(diagnostics.ts) ? diagnostics.ts : Date.now(),
99130
turnCount: Number.isFinite(diagnostics.turnCount) ? diagnostics.turnCount : 0,
100131
probe: {
101132
platform: sanitizeText(diagnostics.probe?.platform, 24),
102133
version: sanitizeText(diagnostics.probe?.version, 40),
103134
ts: Number.isFinite(diagnostics.probe?.ts) ? diagnostics.probe.ts : null,
104-
url: sanitizeText(diagnostics.probe?.url, 240),
135+
url: sanitizeUrlForDiagnostics(diagnostics.probe?.url),
105136
hits: diagnostics.probe?.hits && typeof diagnostics.probe.hits === 'object' ? diagnostics.probe.hits : {},
106137
broken: (Array.isArray(diagnostics.probe?.broken) ? diagnostics.probe.broken : []).map(item => sanitizeText(item, 60)),
107138
},
@@ -115,7 +146,7 @@ function sanitizeDiagnostics(diagnostics) {
115146
tag: sanitizeText(sample?.tag, 24),
116147
testid: sanitizeText(sample?.testid, 80),
117148
cls: sanitizeText(sample?.cls, 160),
118-
text: sanitizeText(sample?.text, 160),
149+
text: '',
119150
})),
120151
})),
121152
};
@@ -169,8 +200,8 @@ async function postReport({ type, diagnostics, description = '', sender }) {
169200
client: 'chrome-extension',
170201
publicKey: config.publicKey || '',
171202
description: sanitizeText(description, 1500),
172-
tabUrl: sanitizeText(sender?.tab?.url || sanitized.url, 240),
173-
pageTitle: sanitizeText(sender?.tab?.title || '', 120),
203+
tabUrl: sanitizeUrlForDiagnostics(sender?.tab?.url || sanitized.url),
204+
pageTitle: sanitizePageLabel(sender?.tab?.url || sanitized.url, sanitized.platform),
174205
extensionVersion: sanitizeText(sanitized.extensionVersion, 24),
175206
selectorVersion: sanitizeText(sanitized.selectorVersion, 40),
176207
diagnostics: sanitized,

0 commit comments

Comments
 (0)