Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New logs page #2143

Closed
wants to merge 44 commits into from
Closed

Conversation

ogzhanolguncu
Copy link
Contributor

@ogzhanolguncu ogzhanolguncu commented Sep 28, 2024

What does this PR do?

Fixes # (issue)

If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Test A
  • Test B

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

Summary by CodeRabbit

  • New Features

    • Introduced a LogsChart component to visualize log data with a bar chart.
    • Added a DatePickerWithRange component for selecting date ranges.
    • Implemented a ResponseStatus component for filtering logs by HTTP response status codes.
    • Added a SearchCombobox for managing search items.
    • Enhanced log filtering with a new LogsFilters component.
    • Introduced LogDetails components for displaying detailed log information.
    • Added a TimestampInfo component for formatting and displaying timestamps.
    • Introduced a Calendar component for date selection.
    • Implemented a LogsTable component for displaying logs in a virtualized format.
    • Added a ResizablePanel component for adjustable log detail views.
    • Implemented a ButtonGroup component for grouping buttons.
    • Enhanced the CommandItem component to support a disabled state.
    • Added a LogMetaSection component for displaying meta information about logs.
    • Introduced a LogSection component for organized display of log details.
  • Bug Fixes

    • Improved error handling for log fetching and querying processes.
  • Documentation

    • Updated documentation to reflect new components and functionalities.
  • Chores

    • Added new dependencies to enhance data visualization and user interaction capabilities.

Copy link

changeset-bot bot commented Sep 28, 2024

⚠️ No Changeset found

Latest commit: 542e577

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Sep 28, 2024

@ogzhanolguncu is attempting to deploy a commit to the Unkey Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

coderabbitai bot commented Sep 28, 2024

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough
📝 Walkthrough
📝 Walkthrough

Walkthrough

This pull request introduces several new components and enhancements to the dashboard's logging functionality. Key changes include a new layout structure in layout.tsx, which removes a maximum width constraint, and the addition of various components for log visualization and filtering, such as LogsChart, DatePickerWithRange, and ResponseStatus. The request also implements new utility functions and types for managing log data, improves error handling, and updates the Page component to handle dynamic query parameters for log fetching.

Changes

File Path Change Summary
apps/dashboard/app/(app)/layout.tsx Removed maximum width constraint from workspace content div.
apps/dashboard/app/(app)/logs/components/chart.tsx Added LogsChart component for visualizing log data with a bar chart.
apps/dashboard/app/(app)/logs/components/filters/components/custom-date-filter.tsx Introduced DatePickerWithRange component for selecting date ranges.
apps/dashboard/app/(app)/logs/components/filters/components/response-status.tsx Added ResponseStatus component for filtering logs by HTTP response status.
apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/badge.tsx Introduced ComboboxBadge component for displaying editable search badges.
apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/constants.ts Added constants for search functionality, including options and explanations.
apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/hooks.ts Added custom hooks for managing search combobox state and interactions.
apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/search-combobox.tsx Added SearchCombobox component for managing search items.
apps/dashboard/app/(app)/logs/components/filters/components/time-split.tsx Introduced TimeSplitInput component for split time input management.
apps/dashboard/app/(app)/logs/components/filters/components/timeline.tsx Added Timeline component for selecting time ranges for log filtering.
apps/dashboard/app/(app)/logs/components/filters/index.tsx Introduced LogsFilters component for managing log filters.
apps/dashboard/app/(app)/logs/components/log-details/components/log-body.tsx Added LogBody component for displaying highlighted log details.
apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx Introduced LogFooter component for displaying detailed log entry information.
apps/dashboard/app/(app)/logs/components/log-details/components/log-header.tsx Added LogHeader component for displaying log entry headers.
apps/dashboard/app/(app)/logs/components/log-details/components/meta-content.tsx Introduced MetaContent component for displaying metadata in a highlighted format.
apps/dashboard/app/(app)/logs/components/log-details/components/request-response-details.tsx Added RequestResponseDetails component for displaying request and response details with tooltips.
apps/dashboard/app/(app)/logs/components/log-details/index.tsx Introduced _LogDetails component for displaying log details in a resizable panel.
apps/dashboard/app/(app)/logs/components/logs-table.tsx Added LogsTable component for displaying logs in a virtualized table format.
apps/dashboard/app/(app)/logs/constants.ts Introduced several constants for log management and UI states.
apps/dashboard/app/(app)/logs/logs-page.tsx Added LogsPage component for displaying logs for a specific workspace with dynamic fetching.
apps/dashboard/app/(app)/logs/page.tsx Updated Page component to handle dynamic query parameters for log fetching.
apps/dashboard/app/(app)/logs/query-state.ts Introduced functionality for managing and parsing query parameters related to log searches.
apps/dashboard/app/(app)/logs/types.ts Added Log and ResponseBody types for log data structure.
apps/dashboard/app/(app)/logs/utils.ts Introduced utility functions for handling log data and error management.
apps/dashboard/components/timestamp-info.tsx Added TimestampInfo component for formatting and displaying timestamps.
apps/dashboard/components/ui/calendar.tsx Introduced Calendar component utilizing react-day-picker.
apps/dashboard/components/ui/chart.tsx Added components and context for rendering charts using the Recharts library.
apps/dashboard/components/ui/command.tsx Updated CommandItem component to include a disabled prop for enhanced interactivity.
apps/dashboard/components/ui/group-button.tsx Introduced ButtonGroup component for managing button layouts.
apps/dashboard/components/ui/input.tsx Modified Input component to include optional startIcon and endIcon props.
apps/dashboard/components/ui/tabs.tsx Introduced tab interface components: Tabs, TabsList, TabsTrigger, and TabsContent.
apps/dashboard/lib/trpc/routers/index.ts Added logs router with a queryLogs route for log queries.
apps/dashboard/lib/trpc/routers/logs/query-log.ts Introduced queryLogs procedure with rate limiting and input validation.
apps/dashboard/lib/utils.ts Added debounce utility function for managing function calls with a delay.
apps/dashboard/package.json Added new dependencies: @tanstack/react-virtual, react-day-picker, and recharts.
apps/dashboard/styles/tailwind/base.css Introduced new CSS custom properties for chart colors and styles for code elements.
internal/clickhouse/src/logs.ts Enhanced structure and validation of payload for the getLogs function with new Zod schema.
apps/dashboard/app/(app)/logs/components/log-details/components/log-meta.tsx Added LogMetaSection component for displaying meta information.
apps/dashboard/app/(app)/logs/components/log-details/components/log-section.tsx Introduced LogSection component for displaying key-value pairs in logs.

Possibly related PRs

Suggested labels

🕹️ oss.gg, :joystick: 150 points, hacktoberfest

Suggested reviewers

  • mcstepp
  • chronark
  • perkinsjr
  • MichaelUnkey

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ogzhanolguncu ogzhanolguncu changed the title Logs page Feat: New logs page Sep 28, 2024
Copy link
Contributor

github-actions bot commented Sep 28, 2024

Thank you for following the naming conventions for pull request titles! 🙏

@ogzhanolguncu ogzhanolguncu changed the title Feat: New logs page feat: New logs page Sep 28, 2024
@chronark chronark self-assigned this Sep 28, 2024
Copy link

vercel bot commented Oct 9, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
dashboard ❌ Failed (Inspect) Dec 4, 2024 10:19am
engineering ❌ Failed (Inspect) Dec 4, 2024 10:19am

Comment on lines 43 to 79
function aggregateData(data: Log[]) {
const aggregatedData: {
date: string;
success: number;
warning: number;
error: number;
}[] = [];
const intervalMs = 60 * 1000 * 10;

if (data.length === 0) {
return aggregatedData;
}

const startOfDay = new Date(data[0].time).setHours(0, 0, 0, 0);
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;

for (let timestamp = startOfDay; timestamp < endOfDay; timestamp += intervalMs) {
const filteredLogs = data.filter((d) => d.time >= timestamp && d.time < timestamp + intervalMs);

const success = filteredLogs.filter(
(log) => log.response_status >= 200 && log.response_status < 300,
).length;
const warning = filteredLogs.filter(
(log) => log.response_status >= 400 && log.response_status < 500,
).length;
const error = filteredLogs.filter((log) => log.response_status >= 500).length;

aggregatedData.push({
date: format(timestamp, "yyyy-MM-dd'T'HH:mm:ss"),
success,
warning,
error,
});
}

return aggregatedData;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that feels a bit better, our arrays aren't huge, but as we might add more status types, we could end up looping over them quite a few times.

});
}}
/>
<CartesianGrid strokeDasharray="2" stroke="#DFE2E6" vertical={false} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will this work in dark/light mode?
can we reference a css variable instead so it works automatically?

to: new Date(),
});
const [finalDate, setFinalDate] = useState<DateRange>();
const [startTime, setStartTime] = useState({ HH: "09", mm: "00", ss: "00" });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do these defaults come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a random starting hour. Do you have something else on your mind?

apps/dashboard/app/(app)/logs/logs-page.tsx Outdated Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (16)
apps/dashboard/app/(app)/logs/page.tsx (1)

13-18: Remove unused params from type definition

The type definition includes params: { slug: string } but this parameter isn't used in the component.

export default async function Page({
  searchParams,
}: {
-  params: { slug: string };
  searchParams: Record<string, string | string[] | undefined>;
}) {
apps/dashboard/components/ui/input.tsx (2)

4-9: LGTM! Consider adding JSDoc comments.

The type definitions and imports are well-structured. The change from type to interface is a good choice for extensibility.

Consider adding JSDoc comments to document the props:

+/**
+ * Props for the Input component
+ * @property {LucideIcon} [startIcon] - Icon to display at the start of the input
+ * @property {LucideIcon} [endIcon] - Icon to display at the end of the input
+ */
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  startIcon?: LucideIcon;
  endIcon?: LucideIcon;
}

23-33: Consider enhancing accessibility with aria-label.

When icons are present, it would be helpful to provide additional context to screen readers.

        <input
          type={type}
          className={cn(
            "flex h-8 w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:border-primary placeholder:text-content-subtle focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
            startIcon ? "pl-8" : "",
            endIcon ? "pr-8" : "",
            className,
          )}
          ref={ref}
+         aria-label={startIcon || endIcon ? props['aria-label'] || props.placeholder : undefined}
          {...props}
        />
apps/dashboard/app/(app)/logs/components/filters/components/time-split.tsx (2)

7-25: Consider making props interface readonly for better immutability

The type definitions are well-structured, but the interface could be improved by making the properties readonly to prevent accidental mutations.

 export interface TimeSplitInputProps {
-  time: Time;
+  readonly time: Time;
-  setTime: (x: Time) => void;
+  readonly setTime: (x: Time) => void;
-  type: "start" | "end";
+  readonly type: "start" | "end";
-  setStartTime: (x: Time) => void;
+  readonly setStartTime: (x: Time) => void;
-  setEndTime: (x: Time) => void;
+  readonly setEndTime: (x: Time) => void;
-  startTime: Time;
+  readonly startTime: Time;
-  endTime: Time;
+  readonly endTime: Time;
-  startDate: Date;
+  readonly startDate: Date;
-  endDate: Date;
+  readonly endDate: Date;
 }

156-188: Improve time validation with constants and declarative checks

The validation logic uses magic numbers and could be more maintainable.

+const TIME_LIMITS = {
+  HH: 23,
+  mm: 59,
+  ss: 59
+} as const;
+
+const isValidTimeValue = (value: string, type: TimeType): boolean => {
+  if (!value || value.length > 2) return false;
+  const numValue = Number(value);
+  return !isNaN(numValue) && numValue >= 0 && numValue <= TIME_LIMITS[type];
+};
+
 function handleOnChange(value: string, valueType: TimeType) {
-  const payload = {
-    HH: time.HH,
-    mm: time.mm,
-    ss: time.ss,
-  };
-  if (value.length > 2) return;
-
-  switch (valueType) {
-    case "HH":
-      if (value && Number(value) > 23) return;
-      break;
-    case "mm":
-      if (value && Number(value) > 59) return;
-      break;
-    case "ss":
-      if (value && Number(value) > 59) return;
-      break;
-    default:
-      break;
-  }
-
-  payload[valueType] = value;
-  setTime({ ...payload });
+  if (!isValidTimeValue(value, valueType)) return;
+  setTime({ ...time, [valueType]: value });
 }
apps/dashboard/app/(app)/logs/utils.ts (1)

50-71: Add null checks and improve header parsing efficiency.

The implementation is good but could be enhanced with additional safety checks and performance improvements.

Consider these improvements:

 export const getRequestHeader = (log: Log, headerName: string): string | null => {
-  if (!headerName.trim()) {
+  if (!headerName?.trim()) {
     console.error("Invalid header name provided");
     return null;
   }

   if (!Array.isArray(log.request_headers)) {
     console.error("request_headers is not an array");
     return null;
   }

   const lowerHeaderName = headerName.toLowerCase();
-  const header = log.request_headers.find((h) => h.toLowerCase().startsWith(`${lowerHeaderName}:`));
+  // Use for...of for better performance as it can break early
+  let header: string | undefined;
+  for (const h of log.request_headers) {
+    if (h.toLowerCase().startsWith(`${lowerHeaderName}:`)) {
+      header = h;
+      break;
+    }
+  }

   if (!header) {
     console.warn(`Header "${headerName}" not found in request headers`);
     return null;
   }

-  const [, value] = header.split(":", 2);
-  return value ? value.trim() : null;
+  // More robust header value extraction
+  const colonIndex = header.indexOf(':');
+  if (colonIndex === -1) {
+    console.warn(`Invalid header format: "${header}"`);
+    return null;
+  }
+  const value = header.slice(colonIndex + 1);
+  return value.trim() || null;
 };
apps/dashboard/app/(app)/logs/query-state.ts (3)

14-14: Consider reorganizing and expanding HTTP status codes.

The status codes are in an unusual order (400, 500, 200). Consider:

  1. Organizing them in ascending order for better readability
  2. Including other common status codes (e.g., 201, 401, 403, 404)
-export const STATUSES = [400, 500, 200] as const;
+export const STATUSES = [200, 201, 400, 401, 403, 404, 500] as const;

18-26: Consider adding documentation and more specific types.

The QuerySearchParams type could benefit from:

  1. JSDoc documentation explaining the purpose of each field
  2. A more specific type for the method field (e.g., HTTP methods)
+/** Parameters for filtering and searching logs */
 export type QuerySearchParams = {
+  /** The host/domain to filter logs by */
   host: string;
+  /** Unique identifier for the request */
   requestId: string;
-  method: string;
+  /** HTTP method */
+  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
+  /** URL path pattern to filter by */
   path: string;
+  /** Array of HTTP status codes to filter by */
   responseStatuses: ResponseStatus[];
+  /** Start timestamp in milliseconds */
   startTime: number;
+  /** End timestamp in milliseconds */
   endTime: number;
 };

38-53: Enhance error handling in the hook.

While the time range validation is good, consider:

  1. Using a more robust error handling mechanism instead of just console.warn
  2. Adding validation for other parameters
 export const useLogSearchParams = () => {
   const [searchParams, setSearchParams] = useQueryStates(queryParamsPayload);
 
   const validateAndSetSearchParams = useCallback(
     (params: Partial<typeof searchParams>) => {
+      const errors: string[] = [];
+
       if (params.startTime && params.endTime && params.startTime > params.endTime) {
-        console.warn("Invalid time range: start time is after end time");
-        return;
+        errors.push("Invalid time range: start time is after end time");
       }
-      setSearchParams(params);
+
+      if (params.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'].includes(params.method)) {
+        errors.push("Invalid HTTP method");
+      }
+
+      if (errors.length > 0) {
+        console.error("Validation errors:", errors);
+        return errors;
+      }
+
+      setSearchParams(params);
+      return null;
     },
     [setSearchParams],
   );
 
   return { searchParams, setSearchParams: validateAndSetSearchParams };
 };
apps/dashboard/app/(app)/logs/components/log-details/components/log-body.tsx (1)

40-42: Add security considerations for dangerouslySetInnerHTML

While the HTML is generated from a trusted source (shiki), it's good practice to add a comment explaining the security implications.

Add a comment explaining why dangerouslySetInnerHTML is safe in this context:

 dangerouslySetInnerHTML={{
+  // Safe to use dangerouslySetInnerHTML as HTML is generated by shiki
   __html: innerHtml,
 }}
apps/dashboard/app/(app)/logs/components/log-details/index.tsx (1)

89-93: Use more specific key for header items

Using the entire header string as a key could lead to duplicate keys if headers have the same content.

Apply this diff:

-                <span key={header}>
+                <span key={`${key}-${header}`}>
                   <span className="text-content/65 capitalize">{key}</span>
                   <span className="text-content whitespace-pre-line">: {value}</span>
                 </span>
apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx (1)

63-76: Add aria-label to outcome badge

The Badge component should have an aria-label for better accessibility.

Apply this diff:

              <Badge
+               aria-label={`Outcome status: ${content}`}
                className={cn(
                  {
                    "text-amber-11 bg-amber-3 hover:bg-amber-3 font-medium":
                      YELLOW_STATES.includes(contentCopy),
                    "text-red-11 bg-red-3 hover:bg-red-3 font-medium":
                      RED_STATES.includes(contentCopy),
                  },
                  "uppercase",
                )}
              >
                {content}
              </Badge>
apps/dashboard/app/(app)/logs/components/logs-table.tsx (4)

14-17: Consider strengthening the type definition.

While the current implementation works, you could make the type more explicit by using a readonly array type to prevent accidental mutations.

-export const LogsTable = ({ logs }: { logs?: Log[] }) => {
+export const LogsTable = ({ logs }: { logs?: readonly Log[] }) => {

18-25: Consider memoizing virtualization configuration.

The virtualization config could be memoized to prevent unnecessary recalculations.

+const OVERSCAN = 5;
+
 const virtualizer = useVirtualizer({
   count: logs?.length ?? 0,
   getScrollElement: () => parentRef.current,
-  estimateSize: () => ROW_HEIGHT,
-  overscan: 5,
+  estimateSize: useCallback(() => ROW_HEIGHT, []),
+  overscan: OVERSCAN,
 });

Don't forget to add the import:

import { useCallback } from 'react';

100-122: Simplify status-based styling logic.

The current implementation has complex nested conditions for status-based styling. Consider extracting this logic into a utility function.

const getStatusStyles = (status: number, isSelected: boolean) => {
  if (status >= 500) {
    return isSelected ? 'bg-red-3' : 'bg-red-2 text-red-11 hover:bg-red-3';
  }
  if (status >= 400) {
    return isSelected ? 'bg-amber-3' : 'bg-amber-2 text-amber-11 hover:bg-amber-3';
  }
  return isSelected ? 'bg-background-subtle/90' : '';
};

Then simplify the className assignment:

-className={cn(
-  "font-mono grid grid-cols-[166px_72px_20%_1fr] text-[13px] leading-[14px] mb-[1px] rounded-[5px] h-[26px] cursor-pointer absolute top-0 left-0 w-full",
-  "hover:bg-background-subtle/90 pl-1",
-  {
-    "bg-amber-2 text-amber-11 hover:bg-amber-3":
-      l.response_status >= 400 && l.response_status < 500,
-    "bg-red-2 text-red-11 hover:bg-red-3": l.response_status >= 500,
-  },
-  selectedLog && {
-    "opacity-50": selectedLog.request_id !== l.request_id,
-    "opacity-100": selectedLog.request_id === l.request_id,
-    "bg-background-subtle/90":
-      selectedLog.request_id === l.request_id &&
-      l.response_status >= 200 &&
-      l.response_status < 300,
-    "bg-amber-3":
-      selectedLog.request_id === l.request_id &&
-      l.response_status >= 400 &&
-      l.response_status < 500,
-    "bg-red-3":
-      selectedLog.request_id === l.request_id && l.response_status >= 500,
-  },
-)
+className={cn(
+  "font-mono grid grid-cols-[166px_72px_20%_1fr] text-[13px] leading-[14px] mb-[1px] rounded-[5px] h-[26px] cursor-pointer absolute top-0 left-0 w-full",
+  "hover:bg-background-subtle/90 pl-1",
+  getStatusStyles(l.response_status, selectedLog?.request_id === l.request_id),
+  selectedLog && {
+    "opacity-50": selectedLog.request_id !== l.request_id,
+    "opacity-100": selectedLog.request_id === l.request_id,
+  }
+)}

153-155: Consider adding a tooltip for truncated text.

The response body is truncated but doesn't provide a way to view the full text without selecting the row.

-<div className="px-[2px] flex items-center max-w-[800px]">
-  <span className="truncate">{l.response_body}</span>
+<div className="px-[2px] flex items-center max-w-[800px]" title={l.response_body}>
+  <span className="truncate">{l.response_body}</span>
 </div>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 5213279 and 56a14bf.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • apps/dashboard/app/(app)/logs/components/filters/components/custom-date-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/filters/components/time-split.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/filters/components/timeline.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/filters/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-body.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-header.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/request-response-details.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/logs-table.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/logs-page.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/logs/query-state.ts (1 hunks)
  • apps/dashboard/app/(app)/logs/utils.ts (1 hunks)
  • apps/dashboard/components/ui/command.tsx (1 hunks)
  • apps/dashboard/components/ui/group-button.tsx (1 hunks)
  • apps/dashboard/components/ui/input.tsx (1 hunks)
  • apps/dashboard/lib/trpc/routers/logs/query-log.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-header.tsx
  • apps/dashboard/app/(app)/logs/components/filters/components/timeline.tsx
  • apps/dashboard/components/ui/command.tsx
  • apps/dashboard/app/(app)/logs/components/filters/components/custom-date-filter.tsx
  • apps/dashboard/app/(app)/logs/components/filters/index.tsx
  • apps/dashboard/lib/trpc/routers/logs/query-log.ts
  • apps/dashboard/app/(app)/logs/logs-page.tsx
  • apps/dashboard/components/ui/group-button.tsx
  • apps/dashboard/app/(app)/logs/components/log-details/components/request-response-details.tsx
🧰 Additional context used
📓 Learnings (2)
apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx (1)
Learnt from: chronark
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx:58-61
Timestamp: 2024-12-03T14:21:19.543Z
Learning: For the "Outcome" field in the `LogFooter` component (`apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx`), default to "N/A" instead of "VALID" when handling null values to avoid confusing customers.
apps/dashboard/app/(app)/logs/components/logs-table.tsx (1)
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.
🔇 Additional comments (17)
apps/dashboard/app/(app)/logs/page.tsx (5)

Line range hint 22-29: LGTM! Proper workspace validation

The workspace fetching includes proper null checks and tenant validation.


30-40: LGTM! Comprehensive log fetching parameters

The logs API call includes all necessary filtering parameters from the parsed search params.


41-43: LGTM! Error handling follows best practices

The error handling has been implemented correctly as suggested in the previous review.


7-7: Verify the default logs fetch count value

Let's ensure the default fetch count is appropriate for performance and UX.

✅ Verification successful

Default logs fetch count value is reasonable

The constant DEFAULT_LOGS_FETCH_COUNT is set to 100 entries, which is a reasonable default that balances between providing sufficient data for users and maintaining good performance. This value is appropriately used in the logs page component for initial fetching.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check the DEFAULT_LOGS_FETCH_COUNT value
cat apps/dashboard/app/(app)/logs/constants.ts | grep DEFAULT_LOGS_FETCH_COUNT

Length of output: 261


Script:

#!/bin/bash
# Search for the constant using ripgrep to handle special characters in path
rg "DEFAULT_LOGS_FETCH_COUNT" -A 1

Length of output: 500


45-45: Verify LogsPage component props

Let's ensure the LogsPage component properly handles these props.

✅ Verification successful

Props are correctly handled by LogsPage component

The LogsPage component properly accepts and uses both initialLogs and workspaceId props:

  • initialLogs is used to initialize the logs state
  • workspaceId is used in the TRPC query for fetching logs
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check LogsPage component implementation
ast-grep --pattern 'export function LogsPage({ initialLogs, workspaceId }: { $_ }) {
  $$$
}'

Length of output: 95


Script:

#!/bin/bash
# Let's try a broader search for LogsPage component
ast-grep --pattern 'LogsPage'

# Also search for the file containing LogsPage
fd LogsPage

Length of output: 313


Script:

#!/bin/bash
# Let's check the implementation of LogsPage in logs-page.tsx
cat apps/dashboard/app/\(app\)/logs/logs-page.tsx

Length of output: 2469

apps/dashboard/components/ui/input.tsx (2)

12-33: LGTM! Clean and efficient implementation.

The component implementation is well-structured with proper:

  • Props handling and ref forwarding
  • Conditional rendering of icons
  • Dynamic className management
  • Performance considerations through icon component memoization

1-42: Verify integration with logs page components.

The Input component's design aligns well with the logs page requirements. However, ensure consistent usage across the logs page components (LogsChart, LogsFilters) for a unified experience.

✅ Verification successful

Let me check the actual usage patterns in these files to verify the integration.


Let me try again with the correct command.


Input component is properly integrated across logs page components

The verification shows consistent and proper usage of the Input component across the logs page:

  • Used in search-combobox for search functionality with proper ref handling and keyboard interactions
  • Integrated with appropriate props and styling in the time-split component for date/time filtering
  • Maintains consistent styling and behavior through proper prop passing
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check usage patterns of the Input component in logs page components

# Search for Input component usage in logs-related files
rg -l "Input" apps/dashboard/app/\(app\)/logs/

Length of output: 471


Script:

#!/bin/bash
# Check how Input component is used in the logs-related files
rg "Input" --type typescript --context 3 apps/dashboard/app/\(app\)/logs/components/

Length of output: 119


Script:

#!/bin/bash
# Check how Input component is used in the logs-related files
rg "Input" -A 3 -B 3 apps/dashboard/app/\(app\)/logs/components/filters/components/

Length of output: 17823

apps/dashboard/app/(app)/logs/components/filters/components/time-split.tsx (2)

1-6: LGTM! Good attribution and clean imports.

The code properly attributes the original source and includes only the necessary imports.


195-276: Previous review comments are still applicable

The following issues from previous reviews remain relevant:

  1. useEffect dependencies and lint ignore
  2. Input field implementation improvements
apps/dashboard/app/(app)/logs/utils.ts (2)

3-11: LGTM! Well-structured custom error class.

The ResponseBodyParseError class is well-implemented with proper typing and follows best practices for custom error classes in TypeScript.


13-48: Verify the correct ID field usage and add type safety improvements.

The implementation looks solid but has a few areas for improvement:

Consider these improvements:

  1. Add runtime type validation after the type assertion:
-const parsedBody = JSON.parse(log.response_body) as ResponseBody;
+const parsedBody = JSON.parse(log.response_body) as unknown;
+const isResponseBody = (obj: unknown): obj is ResponseBody => {
+  return obj !== null && typeof obj === 'object';
+};
+if (!isResponseBody(parsedBody)) {
+  throw new ResponseBodyParseError("Invalid response body structure", parsedBody);
+}
  1. Add try-catch specifically for JSON.parse:
 try {
-  const parsedBody = JSON.parse(log.response_body) as ResponseBody;
+  let parsedBody;
+  try {
+    parsedBody = JSON.parse(log.response_body) as ResponseBody;
+  } catch (e) {
+    throw new ResponseBodyParseError("Invalid JSON in response_body", {
+      error: e instanceof Error ? e.message : "Unknown parsing error"
+    });
+  }
apps/dashboard/app/(app)/logs/query-state.ts (2)

13-13: Export TIMELINE_OPTIONS if intended for external use.

The TIMELINE_OPTIONS constant is defined but not exported. If it's intended to be used by other components, consider exporting it.


11-11: Consider removing or utilizing the PickKeys type.

The PickKeys type appears to be unused within this file. If it's not being used elsewhere, consider removing it. If it's intended for external use, consider adding documentation explaining its purpose and usage.

✅ Verification successful

PickKeys type is correctly used and serves a specific purpose

The PickKeys type is actively used in the search-combobox component to create strongly-typed options for filtering logs. It ensures type safety by restricting the possible filter keys to specific fields of QuerySearchParams ("host", "requestId", "path", "method"). The type definition is appropriate and serves its purpose well.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usage of PickKeys type
rg "PickKeys" --type ts

Length of output: 467


Script:

#!/bin/bash
# Get the content of the file using the PickKeys type to understand its usage
cat apps/dashboard/app/\(app\)/logs/components/filters/components/search-combobox/hooks.ts

Length of output: 3244

apps/dashboard/app/(app)/logs/components/log-details/components/log-body.tsx (2)

12-15: 🛠️ Refactor suggestion

Move highlighter initialization outside component

The highlighter initialization should be moved outside the component to prevent recreation on each render.

Apply this diff:

-const highlighter = createHighlighter({
+const getHighlighter = (() => {
+  let instance: Promise<any> | null = null;
+  return () => {
+    if (!instance) {
+      instance = createHighlighter({
+        themes: ["github-light", "github-dark"],
+        langs: ["json"],
+      });
+    }
+    return instance;
+  };
+})();

20-32: ⚠️ Potential issue

Add error handling for JSON parsing and highlighting

The code lacks proper error handling for JSON parsing and highlighting operations, and doesn't clean up properly.

Apply this diff:

 useEffect(() => {
+  let mounted = true;
+
+  try {
+    const parsedJson = JSON.parse(field);
     highlighter.then((highlight) => {
-      const html = highlight.codeToHtml(JSON.stringify(JSON.parse(field), null, 2), {
+      if (!mounted) return;
+      const html = highlight.codeToHtml(JSON.stringify(parsedJson, null, 2), {
         lang: "json",
         themes: {
           dark: "github-dark",
           light: "github-light",
         },
         mergeWhitespaces: true,
       });
       setHtml(html);
+    }).catch((error) => {
+      console.error('Failed to highlight code:', error);
+      setHtml('Failed to highlight code');
     });
+  } catch (error) {
+    console.error('Invalid JSON:', error);
+    setHtml('Invalid JSON');
+  }
+
+  return () => {
+    mounted = false;
+  };
 }, [field]);

Likely invalid or redundant comment.

apps/dashboard/app/(app)/logs/components/logs-table.tsx (2)

1-13: LGTM! Well-organized imports and meaningful constants.

The imports are logically grouped and the constants provide clear styling parameters for the table.


69-99: ⚠️ Potential issue

Improve keyboard navigation accessibility.

The current keyboard navigation implementation has several issues:

  1. Using tabIndex on non-interactive elements
  2. Direct DOM manipulation for focus management
  3. Missing ARIA roles and labels
-<div
+<button
   key={virtualRow.key}
   data-index={virtualRow.index}
   ref={virtualizer.measureElement}
   onClick={() => handleLogSelection(l)}
-  tabIndex={virtualRow.index}
+  role="row"
+  aria-label={`Log entry for ${l.path} with status ${l.response_status}`}
   aria-selected={selectedLog?.request_id === l.request_id}
   onKeyDown={(event) => {
     if (event.key === "Enter" || event.key === " ") {
       event.preventDefault();
       handleLogSelection(l);
     }
-    if (event.key === "ArrowDown") {
-      event.preventDefault();
-      const nextElement = document.querySelector(
-        `[data-index="${virtualRow.index + 1}"]`,
-      ) as HTMLElement;
-      nextElement?.focus();
-    }
-    if (event.key === "ArrowUp") {
-      event.preventDefault();
-      const prevElement = document.querySelector(
-        `[data-index="${virtualRow.index - 1}"]`,
-      ) as HTMLElement;
-      prevElement?.focus();
-    }
   }}

Consider using a proper table structure with role="grid" and implementing keyboard navigation using a focus management library or React's ref system.

Likely invalid or redundant comment.

Comment on lines +40 to +154
}
if (!_time.mm) {
_time.mm = "00";
}
if (!_time.ss) {
_time.ss = "00";
}

let endTimeChanges = false;
const endTimePayload = endTime;

let startTimeChanges = false;
const startTimePayload = startTime;

// Only run time conflicts if
// startDate and endDate are the same date

if (format(new Date(startDate), "dd/mm/yyyy") === format(new Date(endDate), "dd/mm/yyyy")) {
// checks if start time is ahead of end time

if (type === "start") {
if (_time.HH && Number(_time.HH) > Number(endTime.HH)) {
endTimePayload.HH = _time.HH;
endTimeChanges = true;
}

if (
// also check the hour
_time.HH &&
Number(_time.HH) >= Number(endTime.HH) &&
// check the minutes
_time.mm &&
Number(_time.mm) > Number(endTime.mm)
) {
endTimePayload.mm = _time.mm;
endTimeChanges = true;
}

if (
// also check the hour
_time.HH &&
Number(_time.HH) >= Number(endTime.HH) &&
// check the minutes
_time.mm &&
Number(_time.mm) >= Number(endTime.mm) &&
// check the seconds
_time.ss &&
Number(_time.ss) > Number(endTime.ss)
) {
endTimePayload.ss = _time.ss;
endTimeChanges = true;
}
}

if (type === "end") {
if (_time.HH && Number(_time.HH) < Number(startTime.HH)) {
startTimePayload.HH = _time.HH;
startTimeChanges = true;
}

if (
// also check the hour
_time.HH &&
Number(_time.HH) <= Number(startTime.HH) &&
// check the minutes
_time.mm &&
Number(_time.mm) < Number(startTime.mm)
) {
startTimePayload.mm = _time.mm;
startTimeChanges = true;
}

if (
// also check the hour
_time.HH &&
Number(_time.HH) <= Number(startTime.HH) &&
// check the minutes
_time.mm &&
Number(_time.mm) <= Number(startTime.mm) &&
// check the seconds
_time.ss &&
Number(_time.ss) < Number(startTime.ss)
) {
startTimePayload.ss = _time.ss;
startTimeChanges = true;
}
}
}

setTime({ ..._time });

if (endTimeChanges) {
setEndTime({ ...endTimePayload });
}
if (startTimeChanges) {
setStartTime({ ...startTimePayload });
}

setFocus(false);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve handleOnBlur implementation for better maintainability

The current implementation has several issues:

  1. Direct mutation of the time object
  2. Complex nested conditions for time conflict resolution
  3. Mixed responsibilities (formatting and conflict resolution)

Consider splitting the function into smaller, focused functions:

const formatTimeSegment = (value: string) => (!value ? "00" : value.padStart(2, "0"));

const formatTime = (time: Time): Time => ({
  HH: formatTimeSegment(time.HH),
  mm: formatTimeSegment(time.mm),
  ss: formatTimeSegment(time.ss)
});

const isTimeConflict = (
  type: "start" | "end",
  currentTime: Time,
  startTime: Time,
  endTime: Time
): { startTimeChanges: boolean; endTimeChanges: boolean; updates: Partial<Time> } => {
  // Extract conflict detection logic here
  // Return necessary updates without mutations
};

function handleOnBlur() {
  const formattedTime = formatTime(time);
  
  if (format(startDate, "dd/mm/yyyy") === format(endDate, "dd/mm/yyyy")) {
    const { startTimeChanges, endTimeChanges, updates } = isTimeConflict(
      type,
      formattedTime,
      startTime,
      endTime
    );
    
    setTime(formattedTime);
    if (endTimeChanges) setEndTime({ ...endTime, ...updates });
    if (startTimeChanges) setStartTime({ ...startTime, ...updates });
  } else {
    setTime(formattedTime);
  }
  
  setFocus(false);
}

apps/dashboard/app/(app)/logs/query-state.ts Outdated Show resolved Hide resolved
Comment on lines +27 to +34
const handleLogSelection = (log: Log) => {
setSelectedLog(log);
setTableDistanceToTop(
document.getElementById("log-table")!.getBoundingClientRect().top +
window.scrollY -
TABLE_BORDER_THICKNESS,
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace direct DOM manipulation with ref-based approach.

The current implementation uses direct DOM manipulation with getElementById and non-null assertion (!), which could lead to runtime errors if the element is not found.

+const tableRef = useRef<HTMLDivElement>(null);
+
 const handleLogSelection = (log: Log) => {
   setSelectedLog(log);
-  setTableDistanceToTop(
-    document.getElementById("log-table")!.getBoundingClientRect().top +
-      window.scrollY -
-      TABLE_BORDER_THICKNESS,
-  );
+  if (tableRef.current) {
+    setTableDistanceToTop(
+      tableRef.current.getBoundingClientRect().top +
+        window.scrollY -
+        TABLE_BORDER_THICKNESS,
+    );
+  }
 };

Then update the div reference:

-<div className="h-[75vh] overflow-auto" id="log-table" ref={parentRef}>
+<div className="h-[75vh] overflow-auto" ref={(el) => {
+  parentRef.current = el;
+  tableRef.current = el;
+}}>

Committable suggestion skipped: line range outside the PR's diff.

Improve performance of chart calculation. And, move meta field of log
details out of log footer
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (5)
apps/dashboard/app/(app)/logs/query-state.ts (1)

13-13: Consider exporting TIMELINE_OPTIONS constant

The constant is used to define the exported Timeline type but isn't exported itself. This could limit reusability if other components need access to the timeline options.

-const TIMELINE_OPTIONS = ["1h", "3h", "6h", "12h", "24h"] as const;
+export const TIMELINE_OPTIONS = ["1h", "3h", "6h", "12h", "24h"] as const;
apps/dashboard/app/(app)/logs/components/chart.tsx (1)

150-153: Add block statements for better readability

Following the static analysis suggestion, wrap single-line if statements in blocks for consistency and maintainability.

-      if (status >= 200 && status < 300) bucket.success++;
-      else if (status >= 400 && status < 500) bucket.warning++;
-      else if (status >= 500) bucket.error++;
+      if (status >= 200 && status < 300) {
+        bucket.success++;
+      } else if (status >= 400 && status < 500) {
+        bucket.warning++;
+      } else if (status >= 500) {
+        bucket.error++;
+      }
🧰 Tools
🪛 Biome (1.9.4)

[error] 150-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 151-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 152-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)

apps/dashboard/app/(app)/logs/components/log-details/components/log-meta.tsx (1)

3-14: Add prop validation and improve responsive design

Consider these improvements:

  1. Add prop validation to ensure content is always a string
  2. Make the width responsive instead of fixed
-export const LogMetaSection = ({ content }: { content: string }) => {
+import { type FC } from 'react';
+
+interface LogMetaSectionProps {
+  content: string;
+}
+
+export const LogMetaSection: FC<LogMetaSectionProps> = ({ content }) => {
   return (
     <div className="flex justify-between pt-2.5 px-3">
       <div className="text-sm text-content/65 font-sans">Meta</div>
       <Card className="rounded-[5px] flex">
-        <CardContent className="text-[12px] w-[300px] flex-2 bg-background-subtle p-3">
+        <CardContent className="text-[12px] w-full min-w-[300px] max-w-[500px] flex-2 bg-background-subtle p-3">
           <pre>{content}</pre>
         </CardContent>
       </Card>
apps/dashboard/app/(app)/logs/components/log-details/index.tsx (2)

22-28: Consider using ResizeObserver instead of debounce

The current debounce implementation might cause jank during resizing.

-const PANEL_WIDTH_SET_DELAY = 150;
+import { useResizeObserver } from '@/hooks/use-resize-observer';

 const _LogDetails = ({ log, onClose, distanceToTop }: Props) => {
-  const [panelWidth, setPanelWidth] = useState(DEFAULT_DRAGGABLE_WIDTH);
-
-  const debouncedSetPanelWidth = useDebounceCallback((newWidth) => {
-    setPanelWidth(newWidth);
-  }, PANEL_WIDTH_SET_DELAY);
+  const { ref, width } = useResizeObserver<HTMLDivElement>();
+  const panelWidth = width ?? DEFAULT_DRAGGABLE_WIDTH;

52-54: Remove unnecessary div and negative margin

The empty div with negative margin seems to be a hack.

 <div className="space-y-3 border-b-[1px] border-border py-4">
-  <div className="mt-[-24px]" />
   <LogSection details={log.request_headers} title="Request Header" />

Consider adjusting the padding of the parent container instead if more space is needed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 56a14bf and 7f94805.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • apps/dashboard/app/(app)/logs/components/chart.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/filters/components/response-status.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-meta.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-section.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/components/log-details/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/logs/query-state.ts (1 hunks)
  • apps/dashboard/package.json (2 hunks)
  • apps/dashboard/styles/tailwind/base.css (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/dashboard/package.json
  • apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx
  • apps/dashboard/app/(app)/logs/components/filters/components/response-status.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
apps/dashboard/app/(app)/logs/components/chart.tsx

[error] 150-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 151-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 152-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)

🔇 Additional comments (6)
apps/dashboard/app/(app)/logs/query-state.ts (3)

18-26: LGTM! Well-structured type definition

The QuerySearchParams type clearly defines all necessary fields for the search parameters.


40-44: Consider adding error handling for invalid query parameters

The useLogSearchParams hook should handle cases where query parameters are invalid or malformed.


28-38: ⚠️ Potential issue

Fix naming inconsistency in query parameters

The parameter name responseStatus in queryParamsPayload doesn't match the responseStatuses field in QuerySearchParams type.

-  responseStatus: parseAsArrayOf(parseAsNumberLiteral(STATUSES)).withDefault([]),
+  responseStatuses: parseAsArrayOf(parseAsNumberLiteral(STATUSES)).withDefault([]),

Likely invalid or redundant comment.

apps/dashboard/styles/tailwind/base.css (1)

14-19: LGTM! Well-implemented chart color system

The chart colors are properly defined with HSL values and have appropriate dark mode variants. The use of CSS variables allows for consistent theming across the application.

Also applies to: 66-71

apps/dashboard/app/(app)/logs/components/chart.tsx (1)

112-157: Consider optimizing aggregateData function

The current implementation could be optimized for performance with large datasets.

🧰 Tools
🪛 Biome (1.9.4)

[error] 150-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 151-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 152-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)

apps/dashboard/app/(app)/logs/components/log-details/index.tsx (1)

56-57: ⚠️ Potential issue

Add error handling for JSON parsing operations

The code assumes JSON.parse will always succeed and the result will be an object.

 <LogSection
-  details={flattenObject(JSON.parse(log.request_body))}
+  details={(() => {
+    try {
+      const parsed = JSON.parse(log.request_body);
+      if (typeof parsed !== 'object' || parsed === null) {
+        throw new Error('Expected object');
+      }
+      return flattenObject(parsed);
+    } catch (error) {
+      console.error('Invalid request body:', error);
+      return ['Error: Invalid JSON or non-object data'];
+    }
+  })()}
   title="Request Body"
 />

Apply the same pattern to the response body parsing.

Also applies to: 61-62

error: 0,
});
}
console.log(buckets);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove console.log statement

Remove debugging console.log statement before deploying to production.

-  console.log(buckets);

Comment on lines +94 to +106
<Bar
dataKey="success"
stackId="a"
fill="var(--color-success)"
radius={3}
/>
<Bar
dataKey="warning"
stackId="a"
fill="var(--color-warning)"
radius={3}
/>
<Bar dataKey="error" stackId="a" fill="var(--color-error)" radius={3} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix inconsistency in color variable usage

The Bar components use different color variables (--color-success) than those defined in chartConfig (--chart-3). This could lead to inconsistent styling.

-          fill="var(--color-success)"
+          fill={chartConfig.success.color}
-          fill="var(--color-warning)"
+          fill={chartConfig.warning.color}
-          fill="var(--color-error)"
+          fill={chartConfig.error.color}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Bar
dataKey="success"
stackId="a"
fill="var(--color-success)"
radius={3}
/>
<Bar
dataKey="warning"
stackId="a"
fill="var(--color-warning)"
radius={3}
/>
<Bar dataKey="error" stackId="a" fill="var(--color-error)" radius={3} />
<Bar
dataKey="success"
stackId="a"
fill={chartConfig.success.color}
radius={3}
/>
<Bar
dataKey="warning"
stackId="a"
fill={chartConfig.warning.color}
radius={3}
/>
<Bar dataKey="error" stackId="a" fill={chartConfig.error.color} radius={3} />

Comment on lines +16 to +28
{details.map((header) => {
const [key, ...valueParts] = header.split(":");
const value = valueParts.join(":").trim();
return (
<span key={header}>
<span className="text-content/65 capitalize">{key}</span>
<span className="text-content whitespace-pre-line">
: {value}
</span>
{"\n"}
</span>
);
})}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for malformed detail strings

The current implementation assumes all detail strings contain a colon separator. Add error handling to gracefully handle malformed inputs.

 {details.map((header) => {
+  if (!header.includes(':')) {
+    console.warn(`Malformed detail string: ${header}`);
+    return (
+      <span key={header}>
+        <span className="text-content/65">{header}</span>
+        {"\n"}
+      </span>
+    );
+  }
   const [key, ...valueParts] = header.split(":");
   const value = valueParts.join(":").trim();
   return (
     <span key={header}>
       <span className="text-content/65 capitalize">{key}</span>
       <span className="text-content whitespace-pre-line">
         : {value}
       </span>
       {"\n"}
     </span>
   );
 })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{details.map((header) => {
const [key, ...valueParts] = header.split(":");
const value = valueParts.join(":").trim();
return (
<span key={header}>
<span className="text-content/65 capitalize">{key}</span>
<span className="text-content whitespace-pre-line">
: {value}
</span>
{"\n"}
</span>
);
})}
{details.map((header) => {
if (!header.includes(':')) {
console.warn(`Malformed detail string: ${header}`);
return (
<span key={header}>
<span className="text-content/65">{header}</span>
{"\n"}
</span>
);
}
const [key, ...valueParts] = header.split(":");
const value = valueParts.join(":").trim();
return (
<span key={header}>
<span className="text-content/65 capitalize">{key}</span>
<span className="text-content whitespace-pre-line">
: {value}
</span>
{"\n"}
</span>
);
})}

Comment on lines +83 to +91
function flattenObject(obj: object, prefix = ""): string[] {
return Object.entries(obj).flatMap(([key, value]) => {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
return flattenObject(value, newKey);
}
return `${newKey}:${value}`;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety in flattenObject utility

The function needs better type safety and null checks.

-function flattenObject(obj: object, prefix = ""): string[] {
+function flattenObject(obj: Record<string, unknown>, prefix = ""): string[] {
+  if (typeof obj !== 'object' || obj === null) {
+    throw new TypeError('Expected non-null object');
+  }
   return Object.entries(obj).flatMap(([key, value]) => {
     const newKey = prefix ? `${prefix}.${key}` : key;
-    if (typeof value === "object" && value !== null) {
-      return flattenObject(value, newKey);
+    if (value === null) {
+      return `${newKey}:null`;
+    }
+    if (typeof value === "object") {
+      return flattenObject(value as Record<string, unknown>, newKey);
     }
     return `${newKey}:${value}`;
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function flattenObject(obj: object, prefix = ""): string[] {
return Object.entries(obj).flatMap(([key, value]) => {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
return flattenObject(value, newKey);
}
return `${newKey}:${value}`;
});
}
function flattenObject(obj: Record<string, unknown>, prefix = ""): string[] {
if (typeof obj !== 'object' || obj === null) {
throw new TypeError('Expected non-null object');
}
return Object.entries(obj).flatMap(([key, value]) => {
const newKey = prefix ? `${prefix}.${key}` : key;
if (value === null) {
return `${newKey}:null`;
}
if (typeof value === "object") {
return flattenObject(value as Record<string, unknown>, newKey);
}
return `${newKey}:${value}`;
});
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (5)
apps/dashboard/app/(app)/logs/query-state.ts (1)

13-13: Consider exporting TIMELINE_OPTIONS constant

The constant is used to define the exported Timeline type but isn't exported itself. This could limit reusability if other components need access to the timeline options.

-const TIMELINE_OPTIONS = ["1h", "3h", "6h", "12h", "24h"] as const;
+export const TIMELINE_OPTIONS = ["1h", "3h", "6h", "12h", "24h"] as const;
apps/dashboard/app/(app)/logs/components/chart.tsx (1)

150-153: Add block statements for better readability

Following the static analysis suggestion, wrap single-line if statements in blocks for consistency and maintainability.

-      if (status >= 200 && status < 300) bucket.success++;
-      else if (status >= 400 && status < 500) bucket.warning++;
-      else if (status >= 500) bucket.error++;
+      if (status >= 200 && status < 300) {
+        bucket.success++;
+      } else if (status >= 400 && status < 500) {
+        bucket.warning++;
+      } else if (status >= 500) {
+        bucket.error++;
+      }
🧰 Tools
🪛 Biome (1.9.4)

[error] 150-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 151-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)


[error] 152-152: Block statements are preferred in this position.

Unsafe fix: Wrap the statement with a JsBlockStatement

(lint/style/useBlockStatements)

apps/dashboard/app/(app)/logs/components/log-details/components/log-meta.tsx (1)

3-14: Add prop validation and improve responsive design

Consider these improvements:

  1. Add prop validation to ensure content is always a string
  2. Make the width responsive instead of fixed
-export const LogMetaSection = ({ content }: { content: string }) => {
+import { type FC } from 'react';
+
+interface LogMetaSectionProps {
+  content: string;
+}
+
+export const LogMetaSection: FC<LogMetaSectionProps> = ({ content }) => {
   return (
     <div className="flex justify-between pt-2.5 px-3">
       <div className="text-sm text-content/65 font-sans">Meta</div>
       <Card className="rounded-[5px] flex">
-        <CardContent className="text-[12px] w-[300px] flex-2 bg-background-subtle p-3">
+        <CardContent className="text-[12px] w-full min-w-[300px] max-w-[500px] flex-2 bg-background-subtle p-3">
           <pre>{content}</pre>
         </CardContent>
       </Card>
apps/dashboard/app/(app)/logs/components/log-details/index.tsx (2)

22-28: Consider using ResizeObserver instead of debounce

The current debounce implementation might cause jank during resizing.

-const PANEL_WIDTH_SET_DELAY = 150;
+import { useResizeObserver } from '@/hooks/use-resize-observer';

 const _LogDetails = ({ log, onClose, distanceToTop }: Props) => {
-  const [panelWidth, setPanelWidth] = useState(DEFAULT_DRAGGABLE_WIDTH);
-
-  const debouncedSetPanelWidth = useDebounceCallback((newWidth) => {
-    setPanelWidth(newWidth);
-  }, PANEL_WIDTH_SET_DELAY);
+  const { ref, width } = useResizeObserver<HTMLDivElement>();
+  const panelWidth = width ?? DEFAULT_DRAGGABLE_WIDTH;

52-54: Remove unnecessary div and negative margin

The empty div with negative margin seems to be a hack.

 <div className="space-y-3 border-b-[1px] border-border py-4">
-  <div className="mt-[-24px]" />
   <LogSection details={log.request_headers} title="Request Header" />

Consider adjusting the padding of the parent container instead if more space is needed.

🛑 Comments failed to post (4)
apps/dashboard/app/(app)/logs/components/chart.tsx (2)

141-141: ⚠️ Potential issue

Remove console.log statement

Remove debugging console.log statement before deploying to production.

-  console.log(buckets);

94-106: ⚠️ Potential issue

Fix inconsistency in color variable usage

The Bar components use different color variables (--color-success) than those defined in chartConfig (--chart-3). This could lead to inconsistent styling.

-          fill="var(--color-success)"
+          fill={chartConfig.success.color}
-          fill="var(--color-warning)"
+          fill={chartConfig.warning.color}
-          fill="var(--color-error)"
+          fill={chartConfig.error.color}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        <Bar
          dataKey="success"
          stackId="a"
          fill={chartConfig.success.color}
          radius={3}
        />
        <Bar
          dataKey="warning"
          stackId="a"
          fill={chartConfig.warning.color}
          radius={3}
        />
        <Bar dataKey="error" stackId="a" fill={chartConfig.error.color} radius={3} />
apps/dashboard/app/(app)/logs/components/log-details/components/log-section.tsx (1)

16-28: ⚠️ Potential issue

Add error handling for malformed detail strings

The current implementation assumes all detail strings contain a colon separator. Add error handling to gracefully handle malformed inputs.

 {details.map((header) => {
+  if (!header.includes(':')) {
+    console.warn(`Malformed detail string: ${header}`);
+    return (
+      <span key={header}>
+        <span className="text-content/65">{header}</span>
+        {"\n"}
+      </span>
+    );
+  }
   const [key, ...valueParts] = header.split(":");
   const value = valueParts.join(":").trim();
   return (
     <span key={header}>
       <span className="text-content/65 capitalize">{key}</span>
       <span className="text-content whitespace-pre-line">
         : {value}
       </span>
       {"\n"}
     </span>
   );
 })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            {details.map((header) => {
              if (!header.includes(':')) {
                console.warn(`Malformed detail string: ${header}`);
                return (
                  <span key={header}>
                    <span className="text-content/65">{header}</span>
                    {"\n"}
                  </span>
                );
              }
              const [key, ...valueParts] = header.split(":");
              const value = valueParts.join(":").trim();
              return (
                <span key={header}>
                  <span className="text-content/65 capitalize">{key}</span>
                  <span className="text-content whitespace-pre-line">
                    : {value}
                  </span>
                  {"\n"}
                </span>
              );
            })}
apps/dashboard/app/(app)/logs/components/log-details/index.tsx (1)

83-91: 🛠️ Refactor suggestion

Improve type safety in flattenObject utility

The function needs better type safety and null checks.

-function flattenObject(obj: object, prefix = ""): string[] {
+function flattenObject(obj: Record<string, unknown>, prefix = ""): string[] {
+  if (typeof obj !== 'object' || obj === null) {
+    throw new TypeError('Expected non-null object');
+  }
   return Object.entries(obj).flatMap(([key, value]) => {
     const newKey = prefix ? `${prefix}.${key}` : key;
-    if (typeof value === "object" && value !== null) {
-      return flattenObject(value, newKey);
+    if (value === null) {
+      return `${newKey}:null`;
+    }
+    if (typeof value === "object") {
+      return flattenObject(value as Record<string, unknown>, newKey);
     }
     return `${newKey}:${value}`;
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

function flattenObject(obj: Record<string, unknown>, prefix = ""): string[] {
  if (typeof obj !== 'object' || obj === null) {
    throw new TypeError('Expected non-null object');
  }
  return Object.entries(obj).flatMap(([key, value]) => {
    const newKey = prefix ? `${prefix}.${key}` : key;
    if (value === null) {
      return `${newKey}:null`;
    }
    if (typeof value === "object") {
      return flattenObject(value as Record<string, unknown>, newKey);
    }
    return `${newKey}:${value}`;
  });
}

@ogzhanolguncu
Copy link
Contributor Author

Moved to here due to too many lock conflicts -> https://github.com/unkeyed/unkey/compare/logs-page-v2

@coderabbitai coderabbitai bot mentioned this pull request Dec 4, 2024
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants