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
5 changes: 5 additions & 0 deletions .changeset/spec211-failed-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-go": patch
---

Preserve response-wide computation errors alongside evaluation_failed in direct flag-read events when remote evaluation uses the local poller fallback.
63 changes: 63 additions & 0 deletions feature_flags_failed_telemetry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package posthog

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestPollerFailedFlagTelemetryRetainsResponseErrors(t *testing.T) {
for _, globalError := range []string{"true", "false", "absent"} {
for _, inconclusive := range []bool{false, true} {
for _, variant := range []string{"null", `"placeholder"`} {
name := globalError + "/missing-local/" + variant
definitions := localDefinitions()
if inconclusive {
name = globalError + "/inconclusive-local/" + variant
definitions = localDefinitions(continuityCheckoutDefinition)
}
t.Run(name, func(t *testing.T) {
globalField := ""
if globalError != "absent" {
globalField = `"errorsWhileComputingFlags":` + globalError + `,`
}
remote := `{` + globalField + `"flags":{"checkout":{"key":"checkout","enabled":false,"variant":` + variant + `,"failed":true,"metadata":{"payload":"\"remote\""}},"healthy":{"key":"healthy","enabled":false,"failed":false}}}`
client, capture, _ := newUnresolvedFlagsClient(t, definitions, remote)
config := FeatureFlagPayload{Key: "checkout", DistinctId: "person"}
for i := 0; i < 2; i++ {
result, err := client.GetFeatureFlagResult(config)
require.Nil(t, result)
require.ErrorIs(t, err, ErrFlagNotFound)
}
expected := FeatureFlagErrorEvaluationFailed
if globalError == "true" {
expected = FeatureFlagErrorErrorsWhileComputing + "," + expected
}
require.Equal(t, expected, flagCalledError(t, client, capture, "checkout"))
events := snapshotCapturedEvents(capture)
require.Equal(t, 1, countFlagCalledEvents(events, "checkout"))
require.Nil(t, findEvent(events, "$feature_flag_called", "checkout").Properties["$feature_flag_response"])

config.DistinctId = "suppressed"
config.SendFeatureFlagEvents = Ptr(false)
result, err := client.GetFeatureFlagResult(config)
require.Nil(t, result)
require.ErrorIs(t, err, ErrFlagNotFound)
flushEvaluationEvents(t, client)
require.Equal(t, 1, countFlagCalledEvents(snapshotCapturedEvents(capture), "checkout"))

healthy, err := client.GetFeatureFlagResult(FeatureFlagPayload{Key: "healthy", DistinctId: "person"})
require.NoError(t, err)
require.False(t, healthy.Enabled)
healthyError := flagCalledError(t, client, capture, "healthy")
require.NotContains(t, healthyError, FeatureFlagErrorEvaluationFailed)
if globalError == "true" {
require.Equal(t, FeatureFlagErrorErrorsWhileComputing, healthyError)
} else {
require.Empty(t, healthyError)
}
})
}
}
}
}
7 changes: 5 additions & 2 deletions featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -1390,7 +1390,8 @@ type flagValueAndPayload struct {
minimalFlagCalledEvents bool
// flagFailed is set when /flags included the key but could not evaluate it.
// The enabled value on that detail is not a decision.
flagFailed bool
flagFailed bool
errorsWhileComputingFlags bool
}

// GetFeatureFlagWithPayload evaluates a feature flag once and returns both its value
Expand Down Expand Up @@ -1429,6 +1430,7 @@ func (poller *FeatureFlagsPoller) GetFeatureFlagWithPayload(flagConfig FeatureFl

locallyEvaluated := err == nil && result != nil
flagFailed := false
errorsWhileComputingFlags := false
hasExperiment := flag.HasExperiment
minimalFlagCalledEvents := state != nil && state.minimalFlagCalledEvents

Expand All @@ -1447,6 +1449,7 @@ func (poller *FeatureFlagsPoller) GetFeatureFlagWithPayload(flagConfig FeatureFl
hasExperiment = nil
minimalFlagCalledEvents = false
if flagsResponse != nil {
errorsWhileComputingFlags = flagsResponse.ErrorsWhileComputingFlags
minimalFlagCalledEvents = flagsResponse.MinimalFlagCalledEvents
if remoteFlagFailed(flagsResponse, flagConfig.Key) {
// enabled=false on a failed detail is a placeholder. Leave the
Expand All @@ -1470,7 +1473,7 @@ func (poller *FeatureFlagsPoller) GetFeatureFlagWithPayload(flagConfig FeatureFl
}
}

return flagValueAndPayload{value: result, payload: payload, err: err, locallyEvaluated: locallyEvaluated, hasExperiment: hasExperiment, minimalFlagCalledEvents: minimalFlagCalledEvents, flagFailed: flagFailed}
return flagValueAndPayload{value: result, payload: payload, err: err, locallyEvaluated: locallyEvaluated, hasExperiment: hasExperiment, minimalFlagCalledEvents: minimalFlagCalledEvents, flagFailed: flagFailed, errorsWhileComputingFlags: errorsWhileComputingFlags}
}

func (poller *FeatureFlagsPoller) getFeatureFlag(flagConfig FeatureFlagPayload) (FeatureFlag, error) {
Expand Down
1 change: 1 addition & 0 deletions posthog.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ func (c *client) getFeatureFlagResultWithContext(ctx context.Context, flagConfig
evalResult.Value = flagValue
evalResult.Err = err
evalResult.FlagFailed = combined.flagFailed
evalResult.ErrorsWhileComputingFlags = combined.errorsWhileComputingFlags
if combined.payload != "" {
payloadStr = combined.payload
hasPayload = true
Expand Down
Loading