2222//
2323// ctx = MyFeature.WithEnabled(ctx)
2424//
25- // # WithName Keys for Debugging
25+ // # Named Keys for Debugging
2626//
2727// You can create named keys to help with debugging:
2828//
2929// var MyFeature = feature.NewNamedBool("my-feature")
3030// fmt.Println(MyFeature) // Output: my-feature
3131//
32+ // Anonymous keys (without a name) automatically include call site information:
33+ //
34+ // var AnonFeature = feature.NewBool()
35+ // fmt.Println(AnonFeature) // Output: anonymous(/path/to/file.go:42)@0x14000010098
36+ //
3237// # Value-Based Feature Flags
3338//
3439// You can also use feature flags with arbitrary types:
@@ -48,6 +53,7 @@ package feature
4853import (
4954 "context"
5055 "fmt"
56+ "runtime"
5157)
5258
5359// Key is a type-safe accessor for feature flags stored in context.Context.
@@ -127,19 +133,15 @@ type BoolKey interface {
127133 WithDisabled (ctx context.Context ) context.Context
128134}
129135
130- // StringerFunc is a function that formats a key name as a string.
131- // It receives a resolved key name (never empty - anonymous keys are already
132- // formatted as "anonymous@<address>") and returns the final string representation.
133- // The default implementation returns the name as-is.
134- type StringerFunc func (name string ) string
135-
136136// Option is a function that configures the behavior of a feature flag key.
137137type Option func (* options )
138138
139139// options configures the behavior of a feature flag key.
140140type options struct {
141- name string
142- stringer StringerFunc
141+ name string
142+
143+ // internal use only - tracks the caller depth for name fallback
144+ depth int
143145}
144146
145147// WithName returns an option that sets a debug name for the key.
@@ -155,35 +157,19 @@ func WithName(name string) Option {
155157 }
156158}
157159
158- // WithStringer returns an option that sets a custom string formatter for the key name.
159- // The formatter function receives a resolved key name (never empty) and returns
160- // the final string representation.
161- //
162- // If not provided, the default formatter returns the name as-is.
163- //
164- // Example:
165- //
166- // customFormatter := func(name string) string {
167- // return fmt.Sprintf("[%s]", name)
168- // }
169- // var MyKey = feature.New[int](feature.WithStringer(customFormatter))
170- func WithStringer (f StringerFunc ) Option {
171- return func (o * options ) {
172- o .stringer = f
173- }
174- }
175-
176- // defaultStringer is the default string formatter for keys.
177- // It returns the name as-is (identity function).
178- func defaultStringer (name string ) string {
179- return name
160+ // appendCallerDepthIncr appends an option that increments the caller depth for name fallback.
161+ // This is used internally to ensure correct caller depth when deriving names from call sites.
162+ func appendCallerDepthIncr (opts []Option ) []Option {
163+ return append (opts , func (o * options ) {
164+ o .depth ++
165+ })
180166}
181167
182168// defaultOptions returns a new options with default values.
183169func defaultOptions () * options {
184170 return & options {
185- name : "" ,
186- stringer : defaultStringer ,
171+ name : "" ,
172+ depth : 0 ,
187173 }
188174}
189175
@@ -197,6 +183,24 @@ func optionsFrom(opts []Option) *options {
197183 return o
198184}
199185
186+ func computeKeyName (ident * opaque , name string , depth int ) string {
187+ // Resolve the base name (handle anonymous keys)
188+ if name == "" {
189+ // depth is the number of stack frames added by wrapper functions.
190+ // Each exported function (New, NewBool, NewNamed, NewNamedBool) calls appendCallerDepthIncr.
191+ // The call stack is: runtime.Caller -> computeKeyName -> New -> [wrappers...] -> user code
192+ // Base offset is 1 (computeKeyName itself), plus depth for wrapper functions.
193+ _ , file , line , ok := runtime .Caller (1 + depth )
194+ if ok {
195+ name = fmt .Sprintf ("anonymous(%s:%d)@%p" , file , line , ident )
196+ } else {
197+ name = fmt .Sprintf ("anonymous@%p" , ident )
198+ }
199+ }
200+
201+ return name
202+ }
203+
200204// NewBool creates a new boolean feature flag key.
201205//
202206// Each call to NewBool creates a unique key based on pointer identity, preventing collisions.
@@ -212,6 +216,8 @@ func optionsFrom(opts []Option) *options {
212216// }
213217// }
214218func NewBool (options ... Option ) BoolKey {
219+ options = appendCallerDepthIncr (options )
220+
215221 return boolKey {key : New [bool ](options ... ).downcast ()}
216222}
217223
@@ -225,6 +231,8 @@ func NewBool(options ...Option) BoolKey {
225231// var EnableNewUI = feature.NewNamedBool("new-ui")
226232// fmt.Println(EnableNewUI) // Output: new-ui
227233func NewNamedBool (name string , options ... Option ) BoolKey {
234+ options = appendCallerDepthIncr (options )
235+
228236 return NewBool (append ([]Option {WithName (name )}, options ... )... )
229237}
230238
@@ -239,12 +247,13 @@ func NewNamedBool(name string, options ...Option) BoolKey {
239247// ctx = MaxRetries.WithValue(ctx, 5)
240248// retries := MaxRetries.Get(ctx) // Returns 5
241249func New [V any ](options ... Option ) Key [V ] {
250+ options = appendCallerDepthIncr (options )
242251 opts := optionsFrom (options )
252+ ident := new (opaque )
243253
244254 return key [V ]{
245- name : opts .name ,
246- stringer : opts .stringer ,
247- ident : new (opaque ),
255+ name : computeKeyName (ident , opts .name , opts .depth ),
256+ ident : ident ,
248257 }
249258}
250259
@@ -258,38 +267,25 @@ func New[V any](options ...Option) Key[V] {
258267// var MaxRetries = feature.NewNamed[int]("max-retries")
259268// fmt.Println(MaxRetries) // Output: max-retries
260269func NewNamed [V any ](name string , options ... Option ) Key [V ] {
270+ options = appendCallerDepthIncr (options )
271+
261272 return New [V ](append ([]Option {WithName (name )}, options ... )... )
262273}
263274
264275// key is the internal implementation of Key[V].
265276type key [V any ] struct {
266- name string
267- stringer StringerFunc
268- ident * opaque
277+ name string
278+ ident * opaque
269279}
270280
271281// boolKey is the internal implementation of BoolKey.
272282type boolKey struct {
273283 key [bool ]
274284}
275285
276- // String returns a string representation of the key name.
277- // The format can be customized via the WithStringer option.
278- // By default, it returns the debug name if provided, or "anonymous@<address>" otherwise.
286+ // String returns the debug name of the key.
279287func (k key [V ]) String () string {
280- // Resolve the base name (handle anonymous keys)
281- name := k .name
282- if name == "" {
283- name = fmt .Sprintf ("anonymous@%p" , k .ident )
284- }
285-
286- // Apply custom stringer if provided
287- stringer := k .stringer
288- if stringer == nil {
289- stringer = defaultStringer
290- }
291-
292- return stringer (name )
288+ return k .name
293289}
294290
295291// DebugValue returns a string representation combining the key name and its value from the context.
0 commit comments