fixes
This commit is contained in:
1 parent
f30adf05af
commit
8824af8699
29 files changed
+1201
-115
No files matched your search
@@ -34,9 +34,11 @@ import {
|
||||
} from "../lib/issue-utils"
|
||||
import { useSubRoute } from "../lib/routes"
|
||||
import { CopyRef } from "./common/CopyRef"
|
||||
import { IpButton } from "./common/IpButton"
|
||||
import { IssueOccurrences } from "./IssueOccurrences"
|
||||
import { NeedleTestModal } from "./NeedleTestModal"
|
||||
import { FilterBar, type FilterChip } from "./FilterBar"
|
||||
import { UA_CLASSES, uaClassLabel } from "../lib/ua-class"
|
||||
import type { Issue, IssueSample } from "../lib/types"
|
||||
|
||||
export const kindMeta: Record<string, { label: string; Icon: typeof Activity }> = {
|
||||
@@ -46,6 +48,12 @@ export const kindMeta: Record<string, { label: string; Icon: typeof Activity }>
|
||||
}
|
||||
|
||||
type StatusFilter = "all" | "open" | "resolved" | "ignored"
|
||||
const without = (set: Set<string>, value: string) => {
|
||||
const next = new Set(set)
|
||||
next.delete(value)
|
||||
return next
|
||||
}
|
||||
|
||||
type KindFilter = "all" | "span_error" | "log_error" | "frontend_error"
|
||||
|
||||
interface HoneypotIP {
|
||||
@@ -438,6 +446,9 @@ const IssueCard = ({
|
||||
{(contextSample?.userAgent || latestSample?.userAgent)?.slice(0, 80)}
|
||||
</span>
|
||||
)}
|
||||
{(contextSample?.sourceIp || latestSample?.sourceIp) && (
|
||||
<IpButton ip={(contextSample?.sourceIp || latestSample?.sourceIp)!} />
|
||||
)}
|
||||
{(contextSample?.release || latestSample?.release) && (
|
||||
<span>release: {contextSample?.release || latestSample?.release}</span>
|
||||
)}
|
||||
@@ -666,6 +677,11 @@ export const IssuesVisualizer = ({
|
||||
const [detectorTest, setDetectorTest] = useState<DetectorTestResult | null>(null)
|
||||
const [detectorTestLoading, setDetectorTestLoading] = useState(false)
|
||||
const [excludeSpamIPs, setExcludeSpamIPs] = useState(false)
|
||||
// Client-class filters: an issue matches "client" when any sample came from
|
||||
// one of these classes, and is hidden by "not client" when every classified
|
||||
// sample did (a mixed human + bot issue stays visible).
|
||||
const [clientInclude, setClientInclude] = useState<Set<string>>(new Set())
|
||||
const [clientExclude, setClientExclude] = useState<Set<string>>(new Set())
|
||||
const [showSpamIPs, setShowSpamIPs] = useState(false)
|
||||
const [spamIPs, setSpamIPs] = useState<HoneypotIP[]>([])
|
||||
const [spamIPsLoading, setSpamIPsLoading] = useState(false)
|
||||
@@ -710,6 +726,11 @@ export const IssuesVisualizer = ({
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (clientInclude.size > 0 || clientExclude.size > 0) {
|
||||
const classes = issue.samples.map((s) => s.uaClass).filter((c): c is string => Boolean(c))
|
||||
if (clientInclude.size > 0 && !classes.some((c) => clientInclude.has(c))) return false
|
||||
if (clientExclude.size > 0 && classes.length > 0 && classes.every((c) => clientExclude.has(c))) return false
|
||||
}
|
||||
if (selectedServices.size > 0) {
|
||||
const matches =
|
||||
selectedServices.has(issue.service) ||
|
||||
@@ -724,7 +745,7 @@ export const IssuesVisualizer = ({
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [issues, selectedServices, searchQuery, statusFilter, kindFilter, excludeSpamIPs, spamIPSet])
|
||||
}, [issues, selectedServices, searchQuery, statusFilter, kindFilter, excludeSpamIPs, spamIPSet, clientInclude, clientExclude])
|
||||
|
||||
const { primaryIssues, secondaryIssues } = useMemo(() => {
|
||||
const sorted = [...filteredIssues].sort((a, b) => b.count - a.count)
|
||||
@@ -753,7 +774,12 @@ export const IssuesVisualizer = ({
|
||||
}
|
||||
|
||||
const activeFilterCount =
|
||||
selectedServices.size + (statusFilter !== "all" ? 1 : 0) + (excludeSpamIPs ? 1 : 0) + (searchQuery ? 1 : 0)
|
||||
selectedServices.size +
|
||||
clientInclude.size +
|
||||
clientExclude.size +
|
||||
(statusFilter !== "all" ? 1 : 0) +
|
||||
(excludeSpamIPs ? 1 : 0) +
|
||||
(searchQuery ? 1 : 0)
|
||||
|
||||
const chips: FilterChip[] = [
|
||||
...Array.from(selectedServices).map((service) => ({
|
||||
@@ -763,6 +789,20 @@ export const IssuesVisualizer = ({
|
||||
tone: "neutral" as const,
|
||||
onRemove: () => toggleService(service),
|
||||
})),
|
||||
...Array.from(clientInclude).map((c) => ({
|
||||
id: `client-${c}`,
|
||||
filterKey: "client",
|
||||
value: uaClassLabel(c),
|
||||
tone: "neutral" as const,
|
||||
onRemove: () => setClientInclude((prev) => without(prev, c)),
|
||||
})),
|
||||
...Array.from(clientExclude).map((c) => ({
|
||||
id: `not-client-${c}`,
|
||||
filterKey: "not client",
|
||||
value: uaClassLabel(c),
|
||||
tone: "neutral" as const,
|
||||
onRemove: () => setClientExclude((prev) => without(prev, c)),
|
||||
})),
|
||||
...(statusFilter !== "all"
|
||||
? [
|
||||
{
|
||||
@@ -798,15 +838,39 @@ export const IssuesVisualizer = ({
|
||||
: []),
|
||||
]
|
||||
|
||||
const addFilterOptions = uniqueServices
|
||||
.filter((s) => !selectedServices.has(s))
|
||||
.map((s) => ({ value: s, label: s }))
|
||||
const addFilterOptions = [
|
||||
...uniqueServices.filter((s) => !selectedServices.has(s)).map((s) => ({ value: s, label: s })),
|
||||
...UA_CLASSES.filter((c) => !clientInclude.has(c.value)).map((c) => ({
|
||||
value: `client:${c.value}`,
|
||||
label: `client: ${c.label}`,
|
||||
})),
|
||||
...UA_CLASSES.filter((c) => !clientExclude.has(c.value)).map((c) => ({
|
||||
value: `not-client:${c.value}`,
|
||||
label: `hide client: ${c.label}`,
|
||||
})),
|
||||
]
|
||||
|
||||
const addFilter = (value: string) => {
|
||||
if (value.startsWith("client:")) {
|
||||
const c = value.slice("client:".length)
|
||||
setClientInclude((prev) => new Set(prev).add(c))
|
||||
setClientExclude((prev) => without(prev, c))
|
||||
} else if (value.startsWith("not-client:")) {
|
||||
const c = value.slice("not-client:".length)
|
||||
setClientExclude((prev) => new Set(prev).add(c))
|
||||
setClientInclude((prev) => without(prev, c))
|
||||
} else {
|
||||
toggleService(value)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
resetFilters()
|
||||
setSearchQuery("")
|
||||
setStatusFilter("all")
|
||||
setExcludeSpamIPs(false)
|
||||
setClientInclude(new Set())
|
||||
setClientExclude(new Set())
|
||||
}
|
||||
|
||||
if (loading && issues.length === 0) {
|
||||
@@ -852,7 +916,7 @@ export const IssuesVisualizer = ({
|
||||
}}
|
||||
chips={chips}
|
||||
addFilterOptions={addFilterOptions}
|
||||
onAddFilter={(v) => toggleService(v)}
|
||||
onAddFilter={addFilter}
|
||||
matchText={`Matching ${filteredIssues.length} of ${issues.length} issues · ${activeFilterCount} filter${activeFilterCount === 1 ? "" : "s"}`}
|
||||
onClearAll={activeFilterCount > 0 ? clearAll : undefined}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user