admin/lib/observability/expectedProductError.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

89 lines
2.7 KiB
TypeScript

import { getProductApiErrorMessage } from '@/services/apiErrorLocalization'
/**
* Business-rule / expected client API codes. These are user-facing 4xx
* outcomes (block, full event, invalid OTP…), not application crashes.
* Keep INTERNAL_ERROR, SERVICE_UNAVAILABLE, FORBIDDEN, and NOT_FOUND out —
* those still need Sentry when they leak as unhandled exceptions.
*/
export const EXPECTED_PRODUCT_API_CODES = new Set([
'MESSAGING_BLOCKED',
'CHAT_CLOSED',
'NOT_PARTICIPANT',
'SELF_ACTION_FORBIDDEN',
'OTP_INVALID',
'OTP_EXPIRED',
'OTP_RATE_LIMITED',
'OTP_ATTEMPTS_EXCEEDED',
'TERMS_ACCEPTANCE_REQUIRED',
'EVENT_FULL',
'EVENT_NOT_BOOKABLE',
'WAITLIST_NOT_AVAILABLE',
'ACTIVE_BOOKING_EXISTS',
'ALREADY_BOOKED',
'IDENTITY_REQUIRED',
'HOST_EVENT_LIMIT_REACHED',
'REVIEW_WINDOW_EXPIRED',
'HOST_REPLY_WINDOW_EXPIRED',
'REVIEW_LOCKED',
'EVENT_NOT_EDITABLE',
'EVENT_FIELD_LOCKED',
'PRICE_LOCKED',
'CAPACITY_BELOW_BOOKED',
'BOOKING_NOT_CONFIRMED',
'NOT_CHECKED_IN',
'ACCOUNT_SUSPENDED',
'ACCOUNT_DELETED',
'VALIDATION_FAILED',
'RATE_LIMITED',
'PUSH_SUBSCRIPTION_LIMIT_EXCEEDED',
'EVENT_HAS_ACTIVE_BOOKINGS',
'UNAUTHORIZED',
'ACCESS_TOKEN_INVALID',
'ACCESS_TOKEN_EXPIRED',
'REFRESH_TOKEN_INVALID',
'REFRESH_TOKEN_EXPIRED',
])
const EXPECTED_PRODUCT_MESSAGES = new Set(
[...EXPECTED_PRODUCT_API_CODES].flatMap((code) => {
const message = getProductApiErrorMessage(code)
return message ? [message] : []
})
)
/** Soft session-expiry signal from axios `handleSessionExpired` — not an app crash. */
EXPECTED_PRODUCT_MESSAGES.add('نشست منقضی شده است')
const readApiCode = (error: object): string | undefined => {
if ('apiCode' in error && typeof error.apiCode === 'string') return error.apiCode
if ('code' in error && typeof error.code === 'string') return error.code
return undefined
}
export const isExpectedProductMessage = (message: string): boolean => EXPECTED_PRODUCT_MESSAGES.has(message.trim())
export const isExpectedProductError = (error: unknown): boolean => {
if (typeof error === 'string') return isExpectedProductMessage(error)
if (!error || typeof error !== 'object') return false
const apiCode = readApiCode(error)
if (apiCode && EXPECTED_PRODUCT_API_CODES.has(apiCode)) return true
return 'message' in error && typeof error.message === 'string' && isExpectedProductMessage(error.message)
}
export const shouldDropExpectedProductSentryEvent = (
event: { exception?: { values?: { value?: string }[] }; message?: string },
originalException?: unknown
): boolean => {
if (isExpectedProductError(originalException)) return true
const value = event.exception?.values?.[0]?.value ?? event.message
return typeof value === 'string' && isExpectedProductMessage(value)
}