Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/npm_and_yarn/examples/openfeature…
Browse files Browse the repository at this point in the history
…_react/react-app/typescript-eslint/eslint-plugin-8.21.0
  • Loading branch information
kodiakhq[bot] authored Jan 27, 2025
2 parents 22af5de + 2d17828 commit 4b800e7
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
entry: golangci-lint run --enable-only=gci --fix

- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.11.0
rev: 24.10.0
hooks:
- id: black
language_version: python3.12
66 changes: 61 additions & 5 deletions exporter/data_exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (

"github.com/stretchr/testify/assert"
"github.com/thejerf/slogassert"
ffclient "github.com/thomaspoignant/go-feature-flag"
"github.com/thomaspoignant/go-feature-flag/exporter"
"github.com/thomaspoignant/go-feature-flag/ffcontext"
"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"
"github.com/thomaspoignant/go-feature-flag/testutils/mock"
"github.com/thomaspoignant/go-feature-flag/utils/fflog"
)
Expand All @@ -27,7 +29,7 @@ func TestDataExporterScheduler_flushWithTime(t *testing.T) {
inputEvents := []exporter.FeatureEvent{
exporter.NewFeatureEvent(
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), "random-key",
"YO", "defaultVar", false, "", "SERVER"),
"YO", "defaultVar", false, "", "SERVER", nil),
}

for _, event := range inputEvents {
Expand All @@ -50,7 +52,7 @@ func TestDataExporterScheduler_flushWithNumberOfEvents(t *testing.T) {
for i := 0; i <= 100; i++ {
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
"random-key", "YO", "defaultVar", false, "", "SERVER"))
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
}
for _, event := range inputEvents {
dc.AddEvent(event)
Expand All @@ -70,7 +72,7 @@ func TestDataExporterScheduler_defaultFlush(t *testing.T) {
for i := 0; i <= 100000; i++ {
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
"random-key", "YO", "defaultVar", false, "", "SERVER"))
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
}
for _, event := range inputEvents {
dc.AddEvent(event)
Expand All @@ -97,7 +99,7 @@ func TestDataExporterScheduler_exporterReturnError(t *testing.T) {
for i := 0; i <= 200; i++ {
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
"random-key", "YO", "defaultVar", false, "", "SERVER"))
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
}
for _, event := range inputEvents {
dc.AddEvent(event)
Expand All @@ -117,7 +119,7 @@ func TestDataExporterScheduler_nonBulkExporter(t *testing.T) {
for i := 0; i < 100; i++ {
inputEvents = append(inputEvents, exporter.NewFeatureEvent(
ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(),
"random-key", "YO", "defaultVar", false, "", "SERVER"))
"random-key", "YO", "defaultVar", false, "", "SERVER", nil))
}
for _, event := range inputEvents {
dc.AddEvent(event)
Expand All @@ -127,3 +129,57 @@ func TestDataExporterScheduler_nonBulkExporter(t *testing.T) {

assert.Equal(t, inputEvents[:100], mockExporter.GetExportedEvents())
}

func TestAddExporterMetadataFromContextToExporter(t *testing.T) {
tests := []struct {
name string
ctx ffcontext.EvaluationContext
want map[string]interface{}
}{
{
name: "extract exporter metadata from context",
ctx: ffcontext.NewEvaluationContextBuilder("targeting-key").AddCustom("gofeatureflag", map[string]interface{}{
"exporterMetadata": map[string]interface{}{
"key1": "value1",
"key2": 123,
"key3": true,
"key4": 123.45,
},
}).Build(),
want: map[string]interface{}{
"key1": "value1",
"key2": 123,
"key3": true,
"key4": 123.45,
},
},
{
name: "no exporter metadata in the context",
ctx: ffcontext.NewEvaluationContextBuilder("targeting-key").Build(),
want: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockExporter := &mock.Exporter{}
config := ffclient.Config{
Retriever: &fileretriever.Retriever{Path: "../testdata/flag-config.yaml"},
DataExporter: ffclient.DataExporter{
Exporter: mockExporter,
FlushInterval: 100 * time.Millisecond,
},
}
goff, err := ffclient.New(config)
assert.NoError(t, err)

_, err = goff.BoolVariation("test-flag", tt.ctx, false)
assert.NoError(t, err)

time.Sleep(120 * time.Millisecond)
assert.Equal(t, 1, len(mockExporter.GetExportedEvents()))
got := mockExporter.GetExportedEvents()[0].Metadata
assert.Equal(t, tt.want, got)
})
}
}
3 changes: 2 additions & 1 deletion exporter/feature_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ func NewFeatureEvent(
failed bool,
version string,
source string,
metadata FeatureEventMetadata,
) FeatureEvent {
contextKind := "user"
if ctx.IsAnonymous() {
contextKind = "anonymousUser"
}

return FeatureEvent{
Kind: "feature",
ContextKind: contextKind,
Expand All @@ -34,6 +34,7 @@ func NewFeatureEvent(
Default: failed,
Version: version,
Source: source,
Metadata: metadata,
}
}

Expand Down
17 changes: 9 additions & 8 deletions exporter/feature_event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import (

func TestNewFeatureEvent(t *testing.T) {
type args struct {
user ffcontext.Context
flagKey string
value interface{}
variation string
failed bool
version string
source string
user ffcontext.Context
flagKey string
value interface{}
variation string
failed bool
version string
source string
exporterMetadata exporter.FeatureEventMetadata
}
tests := []struct {
name string
Expand All @@ -44,7 +45,7 @@ func TestNewFeatureEvent(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
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)
})
}
}
Expand Down
3 changes: 2 additions & 1 deletion ffcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ func (u EvaluationContext) ExtractGOFFProtectedFields() GoffContextSpecifics {
case map[string]string:
goff.addCurrentDateTime(v["currentDateTime"])
goff.addListFlags(v["flagList"])
goff.addExporterMetadata(v["exporterMetadata"])
case map[string]interface{}:
goff.addCurrentDateTime(v["currentDateTime"])
goff.addListFlags(v["flagList"])
goff.addExporterMetadata(v["exporterMetadata"])
case GoffContextSpecifics:
return v
}

return goff
}
19 changes: 19 additions & 0 deletions ffcontext/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,25 @@ func Test_ExtractGOFFProtectedFields(t *testing.T) {
FlagList: []string{"flag1", "flag2"},
},
},
{
name: "context goff specifics with exporter metadata",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
"exporterMetadata": map[string]interface{}{
"toto": 123,
"titi": 123.45,
"tutu": true,
"tata": "bonjour",
},
}).Build(),
want: ffcontext.GoffContextSpecifics{
ExporterMetadata: map[string]interface{}{
"toto": 123,
"titi": 123.45,
"tutu": true,
"tata": "bonjour",
},
},
},
}

for _, tt := range tests {
Expand Down
8 changes: 8 additions & 0 deletions ffcontext/goff_context_specifics.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type GoffContextSpecifics struct {
CurrentDateTime *time.Time `json:"currentDateTime"`
// FlagList is the list of flags to evaluate in a bulk evaluation.
FlagList []string `json:"flagList"`
// ExporterMetadata is the metadata to be used by the exporter.
ExporterMetadata map[string]interface{} `json:"exporterMetadata"`
}

// addCurrentDateTime adds the current date time to the context.
Expand Down Expand Up @@ -40,3 +42,9 @@ func (g *GoffContextSpecifics) addListFlags(flagList any) {
}
}
}

func (g *GoffContextSpecifics) addExporterMetadata(exporterMetadata any) {
if value, ok := exporterMetadata.(map[string]interface{}); ok {
g.ExporterMetadata = value
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import json
from http import HTTPStatus
from threading import Thread
from typing import List, Optional, Type, Union
from urllib.parse import urljoin

import pylru
import urllib3
import websocket
from gofeatureflag_python_provider.data_collector_hook import DataCollectorHook
from gofeatureflag_python_provider.metadata import GoFeatureFlagMetadata
from gofeatureflag_python_provider.options import BaseModel, GoFeatureFlagOptions
from gofeatureflag_python_provider.request_flag_evaluation import (
RequestFlagEvaluation,
convert_evaluation_context,
)
from gofeatureflag_python_provider.response_flag_evaluation import (
JsonType,
ResponseFlagEvaluation,
)
from http import HTTPStatus
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import (
ErrorCode,
Expand All @@ -17,21 +24,11 @@
)
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.hook import Hook
from openfeature.provider.metadata import Metadata
from openfeature.provider import AbstractProvider
from openfeature.provider.metadata import Metadata
from pydantic import PrivateAttr, ValidationError

from gofeatureflag_python_provider.data_collector_hook import DataCollectorHook
from gofeatureflag_python_provider.metadata import GoFeatureFlagMetadata
from gofeatureflag_python_provider.options import BaseModel, GoFeatureFlagOptions
from gofeatureflag_python_provider.request_flag_evaluation import (
RequestFlagEvaluation,
convert_evaluation_context,
)
from gofeatureflag_python_provider.response_flag_evaluation import (
JsonType,
ResponseFlagEvaluation,
)
from threading import Thread
from typing import List, Optional, Type, Union

AbstractProviderMetaclass = type(AbstractProvider)
BaseModelMetaclass = type(BaseModel)
Expand Down Expand Up @@ -196,6 +193,16 @@ def generic_go_feature_flag_resolver(
"/v1/feature/{}/eval".format(flag_key),
)

# add exporter metadata to the context if it exists
if self.options.exporter_metadata:
goff_request.gofeatureflag["exporterMetadata"] = (
self.options.exporter_metadata
)
goff_request.gofeatureflag["exporterMetadata"]["openfeature"] = True
goff_request.gofeatureflag["exporterMetadata"][
"provider"
] = "python"

response = self._http_client.request(
method="POST",
url=url,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import hashlib
import json
from typing import Optional, Any

from gofeatureflag_python_provider.options import BaseModel
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import (
TargetingKeyMissingError,
InvalidContextError,
)
from pydantic import SkipValidation

from gofeatureflag_python_provider.options import BaseModel
from typing import Optional, Any, Dict


class GoFeatureFlagEvaluationContext(BaseModel):
Expand Down Expand Up @@ -56,3 +54,4 @@ def convert_evaluation_context(
class RequestFlagEvaluation(BaseModel):
user: GoFeatureFlagEvaluationContext
defaultValue: SkipValidation[Any] = None
gofeatureflag: Optional[Dict] = {}
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,50 @@ def test_url_parsing(mock_request):
assert got == want


@patch("urllib3.poolmanager.PoolManager.request")
def test_should_call_evaluation_api_with_exporter_metadata(
mock_request: Mock,
):
flag_key = "bool_targeting_match"
default_value = False
mock_request.side_effect = [
Mock(
status="200", data=_read_mock_file(flag_key)
), # first call to get the flag
Mock(status="200", data={}), # second call to send the data
Mock(status="200", data={}),
]
goff_provider = GoFeatureFlagProvider(
options=GoFeatureFlagOptions(
endpoint="https://gofeatureflag.org/",
data_flush_interval=100,
disable_cache_invalidation=True,
exporter_metadata={"version": "1.0.0", "name": "myapp", "id": 123},
)
)
api.set_provider(goff_provider)
client = api.get_client(domain="test-client")

client.get_boolean_details(
flag_key=flag_key,
default_value=default_value,
evaluation_context=_default_evaluation_ctx,
)

api.shutdown()
got = json.loads(mock_request.call_args.kwargs["body"])["gofeatureflag"]
want = {
"exporterMetadata": {
"version": "1.0.0",
"name": "myapp",
"id": 123,
"provider": "python",
"openfeature": True,
},
}
assert got == want


def _read_mock_file(flag_key: str) -> str:
# This hacky if is here to make test run inside pycharm and from the root of the project
if os.getcwd().endswith("/tests"):
Expand Down
2 changes: 1 addition & 1 deletion variation.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func notifyVariation[T model.JSONType](
) {
if result.TrackEvents {
event := exporter.NewFeatureEvent(ctx, flagKey, result.Value, result.VariationType, result.Failed, result.Version,
"SERVER")
"SERVER", ctx.ExtractGOFFProtectedFields().ExporterMetadata)
g.CollectEventData(event)
}
}
Expand Down
Loading

0 comments on commit 4b800e7

Please sign in to comment.