Skip to content

Conversation

@aepfli
Copy link
Member

@aepfli aepfli commented Sep 9, 2025

This pull request introduces a significant enhancement to how flag selectors are parsed, particularly for simplified selector strings that do not explicitly define a key-value pair. By adding a configurable fallback key, the system gains increased flexibility, allowing users to customize the interpretation of such selectors beyond the default 'source' key. This change improves the adaptability of the flag evaluation logic without breaking backward compatibility.

Simply put: if we have a selector without a = we are currently assume that this is a "source"-selector and transform it to source=<selector>. With this changes, we can configure the key of this selector to any arbitrary key. So when selector-fallback-key is foo this will turn it into foo=<selector>.

Highlights

  • Configurable Selector Fallback: Introduced a new configuration property (selector-fallback-key) that allows defining which key is used when a selector string without an equals sign is provided (e.g., "myValue" instead of "key=myValue").
  • Enhanced Selector Parsing: The internal Selector and expressionToMap functions now support this configurable fallback, defaulting to the existing sourceIndex if no fallback key is specified.
  • Store Initialization Refactor: The store.NewStore function now accepts a StoreConfig struct, encapsulating source and the new selector fallback key, leading to cleaner initialization.
  • Command-Line Option: A new --selector-fallback-key flag has been added to the flagd command to allow users to specify this fallback behavior via command-line arguments.

@aepfli aepfli requested review from a team as code owners September 9, 2025 16:24
@netlify
Copy link

netlify bot commented Sep 9, 2025

Deploy Preview for polite-licorice-3db33c canceled.

Name Link
🔨 Latest commit 9ce0181
🔍 Latest deploy log https://app.netlify.com/projects/polite-licorice-3db33c/deploys/68d65896f323bc0008ba1c42

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Sep 9, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @aepfli, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to how flag selectors are parsed, particularly for simplified selector strings that do not explicitly define a key-value pair. By adding a configurable fallback key, the system gains increased flexibility, allowing users to customize the interpretation of such selectors beyond the default 'source' key. This change improves the adaptability of the flag evaluation logic without breaking backward compatibility.

Highlights

  • Configurable Selector Fallback: Introduced a new configuration property (selector-fallback-key) that allows defining which key is used when a selector string without an equals sign is provided (e.g., "myValue" instead of "key=myValue").
  • Enhanced Selector Parsing: The internal Selector and expressionToMap functions now support this configurable fallback, defaulting to the existing sourceIndex if no fallback key is specified.
  • Store Initialization Refactor: The store.NewStore function now accepts a StoreConfig struct, encapsulating source and the new selector fallback key, leading to cleaner initialization.
  • Command-Line Option: A new --selector-fallback-key flag has been added to the flagd command to allow users to specify this fallback behavior via command-line arguments.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as outdated.

gemini-code-assist[bot]

This comment was marked as outdated.

gemini-code-assist[bot]

This comment was marked as outdated.

aepfli and others added 7 commits September 24, 2025 16:00
…ing without equals is used as a selector

Signed-off-by: Simon Schrottner <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: Simon Schrottner <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: Simon Schrottner <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: Simon Schrottner <[email protected]>
Signed-off-by: Simon Schrottner <[email protected]>
Signed-off-by: Simon Schrottner <[email protected]>
@aepfli aepfli force-pushed the feat/make_flagsetid_default_for_only_string_selectors branch from bef0dd9 to 62c0b36 Compare September 24, 2025 14:03
gemini-code-assist[bot]

This comment was marked as outdated.

@aepfli aepfli force-pushed the feat/make_flagsetid_default_for_only_string_selectors branch from 62c0b36 to 484cf74 Compare September 24, 2025 14:12
Signed-off-by: Simon Schrottner <[email protected]>

diff --git c/core/pkg/service/iservice.go i/core/pkg/service/iservice.go
index 97d8ec2..3ce9a84 100644
--- c/core/pkg/service/iservice.go
+++ i/core/pkg/service/iservice.go
@@ -36,6 +36,7 @@ type Configuration struct {
 	ContextValues              map[string]any
 	HeaderToContextKeyMappings map[string]string
 	StreamDeadline             time.Duration
+	SelectorFallbackKey        string
 }

 /*
diff --git c/flagd/pkg/runtime/from_config.go i/flagd/pkg/runtime/from_config.go
index 6b56ec8..8b3b544 100644
--- c/flagd/pkg/runtime/from_config.go
+++ i/flagd/pkg/runtime/from_config.go
@@ -103,7 +103,8 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
 		logger.WithFields(zap.String("component", "service")),
 		jsonEvaluator,
 		store,
-		recorder)
+		recorder,
+		config.SelectorFallbackKey)

 	// ofrep service
 	ofrepService, err := ofrep.NewOfrepService(jsonEvaluator, config.CORS, ofrep.SvcConfiguration{
@@ -111,6 +112,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
 		Port:   config.OfrepServicePort,
 		ServiceName: svcName,
 		MetricsRecorder: recorder,
+		SelectorFallbackKey: config.SelectorFallbackKey,
 	},
 		config.ContextValues,
 		config.HeaderToContextKeyMappings,
@@ -131,6 +133,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
 		SocketPath:          config.SyncServiceSocketPath,
 		StreamDeadline:      config.StreamDeadline,
 		DisableSyncMetadata: config.DisableSyncMetadata,
+		SelectorFallbackKey: config.SelectorFallbackKey,
 	})
 	if err != nil {
 		return nil, fmt.Errorf("error creating sync service: %w", err)
@@ -167,6 +170,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
 			ContextValues:              config.ContextValues,
 			HeaderToContextKeyMappings: config.HeaderToContextKeyMappings,
 			StreamDeadline:             config.StreamDeadline,
+			SelectorFallbackKey:        config.SelectorFallbackKey,
 		},
 		Syncs: iSyncs,
 	}, nil
diff --git c/flagd/pkg/service/flag-evaluation/connect_service.go i/flagd/pkg/service/flag-evaluation/connect_service.go
index 3fa1b4a..b18be7f 100644
--- c/flagd/pkg/service/flag-evaluation/connect_service.go
+++ i/flagd/pkg/service/flag-evaluation/connect_service.go
@@ -68,12 +68,12 @@ type ConnectService struct {
 	metricsServerMtx sync.RWMutex

 	readinessEnabled bool
+
+	selectorFallbackKey string
 }

 // NewConnectService creates a ConnectService with provided parameters
-func NewConnectService(
-	logger *logger.Logger, evaluator evaluator.IEvaluator, store store.IStore, mRecorder telemetry.IMetricsRecorder,
-) *ConnectService {
+func NewConnectService(logger *logger.Logger, evaluator evaluator.IEvaluator, store store.IStore, mRecorder telemetry.IMetricsRecorder, selectorFallbackKey string) *ConnectService {
 	cs := &ConnectService{
 		logger:  logger,
 		eval:    evaluator,
@@ -84,6 +84,7 @@ func NewConnectService(
 			store:  store,
 			logger: logger,
 		},
+		selectorFallbackKey: selectorFallbackKey,
 	}
 	if mRecorder != nil {
 		cs.metrics = mRecorder
@@ -158,6 +159,7 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
 		s.eventingConfiguration,
 		s.metrics,
 		svcConf.ContextValues,
+		s.selectorFallbackKey,
 	)

 	marshalOpts := WithJSON(
@@ -177,6 +179,7 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
 		svcConf.ContextValues,
 		svcConf.HeaderToContextKeyMappings,
 		svcConf.StreamDeadline,
+		s.selectorFallbackKey,
 	)

 	_, newHandler := evaluationV1.NewServiceHandler(newFes, append(svcConf.Options, marshalOpts)...)
diff --git c/flagd/pkg/service/flag-evaluation/connect_service_test.go i/flagd/pkg/service/flag-evaluation/connect_service_test.go
index 8e89d62..5473442 100644
--- c/flagd/pkg/service/flag-evaluation/connect_service_test.go
+++ i/flagd/pkg/service/flag-evaluation/connect_service_test.go
@@ -84,7 +84,7 @@ func TestConnectService_UnixConnection(t *testing.T) {
 			exp := metric.NewManualReader()
 			rs := resource.NewWithAttributes("testSchema")
 			metricRecorder := telemetry.NewOTelRecorder(exp, rs, tt.name)
-			svc := NewConnectService(logger.NewLogger(nil, false), eval, nil, metricRecorder)
+			svc := NewConnectService(logger.NewLogger(nil, false), eval, nil, metricRecorder, "")
 			serveConf := iservice.Configuration{
 				ReadinessProbe: func() bool {
 					return true
@@ -139,7 +139,7 @@ func TestAddMiddleware(t *testing.T) {
 	rs := resource.NewWithAttributes("testSchema")
 	metricRecorder := telemetry.NewOTelRecorder(exp, rs, "my-exporter")

-	svc := NewConnectService(logger.NewLogger(nil, false), nil, nil, metricRecorder)
+	svc := NewConnectService(logger.NewLogger(nil, false), nil, nil, metricRecorder, "")

 	serveConf := iservice.Configuration{
 		ReadinessProbe: func() bool {
@@ -187,7 +187,7 @@ func TestConnectServiceNotify(t *testing.T) {
 	rs := resource.NewWithAttributes("testSchema")
 	metricRecorder := telemetry.NewOTelRecorder(exp, rs, "my-exporter")

-	service := NewConnectService(logger.NewLogger(nil, false), eval, s, metricRecorder)
+	service := NewConnectService(logger.NewLogger(nil, false), eval, s, metricRecorder, "")

 	sChan := make(chan iservice.Notification, 1)
 	eventing := service.eventingConfiguration
@@ -278,7 +278,7 @@ func TestConnectServiceShutdown(t *testing.T) {
 	rs := resource.NewWithAttributes("testSchema")
 	metricRecorder := telemetry.NewOTelRecorder(exp, rs, "my-exporter")

-	service := NewConnectService(logger.NewLogger(nil, false), eval, s, metricRecorder)
+	service := NewConnectService(logger.NewLogger(nil, false), eval, s, metricRecorder, "")

 	sChan := make(chan iservice.Notification, 1)
 	eventing := service.eventingConfiguration
diff --git c/flagd/pkg/service/flag-evaluation/flag_evaluator.go i/flagd/pkg/service/flag-evaluation/flag_evaluator.go
index 58c1c3b..88a169e 100644
--- c/flagd/pkg/service/flag-evaluation/flag_evaluator.go
+++ i/flagd/pkg/service/flag-evaluation/flag_evaluator.go
@@ -36,6 +36,7 @@ type OldFlagEvaluationService struct {
 	eventingConfiguration IEvents
 	flagEvalTracer        trace.Tracer
 	contextValues         map[string]any
+	selectorFallbackKey   string
 }

 // NewOldFlagEvaluationService creates a OldFlagEvaluationService with provided parameters
@@ -45,6 +46,7 @@ func NewOldFlagEvaluationService(
 	eventingCfg IEvents,
 	metricsRecorder telemetry.IMetricsRecorder,
 	contextValues map[string]any,
+	selectorFallback string,
 ) *OldFlagEvaluationService {
 	svc := &OldFlagEvaluationService{
 		logger:                log,
@@ -53,6 +55,7 @@ func NewOldFlagEvaluationService(
 		eventingConfiguration: eventingCfg,
 		flagEvalTracer:        otel.Tracer("flagEvaluationService"),
 		contextValues:         contextValues,
+		selectorFallbackKey:   selectorFallback,
 	}

 	if metricsRecorder != nil {
@@ -76,7 +79,7 @@ func (s *OldFlagEvaluationService) ResolveAll(
 	}

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	values, _, err := s.eval.ResolveAllValues(ctx, reqID, mergeContexts(req.Msg.GetContext().AsMap(), s.contextValues, req.Header(), make(map[string]string)))
@@ -143,7 +146,7 @@ func (s *OldFlagEvaluationService) EventStream(

 	requestNotificationChan := make(chan service.Notification, 1)
 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	s.eventingConfiguration.Subscribe(ctx, req, &selector, requestNotificationChan)
 	defer s.eventingConfiguration.Unsubscribe(req)

@@ -186,7 +189,7 @@ func (s *OldFlagEvaluationService) ResolveBoolean(
 	defer span.End()
 	res := connect.NewResponse(&schemaV1.ResolveBooleanResponse{})
 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	err := resolve[bool](
@@ -218,7 +221,7 @@ func (s *OldFlagEvaluationService) ResolveString(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&schemaV1.ResolveStringResponse{})
@@ -251,7 +254,7 @@ func (s *OldFlagEvaluationService) ResolveInt(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&schemaV1.ResolveIntResponse{})
@@ -284,7 +287,7 @@ func (s *OldFlagEvaluationService) ResolveFloat(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&schemaV1.ResolveFloatResponse{})
@@ -317,7 +320,7 @@ func (s *OldFlagEvaluationService) ResolveObject(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&schemaV1.ResolveObjectResponse{})
diff --git c/flagd/pkg/service/flag-evaluation/flag_evaluator_v2.go i/flagd/pkg/service/flag-evaluation/flag_evaluator_v2.go
index 0831737..eb67bf4 100644
--- c/flagd/pkg/service/flag-evaluation/flag_evaluator_v2.go
+++ i/flagd/pkg/service/flag-evaluation/flag_evaluator_v2.go
@@ -31,6 +31,7 @@ type FlagEvaluationService struct {
 	contextValues              map[string]any
 	headerToContextKeyMappings map[string]string
 	deadline                   time.Duration
+	selectorFallbackKey        string
 }

 // NewFlagEvaluationService creates a FlagEvaluationService with provided parameters
@@ -41,6 +42,7 @@ func NewFlagEvaluationService(log *logger.Logger,
 	contextValues map[string]any,
 	headerToContextKeyMappings map[string]string,
 	streamDeadline time.Duration,
+	selectorFallbackKey string,
 ) *FlagEvaluationService {
 	svc := &FlagEvaluationService{
 		logger:                     log,
@@ -51,6 +53,7 @@ func NewFlagEvaluationService(log *logger.Logger,
 		contextValues:              contextValues,
 		headerToContextKeyMappings: headerToContextKeyMappings,
 		deadline:                   streamDeadline,
+		selectorFallbackKey:        selectorFallbackKey,
 	}

 	if metricsRecorder != nil {
@@ -76,7 +79,7 @@ func (s *FlagEvaluationService) ResolveAll(
 	}

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	evaluationContext := mergeContexts(req.Msg.GetContext().AsMap(), s.contextValues, req.Header(), s.headerToContextKeyMappings)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

@@ -165,7 +168,7 @@ func (s *FlagEvaluationService) EventStream(
 	s.logger.Debug("starting event stream for request")
 	requestNotificationChan := make(chan service.Notification, 1)
 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	s.eventingConfiguration.Subscribe(ctx, req, &selector, requestNotificationChan)
 	defer s.eventingConfiguration.Unsubscribe(req)

@@ -211,7 +214,7 @@ func (s *FlagEvaluationService) ResolveBoolean(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&evalV1.ResolveBooleanResponse{})
@@ -243,7 +246,7 @@ func (s *FlagEvaluationService) ResolveString(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&evalV1.ResolveStringResponse{})
@@ -275,7 +278,7 @@ func (s *FlagEvaluationService) ResolveInt(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&evalV1.ResolveIntResponse{})
@@ -307,7 +310,7 @@ func (s *FlagEvaluationService) ResolveFloat(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&evalV1.ResolveFloatResponse{})
@@ -339,7 +342,7 @@ func (s *FlagEvaluationService) ResolveObject(
 	defer span.End()

 	selectorExpression := req.Header().Get(flagdService.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx = context.WithValue(ctx, store.SelectorContextKey{}, selector)

 	res := connect.NewResponse(&evalV1.ResolveObjectResponse{})
diff --git c/flagd/pkg/service/flag-evaluation/ofrep/handler.go i/flagd/pkg/service/flag-evaluation/ofrep/handler.go
index cbc8d43..18f0b57 100644
--- c/flagd/pkg/service/flag-evaluation/ofrep/handler.go
+++ i/flagd/pkg/service/flag-evaluation/ofrep/handler.go
@@ -33,6 +33,7 @@ type handler struct {
 	contextValues              map[string]any
 	headerToContextKeyMappings map[string]string
 	tracer                     trace.Tracer
+	selectorFallbackKey        string
 }

 func NewOfrepHandler(
@@ -42,6 +43,7 @@ func NewOfrepHandler(
 	headerToContextKeyMappings map[string]string,
 	metricsRecorder telemetry.IMetricsRecorder,
 	serviceName string,
+	selectorFallbackKey:        selectorFallbackKey,
 ) http.Handler {
 	h := handler{
 		Logger:                     logger,
@@ -49,6 +51,7 @@ func NewOfrepHandler(
 		contextValues:              contextValues,
 		headerToContextKeyMappings: headerToContextKeyMappings,
 		tracer:                     otel.Tracer("flagd.ofrep.v1"),
+		selectorFallbackKey:        selectorFallbackKey,
 	}

 	router := mux.NewRouter()
@@ -93,7 +96,7 @@ func (h *handler) HandleFlagEvaluation(w http.ResponseWriter, r *http.Request) {
 	}
 	evaluationContext := flagdContext(h.Logger, requestID, request, h.contextValues, r.Header, h.headerToContextKeyMappings)
 	selectorExpression := r.Header.Get(service.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, h.selectorFallbackKey)
 	ctx := context.WithValue(r.Context(), store.SelectorContextKey{}, selector)

 	evaluation := h.evaluator.ResolveAsAnyValue(ctx, requestID, flagKey, evaluationContext)
@@ -117,7 +120,7 @@ func (h *handler) HandleBulkEvaluation(w http.ResponseWriter, r *http.Request) {

 	evaluationContext := flagdContext(h.Logger, requestID, request, h.contextValues, r.Header, h.headerToContextKeyMappings)
 	selectorExpression := r.Header.Get(service.FLAGD_SELECTOR_HEADER)
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, h.selectorFallbackKey)
 	ctx := context.WithValue(r.Context(), store.SelectorContextKey{}, selector)

 	evaluations, metadata, err := h.evaluator.ResolveAllValues(ctx, requestID, evaluationContext)
diff --git c/flagd/pkg/service/flag-evaluation/ofrep/ofrep_service.go i/flagd/pkg/service/flag-evaluation/ofrep/ofrep_service.go
index bc5689e..9048112 100644
--- c/flagd/pkg/service/flag-evaluation/ofrep/ofrep_service.go
+++ i/flagd/pkg/service/flag-evaluation/ofrep/ofrep_service.go
@@ -24,6 +24,7 @@ type SvcConfiguration struct {
 	Port            uint16
 	ServiceName     string
 	MetricsRecorder telemetry.IMetricsRecorder
+	SelectorFallbackKey string
 }

 type Service struct {
@@ -47,6 +48,7 @@ func NewOfrepService(
 		headerToContextKeyMappings,
 		cfg.MetricsRecorder,
 		cfg.ServiceName,
+		cfg.SelectorFallbackKey,
 	))

 	server := http.Server{
diff --git c/flagd/pkg/service/flag-sync/handler.go i/flagd/pkg/service/flag-sync/handler.go
index 7bf83e2..3abbda7 100644
--- c/flagd/pkg/service/flag-sync/handler.go
+++ i/flagd/pkg/service/flag-sync/handler.go
@@ -26,12 +26,14 @@ type syncHandler struct {
 	contextValues       map[string]any
 	deadline            time.Duration
 	disableSyncMetadata bool
+
+	selectorFallbackKey string
 }

 func (s syncHandler) SyncFlags(req *syncv1.SyncFlagsRequest, server syncv1grpc.FlagSyncService_SyncFlagsServer) error {
 	watcher := make(chan store.FlagQueryResult, 1)
 	selectorExpression := req.GetSelector()
-	selector := store.NewSelector(selectorExpression)
+	selector := store.NewSelectorWithFallback(selectorExpression, s.selectorFallbackKey)
 	ctx := server.Context()

 	syncContextMap := make(map[string]any)
diff --git c/flagd/pkg/service/flag-sync/sync_service.go i/flagd/pkg/service/flag-sync/sync_service.go
index 22f6c0c..8b5680b 100644
--- c/flagd/pkg/service/flag-sync/sync_service.go
+++ i/flagd/pkg/service/flag-sync/sync_service.go
@@ -35,6 +35,7 @@ type SvcConfigurations struct {
 	SocketPath          string
 	StreamDeadline      time.Duration
 	DisableSyncMetadata bool
+	SelectorFallbackKey string
 }

 type Service struct {
@@ -86,6 +87,7 @@ func NewSyncService(cfg SvcConfigurations) (*Service, error) {
 		contextValues:       cfg.ContextValues,
 		deadline:            cfg.StreamDeadline,
 		disableSyncMetadata: cfg.DisableSyncMetadata,
+		selectorFallbackKey: cfg.SelectorFallbackKey,
 	})

 	var lis net.Listener
@aepfli aepfli force-pushed the feat/make_flagsetid_default_for_only_string_selectors branch from 484cf74 to 6656c15 Compare September 24, 2025 14:16
gemini-code-assist[bot]

This comment was marked as outdated.

Signed-off-by: Simon Schrottner <[email protected]>
gemini-code-assist[bot]

This comment was marked as outdated.

@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Sep 26, 2025
gemini-code-assist[bot]

This comment was marked as outdated.

Signed-off-by: Simon Schrottner <[email protected]>
@aepfli aepfli force-pushed the feat/make_flagsetid_default_for_only_string_selectors branch from 2ce0312 to 7e3247d Compare September 26, 2025 08:02
gemini-code-assist[bot]

This comment was marked as outdated.

Signed-off-by: Simon Schrottner <[email protected]>
gemini-code-assist[bot]

This comment was marked as outdated.

@aepfli
Copy link
Member Author

aepfli commented Sep 26, 2025

/gemini review

last review from gemini, rest is for the community

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a configurable fallback key for flag selectors, which is a great enhancement for flexibility. The implementation is solid, with good refactoring of the store initialization and extensive test coverage for the new selector parsing logic. I have a few suggestions to improve robustness and clarity.

Signed-off-by: Simon Schrottner <[email protected]>
}

// Handle pure fallback case (no equals anywhere)
if strings.Index(sExp, "=") == -1 {
Copy link
Member

Choose a reason for hiding this comment

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

@aepfli staticcheck says: S1003: should use !strings.Contains(sExp, "=") instead (staticcheck), which makes sense to me.

type Selector struct {
indexMap map[string]string
indexMap map[string]string
usingFallback bool
Copy link
Member

@toddbaert toddbaert Oct 6, 2025

Choose a reason for hiding this comment

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

I don't think we need this member (per Selector), and we don't need a separate "With" for it either, since this is an immutable global setting (all Selectors will share it).

Instead, I would recommend taking advantage of module initialization and sync.Once to do this, by adding something like this at the top of the file (just below the imports):

var fallbackKey string // our fallback key global
var fallbackKeyOnce sync.Once // sync.Once instance

// public function
func SetFallbackKey(key string) {
	fallbackKeyOnce.Do(func() {
		fallbackKey = key
	})
}

Then we just call SetFallbackKey at some point during init, which sets the fallback key ONLY ONCE for the duration of the program; then we can get rid of this per instance and just always refer to this module global.

Copy link
Member

Choose a reason for hiding this comment

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

BTW for testing, you can manually reset the module private sync.Once.

b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = selector.WithIndex("key", "benchmark-key")
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need specific benchmark tests for this.

}
}

func TestSelector_WithFallback(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

We can remove all these tests (perhaps moving 1 or 2 to the other functions) if we do this.

@toddbaert
Copy link
Member

This was mostly to facilitate an internal migration issue, but we've come up with an alternative that will avoid adding this to flagd.

@toddbaert toddbaert closed this Oct 8, 2025
@aepfli
Copy link
Member Author

aepfli commented Oct 8, 2025

I am not sure I agree here, yes the motivation stems from an internal problem, and we might be able to fix this differently internally, but the problem still exists for other users of flagd, which also do need some kind of migration path.

How should they handle this?

@toddbaert
Copy link
Member

toddbaert commented Oct 9, 2025

I am not sure I agree here, yes the motivation stems from an internal problem, and we might be able to fix this differently internally, but the problem still exists for other users of flagd, which also do need some kind of migration path.

How should they handle this?

This is not a problem for external users. A selector without an expression will continue to represent a source, there's no plan to change this, and no need to change it, since up to now is been the only possibility.

@aepfli
Copy link
Member Author

aepfli commented Oct 9, 2025

i am not sure i am agreeing with this comment, because flagset will be in the future the default approach, and as a library/service we should provide a migration path also for other projects. But it is not he right approach to this via this pr implicitly, we should do this explicitly with also defining the migration and deprecation path. In the end this is also something like the experimental flags for the jvm, till a feature reaches GA and afterwards they get removed again. We should make this migration easy for users of flagd to also rely on the flagset as a default, without forcing their sdk users, to update their dependencies.

@toddbaert
Copy link
Member

i am not sure i am agreeing with this comment, because flagset will be in the future the default approach, and as a library/service we should provide a migration path also for other projects. But it is not he right approach to this via this pr implicitly, we should do this explicitly with also defining the migration and deprecation path. In the end this is also something like the experimental flags for the jvm, till a feature reaches GA and afterwards they get removed again. We should make this migration easy for users of flagd to also rely on the flagset as a default, without forcing their sdk users, to update their dependencies.

Nobody who is using flagd right now is using the selector for a flagSetId because it's not possible, so there's no need for a "migration path". Anyone who wants to select on a flagSetId can use an expression to do that in the future, so I'm not seeing the point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants