This commit is contained in:
devin-ai-integration[bot] 2026-09-30 21:33:50 -04:00 • committed by GitHub
commit 36e1ad37ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 138 additions and 14 deletions

View file

@ -53,6 +53,21 @@ describe("KeyActivityPanel", () => {
expect(screen.getByText("Showing 1 of 2 keys")).toBeInTheDocument();
});
it("narrows the rendered keys with a glob", () => {
render(<KeyActivityPanel keyMetrics={keyMetrics} />);
fireEvent.change(screen.getByLabelText("Search keys"), { target: { value: "bob-*" } });
expect(screen.getByTestId("rendered-keys")).toHaveTextContent("hash-bob");
expect(screen.getByTestId("rendered-keys")).not.toHaveTextContent("hash-alice");
expect(screen.getByText("Showing 1 of 2 keys")).toBeInTheDocument();
});
it("shows an invalid regular expression state instead of the keys", () => {
render(<KeyActivityPanel keyMetrics={keyMetrics} />);
fireEvent.change(screen.getByLabelText("Search keys"), { target: { value: "/[/" } });
expect(screen.getByText("Invalid regular expression: /[/")).toBeInTheDocument();
expect(screen.queryByTestId("rendered-keys")).not.toBeInTheDocument();
});
it("shows an empty state instead of zeroed metrics when nothing matches", () => {
render(<KeyActivityPanel keyMetrics={keyMetrics} />);
fireEvent.change(screen.getByLabelText("Search keys"), { target: { value: "carol" } });

View file

@ -4,7 +4,7 @@ import React, { useMemo, useState } from "react";
import { ActivityMetrics } from "@/components/activity_metrics";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { filterKeyActivity } from "../keyActivityFilter";
import { filterKeyActivity, parseKeyQuery } from "../keyActivityFilter";
import type { ModelActivityData } from "../types";
interface KeyActivityPanelProps {
@ -14,10 +14,12 @@ interface KeyActivityPanelProps {
const KeyActivityPanel: React.FC<KeyActivityPanelProps> = ({ keyMetrics, hidePromptCachingMetrics = false }) => {
const [query, setQuery] = useState("");
const parsed = useMemo(() => parseKeyQuery(query), [query]);
const filtered = useMemo(() => filterKeyActivity(keyMetrics, query), [keyMetrics, query]);
const totalKeys = Object.keys(keyMetrics).length;
const shownKeys = Object.keys(filtered).length;
const isFiltering = query.trim() !== "";
const isFiltering = parsed.kind !== "all";
const hasNoMatches = isFiltering && totalKeys > 0 && shownKeys === 0;
return (
<div className="space-y-4">
@ -28,7 +30,7 @@ const KeyActivityPanel: React.FC<KeyActivityPanelProps> = ({ keyMetrics, hidePro
</InputGroupAddon>
<InputGroupInput
aria-label="Search keys"
placeholder="Search by key alias, key hash, user ID, or email"
placeholder="Search by key alias, hash, user ID, or email. Supports * and /regex/"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
@ -44,11 +46,17 @@ const KeyActivityPanel: React.FC<KeyActivityPanelProps> = ({ keyMetrics, hidePro
Showing {shownKeys.toLocaleString()} of {totalKeys.toLocaleString()} keys
</span>
</div>
{isFiltering && totalKeys > 0 && shownKeys === 0 ? (
{parsed.kind === "invalid" && (
<p className="rounded-lg border p-6 text-center text-sm text-muted-foreground">
Invalid regular expression: {parsed.source}
</p>
)}
{parsed.kind !== "invalid" && hasNoMatches && (
<p className="rounded-lg border p-6 text-center text-sm text-muted-foreground">
No keys match &quot;{query.trim()}&quot; in this date range
</p>
) : (
)}
{parsed.kind !== "invalid" && !hasNoMatches && (
<ActivityMetrics modelMetrics={filtered} hidePromptCachingMetrics={hidePromptCachingMetrics} />
)}
</div>

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { filterKeyActivity, keyActivityMatches } from "./keyActivityFilter";
import { filterKeyActivity, keyActivityMatches, parseKeyQuery } from "./keyActivityFilter";
import type { KeyMetadata, ModelActivityData } from "./types";
function activity(label: string, key_metadata?: KeyMetadata): ModelActivityData {
@ -75,6 +75,56 @@ describe("keyActivityMatches", () => {
});
});
describe("parseKeyQuery and keyActivityMatches", () => {
it("matches anchored, case-insensitive globs", () => {
expect(keyActivityMatches("hash-alice", alice, "alice-*")).toBe(true);
expect(keyActivityMatches("hash-alice", alice, "batch-*")).toBe(false);
expect(keyActivityMatches("hash-alice", activity("production batch-key", aliceMeta), "batch-*")).toBe(false);
expect(keyActivityMatches("hash-alice", alice, "*-batch")).toBe(true);
expect(keyActivityMatches("hash-alice", alice, "ALICE-*")).toBe(true);
});
it("treats regex punctuation literally in globs", () => {
expect(keyActivityMatches("hash-alice", alice, "alice.batch*")).toBe(false);
});
it("matches the key hash with a glob", () => {
expect(keyActivityMatches("deadbeef", orphan, "dead*")).toBe(true);
});
it("matches regular expressions with standard case sensitivity", () => {
expect(keyActivityMatches("hash-bob", bob, "/^user-bob-\\d+$/")).toBe(true);
expect(keyActivityMatches("hash-alice", alice, "/^user-bob-\\d+$/")).toBe(false);
expect(keyActivityMatches("hash-alice", alice, "/^ALICE/")).toBe(false);
expect(keyActivityMatches("hash-alice", alice, "/^ALICE/i")).toBe(true);
});
it("marks malformed regular expressions invalid and matches nothing", () => {
expect(parseKeyQuery("/[/")).toEqual({ kind: "invalid", source: "/[/" });
expect(keyActivityMatches("hash-alice", alice, "/[/")).toBe(false);
});
it("strips stateful flags from a regular expression reused for multiple matches", () => {
const query = parseKeyQuery("/^alice/g");
expect(query.kind).toBe("pattern");
if (query.kind !== "pattern") return;
expect(query.regex.flags).not.toContain("g");
expect(query.regex.test("alice-batch")).toBe(true);
expect(query.regex.test("alice-batch")).toBe(true);
expect(
Object.keys(
filterKeyActivity({ "hash-alice": alice, "hash-alice-2": activity("alice-secondary", aliceMeta) }, "/^alice/g"),
),
).toEqual(["hash-alice", "hash-alice-2"]);
});
it("keeps a lone slash and a slash within text as substring queries", () => {
expect(parseKeyQuery("/")).toEqual({ kind: "substring", needle: "/" });
expect(parseKeyQuery("a/b")).toEqual({ kind: "substring", needle: "a/b" });
});
});
describe("filterKeyActivity", () => {
it("returns the same object when the query is blank", () => {
expect(filterKeyActivity(keyMetrics, "")).toBe(keyMetrics);
@ -88,4 +138,8 @@ describe("filterKeyActivity", () => {
it("returns an empty record when nothing matches", () => {
expect(filterKeyActivity(keyMetrics, "nobody")).toEqual({});
});
it("keeps the hashes matching a glob", () => {
expect(filterKeyActivity(keyMetrics, "user-bob-*")).toEqual({ "hash-bob": bob });
});
});

View file

@ -1,20 +1,67 @@
import type { ModelActivityData } from "./types";
export function keyActivityMatches(apiKey: string, data: ModelActivityData, query: string): boolean {
const needle = query.trim().toLowerCase();
if (needle === "") return true;
export type KeyQuery =
| { readonly kind: "all" }
| { readonly kind: "substring"; readonly needle: string }
| { readonly kind: "pattern"; readonly regex: RegExp }
| { readonly kind: "invalid"; readonly source: string };
export function parseKeyQuery(query: string): KeyQuery {
const trimmedQuery = query.trim();
if (trimmedQuery === "") return { kind: "all" };
const regexLiteral = new RegExp("^/(.+)/([a-z]*)$", "s").exec(trimmedQuery);
if (regexLiteral !== null) {
const [, body, flags] = regexLiteral;
try {
return { kind: "pattern", regex: new RegExp(body, flags.replace(/[gy]/g, "")) };
} catch {
return { kind: "invalid", source: trimmedQuery };
}
}
if (trimmedQuery.includes("*")) {
const escapedGlob = trimmedQuery.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
return { kind: "pattern", regex: new RegExp(`^${escapedGlob.replace(/\*/g, ".*")}$`, "i") };
}
return { kind: "substring", needle: trimmedQuery.toLowerCase() };
}
function keyActivityFields(apiKey: string, data: ModelActivityData): readonly (string | null | undefined)[] {
const meta = data.key_metadata;
return [apiKey, data.label, meta?.key_alias, meta?.user_id, meta?.user_email].some(
(field) => field?.toLowerCase().includes(needle) ?? false,
);
return [apiKey, data.label, meta?.key_alias, meta?.user_id, meta?.user_email];
}
function matchesQuery(fields: readonly (string | null | undefined)[], query: KeyQuery): boolean {
switch (query.kind) {
case "all":
return true;
case "substring":
return fields.some((field) => field?.toLowerCase().includes(query.needle) ?? false);
case "pattern":
return fields.some((field) => typeof field === "string" && query.regex.test(field));
case "invalid":
return false;
default: {
const unreachableQuery: never = query;
return unreachableQuery;
}
}
}
export function keyActivityMatches(apiKey: string, data: ModelActivityData, query: string): boolean {
return matchesQuery(keyActivityFields(apiKey, data), parseKeyQuery(query));
}
export function filterKeyActivity(
keyMetrics: Record<string, ModelActivityData>,
query: string,
): Record<string, ModelActivityData> {
if (query.trim() === "") return keyMetrics;
const parsedQuery = parseKeyQuery(query);
if (parsedQuery.kind === "all") return keyMetrics;
return Object.fromEntries(
Object.entries(keyMetrics).filter(([apiKey, data]) => keyActivityMatches(apiKey, data, query)),
Object.entries(keyMetrics).filter(([apiKey, data]) => matchesQuery(keyActivityFields(apiKey, data), parsedQuery)),
);
}