Skip to content

Commit 4eab524

Browse files
committed
Client Side Error Reporting: update
1 parent 2c6fd98 commit 4eab524

File tree

8 files changed

+306
-793
lines changed

8 files changed

+306
-793
lines changed

package-lock.json

Lines changed: 185 additions & 673 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"build": "vite build"
66
},
77
"dependencies": {
8-
"stacktrace-js": "^2.0.2"
8+
"stacktrace-js": "^2.0.2",
9+
"stacktrace-gps": "stacktracejs/stacktrace-gps#master"
910
},
1011
"devDependencies": {
1112
"laravel-vite-plugin": "^2",

src/AffordableMobiles/GServerlessSupportLaravel/Integration/ErrorReporting/ClientSideJavaScript/Http/Controllers/ErrorReporterController.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function report(Request $request): JsonResponse
5252
'serviceContext.resourceType' => 'prohibited',
5353
'traceId' => 'required|string|size:32',
5454
'stack_trace_frames' => 'required|array',
55-
'stack_trace_frames.*.function_name' => 'nullable|string',
55+
'stack_trace_frames.*.function_name' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9_$.<>]+$/'],
5656
'stack_trace_frames.*.file_name' => 'nullable|string',
5757
'stack_trace_frames.*.line_number' => 'nullable|integer',
5858
'stack_trace_frames.*.column_number' => 'nullable|integer',
@@ -85,6 +85,7 @@ public function report(Request $request): JsonResponse
8585
'context' => [
8686
'httpRequest' => [
8787
'method' => 'CLIENT_SIDE_ERROR',
88+
'responseStatusCode' => 418,
8889
'url' => $validated['context']['httpRequest']['url'] ?? 'unknown',
8990
'userAgent' => $validated['context']['httpRequest']['userAgent'] ?? 'unknown',
9091
'referrer' => $request->header('referer'),
@@ -159,12 +160,16 @@ private function formatStackTrace(string $message, array $stackFrames): string
159160
$formattedLines = [$message];
160161

161162
foreach ($stackFrames as $frame) {
162-
$functionName = $frame['function_name'] ?? 'anonymous';
163-
$fileName = $frame['file_name'] ?? 'unknown.js';
163+
$functionName = $frame['function_name'] ?? null;
164+
$fileName = $frame['file_name'] ?? 'unknown';
164165
$lineNumber = $frame['line_number'] ?? 0;
165166
$columnNumber = $frame['column_number'] ?? 0;
166167

167-
$formattedLines[] = " at {$functionName} ({$fileName}:{$lineNumber}:{$columnNumber})";
168+
if ($functionName) {
169+
$formattedLines[] = " at {$functionName} ({$fileName}:{$lineNumber}:{$columnNumber})";
170+
} else {
171+
$formattedLines[] = " at {$fileName}:{$lineNumber}:{$columnNumber}";
172+
}
168173
}
169174

170175
return implode("\n", $formattedLines);

src/resources/js/dist/.vite/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"src/resources/js/error-reporter.js": {
3-
"file": "error-reporter.CXJCOPgA.js",
3+
"file": "error-reporter.llYQMYLD.js",
44
"name": "error-reporter",
55
"src": "src/resources/js/error-reporter.js",
66
"isEntry": true

src/resources/js/dist/error-reporter.CXJCOPgA.js

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/resources/js/dist/error-reporter.llYQMYLD.js

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/resources/js/dist/error-reporter.CXJCOPgA.js.map renamed to src/resources/js/dist/error-reporter.llYQMYLD.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/resources/js/error-reporter.js

Lines changed: 97 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,113 @@
11
// Import the dependency directly. Vite will bundle it.
22
import StackTrace from 'stacktrace-js';
33

4-
// --- Module Configuration & State ---
5-
// All variables are now in the module's top-level scope.
6-
let config = {
7-
endpointUrl: null,
8-
service: 'not-configured',
9-
version: 'not-configured',
10-
traceId: 'not-configured',
11-
context: {},
12-
};
13-
14-
// --- Private Functions ---
15-
164
/**
17-
* Handles the error, generates a stack trace, and sends the report.
18-
* @param {Error|string} error - The error object or message.
5+
* Checks if a function name looks like a valid identifier, not a parser artifact.
6+
* A valid name should not contain characters like parentheses, spaces, etc.
7+
* @param {string | null | undefined} name The function name to check.
8+
* @returns {boolean}
199
*/
20-
function handleError(error) {
21-
StackTrace.fromError(error)
22-
.then(stackFrames => {
23-
const payload = formatPayload(error, stackFrames);
24-
sendReport(payload);
25-
})
26-
.catch(err => {
27-
console.error('Error while generating stacktrace:', err);
28-
// Fallback: send report with no stack trace
29-
const payload = formatPayload(error, []);
30-
sendReport(payload);
31-
});
10+
function isValidFunctionName(name) {
11+
if (!name || name === '<unknown>') {
12+
return false;
13+
}
14+
// This regex allows for typical JS identifiers, including object properties (e.g., object.method).
15+
// It explicitly disallows characters commonly found in parser artifacts like ')' or ' '.
16+
return /^[a-zA-Z0-9_$.<>]+$/.test(name);
3217
}
3318

34-
/**
35-
* Formats the error data into the payload for the backend.
36-
* @param {Error|string} error - The error object or message.
37-
* @param {Array} stackFrames - The array of stack frames from StackTrace.js.
38-
* @returns {object} The formatted payload.
39-
*/
40-
function formatPayload(error, stackFrames) {
41-
return {
42-
message: error.message || String(error),
43-
serviceContext: {
44-
service: config.service,
45-
version: config.version,
46-
},
47-
traceId: config.traceId,
48-
stack_trace_frames: stackFrames.map(sf => ({
49-
function_name: sf.functionName,
50-
file_name: sf.fileName,
51-
line_number: sf.lineNumber,
52-
column_number: sf.columnNumber,
53-
})),
54-
context: {
55-
...config.context,
56-
httpRequest: {
57-
url: window.location.href,
58-
userAgent: navigator.userAgent,
59-
},
60-
},
61-
};
62-
}
6319

64-
/**
65-
* Sends the report to the configured backend endpoint.
66-
* @param {object} payload - The error report payload.
67-
*/
68-
function sendReport(payload) {
69-
if (!config.endpointUrl) {
70-
console.error('Error reporter: endpointUrl is not configured.');
71-
console.log('Error payload:', payload); // Log to console for debugging
72-
return;
73-
}
20+
const errorReporter = {
21+
config: {
22+
endpointUrl: null,
23+
service: 'not-configured',
24+
version: 'not-configured',
25+
traceId: 'not-configured',
26+
context: {},
27+
},
7428

75-
if (navigator.sendBeacon) {
76-
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
77-
navigator.sendBeacon(config.endpointUrl, blob);
78-
} else {
79-
fetch(config.endpointUrl, {
80-
method: 'POST',
81-
headers: { 'Content-Type': 'application/json' },
82-
body: JSON.stringify(payload),
83-
keepalive: true,
84-
}).catch(err => console.error('Error sending report:', err));
85-
}
86-
}
29+
init(userConfig) {
30+
this.config = { ...this.config, ...userConfig };
8731

88-
// --- Public API ---
32+
if (typeof StackTrace === 'undefined') {
33+
console.error('StackTrace.js is not available. Error reporting is disabled.');
34+
return;
35+
}
8936

90-
/**
91-
* Initializes the error reporter and attaches global handlers.
92-
* @param {object} userConfig - The configuration object.
93-
*/
94-
function init(userConfig) {
95-
config = { ...config, ...userConfig };
37+
window.onerror = (message, source, lineno, colno, error) => {
38+
this.handleError(error || message);
39+
return false;
40+
};
9641

97-
if (typeof StackTrace === 'undefined') {
98-
console.error('StackTrace.js dependency is not available. Error reporting is disabled.');
99-
return;
100-
}
42+
window.addEventListener('unhandledrejection', event => {
43+
this.handleError(event.reason || 'Unhandled promise rejection');
44+
});
45+
},
10146

102-
window.onerror = (message, source, lineno, colno, error) => {
103-
handleError(error || message);
104-
return false;
105-
};
47+
handleError(error) {
48+
StackTrace.fromError(error)
49+
.then(stackFrames => {
50+
const payload = this.formatPayload(error, stackFrames);
51+
this.sendReport(payload);
52+
})
53+
.catch(err => {
54+
console.error('Error while generating stacktrace:', err);
55+
const payload = this.formatPayload(error, []);
56+
this.sendReport(payload);
57+
});
58+
},
10659

107-
window.addEventListener('unhandledrejection', event => {
108-
handleError(event.reason || 'Unhandled promise rejection');
109-
});
110-
}
60+
formatPayload(error, stackFrames) {
61+
let errorMessage = String(error);
62+
// If the error is a proper Error object, prefix the message with its type (e.g., "ReferenceError: ...").
63+
if (error instanceof Error && error.name && error.message) {
64+
errorMessage = `${error.name}: ${error.message}`;
65+
}
11166

112-
// --- EXPLICIT GLOBAL ASSIGNMENT ---
113-
// Instead of relying on a default export and Vite's 'name' property in UMD mode,
114-
// we explicitly assign our public API to the window object. This is a more direct
115-
// and robust method to ensure the global variable is available after bundling.
116-
window.errorReporter = {
117-
init,
67+
return {
68+
message: errorMessage,
69+
serviceContext: {
70+
service: this.config.service,
71+
version: this.config.version,
72+
},
73+
traceId: this.config.traceId,
74+
stack_trace_frames: stackFrames.map(sf => ({
75+
function_name: isValidFunctionName(sf.functionName) ? sf.functionName : null,
76+
file_name: sf.fileName,
77+
line_number: sf.lineNumber,
78+
column_number: sf.columnNumber,
79+
})),
80+
context: {
81+
...this.config.context,
82+
url: window.location.href,
83+
userAgent: navigator.userAgent,
84+
},
85+
};
86+
},
87+
88+
sendReport(payload) {
89+
if (!this.config.endpointUrl) {
90+
console.error('Error reporter: endpointUrl is not configured.');
91+
console.log('Error payload:', payload);
92+
return;
93+
}
94+
95+
if (navigator.sendBeacon) {
96+
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
97+
navigator.sendBeacon(this.config.endpointUrl, blob);
98+
} else {
99+
fetch(this.config.endpointUrl, {
100+
method: 'POST',
101+
headers: {
102+
'Content-Type': 'application/json',
103+
'Accept': 'application/json',
104+
},
105+
body: JSON.stringify(payload),
106+
keepalive: true,
107+
}).catch(err => console.error('Error sending report:', err));
108+
}
109+
}
118110
};
111+
112+
// Directly assign to window to ensure it's globally available for UMD builds.
113+
window.errorReporter = errorReporter;

0 commit comments

Comments
 (0)