1 package ldreason 2 3 import ( 4 "fmt" 5 6 "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 7 8 "github.com/launchdarkly/go-jsonstream/v3/jreader" 9 "github.com/launchdarkly/go-jsonstream/v3/jwriter" 10 ) 11 12 // EvalReasonKind defines the possible values of [EvaluationReason.GetKind]. 13 type EvalReasonKind string 14 15 const ( 16 // EvalReasonOff indicates that the flag was off and therefore returned its configured off value. 17 EvalReasonOff EvalReasonKind = "OFF" 18 // EvalReasonTargetMatch indicates that the context key was specifically targeted for this flag. 19 EvalReasonTargetMatch EvalReasonKind = "TARGET_MATCH" 20 // EvalReasonRuleMatch indicates that the context matched one of the flag's rules. 21 EvalReasonRuleMatch EvalReasonKind = "RULE_MATCH" 22 // EvalReasonPrerequisiteFailed indicates that the flag was considered off because it had at 23 // least one prerequisite flag that either was off or did not return the desired variation. 24 EvalReasonPrerequisiteFailed EvalReasonKind = "PREREQUISITE_FAILED" 25 // EvalReasonFallthrough indicates that the flag was on but the context did not match any targets 26 // or rules. 27 EvalReasonFallthrough EvalReasonKind = "FALLTHROUGH" 28 // EvalReasonError indicates that the flag could not be evaluated, e.g. because it does not 29 // exist or due to an unexpected error. In this case the result value will be the default value 30 // that the caller passed to the client. 31 EvalReasonError EvalReasonKind = "ERROR" 32 ) 33 34 // EvalErrorKind defines the possible values of EvaluationReason.GetErrorKind(). 35 type EvalErrorKind string 36 37 const ( 38 // EvalErrorClientNotReady indicates that the caller tried to evaluate a flag before the client 39 // had successfully initialized. 40 EvalErrorClientNotReady EvalErrorKind = "CLIENT_NOT_READY" 41 // EvalErrorFlagNotFound indicates that the caller provided a flag key that did not match any 42 // known flag. 43 EvalErrorFlagNotFound EvalErrorKind = "FLAG_NOT_FOUND" 44 // EvalErrorMalformedFlag indicates that there was an internal inconsistency in the flag data, 45 // e.g. a rule specified a nonexistent variation. 46 EvalErrorMalformedFlag EvalErrorKind = "MALFORMED_FLAG" 47 // EvalErrorUserNotSpecified indicates that the caller passed an invalid or uninitialized 48 // context. The name and value of this constant refer to "user" rather than "context" only for 49 // backward compatibility with older SDKs that used the term "user". 50 EvalErrorUserNotSpecified EvalErrorKind = "USER_NOT_SPECIFIED" 51 // EvalErrorWrongType indicates that the result value was not of the requested type, e.g. you 52 // called BoolVariationDetail but the value was an integer. 53 EvalErrorWrongType EvalErrorKind = "WRONG_TYPE" 54 // EvalErrorException indicates that an unexpected error stopped flag evaluation; check the 55 // log for details. 56 EvalErrorException EvalErrorKind = "EXCEPTION" 57 ) 58 59 // BigSegmentsStatus defines the possible values of [EvaluationReason.GetBigSegmentsStatus]. 60 // 61 // "Big segments" are a specific type of segments. For more information, read the LaunchDarkly 62 // documentation: https://docs.launchdarkly.com/home/contexts/big-segments 63 type BigSegmentsStatus string 64 65 const ( 66 // BigSegmentsHealthy indicates that the Big Segment query involved in the flag 67 // evaluation was successful, and that the segment state is considered up to date. 68 BigSegmentsHealthy BigSegmentsStatus = "HEALTHY" 69 70 // BigSegmentsStale indicates that the Big Segment query involved in the flag 71 // evaluation was successful, but that the segment state may not be up to date. 72 BigSegmentsStale BigSegmentsStatus = "STALE" 73 74 // BigSegmentsNotConfigured indicates that Big Segments could not be queried for the 75 // flag evaluation because the SDK configuration did not include a big segment store. 76 BigSegmentsNotConfigured BigSegmentsStatus = "NOT_CONFIGURED" 77 78 // BigSegmentsStoreError indicates that the Big Segment query involved in the flag 79 // evaluation failed, for instance due to a database error. 80 BigSegmentsStoreError BigSegmentsStatus = "STORE_ERROR" 81 ) 82 83 // EvaluationReason describes the reason that a flag evaluation producted a particular value. 84 // 85 // This struct is immutable; its properties can be accessed only via getter methods. 86 type EvaluationReason struct { 87 kind EvalReasonKind 88 ruleIndex ldvalue.OptionalInt 89 ruleID string 90 prerequisiteKey string 91 inExperiment bool 92 errorKind EvalErrorKind 93 bigSegmentsStatus BigSegmentsStatus 94 } 95 96 // IsDefined returns true if this EvaluationReason has a non-empty [EvaluationReason.GetKind]. It is 97 // false for a zero value of EvaluationReason{}. 98 func (r EvaluationReason) IsDefined() bool { 99 return r.kind != "" 100 } 101 102 // String returns a concise string representation of the reason. Examples: "OFF", "ERROR(WRONG_TYPE)". 103 // 104 // This value is intended only for convenience in logging or debugging. Application code should not 105 // rely on its specific format. 106 func (r EvaluationReason) String() string { 107 switch r.kind { 108 case EvalReasonRuleMatch: 109 return fmt.Sprintf("%s(%d,%s)", r.kind, r.ruleIndex.OrElse(0), r.ruleID) 110 case EvalReasonPrerequisiteFailed: 111 return fmt.Sprintf("%s(%s)", r.kind, r.prerequisiteKey) 112 case EvalReasonError: 113 return fmt.Sprintf("%s(%s)", r.kind, r.errorKind) 114 default: 115 return string(r.GetKind()) 116 } 117 } 118 119 // GetKind describes the general category of the reason. 120 func (r EvaluationReason) GetKind() EvalReasonKind { 121 return r.kind 122 } 123 124 // GetRuleIndex provides the index of the rule that was matched (0 being the first), if 125 // the Kind is [EvalReasonRuleMatch]. Otherwise it returns -1. 126 func (r EvaluationReason) GetRuleIndex() int { 127 return r.ruleIndex.OrElse(-1) 128 } 129 130 // GetRuleID provides the unique identifier of the rule that was matched, if the Kind is 131 // [EvalReasonRuleMatch]. Otherwise it returns an empty string. Unlike the rule index, this 132 // identifier will not change if other rules are added or deleted. 133 func (r EvaluationReason) GetRuleID() string { 134 return r.ruleID 135 } 136 137 // GetPrerequisiteKey provides the flag key of the prerequisite that failed, if the Kind 138 // is [EvalReasonPrerequisiteFailed]. Otherwise it returns an empty string. 139 func (r EvaluationReason) GetPrerequisiteKey() string { 140 return r.prerequisiteKey 141 } 142 143 // IsInExperiment describes whether the evaluation was part of an experiment. It returns 144 // true if the evaluation resulted in an experiment rollout *and* served one of the 145 // variations in the experiment. Otherwise it returns false. 146 func (r EvaluationReason) IsInExperiment() bool { 147 return r.inExperiment 148 } 149 150 // GetErrorKind describes the general category of the error, if the Kind is [EvalReasonError]. 151 // Otherwise it returns an empty string. 152 func (r EvaluationReason) GetErrorKind() EvalErrorKind { 153 return r.errorKind 154 } 155 156 // GetBigSegmentsStatus describes the validity of Big Segment information, if and only if the flag 157 // evaluation required querying at least one Big Segment. Otherwise it returns an empty string. 158 // 159 // "Big segments" are a specific kind of segments. For more information, read the LaunchDarkly 160 // documentation: https://docs.launchdarkly.com/home/contexts/big-segments 161 func (r EvaluationReason) GetBigSegmentsStatus() BigSegmentsStatus { 162 return r.bigSegmentsStatus 163 } 164 165 // NewEvalReasonOff returns an EvaluationReason whose Kind is [EvalReasonOff]. 166 func NewEvalReasonOff() EvaluationReason { 167 return EvaluationReason{kind: EvalReasonOff} 168 } 169 170 // NewEvalReasonFallthrough returns an EvaluationReason whose Kind is [EvalReasonFallthrough]. 171 func NewEvalReasonFallthrough() EvaluationReason { 172 return EvaluationReason{kind: EvalReasonFallthrough} 173 } 174 175 // NewEvalReasonFallthroughExperiment returns an EvaluationReason whose Kind is 176 // [EvalReasonFallthrough]. The inExperiment parameter represents whether the evaluation was 177 // part of an experiment. 178 func NewEvalReasonFallthroughExperiment(inExperiment bool) EvaluationReason { 179 return EvaluationReason{kind: EvalReasonFallthrough, inExperiment: inExperiment} 180 } 181 182 // NewEvalReasonTargetMatch returns an EvaluationReason whose Kind is [EvalReasonTargetMatch]. 183 func NewEvalReasonTargetMatch() EvaluationReason { 184 return EvaluationReason{kind: EvalReasonTargetMatch} 185 } 186 187 // NewEvalReasonRuleMatch returns an EvaluationReason whose Kind is [EvalReasonRuleMatch]. 188 func NewEvalReasonRuleMatch(ruleIndex int, ruleID string) EvaluationReason { 189 return EvaluationReason{kind: EvalReasonRuleMatch, 190 ruleIndex: ldvalue.NewOptionalInt(ruleIndex), ruleID: ruleID} 191 } 192 193 // NewEvalReasonRuleMatchExperiment returns an EvaluationReason whose Kind is 194 // [EvalReasonRuleMatch]. The inExperiment parameter represents whether the evaluation was 195 // part of an experiment. 196 func NewEvalReasonRuleMatchExperiment(ruleIndex int, ruleID string, inExperiment bool) EvaluationReason { 197 return EvaluationReason{ 198 kind: EvalReasonRuleMatch, 199 ruleIndex: ldvalue.NewOptionalInt(ruleIndex), 200 ruleID: ruleID, 201 inExperiment: inExperiment, 202 } 203 } 204 205 // NewEvalReasonPrerequisiteFailed returns an EvaluationReason whose Kind is [EvalReasonPrerequisiteFailed]. 206 func NewEvalReasonPrerequisiteFailed(prereqKey string) EvaluationReason { 207 return EvaluationReason{kind: EvalReasonPrerequisiteFailed, prerequisiteKey: prereqKey} 208 } 209 210 // NewEvalReasonError returns an EvaluationReason whose Kind is [EvalReasonError]. 211 func NewEvalReasonError(errorKind EvalErrorKind) EvaluationReason { 212 return EvaluationReason{kind: EvalReasonError, errorKind: errorKind} 213 } 214 215 // NewEvalReasonFromReasonWithBigSegmentsStatus returns a copy of an EvaluationReason 216 // with a specific [BigSegmentsStatus] value added. 217 func NewEvalReasonFromReasonWithBigSegmentsStatus( 218 reason EvaluationReason, 219 bigSegmentsStatus BigSegmentsStatus, 220 ) EvaluationReason { 221 reason.bigSegmentsStatus = bigSegmentsStatus 222 return reason 223 } 224 225 // MarshalJSON implements custom JSON serialization for EvaluationReason. 226 func (r EvaluationReason) MarshalJSON() ([]byte, error) { 227 return jwriter.MarshalJSONWithWriter(r) 228 } 229 230 // UnmarshalJSON implements custom JSON deserialization for EvaluationReason. 231 func (r *EvaluationReason) UnmarshalJSON(data []byte) error { 232 return jreader.UnmarshalJSONWithReader(data, r) 233 } 234 235 // ReadFromJSONReader provides JSON deserialization for use with the jsonstream API. 236 // 237 // This implementation is used by the SDK in cases where it is more efficient than [encoding/json.Unmarshal]. 238 // See [github.com/launchdarkly/go-jsonstream/v3] for more details. 239 func (r *EvaluationReason) ReadFromJSONReader(reader *jreader.Reader) { 240 var ret EvaluationReason 241 for obj := reader.ObjectOrNull(); obj.Next(); { 242 switch string(obj.Name()) { 243 case "kind": 244 ret.kind = EvalReasonKind(reader.String()) 245 case "ruleId": 246 ret.ruleID = reader.String() 247 case "ruleIndex": 248 ret.ruleIndex = ldvalue.NewOptionalInt(reader.Int()) 249 case "errorKind": 250 ret.errorKind = EvalErrorKind(reader.String()) 251 case "prerequisiteKey": 252 ret.prerequisiteKey = reader.String() 253 case "inExperiment": 254 ret.inExperiment = reader.Bool() 255 case "bigSegmentsStatus": 256 ret.bigSegmentsStatus = BigSegmentsStatus(reader.String()) 257 } 258 } 259 if reader.Error() == nil { 260 *r = ret 261 } 262 } 263 264 // WriteToJSONWriter provides JSON serialization for use with the jsonstream API. 265 // 266 // This implementation is used by the SDK in cases where it is more efficient than [encoding/json.Marshal]. 267 // See [github.com/launchdarkly/go-jsonstream/v3] for more details. 268 func (r EvaluationReason) WriteToJSONWriter(w *jwriter.Writer) { 269 if r.kind == "" { 270 w.Null() 271 return 272 } 273 obj := w.Object() 274 obj.Name("kind").String(string(r.kind)) 275 if r.ruleIndex.IsDefined() { 276 obj.Name("ruleIndex").Int(r.ruleIndex.OrElse(0)) 277 obj.Maybe("ruleId", r.ruleID != "").String(r.ruleID) 278 } 279 obj.Maybe("inExperiment", r.inExperiment).Bool(r.inExperiment) 280 if r.kind == EvalReasonPrerequisiteFailed { 281 obj.Name("prerequisiteKey").String(r.prerequisiteKey) 282 } 283 if r.kind == EvalReasonError { 284 obj.Name("errorKind").String(string(r.errorKind)) 285 } 286 if r.bigSegmentsStatus != "" { 287 obj.Name("bigSegmentsStatus").String(string(r.bigSegmentsStatus)) 288 } 289 obj.End() 290 } 291