Skip to content

Commit 5e4e7f6

Browse files
fix: Use exporter metadata in remote evaluations
Signed-off-by: Thomas Poignant <[email protected]>
1 parent 61a8753 commit 5e4e7f6

File tree

8 files changed

+113
-21
lines changed

8 files changed

+113
-21
lines changed

exporter/data_exporter_test.go

+61-5
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010

1111
"github.com/stretchr/testify/assert"
1212
"github.com/thejerf/slogassert"
13+
ffclient "github.com/thomaspoignant/go-feature-flag"
1314
"github.com/thomaspoignant/go-feature-flag/exporter"
1415
"github.com/thomaspoignant/go-feature-flag/ffcontext"
16+
"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"
1517
"github.com/thomaspoignant/go-feature-flag/testutils/mock"
1618
"github.com/thomaspoignant/go-feature-flag/utils/fflog"
1719
)
@@ -27,7 +29,7 @@ func TestDataExporterScheduler_flushWithTime(t *testing.T) {
2729
inputEvents := []exporter.FeatureEvent{
2830
exporter.NewFeatureEvent(
2931
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), "random-key",
30-
"YO", "defaultVar", false, "", "SERVER"),
32+
"YO", "defaultVar", false, "", "SERVER", nil),
3133
}
3234

3335
for _, event := range inputEvents {
@@ -50,7 +52,7 @@ func TestDataExporterScheduler_flushWithNumberOfEvents(t *testing.T) {
5052
for i := 0; i <= 100; i++ {
5153
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
5254
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
53-
"random-key", "YO", "defaultVar", false, "", "SERVER"))
55+
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
5456
}
5557
for _, event := range inputEvents {
5658
dc.AddEvent(event)
@@ -70,7 +72,7 @@ func TestDataExporterScheduler_defaultFlush(t *testing.T) {
7072
for i := 0; i <= 100000; i++ {
7173
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
7274
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
73-
"random-key", "YO", "defaultVar", false, "", "SERVER"))
75+
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
7476
}
7577
for _, event := range inputEvents {
7678
dc.AddEvent(event)
@@ -97,7 +99,7 @@ func TestDataExporterScheduler_exporterReturnError(t *testing.T) {
9799
for i := 0; i <= 200; i++ {
98100
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
99101
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
100-
"random-key", "YO", "defaultVar", false, "", "SERVER"))
102+
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
101103
}
102104
for _, event := range inputEvents {
103105
dc.AddEvent(event)
@@ -117,7 +119,7 @@ func TestDataExporterScheduler_nonBulkExporter(t *testing.T) {
117119
for i := 0; i < 100; i++ {
118120
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
119121
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
120-
"random-key", "YO", "defaultVar", false, "", "SERVER"))
122+
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
121123
}
122124
for _, event := range inputEvents {
123125
dc.AddEvent(event)
@@ -127,3 +129,57 @@ func TestDataExporterScheduler_nonBulkExporter(t *testing.T) {
127129

128130
assert.Equal(t, inputEvents[:100], mockExporter.GetExportedEvents())
129131
}
132+
133+
func TestAddExporterMetadataFromContextToExporter(t *testing.T) {
134+
tests := []struct {
135+
name string
136+
ctx ffcontext.EvaluationContext
137+
want map[string]interface{}
138+
}{
139+
{
140+
name: "extract exporter metadata from context",
141+
ctx: ffcontext.NewEvaluationContextBuilder("targeting-key").AddCustom("gofeatureflag", map[string]interface{}{
142+
"exporterMetadata": map[string]interface{}{
143+
"key1": "value1",
144+
"key2": 123,
145+
"key3": true,
146+
"key4": 123.45,
147+
},
148+
}).Build(),
149+
want: map[string]interface{}{
150+
"key1": "value1",
151+
"key2": 123,
152+
"key3": true,
153+
"key4": 123.45,
154+
},
155+
},
156+
{
157+
name: "no exporter metadata in the context",
158+
ctx: ffcontext.NewEvaluationContextBuilder("targeting-key").Build(),
159+
want: nil,
160+
},
161+
}
162+
163+
for _, tt := range tests {
164+
t.Run(tt.name, func(t *testing.T) {
165+
mockExporter := &mock.Exporter{}
166+
config := ffclient.Config{
167+
Retriever: &fileretriever.Retriever{Path: "../testdata/flag-config.yaml"},
168+
DataExporter: ffclient.DataExporter{
169+
Exporter: mockExporter,
170+
FlushInterval: 100 * time.Millisecond,
171+
},
172+
}
173+
goff, err := ffclient.New(config)
174+
assert.NoError(t, err)
175+
176+
_, err = goff.BoolVariation("test-flag", tt.ctx, false)
177+
assert.NoError(t, err)
178+
179+
time.Sleep(120 * time.Millisecond)
180+
assert.Equal(t, 1, len(mockExporter.GetExportedEvents()))
181+
got := mockExporter.GetExportedEvents()[0].Metadata
182+
assert.Equal(t, tt.want, got)
183+
})
184+
}
185+
}

exporter/feature_event.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ func NewFeatureEvent(
1717
failed bool,
1818
version string,
1919
source string,
20+
metadata FeatureEventMetadata,
2021
) FeatureEvent {
2122
contextKind := "user"
2223
if ctx.IsAnonymous() {
2324
contextKind = "anonymousUser"
2425
}
25-
2626
return FeatureEvent{
2727
Kind: "feature",
2828
ContextKind: contextKind,
@@ -34,6 +34,7 @@ func NewFeatureEvent(
3434
Default: failed,
3535
Version: version,
3636
Source: source,
37+
Metadata: metadata,
3738
}
3839
}
3940

exporter/feature_event_test.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import (
1212

1313
func TestNewFeatureEvent(t *testing.T) {
1414
type args struct {
15-
user ffcontext.Context
16-
flagKey string
17-
value interface{}
18-
variation string
19-
failed bool
20-
version string
21-
source string
15+
user ffcontext.Context
16+
flagKey string
17+
value interface{}
18+
variation string
19+
failed bool
20+
version string
21+
source string
22+
exporterMetadata exporter.FeatureEventMetadata
2223
}
2324
tests := []struct {
2425
name string
@@ -44,7 +45,7 @@ func TestNewFeatureEvent(t *testing.T) {
4445
}
4546
for _, tt := range tests {
4647
t.Run(tt.name, func(t *testing.T) {
47-
assert.Equalf(t, tt.want, exporter.NewFeatureEvent(tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source), "NewFeatureEvent(%v, %v, %v, %v, %v, %v, %V)", tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source)
48+
assert.Equalf(t, tt.want, exporter.NewFeatureEvent(tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source, tt.args.exporterMetadata), "NewFeatureEvent(%v, %v, %v, %v, %v, %v, %V)", tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source)
4849
})
4950
}
5051
}

ffcontext/context.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,13 @@ func (u EvaluationContext) ExtractGOFFProtectedFields() GoffContextSpecifics {
8181
case map[string]string:
8282
goff.addCurrentDateTime(v["currentDateTime"])
8383
goff.addListFlags(v["flagList"])
84+
goff.addExporterMetadata(v["exporterMetadata"])
8485
case map[string]interface{}:
8586
goff.addCurrentDateTime(v["currentDateTime"])
8687
goff.addListFlags(v["flagList"])
88+
goff.addExporterMetadata(v["exporterMetadata"])
8789
case GoffContextSpecifics:
8890
return v
8991
}
90-
9192
return goff
9293
}

ffcontext/context_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,25 @@ func Test_ExtractGOFFProtectedFields(t *testing.T) {
142142
FlagList: []string{"flag1", "flag2"},
143143
},
144144
},
145+
{
146+
name: "context goff specifics with exporter metadata",
147+
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
148+
"exporterMetadata": map[string]interface{}{
149+
"toto": 123,
150+
"titi": 123.45,
151+
"tutu": true,
152+
"tata": "bonjour",
153+
},
154+
}).Build(),
155+
want: ffcontext.GoffContextSpecifics{
156+
ExporterMetadata: map[string]interface{}{
157+
"toto": 123,
158+
"titi": 123.45,
159+
"tutu": true,
160+
"tata": "bonjour",
161+
},
162+
},
163+
},
145164
}
146165

147166
for _, tt := range tests {

ffcontext/goff_context_specifics.go

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ type GoffContextSpecifics struct {
77
CurrentDateTime *time.Time `json:"currentDateTime"`
88
// FlagList is the list of flags to evaluate in a bulk evaluation.
99
FlagList []string `json:"flagList"`
10+
// ExporterMetadata is the metadata to be used by the exporter.
11+
ExporterMetadata map[string]interface{} `json:"exporterMetadata"`
1012
}
1113

1214
// addCurrentDateTime adds the current date time to the context.
@@ -40,3 +42,9 @@ func (g *GoffContextSpecifics) addListFlags(flagList any) {
4042
}
4143
}
4244
}
45+
46+
func (g *GoffContextSpecifics) addExporterMetadata(exporterMetadata any) {
47+
if value, ok := exporterMetadata.(map[string]interface{}); ok {
48+
g.ExporterMetadata = value
49+
}
50+
}

variation.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func notifyVariation[T model.JSONType](
270270
) {
271271
if result.TrackEvents {
272272
event := exporter.NewFeatureEvent(ctx, flagKey, result.Value, result.VariationType, result.Failed, result.Version,
273-
"SERVER")
273+
"SERVER", ctx.ExtractGOFFProtectedFields().ExporterMetadata)
274274
g.CollectEventData(event)
275275
}
276276
}

website/docs/concepts/evaluation-context.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,16 @@ The targeting key is used to ensure that a user consistently receives the same v
6767
For instance, **GO Feature Flag** ensures that in cases where a feature is being rolled out to a percentage of users, based on the targeting key, they will see the same variation each time they encounter the feature flag.
6868

6969
## Reserved properties in the evaluation context
70+
:::danger
71+
If you put a key named `gofeatureflag` in your evaluation context, it may break internal features of GO Feature Flag.
72+
This property name is reserved for internal use.
73+
:::
74+
7075
When you create an evaluation context some fields are reserved for GO Feature Flag.
71-
Those fields are used by GO Feature Flag directly, you can use them as will in your targeting queries but you should be aware that they are used internally for GO Feature Flag.
76+
Those fields are used by GO Feature Flag directly, you can use them as will in your targeting queries, but you should be aware that they are used internally for GO Feature Flag.
7277

73-
| Field | Description |
74-
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
75-
| `gofeatureflag.currentDateTime` | If this property is set, we will use this date as base for all the rollout strategies which implies dates _(experimentation, progressive and scheduled)_.<br/>**Format:** Date following the RF3339 format. |
76-
| `gofeatureflag.flagList` | If this property is set, in the bulk evaluation mode (for the client SDK) we will only evaluate the flags in this list.<br/>If empty or not set the default behavior is too evaluate all the flags.<br/>**Format:** []string |
78+
| Field | Description |
79+
|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
80+
| `gofeatureflag.currentDateTime` | If this property is set, we will use this date as base for all the rollout strategies which implies dates _(experimentation, progressive and scheduled)_.<br/>**Format:** Date following the RF3339 format. |
81+
| `gofeatureflag.flagList` | If this property is set, in the bulk evaluation mode (for the client SDK) we will only evaluate the flags in this list.<br/>If empty or not set the default behavior is too evaluate all the flags.<br/>**Format:** []string |
82+
| `gofeatureflag.exporterMetadata` | If this property is set, we will add all the fields in the feature event send to the provider.<br/>**Format:** map[string]string\|number\|bool |

0 commit comments

Comments
 (0)