Skip to content

Commit 63bc26b

Browse files
authored
fix(content): don't suppress non-extension unhandledrejection (#17)
Only call preventDefault() for unhandledrejection events attributable to LightSession; keep site errors visible in DevTools. Adds a small filter helper + unit tests.
1 parent 1cd6d1f commit 63bc26b

File tree

3 files changed

+104
-2
lines changed

3 files changed

+104
-2
lines changed

extension/src/content/content.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from './status-bar';
2323
import { isEmptyChatView } from './chat-view';
2424
import { installUserCollapse, type UserCollapseController } from './user-collapse';
25+
import { isLightSessionRejection } from './rejection-filter';
2526

2627

2728
// ============================================================================
@@ -412,6 +413,23 @@ window.addEventListener('error', (event) => {
412413
});
413414

414415
window.addEventListener('unhandledrejection', (event) => {
415-
logError('Unhandled promise rejection:', event.reason);
416-
event.preventDefault();
416+
let extensionUrlPrefix: string | undefined;
417+
try {
418+
extensionUrlPrefix = browser?.runtime?.getURL?.('');
419+
} catch {
420+
extensionUrlPrefix = undefined;
421+
}
422+
423+
// Do not suppress site (ChatGPT) errors. Only suppress if it clearly originates from LightSession.
424+
const strictIsOurs = isLightSessionRejection(event.reason, extensionUrlPrefix);
425+
const looseIsOurs = isLightSessionRejection(event.reason);
426+
427+
if (strictIsOurs || looseIsOurs) {
428+
logError('Unhandled promise rejection:', event.reason);
429+
430+
// Only suppress default reporting if we are confident this originates from our extension.
431+
if (strictIsOurs) {
432+
event.preventDefault();
433+
}
434+
}
417435
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Helpers for filtering global error events so we don't suppress site errors.
3+
*
4+
* The content script can observe `window` errors/rejections originating from the page.
5+
* Calling `preventDefault()` on these events suppresses the browser's default reporting,
6+
* so we must only suppress errors that are clearly caused by LightSession itself.
7+
*/
8+
9+
export function isLightSessionRejection(
10+
reason: unknown,
11+
extensionUrlPrefix?: string,
12+
): boolean {
13+
const parts: string[] = [];
14+
15+
if (typeof reason === 'string') {
16+
parts.push(reason);
17+
} else if (reason instanceof Error) {
18+
if (typeof reason.message === 'string') parts.push(reason.message);
19+
if (typeof reason.stack === 'string') parts.push(reason.stack);
20+
if (typeof reason.name === 'string') parts.push(reason.name);
21+
} else if (typeof reason === 'object' && reason !== null) {
22+
const r = reason as Record<string, unknown>;
23+
if (typeof r.message === 'string') parts.push(r.message);
24+
if (typeof r.stack === 'string') parts.push(r.stack);
25+
if (typeof r.name === 'string') parts.push(r.name);
26+
// Some browsers use different keys on Error-like objects.
27+
if (typeof r.filename === 'string') parts.push(r.filename);
28+
if (typeof r.fileName === 'string') parts.push(r.fileName);
29+
}
30+
31+
if (parts.length === 0) return false;
32+
33+
const haystack = parts.join('\n');
34+
35+
// The most reliable signal: our own extension base URL.
36+
// If we have it, prefer it exclusively to avoid suppressing unrelated site errors.
37+
if (extensionUrlPrefix) {
38+
return haystack.includes(extensionUrlPrefix);
39+
}
40+
41+
// Fallback heuristics (only used if runtime URL isn't available).
42+
// Our logger prefix sometimes appears in thrown messages.
43+
if (haystack.includes('LS:')) return true;
44+
45+
// Useful in dev builds / source maps.
46+
if (haystack.includes('light-session')) return true;
47+
48+
return false;
49+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { isLightSessionRejection } from '../../extension/src/content/rejection-filter';
4+
5+
describe('isLightSessionRejection', () => {
6+
it('returns false for empty/unknown reasons', () => {
7+
expect(isLightSessionRejection(undefined)).toBe(false);
8+
expect(isLightSessionRejection(null)).toBe(false);
9+
expect(isLightSessionRejection(123)).toBe(false);
10+
expect(isLightSessionRejection({})).toBe(false);
11+
});
12+
13+
it('matches when reason string contains LS:', () => {
14+
expect(isLightSessionRejection('LS: boom')).toBe(true);
15+
});
16+
17+
it('does not match LS: in message when extension URL is provided but not present', () => {
18+
const prefix = 'chrome-extension://abc123/';
19+
expect(isLightSessionRejection('LS: boom', prefix)).toBe(false);
20+
});
21+
22+
it('matches when Error.stack contains the extension base URL', () => {
23+
const prefix = 'chrome-extension://abc123/';
24+
const err = new Error('nope');
25+
// Simulate a stack that points at our bundled content script URL.
26+
err.stack = `Error: nope\n at doThing (${prefix}dist/content.js:1:1)`;
27+
expect(isLightSessionRejection(err, prefix)).toBe(true);
28+
});
29+
30+
it('does not match non-LightSession errors by default', () => {
31+
const err = new Error('Some site error');
32+
err.stack = `Error: Some site error\n at foo (https://chatgpt.com/app.js:1:1)`;
33+
expect(isLightSessionRejection(err)).toBe(false);
34+
});
35+
});

0 commit comments

Comments
 (0)