diff --git a/pkg/api/api/grpc_status_test.go b/pkg/api/api/grpc_status_test.go index 07d1b07db5..d32503650b 100644 --- a/pkg/api/api/grpc_status_test.go +++ b/pkg/api/api/grpc_status_test.go @@ -142,6 +142,17 @@ func TestNewGRPCStatus(t *testing.T) { "messagekey": "InternalServerError", }, }, + { + name: "FailedPrecondition with messageKey override", + err: pkgErr.NewErrorFailedPrecondition("test", "comment required"). + WithMessageKey("CommentRequiredForUpdating"), + expectedCode: codes.FailedPrecondition, + expectedMessage: "test:comment required", + expectedReason: "FAILED_PRECONDITION", + expectedMetadata: map[string]string{ + "messagekey": "CommentRequiredForUpdating", + }, + }, { name: "Non-BucketeerError", err: errors.New("standard error"), diff --git a/pkg/autoops/api/error.go b/pkg/autoops/api/error.go index ecafcde119..b05e45b826 100644 --- a/pkg/autoops/api/error.go +++ b/pkg/autoops/api/error.go @@ -219,13 +219,13 @@ var ( statusProgressiveRolloutWaitingOrRunningExperimentExists = api.NewGRPCStatus( pkgErr.NewErrorFailedPrecondition( pkgErr.AutoopsPackageName, - "cannot create a progressive rollout when there is a scheduled or running experiment", - )) + "cannot create a progressive rollout when there is a scheduled or running experiment"). + WithMessageKey("AutoOpsWaitingOrRunningExperimentExists")) statusProgressiveRolloutInsufficientVariations = api.NewGRPCStatus( pkgErr.NewErrorFailedPrecondition( pkgErr.AutoopsPackageName, - "the feature must have at least 2 variations when creating a progressive rollout", - )) + "the feature must have at least 2 variations when creating a progressive rollout"). + WithMessageKey("AutoOpsInvalidVariationSize")) statusProgressiveRolloutControlVariationRequired = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty( pkgErr.AutoopsPackageName, @@ -241,8 +241,8 @@ var ( statusProgressiveRolloutVariationsMustBeDifferent = api.NewGRPCStatus( pkgErr.NewErrorFailedPrecondition( pkgErr.AutoopsPackageName, - "control and target variations must be different for a progressive rollout", - )) + "control and target variations must be different for a progressive rollout"). + WithMessageKey("ProgressiveRolloutVariationsMustBeDifferent")) statusProgressiveRolloutControlVariationNotFound = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty( pkgErr.AutoopsPackageName, @@ -258,8 +258,8 @@ var ( statusProgressiveRolloutInvalidScheduleSpans = api.NewGRPCStatus( pkgErr.NewErrorFailedPrecondition( pkgErr.AutoopsPackageName, - "the span of time for each scheduled time must be at least 5 minutes for a progressive rollout", - )) + "the span of time for each scheduled time must be at least 5 minutes for a progressive rollout"). + WithMessageKey("AutoOpsInvalidScheduleSpans")) statusProgressiveRolloutScheduleExecutedAtRequired = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty( pkgErr.AutoopsPackageName, diff --git a/pkg/environment/api/error.go b/pkg/environment/api/error.go index 087dbab6df..685db2b95e 100644 --- a/pkg/environment/api/error.go +++ b/pkg/environment/api/error.go @@ -89,8 +89,8 @@ var ( statusCannotUpdateSystemAdmin = api.NewGRPCStatus( pkgErr.NewErrorFailedPrecondition( pkgErr.EnvironmentPackageName, - "cannot update system admin organization", - )) + "cannot update system admin organization"). + WithMessageKey("CannotUpdateSystemAdminOrganizationError")) statusEnvironmentNotFound = api.NewGRPCStatus( pkgErr.NewErrorNotFound(pkgErr.EnvironmentPackageName, "environment not found", "Environment")) statusProjectNotFound = api.NewGRPCStatus( @@ -104,7 +104,8 @@ var ( statusOrganizationAlreadyExists = api.NewGRPCStatus( pkgErr.NewErrorAlreadyExists(pkgErr.EnvironmentPackageName, "organization already exists")) statusProjectDisabled = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.EnvironmentPackageName, "project disabled")) + pkgErr.NewErrorFailedPrecondition(pkgErr.EnvironmentPackageName, "project disabled"). + WithMessageKey("ProjectDisabled")) statusUnauthenticated = api.NewGRPCStatus( pkgErr.NewErrorUnauthenticated(pkgErr.EnvironmentPackageName, "unauthenticated")) statusPermissionDenied = api.NewGRPCStatus( @@ -112,9 +113,11 @@ var ( statusAccountNotFound = api.NewGRPCStatus( pkgErr.NewErrorNotFound(pkgErr.EnvironmentPackageName, "account not found", "Account")) statusDemoSiteDisabled = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.EnvironmentPackageName, "demo site is not enabled")) + pkgErr.NewErrorFailedPrecondition(pkgErr.EnvironmentPackageName, "demo site is not enabled"). + WithMessageKey("DemoSiteNotEnabled")) statusUserAlreadyInOrganization = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.EnvironmentPackageName, "user already in organization")) + pkgErr.NewErrorFailedPrecondition(pkgErr.EnvironmentPackageName, "user already in organization"). + WithMessageKey("UserAlreadyInOrganization")) statusInvalidAutoArchiveUnusedDays = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgNotMatchFormat( pkgErr.EnvironmentPackageName, diff --git a/pkg/error/error.go b/pkg/error/error.go index bbfd6e7140..b78d63ffb3 100644 --- a/pkg/error/error.go +++ b/pkg/error/error.go @@ -80,13 +80,24 @@ type BktError struct { wrappedError error field string // optional - embeddedKeyValues map[string]string + embeddedKeyValues map[string]string + messageKeyOverride string } func (e *BktError) PackageName() string { return e.packageName } func (e *BktError) ErrorType() ErrorType { return e.errorType } -func (e *BktError) MessageKey() string { return string(e.errorType) } +func (e *BktError) MessageKey() string { + if e.messageKeyOverride != "" { + return e.messageKeyOverride + } + return string(e.errorType) +} + +func (e *BktError) WithMessageKey(key string) *BktError { + e.messageKeyOverride = key + return e +} func (e *BktError) EmbeddedKeyValues() map[string]string { return e.embeddedKeyValues } func (e *BktError) Error() string { diff --git a/pkg/error/error_test.go b/pkg/error/error_test.go index 60c5f0e5db..5b7685959f 100644 --- a/pkg/error/error_test.go +++ b/pkg/error/error_test.go @@ -275,6 +275,31 @@ func TestErrorType_String(t *testing.T) { } } +func TestMessageKey(t *testing.T) { + t.Parallel() + + t.Run("default returns error type", func(t *testing.T) { + t.Parallel() + err := NewErrorFailedPrecondition("test", "precondition failed") + assert.Equal(t, "FailedPreconditionError", err.MessageKey()) + }) + + t.Run("override replaces default", func(t *testing.T) { + t.Parallel() + err := NewErrorFailedPrecondition("test", "comment required"). + WithMessageKey("CommentRequiredForUpdating") + assert.Equal(t, "CommentRequiredForUpdating", err.MessageKey()) + assert.Equal(t, ErrorTypeFailedPrecondition, err.ErrorType()) + }) + + t.Run("empty override falls back to default", func(t *testing.T) { + t.Parallel() + err := NewErrorFailedPrecondition("test", "generic"). + WithMessageKey("") + assert.Equal(t, "FailedPreconditionError", err.MessageKey()) + }) +} + func TestErrorWrapComplex(t *testing.T) { t.Parallel() diff --git a/pkg/feature/api/error.go b/pkg/feature/api/error.go index d5216d5ccf..bfe2af8aaf 100644 --- a/pkg/feature/api/error.go +++ b/pkg/feature/api/error.go @@ -65,7 +65,8 @@ var ( statusMissingFeatureTags = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty(pkgErr.FeaturePackageName, "feature must contain one or more tags", "Tag")) statusCommentRequiredForUpdating = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "a comment is required for updating")) + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "a comment is required for updating"). + WithMessageKey("CommentRequiredForUpdating")) statusMissingSegmentID = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty(pkgErr.FeaturePackageName, "missing segment id", "Segment")) statusMissingSegmentUsersData = api.NewGRPCStatus( @@ -121,11 +122,14 @@ var ( statusAlreadyExists = api.NewGRPCStatus( pkgErr.NewErrorAlreadyExists(pkgErr.FeaturePackageName, "already exists")) statusSegmentUsersAlreadyUploading = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "segment users already uploading")) - statusSegmentStatusNotSuceeded = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "segment status is not suceeded")) + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "segment users already uploading"). + WithMessageKey("SegmentUsersAlreadyUploading")) + statusSegmentStatusNotSucceeded = api.NewGRPCStatus( + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "segment status is not succeeded"). + WithMessageKey("SegmentStatusNotSucceeded")) statusSegmentInUse = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "segment is in use")) + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "segment is in use"). + WithMessageKey("SegmentInUse")) // segment rules statusExceededMaxSegmentRules = api.NewGRPCStatus( pkgErr.NewErrorExceededMax( @@ -202,11 +206,13 @@ var ( statusPermissionDenied = api.NewGRPCStatus( pkgErr.NewErrorPermissionDenied(pkgErr.FeaturePackageName, "permission denied")) statusWaitingOrRunningExperimentExists = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "experiment in waiting or running status exists")) + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "experiment in waiting or running status exists"). + WithMessageKey("HasWaitingOrRunningExperiment")) statusInvalidArchive = api.NewGRPCStatus( pkgErr.NewErrorFailedPrecondition( pkgErr.FeaturePackageName, - "can't archive because this feature is used as a prerequsite")) + "can't archive because this feature is used as a prerequisite"). + WithMessageKey("InvalidArchive")) // flag trigger statusMissingTriggerFeatureID = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty(pkgErr.FeaturePackageName, "missing trigger feature id", "FeatureFlagID")) @@ -219,7 +225,8 @@ var ( statusSecretRequired = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty(pkgErr.FeaturePackageName, "trigger secret is required", "TriggerSecret")) statusTriggerAlreadyDisabled = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "trigger already disabled")) + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "trigger already disabled"). + WithMessageKey("TriggerAlreadyDisabled")) statusTriggerNotFound = api.NewGRPCStatus( pkgErr.NewErrorNotFound(pkgErr.FeaturePackageName, "trigger not found", "FlagTrigger")) statusTriggerDisableFailed = api.NewGRPCStatus( @@ -278,7 +285,8 @@ var ( statusScheduledFlagChangeNotFound = api.NewGRPCStatus( pkgErr.NewErrorNotFound(pkgErr.FeaturePackageName, "scheduled flag change not found", "ScheduledFlagChange")) statusScheduledFlagChangeNotPending = api.NewGRPCStatus( - pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "scheduled flag change is not pending")) + pkgErr.NewErrorFailedPrecondition(pkgErr.FeaturePackageName, "scheduled flag change is not pending"). + WithMessageKey("ScheduledFlagChangeNotPending")) statusEmptyPayload = api.NewGRPCStatus( pkgErr.NewErrorInvalidArgEmpty(pkgErr.FeaturePackageName, "payload must contain at least one change", "Payload")) statusInvalidVariationReference = api.NewGRPCStatus( diff --git a/pkg/feature/api/segment_user.go b/pkg/feature/api/segment_user.go index 8913683710..c3b8816633 100644 --- a/pkg/feature/api/segment_user.go +++ b/pkg/feature/api/segment_user.go @@ -249,7 +249,7 @@ func (s *FeatureService) BulkDownloadSegmentUsers( return nil, api.NewGRPCStatus(err).Err() } if segment.Status != featureproto.Segment_SUCEEDED { - return nil, statusSegmentStatusNotSuceeded.Err() + return nil, statusSegmentStatusNotSucceeded.Err() } stateVal := int32(req.State) users, _, err := s.segmentUserStorage.ListSegmentUsers( diff --git a/pkg/feature/api/segment_user_test.go b/pkg/feature/api/segment_user_test.go index 63c9699fb8..ebd609d057 100644 --- a/pkg/feature/api/segment_user_test.go +++ b/pkg/feature/api/segment_user_test.go @@ -275,7 +275,7 @@ func TestBulkDownloadSegmentUsersMySQL(t *testing.T) { expectedErr: statusSegmentNotFound.Err(), }, { - desc: "ErrSegmentStatusNotSuceeded", + desc: "ErrSegmentStatusNotSucceeded", setup: func(s *FeatureService) { s.segmentStorage.(*storagemock.MockSegmentStorage).EXPECT().GetSegment( gomock.Any(), gomock.Any(), gomock.Any(), @@ -286,7 +286,7 @@ func TestBulkDownloadSegmentUsersMySQL(t *testing.T) { environmentId: "ns0", segmentID: "id", state: featureproto.SegmentUser_INCLUDED, - expectedErr: statusSegmentStatusNotSuceeded.Err(), + expectedErr: statusSegmentStatusNotSucceeded.Err(), }, } for _, tc := range testcases { diff --git a/pkg/notification/api/error.go b/pkg/notification/api/error.go index edf1cbd24a..0cc8be783a 100644 --- a/pkg/notification/api/error.go +++ b/pkg/notification/api/error.go @@ -71,7 +71,8 @@ var ( statusNotificationAlreadyPublished = api.NewGRPCStatus( bkterr.NewErrorFailedPrecondition( bkterr.NotificationPackageName, - "notification is already published")) + "notification is already published"). + WithMessageKey("NotificationAlreadyPublished")) statusInvalidCursor = api.NewGRPCStatus( bkterr.NewErrorInvalidArgNotMatchFormat( bkterr.NotificationPackageName, diff --git a/pkg/subscriber/processor/segment_user_persister.go b/pkg/subscriber/processor/segment_user_persister.go index 75cc4b97a3..8f2eee9982 100644 --- a/pkg/subscriber/processor/segment_user_persister.go +++ b/pkg/subscriber/processor/segment_user_persister.go @@ -324,7 +324,7 @@ func (p *segmentUserPersister) handleChunk(ctx context.Context, chunk map[string } msg.Ack() p.logger.Debug( - "suceeded to persist segment users", + "succeeded to persist segment users", zap.String("msgID", msg.ID), zap.String("environmentId", event.EnvironmentId), zap.String("segmentId", event.SegmentId), diff --git a/pkg/tag/api/error.go b/pkg/tag/api/error.go index 33a82668c4..536ec81c7f 100644 --- a/pkg/tag/api/error.go +++ b/pkg/tag/api/error.go @@ -27,7 +27,8 @@ var ( err.NewErrorInvalidArgEmpty(err.TagPackageName, "entity_type must be specified", "EntityType"), ) statusTagInUsed = api.NewGRPCStatus( - err.NewErrorFailedPrecondition(err.TagPackageName, "tag is in use")) + err.NewErrorFailedPrecondition(err.TagPackageName, "tag is in use"). + WithMessageKey("TagInUse")) statusInvalidCursor = api.NewGRPCStatus( err.NewErrorInvalidArgNotMatchFormat(err.TagPackageName, "cursor is invalid", "Cursor"), ) diff --git a/test/e2e/feature/feature_auto_archive_test.go b/test/e2e/feature/feature_auto_archive_test.go index cad51e3462..f8d0532ac0 100644 --- a/test/e2e/feature/feature_auto_archive_test.go +++ b/test/e2e/feature/feature_auto_archive_test.go @@ -473,7 +473,7 @@ func executeBatchJob(t *testing.T, client btclient.Client, job btproto.BatchJob) // FailedPrecondition errors are expected when the environment contains // features with prerequisites that can't be archived. This is not a test failure. if strings.Contains(err.Error(), "FailedPrecondition") || - strings.Contains(err.Error(), "used as a prerequsite") { + strings.Contains(err.Error(), "used as a prerequisite") { t.Logf("Batch job completed with expected prerequisite warning: %v", err) return } diff --git a/ui/dashboard/src/@locales/en/backend.json b/ui/dashboard/src/@locales/en/backend.json index 7fc2bc6071..9f111aef88 100644 --- a/ui/dashboard/src/@locales/en/backend.json +++ b/ui/dashboard/src/@locales/en/backend.json @@ -162,7 +162,15 @@ "VariationInUseByFeatureFlagRuleError": "This variation cannot be deleted because the flag \"{{featureName}}\" uses it in a targeting rule. Remove the rule from that flag first.", "StartAtIsAfterEndAt": "Start at must be before end at", "PeriodOutOfRange": "The period must be within the last 30 days", - "ProjectDisabled": "Project is disabled" + "ProjectDisabled": "Project is disabled", + "CannotUpdateSystemAdminOrganizationError": "System admin organization cannot be disabled or archived", + "DemoSiteNotEnabled": "Demo site is not enabled", + "UserAlreadyInOrganization": "User is already a member of this organization", + "TriggerAlreadyDisabled": "Trigger is already disabled", + "ScheduledFlagChangeNotPending": "Scheduled flag change is not in pending status", + "NotificationAlreadyPublished": "Notification has already been published", + "TagInUse": "Tag cannot be deleted because it is in use", + "ProgressiveRolloutVariationsMustBeDifferent": "Control and target variations must be different for a progressive rollout" }, "domainEvents": { "UnknownOperation": "An unknown operation occurred", diff --git a/ui/dashboard/src/@locales/ja/backend.json b/ui/dashboard/src/@locales/ja/backend.json index d768e3cd02..f040bd8ca1 100644 --- a/ui/dashboard/src/@locales/ja/backend.json +++ b/ui/dashboard/src/@locales/ja/backend.json @@ -188,7 +188,14 @@ "VariationInUseByFeatureFlagRuleError": "このバリエーションはフラグ「{{featureName}}」のターゲティングルールで使用されているため削除できません。先に該当フラグのルールを削除してください。", "StartAtIsAfterEndAt": "開始時間は終了時間以前を指定してください", "PeriodOutOfRange": "期間は過去30日以内を選択してください", - "ProjectDisabled": "プロジェクトが無効化されています" + "ProjectDisabled": "プロジェクトが無効化されています", + "DemoSiteNotEnabled": "デモサイトが有効化されていません", + "UserAlreadyInOrganization": "ユーザーは既にこのオーガニゼーションのメンバーです", + "TriggerAlreadyDisabled": "トリガーは既に無効化されています", + "ScheduledFlagChangeNotPending": "スケジュールされたフラグ変更は保留中ではありません", + "NotificationAlreadyPublished": "通知は既に公開されています", + "TagInUse": "タグが使用されているため削除できません", + "ProgressiveRolloutVariationsMustBeDifferent": "プログレッシブロールアウトのコントロールバリエーションとターゲットバリエーションは異なる必要があります" }, "domainEvents": { "UnknownOperation": "不明な操作が実行されました",