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

refactor(server): return detailed error by lang #1342

Merged
merged 42 commits into from
Jan 22, 2025

Conversation

soneda-yuya
Copy link
Contributor

@soneda-yuya soneda-yuya commented Jan 9, 2025

Overview

This document outlines the design for localizing GraphQL errors on the backend. The goal is to reduce the cost of managing error messages by centralizing their localization on the backend, instead of handling them separately in the frontend.


Approach

Frontend Request Handling

  1. Attach lang to Request Header:
    • The language is determined based on the user's settings:
      1. If set to auto:
        • Use the browser's preferred language.
        • Default to English if the language is unsupported.
      2. If set to Japanese: Use lang=ja.
      3. If set to English: Use lang=en.
  2. Backend Response:
    • The backend includes localized error messages in the errors field of the GraphQL response.
    • The frontend simply displays the error messages as provided.

Problem Statement

Current Challenges

  • Frontend i18n for Errors:
    • Errors are currently mapped between backend error codes and frontend translations.
    • This introduces overhead as both systems must stay in sync.
  • Error Message Duplication:
    • Managing error messages in both the backend and frontend leads to redundancy and higher maintenance costs.

Proposed Solution

  • Backend-Managed Localization:
    • Localize error messages directly on the backend.
    • Include the fully localized error message in the GraphQL response.
  • Simplified Frontend:
    • Display error messages from the backend without additional processing.

Implementation Details

Error Definition Files

Error messages are managed in JSON files, organized by language and path.

  • Location:
    • server/internal/locales/ja/error.json
    • server/internal/locales/en/error.json

Sample Error Definition

{
    "unknown": {
        "message": "An unknown error occurred.",
        "description": "An unknown error occurred."
    },
    "repo": {
        "not_found": {
            "message": "Resource not found.",
            "description": "Resource not found."
        }
    },
    "pkg": {
        "project": {
            "invalid_alias": {
                "message": "Invalid alias name",
                "description": "The alias must be 5-32 characters long and can only contain alphanumeric characters, underscores, and hyphens."
            }
        }
    }
}

Error Display Workflow

graph TD
    A[Frontend] -->|Attach lang to header| B[Backend]
    B --> C{Error occurs}
    C -->|Fetch localized message| D[Retrieve error message from locales]
    D -->|GraphQL response| A
    A -->|Loop over errors and display| E[Display error to user]
Loading

Directory Structure

internal/i18n is internal locales file

pkg/i18n is public locales file

server/
├── app/
│   ├── i18n/
│   │   ├── locales/                     # JSON resource files for localization
│   │   │   └── errmsg/                  # Error message localization
│   │   │       ├── en.json              # English error messages
│   │   │       └── ja.json              # Japanese error messages
│   │   ├── message/                     # Generated message code
│   │   │   └── errmsg/                  # Generated Go code for error messages
│   │   │       └── message.go           # Shared logic for handling error messages
├── pkg/
│   ├── i18n/
│   │   ├── gen/                         # Code generation logic for localization
│   │   │   ├── entitymsg/
│   │   │   │   └── gen.go               # EntityMessage generation script
│   │   │   ├── errmsg/
│   │   │   │   └── gen.go               # ErrorMessage generation script
│   │   ├── locales/                     # Shared localization resources
│   │   │   ├── entitymsg/
│   │   │   │   ├── en.json              # English entity messages
│   │   │   │   └── ja.json              # Japanese entity messages
│   │   │   ├── errmsg/
│   │   │   │   ├── en.json              # English error messages
│   │   │   │   └── ja.json              # Japanese error messages
│   │   ├── message/                     # Shared message handling logic
│   │   │   ├── entitymsg/
│   │   │   │   └── entitymsg_generated.go # Generated code for entity messages
│   │   │   ├── errmsg/
│   │   │   │   └── errmsg_generated.go  # Generated code for error messages
│   │   │   └── message.go               # Shared logic for applying templates

Editing or Adding Error Messages

To add or edit error messages, modify the following files:
server/internal/app/i18n/locales/errmsg/ja.json
server/internal/app/i18n/locales/errmsg/en.json

server/pkg/i18n/locales/errmsg/ja.json
server/pkg/i18n/locales/errmsg/en.json

After updating these files, you must run the go generate command to regenerate the error key constants.

Steps to Regenerate Constants

  1. Run the following command:
  • generator exec on server/pkg/i18n/gen
make generate
  1. xxx/i18n/messages/errmsg/errmsg_generated.go is updated like this.
// Code generated by go generate; DO NOT EDIT.
package errmsg

import (
	"golang.org/x/text/language"
	"github.com/reearth/reearth/server/pkg/i18n/message"
)

const (
	ErrKeyPkgProjectInvalidAlias message.ErrKey = "pkg.project.invalid_alias"
)

var ErrorMessages = map[message.ErrKey]map[language.Tag]message.ErrorMessage{
	ErrKeyPkgProjectInvalidAlias: {
		language.English: {
			Message:     "Invalid alias name: {{.aliasName}}",
			Description: "The alias '{{.aliasName}}' must be {{.minLength}}-{{.maxLength}} characters long and can only contain {{.allowedChars}}.",
		},
		language.Japanese: {
			Message:     "不正なエイリアス名です: {{.aliasName}}",
			Description: "エイリアス名は{{.minLength}}-{{.maxLength}}文字で、{{.allowedChars}}のみ使用できます。",
		},
	},
}

Implementation Rules

  1. Path-Based Error Keys:

• Use hierarchical keys based on the originating path (e.g., repo.not_found, pkg.project.invalid_alias).

  1. Load on Server Startup:

• All error definitions are loaded at startup.

• The server will fail to start if definitions are missing.

Error Definition Example on BE

import "server/internal/locales"

var	ErrInvalidAlias = verror.NewVError(
		errmsg.ErrKeyPkgProjectInvalidAlias,
		errmsg.ErrorMessages[errmsg.ErrKeyPkgProjectInvalidAlias],
		message.MultiLocaleTemplateData(map[string]interface{}{
			"minLength": 5,
			"maxLength": 32,
			"allowedChars": func(locale language.Tag) string {
				return entitymsg.GetLocalizedEntityMessage(entitymsg.EntityKeyPkgProjectAliasAllowedChars, locale)
			},
		}), nil)

First argument: Error key (e.g., "repo.not_found").

Second argument: Error object (nil for static errors or dynamic values when necessary).

GraphQL Response Example

{
    "errors": [
        {
            "message": "Invalid alias name",
            "path": ["updateProject"],
            "extensions": {
                "code": "invalid_alias",
                "description": "The alias must be 5-32 characters long and can only contain alphanumeric characters, underscores, and hyphens.",
                "system_error": "input: updateProject [en] Invalid alias name, [ja] 無効なエイリアス名です。"
            }
        }
    ],
    "data": {
        "updateProject": {
            "project": {
                "id": "01jgz89mdmm5p25t2867wpembv",
                "name": "test",
                "alias": "test1"
            }
        }
    }
}

Conclusion

Centralized Error Management:

• By managing error localization on the backend, we simplify maintenance and reduce redundancy.

Localized Error Messages:

• The frontend can display backend-provided errors directly, ensuring consistency and reducing complexity.

Implementation Benefits:

• Streamlined error handling process.

• Faster development by avoiding duplication between frontend and backend.

What I've done

implement backend according to design.

What I haven't done

implement frontend

How I tested

return error from backend like this on my local

{
    "errors": [
        {
            "message": "Invalid alias name",
            "path": ["updateProject"],
            "extensions": {
                "code": "invalid_alias",
                "description": "The alias must be 5-32 characters long and can only contain alphanumeric characters, underscores, and hyphens.",
                "system_error": "input: updateProject [en] Invalid alias name, [ja] 無効なエイリアス名です。"
            }
        }
    ],
    "data": {
        "updateProject": {
            "project": {
                "id": "01jgz89mdmm5p25t2867wpembv",
                "name": "test",
                "alias": "test1"
            }
        }
    }
}

Which point I want you to review particularly

Memo

Summary by CodeRabbit

Release Notes

  • Language Support

    • Added robust internationalization (i18n) support with English and Japanese locales.
    • Implemented language detection and context attachment middleware.
    • Enhanced error message localization for project aliases.
  • Error Handling

    • Introduced a new custom error handling mechanism with detailed, localized error messages.
    • Improved GraphQL error presentation with context-aware error reporting.
    • Streamlined error messages for various operations, enhancing clarity.
  • Project Management

    • Enhanced project alias validation with more precise error messaging.
    • Improved error handling for project-related operations.
  • Testing

    • Added comprehensive unit tests for new language and error handling features.
    • Expanded test coverage for internationalization components.
    • Validated localization of error messages across different locales.

These changes focus on improving the application's error handling, language support, and overall user experience.

Copy link

coderabbitai bot commented Jan 9, 2025

Walkthrough

This pull request introduces modifications across various files to enhance error handling, internationalization (i18n), and language management within the server application. Key changes include the addition of JSON file support in the build configuration, improvements to error messages for GraphQL operations, and the introduction of middleware for language extraction. The updates also encompass new error types for better localization and refined context management for language settings, ultimately aiming to improve the application's responsiveness to user language preferences and error reporting.

Changes

File Change Summary
server/.air.toml Updated include_ext to include JSON files in build configuration
server/e2e/gql_storytelling_test.go Simplified error message for invalid page ID
server/e2e/gql_workspace_test.go Streamlined error messages for team management operations
server/internal/adapter/context.go Added language context management functions
server/internal/app/app.go Introduced AttachLanguageMiddleware to the middleware chain
server/internal/app/graphql.go Implemented customErrorPresenter for enhanced GraphQL error handling
server/internal/infrastructure/mongo/project.go Improved error handling in FindByID method for project lookup
server/internal/usecase/interactor/project.go Modified error handling to integrate with GraphQL context
server/internal/usecase/repo/container.go Added ErrResourceNotFound for better error clarity
server/pkg/project/project.go Updated ErrInvalidAlias for more descriptive error handling
Multiple i18n files Introduced localization support for error messages and entity descriptions

Possibly related PRs

  • feat(server): Import or Export Project Functionality #1141: The changes in this PR involve the introduction of import and export functionalities for project data, which may relate to the main PR's modification of the include_ext configuration to include JSON files, as both involve handling project data formats.
  • feat(server): project import export external url #1205: This PR enhances the import/export functionalities by adding support for external URLs, which aligns with the main PR's focus on expanding the types of files handled in the build process, suggesting a connection in terms of data management and format handling.
  • feat(server): sync sketch layer properties  #1348: This PR focuses on synchronizing sketch layer properties and introduces new input types for modifying and removing custom properties. The connection to the main PR lies in the broader context of managing project data and configurations, particularly as both involve updates to how project-related data is structured and handled.

Suggested labels

web

Suggested reviewers

  • pyshx
  • airslice
  • m-abe-dev

Poem

🐰 In the code where errors play,
Languages dance both night and day,
Middleware weaves a gentle thread,
Localized tales where words are spread,
Hopping rabbits bring joy to the way! 🌟

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

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 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.

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

netlify bot commented Jan 9, 2025

Deploy Preview for reearth-web canceled.

Name Link
🔨 Latest commit ad860b5
🔍 Latest deploy log https://app.netlify.com/sites/reearth-web/deploys/6790a896991c1f0008d19fe4

@soneda-yuya soneda-yuya marked this pull request as ready for review January 10, 2025 08:45
@soneda-yuya soneda-yuya requested a review from pyshx as a code owner January 10, 2025 08:45
@soneda-yuya soneda-yuya changed the title refactor(server): [BE] Improvement: Detailed error types from BE refactor(server): [BE] return detailed error by lang Jan 10, 2025
Copy link

@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: 6

🧹 Nitpick comments (13)
server/pkg/error/error.go (1)

53-59: Ensure consistent ordering of error messages

In the Error() method, iterating over a map (LocalesError) does not guarantee order, which may lead to inconsistent error message outputs. To provide consistent ordering, consider sorting the keys before appending the messages.

Apply this diff to sort the locales:

 func (e *AppError) Error() string {
 	var errorMessage []string
+	langs := make([]string, 0, len(e.LocalesError))
+	for locale := range e.LocalesError {
+		langs = append(langs, locale)
+	}
+	sort.Strings(langs)
-	for locale, err := range e.LocalesError {
+	for _, locale := range langs {
+		err := e.LocalesError[locale]
 		errorMessage = append(errorMessage, fmt.Sprintf("[%s] %s", locale, err.Message))
 	}
 	return strings.Join(errorMessage, ",")
 }

Don't forget to import the sort package at the beginning of the file:

 import (
 	"fmt"
 	"strings"
+	"sort"

 	"github.com/reearth/reearth/server/internal/locales"
 )
server/internal/locales/cache.go (1)

7-9: Define a concrete type instead of using map[string]interface{}

Using map[string]interface{} for fileCache can lead to type safety issues and makes the code harder to understand. Consider defining a concrete type or struct for the cached data to improve code readability and maintainability.

For example, define a struct for the cached data:

type LocaleData struct {
    // Define the fields according to the structure of your locale JSON files
    // For example:
    Errors map[string]ErrorDefinition `json:"errors"`
    // Add other relevant fields
}

Then update fileCache to use this type:

 type LocalesCache struct {
     mu        sync.RWMutex
-    fileCache map[string]map[string]interface{}
+    fileCache map[string]*LocaleData
 }
server/internal/locales/locales.go (3)

90-91: Translate the Japanese comment to English.

For consistency and maintainability, all comments should be in English.

-		// map[string]interface{} 型であることを確認しつつ検索を進める
+		// Continue searching while confirming the type is map[string]interface{}

93-94: Translate the Japanese comment to English.

For consistency and maintainability, all comments should be in English.

-		} else if len(keys) == 1 { // 最終キーの場合
+		} else if len(keys) == 1 { // If it's the final key

95-96: Translate the Japanese comment to English.

For consistency and maintainability, all comments should be in English.

-			return nil // 存在しないキーの場合
+			return nil // If the key doesn't exist
server/internal/usecase/repo/container.go (1)

18-18: Consider adding error context.

The ResourceNotFound error could benefit from additional context about which resource was not found.

-	ResourceNotFound   = apperror.NewAppError("repo.not_found", nil)
+	ResourceNotFound   = apperror.NewAppError("repo.not_found", map[string]interface{}{
+		"details": "The requested resource could not be found in the repository",
+	})
server/internal/infrastructure/mongo/project.go (1)

51-62: LGTM! Consider similar improvements for other methods.

The improved error handling for FindByID is good. The explicit handling of mongo.ErrNoDocuments provides clearer feedback.

Consider applying similar error handling improvements to other methods that use findOne, such as FindByScene and FindByPublicName.

server/internal/app/app.go (1)

119-134: LGTM! Consider improving language fallback handling.

The language middleware implementation looks good. It correctly prioritizes user language settings over browser language when available.

Consider adding a fallback chain for language selection:

  1. User's explicitly set language (if not "und")
  2. Browser's language header
  3. Default application language

Example implementation:

 func(c echo.Context) error {
-    lang := c.Request().Header.Get("lang")
+    // Default language
+    lang := "en"
+    // Browser language
+    if browserLang := c.Request().Header.Get("lang"); browserLang != "" {
+        lang = browserLang
+    }
     // User language
     u := adapter.User(c.Request().Context())
     if u != nil && u.Lang().String() != "und" {
         lang = u.Lang().String()
     }
     c.SetRequest(c.Request().WithContext(adapter.AttachLang(c.Request().Context(), lang)))
     return next(c)
 }
server/internal/locales/ja/error.json (2)

6-11: Consider expanding repository error types

The repo section currently only handles "not_found". Consider adding other common repository errors like "already_exists", "validation_failed", etc.


12-19: Project error messages need expansion

The pkg section only includes project.invalid_alias. Consider adding other common project-related errors for completeness.

Example structure:

 "pkg": {
     "project": {
         "invalid_alias": {
             "message": "無効なエイリアス名です。",
             "description": "エイリアスは5-32文字で、英数字、アンダースコア、ハイフンのみ使用できます。"
-        }
+        },
+        "already_exists": {
+            "message": "プロジェクトは既に存在します。",
+            "description": "同じ名前のプロジェクトが既に存在します。"
+        },
+        "not_found": {
+            "message": "プロジェクトが見つかりません。",
+            "description": "指定されたプロジェクトは存在しないか、アクセス権限がありません。"
+        }
     }
 }
server/internal/locales/en/error.json (3)

2-6: Enhance the unknown error description

The description should provide more context than the message to help developers understand when and why this error occurs.

     "unknown": {
         "code": "unknown",
         "message": "An unknown error occurred.",
-        "description": "An unknown error occurred."
+        "description": "An unexpected error occurred that couldn't be mapped to a specific error type. This typically indicates an unhandled edge case or system error."
     },

7-13: Improve the repo not_found error details

The error message and description should be more specific and include troubleshooting guidance.

     "repo": {
         "not_found": {
             "code": "not_found",
-            "message": "Resource not found.",
-            "description": "Resource not found."
+            "message": "The requested resource could not be found.",
+            "description": "The system could not locate the requested resource. This may occur if the resource was deleted, moved, or never existed. Please verify the resource identifier and try again."
         }
     },

14-22: Enhance the invalid_alias error message and description

While the validation rules are clear, the message could be more user-friendly, and the description could include examples.

             "invalid_alias": {
                 "code": "invalid_alias",
-                "message": "invalid alias name",
-                "description": "The alias must be 5-32 characters long and can only contain alphanumeric characters, underscores, and hyphens."
+                "message": "The alias name format is invalid",
+                "description": "The alias must be 5-32 characters long and can only contain alphanumeric characters, underscores, and hyphens. Examples of valid aliases: 'project-2023', 'my_awesome_project', 'team123'."
             }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 887173f and e26ddec.

📒 Files selected for processing (16)
  • server/.air.toml (1 hunks)
  • server/e2e/gql_storytelling_test.go (1 hunks)
  • server/e2e/gql_workspace_test.go (5 hunks)
  • server/internal/adapter/context.go (3 hunks)
  • server/internal/adapter/gql/context.go (1 hunks)
  • server/internal/app/app.go (2 hunks)
  • server/internal/app/graphql.go (4 hunks)
  • server/internal/infrastructure/mongo/project.go (2 hunks)
  • server/internal/locales/cache.go (1 hunks)
  • server/internal/locales/en/error.json (1 hunks)
  • server/internal/locales/ja/error.json (1 hunks)
  • server/internal/locales/locales.go (1 hunks)
  • server/internal/usecase/interactor/project.go (3 hunks)
  • server/internal/usecase/repo/container.go (2 hunks)
  • server/pkg/error/error.go (1 hunks)
  • server/pkg/project/project.go (1 hunks)
🔇 Additional comments (10)
server/internal/adapter/gql/context.go (1)

39-39: Update to use adapter.LangByTag

Replacing adapter.Lang(ctx, lang) with adapter.LangByTag(ctx, lang) ensures that the language is correctly extracted from the provided lang parameter. This is appropriate for accurate localization handling.

server/internal/app/graphql.go (1)

67-75: Handle missing translations gracefully.

The code assumes that appErr.LocalesError[lang] always exists for the current language.

Add a fallback mechanism:

 			if ok := errors.As(e, &appErr); ok {
-				localesErr := appErr.LocalesError[lang]
+				localesErr, exists := appErr.LocalesError[lang]
+				if !exists {
+					// Fallback to English if translation is missing
+					localesErr = appErr.LocalesError["en"]
+				}
 				graphqlErr = &gqlerror.Error{

Let's verify the locales structure:

server/internal/adapter/context.go (1)

71-76: Consider validating language tag format.

The function accepts any string as a language tag without validation.

Add validation for language tag format:

 func LangByTag(ctx context.Context, lang *language.Tag) string {
 	if lang != nil && !lang.IsRoot() {
+		// Verify the language tag is well-formed
+		if _, err := language.Parse(lang.String()); err != nil {
+			return defaultLang.String()
+		}
 		return lang.String()
 	}
 	return Lang(ctx)
 }

Let's verify the language tags in use:

server/e2e/gql_workspace_test.go (1)

44-44: LGTM! Consider adding more test cases.

The simplified error messages are clearer and more maintainable. However, consider adding test cases for different languages to verify the localization functionality works as expected.

Run this script to check if there are similar test files that might need the same updates:

Also applies to: 70-70, 96-96, 117-117, 140-140

server/internal/usecase/interactor/project.go (2)

207-207: Error handling improvement: Using GraphQL error context

The change enhances error handling by adding errors to the GraphQL context instead of immediately returning, allowing for better error aggregation.


284-287: Verify accumulated errors before saving

Good addition of error check before proceeding with save operation. This ensures that any validation errors are properly handled before persisting changes.

server/e2e/gql_storytelling_test.go (1)

854-854: Simplified error message assertion

The test has been updated to match the new simplified error message format, aligning with the improved error handling approach.

server/.air.toml (1)

16-16: Added JSON files to build monitoring

Including JSON files in the build process is essential for the new localization system to detect changes in error message definitions.

server/internal/locales/ja/error.json (1)

2-5: Well-structured unknown error fallback

The unknown error message provides a good fallback for unexpected errors.

server/internal/locales/en/error.json (1)

1-23: Well-structured error definitions!

The hierarchical organization and consistent structure make it easy to maintain and extend error definitions.

Copy link

codecov bot commented Jan 10, 2025

Codecov Report

Attention: Patch coverage is 44.04145% with 108 lines in your changes missing coverage. Please review.

Project coverage is 23.00%. Comparing base (52f66e4) to head (c8d4dfe).
Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
server/pkg/locales/gen/error/generate.go 0.00% 77 Missing ⚠️
server/internal/app/app.go 0.00% 9 Missing ⚠️
server/internal/app/graphql.go 82.97% 6 Missing and 2 partials ⚠️
server/internal/infrastructure/mongo/project.go 0.00% 8 Missing ⚠️
server/internal/usecase/interactor/project.go 0.00% 4 Missing ⚠️
server/pkg/verror/error.go 80.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1342      +/-   ##
==========================================
- Coverage   23.00%   23.00%   -0.01%     
==========================================
  Files        1041     1046       +5     
  Lines      107732   107892     +160     
  Branches      663      663              
==========================================
+ Hits        24786    24818      +32     
- Misses      81687    81815     +128     
  Partials     1259     1259              
Flag Coverage Δ
server 31.44% <44.04%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
server/internal/adapter/context.go 18.84% <100.00%> (-63.77%) ⬇️
server/internal/app/lang.go 100.00% <100.00%> (ø)
server/internal/app/locales/locales.go 100.00% <100.00%> (ø)
server/internal/usecase/repo/container.go 37.00% <ø> (ø)
server/pkg/locales/locales.go 100.00% <100.00%> (ø)
server/pkg/plugin/manifest/parser.go 69.49% <ø> (ø)
server/pkg/plugin/manifest/parser_translation.go 100.00% <ø> (ø)
server/pkg/project/project.go 89.51% <ø> (ø)
server/pkg/property/initializer.go 72.58% <ø> (ø)
server/pkg/verror/error.go 80.00% <80.00%> (ø)
... and 5 more

... and 2 files with indirect coverage changes

Copy link

@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

♻️ Duplicate comments (1)
server/internal/app/graphql.go (1)

91-104: ⚠️ Potential issue

Improve security of error handling.

System errors are still being exposed to clients, which could leak sensitive information.

🧹 Nitpick comments (13)
server/internal/locales/locales_test.go (5)

3-12: LGTM! Consider adding package documentation.

The setup with embedded filesystem for test locales is well-structured. Consider adding a package documentation comment to explain the purpose of these tests and the structure of the embedded locale files.

Add this documentation at the top of the file:

 package locales

+// Package locales_test validates the localization system's functionality,
+// specifically testing the loading and retrieval of error messages from
+// embedded locale files organized by language (en/*, ja/*).

14-15: Translate comments to English for consistency.

The comment // モック用の localesJson を置き換える should be in English to maintain codebase consistency.

-	// モック用の localesJson を置き換える
+	// Replace localesJson with test mock data

17-50: Add more test cases for edge scenarios.

While the current test cases cover basic scenarios, consider adding tests for:

  • Empty key ("")
  • Malformed keys (e.g., "pkg..project")
  • Keys with special characters
  • Very long keys

Example additional test cases:

{
    name:    "Empty key",
    key:     "",
    isPanic: true,
},
{
    name:    "Malformed key",
    key:     "pkg..project",
    isPanic: true,
},
{
    name:    "Key with special chars",
    key:     "pkg.project.@invalid",
    isPanic: true,
},

67-108: Enhance test coverage with edge cases.

The test cases are well-structured, but consider adding scenarios for:

  • Deeper nesting levels (>3 levels)
  • Empty maps at intermediate levels
  • nil values in the nested structure

Example additional test cases:

{
    name:     "Deep nesting",
    keys:     []string{"pkg", "project", "category", "subcategory", "item"},
    expected: nil,
},
{
    name:     "Empty map at intermediate level",
    keys:     []string{"pkg", "empty_map", "key"},
    expected: nil,
},
{
    name:     "Nil value in nested structure",
    keys:     []string{"pkg", "project", "nil_value"},
    expected: nil,
},

And add corresponding mock data:

mockData := map[string]interface{}{
    "pkg": map[string]interface{}{
        "project": map[string]interface{}{
            "nil_value": nil,
        },
        "empty_map": map[string]interface{}{},
    },
}

1-108: Consider extracting test data to separate files.

To improve maintainability and readability, consider moving the test data (mock objects and expected results) to separate test fixture files. This would make it easier to maintain the test data, especially as more localized error messages are added.

Suggested structure:

server/internal/locales/
├── testdata/
│   ├── mock_errors.json
│   └── mock_nested_data.json
└── locales_test.go

This would also make it easier to maintain consistency between test data and actual locale files.

server/internal/locales/cache_test.go (4)

9-23: Consider enhancing test data and type safety.

While the current mock data is good for basic testing, consider these improvements:

  1. Add test cases for empty maps and special characters
  2. Consider using a strongly typed structure instead of map[string]interface{}
  3. Add test cases for nested translation structures that might be needed for complex error messages

Example of a strongly typed approach:

type TranslationEntry struct {
    Message string
    Params  map[string]string // For interpolation if needed
}

mockDataEN := map[string]TranslationEntry{
    "hello": {Message: "Hello"},
    "error.notFound": {
        Message: "Resource %s not found",
        Params: map[string]string{"resource": "user"},
    },
}

24-35: Add edge cases and concurrent access tests.

The basic functionality is well tested, but consider adding these scenarios:

  1. Empty string as language code
  2. Concurrent access to the cache
  3. Case sensitivity handling for language codes

Example of a concurrent access test:

t.Run("concurrent access", func(t *testing.T) {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            cache.SetFileCache("en", mockDataEN)
            _, _ = cache.GetFromFileCache("en")
        }()
    }
    wg.Wait()
})

52-60: Enhance error case coverage.

While the basic error case is covered, consider adding tests for:

  1. Similar language codes (e.g., "en" vs "en-US")
  2. Invalid language codes (e.g., "invalid", "123", "")
  3. Special characters in language codes

Example test cases:

testCases := []struct {
    name     string
    langCode string
    wantOk   bool
}{
    {"empty string", "", false},
    {"invalid format", "en_US", false},
    {"special chars", "en@#$", false},
    {"similar code", "en-US", false},
}

for _, tc := range testCases {
    t.Run(tc.name, func(t *testing.T) {
        data, ok := cache.GetFromFileCache(tc.langCode)
        assert.Equal(t, tc.wantOk, ok)
        assert.Nil(t, data)
    })
}

1-60: Add test documentation for maintainability.

Consider adding a package-level comment that describes:

  1. The purpose and scope of these tests
  2. Any assumptions about the cache behavior
  3. How these tests relate to the larger localization system

Example documentation:

// Package locales_test verifies the behavior of the localization cache system.
// The cache is designed to store and retrieve language-specific translation data
// loaded from JSON files at server startup. These tests ensure that:
// - Language data can be stored and retrieved correctly
// - Multiple languages can coexist in the cache
// - Missing languages are handled appropriately
server/pkg/apperror/error.go (1)

21-31: Improve variable naming for clarity

The variable localesError represents a map of errors for different locales, but the name suggests it is a singular error. Consider renaming it to localizedErrors or localesErrorsMap to better reflect that it contains multiple errors, enhancing code readability.

Apply this diff to improve variable naming:

 func buildLocalesError(localesErrors map[string]*locales.Error) map[string]*LocalesError {
-	localesError := make(map[string]*LocalesError)
+	localizedErrors := make(map[string]*LocalesError)
 	for lang, locale := range localesErrors {
-		localesError[lang] = &LocalesError{
+		localizedErrors[lang] = &LocalesError{
 			Code:        locale.Code,
 			Message:     locale.Message,
 			Description: locale.Description,
 		}
 	}
-	return localesError
+	return localizedErrors
 }
server/internal/app/graphql_test.go (2)

15-16: Translate Japanese comments to English for consistency.

The codebase appears to use English as the primary language. Consider translating these comments:

-	// モック用のコンテキストとロケール
+	// Mock context and locale for testing
-	// アサーション: GraphQL エラーの内容を検証
+	// Assertion: Verify GraphQL error content

Also applies to: 34-34


31-40: Enhance test coverage for error scenarios.

While the test covers basic functionality, consider adding test cases for:

  • Missing locale for the requested language
  • Invalid language tags
  • Empty or malformed error messages
server/internal/app/lang_test.go (1)

63-83: Enhance middleware test coverage.

The middleware test only covers the happy path. Consider adding test cases for:

  • Missing language header
  • Invalid language tags
  • Multiple language headers
  • Language negotiation with Accept-Language header
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5f4976 and d574d55.

📒 Files selected for processing (14)
  • server/internal/adapter/context.go (3 hunks)
  • server/internal/app/app.go (2 hunks)
  • server/internal/app/graphql.go (5 hunks)
  • server/internal/app/graphql_test.go (1 hunks)
  • server/internal/app/lang.go (1 hunks)
  • server/internal/app/lang_test.go (1 hunks)
  • server/internal/locales/cache_test.go (1 hunks)
  • server/internal/locales/en/error.json (1 hunks)
  • server/internal/locales/locales.go (1 hunks)
  • server/internal/locales/locales_test.go (1 hunks)
  • server/internal/usecase/repo/container.go (2 hunks)
  • server/pkg/apperror/error.go (1 hunks)
  • server/pkg/apperror/error_test.go (1 hunks)
  • server/pkg/project/project.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/internal/app/app.go
  • server/pkg/project/project.go
  • server/internal/locales/en/error.json
  • server/internal/usecase/repo/container.go
  • server/internal/locales/locales.go
🔇 Additional comments (6)
server/internal/locales/cache_test.go (2)

1-8: LGTM! Good choice of testing framework.

The package structure and imports are well-organized. Using testify/assert makes the test assertions clear and readable.


36-51: Add cache manipulation and boundary tests.

Consider adding these test scenarios:

  1. Overwriting existing language data
  2. Cache size limits (if any)
  3. Memory cleanup/eviction strategies

Let's check if there are any cache size limits or cleanup mechanisms in the implementation:

server/internal/app/lang.go (2)

26-34: Middleware implementation is correct

The AttachLanguageMiddleware function correctly attaches the detected language to the request context, facilitating localized responses.


11-24: Ensure robust language extraction and default handling

In LanguageExtractor, consider the following improvements:

  • Verify that the "lang" header is the correct header for language specification. Typically, the "Accept-Language" header is used to indicate the client's preferred languages.
  • Implement validation to ensure the extracted language code is supported by the application.
  • Provide a default language (e.g., English) if the extracted language is empty or unsupported to avoid potential issues with undefined behavior.

Run the following script to check for usage of the "lang" header across the codebase and ensure consistency:

server/pkg/apperror/error_test.go (1)

27-35: Test for Error method is accurate

The TestErrorMethod function correctly verifies that the Error method returns a formatted error message containing the localized messages.

server/internal/adapter/context.go (1)

78-88: Reconsider empty string handling in Lang function.

The function returns defaultLang for empty strings, which might mask configuration errors. Consider:

  1. Distinguishing between "no language set" and "explicitly set to empty"
  2. Logging a warning when falling back to default language

Run this script to check for empty language usage:

✅ Verification successful

Empty string handling in Lang function is appropriate

The current implementation provides a safe fallback to English when no language is set or when an empty string is encountered. This approach:

  • Prevents runtime errors
  • Maintains consistent behavior
  • Follows common i18n patterns

No instances of empty string usage were found in the codebase, indicating proper language value handling throughout the application.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for direct calls to AttachLang with empty string
rg "AttachLang\([^,]+,\s*\"\"" -A 2

Length of output: 35


Script:

#!/bin/bash
# Search for all AttachLang calls
rg "AttachLang\(" -A 2

# Search for test files containing language-related tests
fd ".*_test\.go$" --exec rg -l "TestLang|Lang\(" {}

# Search for defaultLang usage
rg "defaultLang" -A 2

Length of output: 1268

@soneda-yuya soneda-yuya force-pushed the refactor/error_handling_be branch from 834688e to ada5a33 Compare January 17, 2025 11:02
Copy link

@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

🧹 Nitpick comments (11)
server/internal/app/i18n/message/errmsg/errmsg_generated.go (1)

9-12: LGTM! Consider adding godoc comments.

The error keys follow a consistent naming pattern and cover essential error cases. Consider adding godoc comments to document the usage of each error key.

 const (
+    // ErrKeyUnknown represents an unspecified or unexpected error
     ErrKeyUnknown message.ErrKey = "unknown"
+    // ErrKeyRepoResourceNotFound indicates that the requested resource was not found
     ErrKeyRepoResourceNotFound message.ErrKey = "repo.resource_not_found"
 )
server/pkg/i18n/message/message.go (3)

14-21: Add documentation for exported types.

The exported types EntityKey, EntityMessage, ErrKey, and ErrorMessage should have proper documentation comments explaining their purpose and usage.


27-28: Translate Japanese comments to English.

For better maintainability and collaboration, translate the Japanese comment "関数を評価し、結果を processedData に格納" to English.


53-57: Add documentation for MultiLocaleTemplateData function.

The exported function MultiLocaleTemplateData should have a documentation comment explaining its purpose, parameters, and return value.

server/pkg/verror/verror.go (1)

41-42: Fix incorrect comment.

The comment "Error returns the error message" is incorrect for the AddTemplateData method.

-// Error returns the error message.
+// AddTemplateData adds template data for all supported locales and returns the error for chaining.
 func (e *VError) AddTemplateData(key string, value interface{}) *VError {
server/internal/app/graphql_test.go (1)

16-87: Consider using a table-driven test approach.

While the current test structure is good, using a table-driven approach could make the tests more maintainable and reduce code duplication.

Example refactor:

func TestCustomErrorPresenter(t *testing.T) {
    tests := []struct {
        name     string
        ctx      context.Context
        err      error
        devMode  bool
        expected struct {
            message     string
            code       string
            sysError   interface{}
        }
    }{
        {
            name:    "vErr with English language",
            ctx:     adapter.AttachLang(context.Background(), language.English),
            err:     vErr,
            devMode: false,
            expected: struct{
                message:   "An unknown error occurred.",
                code:     string(errmsg.ErrKeyUnknown),
                sysError: nil,
            },
        },
        // ... other test cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            graphqlErr := customErrorPresenter(tt.ctx, tt.err, tt.devMode)
            assert.NotNil(t, graphqlErr)
            assert.Equal(t, tt.expected.message, graphqlErr.Message)
            assert.Equal(t, tt.expected.code, graphqlErr.Extensions["code"])
            assert.Equal(t, tt.expected.sysError, graphqlErr.Extensions["system_error"])
        })
    }
}
server/internal/app/graphql.go (4)

65-67: Consider adding panic recovery to the error presenter.

While the error presenter setup is correct, consider wrapping it with panic recovery to handle potential panics gracefully:

 srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
+    defer func() {
+        if r := recover(); r != nil {
+            log.Errorfc(ctx, "panic recovered in error presenter: %v", r)
+            return graphql.DefaultErrorPresenter(ctx, fmt.Errorf("internal server error"))
+        }
+    }()
     return customErrorPresenter(ctx, e, dev)
 })

87-92: Enhance function documentation.

While the function has a basic comment, it would benefit from more detailed documentation including:

  • Parameter descriptions
  • Return value description
  • Example usage
  • Error handling behavior
-// customErrorPresenter handles custom GraphQL error presentation.
+// customErrorPresenter handles custom GraphQL error presentation by converting various error types
+// into localized GraphQL errors.
+//
+// Parameters:
+//   - ctx: The context containing language preferences
+//   - e: The error to be presented
+//   - devMode: If true, includes additional debugging information
+//
+// Returns a GraphQL error with localized messages and optional debug information.

93-95: Move systemError declaration closer to usage.

The systemError variable is declared far from its first usage. Consider moving it closer to where it's needed for better code readability.


135-137: Enhance error logging with more context.

The error logging could be more specific about the source and nature of the system error:

-log.Errorfc(ctx, "system error: %+v", e)
+log.Errorfc(ctx, "graphql system error occurred during error presentation: %+v", e)
server/pkg/project/project.go (1)

17-27: Define constants for magic numbers.

Consider extracting the magic numbers into named constants for better maintainability:

+const (
+    aliasMinLength = 5
+    aliasMaxLength = 32
+)

 ErrInvalidAlias = verror.NewVError(
     errmsg.ErrKeyPkgProjectInvalidAlias,
     errmsg.ErrorMessages[errmsg.ErrKeyPkgProjectInvalidAlias],
     message.MultiLocaleTemplateData(map[string]interface{}{
-        "minLength": 5,
-        "maxLength": 32,
+        "minLength": aliasMinLength,
+        "maxLength": aliasMaxLength,
         "allowedChars": func(locale language.Tag) string {
             return entitymsg.GetLocalizedEntityMessage(entitymsg.EntityKeyPkgProjectAliasAllowedChars, locale)
         },
     }), nil)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 834688e and ada5a33.

⛔ Files ignored due to path filters (2)
  • server/pkg/i18n/gen/entitymsg/gen.go is excluded by !**/gen/**
  • server/pkg/i18n/gen/errmsg/gen.go is excluded by !**/gen/**
📒 Files selected for processing (19)
  • server/internal/app/app.go (3 hunks)
  • server/internal/app/graphql.go (5 hunks)
  • server/internal/app/graphql_test.go (1 hunks)
  • server/internal/app/i18n/locales/errmsg/en.json (1 hunks)
  • server/internal/app/i18n/locales/errmsg/ja.json (1 hunks)
  • server/internal/app/i18n/message/errmsg/errmsg_generated.go (1 hunks)
  • server/internal/app/i18n/message/messageg.go (1 hunks)
  • server/internal/usecase/repo/container.go (1 hunks)
  • server/pkg/i18n/locales.go (1 hunks)
  • server/pkg/i18n/locales/entitymsg/en.json (1 hunks)
  • server/pkg/i18n/locales/entitymsg/ja.json (1 hunks)
  • server/pkg/i18n/locales/errmsg/en.json (1 hunks)
  • server/pkg/i18n/locales/errmsg/ja.json (1 hunks)
  • server/pkg/i18n/message/entitymsg/entitymsg_generated.go (1 hunks)
  • server/pkg/i18n/message/errmsg/errmsg_generated.go (1 hunks)
  • server/pkg/i18n/message/message.go (1 hunks)
  • server/pkg/project/project.go (2 hunks)
  • server/pkg/verror/verror.go (1 hunks)
  • server/pkg/verror/verror_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
  • server/pkg/i18n/locales.go
  • server/internal/app/app.go
  • server/internal/app/i18n/message/messageg.go
  • server/pkg/i18n/locales/errmsg/ja.json
  • server/pkg/i18n/locales/entitymsg/ja.json
  • server/internal/usecase/repo/container.go
  • server/pkg/i18n/locales/entitymsg/en.json
  • server/pkg/i18n/locales/errmsg/en.json
  • server/pkg/i18n/message/errmsg/errmsg_generated.go
  • server/internal/app/i18n/locales/errmsg/en.json
  • server/pkg/i18n/message/entitymsg/entitymsg_generated.go
  • server/internal/app/i18n/locales/errmsg/ja.json
🧰 Additional context used
📓 Learnings (1)
server/pkg/verror/verror.go (1)
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/pkg/verror/verror.go:14-0
Timestamp: 2025-01-16T10:43:15.903Z
Learning: In the verror package, NewVError function:
1. Should not validate or provide fallbacks for invalid keys as issues should be caught during development
2. Accepts nil errors as valid input
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: deploy-server / Deploy app to test env
🔇 Additional comments (6)
server/internal/app/i18n/message/errmsg/errmsg_generated.go (1)

1-7: Verify the code generation process.

Since this is a generated file, please ensure:

  1. The generation source (JSON files) is properly version controlled
  2. The generation process is documented
  3. The go generate command is included in the build pipeline
✅ Verification successful

Code generation setup is properly configured

The error message generation system is well-structured with:

  • Source JSON files in server/internal/app/i18n/locales/errmsg/
  • Generator at server/pkg/i18n/gen/errmsg/gen.go
  • Proper go:generate command in messageg.go
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for the presence of source files and generation command
echo "Checking for source files and generation command..."
fd -e json . "server/internal/app/i18n/locales/errmsg"
rg "//go:generate" "server/internal/app"

Length of output: 407


Script:

#!/bin/bash
# Check if generator exists and examine JSON structure
echo "=== Checking generator ==="
cat pkg/i18n/gen/errmsg/gen.go || echo "Generator not found"

echo -e "\n=== Checking JSON structure ==="
head -n 10 server/internal/app/i18n/locales/errmsg/en.json
head -n 10 server/internal/app/i18n/locales/errmsg/ja.json

Length of output: 915


Script:

#!/bin/bash
# Check for the generator using the correct relative path
echo "=== Checking for generator from message directory ==="
ls -l server/pkg/i18n/gen/errmsg/gen.go || echo "Generator not found in server path"

echo -e "\n=== Checking go:generate command context ==="
rg -A 2 "//go:generate" server/internal/app/i18n/message/messageg.go

Length of output: 492

server/pkg/verror/verror_test.go (1)

14-39: 🛠️ Refactor suggestion

Add test cases for Japanese localization.

The test only covers English localization. Add test cases for Japanese localization to ensure proper i18n support.

Example addition:

 func TestNewVError(t *testing.T) {
     errmsg.ErrorMessages = map[message.ErrKey]map[language.Tag]message.ErrorMessage{
         errmsg.ErrKeyPkgProjectInvalidAlias: {
             language.English: {
                 Message:     "Invalid alias name: {{.aliasName}}",
                 Description: "The alias '{{.aliasName}}' must be {{.minLength}}-{{.maxLength}} characters long.",
             },
+            language.Japanese: {
+                Message:     "無効なエイリアス名: {{.aliasName}}",
+                Description: "エイリアス '{{.aliasName}}' は {{.minLength}}-{{.maxLength}} 文字である必要があります。",
+            },
         },
     }

Likely invalid or redundant comment.

server/internal/app/graphql.go (2)

18-21: LGTM!

The new imports are correctly organized and necessary for implementing localized error handling.


139-139: Review warning log for potential security concerns.

The warning log might expose sensitive information in the GraphQL error. Consider logging only non-sensitive fields or redacting sensitive information.

✅ Verification successful

GraphQL error logging implementation is secure

The warning log is internal and doesn't expose sensitive information to clients. The error handling implementation includes proper sanitization:

  • System errors are only exposed in development mode
  • User-facing messages are controlled through i18n templates
  • GraphQL responses are properly structured with controlled fields
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential sensitive information in GraphQL errors
ast-grep --pattern 'log.$$(_, "graphqlErr: %+v", $_)'

Length of output: 55


Script:

#!/bin/bash
# Search for GraphQL error related code
rg -l "graphqlErr" --type go

# Find error logging patterns
rg "log\.(Warn|Error|Info).*Error" --type go -A 2

# Look for GraphQL error type definitions
rg "type.*Error" --type go | grep -i "graphql"

# Check for error handling middleware or utils
fd -e go -x grep -l "func.*Error" {}

Length of output: 1996


Script:

#!/bin/bash
# Check the GraphQL error context
rg -A 5 -B 5 "graphqlErr" server/internal/app/graphql.go

# Look for error handling patterns
cat server/internal/app/graphql.go

# Check for error sanitization
rg -l "sanitize|redact|mask" --type go

Length of output: 5502

server/pkg/project/project.go (2)

8-13: LGTM!

The new imports are correctly organized and necessary for implementing localized error handling.


216-216: LGTM!

The error handling correctly uses the new localized error pattern with template data.

@soneda-yuya soneda-yuya force-pushed the refactor/error_handling_be branch from ada5a33 to 91e8f7b Compare January 17, 2025 11:27
@soneda-yuya soneda-yuya force-pushed the refactor/error_handling_be branch 7 times, most recently from 51ba37d to eb619f0 Compare January 17, 2025 12:34
@soneda-yuya
Copy link
Contributor Author

some Nitpick comments is fixed.

Copy link

@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

🧹 Nitpick comments (7)
server/pkg/i18n/message/message.go (2)

25-50: Consider improvements to template handling and error reporting.

While the implementation is functional, consider these improvements:

  1. Instead of returning an empty string on errors, consider returning a fallback message or error indicator.
  2. Template parsing could be cached to improve performance.
  3. Add input validation for nil maps to prevent panics.
 func ApplyTemplate(ctx context.Context, tmpl string, data map[language.Tag]map[string]interface{}, locale language.Tag) string {
+	if data == nil {
+		log.Warnfc(ctx, "template data is nil")
+		return "[TEMPLATE_ERROR]"
+	}
+
+	localeData, ok := data[locale]
+	if !ok {
+		log.Warnfc(ctx, "no data for locale: %s", locale)
+		return "[MISSING_LOCALE]"
+	}
+
 	processedData := make(map[string]interface{})
 
-	for key, value := range data[locale] {
+	for key, value := range localeData {

52-57: Add documentation about memory implications.

The implementation is clean, but consider adding documentation about:

  1. Memory implications when dealing with large data sets (as the function creates copies for each locale)
  2. Input validation behavior with nil data
 // MultiLocaleTemplateData creates a map of template data for multiple locales.
+// Note: This function creates a copy of the data for each supported locale,
+// which may have memory implications for large data sets.
+// Returns an empty map if input data is nil.
 func MultiLocaleTemplateData(data map[string]interface{}) map[language.Tag]map[string]interface{} {
+	if data == nil {
+		return make(map[language.Tag]map[string]interface{})
+	}
 	return lo.SliceToMap(i18n.LocaleTypes(), func(locale language.Tag) (language.Tag, map[string]interface{}) {
 		return locale, data
 	})
server/internal/app/graphql_test.go (1)

16-87: Enhance test coverage with additional edge cases.

The test suite is well-structured but could benefit from additional test cases:

  1. Test handling of invalid language tags
  2. Test behavior when error messages map is empty
  3. Test fallback to default language when requested language is unavailable

Consider refactoring the tests using a table-driven approach:

 func TestCustomErrorPresenter(t *testing.T) {
+    tests := []struct {
+        name     string
+        ctx      context.Context
+        err      error
+        devMode  bool
+        want     *gqlerror.Error
+        wantCode string
+    }{
+        {
+            name:    "invalid language tag",
+            ctx:     adapter.AttachLang(context.Background(), language.Make("invalid")),
+            err:     vErr,
+            want:    &gqlerror.Error{Message: "An unknown error occurred."},
+            wantCode: string(errmsg.ErrKeyUnknown),
+        },
+        // ... existing test cases ...
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            got := customErrorPresenter(tt.ctx, tt.err, tt.devMode)
+            assert.Equal(t, tt.want.Message, got.Message)
+            assert.Equal(t, tt.wantCode, got.Extensions["code"])
+        })
+    }
server/pkg/verror/verror_test.go (1)

15-40: Improve test maintainability and coverage.

The test setup could be improved for better maintainability:

  1. Extract test data to constants or test fixtures
  2. Add negative test cases for error handling
  3. Test error message validation

Consider extracting test data:

+const (
+    testAliasName = "test_alias"
+    testMinLength = 5
+    testMaxLength = 32
+)
+
+var (
+    testTemplateData = map[language.Tag]map[string]interface{}{
+        language.English: {
+            "aliasName": testAliasName,
+            "minLength": testMinLength,
+            "maxLength": testMaxLength,
+        },
+    }
+)
+
 func TestNewVError(t *testing.T) {
-    templateData := map[language.Tag]map[string]interface{}{
-        language.English: {
-            "aliasName": "test_alias",
-            "minLength": 5,
-            "maxLength": 32,
-        },
-    }
+    templateData := testTemplateData
server/internal/app/graphql.go (2)

149-154: Improve error logging.

The logging could be more structured and informative:

  1. Log entries should include more context
  2. Warning log for graphqlErr might be too verbose

Consider this improvement:

 if systemError != "" {
-    log.Errorfc(ctx, "system error: %+v", e)
+    log.Errorfc(ctx, "GraphQL system error: %+v, code: %s, path: %v", 
+        e, 
+        graphqlErr.Extensions["code"],
+        graphqlErr.Path,
+    )
 }

-log.Warnfc(ctx, "graphqlErr: %+v", graphqlErr)
+// Only log non-system errors at debug level
+if systemError == "" {
+    log.Debugfc(ctx, "GraphQL error: code=%s, message=%s",
+        graphqlErr.Extensions["code"],
+        graphqlErr.Message,
+    )
+}

65-73: Improve panic recovery logging.

The panic recovery could provide more context about the error that was being processed.

 srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
     defer func() {
         if r := recover(); r != nil {
-            log.Errorfc(ctx, "panic recovered in error presenter: %v", r)
+            log.Errorfc(ctx, "panic recovered in error presenter while processing error: %v, panic: %v", e, r)
             return
         }
     }()
     return customErrorPresenter(ctx, e, dev)
 })
server/internal/usecase/repo/container_errkey_test.go (1)

12-19: Enhance test coverage with specific message validation.

While the test verifies non-empty messages, consider enhancing it with:

  1. Validation of actual message content against expected strings
  2. Testing with different template data variations
  3. Verification of error code consistency across locales

Example enhancement:

 func TestRepoErrResourceNotFound(t *testing.T) {
 	ctx := context.Background()
 	vErr := ErrResourceNotFound
+	testCases := []struct {
+		templateData map[string]interface{}
+		expected    map[i18n.Locale]string
+	}{
+		{
+			templateData: map[string]interface{}{"resource": "test"},
+			expected: map[i18n.Locale]string{
+				i18n.LocaleEn: "Resource test not found",
+				i18n.LocaleJa: "リソース test が見つかりません",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		vErr := vErr.AddTemplateData("resource", tc.templateData["resource"])
 		for _, locale := range i18n.LocaleTypes() {
-			assert.NotEqual(t, "", message.ApplyTemplate(ctx, vErr.ErrMsg[locale].Message, vErr.TemplateData, locale))
+			actual := message.ApplyTemplate(ctx, vErr.ErrMsg[locale].Message, vErr.TemplateData, locale)
+			assert.Equal(t, tc.expected[locale], actual)
 			assert.NotEqual(t, "", message.ApplyTemplate(ctx, vErr.ErrMsg[locale].Description, vErr.TemplateData, locale))
 		}
+	}
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ada5a33 and eb619f0.

⛔ Files ignored due to path filters (2)
  • server/pkg/i18n/gen/entitymsg/gen.go is excluded by !**/gen/**
  • server/pkg/i18n/gen/errmsg/gen.go is excluded by !**/gen/**
📒 Files selected for processing (22)
  • server/e2e/gql_storytelling_test.go (1 hunks)
  • server/internal/app/app.go (3 hunks)
  • server/internal/app/graphql.go (5 hunks)
  • server/internal/app/graphql_test.go (1 hunks)
  • server/internal/app/i18n/locales/errmsg/en.json (1 hunks)
  • server/internal/app/i18n/locales/errmsg/ja.json (1 hunks)
  • server/internal/app/i18n/message/errmsg/errmsg_generated.go (1 hunks)
  • server/internal/app/i18n/message/messageg.go (1 hunks)
  • server/internal/usecase/repo/container.go (1 hunks)
  • server/internal/usecase/repo/container_errkey_test.go (1 hunks)
  • server/pkg/i18n/locales.go (1 hunks)
  • server/pkg/i18n/locales/entitymsg/en.json (1 hunks)
  • server/pkg/i18n/locales/entitymsg/ja.json (1 hunks)
  • server/pkg/i18n/locales/errmsg/en.json (1 hunks)
  • server/pkg/i18n/locales/errmsg/ja.json (1 hunks)
  • server/pkg/i18n/message/entitymsg/entitymsg_generated.go (1 hunks)
  • server/pkg/i18n/message/errmsg/errmsg_generated.go (1 hunks)
  • server/pkg/i18n/message/message.go (1 hunks)
  • server/pkg/project/project.go (2 hunks)
  • server/pkg/project/project_errkey_test.go (1 hunks)
  • server/pkg/verror/verror.go (1 hunks)
  • server/pkg/verror/verror_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (16)
  • server/internal/app/app.go
  • server/pkg/i18n/locales.go
  • server/internal/app/i18n/message/messageg.go
  • server/pkg/i18n/locales/errmsg/ja.json
  • server/pkg/i18n/locales/entitymsg/ja.json
  • server/pkg/i18n/locales/entitymsg/en.json
  • server/e2e/gql_storytelling_test.go
  • server/internal/app/i18n/message/errmsg/errmsg_generated.go
  • server/internal/usecase/repo/container.go
  • server/pkg/i18n/message/entitymsg/entitymsg_generated.go
  • server/pkg/i18n/locales/errmsg/en.json
  • server/pkg/i18n/message/errmsg/errmsg_generated.go
  • server/internal/app/i18n/locales/errmsg/en.json
  • server/pkg/project/project.go
  • server/internal/app/i18n/locales/errmsg/ja.json
  • server/pkg/verror/verror.go
🧰 Additional context used
📓 Learnings (2)
server/internal/app/graphql.go (1)
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:0-0
Timestamp: 2025-01-17T12:40:02.613Z
Learning: In the GraphQL error presenter, when a localized error message is not available for the requested language, the code falls back to using graphql.DefaultErrorPresenter(ctx, e) instead of attempting to use alternative languages. This ensures that error messages are always presented, even when localization is not available.
server/internal/app/graphql_test.go (1)
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:0-0
Timestamp: 2025-01-17T12:40:02.613Z
Learning: In the GraphQL error presenter, when a localized error message is not available for the requested language, the code falls back to using graphql.DefaultErrorPresenter(ctx, e) instead of attempting to use alternative languages. This ensures that error messages are always presented, even when localization is not available.
🔇 Additional comments (3)
server/pkg/i18n/message/message.go (2)

16-23: Well-structured type definitions for i18n messages.

The type definitions provide a clear separation between entity and error messages, with ErrorMessage properly separating the message from its description. This aligns well with the PR's goal of centralizing message management.


1-14: Verify the existence and functionality of message generators.

Let's ensure the referenced generator scripts exist and are functional.

✅ Verification successful

Generator scripts and their dependencies are properly configured

The message generation system is complete with:

  • Functional generator scripts
  • Required locale files (en, ja)
  • Generated output files in place
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the generator scripts exist
fd -t f "gen\.go$" server/pkg/i18n/gen/

# Check their content to understand the generation logic
for f in $(fd -t f "gen\.go$" server/pkg/i18n/gen/); do
  echo "=== $f ==="
  cat "$f"
done

Length of output: 11144


Script:

#!/bin/bash
# Check if the locale directories and JSON files exist
echo "=== Checking locale directories ==="
fd -t d "locales" server/pkg/i18n/
fd -t f "\.json$" server/pkg/i18n/locales/

# Check if the generated files exist
echo -e "\n=== Checking generated files ==="
fd -t f "generated\.go$" server/pkg/i18n/

Length of output: 580

server/pkg/project/project_errkey_test.go (1)

7-10: Consider adding test coverage for missing locales.

The current tests only verify existing locales. Consider adding tests for:

  1. Handling of unsupported locales
  2. Fallback behavior when a locale is missing

@soneda-yuya soneda-yuya force-pushed the refactor/error_handling_be branch from eb619f0 to 0ae0aed Compare January 17, 2025 16:21
Copy link

@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 (4)
server/pkg/verror/verror_test.go (3)

15-40: Consider extracting test data to constants.

The test data (error messages, template data) could be moved to test constants for better maintainability and reuse across tests.

+const (
+    testErrCode = "invalid_alias"
+    testErrMsg  = "Invalid alias name: {{.aliasName}}"
+    testErrDesc = "The alias '{{.aliasName}}' must be {{.minLength}}-{{.maxLength}} characters long."
+)
+
+var testTemplateData = map[language.Tag]map[string]interface{}{
+    language.English: {
+        "aliasName": "test_alias",
+        "minLength": 5,
+        "maxLength": 32,
+    },
+}

 func TestNewVError(t *testing.T) {
     errmsg.ErrorMessages = map[message.ErrKey]map[language.Tag]message.ErrorMessage{
         errmsg.ErrKeyPkgProjectInvalidAlias: {
             language.English: {
-                Message:     "Invalid alias name: {{.aliasName}}",
-                Description: "The alias '{{.aliasName}}' must be {{.minLength}}-{{.maxLength}} characters long.",
+                Message:     testErrMsg,
+                Description: testErrDesc,
             },
         },
     }

-    templateData := map[language.Tag]map[string]interface{}{
-        language.English: {
-            "aliasName": "test_alias",
-            "minLength": 5,
-            "maxLength": 32,
-        },
-    }

42-59: Convert to table-driven test for better test coverage.

Consider using table-driven tests to cover more test cases and make it easier to add new ones.

 func TestAddTemplateData(t *testing.T) {
+    tests := []struct {
+        name     string
+        data     []struct{ key string; value interface{} }
+        expected map[language.Tag]map[string]interface{}
+    }{
+        {
+            name: "multiple values",
+            data: []struct{ key string; value interface{} }{
+                {key: "key1", value: "value1"},
+                {key: "key2", value: 123},
+            },
+            expected: map[language.Tag]map[string]interface{}{
+                language.English: {
+                    "key1": "value1",
+                    "key2": 123,
+                },
+                language.Japanese: {
+                    "key1": "value1",
+                    "key2": 123,
+                },
+            },
+        },
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
             ve := verror.NewVError(errmsg.ErrKeyPkgProjectInvalidAlias, errmsg.ErrorMessages[errmsg.ErrKeyPkgProjectInvalidAlias], nil, nil)
-            ve = ve.AddTemplateData("key1", "value1")
-            ve = ve.AddTemplateData("key2", 123)
+            for _, d := range tt.data {
+                ve = ve.AddTemplateData(d.key, d.value)
+            }
-            expectedData := map[language.Tag]map[string]interface{}{
-                language.English: {
-                    "key1": "value1",
-                    "key2": 123,
-                },
-                language.Japanese: {
-                    "key1": "value1",
-                    "key2": 123,
-                },
-            }
-            assert.Equal(t, expectedData, ve.TemplateData)
+            assert.Equal(t, tt.expected, ve.TemplateData)
+        })
+    }
 }

61-75: Add test cases for error scenarios.

Consider adding test cases for:

  1. Missing template variables
  2. Invalid template syntax
  3. Empty template string
 func TestApplyTemplate(t *testing.T) {
+    tests := []struct {
+        name           string
+        template      string
+        data          map[language.Tag]map[string]interface{}
+        expected      string
+        expectError   bool
+    }{
+        {
+            name: "valid template",
+            template: "Invalid alias name: {{.aliasName}}, must be between {{.minLength}} and {{.maxLength}} characters.",
+            data: map[language.Tag]map[string]interface{}{
+                language.English: {
+                    "aliasName": "test_alias",
+                    "minLength": 5,
+                    "maxLength": 32,
+                },
+            },
+            expected: "Invalid alias name: test_alias, must be between 5 and 32 characters.",
+        },
+        {
+            name: "missing variable",
+            template: "Invalid alias name: {{.missing}}",
+            data: map[language.Tag]map[string]interface{}{
+                language.English: {},
+            },
+            expected: "Invalid alias name: <no value>",
+        },
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            ctx := context.Background()
+            result := message.ApplyTemplate(ctx, tt.template, tt.data, language.English)
+            assert.Equal(t, tt.expected, result)
+        })
+    }
server/internal/app/graphql.go (1)

102-156: Consider enhancing error handling and logging.

While the implementation is solid, consider these improvements:

  1. Add structured logging fields for better error tracking
  2. Consider using constants for extension keys
  3. Add trace ID to extensions in development mode for better debugging
+const (
+    extensionKeyCode        = "code"
+    extensionKeyMessage     = "message"
+    extensionKeyDescription = "description"
+    extensionKeySystemError = "system_error"
+)

 func customErrorPresenter(ctx context.Context, e error, devMode bool) *gqlerror.Error {
     // ... existing code ...

     if devMode {
         if fieldCtx := graphql.GetFieldContext(ctx); fieldCtx != nil {
             graphqlErr.Path = fieldCtx.Path()
         } else {
             graphqlErr.Path = ast.Path{}
         }

-        graphqlErr.Extensions["system_error"] = systemError
+        graphqlErr.Extensions[extensionKeySystemError] = systemError
+        // Add trace ID for better debugging
+        if traceID := log.GetTraceID(ctx); traceID != "" {
+            graphqlErr.Extensions["trace_id"] = traceID
+        }
     }

     if systemError != "" {
-        log.Errorfc(ctx, "system error: %+v", e)
+        log.Errorfc(ctx, "system error",
+            "error", e,
+            "code", graphqlErr.Extensions[extensionKeyCode],
+            "path", graphqlErr.Path,
+        )
     }

-    log.Warnfc(ctx, "graphqlErr: %+v", graphqlErr)
+    log.Warnfc(ctx, "graphql error occurred",
+        "message", graphqlErr.Message,
+        "path", graphqlErr.Path,
+        "extensions", graphqlErr.Extensions,
+    )

     return graphqlErr
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb619f0 and 0ae0aed.

⛔ Files ignored due to path filters (2)
  • server/pkg/i18n/gen/entitymsg/gen.go is excluded by !**/gen/**
  • server/pkg/i18n/gen/errmsg/gen.go is excluded by !**/gen/**
📒 Files selected for processing (7)
  • server/internal/app/graphql.go (5 hunks)
  • server/internal/app/i18n/locales/errmsg/en.json (1 hunks)
  • server/internal/app/i18n/locales/errmsg/ja.json (1 hunks)
  • server/internal/usecase/repo/container_errkey_test.go (1 hunks)
  • server/pkg/i18n/message/message.go (1 hunks)
  • server/pkg/verror/verror.go (1 hunks)
  • server/pkg/verror/verror_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/internal/usecase/repo/container_errkey_test.go
  • server/pkg/i18n/message/message.go
  • server/internal/app/i18n/locales/errmsg/ja.json
  • server/internal/app/i18n/locales/errmsg/en.json
  • server/pkg/verror/verror.go
🧰 Additional context used
📓 Learnings (1)
server/internal/app/graphql.go (2)
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:0-0
Timestamp: 2025-01-17T12:40:02.613Z
Learning: In the GraphQL error presenter, when a localized error message is not available for the requested language, the code falls back to using graphql.DefaultErrorPresenter(ctx, e) instead of attempting to use alternative languages. This ensures that error messages are always presented, even when localization is not available.
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:109-125
Timestamp: 2025-01-17T12:51:23.131Z
Learning: In the GraphQL error presenter, when handling VError with translations, if the error message generation fails (e.g., missing translation), fallback to graphql.DefaultErrorPresenter(ctx, e) by checking if graphqlErr is nil. This pattern is more maintainable than implementing complex language fallback logic.
🔇 Additional comments (4)
server/pkg/verror/verror_test.go (2)

103-108: LGTM!

The test is well-focused and correctly verifies the error unwrapping functionality.


77-101: 🛠️ Refactor suggestion

Add test cases for Japanese localization.

The test only covers English localization. Add test cases for Japanese to ensure proper i18n support.

 func TestError(t *testing.T) {
-    en := language.English
+    tests := []struct {
+        name     string
+        lang     language.Tag
+        message  string
+        desc     string
+        expected string
+    }{
+        {
+            name: "English localization",
+            lang: language.English,
+            message: "Invalid alias name: {{.aliasName}}",
+            desc: "The alias '{{.aliasName}}' must be {{.minLength}}-{{.maxLength}} characters long.",
+            expected: "Invalid alias name: test_alias",
+        },
+        {
+            name: "Japanese localization",
+            lang: language.Japanese,
+            message: "無効なエイリアス名: {{.aliasName}}",
+            desc: "エイリアス '{{.aliasName}}' は {{.minLength}}-{{.maxLength}} 文字である必要があります。",
+            expected: "無効なエイリアス名: test_alias",
+        },
+    }

-    errmsg.ErrorMessages = map[message.ErrKey]map[language.Tag]message.ErrorMessage{
-        errmsg.ErrKeyPkgProjectInvalidAlias: {
-            en: {
-                Message:     "Invalid alias name: {{.aliasName}}",
-                Description: "The alias '{{.aliasName}}' must be {{.minLength}}-{{.maxLength}} characters long.",
-            },
-        },
-    }
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            errmsg.ErrorMessages = map[message.ErrKey]map[language.Tag]message.ErrorMessage{
+                errmsg.ErrKeyPkgProjectInvalidAlias: {
+                    tt.lang: {
+                        Message:     tt.message,
+                        Description: tt.desc,
+                    },
+                },
+            }

Likely invalid or redundant comment.

server/internal/app/graphql.go (2)

65-73: LGTM! Well-implemented error handling with panic recovery.

The error presenter setup with panic recovery is a good practice to prevent crashes.


93-102: LGTM! Clear and comprehensive documentation.

The function documentation follows Go standards and clearly describes the purpose, parameters, and return values.

Err error
}

// GetErrCode returns the error code.
Copy link
Contributor

@pyshx pyshx Jan 22, 2025

Choose a reason for hiding this comment

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

remove

Copy link

@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 (4)
server/internal/app/graphql.go (4)

5-5: Consider organizing imports into logical groups.

Consider organizing imports into standard library, external packages, and internal packages for better readability:

import (
    // Standard library
    "context"
    "errors"
    "time"

    // External packages
    "github.com/99designs/gqlgen/graphql"
    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/handler/extension"
    "github.com/99designs/gqlgen/graphql/handler/lru"
    "github.com/99designs/gqlgen/graphql/handler/transport"
    "github.com/labstack/echo/v4"
    "github.com/ravilushqa/otelgqlgen"
    "github.com/vektah/gqlparser/v2/ast"
    "github.com/vektah/gqlparser/v2/gqlerror"
    "golang.org/x/text/language"

    // Internal packages
    "github.com/reearth/reearth/server/internal/adapter"
    "github.com/reearth/reearth/server/internal/adapter/gql"
    "github.com/reearth/reearth/server/internal/app/config"
    "github.com/reearth/reearth/server/pkg/i18n/message"
    "github.com/reearth/reearth/server/pkg/verror"
    "github.com/reearth/reearthx/log"
)

Also applies to: 18-23


65-73: Enhance panic recovery logging.

The panic recovery logging could be more detailed to aid in debugging:

     srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
         defer func() {
             if r := recover(); r != nil {
-                log.Errorfc(ctx, "panic recovered in error presenter: %v", r)
+                log.Errorfc(ctx, "panic recovered in error presenter: %v, original error: %+v, stack: %s", r, e, debug.Stack())
                 return
             }
         }()
         return customErrorPresenter(ctx, e, dev)
     })

93-94: Remove unnecessary function comment.

Based on past review comments, the implementation is self-documenting and doesn't need a long comment.

-// customErrorPresenter handles custom GraphQL error presentation by converting various error types
-// into localized GraphQL errors.
 func customErrorPresenter(ctx context.Context, e error, devMode bool) *gqlerror.Error {

142-147: Adjust logging levels based on error severity.

Currently, all errors are logged at WARN level, which might create noise in logs. Consider:

  1. Log system errors at ERROR level (already done)
  2. Log validation/business errors at INFO level
  3. Only log unexpected errors at WARN level
     if systemError != "" {
         log.Errorfc(ctx, "system error: %+v", e)
+        log.Warnfc(ctx, "graphqlErr: %+v", graphqlErr)
+    } else if vError != nil {
+        log.Infofc(ctx, "graphqlErr: %+v", graphqlErr)
+    } else {
+        log.Warnfc(ctx, "unexpected graphqlErr: %+v", graphqlErr)
     }
-
-    log.Warnfc(ctx, "graphqlErr: %+v", graphqlErr)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae0aed and d06586b.

📒 Files selected for processing (2)
  • server/internal/app/graphql.go (5 hunks)
  • server/pkg/verror/verror.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/pkg/verror/verror.go
🧰 Additional context used
📓 Learnings (1)
server/internal/app/graphql.go (2)
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:0-0
Timestamp: 2025-01-17T12:40:02.613Z
Learning: In the GraphQL error presenter, when a localized error message is not available for the requested language, the code falls back to using graphql.DefaultErrorPresenter(ctx, e) instead of attempting to use alternative languages. This ensures that error messages are always presented, even when localization is not available.
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:109-125
Timestamp: 2025-01-17T12:51:23.131Z
Learning: In the GraphQL error presenter, when handling VError with translations, if the error message generation fails (e.g., missing translation), fallback to graphql.DefaultErrorPresenter(ctx, e) by checking if graphqlErr is nil. This pattern is more maintainable than implementing complex language fallback logic.
🔇 Additional comments (1)
server/internal/app/graphql.go (1)

102-118: LGTM! Robust error handling implementation.

The error handling implementation correctly:

  1. Handles localization with proper fallback (as confirmed by retrieved learnings)
  2. Safely handles template data
  3. Includes proper error code and description

@soneda-yuya soneda-yuya requested a review from pyshx January 22, 2025 07:18
var vError *verror.VError
lang := adapter.Lang(ctx, nil)

// Handle application-specific errors
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

systemError = e.Error()
}

// Ensure Extensions map exists
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

}

if graphqlErr == nil {
// Fallback to default GraphQL error presenter
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

graphqlErr.Extensions = make(map[string]interface{})
}

// Add debugging information in development mode
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

Comment on lines 16 to 18
// Extract user language from the adapter
// if user language is not "und", use user language
// if user language is "und", use browser language
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

return tag
}

// AttachLanguageMiddleware attaches the detected language to the request context.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

"golang.org/x/text/language"
)

// VError represents an error with a code identifier.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

return parts[len(parts)-1]
}

// NewVError creates a new VError with the given key and error.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

}
}

// AddTemplateData adds template data to the cloned VError.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

return clone
}

// Error returns the error message.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

return ""
}

// Unwrap returns the underlying error.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

Copy link

@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 (4)
server/internal/app/graphql.go (4)

5-5: Consider organizing imports by groups.

Consider organizing imports into standard library, external packages, and internal packages for better readability:

import (
	"context"
	"errors"
	"time"

	"golang.org/x/text/language"

	"github.com/99designs/gqlgen/graphql"
	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/handler/extension"
	"github.com/99designs/gqlgen/graphql/handler/lru"
	"github.com/99designs/gqlgen/graphql/handler/transport"
	"github.com/labstack/echo/v4"
	"github.com/ravilushqa/otelgqlgen"
	"github.com/vektah/gqlparser/v2/ast"
	"github.com/vektah/gqlparser/v2/gqlerror"

	"github.com/reearth/reearth/server/internal/adapter"
	"github.com/reearth/reearth/server/internal/adapter/gql"
	"github.com/reearth/reearth/server/internal/app/config"
	"github.com/reearth/reearth/server/pkg/i18n/message"
	"github.com/reearth/reearth/server/pkg/verror"
	"github.com/reearth/reearthx/log"
)

Also applies to: 18-21, 23-23


93-94: Remove unnecessary function comment.

Based on past review comments from pyshx, we should remove long comments as the implementation is already understandable.

-// customErrorPresenter handles custom GraphQL error presentation by converting various error types
-// into localized GraphQL errors.
 func customErrorPresenter(ctx context.Context, e error, devMode bool) *gqlerror.Error {

108-111: Consider defining error codes as constants.

Error codes should be defined as constants to ensure consistency and prevent typos:

+const (
+    ExtCodeKey        = "code"
+    ExtMessageKey     = "message"
+    ExtDescriptionKey = "description"
+)

 Extensions: map[string]interface{}{
-    "code":        vError.GetErrCode(),
-    "message":     messageText,
-    "description": message.ApplyTemplate(ctx, errMsg.Description, vError.TemplateData, language.Make(lang)),
+    ExtCodeKey:        vError.GetErrCode(),
+    ExtMessageKey:     messageText,
+    ExtDescriptionKey: message.ApplyTemplate(ctx, errMsg.Description, vError.TemplateData, language.Make(lang)),
 }

138-143: Adjust logging levels for better observability.

Consider adjusting the logging levels:

  1. System errors should remain at ERROR level
  2. GraphQL errors could be at DEBUG level in production as they're expected errors
 if systemError != "" {
 	log.Errorfc(ctx, "system error: %+v", e)
 }

-log.Warnfc(ctx, "graphqlErr: %+v", graphqlErr)
+if devMode {
+    log.Warnfc(ctx, "graphqlErr: %+v", graphqlErr)
+} else {
+    log.Debugfc(ctx, "graphqlErr: %+v", graphqlErr)
+}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d06586b and ad860b5.

📒 Files selected for processing (3)
  • server/internal/app/graphql.go (5 hunks)
  • server/internal/app/lang.go (1 hunks)
  • server/pkg/verror/verror.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/internal/app/lang.go
  • server/pkg/verror/verror.go
🧰 Additional context used
📓 Learnings (1)
server/internal/app/graphql.go (2)
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:0-0
Timestamp: 2025-01-17T12:40:02.613Z
Learning: In the GraphQL error presenter, when a localized error message is not available for the requested language, the code falls back to using graphql.DefaultErrorPresenter(ctx, e) instead of attempting to use alternative languages. This ensures that error messages are always presented, even when localization is not available.
Learnt from: soneda-yuya
PR: reearth/reearth-visualizer#1342
File: server/internal/app/graphql.go:109-125
Timestamp: 2025-01-17T12:51:23.131Z
Learning: In the GraphQL error presenter, when handling VError with translations, if the error message generation fails (e.g., missing translation), fallback to graphql.DefaultErrorPresenter(ctx, e) by checking if graphqlErr is nil. This pattern is more maintainable than implementing complex language fallback logic.
🔇 Additional comments (1)
server/internal/app/graphql.go (1)

65-73: LGTM! Robust error handling with panic recovery.

The error presenter implementation is well-designed with:

  • Panic recovery to prevent crashes
  • Proper logging of recovered panics
  • Clean delegation to customErrorPresenter

@soneda-yuya soneda-yuya merged commit 70d3dcd into main Jan 22, 2025
17 checks passed
@soneda-yuya soneda-yuya deleted the refactor/error_handling_be branch January 22, 2025 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants