mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +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 { isEmpty } from 'es-toolkit/compat'
|
||||||
|
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
@@ -8,84 +9,352 @@ export interface UserCloudStatus {
|
|||||||
|
|
||||||
const ONBOARDING_SURVEY_KEY = 'onboarding_survey'
|
const ONBOARDING_SURVEY_KEY = 'onboarding_survey'
|
||||||
|
|
||||||
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
|
/**
|
||||||
const response = await api.fetchApi('/user', {
|
* Helper function to capture API errors with Sentry
|
||||||
method: 'GET',
|
*/
|
||||||
headers: {
|
function captureApiError(
|
||||||
'Content-Type': 'application/json'
|
error: Error,
|
||||||
}
|
endpoint: string,
|
||||||
})
|
errorType: 'http_error' | 'network_error',
|
||||||
if (!response.ok) {
|
httpStatus?: number,
|
||||||
throw new Error(`Failed to get user: ${response.statusText}`)
|
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(
|
export async function getInviteCodeStatus(
|
||||||
inviteCode: string
|
inviteCode: string
|
||||||
): Promise<{ expired: boolean }> {
|
): Promise<{ expired: boolean }> {
|
||||||
const response = await api.fetchApi(
|
try {
|
||||||
`/invite/${encodeURIComponent(inviteCode)}/status`,
|
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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'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
|
||||||
}
|
}
|
||||||
)
|
const data = await response.json()
|
||||||
if (!response.ok) {
|
// Check if data exists and is not empty
|
||||||
throw new Error(`Failed to get invite code status: ${response.statusText}`)
|
return !isEmpty(data.value)
|
||||||
}
|
} catch (error) {
|
||||||
|
// Network error - still capture it as it's not thrown from above
|
||||||
return response.json()
|
Sentry.captureException(error, {
|
||||||
}
|
tags: {
|
||||||
|
api_endpoint: '/settings/{key}',
|
||||||
export async function getSurveyCompletedStatus(): Promise<boolean> {
|
error_type: 'network_error'
|
||||||
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
},
|
||||||
method: 'GET',
|
extra: {
|
||||||
headers: {
|
route_template: '/settings/{key}',
|
||||||
'Content-Type': 'application/json'
|
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||||
}
|
},
|
||||||
})
|
level: 'warning'
|
||||||
if (!response.ok) {
|
})
|
||||||
return false
|
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> {
|
export async function postSurveyStatus(): Promise<void> {
|
||||||
await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
try {
|
||||||
method: 'POST',
|
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json'
|
headers: {
|
||||||
},
|
'Content-Type': 'application/json'
|
||||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: undefined })
|
},
|
||||||
})
|
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(
|
export async function submitSurvey(
|
||||||
survey: Record<string, unknown>
|
survey: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const response = await api.fetchApi('/settings', {
|
try {
|
||||||
method: 'POST',
|
Sentry.addBreadcrumb({
|
||||||
headers: {
|
category: 'auth',
|
||||||
'Content-Type': 'application/json'
|
message: 'Submitting survey',
|
||||||
},
|
level: 'info',
|
||||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: survey })
|
data: {
|
||||||
})
|
survey_fields: Object.keys(survey)
|
||||||
if (!response.ok) {
|
}
|
||||||
throw new Error(`Failed to submit survey: ${response.statusText}`)
|
})
|
||||||
|
|
||||||
|
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> {
|
export async function claimInvite(code: string): Promise<void> {
|
||||||
const res = await api.fetchApi(`/invite/${encodeURIComponent(code)}/claim`, {
|
try {
|
||||||
method: 'POST'
|
Sentry.addBreadcrumb({
|
||||||
})
|
category: 'auth',
|
||||||
if (!res.ok) {
|
message: 'Attempting to claim invite',
|
||||||
throw new Error(`Failed to claim invite: ${res.status} ${res.statusText}`)
|
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