Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apis/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import (
"net/http"

custom_errors "github.com/cheetahbyte/flagly/internal/error"
"github.com/cheetahbyte/flagly/internal/evaluation"
"github.com/cheetahbyte/flagly/internal/utils"
"github.com/cheetahbyte/flagly/pkg/flagly"
"github.com/gin-gonic/gin"
)

type FlagAPI struct {
store *flagly.Storage
auditService flagly.AuditService
store *flagly.Storage
auditService flagly.AuditService
evaluationService flagly.EvaluationService
}

func NewFlagAPI(store *flagly.Storage, auditService flagly.AuditService) *FlagAPI {
return &FlagAPI{store: store, auditService: auditService}
func NewFlagAPI(store *flagly.Storage, auditService flagly.AuditService, evaluationService flagly.EvaluationService) *FlagAPI {
return &FlagAPI{store: store, auditService: auditService, evaluationService: evaluationService}
}

func (api *FlagAPI) RegisterRoutes(router *gin.RouterGroup) {
Expand Down Expand Up @@ -101,7 +101,7 @@ func (api *FlagAPI) PostEvaluateFlag(c *gin.Context) {
return
}

result := evaluation.EvaluateFlag(*flag, data.User, data.Environment)
result, _ := api.evaluationService.EvaluateFlag(*flag, data.User, data.Environment)

api.auditService.TrackEvaluation(c, *flag, data.User, data.Environment, result)

Expand Down
6 changes: 1 addition & 5 deletions internal/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import (
"github.com/gin-gonic/gin"
)

type AuditService interface {
TrackEvaluation(c *gin.Context, flag flagly.Flag, user flagly.User, environment string, result bool)
}

type DefaultAuditService struct{}

func NewDefaultAuditService() AuditService {
func NewDefaultAuditService() flagly.AuditService {
return &DefaultAuditService{}
}

Expand Down
16 changes: 10 additions & 6 deletions internal/evaluation/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@ import (
"github.com/spaolacci/murmur3"
)

// calculates the rollout percentage bucket in which the user is located.
type DefaultEvaluationService struct{}

func NewDefaultAuditService() flagly.EvaluationService {
return &DefaultEvaluationService{}
}

func calculateRolloutBucket(identifier string) int {
hash := murmur3.Sum32([]byte(identifier))
return int(hash % 100)
}

func EvaluateFlag(flag flagly.Flag, user flagly.User, environment string) bool {
// check if environment is enabled
func (s *DefaultEvaluationService) EvaluateFlag(flag flagly.Flag, user flagly.User, environment string) (bool, error) {
env, ok := flag.Environments[environment]
if !ok {
return false
return false, nil
}
fmt.Println(env)
if env.Rollout.Percentage == 0 || env.Rollout.Percentage == 100 {
return true
return true, nil
}

// TODO: make this more dynamic
Expand All @@ -35,5 +39,5 @@ func EvaluateFlag(flag flagly.Flag, user flagly.User, environment string) bool {

// Rollout specific logic
hashedPercentage := calculateRolloutBucket(stickiness)
return env.Rollout.Percentage > hashedPercentage
return env.Rollout.Percentage > hashedPercentage, nil
}
44 changes: 33 additions & 11 deletions internal/evaluation/evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@ import (
)

func TestEvaluateFlag_UnknownEnvironment(t *testing.T) {
service := &DefaultEvaluationService{}
flag := flagly.Flag{
Environments: map[string]flagly.Environment{},
}
user := flagly.User{ID: "123"}

result := EvaluateFlag(flag, user, "production")
result, err := service.EvaluateFlag(flag, user, "production")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result {
t.Errorf("expected false for unknown environment, got true")
}
}

func TestEvaluateFlag_PercentageZero(t *testing.T) {
service := &DefaultEvaluationService{}
flag := flagly.Flag{
Environments: map[string]flagly.Environment{
"production": {
Expand All @@ -30,12 +35,17 @@ func TestEvaluateFlag_PercentageZero(t *testing.T) {
}
user := flagly.User{ID: "user1"}

if !EvaluateFlag(flag, user, "production") {
t.Errorf("expected true when rollout is 0%% or 100%%, got false")
result, err := service.EvaluateFlag(flag, user, "production")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result {
t.Errorf("expected true when rollout is 0%%, got false")
}
}

func TestEvaluateFlag_Percentage100(t *testing.T) {
service := &DefaultEvaluationService{}
flag := flagly.Flag{
Environments: map[string]flagly.Environment{
"production": {
Expand All @@ -46,12 +56,17 @@ func TestEvaluateFlag_Percentage100(t *testing.T) {
}
user := flagly.User{ID: "user1"}

if !EvaluateFlag(flag, user, "production") {
t.Errorf("expected true for 100%% rollout")
result, err := service.EvaluateFlag(flag, user, "production")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result {
t.Errorf("expected true for 100%% rollout, got false")
}
}

func TestEvaluateFlag_ConsistentRollout(t *testing.T) {
service := &DefaultEvaluationService{}
user := flagly.User{ID: "user42"}
flag := flagly.Flag{
Environments: map[string]flagly.Environment{
Expand All @@ -62,17 +77,21 @@ func TestEvaluateFlag_ConsistentRollout(t *testing.T) {
},
}

// Should always return the same result for the same user
result1 := EvaluateFlag(flag, user, "production")
result2 := EvaluateFlag(flag, user, "production")
result1, err1 := service.EvaluateFlag(flag, user, "production")
result2, err2 := service.EvaluateFlag(flag, user, "production")

if err1 != nil || err2 != nil {
t.Fatalf("unexpected error(s): %v, %v", err1, err2)
}
if result1 != result2 {
t.Errorf("expected consistent result for same user, got %v and %v", result1, result2)
}
}

func TestEvaluateFlag_WithinRollout(t *testing.T) {
// Find a user ID that gives a bucket under 10
service := &DefaultEvaluationService{}

// Find a user ID that hashes to a bucket < 10
var userID string
for i := 0; i < 10000; i++ {
id := "user" + strconv.Itoa(i)
Expand All @@ -81,7 +100,6 @@ func TestEvaluateFlag_WithinRollout(t *testing.T) {
break
}
}

if userID == "" {
t.Fatalf("could not find user ID with bucket < 10")
}
Expand All @@ -96,7 +114,11 @@ func TestEvaluateFlag_WithinRollout(t *testing.T) {
},
}

if !EvaluateFlag(flag, user, "production") {
result, err := service.EvaluateFlag(flag, user, "production")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result {
t.Errorf("expected true for user within rollout range")
}
}
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/cheetahbyte/flagly/apis"
"github.com/cheetahbyte/flagly/internal/audit"
"github.com/cheetahbyte/flagly/internal/evaluation"
"github.com/cheetahbyte/flagly/internal/storage"
"github.com/cheetahbyte/flagly/pkg/flagly/middleware"
"github.com/gin-contrib/cors"
Expand Down Expand Up @@ -45,10 +46,11 @@ func main() {
router.Use(middleware.ErrorHandlerMiddleware())

auditService := audit.NewDefaultAuditService()
evaluationService := evaluation.NewDefaultAuditService()

apiGroup := router.Group("/api")
apis.NewGeneralAPI(store).RegisterRoutes(apiGroup)
apis.NewFlagAPI(store, auditService).RegisterRoutes(apiGroup)
apis.NewFlagAPI(store, auditService, evaluationService).RegisterRoutes(apiGroup)
apis.NewEnvironmentAPI(store).RegisterRoutes(apiGroup)

if os.Getenv("GIN_MODE") == "release" {
Expand Down
5 changes: 5 additions & 0 deletions pkg/flagly/evaluation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package flagly

type EvaluationService interface {
EvaluateFlag(flag Flag, user User, environment string) (bool, error)
}