mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-28 02:34:10 +00:00
feat: Add Sentry error tracking to auth API functions (#5623)
## Summary - Added comprehensive Sentry error tracking to all auth API functions - Implemented helper functions to reduce code duplication - Properly distinguish between HTTP errors and network errors ## Changes - Added `captureApiError` helper function for consistent error reporting - Added `isHttpError` helper to prevent duplicate error capture - Enhanced error tracking with: - Proper error type classification (`http_error` vs `network_error`) - HTTP status codes and response details - Operation names for better context - Route templates for better API endpoint tracking ## Test plan - [ ] Verify auth functions work correctly in normal flow - [ ] Test error scenarios (network failures, 4xx/5xx responses) - [ ] Confirm Sentry receives proper error reports without duplicates - [ ] Check that error messages are informative and actionable 🤖 Generated with [Claude Code](https://claude.ai/code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5623-feat-Add-Sentry-error-tracking-to-auth-API-functions-2716d73d3650819fbb15e73d19642235) by [Unito](https://www.unito.io)
This commit is contained in:
375
src/api/auth.ts
375
src/api/auth.ts
@@ -1,3 +1,4 @@
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { isEmpty } from 'es-toolkit/compat'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -8,84 +9,352 @@ export interface UserCloudStatus {
|
||||
|
||||
const ONBOARDING_SURVEY_KEY = 'onboarding_survey'
|
||||
|
||||
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
|
||||
const response = await api.fetchApi('/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get user: ${response.statusText}`)
|
||||
/**
|
||||
* Helper function to capture API errors with Sentry
|
||||
*/
|
||||
function captureApiError(
|
||||
error: Error,
|
||||
endpoint: string,
|
||||
errorType: 'http_error' | 'network_error',
|
||||
httpStatus?: number,
|
||||
operation?: string,
|
||||
extraContext?: Record<string, any>
|
||||
) {
|
||||
const tags: Record<string, any> = {
|
||||
api_endpoint: endpoint,
|
||||
error_type: errorType
|
||||
}
|
||||
|
||||
return response.json()
|
||||
if (httpStatus !== undefined) {
|
||||
tags.http_status = httpStatus
|
||||
}
|
||||
|
||||
if (operation) {
|
||||
tags.operation = operation
|
||||
}
|
||||
|
||||
const sentryOptions: any = {
|
||||
tags,
|
||||
extra: extraContext ? { ...extraContext } : undefined
|
||||
}
|
||||
|
||||
Sentry.captureException(error, sentryOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if error is already handled HTTP error
|
||||
*/
|
||||
function isHttpError(error: unknown, errorMessagePrefix: string): boolean {
|
||||
return error instanceof Error && error.message.startsWith(errorMessagePrefix)
|
||||
}
|
||||
|
||||
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
|
||||
try {
|
||||
const response = await api.fetchApi('/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = new Error(`Failed to get user: ${response.statusText}`)
|
||||
captureApiError(
|
||||
error,
|
||||
'/user',
|
||||
'http_error',
|
||||
response.status,
|
||||
undefined,
|
||||
{
|
||||
api: {
|
||||
method: 'GET',
|
||||
endpoint: '/user',
|
||||
status_code: response.status,
|
||||
status_text: response.statusText
|
||||
}
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to get user:')) {
|
||||
captureApiError(error as Error, '/user', 'network_error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInviteCodeStatus(
|
||||
inviteCode: string
|
||||
): Promise<{ expired: boolean }> {
|
||||
const response = await api.fetchApi(
|
||||
`/invite/${encodeURIComponent(inviteCode)}/status`,
|
||||
{
|
||||
try {
|
||||
const response = await api.fetchApi(
|
||||
`/invite/${encodeURIComponent(inviteCode)}/status`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
const error = new Error(
|
||||
`Failed to get invite code status: ${response.statusText}`
|
||||
)
|
||||
captureApiError(
|
||||
error,
|
||||
'/invite/{code}/status',
|
||||
'http_error',
|
||||
response.status,
|
||||
undefined,
|
||||
{
|
||||
api: {
|
||||
method: 'GET',
|
||||
endpoint: `/invite/${inviteCode}/status`,
|
||||
status_code: response.status,
|
||||
status_text: response.statusText
|
||||
},
|
||||
extra: {
|
||||
invite_code_length: inviteCode.length
|
||||
},
|
||||
route_template: '/invite/{code}/status',
|
||||
route_actual: `/invite/${inviteCode}/status`
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to get invite code status:')) {
|
||||
captureApiError(
|
||||
error as Error,
|
||||
'/invite/{code}/status',
|
||||
'network_error',
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
route_template: '/invite/{code}/status',
|
||||
route_actual: `/invite/${inviteCode}/status`
|
||||
}
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSurveyCompletedStatus(): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
// Not an error case - survey not completed is a valid state
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Survey status check returned non-ok response',
|
||||
level: 'info',
|
||||
data: {
|
||||
status: response.status,
|
||||
endpoint: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get invite code status: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function getSurveyCompletedStatus(): Promise<boolean> {
|
||||
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
// Check if data exists and is not empty
|
||||
return !isEmpty(data.value)
|
||||
} catch (error) {
|
||||
// Network error - still capture it as it's not thrown from above
|
||||
Sentry.captureException(error, {
|
||||
tags: {
|
||||
api_endpoint: '/settings/{key}',
|
||||
error_type: 'network_error'
|
||||
},
|
||||
extra: {
|
||||
route_template: '/settings/{key}',
|
||||
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
},
|
||||
level: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
const data = await response.json()
|
||||
// Check if data exists and is not empty
|
||||
return !isEmpty(data.value)
|
||||
}
|
||||
|
||||
export async function postSurveyStatus(): Promise<void> {
|
||||
await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: undefined })
|
||||
})
|
||||
try {
|
||||
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: undefined })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(
|
||||
`Failed to post survey status: ${response.statusText}`
|
||||
)
|
||||
captureApiError(
|
||||
error,
|
||||
'/settings/{key}',
|
||||
'http_error',
|
||||
response.status,
|
||||
'post_survey_status',
|
||||
{
|
||||
route_template: '/settings/{key}',
|
||||
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to post survey status:')) {
|
||||
captureApiError(
|
||||
error as Error,
|
||||
'/settings/{key}',
|
||||
'network_error',
|
||||
undefined,
|
||||
'post_survey_status',
|
||||
{
|
||||
route_template: '/settings/{key}',
|
||||
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
}
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitSurvey(
|
||||
survey: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
const response = await api.fetchApi('/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: survey })
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to submit survey: ${response.statusText}`)
|
||||
try {
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Submitting survey',
|
||||
level: 'info',
|
||||
data: {
|
||||
survey_fields: Object.keys(survey)
|
||||
}
|
||||
})
|
||||
|
||||
const response = await api.fetchApi('/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: survey })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(`Failed to submit survey: ${response.statusText}`)
|
||||
captureApiError(
|
||||
error,
|
||||
'/settings',
|
||||
'http_error',
|
||||
response.status,
|
||||
'submit_survey',
|
||||
{
|
||||
survey: {
|
||||
field_count: Object.keys(survey).length,
|
||||
field_names: Object.keys(survey)
|
||||
}
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Log successful survey submission
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Survey submitted successfully',
|
||||
level: 'info'
|
||||
})
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to submit survey:')) {
|
||||
captureApiError(
|
||||
error as Error,
|
||||
'/settings',
|
||||
'network_error',
|
||||
undefined,
|
||||
'submit_survey'
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function claimInvite(code: string): Promise<void> {
|
||||
const res = await api.fetchApi(`/invite/${encodeURIComponent(code)}/claim`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to claim invite: ${res.status} ${res.statusText}`)
|
||||
try {
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Attempting to claim invite',
|
||||
level: 'info',
|
||||
data: {
|
||||
code_length: code.length
|
||||
}
|
||||
})
|
||||
|
||||
const res = await api.fetchApi(
|
||||
`/invite/${encodeURIComponent(code)}/claim`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
const error = new Error(
|
||||
`Failed to claim invite: ${res.status} ${res.statusText}`
|
||||
)
|
||||
captureApiError(
|
||||
error,
|
||||
'/invite/{code}/claim',
|
||||
'http_error',
|
||||
res.status,
|
||||
'claim_invite',
|
||||
{
|
||||
invite: {
|
||||
code_length: code.length,
|
||||
status_code: res.status,
|
||||
status_text: res.statusText
|
||||
},
|
||||
route_template: '/invite/{code}/claim',
|
||||
route_actual: `/invite/${encodeURIComponent(code)}/claim`
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Log successful invite claim
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Invite claimed successfully',
|
||||
level: 'info'
|
||||
})
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to claim invite:')) {
|
||||
captureApiError(
|
||||
error as Error,
|
||||
'/invite/{code}/claim',
|
||||
'network_error',
|
||||
undefined,
|
||||
'claim_invite',
|
||||
{
|
||||
route_template: '/invite/{code}/claim',
|
||||
route_actual: `/invite/${encodeURIComponent(code)}/claim`
|
||||
}
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user