1 package flagstate 2 3 import ( 4 "fmt" 5 6 "github.com/launchdarkly/go-jsonstream/v3/jwriter" 7 "github.com/launchdarkly/go-sdk-common/v3/ldreason" 8 "github.com/launchdarkly/go-sdk-common/v3/ldtime" 9 "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 10 11 "golang.org/x/exp/maps" 12 ) 13 14 // AllFlags is a snapshot of the state of multiple feature flags with regard to a specific evaluation 15 // context. This is the return type of LDClient.AllFlagsState(). 16 // 17 // Serializing this object to JSON using json.Marshal() will produce the appropriate data structure for 18 // bootstrapping the LaunchDarkly JavaScript client. 19 type AllFlags struct { 20 flags map[string]FlagState 21 valid bool 22 } 23 24 // AllFlagsBuilder is a builder that creates AllFlags instances. This is normally done only by the SDK, but 25 // it may also be used in test code. 26 // 27 // AllFlagsBuilder methods should not be used concurrently from multiple goroutines. 28 type AllFlagsBuilder struct { 29 state AllFlags 30 options allFlagsOptions 31 } 32 33 type allFlagsOptions struct { 34 withReasons bool 35 detailsOnlyIfTracked bool 36 } 37 38 // FlagState represents the state of an individual feature flag, with regard to a specific evaluation 39 // context, at the time when LDClient.AllFlagsState() was called. 40 type FlagState struct { 41 // Value is the result of evaluating the flag for the specified evaluation context. 42 Value ldvalue.Value 43 44 // Variation is the variation index that was selected for the specified evaluation context. 45 Variation ldvalue.OptionalInt 46 47 // Version is the flag's version number when it was evaluated. This is an int rather than an OptionalInt 48 // because a flag always has a version and nonexistent flag keys are not included in AllFlags. 49 Version int 50 51 // Reason is the evaluation reason from evaluating the flag. 52 Reason ldreason.EvaluationReason 53 54 // TrackEvents is true if a full feature event must be sent whenever evaluating this flag. This will be 55 // true if tracking was explicitly enabled for this flag for data export, or if the evaluation involved 56 // an experiment, or both. 57 TrackEvents bool 58 59 // TrackReason is true if the evaluation reason should always be included in any full feature event 60 // created for this flag, regardless of whether variationDetail was called. This will be true if the 61 // evaluation involved an experiment. 62 TrackReason bool 63 64 // DebugEventsUntilDate is non-zero if event debugging is enabled for this flag until the specified time. 65 DebugEventsUntilDate ldtime.UnixMillisecondTime 66 67 // OmitDetails is true if, based on the options passed to AllFlagsState and the flag state, some of the 68 // metadata can be left out of the JSON representation. 69 OmitDetails bool 70 } 71 72 // Option is the interface for optional parameters that can be passed to LDClient.AllFlagsState. 73 type Option interface { 74 fmt.Stringer 75 apply(*allFlagsOptions) 76 } 77 78 type clientSideOnlyOption struct{} 79 type withReasonsOption struct{} 80 type detailsOnlyForTrackedFlagsOption struct{} 81 82 // OptionClientSideOnly is an option that can be passed to LDClient.AllFlagsState(). 83 // 84 // It specifies that only flags marked for use with the client-side SDK should be included in the state 85 // object. By default, all flags are included. 86 func OptionClientSideOnly() Option { 87 return clientSideOnlyOption{} 88 } 89 90 // OptionWithReasons is an option that can be passed to LDClient.AllFlagsState(). It specifies that 91 // evaluation reasons should be included in the state object. By default, they are not. 92 func OptionWithReasons() Option { 93 return withReasonsOption{} 94 } 95 96 // OptionDetailsOnlyForTrackedFlags is an option that can be passed to LDClient.AllFlagsState(). It 97 // specifies that any flag metadata that is normally only used for event generation - such as flag versions 98 // and evaluation reasons - should be omitted for any flag that does not have event tracking or debugging 99 // turned on. This reduces the size of the JSON data if you are passing the flag state to the front end. 100 func OptionDetailsOnlyForTrackedFlags() Option { 101 return detailsOnlyForTrackedFlagsOption{} 102 } 103 104 // IsValid returns true if the call to LDClient.AllFlagsState() succeeded. It returns false if there was an 105 // error (such as the data store not being available), in which case no flag data is in this object. 106 func (a AllFlags) IsValid() bool { 107 return a.valid 108 } 109 110 // GetFlag looks up information for a specific flag by key. The returned FlagState struct contains the flag 111 // flag evaluation result and flag metadata that was recorded when LDClient.AllFlagsState() was called. The 112 // second return value is true if successful, or false if there was no such flag. 113 func (a AllFlags) GetFlag(flagKey string) (FlagState, bool) { 114 f, ok := a.flags[flagKey] 115 return f, ok 116 } 117 118 // GetValue returns the value of an individual feature flag at the time the state was recorded. The return 119 // value will be ldvalue.Null() if the flag returned the default value, or if there was no such flag. 120 // 121 // This is equivalent to calling GetFlag for the flag and then getting the Value property. 122 func (a AllFlags) GetValue(flagKey string) ldvalue.Value { 123 return a.flags[flagKey].Value 124 } 125 126 // ToValuesMap returns a map of flag keys to flag values. If a flag would have evaluated to the default 127 // value, its value will be ldvalue.Null(). 128 // 129 // Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client. 130 // Instead, convert the state object to JSON using json.Marshal. 131 func (a AllFlags) ToValuesMap() map[string]ldvalue.Value { 132 ret := make(map[string]ldvalue.Value, len(a.flags)) 133 for k, v := range a.flags { 134 ret[k] = v.Value 135 } 136 return ret 137 } 138 139 // MarshalJSON implements a custom JSON serialization for AllFlags, to produce the correct data structure 140 // for "bootstrapping" the LaunchDarkly JavaScript client. 141 func (a AllFlags) MarshalJSON() ([]byte, error) { 142 w := jwriter.NewWriter() 143 obj := w.Object() 144 obj.Name("$valid").Bool(a.valid) 145 for key, flag := range a.flags { 146 flag.Value.WriteToJSONWriter(obj.Name(key)) 147 } 148 stateObj := obj.Name("$flagsState").Object() 149 for key, flag := range a.flags { 150 flagObj := stateObj.Name(key).Object() 151 flagObj.Maybe("variation", flag.Variation.IsDefined()).Int(flag.Variation.IntValue()) 152 flagObj.Maybe("version", !flag.OmitDetails).Int(flag.Version) 153 if flag.Reason.IsDefined() && !flag.OmitDetails { 154 flag.Reason.WriteToJSONWriter(flagObj.Name("reason")) 155 } 156 flagObj.Maybe("trackEvents", flag.TrackEvents).Bool(flag.TrackEvents) 157 flagObj.Maybe("trackReason", flag.TrackReason).Bool(flag.TrackReason) 158 flagObj.Maybe("debugEventsUntilDate", flag.DebugEventsUntilDate > 0).Float64(float64(flag.DebugEventsUntilDate)) 159 flagObj.End() 160 } 161 stateObj.End() 162 obj.End() 163 return w.Bytes(), w.Error() 164 } 165 166 // NewAllFlagsBuilder creates a builder for constructing an AllFlags instance. This is normally done only by 167 // the SDK, but it may also be used in test code. 168 func NewAllFlagsBuilder(options ...Option) *AllFlagsBuilder { 169 b := &AllFlagsBuilder{ 170 state: AllFlags{ 171 flags: make(map[string]FlagState), 172 valid: true, 173 }, 174 } 175 for _, o := range options { 176 o.apply(&b.options) 177 } 178 return b 179 } 180 181 // Build returns an immutable State instance copied from the current builder data. 182 func (b *AllFlagsBuilder) Build() AllFlags { 183 return AllFlags{valid: b.state.valid, flags: maps.Clone(b.state.flags)} 184 } 185 186 // AddFlag adds information about a flag. 187 // 188 // The Reason property in the FlagState may or may not be recorded in the State, depending on the builder 189 // options. 190 func (b *AllFlagsBuilder) AddFlag(flagKey string, flag FlagState) *AllFlagsBuilder { 191 // To save bandwidth, we include evaluation reasons only if 1. the application explicitly said to 192 // include them or 2. they must be included because of experimentation 193 if b.options.detailsOnlyIfTracked { 194 if !flag.TrackEvents && !flag.TrackReason && 195 !(flag.DebugEventsUntilDate != 0 && flag.DebugEventsUntilDate > ldtime.UnixMillisNow()) { 196 flag.OmitDetails = true 197 } 198 } 199 if !b.options.withReasons && !flag.TrackReason { 200 flag.Reason = ldreason.EvaluationReason{} 201 } 202 b.state.flags[flagKey] = flag 203 return b 204 } 205 206 func (o clientSideOnlyOption) String() string { 207 return "ClientSideOnly" 208 } 209 210 func (o clientSideOnlyOption) apply(options *allFlagsOptions) { 211 } 212 213 func (o withReasonsOption) String() string { 214 return "WithReasons" 215 } 216 217 func (o withReasonsOption) apply(options *allFlagsOptions) { 218 options.withReasons = true 219 } 220 221 func (o detailsOnlyForTrackedFlagsOption) String() string { 222 return "DetailsOnlyForTrackedFlags" 223 } 224 225 func (o detailsOnlyForTrackedFlagsOption) apply(options *allFlagsOptions) { 226 options.detailsOnlyIfTracked = true 227 } 228