Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9e83c8d
feat: add initial overview
ogzhanolguncu Feb 11, 2025
e1fa1d8
fix: run formatter
ogzhanolguncu Feb 11, 2025
5c01407
refactor: Add card back
ogzhanolguncu Feb 11, 2025
8323bec
feat: add first draft of barchart
ogzhanolguncu Feb 12, 2025
3ffdd3b
feat: add latency line chart
ogzhanolguncu Feb 12, 2025
c14754a
refactor: update timestamp colors
ogzhanolguncu Feb 12, 2025
e4372a8
feat: add initial draft of filters for overvioew
ogzhanolguncu Feb 12, 2025
8b6ce2f
feat: add overview filters
ogzhanolguncu Feb 12, 2025
2a33711
fix: round to nearest for latencies
ogzhanolguncu Feb 12, 2025
81f482c
feat: add data fetching for overviews
ogzhanolguncu Feb 12, 2025
0b152ba
fix: add chart states
ogzhanolguncu Feb 13, 2025
4eb831f
fix: add mvs for timeseries latency
ogzhanolguncu Feb 13, 2025
a5b07e7
fix: chart colors
ogzhanolguncu Feb 14, 2025
2eea8c7
refactor: organize files
ogzhanolguncu Feb 14, 2025
8d5aaa0
feat: add new optinos to action menu
ogzhanolguncu Feb 14, 2025
fcab8df
feat: add override modal
ogzhanolguncu Feb 14, 2025
5209c9e
feat: add new override fields
ogzhanolguncu Feb 14, 2025
d2c12ef
feat: extend logs data
ogzhanolguncu Feb 14, 2025
2f6decd
fix: styles
ogzhanolguncu Feb 14, 2025
f479630
fix: small ui issues
ogzhanolguncu Feb 17, 2025
580b859
feat: add status to filters for log
ogzhanolguncu Feb 17, 2025
c025068
fix: bugs
ogzhanolguncu Feb 17, 2025
2e3e67f
feat: add new sebmenu
ogzhanolguncu Feb 17, 2025
b96cec1
feat: add delete and update for quick navbar
ogzhanolguncu Feb 18, 2025
52a57fc
feat: add new quickbar to every ratelimit page
ogzhanolguncu Feb 18, 2025
10efc04
feat: add charts for ratelimit list
ogzhanolguncu Feb 18, 2025
f0ae734
feat: add missing search to ratelimit
ogzhanolguncu Feb 18, 2025
499d978
fix: ui issues
ogzhanolguncu Feb 19, 2025
54ba243
fix: ordering issues
ogzhanolguncu Feb 19, 2025
b2f7427
feat: add more granular timeseries for logs
ogzhanolguncu Feb 19, 2025
68c0d14
feat: add more granular options for charts
ogzhanolguncu Feb 19, 2025
c77360a
feat: add new search for identifiers and paths
ogzhanolguncu Feb 19, 2025
bc2ccaa
chore: cleanup
ogzhanolguncu Feb 19, 2025
954324f
fix: typo
ogzhanolguncu Feb 20, 2025
0d074c6
feat: add sorting
ogzhanolguncu Feb 20, 2025
1a78349
chore: remove mvs
ogzhanolguncu Feb 20, 2025
4272fb7
chore: formatter
ogzhanolguncu Feb 20, 2025
65f07de
feat: replace icons and remove latency
ogzhanolguncu Feb 20, 2025
8c408d6
fix: icon
ogzhanolguncu Feb 20, 2025
ff96dcb
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overvi…
ogzhanolguncu Feb 20, 2025
95c6c15
fix: conflict issue
ogzhanolguncu Feb 20, 2025
c38d27b
fix: coderabit issues
ogzhanolguncu Feb 20, 2025
6e1bddc
fix: review comments
ogzhanolguncu Feb 20, 2025
99ecae9
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overvi…
ogzhanolguncu Feb 20, 2025
b893b39
chore: remove unused icon
ogzhanolguncu Feb 20, 2025
f20639c
fix: load issue
ogzhanolguncu Feb 20, 2025
341c340
fix: missing error check
ogzhanolguncu Feb 20, 2025
8d47a24
fix: build issue
ogzhanolguncu Feb 21, 2025
825aecf
Merge branch 'main' of github.com:unkeyed/unkey into ratelimit-overvi…
ogzhanolguncu Feb 21, 2025
4316e95
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 21, 2025
5ca0947
fix: missing action for logs action menu item
ogzhanolguncu Feb 21, 2025
6259a45
Merge branch 'ratelimit-overview-v2' of github.com:unkeyed/unkey into…
ogzhanolguncu Feb 21, 2025
ad19525
fix: ch data fetching
ogzhanolguncu Feb 21, 2025
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
2 changes: 1 addition & 1 deletion apps/dashboard/app/(app)/desktop-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { createWorkspaceNavigation, resourcesNavigation } from "@/app/(app)/work
import { Feedback } from "@/components/dashboard/feedback-component";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDelayLoader } from "@/hooks/useDelayLoader";
import type { Workspace } from "@/lib/db";
import { useDelayLoader } from "@/lib/hooks/useDelayLoader";
import { cn } from "@/lib/utils";
import { Loader2, type LucideIcon } from "lucide-react";
import Link from "next/link";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,11 @@
import { formatTimestampForChart } from "@/components/logs/chart/utils/format-timestamp";
import { TIMESERIES_DATA_WINDOW } from "@/components/logs/constants";
import { trpc } from "@/lib/trpc/client";
import type { TimeseriesGranularity } from "@/lib/trpc/routers/logs/query-timeseries/utils";
import { addMinutes, format } from "date-fns";
import { useMemo } from "react";
import type { z } from "zod";
import { useFilters } from "../../../hooks/use-filters";
import type { queryTimeseriesPayload } from "../query-timeseries.schema";

// Duration in milliseconds for historical data fetch window (1 hours)
const TIMESERIES_DATA_WINDOW = 60 * 60 * 1000;

const formatTimestamp = (value: string | number, granularity: TimeseriesGranularity) => {
const date = new Date(value);
const offset = new Date().getTimezoneOffset() * -1;
const localDate = addMinutes(date, offset);

switch (granularity) {
case "perMinute":
return format(localDate, "HH:mm:ss");
case "perHour":
return format(localDate, "MMM d, HH:mm");
case "perDay":
return format(localDate, "MMM d");
default:
return format(localDate, "Pp");
}
};

export const useFetchTimeseries = () => {
const { filters } = useFilters();

Expand Down Expand Up @@ -115,7 +95,7 @@ export const useFetchTimeseries = () => {
});

const timeseries = data?.timeseries.map((ts) => ({
displayX: formatTimestamp(ts.x, data.granularity),
displayX: formatTimestampForChart(ts.x, data.granularity),
originalTimestamp: ts.x,
...ts.y,
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants";
import { ControlCloud } from "@/components/logs/control-cloud";
import { format } from "date-fns";
import { HISTORICAL_DATA_WINDOW } from "../../constants";
import { useFilters } from "../../hooks/use-filters";

const formatFieldName = (field: string): string => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,78 +1,36 @@
import { InputSearch } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
import { useState } from "react";
import { useFilters } from "../../../../../hooks/use-filters";
import { logsFilterFieldConfig } from "@/app/(app)/logs/filters.schema";
import { useFilters } from "@/app/(app)/logs/hooks/use-filters";
import { FilterOperatorInput } from "@/components/logs/filter-operator-input";

export const PathsFilter = () => {
const { filters, updateFilters } = useFilters();
const activeFilter = filters.find((f) => f.field === "paths");
const [searchText, setSearchText] = useState(activeFilter?.value.toString() ?? "");
const [isFocused, setIsFocused] = useState(false);

const handleSearch = () => {
const activeFilters = filters.filter((f) => f.field !== "paths");
if (searchText.trim()) {
updateFilters([
...activeFilters,
{
field: "paths",
value: searchText,
id: crypto.randomUUID(),
operator: "contains",
},
]);
} else {
updateFilters(activeFilters);
}
};
const pathOperators = logsFilterFieldConfig.paths.operators;
const options = pathOperators.map((op) => ({
id: op,
label: op,
}));

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (isFocused) {
e.stopPropagation();
if (e.key === "Enter") {
handleSearch();
}
}
};

const handleFocus = () => {
setIsFocused(true);
};

const handleBlur = () => {
setIsFocused(false);
};
const activePathFilter = filters.find((f) => f.field === "paths");

return (
<div className="flex flex-col p-4 gap-2 w-[300px]">
<div className="relative w-full">
<div
className={cn(
"flex items-center gap-2 px-2 py-1 h-8 rounded-md hover:bg-gray-3 bg-gray-4 transition-all duration-200",
isFocused && "bg-gray-4",
)}
>
<InputSearch className="w-4 h-4 text-accent-12" />
<input
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
type="text"
placeholder="Search for path..."
className="w-full text-[13px] font-medium text-accent-12 bg-transparent border-none outline-none focus:ring-0 focus:outline-none placeholder:text-accent-12"
/>
</div>
</div>
<Button
variant="primary"
className="font-sans mt-2 w-full h-9 rounded-md"
onClick={handleSearch}
>
Search
</Button>
</div>
<FilterOperatorInput
label="Path"
options={options}
defaultOption={activePathFilter?.operator}
defaultText={activePathFilter?.value as string}
onApply={(id, text) => {
const activeFiltersWithoutPaths = filters.filter((f) => f.field !== "paths");
updateFilters([
...activeFiltersWithoutPaths,
{
field: "paths",
id: crypto.randomUUID(),
operator: id,
value: text,
},
]);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants";
import { LiveSwitchButton } from "@/components/logs/live-switch-button";
import { HISTORICAL_DATA_WINDOW } from "../../../constants";
import { useLogsContext } from "../../../context/logs";
import { useFilters } from "../../../hooks/use-filters";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants";
import { trpc } from "@/lib/trpc/client";
import type { Log } from "@unkey/clickhouse/src/logs";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { z } from "zod";
import { HISTORICAL_DATA_WINDOW } from "../../../constants";
import { useFilters } from "../../../hooks/use-filters";
import type { queryLogsPayload } from "../query-logs.schema";

Expand All @@ -13,6 +13,8 @@ type UseLogsQueryParams = {
startPolling?: boolean;
};

const REALTIME_DATA_LIMIT = 100;

export function useLogsQuery({
limit = 50,
pollIntervalMs = 5000,
Expand All @@ -25,7 +27,7 @@ export function useLogsQuery({
const queryClient = trpc.useUtils();

const realtimeLogs = useMemo(() => {
return Array.from(realtimeLogsMap.values());
return sortLogs(Array.from(realtimeLogsMap.values()));
}, [realtimeLogsMap]);

const historicalLogs = useMemo(() => Array.from(historicalLogsMap.values()), [historicalLogsMap]);
Expand Down Expand Up @@ -142,7 +144,6 @@ export function useLogsQuery({
});

// Query for new logs (polling)
// biome-ignore lint/correctness/useExhaustiveDependencies: biome wants to everything as dep
const pollForNewLogs = useCallback(async () => {
try {
const latestTime = realtimeLogs[0]?.time ?? historicalLogs[0]?.time;
Expand All @@ -169,19 +170,30 @@ export function useLogsQuery({
newMap.set(log.request_id, log);
added++;

if (newMap.size > Math.min(limit, 100)) {
const oldestKey = Array.from(newMap.keys()).shift()!;
newMap.delete(oldestKey);
// Remove oldest entries when exceeding the size limit `100`
if (newMap.size > Math.min(limit, REALTIME_DATA_LIMIT)) {
const entries = Array.from(newMap.entries());
const oldestEntry = entries.reduce((oldest, current) => {
return oldest[1].time < current[1].time ? oldest : current;
});
newMap.delete(oldestEntry[0]);
}
}

// If nothing was added, return old map to prevent re-render
return added > 0 ? newMap : prevMap;
});
} catch (error) {
console.error("Error polling for new logs:", error);
}
}, [queryParams, queryClient, limit, pollIntervalMs, historicalLogsMap]);
}, [
queryParams,
queryClient,
limit,
pollIntervalMs,
historicalLogsMap,
realtimeLogs,
historicalLogs,
]);

// Set up polling effect
useEffect(() => {
Expand Down Expand Up @@ -221,3 +233,7 @@ export function useLogsQuery({
isPolling: startPolling,
};
}

const sortLogs = (logs: Log[]) => {
return logs.toSorted((a, b) => b.time - a.time);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const LogMetaSection = ({ content }: { content: string }) => {
<div className="text-[13px] text-accent-9 font-sans">Meta</div>
<Card className="bg-gray-2 border-gray-4 rounded-lg">
<CardContent className="py-2 px-3 text-xs relative group min-w-[300px]">
<pre className="text-accent-12">{content ?? "<EMPTY>"}</pre>
<pre className="text-accent-12">{content}</pre>
<Button
shape="square"
onClick={handleClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export const LogSection = ({
const value = valueParts.join(":").trim();
return (
<div className="group flex items-center w-full p-[3px]" key={key}>
<span className="w-28 text-left truncate text-accent-9">{key}:</span>
<span className="ml-2 text-xs text-accent-12 ">{value}</span>
<span className="text-left text-accent-9 whitespace-nowrap">{key}:</span>
<span className="ml-2 text-xs text-accent-12 truncate">{value}</span>
</div>
);
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";

import { useMemo } from "react";
import { DEFAULT_DRAGGABLE_WIDTH } from "../../../constants";
import { useLogsContext } from "../../../context/logs";
Expand Down Expand Up @@ -45,20 +44,39 @@ export const LogDetails = ({ distanceToTop }: Props) => {
style={panelStyle}
>
<LogHeader log={log} onClose={handleClose} />

<LogSection details={log.request_headers} title="Request Header" />
<LogSection
details={JSON.stringify(safeParseJson(log.request_body), null, 2)}
details={log.request_headers.length ? log.request_headers : "<EMPTY>"}
title="Request Header"
/>
<LogSection
details={
JSON.stringify(safeParseJson(log.request_body), null, 2) === "null"
? "<EMPTY>"
: JSON.stringify(safeParseJson(log.request_body), null, 2)
}
title="Request Body"
/>
<LogSection details={log.response_headers} title="Response Header" />
<LogSection
details={JSON.stringify(safeParseJson(log.response_body), null, 2)}
details={log.response_headers.length ? log.response_headers : "<EMPTY>"}
title="Response Header"
/>
<LogSection
details={
JSON.stringify(safeParseJson(log.response_body), null, 2) === "null"
? "<EMPTY>"
: JSON.stringify(safeParseJson(log.response_body), null, 2)
}
title="Response Body"
/>
<div className="mt-3" />
<LogFooter log={log} />
<LogMetaSection content={JSON.stringify(extractResponseField(log, "meta"), null, 2)} />
<LogMetaSection
content={
JSON.stringify(extractResponseField(log, "meta"), null, 2) === "null"
? "<EMPTY>"
: JSON.stringify(extractResponseField(log, "meta"), null, 2)
}
/>
</ResizablePanel>
);
};
16 changes: 14 additions & 2 deletions apps/dashboard/app/(app)/logs/components/table/logs-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { VirtualTable } from "@/components/virtual-table/index";
import type { Column } from "@/components/virtual-table/types";
import { cn } from "@/lib/utils";
import type { Log } from "@unkey/clickhouse/src/logs";
import { TriangleWarning2 } from "@unkey/icons";
import { Empty } from "@unkey/ui";
import { BookBookmark, TriangleWarning2 } from "@unkey/icons";
import { Button, Empty } from "@unkey/ui";
import { useMemo } from "react";
import { isDisplayProperty, useLogsContext } from "../../context/logs";
import { extractResponseField } from "../../utils";
Expand Down Expand Up @@ -253,6 +253,18 @@ export const LogsTable = () => {
Keep track of all activity within your workspace. We collect all API requests, giving
you a clear history to find problems or debug issues.
</Empty.Description>
<Empty.Actions className="mt-4 justify-start">
<a
href="https://www.unkey.com/docs/introduction"
target="_blank"
rel="noopener noreferrer"
>
<Button>
<BookBookmark />
Documentation
</Button>
</a>
</Empty.Actions>
</Empty>
</div>
}
Expand Down
2 changes: 0 additions & 2 deletions apps/dashboard/app/(app)/logs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ export const RED_STATES = ["DISABLED", "FORBIDDEN", "INSUFFICIENT_PERMISSIONS"];

export const METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
export const STATUSES = [200, 400, 500] as const;

export const HISTORICAL_DATA_WINDOW = 12 * 60 * 60 * 1000;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Label } from "@/components/ui/label";
import { CircleInfo } from "@unkey/icons";
import type { ReactNode } from "react";
import { InputTooltip } from "../_overview/components/table/components/logs-actions/components/input-tooltip";

type FormFieldProps = {
label: string;
tooltip?: string;
error?: string;
children: ReactNode;
};

export const FormField = ({ label, tooltip, error, children }: FormFieldProps) => (
// biome-ignore lint/a11y/useKeyWithClickEvents: no need for button
<div className="flex flex-col gap-1" onClick={(e) => e.stopPropagation()}>
<Label
className="text-gray-11 text-[13px] flex items-center"
onClick={(e) => e.preventDefault()}
>
{label}
{tooltip && (
<InputTooltip desc={tooltip}>
<CircleInfo size="md-regular" className="text-accent-8 ml-[10px]" />
</InputTooltip>
)}
</Label>
{children}
{error && <span className="text-error-10 text-[13px] font-medium">{error}</span>}
</div>
);
Loading