Skip to content

Conversation

@sean-zlai
Copy link
Contributor

@sean-zlai sean-zlai commented Dec 19, 2024

Summary

  • New class-based Api interface
    • Support passing custom fetch to allow relative URLs from SvelteKit load() functions
    • Improve response parsing
      • Better error handling
      • TODO: Detect date strings and create Date instances (using parse from @layerstack/utils)
    • Enable setting other api context in the future, such as Authorization header for access tokens
  • Proxy all calls through frontend server
    • Examples:
      • http://localhost:5173/api/* => http://localhost:9000/api/v1/*
      • http://app.zipline.ai/api/* => http://app.zipline.ai:9000/api/v1/*
    • Resolves
      • CORS
      • Consistent URL handling whether issuing requests from browser (ex. +page.svelte) or frontend server (ex +page.server.ts)

Test results

image

Checklist

  • Added Unit Tests
  • Covered by existing CI
  • Integration tested
  • Documentation update

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Enhanced installation instructions in the README, including the creation of a .env file for configuration.
    • Introduced a proxy for API requests to handle CORS issues.
  • Improvements

    • Refactored API handling to use a new Api class, improving organization and encapsulation of API interactions.
    • Updated search functionality in the NavigationBar to utilize the new API class.
    • Enhanced error handling during data fetching across various components.
  • Documentation

    • Clarified setup details in the README file.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 19, 2024

Walkthrough

This pull request introduces significant refactoring of the frontend API interaction and configuration management. Key changes include the creation of a new Api class to encapsulate API requests, enhancements to the README with detailed installation instructions, and the implementation of a server-side proxy for API calls. The modifications improve the structure of API interactions across multiple components, transitioning to a more object-oriented approach for handling network requests and refining the overall setup documentation.

Changes

File Change Summary
frontend/README.md Added instructions to create a .env file with API_BASE_URL configuration.
frontend/src/lib/api/api.ts Introduced Api class with methods for API requests, replacing standalone functions.
frontend/src/routes/api/[...path]/+server.ts New proxy server to handle API requests, addressing CORS issues.
frontend/src/routes/joins/+page.server.ts Updated API interaction to use new Api class instance with fetch parameter.
frontend/src/routes/joins/[slug]/+page.server.ts Updated to instantiate Api class with fetch parameter for API calls.
frontend/src/routes/joins/[slug]/+page.svelte Refactored to use api.getFeatureTimeseries method from Api class.
frontend/src/lib/components/NavigationBar/NavigationBar.svelte Refactored search functionality to use Api class instance.
frontend/src/lib/api/api.test.ts Updated tests to instantiate Api class and use its methods.
frontend/src/lib/types/Model/Model.test.ts Changed to instantiate Api class for API method calls in tests.

Possibly related PRs

  • Frontend: Build, wireframe, config #26: This PR modifies the .gitignore file to manage environment files, which is relevant to the main PR's addition of instructions for creating a .env file.
  • Frontend tests #32: The updates to the frontend/README.md in this PR include instructions for running tests, which aligns with the main PR's focus on clarifying setup instructions.
  • Frontend update model type #49: Changes to the Model type in this PR may indirectly relate to the main PR's focus on API interactions, as both involve the API structure and usage.
  • update some readme stuff #114: The updates to the frontend/README.md in this PR enhance clarity and provide additional instructions, which is consistent with the main PR's goal of improving documentation for setup and configuration.

Suggested reviewers

  • ken-zlai

Poem

🐰 A Rabbit's API Tale 🌐

In code's grand dance, a new class springs,
Api's embrace, how elegantly it swings!
From functions loose to methods tight,
Requests now flow with pure delight.
A proxy here, a method there,
Frontend magic beyond compare! 🚀

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord. This feature will be included in our new Pro Plan when released, so please consider upgrading your current plan.


🪧 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 generate docstrings to generate docstrings for this PR. (Beta)
  • @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.

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: 3

🔭 Outside diff range comments (3)
frontend/src/lib/components/NavigationBar/NavigationBar.svelte (1)

Line range hint 52-57: Add error handling for the search API call.

The search functionality should handle potential API errors gracefully to prevent the UI from breaking and provide feedback to users when the search fails.

 const debouncedSearch = debounce(async () => {
   if (input.length > 0) {
-    const response = await api.search(input);
-    searchResults = response.items;
+    try {
+      const response = await api.search(input);
+      searchResults = response.items;
+    } catch (error) {
+      console.error('Error during search:', error);
+      searchResults = [];
+      // TODO: Consider showing an error message to the user
+    }
   } else {
     searchResults = [];
   }
 }, 300);
frontend/src/routes/joins/[slug]/+page.svelte (2)

Line range hint 204-214: Refactor duplicate API parameters into a reusable configuration.

The API calls share many common parameters. Consider extracting these into a base configuration object to reduce duplication and improve maintainability.

// Create a base configuration
const baseTimeseriesConfig = {
  joinId: joinTimeseries.name,
  startTs: data.dateRange.startTimestamp,
  endTs: data.dateRange.endTimestamp,
  metricType: 'drift',
  offset: '1D',
  algorithm: 'psi',
  granularity: 'percentile'
};

// Use it in API calls
const featureData = await api.getFeatureTimeseries({
  ...baseTimeseriesConfig,
  featureName: seriesName,
  metrics: 'value'
});

Also applies to: 215-225, 424-434


Line range hint 424-442: Add error handling for individual distribution fetches.

The distribution loading could fail for individual features while others succeed. Consider handling errors for each feature separately to prevent a single failure from affecting all distributions.

 const distributionsPromises = allFeatures.map((featureName) =>
-  api.getFeatureTimeseries({
+  api.getFeatureTimeseries({
     ...baseTimeseriesConfig,
     featureName,
     metrics: 'value'
-  })
+  }).catch(error => {
+    console.error(`Error loading distribution for ${featureName}:`, error);
+    return null;
+  })
 );

 const responses = await Promise.all(distributionsPromises);
-distributions = responses.filter((response) => response.isNumeric);
+distributions = responses.filter((response): response is FeatureResponse => 
+  response !== null && response.isNumeric
+);
🧹 Nitpick comments (8)
frontend/src/lib/api/api.ts (3)

14-19: Clarify usage of 'data' in ApiRequestOptions
While 'data' is typed broadly as Record<string, any>, it’s used to build both query string parameters (for GET) and request body (for POST). This is convenient but can confuse maintainers. Consider adding a JSDoc comment or a more explicit naming scheme (e.g., queryData vs. bodyData) to improve readability and prevent misusage.


26-29: Validate accessToken usage
The constructor sets accessToken, but it’s only used for the Authorization header in #send. If additional token-based logic is needed in the future, ensure this field is kept in sync or provide direct setter methods for reusability (e.g., token refresh flows).


33-80: Promote naming clarity and parameter coverage
The methods getModels, getJoins, search, and getJoinTimeseries follow a clear pattern. Here are some suggestions:
• For getJoins, offset and limit are numeric, so the usage of URLSearchParams is appropriate. Consider validating offset and limit to prevent negative values or unexpected large inputs.
• search method uses a single "term" param. If the system eventually supports advanced searches with more parameters, you might want a typed object as input.
• getJoinTimeseries uses default offsets and metrics. Consider clarifying in docstrings or comments what “null” or '10h' implies for the domain.

frontend/src/routes/api/[...path]/+server.ts (2)

4-8: Document the proxy approach
Your docstring is good at explaining the purpose of the proxy (CORS, consistent URL). Consider referencing the new Api class in comments so future contributors can quickly see how everything ties together.


9-11: Handle non-GET/POST methods
Currently, only GET and POST are implemented. In case you need PUT, PATCH, or DELETE in the future, consider either rejecting them explicitly or adding fallback logic. This helps avoid 404 confusion or inconsistent behavior.

Also applies to: 13-15

frontend/src/routes/joins/[slug]/+page.server.ts (1)

26-26: Validate fallback logic
If the user-provided date range is invalid, you catch the error and revert to fallback timestamps (Jan-Mar 2023). That’s a good fallback approach. Consider whether you need to document or display a “Using fallback data range” warning to the UI.

frontend/README.md (1)

35-37: Add language specifier to the code block.

The code block for the .env file content should specify a language for better syntax highlighting and documentation consistency.

-   ```
+   ```env
    API_BASE_URL=http://localhost:9000
    ```
🧰 Tools
🪛 Markdownlint (0.37.0)

35-35: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

frontend/src/routes/joins/[slug]/+page.svelte (1)

Line range hint 424-442: Consider implementing request batching for performance optimization.

Loading distributions triggers multiple parallel API requests. Consider implementing a batched API endpoint to reduce network overhead.

Would you like me to help design a batched API endpoint that can handle multiple feature requests in a single call?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 2b7efd8 and 1253406.

📒 Files selected for processing (7)
  • frontend/README.md (1 hunks)
  • frontend/src/lib/api/api.ts (1 hunks)
  • frontend/src/lib/components/NavigationBar/NavigationBar.svelte (2 hunks)
  • frontend/src/routes/api/[...path]/+server.ts (1 hunks)
  • frontend/src/routes/joins/+page.server.ts (1 hunks)
  • frontend/src/routes/joins/[slug]/+page.server.ts (6 hunks)
  • frontend/src/routes/joins/[slug]/+page.svelte (5 hunks)
🧰 Additional context used
🪛 Markdownlint (0.37.0)
frontend/README.md

35-35: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (8)
frontend/src/lib/api/api.ts (2)

8-12: Ensure consistent base-URL handling
Currently, the optional 'base' property imposes no requirement for a trailing slash. If the user of this API sets a base without a trailing slash, subsequent path concatenations may lead to malformed URLs (e.g. "https://example.comapi/v1"). Consider normalizing the string here (e.g., adding or trimming trailing slashes) to avoid potential errors and improve developer UX.

Would you like to verify usage of this property across the codebase to ensure consistent usage of slashes?


21-24: Private class fields and encapsulation
Using private fields (#base, #fetch, #accessToken) is a neat approach to encapsulate your implementation details. Ensure you have test coverage and that no other parts of your code need direct read access.

frontend/src/routes/joins/+page.server.ts (2)

3-3: Transition to class-based API usage
Replacing individual function imports with { Api } fosters a more standardized approach across the codebase. This is a good maintainability improvement.


5-5: Ensure 'fetch' usage is properly tested
Using fetch from SvelteKit’s server context is convenient, but it may differ from the browser’s fetch, especially around credentials or SSR environment. Confirm that your test coverage includes these aspects (CORS, SSR fetch differences, etc.).

Also applies to: 8-8

frontend/src/routes/api/[...path]/+server.ts (1)

1-2: Check for environment variable consistency
You rely on env.API_BASE_URL. Validate that it’s set in all deployment target environments to avoid runtime errors.

frontend/src/routes/joins/[slug]/+page.server.ts (3)

2-2: Consistent usage of type imports
Importing { Api } from '$lib/api/api' is consistent with your updated approach. Ensure references to the older version of the API do not linger in the codebase.


13-14: Pass 'fetch' for SSR
By passing the fetch parameter, you ensure SSR compatibility. Good job. Verify that the same approach is used consistently in other load functions.


79-79: fetchInitialData must handle concurrency
Promise.all is a great approach for concurrent requests. Make sure to handle partial failures: if one request fails (e.g., getModels), do you want to continue using partial data from getJoinTimeseries?

Comment on lines 82 to 116
async getFeatureTimeseries({
joinId,
featureName,
startTs,
endTs,
metricType = 'drift',
metrics = 'null',
offset = '10h',
algorithm = 'psi',
granularity = 'aggregates'
}: {
joinId: string;
featureName: string;
startTs: number;
endTs: number;
metricType?: string;
metrics?: string;
offset?: string;
algorithm?: string;
granularity?: string;
}): Promise<FeatureResponse> {
const params = new URLSearchParams({
startTs: startTs.toString(),
endTs: endTs.toString(),
metricType,
metrics,
offset,
algorithm,
granularity
});
return this.#send(`join/${joinId}/feature/${featureName}/timeseries?${params.toString()}`);
}
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

Add validations and exceptions
getFeatureTimeseries is quite similar to getJoinTimeseries. You might consider factoring out common logic (e.g., forming query parameters) for reusability. Also, ensure that featureName is validated before constructing the final URL to avoid confusing API errors if an invalid or empty featureName is passed.

Comment on lines 115 to 154
async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
let url = `${this.#base}/${resource}`;

export async function getFeatureTimeseries({
joinId,
featureName,
startTs,
endTs,
metricType = 'drift',
metrics = 'null',
offset = '10h',
algorithm = 'psi',
granularity = 'aggregates'
}: {
joinId: string;
featureName: string;
startTs: number;
endTs: number;
metricType?: string;
metrics?: string;
offset?: string;
algorithm?: string;
granularity?: string;
}): Promise<FeatureResponse> {
const params = new URLSearchParams({
startTs: startTs.toString(),
endTs: endTs.toString(),
metricType,
metrics,
offset,
algorithm,
granularity
});
return get(`join/${joinId}/feature/${featureName}/timeseries?${params.toString()}`);
const method = options?.method ?? 'GET';

if (method === 'GET' && options?.data) {
url += `?${new URLSearchParams(options.data)}`;
}

return this.#fetch(url, {
method: options?.method ?? 'GET',
headers: {
'Content-Type': 'application/json',
...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
},
...(method === 'POST' &&
options?.data && {
body: JSON.stringify(options.data)
})
}).then(async (response) => {
const text = await response.text();
try {
return JSON.parse(text) as Data;
} catch (e) {
console.log(`Unable to parse: "${text}" for url: ${url}`);
throw e;
}
});
}
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

Augment error handling for #send
The #send method currently logs JSON parse errors but does not handle HTTP errors (4xx/5xx). Consider:
• Raising an explicit error if response.ok is false.
• Providing better fallback or retry logic.
• Exposing error messages from the response body to help with debugging.

Also, keep in mind that many requests can fail for network reasons before receiving any response. Consider implementing a try/catch around fetch calls.

// Try with requested date range first
try {
const { joinTimeseries, model } = await fetchInitialData(
api,
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

Consolidate error handling
You have a try/catch block that logs an error, then calls fetchInitialData again. Ensure you consistently handle or log the underlying cause (e.g., 404 vs. network error). Otherwise, you might mask repeated issues.

Also applies to: 56-56

@sean-zlai sean-zlai requested a review from ken-zlai December 19, 2024 19:53
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: 0

♻️ Duplicate comments (1)
frontend/src/lib/api/api.ts (1)

54-116: 🛠️ Refactor suggestion

Refactor duplicate code in timeseries methods.

The getJoinTimeseries and getFeatureTimeseries methods share significant common code. Consider:

  1. Creating a shared type for common parameters
  2. Extracting the common URL parameters logic
+type TimeseriesParams = {
+  startTs: number;
+  endTs: number;
+  metricType?: string;
+  metrics?: string;
+  offset?: string;
+  algorithm?: string;
+};

+private createTimeseriesParams(params: TimeseriesParams): URLSearchParams {
+  return new URLSearchParams({
+    startTs: params.startTs.toString(),
+    endTs: params.endTs.toString(),
+    metricType: params.metricType ?? 'drift',
+    metrics: params.metrics ?? 'null',
+    offset: params.offset ?? '10h',
+    algorithm: params.algorithm ?? 'psi'
+  });
+}
🧹 Nitpick comments (6)
frontend/src/lib/api/api.ts (4)

9-20: LGTM! Consider adding JSDoc comments for better documentation.

The type definitions are well-structured and provide good flexibility. Consider adding JSDoc comments to document the purpose and constraints of each type and its properties.

+/** Options for configuring the Api instance */
 export type ApiOptions = {
+  /** Base URL for API requests. Defaults to '/api/v1' */
   base?: string;
+  /** Custom fetch implementation. Defaults to global fetch */
   fetch?: typeof fetch;
+  /** Optional access token for authentication */
   accessToken?: string;
 };

33-34: Address the TODO comment about project structure.

Consider creating a dedicated models API client class that extends the base Api class. This would improve code organization and maintainability.

Would you like me to help create a proposal for the model-specific API client structure?


38-44: Add parameter validation for pagination.

Consider adding validation for offset and limit parameters to ensure they are non-negative numbers.

 async getJoins(offset: number = 0, limit: number = 10) {
+  if (offset < 0 || limit <= 0) {
+    throw new Error('Offset must be non-negative and limit must be positive');
+  }
   const params = new URLSearchParams({
     offset: offset.toString(),
     limit: limit.toString()
   });
   return this.#send<JoinsResponse>(`joins?${params.toString()}`);
 }

46-52: Add input validation for search parameters.

The search term should be validated to ensure it's not empty or just whitespace. Also, consider consistent validation with getJoins for the limit parameter.

 async search(term: string, limit: number = 20) {
+  if (!term?.trim()) {
+    throw new Error('Search term cannot be empty');
+  }
+  if (limit <= 0) {
+    throw new Error('Limit must be positive');
+  }
   const params = new URLSearchParams({
     term,
     limit: limit.toString()
   });
   return this.#send<ModelsResponse>(`search?${params.toString()}`);
 }
frontend/src/lib/api/api.test.ts (1)

Line range hint 1-65: Enhance test coverage for API module.

Consider adding tests for:

  1. Timeout scenarios
  2. Network failures
  3. Invalid JSON responses
  4. Different HTTP methods (POST, PUT, DELETE)
  5. Authorization header handling

Example test cases:

it('should handle request timeout', async () => {
  mockFetch.mockImplementationOnce(() => new Promise(() => {}));
  await expect(api.getModels()).rejects.toThrow('Request timeout');
});

it('should handle network errors', async () => {
  mockFetch.mockRejectedValueOnce(new Error('Network failure'));
  await expect(api.getModels()).rejects.toThrow('Network failure');
});
frontend/src/lib/types/Model/Model.test.ts (1)

Line range hint 1-200: LGTM! Consider enhancing warning logs.

The tests are comprehensive and well-structured. Consider:

  1. Using a structured logging format for warnings
  2. Adding assertions for the warning messages in tests
-console.warn(`Additional fields found in ModelsResponse: ${additionalKeys.join(', ')}`);
+console.warn(JSON.stringify({
+  message: 'Additional fields found in response',
+  type: 'ModelsResponse',
+  fields: additionalKeys
+}));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 1253406 and 13009c7.

📒 Files selected for processing (3)
  • frontend/src/lib/api/api.test.ts (4 hunks)
  • frontend/src/lib/api/api.ts (1 hunks)
  • frontend/src/lib/types/Model/Model.test.ts (1 hunks)
🔇 Additional comments (1)
frontend/src/lib/api/api.ts (1)

118-154: ⚠️ Potential issue

Enhance error handling in the #send method.

The current error handling could be improved in several ways:

  1. Add request timeout handling
  2. Implement retry logic for transient failures
  3. Provide more detailed error information
  4. Add proper error logging instead of console.log
 async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
+  const timeout = options?.timeout ?? 30000;
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), timeout);
+
   let url = `${this.#base}/${resource}`;
   const method = options?.method ?? 'GET';

   if (method === 'GET' && options?.data) {
     url += `?${new URLSearchParams(options.data)}`;
   }

-  return this.#fetch(url, {
-    method: options?.method ?? 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-      ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
-    },
-    ...(method === 'POST' &&
-      options?.data && {
-        body: JSON.stringify(options.data)
-      })
-  }).then(async (response) => {
+  try {
+    const response = await this.#fetch(url, {
+      method: options?.method ?? 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
+      },
+      signal: controller.signal,
+      ...(method === 'POST' &&
+        options?.data && {
+          body: JSON.stringify(options.data)
+        })
+    });

     if (response.ok) {
       const text = await response.text();
       try {
         if (text) {
           return JSON.parse(text) as Data;
         } else {
           return {};
         }
       } catch (e) {
-        console.log(`Unable to parse: "${text}" for url: ${url}`);
-        throw e;
+        throw new Error(`Failed to parse response: ${e.message}`);
       }
     } else {
-      error(response.status);
+      const errorBody = await response.text();
+      throw new Error(`HTTP ${response.status}: ${errorBody}`);
     }
-  });
+  } catch (e) {
+    if (e.name === 'AbortError') {
+      throw new Error(`Request timeout after ${timeout}ms`);
+    }
+    throw e;
+  } finally {
+    clearTimeout(timeoutId);
+  }
 }

Likely invalid or redundant comment.

@sean-zlai
Copy link
Contributor Author

@ken-zlai based on our discussion earlier, and should replace the need for #141 (and possibly https://github.com/zipline-ai/infrastructure/pull/5)

The api diff is easier to view if you enable "Hide whitespace"

image

@sean-zlai sean-zlai changed the title New class-based Api interface and proxy New class-based Api interface and frontend proxy Dec 19, 2024
@ken-zlai
Copy link
Contributor

@sean-zlai Very crafty, I like this approach quite a bit.

@chewy-zlai Since we proxy all api requests thru our node server, we shouldn't need any sort of public load balanced URL. Apologies since you already implemented that in the infra repo. Having that said, we can test this PR out by building and deploying this (sean/api-proxy) branch.

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: 3

🧹 Nitpick comments (1)
frontend/src/routes/api/[...path]/+server.ts (1)

6-10: Enhance documentation with security considerations

While the current documentation explains the proxy's purpose well, it should include:

  1. Security implications and limitations
  2. Expected request/response format
  3. Error handling behavior

Consider expanding the documentation:

 /**
  * Proxy calls from frontend server (ex. `http://app.zipline.ai/api`) to Scala backend (ex. `http://app:9000/api`), resolving:
  *  - CORS issues
  *  - Consistent URL handling whether issuing requests from browser (ex. `+page.svelte`) or frontend server (ex `+page.server.ts`)
+ *
+ * Security considerations:
+ *  - Requests are proxied as-is, maintain validation in the backend
+ *  - Only whitelisted API endpoints should be accessible
+ * 
+ * Error handling:
+ *  - Backend errors are passed through unchanged
+ *  - Network errors will be thrown as-is
  */
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 13009c7 and 3e85046.

📒 Files selected for processing (1)
  • frontend/src/routes/api/[...path]/+server.ts (1 hunks)
🔇 Additional comments (1)
frontend/src/routes/api/[...path]/+server.ts (1)

1-17: Verify HTTP method support and content type handling

The current implementation might need to support additional HTTP methods (PUT, DELETE, etc.) and handle various content types properly.

Let's verify the API requirements:

✅ Verification successful

Let me gather more information about the API usage patterns to make a comprehensive verification.


The proxy implementation is sufficient for current API requirements

The implementation is correctly handling all necessary HTTP methods. Here's why:

  • The proxy server passes through the original request object which preserves all HTTP methods, headers, and content types
  • The API client (frontend/src/lib/api/api.ts) shows that only GET and POST methods are currently used in the application
  • Content-Type handling is properly implemented in the API client with 'application/json'
  • The proxy's design allows for transparent forwarding of any future HTTP methods without modification
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for HTTP method usage in the codebase
echo "Checking HTTP method usage in the frontend code..."
rg -i "method: ['\"](?:GET|POST|PUT|DELETE|PATCH)" --type ts

# Check for content type handling
echo "Checking content type handling..."
rg -i "content-type" --type ts

# Check for existing API endpoints
echo "Checking API endpoint patterns..."
rg -i "fetch\(.*?/api/" --type ts

Length of output: 829


Script:

#!/bin/bash
# Check for all HTTP method handlers in the API routes
echo "Checking HTTP method handlers in API routes..."
ast-grep --pattern 'export const $METHOD: RequestHandler = $_'

# Check the API client implementation
echo "Checking API client implementation..."
cat frontend/src/lib/api/api.ts

# Check API tests
echo "Checking API tests..."
cat frontend/src/lib/api/api.test.ts

Length of output: 5972

Comment on lines +1 to +4
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';

const API_BASE_URL = env.API_BASE_URL ?? 'http://localhost:9000';
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

Add validation for API_BASE_URL environment variable

The current implementation could benefit from additional validation and type safety:

  1. Validate the URL format
  2. Ensure HTTPS in production
  3. Add type safety for the environment variable

Consider applying this improvement:

 import { env } from '$env/dynamic/private';
 import type { RequestHandler } from './$types';
+import { z } from 'zod';
+
+const ApiBaseUrlSchema = z.string().url();
+const validateApiBaseUrl = (url: string) => {
+  const parsed = ApiBaseUrlSchema.parse(url);
+  if (process.env.NODE_ENV === 'production' && !parsed.startsWith('https://')) {
+    throw new Error('API_BASE_URL must use HTTPS in production');
+  }
+  return parsed;
+};
 
-const API_BASE_URL = env.API_BASE_URL ?? 'http://localhost:9000';
+const API_BASE_URL = validateApiBaseUrl(env.API_BASE_URL ?? 'http://localhost:9000');
📝 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
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
const API_BASE_URL = env.API_BASE_URL ?? 'http://localhost:9000';
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
import { z } from 'zod';
const ApiBaseUrlSchema = z.string().url();
const validateApiBaseUrl = (url: string) => {
const parsed = ApiBaseUrlSchema.parse(url);
if (process.env.NODE_ENV === 'production' && !parsed.startsWith('https://')) {
throw new Error('API_BASE_URL must use HTTPS in production');
}
return parsed;
};
const API_BASE_URL = validateApiBaseUrl(env.API_BASE_URL ?? 'http://localhost:9000');

Comment on lines +11 to +13
export const GET: RequestHandler = ({ params, url, request }) => {
return fetch(`${API_BASE_URL}/api/${params.path + url.search}`, request);
};
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 and request validation

The current implementation needs improvements in several areas:

  1. Path parameter validation
  2. Error handling
  3. Request timeout
  4. Response transformation

Consider this safer implementation:

-export const GET: RequestHandler = ({ params, url, request }) => {
-	return fetch(`${API_BASE_URL}/api/${params.path + url.search}`, request);
-};
+export const GET: RequestHandler = async ({ params, url, request }) => {
+	try {
+		// Validate path to prevent directory traversal
+		if (params.path.includes('..')) {
+			return new Response('Invalid path', { status: 400 });
+		}
+
+		const response = await Promise.race([
+			fetch(`${API_BASE_URL}/api/${params.path + url.search}`, {
+				...request,
+				signal: AbortSignal.timeout(30000) // 30s timeout
+			}),
+			new Promise((_, reject) => 
+				setTimeout(() => reject(new Error('Request timeout')), 30000)
+			)
+		]);
+
+		// Optionally transform response
+		const data = await response.json();
+		return new Response(JSON.stringify(data), {
+			status: response.status,
+			headers: {
+				'Content-Type': 'application/json'
+			}
+		});
+	} catch (error) {
+		console.error('Proxy error:', error);
+		return new Response('Internal Server Error', { status: 500 });
+	}
+};
📝 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
export const GET: RequestHandler = ({ params, url, request }) => {
return fetch(`${API_BASE_URL}/api/${params.path + url.search}`, request);
};
export const GET: RequestHandler = async ({ params, url, request }) => {
try {
// Validate path to prevent directory traversal
if (params.path.includes('..')) {
return new Response('Invalid path', { status: 400 });
}
const response = await Promise.race([
fetch(`${API_BASE_URL}/api/${params.path + url.search}`, {
...request,
signal: AbortSignal.timeout(30000) // 30s timeout
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 30000)
)
]);
// Optionally transform response
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Proxy error:', error);
return new Response('Internal Server Error', { status: 500 });
}
};

Comment on lines +15 to +17
export const POST: RequestHandler = ({ params, url, request }) => {
return fetch(`${API_BASE_URL}/api/${params.path + url.search}`, request);
};
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

Refactor to eliminate code duplication

The POST handler duplicates the GET handler's code. Consider extracting the common logic into a shared function.

Here's a suggested refactor:

+async function proxyRequest(request: Request, params: { path: string }, url: URL) {
+	try {
+		if (params.path.includes('..')) {
+			return new Response('Invalid path', { status: 400 });
+		}
+
+		const response = await Promise.race([
+			fetch(`${API_BASE_URL}/api/${params.path + url.search}`, {
+				...request,
+				signal: AbortSignal.timeout(30000)
+			}),
+			new Promise((_, reject) => 
+				setTimeout(() => reject(new Error('Request timeout')), 30000)
+			)
+		]);
+
+		const data = await response.json();
+		return new Response(JSON.stringify(data), {
+			status: response.status,
+			headers: {
+				'Content-Type': 'application/json'
+			}
+		});
+	} catch (error) {
+		console.error('Proxy error:', error);
+		return new Response('Internal Server Error', { status: 500 });
+	}
+}
+
-export const POST: RequestHandler = ({ params, url, request }) => {
-	return fetch(`${API_BASE_URL}/api/${params.path + url.search}`, request);
-};
+export const GET: RequestHandler = (event) => proxyRequest(event.request, event.params, event.url);
+export const POST: RequestHandler = (event) => proxyRequest(event.request, event.params, event.url);
📝 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
export const POST: RequestHandler = ({ params, url, request }) => {
return fetch(`${API_BASE_URL}/api/${params.path + url.search}`, request);
};
async function proxyRequest(request: Request, params: { path: string }, url: URL) {
try {
if (params.path.includes('..')) {
return new Response('Invalid path', { status: 400 });
}
const response = await Promise.race([
fetch(`${API_BASE_URL}/api/${params.path + url.search}`, {
...request,
signal: AbortSignal.timeout(30000)
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 30000)
)
]);
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Proxy error:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
export const GET: RequestHandler = (event) => proxyRequest(event.request, event.params, event.url);
export const POST: RequestHandler = (event) => proxyRequest(event.request, event.params, event.url);

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: 0

🧹 Nitpick comments (8)
frontend/src/lib/api/api.test.ts (1)

35-39: Enhance test coverage for API interface.

While the current tests cover basic GET functionality, consider adding tests for:

  • POST requests with request body
  • Authorization header when accessToken is provided
  • Other API methods (getJoins, search, getJoinTimeseries, getFeatureTimeseries)

Would you like me to help generate additional test cases?

frontend/README.md (1)

33-37: Improve environment configuration documentation.

The .env setup instructions are helpful, but consider:

  1. Mentioning that API_BASE_URL defaults to http://localhost:9000 if not set
  2. Specifying the language for the code block
-   ```
+   ```env
    API_BASE_URL=http://localhost:9000
    ```
🧰 Tools
🪛 Markdownlint (0.37.0)

35-35: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

frontend/src/lib/api/api.ts (3)

33-33: Address TODO comment with concrete plan.

The TODO comment suggests uncertainty about code organization. Consider:

  1. Creating a dedicated models directory under lib/api/
  2. Moving model-specific API calls to lib/api/models/api.ts
  3. Using a facade pattern to expose a unified API interface

Would you like help implementing this architectural improvement?


38-52: Reduce code duplication in API methods.

The URL parameter handling is duplicated across methods. Consider extracting common logic:

private createSearchParams(params: Record<string, string | number>): string {
  const searchParams = new URLSearchParams();
  Object.entries(params).forEach(([key, value]) => {
    searchParams.append(key, value.toString());
  });
  return searchParams.toString();
}

This would simplify the API methods:

async getJoins(offset: number = 0, limit: number = 10) {
  const params = this.createSearchParams({ offset, limit });
  return this.#send<JoinsResponse>(`joins?${params}`);
}

Also applies to: 54-81, 83-116


15-20: Improve type safety of ApiRequestOptions.

The current type allows any value in the data object:

 export type ApiRequestOptions = {
   method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  data?: Record<string, any>;
+  data?: Record<string, string | number | boolean | null>;
   headers?: Record<string, string>;
 };
frontend/src/routes/joins/[slug]/+page.svelte (3)

Line range hint 204-215: Consider extracting common API parameters.

The API calls share several common parameters. Consider extracting these into a configuration object to improve maintainability and reduce duplication.

+const commonParams = {
+  joinId: joinTimeseries.name,
+  startTs: data.dateRange.startTimestamp,
+  endTs: data.dateRange.endTimestamp,
+  metricType: 'drift',
+  algorithm: 'psi',
+  granularity: 'percentile'
+};

 const [featureData, nullFeatureData] = await Promise.all([
   api.getFeatureTimeseries({
-    joinId: joinTimeseries.name,
-    featureName: seriesName,
-    startTs: data.dateRange.startTimestamp,
-    endTs: data.dateRange.endTimestamp,
-    granularity: 'percentile',
-    metricType: 'drift',
-    metrics: 'value',
-    offset: '1D',
-    algorithm: 'psi'
+    ...commonParams,
+    featureName: seriesName,
+    metrics: 'value',
+    offset: '1D'
   }),
   api.getFeatureTimeseries({
-    joinId: joinTimeseries.name,
-    featureName: seriesName,
-    startTs: data.dateRange.startTimestamp,
-    endTs: data.dateRange.endTimestamp,
-    metricType: 'drift',
-    metrics: 'null',
-    offset: '1D',
-    algorithm: 'psi',
-    granularity: 'percentile'
+    ...commonParams,
+    featureName: seriesName,
+    metrics: 'null',
+    offset: '1D'
   })
 ]);

Line range hint 424-434: Enhance error handling in loadDistributions.

While basic error handling is present, consider providing more specific error feedback to users and implementing retry logic for transient failures.

 try {
   const responses = await Promise.all(distributionsPromises);
   distributions = responses.filter((response) => response.isNumeric);
 } catch (error) {
-  console.error('Error loading distributions:', error);
+  console.error('Failed to load feature distributions:', error);
+  // Consider implementing retry logic for transient failures
+  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+  // Consider showing a user-friendly error notification
+  notifications.error(`Failed to load distributions: ${errorMessage}`);
 } finally {
   isLoadingDistributions = false;
 }

Line range hint 1-724: Consider breaking down the component for better maintainability.

The component handles multiple responsibilities and could benefit from being split into smaller, more focused components:

  1. Chart rendering logic could be moved to separate components
  2. Data fetching logic could be extracted into a custom store or hook
  3. Event handling could be centralized in a dedicated service

This would improve maintainability, testability, and reusability.

Example structure:

// stores/featureMonitoring.ts
export const createFeatureMonitoringStore = () => {
  // Data fetching and state management logic
};

// components/charts/FeatureChart.svelte
// Chart rendering logic

// services/chartEventHandler.ts
export class ChartEventHandler {
  // Event handling logic
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 3e85046 and 9f13f9f.

📒 Files selected for processing (9)
  • frontend/README.md (1 hunks)
  • frontend/src/lib/api/api.test.ts (4 hunks)
  • frontend/src/lib/api/api.ts (1 hunks)
  • frontend/src/lib/components/NavigationBar/NavigationBar.svelte (2 hunks)
  • frontend/src/lib/types/Model/Model.test.ts (1 hunks)
  • frontend/src/routes/api/[...path]/+server.ts (1 hunks)
  • frontend/src/routes/joins/+page.server.ts (1 hunks)
  • frontend/src/routes/joins/[slug]/+page.server.ts (6 hunks)
  • frontend/src/routes/joins/[slug]/+page.svelte (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/lib/components/NavigationBar/NavigationBar.svelte
  • frontend/src/routes/joins/+page.server.ts
  • frontend/src/routes/api/[...path]/+server.ts
  • frontend/src/routes/joins/[slug]/+page.server.ts
  • frontend/src/lib/types/Model/Model.test.ts
🧰 Additional context used
🪛 Markdownlint (0.37.0)
frontend/README.md

35-35: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (3)
frontend/src/lib/api/api.test.ts (1)

9-9: LGTM! Api class instantiation is correctly configured.

The test setup properly instantiates the Api class with a mocked fetch function.

frontend/src/lib/api/api.ts (1)

118-154: 🛠️ Refactor suggestion

Enhance error handling in #send method.

The current error handling could be improved:

  1. Network errors aren't explicitly handled
  2. JSON parse errors only log to console
  3. Error responses might contain useful information

Consider this enhanced implementation:

 async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
   let url = `${this.#base}/${resource}`;
   const method = options?.method ?? 'GET';
   
   if (method === 'GET' && options?.data) {
     url += `?${new URLSearchParams(options.data)}`;
   }

-  return this.#fetch(url, {
+  try {
+    const response = await this.#fetch(url, {
     method: options?.method ?? 'GET',
     headers: {
       'Content-Type': 'application/json',
       ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
     },
     ...(method === 'POST' &&
       options?.data && {
         body: JSON.stringify(options.data)
       })
-  }).then(async (response) => {
-    if (response.ok) {
+    });

+    if (response.ok) {
       const text = await response.text();
       try {
         if (text) {
           return JSON.parse(text) as Data;
         } else {
           return {};
         }
       } catch (e) {
-        console.log(`Unable to parse: "${text}" for url: ${url}`);
-        throw e;
+        throw new Error(`Invalid JSON response from ${url}: ${text}`);
       }
     } else {
-      error(response.status);
+      const errorText = await response.text();
+      throw error(response.status, {
+        message: `API error: ${errorText || response.statusText}`
+      });
     }
-  });
+  } catch (e) {
+    if (e instanceof Error) {
+      throw e;
+    }
+    throw new Error(`Network error while fetching ${url}: ${e}`);
+  }
 }

Likely invalid or redundant comment.

frontend/src/routes/joins/[slug]/+page.svelte (1)

19-19: LGTM: Clean API integration.

The integration of the new Api class is clean and follows the singleton pattern appropriately.

Also applies to: 35-35

…d({ fetch })`), improved response parsing, and future improvements (`Authorization` header for access tokens, etc)
… `app:9000`) to fix CORS issues and provide consistent URL handling regardless of env (browser / `+page.svelte`, server load() / `+page.server.ts`, etc)
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: 2

🔭 Outside diff range comments (1)
frontend/src/lib/api/api.test.ts (1)

Line range hint 1-65: Enhance test coverage

While the basic functionality is tested, consider adding tests for:

  1. Authorization header handling
  2. Timeout scenarios
  3. Network errors
  4. Edge cases for parameter validation
it('should include authorization header when token is provided', async () => {
  const api = new Api({ fetch: mockFetch, accessToken: 'test-token' });
  mockFetch.mockResolvedValueOnce({
    ok: true,
    text: () => Promise.resolve('{}')
  });
  
  await api.getModels();
  
  expect(mockFetch).toHaveBeenCalledWith(
    expect.any(String),
    expect.objectContaining({
      headers: expect.objectContaining({
        'Authorization': 'Bearer test-token'
      })
    })
  );
});
♻️ Duplicate comments (1)
frontend/src/lib/api/api.ts (1)

118-154: ⚠️ Potential issue

Enhance error handling in #send method

The current error handling could be improved:

  1. Add timeout handling
  2. Implement retry logic for transient failures
  3. Include response body in error messages
 async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
     let url = `${this.#base}/${resource}`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 30000);

     const method = options?.method ?? 'GET';
     if (method === 'GET' && options?.data) {
         url += `?${new URLSearchParams(options.data)}`;
     }

-    return this.#fetch(url, {
+    try {
+        const response = await this.#fetch(url, {
+            signal: controller.signal,
             method: options?.method ?? 'GET',
             headers: {
                 'Content-Type': 'application/json',
                 ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
             },
             ...(method === 'POST' &&
                 options?.data && {
                     body: JSON.stringify(options.data)
                 })
-        }).then(async (response) => {
-            if (response.ok) {
+        });
+        clearTimeout(timeout);
+        
+        if (response.ok) {
                 const text = await response.text();
                 try {
                     if (text) {
                         return JSON.parse(text) as Data;
                     } else {
                         return {};
                     }
                 } catch (e) {
                     console.log(`Unable to parse: "${text}" for url: ${url}`);
                     throw e;
                 }
             } else {
-                error(response.status);
+                const errorBody = await response.text();
+                error(response.status, {
+                    message: `API request failed: ${errorBody}`
+                });
             }
-        });
+    } catch (e) {
+        clearTimeout(timeout);
+        if (e.name === 'AbortError') {
+            throw new Error('Request timeout');
+        }
+        throw e;
+    }
 }
🧹 Nitpick comments (4)
frontend/src/lib/api/api.ts (3)

9-20: LGTM! Consider strengthening type definitions

The type definitions are well-structured. Consider these enhancements:

  1. Make method in ApiRequestOptions required since it has a default in #send
  2. Replace Record<string, any> with a more specific type for data
 export type ApiRequestOptions = {
-  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
+  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  data?: Record<string, any>;
+  data?: Record<string, string | number | boolean>;
   headers?: Record<string, string>;
 };

33-34: Address TODO comment about project structure

Consider moving API calls to a dedicated models directory (e.g., src/lib/api/models/) to improve maintainability.

Would you like me to help create a proposal for the API code organization structure?


38-44: Consider adding parameter validation

Add runtime validation for offset and limit parameters to ensure they are positive numbers.

 async getJoins(offset: number = 0, limit: number = 10) {
+    if (offset < 0 || limit <= 0) {
+        throw new Error('Offset must be non-negative and limit must be positive');
+    }
     const params = new URLSearchParams({
         offset: offset.toString(),
         limit: limit.toString()
     });
     return this.#send<JoinsResponse>(`joins?${params.toString()}`);
 }
frontend/README.md (1)

33-37: Improve environment configuration documentation

  1. Add language specification to the code block
  2. Provide more context about the API configuration
 4. Create `.env` file in frontend directory with the follow

-   ```
+   ```env
    API_BASE_URL=http://localhost:9000
    ```
+
+   Note: If `API_BASE_URL` is not defined, it will default to `http://localhost:9000`.
+   This configuration is used by the frontend server to proxy API requests.
🧰 Tools
🪛 Markdownlint (0.37.0)

35-35: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 9f13f9f and 4d6fe92.

📒 Files selected for processing (9)
  • frontend/README.md (1 hunks)
  • frontend/src/lib/api/api.test.ts (4 hunks)
  • frontend/src/lib/api/api.ts (1 hunks)
  • frontend/src/lib/components/NavigationBar/NavigationBar.svelte (2 hunks)
  • frontend/src/lib/types/Model/Model.test.ts (1 hunks)
  • frontend/src/routes/api/[...path]/+server.ts (1 hunks)
  • frontend/src/routes/joins/+page.server.ts (1 hunks)
  • frontend/src/routes/joins/[slug]/+page.server.ts (6 hunks)
  • frontend/src/routes/joins/[slug]/+page.svelte (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/src/lib/components/NavigationBar/NavigationBar.svelte
  • frontend/src/lib/types/Model/Model.test.ts
  • frontend/src/routes/api/[...path]/+server.ts
  • frontend/src/routes/joins/[slug]/+page.svelte
  • frontend/src/routes/joins/[slug]/+page.server.ts
  • frontend/src/routes/joins/+page.server.ts
🧰 Additional context used
🪛 Markdownlint (0.37.0)
frontend/README.md

35-35: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Comment on lines +54 to +116
async getJoinTimeseries({
joinId,
startTs,
endTs,
metricType = 'drift',
metrics = 'null',
offset = '10h',
algorithm = 'psi'
}: {
joinId: string;
startTs: number;
endTs: number;
metricType?: string;
metrics?: string;
offset?: string;
algorithm?: string;
}) {
const params = new URLSearchParams({
startTs: startTs.toString(),
endTs: endTs.toString(),
metricType,
metrics,
offset,
algorithm
});

export async function search(term: string, limit: number = 20): Promise<ModelsResponse> {
const params = new URLSearchParams({
term,
limit: limit.toString()
});
return get(`search?${params.toString()}`);
}
return this.#send<JoinTimeSeriesResponse>(`join/${joinId}/timeseries?${params.toString()}`);
}

async getFeatureTimeseries({
joinId,
featureName,
startTs,
endTs,
metricType = 'drift',
metrics = 'null',
offset = '10h',
algorithm = 'psi',
granularity = 'aggregates'
}: {
joinId: string;
featureName: string;
startTs: number;
endTs: number;
metricType?: string;
metrics?: string;
offset?: string;
algorithm?: string;
granularity?: string;
}) {
const params = new URLSearchParams({
startTs: startTs.toString(),
endTs: endTs.toString(),
metricType,
metrics,
offset,
algorithm,
granularity
});
return this.#send<FeatureResponse>(
`join/${joinId}/feature/${featureName}/timeseries?${params.toString()}`
);
}
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

Reduce code duplication and improve type safety

The timeseries methods have significant duplication in parameter handling. Consider:

  1. Creating shared types for common parameters
  2. Extracting parameter handling to a utility function
type TimeseriesParams = {
  startTs: number;
  endTs: number;
  metricType?: string;
  metrics?: string;
  offset?: string;
  algorithm?: string;
};

function createTimeseriesParams(params: TimeseriesParams): URLSearchParams {
  return new URLSearchParams({
    startTs: params.startTs.toString(),
    endTs: params.endTs.toString(),
    metricType: params.metricType ?? 'drift',
    metrics: params.metrics ?? 'null',
    offset: params.offset ?? '10h',
    algorithm: params.algorithm ?? 'psi'
  });
}

Comment on lines +46 to +52
async search(term: string, limit: number = 20) {
const params = new URLSearchParams({
term,
limit: limit.toString()
});
return this.#send<ModelsResponse>(`search?${params.toString()}`);
}
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 input validation for search parameters

The search term should be validated and sanitized before being used in the query.

 async search(term: string, limit: number = 20) {
+    if (!term?.trim()) {
+        throw new Error('Search term cannot be empty');
+    }
+    if (limit <= 0) {
+        throw new Error('Limit must be positive');
+    }
     const params = new URLSearchParams({
-        term,
+        term: term.trim(),
         limit: limit.toString()
     });
     return this.#send<ModelsResponse>(`search?${params.toString()}`);
 }
📝 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
async search(term: string, limit: number = 20) {
const params = new URLSearchParams({
term,
limit: limit.toString()
});
return this.#send<ModelsResponse>(`search?${params.toString()}`);
}
async search(term: string, limit: number = 20) {
if (!term?.trim()) {
throw new Error('Search term cannot be empty');
}
if (limit <= 0) {
throw new Error('Limit must be positive');
}
const params = new URLSearchParams({
term: term.trim(),
limit: limit.toString()
});
return this.#send<ModelsResponse>(`search?${params.toString()}`);
}

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: 2

♻️ Duplicate comments (1)
frontend/src/lib/api/api.ts (1)

54-116: 🛠️ Refactor suggestion

Reduce code duplication in timeseries methods

The getJoinTimeseries and getFeatureTimeseries methods share significant parameter handling logic. Consider extracting common code:

type BaseTimeseriesParams = {
  startTs: number;
  endTs: number;
  metricType?: string;
  metrics?: string;
  offset?: string;
  algorithm?: string;
};

type JoinTimeseriesParams = BaseTimeseriesParams & {
  joinId: string;
};

type FeatureTimeseriesParams = JoinTimeseriesParams & {
  featureName: string;
  granularity?: string;
};

private createTimeseriesParams(params: BaseTimeseriesParams): URLSearchParams {
  return new URLSearchParams({
    startTs: params.startTs.toString(),
    endTs: params.endTs.toString(),
    metricType: params.metricType ?? 'drift',
    metrics: params.metrics ?? 'null',
    offset: params.offset ?? '10h',
    algorithm: params.algorithm ?? 'psi'
  });
}
🧹 Nitpick comments (3)
frontend/src/lib/api/api.ts (3)

9-20: Consider type improvements for better type safety

  1. The fetch type could be more precise:
-  fetch?: typeof fetch;
+  fetch?: WindowOrWorkerGlobalScope['fetch'];
  1. The data field in ApiRequestOptions could use a more specific type than any:
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  data?: Record<string, any>;
+  data?: Record<string, unknown>;

27-30: Add URL validation in constructor

Consider validating the base URL to ensure it doesn't contain trailing slashes that could cause double-slash issues:

 constructor(opts: ApiOptions = {}) {
-    this.#base = opts.base ?? '/api/v1';
+    const base = opts.base ?? '/api/v1';
+    this.#base = base.endsWith('/') ? base.slice(0, -1) : base;
     this.#fetch = opts.fetch ?? fetch;
     this.#accessToken = opts.accessToken;
 }

33-33: Address TODO comment about project organization

Consider moving model-specific API calls to a dedicated ModelApi class that extends the base Api class. This would improve organization and maintainability.

Would you like me to help create a proposal for the API organization structure?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 4d6fe92 and 69d9a3d.

📒 Files selected for processing (1)
  • frontend/src/lib/api/api.ts (1 hunks)
🔇 Additional comments (1)
frontend/src/lib/api/api.ts (1)

22-155: Verify API usage across the codebase

Since this is a significant refactor of the API interface, let's verify its usage across the codebase.

✅ Verification successful

Let me analyze the imports and usage patterns to verify if we need more information.


API usage is consistent with the new interface

The verification shows that:

  • All API usage follows the new interface pattern with proper instantiation of the Api class
  • Direct fetch usage is only present in the API proxy route ([...path]/+server.ts), which is expected
  • All API method calls (getModels, getJoins, search, getJoinTimeseries, getFeatureTimeseries) match the new interface
  • Error handling is properly implemented in the consuming components
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify API usage patterns across the codebase

# Find direct fetch usage that should be using the new Api class
echo "Checking for direct fetch usage..."
rg --type-add 'frontend:*.{ts,js,svelte}' -t frontend 'fetch\(' -g '!api.ts'

# Find old API function imports that should be updated
echo "Checking for old API imports..."
rg --type-add 'frontend:*.{ts,js,svelte}' -t frontend 'import.*from.*api' -g '!api.ts'

# Find potential places where error handling might need updates
echo "Checking error handling patterns..."
rg --type-add 'frontend:*.{ts,js,svelte}' -t frontend 'error\(' -g '!api.ts'

Length of output: 1502


Script:

#!/bin/bash
# Let's check the actual usage of the Api class in these files to ensure they're using the new methods correctly

echo "Checking Api class usage patterns..."
rg --type-add 'frontend:*.{ts,js,svelte}' -t frontend -A 5 'new Api\(' 

echo "Checking specific API method calls..."
rg --type-add 'frontend:*.{ts,js,svelte}' -t frontend -A 2 'api\.(getModels|getJoins|search|getJoinTimeseries|getFeatureTimeseries)'

Length of output: 6423

Comment on lines +38 to +44
async getJoins(offset: number = 0, limit: number = 10) {
const params = new URLSearchParams({
offset: offset.toString(),
limit: limit.toString()
});
return this.#send<JoinsResponse>(`joins?${params.toString()}`);
}
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

Add input validation for pagination and search parameters

Both getJoins and search methods need input validation:

 async getJoins(offset: number = 0, limit: number = 10) {
+    if (offset < 0) throw new Error('Offset must be non-negative');
+    if (limit <= 0) throw new Error('Limit must be positive');
     const params = new URLSearchParams({
       offset: offset.toString(),
       limit: limit.toString()
     });
     return this.#send<JoinsResponse>(`joins?${params.toString()}`);
 }

 async search(term: string, limit: number = 20) {
+    if (!term?.trim()) throw new Error('Search term cannot be empty');
+    if (limit <= 0) throw new Error('Limit must be positive');
     const params = new URLSearchParams({
-      term,
+      term: term.trim(),
       limit: limit.toString()
     });
     return this.#send<ModelsResponse>(`search?${params.toString()}`);
 }

Also applies to: 46-52

Comment on lines 118 to 155
async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
let url = `${this.#base}/${resource}`;

return get(`join/${joinId}/timeseries?${params.toString()}`);
}
const method = options?.method ?? 'GET';

export async function getFeatureTimeseries({
joinId,
featureName,
startTs,
endTs,
metricType = 'drift',
metrics = 'null',
offset = '10h',
algorithm = 'psi',
granularity = 'aggregates'
}: {
joinId: string;
featureName: string;
startTs: number;
endTs: number;
metricType?: string;
metrics?: string;
offset?: string;
algorithm?: string;
granularity?: string;
}): Promise<FeatureResponse> {
const params = new URLSearchParams({
startTs: startTs.toString(),
endTs: endTs.toString(),
metricType,
metrics,
offset,
algorithm,
granularity
});
return get(`join/${joinId}/feature/${featureName}/timeseries?${params.toString()}`);
if (method === 'GET' && options?.data) {
url += `?${new URLSearchParams(options.data)}`;
}

return this.#fetch(url, {
method: options?.method ?? 'GET',
headers: {
'Content-Type': 'application/json',
...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
},
...(method === 'POST' &&
options?.data && {
body: JSON.stringify(options.data)
})
}).then(async (response) => {
if (response.ok) {
const text = await response.text();
try {
if (text) {
return JSON.parse(text) as Data;
} else {
// TODO: Should we return `null` here and require users to handle
return {} as Data;
}
} catch (e) {
console.log(`Unable to parse: "${text}" for url: ${url}`);
throw e;
}
} else {
error(response.status);
}
});
}
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

Enhance error handling and response processing in #send method

Several improvements are needed:

  1. Better error handling for non-OK responses
  2. Proper typing for empty responses
  3. Simplified headers handling
 async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
   let url = `${this.#base}/${resource}`;
   const method = options?.method ?? 'GET';
+  const headers = {
+    'Content-Type': 'application/json',
+    ...options?.headers,
+    ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
+  };

   if (method === 'GET' && options?.data) {
     url += `?${new URLSearchParams(options.data)}`;
   }

   return this.#fetch(url, {
     method,
-    headers: {
-      'Content-Type': 'application/json',
-      ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
-    },
+    headers,
     ...(method === 'POST' &&
       options?.data && {
         body: JSON.stringify(options.data)
       })
   }).then(async (response) => {
     if (response.ok) {
       const text = await response.text();
       try {
         if (text) {
           return JSON.parse(text) as Data;
         } else {
-          // TODO: Should we return `null` here and require users to handle
-          return {} as Data;
+          return null as unknown as Data;
         }
       } catch (e) {
-        console.log(`Unable to parse: "${text}" for url: ${url}`);
+        const error = new Error(`Unable to parse response: "${text}" for url: ${url}`);
+        console.error(error);
+        throw error;
       }
     } else {
-      error(response.status);
+      const errorText = await response.text();
+      throw error(response.status, {
+        message: errorText || `Request failed with status ${response.status}`
+      });
     }
   });
 }
📝 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
async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
let url = `${this.#base}/${resource}`;
return get(`join/${joinId}/timeseries?${params.toString()}`);
}
const method = options?.method ?? 'GET';
export async function getFeatureTimeseries({
joinId,
featureName,
startTs,
endTs,
metricType = 'drift',
metrics = 'null',
offset = '10h',
algorithm = 'psi',
granularity = 'aggregates'
}: {
joinId: string;
featureName: string;
startTs: number;
endTs: number;
metricType?: string;
metrics?: string;
offset?: string;
algorithm?: string;
granularity?: string;
}): Promise<FeatureResponse> {
const params = new URLSearchParams({
startTs: startTs.toString(),
endTs: endTs.toString(),
metricType,
metrics,
offset,
algorithm,
granularity
});
return get(`join/${joinId}/feature/${featureName}/timeseries?${params.toString()}`);
if (method === 'GET' && options?.data) {
url += `?${new URLSearchParams(options.data)}`;
}
return this.#fetch(url, {
method: options?.method ?? 'GET',
headers: {
'Content-Type': 'application/json',
...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
},
...(method === 'POST' &&
options?.data && {
body: JSON.stringify(options.data)
})
}).then(async (response) => {
if (response.ok) {
const text = await response.text();
try {
if (text) {
return JSON.parse(text) as Data;
} else {
// TODO: Should we return `null` here and require users to handle
return {} as Data;
}
} catch (e) {
console.log(`Unable to parse: "${text}" for url: ${url}`);
throw e;
}
} else {
error(response.status);
}
});
}
async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
let url = `${this.#base}/${resource}`;
const method = options?.method ?? 'GET';
const headers = {
'Content-Type': 'application/json',
...options?.headers,
...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
};
if (method === 'GET' && options?.data) {
url += `?${new URLSearchParams(options.data)}`;
}
return this.#fetch(url, {
method,
headers,
...(method === 'POST' &&
options?.data && {
body: JSON.stringify(options.data)
})
}).then(async (response) => {
if (response.ok) {
const text = await response.text();
try {
if (text) {
return JSON.parse(text) as Data;
} else {
return null as unknown as Data;
}
} catch (e) {
const error = new Error(`Unable to parse response: "${text}" for url: ${url}`);
console.error(error);
throw error;
}
} else {
const errorText = await response.text();
throw error(response.status, {
message: errorText || `Request failed with status ${response.status}`
});
}
});
}

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: 0

♻️ Duplicate comments (3)
frontend/src/lib/api/api.ts (3)

38-44: ⚠️ Potential issue

Add input validation for pagination and search parameters

The input validation suggestions from the previous review are still relevant.

Also applies to: 46-52


54-116: 🛠️ Refactor suggestion

Reduce code duplication in timeseries methods

The timeseries methods share similar parameter handling logic and could be refactored.


118-157: ⚠️ Potential issue

Enhance error handling and response processing

The error handling suggestions from the previous review are still relevant. Additionally:

  1. The TODO comment about null handling needs to be addressed
  2. Error handling should be more informative

Also, consider implementing request timeout handling:

 async #send<Data = unknown>(resource: string, options?: ApiRequestOptions) {
   let url = `${this.#base}/${resource}`;
   const method = options?.method ?? 'GET';
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout

   try {
     return await this.#fetch(url, {
       method,
       headers: {
         'Content-Type': 'application/json',
         ...(this.#accessToken && { Authorization: `Bearer ${this.#accessToken}` })
       },
+      signal: controller.signal,
       ...(method === 'POST' &&
         options?.data && {
           body: JSON.stringify(options.data)
         })
     }).then(/* ... */);
   } finally {
+    clearTimeout(timeoutId);
   }
 }
🧹 Nitpick comments (3)
frontend/src/lib/api/api.ts (3)

15-20: Consider using a more specific type instead of any

Replace the any type with a more specific type to improve type safety. Consider using unknown or defining a union type of expected values.

- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- data?: Record<string, any>;
+ data?: Record<string, string | number | boolean | null>;

22-31: Add JSDoc documentation for the Api class and its constructor

Adding comprehensive documentation will improve maintainability and help other developers understand the purpose and usage of this class.

+/**
+ * Api class for handling HTTP requests to the backend.
+ * Supports custom fetch implementation and configurable base URL.
+ */
 export class Api {
   #base: string;
   #fetch: typeof fetch;
   #accessToken: string | undefined;

+  /**
+   * @param opts - Configuration options for the API
+   * @param opts.base - Base URL for API requests (defaults to '/api/v1')
+   * @param opts.fetch - Custom fetch implementation (defaults to global fetch)
+   * @param opts.accessToken - Optional authentication token
+   */
   constructor(opts: ApiOptions = {}) {

33-34: Address the TODO comment about project structure

Consider moving model-specific API calls to a dedicated ModelApi class that extends the base Api class. This would improve organization and maintainability.

Would you like me to help create a proposal for the API structure reorganization?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 69d9a3d and e8a7950.

📒 Files selected for processing (1)
  • frontend/src/lib/api/api.ts (1 hunks)

Copy link
Contributor

@ken-zlai ken-zlai left a comment

Choose a reason for hiding this comment

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

LGTM!

@sean-zlai sean-zlai merged commit 5ecda19 into main Dec 20, 2024
1 of 2 checks passed
@sean-zlai sean-zlai deleted the sean/api-proxy branch December 20, 2024 20:45
@coderabbitai coderabbitai bot mentioned this pull request Jan 31, 2025
4 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.

4 participants