diff --git a/backend/internal/handler/admin/ops_handler.go b/backend/internal/handler/admin/ops_handler.go index c76b6a60..822f7d62 100644 --- a/backend/internal/handler/admin/ops_handler.go +++ b/backend/internal/handler/admin/ops_handler.go @@ -19,6 +19,34 @@ type OpsHandler struct { opsService *service.OpsService } +// GetErrorLogByID returns ops error log detail. +// GET /api/v1/admin/ops/errors/:id +func (h *OpsHandler) GetErrorLogByID(c *gin.Context) { + if h.opsService == nil { + response.Error(c, http.StatusServiceUnavailable, "Ops service not available") + return + } + if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { + response.ErrorFrom(c, err) + return + } + + idStr := strings.TrimSpace(c.Param("id")) + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + response.BadRequest(c, "Invalid error id") + return + } + + detail, err := h.opsService.GetErrorLogByID(c.Request.Context(), id) + if err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, detail) +} + const ( opsListViewErrors = "errors" opsListViewExcluded = "excluded" @@ -70,16 +98,25 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { return } - filter := &service.OpsErrorLogFilter{ - Page: page, - PageSize: pageSize, - } + filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize} + if !startTime.IsZero() { filter.StartTime = &startTime } if !endTime.IsZero() { filter.EndTime = &endTime } + filter.View = parseOpsViewParam(c) + filter.Phase = strings.TrimSpace(c.Query("phase")) + filter.Owner = strings.TrimSpace(c.Query("error_owner")) + filter.Source = strings.TrimSpace(c.Query("error_source")) + filter.Query = strings.TrimSpace(c.Query("q")) + + // Force request errors: client-visible status >= 400. + // buildOpsErrorLogsWhere already applies this for non-upstream phase. + if strings.EqualFold(strings.TrimSpace(filter.Phase), "upstream") { + filter.Phase = "" + } if platform := strings.TrimSpace(c.Query("platform")); platform != "" { filter.Platform = platform @@ -100,22 +137,7 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { } filter.AccountID = &id } - if phase := strings.TrimSpace(c.Query("phase")); phase != "" { - filter.Phase = phase - } - if owner := strings.TrimSpace(c.Query("error_owner")); owner != "" { - filter.Owner = owner - } - if source := strings.TrimSpace(c.Query("error_source")); source != "" { - filter.Source = source - } - filter.View = parseOpsViewParam(c) - // Legacy endpoint default: unresolved only (backward-compatible). - { - b := false - filter.Resolved = &b - } if v := strings.TrimSpace(c.Query("resolved")); v != "" { switch strings.ToLower(v) { case "1", "true", "yes": @@ -129,9 +151,6 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { return } } - if q := strings.TrimSpace(c.Query("q")); q != "" { - filter.Query = q - } if statusCodesStr := strings.TrimSpace(c.Query("status_codes")); statusCodesStr != "" { parts := strings.Split(statusCodesStr, ",") out := make([]int, 0, len(parts)) @@ -149,57 +168,15 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { } filter.StatusCodes = out } - if v := strings.TrimSpace(c.Query("status_codes_other")); v != "" { - switch strings.ToLower(v) { - case "1", "true", "yes": - filter.StatusCodesOther = true - case "0", "false", "no": - filter.StatusCodesOther = false - default: - response.BadRequest(c, "Invalid status_codes_other") - return - } - } result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) if err != nil { response.ErrorFrom(c, err) return } - response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize) } -// GetErrorLogByID returns a single error log detail. -// GET /api/v1/admin/ops/errors/:id -func (h *OpsHandler) GetErrorLogByID(c *gin.Context) { - if h.opsService == nil { - response.Error(c, http.StatusServiceUnavailable, "Ops service not available") - return - } - if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { - response.ErrorFrom(c, err) - return - } - - idStr := strings.TrimSpace(c.Param("id")) - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil || id <= 0 { - response.BadRequest(c, "Invalid error id") - return - } - - detail, err := h.opsService.GetErrorLogByID(c.Request.Context(), id) - if err != nil { - response.ErrorFrom(c, err) - return - } - - response.Success(c, detail) -} - -// ==================== New split endpoints ==================== - // ListRequestErrors lists client-visible request errors. // GET /api/v1/admin/ops/request-errors func (h *OpsHandler) ListRequestErrors(c *gin.Context) { @@ -307,6 +284,104 @@ func (h *OpsHandler) GetRequestError(c *gin.Context) { h.GetErrorLogByID(c) } +// ListRequestErrorUpstreamErrors lists upstream error logs correlated to a request error. +// GET /api/v1/admin/ops/request-errors/:id/upstream-errors +func (h *OpsHandler) ListRequestErrorUpstreamErrors(c *gin.Context) { + if h.opsService == nil { + response.Error(c, http.StatusServiceUnavailable, "Ops service not available") + return + } + if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { + response.ErrorFrom(c, err) + return + } + + idStr := strings.TrimSpace(c.Param("id")) + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + response.BadRequest(c, "Invalid error id") + return + } + + // Load request error to get correlation keys. + detail, err := h.opsService.GetErrorLogByID(c.Request.Context(), id) + if err != nil { + response.ErrorFrom(c, err) + return + } + + // Correlate by request_id/client_request_id. + requestID := strings.TrimSpace(detail.RequestID) + clientRequestID := strings.TrimSpace(detail.ClientRequestID) + if requestID == "" && clientRequestID == "" { + response.Paginated(c, []*service.OpsErrorLog{}, 0, 1, 10) + return + } + + page, pageSize := response.ParsePagination(c) + if pageSize > 500 { + pageSize = 500 + } + + // Keep correlation window wide enough so linked upstream errors + // are discoverable even when UI defaults to 1h elsewhere. + startTime, endTime, err := parseOpsTimeRange(c, "30d") + if err != nil { + response.BadRequest(c, err.Error()) + return + } + + filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize} + if !startTime.IsZero() { + filter.StartTime = &startTime + } + if !endTime.IsZero() { + filter.EndTime = &endTime + } + filter.View = "all" + filter.Phase = "upstream" + filter.Owner = "provider" + filter.Source = strings.TrimSpace(c.Query("error_source")) + filter.Query = strings.TrimSpace(c.Query("q")) + + if platform := strings.TrimSpace(c.Query("platform")); platform != "" { + filter.Platform = platform + } + + // Prefer exact match on request_id; if missing, fall back to client_request_id. + if requestID != "" { + filter.RequestID = requestID + } else { + filter.ClientRequestID = clientRequestID + } + + result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) + if err != nil { + response.ErrorFrom(c, err) + return + } + + // If client asks for details, expand each upstream error log to include upstream response fields. + includeDetail := strings.TrimSpace(c.Query("include_detail")) + if includeDetail == "1" || strings.EqualFold(includeDetail, "true") || strings.EqualFold(includeDetail, "yes") { + details := make([]*service.OpsErrorLogDetail, 0, len(result.Errors)) + for _, item := range result.Errors { + if item == nil { + continue + } + d, err := h.opsService.GetErrorLogByID(c.Request.Context(), item.ID) + if err != nil || d == nil { + continue + } + details = append(details, d) + } + response.Paginated(c, details, int64(result.Total), result.Page, result.PageSize) + return + } + + response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize) +} + // RetryRequestErrorClient retries the client request based on stored request body. // POST /api/v1/admin/ops/request-errors/:id/retry-client func (h *OpsHandler) RetryRequestErrorClient(c *gin.Context) { diff --git a/backend/internal/repository/ops_repo.go b/backend/internal/repository/ops_repo.go index 0535547d..ef03dc8e 100644 --- a/backend/internal/repository/ops_repo.go +++ b/backend/internal/repository/ops_repo.go @@ -1008,6 +1008,16 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) { args = append(args, pq.Array(known)) clauses = append(clauses, "NOT (COALESCE(upstream_status_code, status_code, 0) = ANY($"+itoa(len(args))+"))") } + // Exact correlation keys (preferred for request↔upstream linkage). + if rid := strings.TrimSpace(filter.RequestID); rid != "" { + args = append(args, rid) + clauses = append(clauses, "COALESCE(request_id,'') = $"+itoa(len(args))) + } + if crid := strings.TrimSpace(filter.ClientRequestID); crid != "" { + args = append(args, crid) + clauses = append(clauses, "COALESCE(client_request_id,'') = $"+itoa(len(args))) + } + if q := strings.TrimSpace(filter.Query); q != "" { like := "%" + q + "%" args = append(args, like) diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 53702766..ae4a6681 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -123,6 +123,7 @@ func registerOpsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { // Request errors (client-visible failures) ops.GET("/request-errors", h.Admin.Ops.ListRequestErrors) ops.GET("/request-errors/:id", h.Admin.Ops.GetRequestError) + ops.GET("/request-errors/:id/upstream-errors", h.Admin.Ops.ListRequestErrorUpstreamErrors) ops.POST("/request-errors/:id/retry-client", h.Admin.Ops.RetryRequestErrorClient) ops.POST("/request-errors/:id/upstream-errors/:idx/retry", h.Admin.Ops.RetryRequestErrorUpstreamEvent) ops.PUT("/request-errors/:id/resolve", h.Admin.Ops.ResolveRequestError) diff --git a/backend/internal/service/ops_models.go b/backend/internal/service/ops_models.go index ebdf148f..c7666e92 100644 --- a/backend/internal/service/ops_models.go +++ b/backend/internal/service/ops_models.go @@ -94,6 +94,10 @@ type OpsErrorLogFilter struct { Resolved *bool Query string + // Optional correlation keys for exact matching. + RequestID string + ClientRequestID string + // View controls error categorization for list endpoints. // - errors: show actionable errors (exclude business-limited / 429 / 529) // - excluded: only show excluded errors diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index 0ac54db6..83258a08 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -1037,6 +1037,17 @@ export async function updateUpstreamErrorResolved(errorId: number, resolved: boo await apiClient.put(`/admin/ops/upstream-errors/${errorId}/resolve`, { resolved }) } +export async function listRequestErrorUpstreamErrors( + id: number, + params: OpsErrorListQueryParams = {}, + options: { include_detail?: boolean } = {} +): Promise> { + const query: Record = { ...params } + if (options.include_detail) query.include_detail = '1' + const { data } = await apiClient.get>(`/admin/ops/request-errors/${id}/upstream-errors`, { params: query }) + return data +} + export async function listRequestDetails(params: OpsRequestDetailsParams): Promise { const { data } = await apiClient.get('/admin/ops/requests', { params }) return data @@ -1173,6 +1184,7 @@ export const opsAPI = { retryUpstreamError, updateRequestErrorResolved, updateUpstreamErrorResolved, + listRequestErrorUpstreamErrors, listRequestDetails, listAlertRules, diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 06fb95e9..e06b658a 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -129,6 +129,8 @@ export default { all: 'All', none: 'None', noData: 'No data', + expand: 'Expand', + collapse: 'Collapse', success: 'Success', error: 'Error', critical: 'Critical', @@ -2094,6 +2096,10 @@ export default { status: 'Status', requestId: 'Request ID' }, + responsePreview: { + expand: 'Response (click to expand)', + collapse: 'Response (click to collapse)' + }, retryMeta: { used: 'Used', success: 'Success', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 71864498..d445058d 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -126,6 +126,8 @@ export default { all: '全部', none: '无', noData: '暂无数据', + expand: '展开', + collapse: '收起', success: '成功', error: '错误', critical: '严重', @@ -2238,6 +2240,10 @@ export default { status: '状态码', requestId: '请求ID' }, + responsePreview: { + expand: '响应内容(点击展开)', + collapse: '响应内容(点击收起)' + }, retryMeta: { used: '使用账号', success: '成功', diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index f6f18f3d..58f7fa23 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -400,11 +400,17 @@ function handleOpenRequestDetails(preset?: OpsRequestDetailsPreset) { requestDetailsPreset.value = { ...basePreset, ...(preset ?? {}) } if (!requestDetailsPreset.value.title) requestDetailsPreset.value.title = basePreset.title + // Ensure only one modal visible at a time. + showErrorDetails.value = false + showErrorModal.value = false showRequestDetails.value = true } function openErrorDetails(kind: 'request' | 'upstream') { errorDetailsType.value = kind + // Ensure only one modal visible at a time. + showRequestDetails.value = false + showErrorModal.value = false showErrorDetails.value = true } @@ -446,6 +452,9 @@ function onQueryModeChange(v: string | number | boolean | null) { function openError(id: number) { selectedErrorId.value = id + // Ensure only one modal visible at a time. + showErrorDetails.value = false + showRequestDetails.value = false showErrorModal.value = true } diff --git a/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue b/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue index d2f185d5..9490790e 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue @@ -12,526 +12,154 @@
- -
-
- {{ t('admin.ops.errorDetail.resolution') }} - - {{ (detail as any).resolved ? t('admin.ops.errorDetails.resolved') : t('admin.ops.errorDetails.unresolved') }} - -
-
- - -
-
- - -
- - - - - -
- - -
-
-
-
{{ t('admin.ops.errorDetail.requestId') }}
-
- {{ detail.request_id || detail.client_request_id || '—' }} -
-
- -
-
{{ t('admin.ops.errorDetail.time') }}
-
- {{ formatDateTime(detail.created_at) }} -
-
- -
-
{{ t('admin.ops.errorDetail.phase') }}
-
- {{ detail.phase || '—' }} -
-
- {{ detail.type || '—' }} -
-
- -
-
{{ t('admin.ops.errorDetail.status') }}
-
- - {{ detail.status_code }} - - - {{ detail.severity }} - -
+ +
+
+
{{ t('admin.ops.errorDetail.requestId') }}
+
+ {{ requestId || '—' }}
- -
-
-

- {{ t('admin.ops.errorDetail.message') }} -

- -
- - -
+
+
{{ t('admin.ops.errorDetail.time') }}
+
+ {{ formatDateTime(detail.created_at) }}
+
-
+
+
{{ t('admin.ops.errorDetail.account') }}
+
+ {{ detail.account_name || (detail.account_id != null ? String(detail.account_id) : '—') }} +
+
+ +
+
{{ t('admin.ops.errorDetail.platform') }}
+
+ {{ detail.platform || '—' }} +
+
+ +
+
{{ t('admin.ops.errorDetail.group') }}
+
+ {{ detail.group_name || (detail.group_id != null ? String(detail.group_id) : '—') }} +
+
+ +
+
{{ t('admin.ops.errorDetail.model') }}
+
+ {{ detail.model || '—' }} +
+
+ +
+
{{ t('admin.ops.errorDetail.status') }}
+
+ + {{ detail.status_code }} + +
+
+ +
+
{{ t('admin.ops.errorDetail.message') }}
+
{{ detail.message || '—' }}
- - -
-

- {{ t('admin.ops.errorDetail.classification') }} -

-
- - {{ t('admin.ops.errorDetail.classificationKeys.phase') }}: - {{ phaseLabel }} - - - {{ t('admin.ops.errorDetail.classificationKeys.owner') }}: - {{ ownerLabel }} - - - {{ t('admin.ops.errorDetail.classificationKeys.source') }}: - {{ sourceLabel }} - - - {{ t('admin.ops.errorDetail.classificationKeys.retryable') }}: - {{ (detail as any).is_retryable ? t('common.yes') : t('common.no') }} - - - {{ t('admin.ops.errorDetail.classificationKeys.resolvedAt') }}: {{ (detail as any).resolved_at }} - - - {{ t('admin.ops.errorDetail.classificationKeys.resolvedBy') }}: - {{ (detail as any).resolved_by_user_id }} - - - {{ t('admin.ops.errorDetail.classificationKeys.resolvedRetryId') }}: - {{ (detail as any).resolved_retry_id }} - - - {{ t('admin.ops.errorDetail.classificationKeys.retryCount') }}: {{ (detail as any).retry_count }} - -
-
- - -
-

{{ t('admin.ops.errorDetail.basicInfo') }}

-
-
-
{{ t('admin.ops.errorDetail.platform') }}
-
{{ detail.platform || '—' }}
-
-
-
{{ t('admin.ops.errorDetail.model') }}
-
{{ detail.model || '—' }}
-
-
-
{{ t('admin.ops.errorDetail.group') }}
-
- - {{ detail.group_name || detail.group_id }} - - -
-
-
-
{{ t('admin.ops.errorDetail.account') }}
-
- - {{ detail.account_name || detail.account_id }} - - -
-
-
-
TTFT
-
- {{ detail.time_to_first_token_ms != null ? `${detail.time_to_first_token_ms}ms` : '—' }} -
-
-
-
{{ t('admin.ops.errorDetail.businessLimited') }}
-
- {{ detail.is_business_limited ? 'true' : 'false' }} -
-
-
-
{{ t('admin.ops.errorDetail.requestPath') }}
-
- {{ detail.request_path || '—' }} -
-
-
-
- - -
-

{{ t('admin.ops.errorDetail.timings') }}

-
-
-
{{ t('admin.ops.errorDetail.auth') }}
-
- {{ detail.auth_latency_ms != null ? `${detail.auth_latency_ms}ms` : '—' }} -
-
-
-
{{ t('admin.ops.errorDetail.routing') }}
-
- {{ detail.routing_latency_ms != null ? `${detail.routing_latency_ms}ms` : '—' }} -
-
-
-
{{ t('admin.ops.errorDetail.upstream') }}
-
- {{ detail.upstream_latency_ms != null ? `${detail.upstream_latency_ms}ms` : '—' }} -
-
-
-
{{ t('admin.ops.errorDetail.response') }}
-
- {{ detail.response_latency_ms != null ? `${detail.response_latency_ms}ms` : '—' }} -
-
-
-
- -
- - -
{{ t('admin.ops.errorDetail.pinnedToOriginalAccountId') }}
-
- -
-
-

- {{ t('admin.ops.errorDetails.upstreamErrors') }} -

- -
-
-
{{ t('admin.ops.errorDetail.upstreamKeys.status') }}
-
- {{ detail.upstream_status_code != null ? detail.upstream_status_code : '—' }} -
-
-
-
{{ t('admin.ops.errorDetail.upstreamKeys.message') }}
-
- {{ detail.upstream_error_message || '—' }} -
-
-
- -
-
{{ t('admin.ops.errorDetail.upstreamKeys.detail') }}
-
{{ prettyJSON(detail.upstream_error_detail) }}
-
- -
-
{{ t('admin.ops.errorDetail.upstreamKeys.upstreamErrors') }}
- -
-
-
-
#{{ idx + 1 }} {{ ev.kind }}
-
- -
- {{ ev.at_unix_ms ? formatDateTime(new Date(ev.at_unix_ms)) : '' }} -
-
-
- -
-
- {{ t('admin.ops.errorDetail.upstreamEvent.account') }}: - - {{ ev.account_name || ev.account_id }} - - -
-
- {{ t('admin.ops.errorDetail.upstreamEvent.status') }}: - {{ ev.upstream_status_code ?? '—' }} -
-
- {{ t('admin.ops.errorDetail.upstreamEvent.requestId') }}: - {{ ev.upstream_request_id || '—' }} -
-
- -
- {{ ev.message }} -
- -
{{ prettyJSON(ev.detail) }}
-
-
- -
{{ prettyJSON(detail.upstream_errors) }}
-
-
+ +
+

{{ t('admin.ops.errorDetail.responseBody') }}

+
{{ prettyJSON(primaryResponseBody || '') }}
- -
+ +
-
{{ t('admin.ops.errorDetail.retryHistory') }}
-
- -
+

{{ t('admin.ops.errorDetails.upstreamErrors') }}

+
{{ t('common.loading') }}
-
-
{{ t('common.loading') }}
-
{{ t('common.noData') }}
-
-
-
-
{{ t('admin.ops.errorDetail.compareA') }}
- -
-
-
{{ t('admin.ops.errorDetail.compareB') }}
- -
-
+
+ {{ t('common.noData') }} +
-
-
-
{{ selectedA ? `#${selectedA.id} · ${selectedA.mode} · ${selectedA.status}` : '—' }}
-
- HTTP: {{ selectedA?.http_status_code ?? '—' }} · {{ t('admin.ops.errorDetail.retryMeta.used') }}: - - - {{ selectedA.used_account_name || selectedA.used_account_id }} - - +
+
+
+
+ #{{ idx + 1 }} + {{ ev.type }} +
+
+
+ {{ ev.status_code ?? '—' }} +
+
-
{{ selectedA?.response_preview || '' }}
-
{{ selectedA.error_message }}
-
- -
-
{{ selectedB ? `#${selectedB.id} · ${selectedB.mode} · ${selectedB.status}` : '—' }}
-
- HTTP: {{ selectedB?.http_status_code ?? '—' }} · {{ t('admin.ops.errorDetail.retryMeta.used') }}: - - - {{ selectedB.used_account_name || selectedB.used_account_id }} - - - -
-
{{ selectedB?.response_preview || '' }}
-
{{ selectedB.error_message }}
+
-
-
-
-
#{{ a.id }} · {{ a.mode }} · {{ a.status }}
-
{{ a.created_at }}
-
-
-
- {{ t('admin.ops.errorDetail.retryMeta.success') }}: {{ a.success ?? '—' }} -
-
HTTP: {{ a.http_status_code ?? '—' }}
-
- {{ t('admin.ops.errorDetail.retryMeta.pinned') }}: - - {{ a.pinned_account_name || a.pinned_account_id }} - - -
-
- {{ t('admin.ops.errorDetail.retryMeta.used') }}: - - {{ a.used_account_name || a.used_account_id }} - - -
-
-
{{ a.response_preview }}
-
{{ a.error_message }}
+
+
+ {{ t('admin.ops.errorDetail.upstreamEvent.status') }}: + {{ ev.status_code ?? '—' }} +
+
+ {{ t('admin.ops.errorDetail.upstreamEvent.requestId') }}: + {{ ev.request_id || ev.client_request_id || '—' }}
-
-
-
- -
-
-

{{ t('admin.ops.errorDetail.requestBody') }}

-
{{ prettyJSON(detail.request_body || '') }}
-
-
+
{{ ev.message }}
- -
-
-

{{ t('admin.ops.errorDetail.responseBody') }}

-
- {{ responseTabHint }} +
{{ prettyJSON(getUpstreamResponsePreview(ev)) }}
-
{{ prettyJSON(responseTabBody || '') }}
- - - -
-
- -
-
diff --git a/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue b/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue index acf9254a..e9e44010 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue @@ -1,7 +1,6 @@