feat(backend): log client-side render errors to the server (#62)

The frontend's error boundaries need somewhere to report to. A boundary that only shows a customer a message leaves nobody knowing it happened, which is the failure shape this project has designed against three times already.

POST /api/client-errors takes a report, truncates its fields, logs it with a [client-error] prefix and returns 204. No storage: the container log is where this project's operational visibility already lives, and a table with a retention policy and an admin screen is a subsystem larger than the issue.

An unrecognised context is a 400 rather than a log line under a guessed label, following parseItemFilters, which refuses a malformed filter instead of coercing it. Oversized fields go the other way and are truncated rather than refused, because an over-long report is still the only record of the failure.

The endpoint gets its own rate limiter rather than reusing passwordResetRequestLimiter, whose comment already warns that its caller-and-email key collapses every caller into one shared bucket on an endpoint without an email. The new one takes the default key generator, which also avoids the ERR_ERL_KEY_GEN_IPV6 warning the custom key produces.

Verified: 138 integration tests pass, 4 of them new, and 79 unit. The unit count rose by one without a test being written — routesAreWrapped.test.ts runs describe.each over the files in src/routes, so a new route file generates a case. The handler is synchronous and needs no asyncRoute wrapper.

Refs #62
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:23:18 -05:00
co-authored by Claude Opus 5
parent 94e2267eaf
commit c693672051
4 changed files with 138 additions and 0 deletions
+2
View File
@@ -13,6 +13,7 @@ import customersRouter from './routes/customers';
import publicRouter from './routes/public';
import cartRouter from './routes/cart';
import shippingAddressesRouter from './routes/shippingAddresses';
import clientErrorsRouter from './routes/clientErrors';
import { attachCustomer } from './middleware/customerAuth';
import { asyncRoute } from './asyncRoute';
@@ -52,6 +53,7 @@ app.use('/api/admin/tags', adminTagsRouter);
app.use('/api/admin', adminRouter);
app.use('/api/customers/me/addresses', shippingAddressesRouter);
app.use('/api/customers', customersRouter);
app.use('/api/client-errors', clientErrorsRouter);
app.use('/', publicRouter);
if (process.env.NODE_ENV !== 'test') {