Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/lib/server/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@ const handlers: Record<string, (args: any) => object> = {
// page returns nextCursor='page2', the second returns the rest with no
// further cursor. The 'sg1' location simulates a location with no log
// capture (error detection unavailable).
'deployment.errors': (args) => {
'error.list': (args) => {
if (args?.location === 'gke.cluster-sg1') {
return err('api: error detection is not available for this location')
}
Expand All @@ -1350,7 +1350,7 @@ const handlers: Record<string, (args: any) => object> = {
const location = String(args?.location ?? LOCATION_ID)

// Per-deployment fixture: every issue belongs to the queried deployment.
const single: Api.DeploymentErrorIssue[] = [
const single: Api.ErrorIssue[] = [
{
id: 'iss_go_nilmap',
deployment: deploymentName,
Expand Down Expand Up @@ -1434,7 +1434,7 @@ const handlers: Record<string, (args: any) => object> = {
// Project-wide fixture: issues span several deployments + locations and
// every kind/status, enough to page across two screens. Sorted lastSeen
// desc to match the page's default sort.
const wide: Api.DeploymentErrorIssue[] = [
const wide: Api.ErrorIssue[] = [
{
id: 'iss_api_go_nilmap',
deployment: 'api',
Expand Down Expand Up @@ -1542,7 +1542,7 @@ const handlers: Record<string, (args: any) => object> = {
]

const all = projectWide ? wide : single
const status = (args?.status as Api.DeploymentErrorStatusFilter | undefined) ?? 'open'
const status = (args?.status as Api.ErrorStatusFilter | undefined) ?? 'open'
const filtered = status === 'all' ? all : all.filter((it) => it.status === status)
// Two-page paging: first request (no cursor) returns the first 3, then
// nextCursor='page2' yields the remainder. Pages are only meaningful when
Expand All @@ -1553,7 +1553,7 @@ const handlers: Record<string, (args: any) => object> = {
}
return ok({ issues: filtered.slice(3), nextCursor: undefined })
},
'deployment.errorGet': (args) => {
'error.get': (args) => {
const base = Date.now()
const at = (mins: number) => new Date(base - mins * 60_000).toISOString()
const samples: Record<string, { title: string, sample: string }> = {
Expand Down Expand Up @@ -1607,7 +1607,7 @@ const handlers: Record<string, (args: any) => object> = {
}
const id = String(args?.id ?? 'iss_go_nilmap')
const s = samples[id] ?? samples.iss_go_nilmap
const issue: Api.DeploymentErrorIssueDetail = {
const issue: Api.ErrorIssueDetail = {
id,
deployment: String(args?.name ?? 'api'),
location: String(args?.location ?? LOCATION_ID),
Expand Down Expand Up @@ -1642,7 +1642,7 @@ const handlers: Record<string, (args: any) => object> = {
}
return ok({ issue })
},
'deployment.errorUpdate': () => ok({}),
'error.update': () => ok({}),

'disk.list': () => list(disks),
'disk.get': (args) => ok({ ...disks[0], name: args?.name ?? 'data', location: args?.location ?? LOCATION_ID }),
Expand Down
33 changes: 17 additions & 16 deletions src/routes/(auth)/(project)/deployment/(detail)/errors/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
const { data }: { data: PageData } = $props()
const deployment = $derived(data.deployment)

// Triage (resolve/mute/reopen) and read both ride the single deployment.logs
// permission for v1 (one surface; a dedicated deployment.errors perm may be
// split out later). Keep this in lockstep with the server-side gate.
const TRIAGE_PERMISSION = 'deployment.logs'
// Reading the issue list + detail is gated by error.list / error.get; triage
// (resolve/mute/reopen) by error.update. Keep these in lockstep with the
// server-side gate.
const READ_PERMISSION = 'error.list'
const TRIAGE_PERMISSION = 'error.update'

// The list endpoint reports this when the deployment's location has no log
// bucket (error detection permanently unavailable there); we match on the
// stable substring rather than an error code, mirroring the API contract.
const UNAVAILABLE_MARKER = 'error detection is not available for this location'

type StatusFilter = Api.DeploymentErrorStatusFilter
type StatusFilter = Api.ErrorStatusFilter

const STATUS_FILTERS: { value: StatusFilter, label: string }[] = [
{ value: 'open', label: 'Open' },
Expand All @@ -28,7 +29,7 @@

// Short, language-coloured kind badges. Hues are token-based so they recolour
// with the theme. Kept LOCAL to this page on purpose (see PR notes).
const KIND_META: Record<Api.DeploymentErrorKind, { label: string, hue: number }> = {
const KIND_META: Record<Api.ErrorKind, { label: string, hue: number }> = {
go: { label: 'Go', hue: 198 },
java: { label: 'Java', hue: 18 },
python: { label: 'Py', hue: 142 },
Expand All @@ -37,7 +38,7 @@
generic: { label: 'Generic', hue: 250 }
}

function kindMeta (kind: Api.DeploymentErrorKind) {
function kindMeta (kind: Api.ErrorKind) {
return KIND_META[kind] ?? { label: kind, hue: 250 }
}

Expand Down Expand Up @@ -65,7 +66,7 @@

let status = $state<StatusFilter>('open')
let query = $state('')
let issues = $state<Api.DeploymentErrorIssue[]>([])
let issues = $state<Api.ErrorIssue[]>([])
let nextCursor = $state<string | undefined>(undefined)
let loading = $state(false)
let loadingMore = $state(false)
Expand All @@ -79,7 +80,7 @@

// Expanded issue id → its loaded detail (or null while loading).
let expandedId = $state<string | null>(null)
let detail = $state<Api.DeploymentErrorIssueDetail | null>(null)
let detail = $state<Api.ErrorIssueDetail | null>(null)
let detailLoading = $state(false)
let detailError = $state('')
// id currently being mutated, so only its buttons show the loading state.
Expand Down Expand Up @@ -121,7 +122,7 @@
// new filter.
expandedId = null
detail = null
const resp = await api.invoke<Api.DeploymentErrorsResult>('deployment.errors', {
const resp = await api.invoke<Api.ErrorListResult>('error.list', {
project: deployment.project,
location: deployment.location,
name: deployment.name,
Expand All @@ -143,7 +144,7 @@
async function loadMore (): Promise<void> {
if (!nextCursor || loadingMore) return
loadingMore = true
const resp = await api.invoke<Api.DeploymentErrorsResult>('deployment.errors', {
const resp = await api.invoke<Api.ErrorListResult>('error.list', {
project: deployment.project,
location: deployment.location,
name: deployment.name,
Expand All @@ -158,7 +159,7 @@
loadingMore = false
}

async function toggleExpand (issue: Api.DeploymentErrorIssue): Promise<void> {
async function toggleExpand (issue: Api.ErrorIssue): Promise<void> {
if (expandedId === issue.id) {
expandedId = null
detail = null
Expand All @@ -169,7 +170,7 @@
detail = null
detailError = ''
detailLoading = true
const resp = await api.invoke<Api.DeploymentErrorGetResult>('deployment.errorGet', {
const resp = await api.invoke<Api.ErrorGetResult>('error.get', {
project: deployment.project,
location: deployment.location,
name: deployment.name,
Expand All @@ -186,9 +187,9 @@
detailLoading = false
}

async function updateStatus (issue: Api.DeploymentErrorIssue, next: Api.DeploymentErrorStatus): Promise<void> {
async function updateStatus (issue: Api.ErrorIssue, next: Api.ErrorStatus): Promise<void> {
updatingId = issue.id
const resp = await api.invoke('deployment.errorUpdate', {
const resp = await api.invoke('error.update', {
project: deployment.project,
location: deployment.location,
name: deployment.name,
Expand Down Expand Up @@ -661,7 +662,7 @@
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<div class="state-pane__title">You don't have access to errors</div>
<div class="state-pane__hint">Viewing application errors requires the <code>deployment.logs</code> permission.</div>
<div class="state-pane__hint">Viewing application errors requires the <code>{READ_PERMISSION}</code> permission.</div>
</div>
{:else if unavailable}
<div class="state-pane">
Expand Down
22 changes: 11 additions & 11 deletions src/routes/(auth)/(project)/errors/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
const { data }: { data: PageData } = $props()
const project = $derived(data.project)

// Reading errors is gated by the same deployment.logs permission as the
// per-deployment Errors tab (one surface; keep in lockstep with the server).
const READ_PERMISSION = 'deployment.logs'
// Reading errors is gated by the error.list permission, matching the
// per-deployment Errors tab (keep in lockstep with the server).
const READ_PERMISSION = 'error.list'

type StatusFilter = Api.DeploymentErrorStatusFilter
type StatusFilter = Api.ErrorStatusFilter

const STATUS_FILTERS: { value: StatusFilter, label: string }[] = [
{ value: 'open', label: 'Open' },
Expand All @@ -21,7 +21,7 @@

// Short, language-coloured kind badges. Hues are token-based so they recolour
// with the theme. Kept LOCAL, mirroring the per-deployment Errors tab.
const KIND_META: Record<Api.DeploymentErrorKind, { label: string, hue: number }> = {
const KIND_META: Record<Api.ErrorKind, { label: string, hue: number }> = {
go: { label: 'Go', hue: 198 },
java: { label: 'Java', hue: 18 },
python: { label: 'Py', hue: 142 },
Expand All @@ -30,7 +30,7 @@
generic: { label: 'Generic', hue: 250 }
}

function kindMeta (kind: Api.DeploymentErrorKind) {
function kindMeta (kind: Api.ErrorKind) {
return KIND_META[kind] ?? { label: kind, hue: 250 }
}

Expand Down Expand Up @@ -58,7 +58,7 @@

// Deep-link to this issue on the owning deployment's Errors tab, where the
// full detail + triage (resolve / mute / reopen) lives.
function issueHref (issue: Api.DeploymentErrorIssue): string {
function issueHref (issue: Api.ErrorIssue): string {
const q = new URLSearchParams({
project,
location: issue.location,
Expand All @@ -69,7 +69,7 @@

let status = $state<StatusFilter>('open')
let query = $state('')
let issues = $state<Api.DeploymentErrorIssue[]>([])
let issues = $state<Api.ErrorIssue[]>([])
let nextCursor = $state<string | undefined>(undefined)
let loading = $state(false)
let loadingMore = $state(false)
Expand Down Expand Up @@ -107,9 +107,9 @@
// forbidden response can recover.
forbidden = false
errorMessage = ''
// Project-wide listing: deployment.errors with no `name` aggregates issues
// Project-wide listing: error.list with no `name` aggregates issues
// across every deployment in the project.
const resp = await api.invoke<Api.DeploymentErrorsResult>('deployment.errors', {
const resp = await api.invoke<Api.ErrorListResult>('error.list', {
project,
status,
sort: 'lastSeen'
Expand All @@ -129,7 +129,7 @@
async function loadMore (): Promise<void> {
if (!nextCursor || loadingMore) return
loadingMore = true
const resp = await api.invoke<Api.DeploymentErrorsResult>('deployment.errors', {
const resp = await api.invoke<Api.ErrorListResult>('error.list', {
project,
status,
sort: 'lastSeen',
Expand Down
54 changes: 27 additions & 27 deletions src/types/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,26 +923,26 @@ declare namespace Api {
// Application-error detection (Sentry-lite). An "issue" is a group of
// identical application-level errors (panics, exceptions, stack traces the
// app prints) deduplicated by a stable server-side fingerprint.
export type DeploymentErrorKind = 'go' | 'java' | 'python' | 'node' | 'ruby' | 'generic'
export type DeploymentErrorStatus = 'open' | 'resolved' | 'muted'
export type ErrorKind = 'go' | 'java' | 'python' | 'node' | 'ruby' | 'generic'
export type ErrorStatus = 'open' | 'resolved' | 'muted'
// The status query also accepts 'all' (no filter); the default is 'open'.
export type DeploymentErrorStatusFilter = DeploymentErrorStatus | 'all'
export type DeploymentErrorSort = 'lastSeen' | 'firstSeen' | 'count'
export type ErrorStatusFilter = ErrorStatus | 'all'
export type ErrorSort = 'lastSeen' | 'firstSeen' | 'count'

// One grouped issue as returned by deployment.errors (the list view).
// One grouped issue as returned by error.list (the list view).
//
// `deployment` + `location` identify which deployment the issue belongs to.
// They are always present; on the per-deployment call they echo the queried
// deployment, and on the project-wide call (deployment.errors without a
// deployment, and on the project-wide call (error.list without a
// `name`) they are the column that distinguishes issues across deployments.
export type DeploymentErrorIssue = {
export type ErrorIssue = {
id: string
deployment: string
location: string
fingerprint: string
kind: DeploymentErrorKind
kind: ErrorKind
title: string
status: DeploymentErrorStatus
status: ErrorStatus
count: number
firstSeen: string
lastSeen: string
Expand All @@ -951,7 +951,7 @@ declare namespace Api {

// A lightweight pointer to one occurrence of an issue (recent_events). The
// full sample stack lives once on the issue; these only carry where/when.
export type DeploymentErrorOccurrence = {
export type ErrorOccurrence = {
pod: string
timestamp: string
// object + offset locate the occurrence in the durable _errorlog stream
Expand All @@ -962,56 +962,56 @@ declare namespace Api {

// The detail view: all list fields plus the representative sample stack and
// the recent occurrence pointers.
export type DeploymentErrorIssueDetail = DeploymentErrorIssue & {
export type ErrorIssueDetail = ErrorIssue & {
sampleMessage: string
recentEvents: DeploymentErrorOccurrence[]
recentEvents: ErrorOccurrence[]
}

// Args of deployment.errors (list). status defaults to 'open', sort to
// Args of error.list. status defaults to 'open', sort to
// 'lastSeen'; cursor is the opaque page token from a prior nextCursor.
//
// `location` + `name` are optional: supplying both scopes the listing to a
// single deployment, while OMITTING `name` lists error issues across every
// deployment in the project (the project-wide Errors view). Each returned
// issue carries its own `deployment` + `location` regardless.
export type DeploymentErrorsArgs = {
export type ErrorListArgs = {
project: string
location?: string
name?: string
status?: DeploymentErrorStatusFilter
status?: ErrorStatusFilter
limit?: number
cursor?: string
sort?: DeploymentErrorSort
sort?: ErrorSort
}

// Result of deployment.errors. nextCursor is non-empty while more issues
// Result of error.list. nextCursor is non-empty while more issues
// remain for the current filter/sort.
export type DeploymentErrorsResult = {
issues: DeploymentErrorIssue[]
export type ErrorListResult = {
issues: ErrorIssue[]
nextCursor?: string
}

// Args of deployment.errorGet (detail).
export type DeploymentErrorGetArgs = {
// Args of error.get (detail).
export type ErrorGetArgs = {
project: string
location: string
name: string
id: string
}

// Result of deployment.errorGet.
export type DeploymentErrorGetResult = {
issue: DeploymentErrorIssueDetail
// Result of error.get.
export type ErrorGetResult = {
issue: ErrorIssueDetail
}

// Args of deployment.errorUpdate (triage). status flips the lifecycle:
// Args of error.update (triage). status flips the lifecycle:
// resolved / open (reopen) / muted.
export type DeploymentErrorUpdateArgs = {
export type ErrorUpdateArgs = {
project: string
location: string
name: string
id: string
status: DeploymentErrorStatus
status: ErrorStatus
}

export type BillingReportProject = {
Expand Down
Loading