mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-15 00:37:33 +01:00
206 lines
15 KiB
HTML
206 lines
15 KiB
HTML
<div class="container paper-page admin-page errors-page">
|
||
<div class="paper-crumbs">Admin / <span class="here">Errors</span></div>
|
||
|
||
<header class="errors-header">
|
||
<h1 class="paper-page-title">Errors</h1>
|
||
<div class="errors-actions">
|
||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export CSV</button>
|
||
<button class="btn btn-sm btn-outline-danger" type="button" @click="clearAll()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Clear all</button>
|
||
</div>
|
||
</header>
|
||
|
||
<nav class="admin-nav">
|
||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||
<a href="/admin/billing"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-card"></use></svg> Billing</a>
|
||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||
<a href="/admin/emails"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Emails</a>
|
||
<a href="/admin/errors" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||
</nav>
|
||
|
||
<section class="kpi-grid">
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Occurrences · 24h</div>
|
||
<div class="kpi-value">{{ fmt?.number(stats?.last24h ?? 0) }}</div>
|
||
<div class="kpi-sub" :class="{up: stats?.delta > 0, down: stats?.delta < 0}">
|
||
<span v-if="stats?.prev24h >= 20">{{ (stats?.last24h ?? 0) - (stats?.prev24h ?? 0) >= 0 ? '+' : '' }}{{ Math.round(((stats?.last24h ?? 0) - (stats?.prev24h ?? 0)) / stats.prev24h * 100) }}% vs yesterday</span><span v-else-if="stats?.prev24h !== undefined">{{ fmt?.number(stats?.prev24h ?? 0) }} in prior 24h</span>
|
||
<span v-if="!stats?.prev24h">no prior baseline</span>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card kpi-error kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'error' }" :aria-pressed="query?.bucket === 'error'" :title="'Show only errors (5xx)'" @click="setBucket(query?.bucket === 'error' ? '' : 'error')" @keydown.enter.prevent="setBucket(query?.bucket === 'error' ? '' : 'error')">
|
||
<div class="kpi-label">Error</div>
|
||
<div class="kpi-value">{{ fmt?.number(stats?.severity?.error ?? 0) }}</div>
|
||
<div class="kpi-sub">5xx · {{ stats?.unique?.error }} unique</div>
|
||
</div>
|
||
<div class="kpi-card kpi-warn kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'warn' }" :aria-pressed="query?.bucket === 'warn'" :title="'Show only warnings (4xx, aborted requests)'" @click="setBucket(query?.bucket === 'warn' ? '' : 'warn')" @keydown.enter.prevent="setBucket(query?.bucket === 'warn' ? '' : 'warn')">
|
||
<div class="kpi-label">Warning</div>
|
||
<div class="kpi-value">{{ fmt?.number(stats?.severity?.warn ?? 0) }}</div>
|
||
<div class="kpi-sub">4xx, aborted · {{ stats?.unique?.warn }} unique</div>
|
||
</div>
|
||
<div class="kpi-card kpi-info kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'info' }" :aria-pressed="query?.bucket === 'info'" :title="'Show only info events'" @click="setBucket(query?.bucket === 'info' ? '' : 'info')" @keydown.enter.prevent="setBucket(query?.bucket === 'info' ? '' : 'info')">
|
||
<div class="kpi-label">Info</div>
|
||
<div class="kpi-value">{{ fmt?.number(stats?.severity?.info ?? 0) }}</div>
|
||
<div class="kpi-sub">Auth, 404 · {{ stats?.unique?.info }} unique</div>
|
||
</div>
|
||
<div class="kpi-card" :class="{'kpi-error': stats?.dropped > 0}">
|
||
<div class="kpi-label">Distinct errors stored</div>
|
||
<div class="kpi-value">{{ fmt?.number(total) }}</div>
|
||
<div class="kpi-sub">
|
||
all time · cap {{ fmt?.number(cap) }} · {{ available ? 'live' : 'redis off' }}
|
||
<span class="dropped-warn" v-if="stats?.dropped > 0"> · {{ stats?.dropped }} dropped</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="volume-chart">
|
||
<div class="volume-head">
|
||
<span class="volume-title">Volume · 24h · 1h buckets</span>
|
||
<span class="volume-legend">
|
||
<span v-if="stats?.severity?.error"><span class="dot dot-error"></span>Error</span>
|
||
<span v-if="stats?.severity?.warn"><span class="dot dot-warn"></span>Warning</span>
|
||
<span v-if="stats?.severity?.info"><span class="dot dot-info"></span>Info</span>
|
||
</span>
|
||
</div>
|
||
<div class="volume-bars">
|
||
<div class="volume-bar" v-for="(b, index) in stats?.buckets" :key="index" :title="(bucketTitle(b))">
|
||
<span class="seg seg-error" :style="{height: barPx(b, 'error') + 'px'}"></span>
|
||
<span class="seg seg-warn" :style="{height: barPx(b, 'warn') + 'px'}"></span>
|
||
<span class="seg seg-info" :style="{height: barPx(b, 'info') + 'px'}"></span>
|
||
</div>
|
||
</div>
|
||
<div class="volume-axis" aria-hidden="true"><span>24h ago</span><span>12h ago</span><span>now</span></div>
|
||
</section>
|
||
|
||
<form class="errors-toolbar" aria-label="Error filters" v-form="viewState">
|
||
<div class="seg-filter" role="radiogroup" aria-label="Filter by severity">
|
||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === '' }" :aria-checked="(query?.bucket || '') === ''" @click="setBucket('')">All <span class="count">{{ fmt?.number((stats?.severity?.error ?? 0) + (stats?.severity?.warn ?? 0) + (stats?.severity?.info ?? 0)) }}</span></button>
|
||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === 'error', 'is-empty': !(stats?.severity?.error) }" :aria-checked="(query?.bucket || '') === 'error'" @click="setBucket('error')"><span class="seg-dot bad" aria-hidden="true"></span>Error <span class="count">{{ fmt?.number(stats?.severity?.error ?? 0) }}</span></button>
|
||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === 'warn', 'is-empty': !(stats?.severity?.warn) }" :aria-checked="(query?.bucket || '') === 'warn'" @click="setBucket('warn')"><span class="seg-dot warn" aria-hidden="true"></span>Warning <span class="count">{{ fmt?.number(stats?.severity?.warn ?? 0) }}</span></button>
|
||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === 'info', 'is-empty': !(stats?.severity?.info) }" :aria-checked="(query?.bucket || '') === 'info'" @click="setBucket('info')"><span class="seg-dot off" aria-hidden="true"></span>Info <span class="count">{{ fmt?.number(stats?.severity?.info ?? 0) }}</span></button>
|
||
</div>
|
||
<div class="search-wrap">
|
||
<svg class="ui-icon search-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>
|
||
<input type="search" class="form-control" placeholder="Search errors…" title="Search messages, or filter with code:repo_not_found module:route status:>=400" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
|
||
</div>
|
||
<div class="select-wrap">
|
||
<label>Sort</label>
|
||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.sort = value }, value: query?.sort, form: null, options: {} }">
|
||
<option value="recent">Most recent</option>
|
||
<option value="count">Most frequent</option>
|
||
</select>
|
||
</div>
|
||
<div class="select-wrap">
|
||
<label title="Rows are always sectioned by severity. This merges repeated events inside each section.">Merge</label>
|
||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.group = value }, value: query?.group, form: null, options: {} }">
|
||
<option value="">Don't merge</option>
|
||
<option value="code">Same error</option>
|
||
<option value="module">Same module</option>
|
||
</select>
|
||
</div>
|
||
<label class="autoref">
|
||
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
||
<span class="auto-refresh-label">Auto-refresh</span>
|
||
</label>
|
||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||
</form>
|
||
|
||
<div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
|
||
<div class="errors-pager" v-if="canLoadMore() && visible?.length">
|
||
<span>{{ query?.group && visible?.length !== entries?.length ? fmt?.number(visible?.length) + (visible?.length === 1 ? ' group' : ' groups') + ' from ' : '' }}{{ fmt?.number(entries?.length) }} of {{ fmt?.number(total) }} stored errors loaded</span>
|
||
<button class="btn btn-sm" type="button" @click="loadMore()">Load 250 older</button>
|
||
</div>
|
||
|
||
<div class="errors-list" v-if="visible?.length">
|
||
<div class="errors-list-head">
|
||
<span class="col-when">When</span>
|
||
<span class="col-sev">Severity</span>
|
||
<span class="col-mod">Module</span>
|
||
<span class="col-msg">Message</span>
|
||
<span class="col-count" title="Total occurrences since first seen">Total</span>
|
||
<span class="col-status">Status</span>
|
||
</div>
|
||
|
||
<template v-for="(row, index) in visible" :key="row?._key">
|
||
<div class="errors-group-head" v-if="index === 0 || visible[index - 1]?._bucket !== row?._bucket"><span class="sev-dot" :class="'sev-' + row?._bucket"></span>{{ row?._bucket === 'error' ? 'Error' : row?._bucket === 'warn' ? 'Warning' : 'Info' }}<span class="errors-group-count">{{ fmt?.number(visible.filter(r => r?._bucket === row?._bucket).length) }}</span><span class="errors-group-note" v-if="index === 0">By severity · {{ query?.group === 'code' ? 'repeats merged by error · ' : query?.group === 'module' ? 'repeats merged by module · ' : '' }}{{ query?.sort === 'count' ? 'most frequent first' : 'most recent first' }}</span></div>
|
||
<div class="errors-row" :class="{open: expanded[row?._key]}">
|
||
<div class="errors-row-main" @click="toggle(row)">
|
||
<div class="col-when">
|
||
<div class="when-rel">{{ relTime(row?.ts) }}</div>
|
||
<div class="when-abs">{{ absTimeShort(row?.ts) }}</div>
|
||
</div>
|
||
<div class="col-sev">
|
||
<span class="sev-dot" :class="'sev-' + row?._bucket"></span>
|
||
<span class="sev-label">{{ row?._bucket === 'error' ? 'Error' : row?._bucket === 'warn' ? 'Warning' : 'Info' }}</span>
|
||
</div>
|
||
<div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
|
||
<div class="col-msg">
|
||
<strong class="msg-code">{{ row?.displayMessage }}</strong>
|
||
<span class="msg-context" v-if="row?.displayContext && row?.displayContext !== row?.displayMessage">{{ row?.displayContext }}</span>
|
||
<span class="msg-detail" v-if="row?._detail">{{ row?._detail }}</span>
|
||
<div class="msg-url" v-if="row?._url" :title="row?._url">{{ row?._url }}</div>
|
||
</div>
|
||
<div class="col-count">
|
||
<span class="count-pill" v-if="row?.count > 1">×{{ row?.count }}</span>
|
||
<span class="count-pill count-pill-muted" v-if="row?.count === 1">×1</span>
|
||
</div>
|
||
<div class="col-status">
|
||
<span class="status-pill" v-if="row?._status" :class="String(row?._status).match(/^[23]/) ? 'status-neutral' : ('status-' + row?._bucket)" :title="String(row?._status).match(/^2/) && row?._bucket === 'warn' ? 'Client closed the connection' : null">{{ String(row?._status).match(/^2/) && row?._bucket === 'warn' ? '—' : row?._status }}</span>
|
||
<span class="status-empty" v-if="!row?._status" title="No HTTP status for this event">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="errors-row-detail" v-if="expanded[row?._key]">
|
||
<div class="detail-tabs">
|
||
<button type="button" :class="{active: detailTab[row?._key] === 'raw' || !detailTab[row?._key]}" @click="detailTab[row._key] = 'raw'">Raw</button>
|
||
<button type="button" v-if="row?._stack" :class="{active: detailTab[row?._key] === 'stack'}" @click="detailTab[row._key] = 'stack'">Stack</button>
|
||
<button type="button" v-if="row?.count > 1" :class="{active: detailTab[row?._key] === 'related'}" @click="detailTab[row._key] = 'related'">Related ({{ row?.count }})</button>
|
||
</div>
|
||
<div class="detail-body">
|
||
<div class="detail-main">
|
||
<pre v-if="(detailTab[row?._key] || 'raw') === 'raw'">{{ row?._detailJson }}</pre>
|
||
<pre class="stack-pre" v-if="detailTab[row?._key] === 'stack'">{{ row?._stack }}</pre>
|
||
<div class="related-list" v-if="detailTab[row?._key] === 'related'">
|
||
<div class="related-row" v-for="(r, index) in row?._related" :key="index">
|
||
<span class="when-abs">{{ absTimeShort(r?.ts) }}</span>
|
||
<span class="msg-url">{{ r?._url }}</span>
|
||
<span class="status-pill" v-if="r?._status" :class="'status-' + r?._bucket">{{ r?._status }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="detail-actions">
|
||
<button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Copy curl</button>
|
||
<button class="btn btn-sm" type="button" @click="copyJson(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-clipboard"></use></svg> Copy JSON</button>
|
||
<span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
|
||
</div>
|
||
</div>
|
||
<aside class="detail-aside">
|
||
<div class="aside-block">
|
||
<div class="aside-label">First seen</div>
|
||
<div class="aside-value" :title="(absTime(row?._firstSeen))">{{ relTime(row?._firstSeen) }}</div>
|
||
</div>
|
||
<div class="aside-block">
|
||
<div class="aside-label">Last seen</div>
|
||
<div class="aside-value" :title="(absTime(row?.ts))">{{ relTime(row?.ts) }}</div>
|
||
</div>
|
||
<div class="aside-block">
|
||
<div class="aside-label">Occurrences</div>
|
||
<div class="aside-value">{{ row?.count }}<span class="aside-sub" v-if="row?._lastHourCount"> · {{ row?._lastHourCount }} this hour</span></div>
|
||
</div>
|
||
<div class="aside-block" v-if="row?._repoId">
|
||
<div class="aside-label">Repository</div>
|
||
<a class="aside-value" :href="safeUrl("/r/" + (row?._repoId))">{{ row?._repoId }}</a>
|
||
</div>
|
||
<div class="aside-block" v-if="row?._url">
|
||
<div class="aside-label">URL</div>
|
||
<div class="aside-value mono">{{ row?._url }}</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|