fix
This commit is contained in:
1 parent
645f923175
commit
2cf2cebf5c
20 files changed
+481
-51
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, type ReactNode } from "react"
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react"
|
||||
import {
|
||||
Activity,
|
||||
AlertOctagon,
|
||||
@@ -56,6 +56,7 @@ const without = (set: Set<string>, value: string) => {
|
||||
}
|
||||
|
||||
type KindFilter = "all" | "span_error" | "log_error" | "frontend_error"
|
||||
type GroupBy = "none" | "service"
|
||||
|
||||
interface HoneypotIP {
|
||||
ip: string
|
||||
@@ -304,11 +305,13 @@ const IssueCard = ({
|
||||
onStatusChange,
|
||||
statusBusy,
|
||||
onFocus,
|
||||
detectedIPByAddress,
|
||||
}: {
|
||||
issue: Issue
|
||||
onStatusChange: (issue: Issue, status: string) => void
|
||||
statusBusy: boolean
|
||||
onFocus: (issue: Issue, traceId?: string) => void
|
||||
detectedIPByAddress: Map<string, DetectedIP>
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [needleResult, setNeedleResult] = useState<NeedleResult | null>(null)
|
||||
@@ -337,6 +340,10 @@ const IssueCard = ({
|
||||
const hasMappedStacktrace = issue.samples.some((sample) => Boolean(sample.resolvedStacktrace))
|
||||
const contextSample = issue.samples.find((s) => s.httpMethod || s.httpRoute || s.httpStatus)
|
||||
const correlate = strongestCorrelate(issue)
|
||||
const clientLabels = [...new Set(issue.samples.map((sample) => sample.uaClass).filter((value): value is string => Boolean(value)))]
|
||||
.filter((value) => value !== "browser" && value !== "unknown" && value !== "none")
|
||||
.map(uaClassLabel)
|
||||
const badActorSources = [...new Set(issue.samples.flatMap((sample) => sample.sourceIp ? detectedIPByAddress.get(sample.sourceIp)?.sources ?? [] : []))]
|
||||
|
||||
const viewTrace = (traceId: string) => onFocus(issue, traceId)
|
||||
|
||||
@@ -368,6 +375,18 @@ const IssueCard = ({
|
||||
<meta.Icon className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
{meta.label}
|
||||
</span>
|
||||
{clientLabels.slice(0, 2).map((label) => (
|
||||
<span key={label} className="inline-flex h-[23px] shrink-0 items-center gap-1.5 whitespace-nowrap rounded-[6px] border border-ds-accent/30 bg-ds-accent-bg px-2 text-[10.5px] font-bold uppercase tracking-wider text-ds-accent">
|
||||
<Bot className="h-3 w-3" aria-hidden="true" />
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
{badActorSources.length > 0 && (
|
||||
<span className="inline-flex h-[23px] shrink-0 items-center gap-1.5 whitespace-nowrap rounded-[6px] border border-ds-warn/30 bg-ds-warn-bg px-2 text-[10.5px] font-bold uppercase tracking-wider text-ds-warn" title={`Detected by ${badActorSources.join(" and ")}`}>
|
||||
<ShieldBan className="h-3 w-3" aria-hidden="true" />
|
||||
Bad actor IP
|
||||
</span>
|
||||
)}
|
||||
<CopyRef id={issue.id} path={`/issues/${encodeURIComponent(issue.id)}`} />
|
||||
<span className="text-[12.5px] text-ds-text-3">
|
||||
Detected {formatRelativeTime(issue.firstSeen / 1e6)} · last seen{" "}
|
||||
@@ -464,6 +483,9 @@ const IssueCard = ({
|
||||
{(contextSample?.sourceIp || latestSample?.sourceIp) && (
|
||||
<IpButton ip={(contextSample?.sourceIp || latestSample?.sourceIp)!} />
|
||||
)}
|
||||
{badActorSources.length > 0 && (
|
||||
<span className="font-[family-name:var(--font-data)] text-ds-warn">detected_ip={badActorSources.join("+")}</span>
|
||||
)}
|
||||
{(contextSample?.release || latestSample?.release) && (
|
||||
<span>release: {contextSample?.release || latestSample?.release}</span>
|
||||
)}
|
||||
@@ -688,6 +710,7 @@ export const IssuesVisualizer = ({
|
||||
const focusIssue = (issue: Issue, traceId?: string) => go(traceId ? [issue.id, "trace", traceId] : [issue.id])
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
||||
const [kindFilter, setKindFilter] = useState<KindFilter>("all")
|
||||
const [groupBy, setGroupBy] = useState<GroupBy>("none")
|
||||
const [needleModalOpen, setNeedleModalOpen] = useState(false)
|
||||
const [detectorTest, setDetectorTest] = useState<DetectorTestResult | null>(null)
|
||||
const [detectorTestLoading, setDetectorTestLoading] = useState(false)
|
||||
@@ -720,6 +743,12 @@ export const IssuesVisualizer = ({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadSpamIPs()
|
||||
// The detector set is loaded once when the Issues view opens.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const detectedIPs = useMemo(() => {
|
||||
const byIP = new Map<string, DetectedIP>()
|
||||
for (const entry of spamIPs) byIP.set(entry.ip, { ip: entry.ip, sources: ["honeypot"], count: entry.count })
|
||||
@@ -731,6 +760,7 @@ export const IssuesVisualizer = ({
|
||||
return Array.from(byIP.values())
|
||||
}, [spamIPs, blockerStatus])
|
||||
const spamIPSet = useMemo(() => new Set(detectedIPs.map((entry) => entry.ip)), [detectedIPs])
|
||||
const detectedIPByAddress = useMemo(() => new Map(detectedIPs.map((entry) => [entry.ip, entry])), [detectedIPs])
|
||||
|
||||
const crawlersHidden = CRAWLER_CLASSES.every((c) => clientExclude.has(c))
|
||||
const toggleCrawlers = () => {
|
||||
@@ -800,6 +830,18 @@ export const IssuesVisualizer = ({
|
||||
}
|
||||
}, [filteredIssues, statusFilter])
|
||||
|
||||
const groupedPrimaryIssues = useMemo(() => {
|
||||
if (groupBy === "none") return [["", primaryIssues] as const]
|
||||
const groups = new Map<string, Issue[]>()
|
||||
for (const issue of primaryIssues) {
|
||||
const name = issue.service || "unknown"
|
||||
const group = groups.get(name) ?? []
|
||||
group.push(issue)
|
||||
groups.set(name, group)
|
||||
}
|
||||
return [...groups.entries()].sort((a, b) => b[1].reduce((n, issue) => n + issue.count, 0) - a[1].reduce((n, issue) => n + issue.count, 0))
|
||||
}, [groupBy, primaryIssues])
|
||||
|
||||
const [showSecondary, setShowSecondary] = useState(false)
|
||||
|
||||
const runDetectorTest = async () => {
|
||||
@@ -912,6 +954,7 @@ export const IssuesVisualizer = ({
|
||||
resetFilters()
|
||||
setSearchQuery("")
|
||||
setStatusFilter("all")
|
||||
setGroupBy("none")
|
||||
setExcludeSpamIPs(false)
|
||||
setClientInclude(new Set())
|
||||
setClientExclude(new Set())
|
||||
@@ -991,6 +1034,21 @@ export const IssuesVisualizer = ({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-[9px] border border-ds-line bg-ds-surface p-0.5" aria-label="Group issues">
|
||||
{(["none", "service"] as GroupBy[]).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setGroupBy(value)}
|
||||
aria-pressed={groupBy === value}
|
||||
className={`rounded-[6px] px-2.5 py-1 text-[11.5px] font-semibold transition-colors ${
|
||||
groupBy === value ? "bg-ds-fill text-ds-text" : "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
>
|
||||
{value === "none" ? "Flat" : "By service"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1150,9 +1208,19 @@ export const IssuesVisualizer = ({
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{primaryIssues.map((issue) => (
|
||||
<IssueCard key={issue.id} issue={issue} onStatusChange={onStatusChange} statusBusy={statusBusy} onFocus={focusIssue} />
|
||||
<div className="flex flex-col gap-5">
|
||||
{groupedPrimaryIssues.map(([service, group]) => (
|
||||
<section key={service || "all"} className="flex flex-col gap-3">
|
||||
{groupBy === "service" && (
|
||||
<div className="flex items-baseline gap-2 px-1">
|
||||
<h2 className="font-[family-name:var(--font-display)] text-[15px] font-bold text-ds-text">{service}</h2>
|
||||
<span className="font-[family-name:var(--font-data)] text-[11px] text-ds-text-3">{group.length} issue{group.length === 1 ? "" : "s"} · {group.reduce((n, issue) => n + issue.count, 0)} occurrences</span>
|
||||
</div>
|
||||
)}
|
||||
{group.map((issue) => (
|
||||
<IssueCard key={issue.id} issue={issue} onStatusChange={onStatusChange} statusBusy={statusBusy} onFocus={focusIssue} detectedIPByAddress={detectedIPByAddress} />
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -1174,7 +1242,7 @@ export const IssuesVisualizer = ({
|
||||
{showSecondary && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{secondaryIssues.map((issue) => (
|
||||
<IssueCard key={issue.id} issue={issue} onStatusChange={onStatusChange} statusBusy={statusBusy} onFocus={focusIssue} />
|
||||
<IssueCard key={issue.id} issue={issue} onStatusChange={onStatusChange} statusBusy={statusBusy} onFocus={focusIssue} detectedIPByAddress={detectedIPByAddress} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user