Skip to content

Commit 2d4d2a1

Browse files
committed
feat: Support transient identities and traits
1 parent ad6f7fe commit 2d4d2a1

File tree

3 files changed

+76
-43
lines changed

3 files changed

+76
-43
lines changed

client.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) {
108108
return Flags{}, &FlagsmithClientError{msg: fmt.Sprintf("Failed to fetch flags with error: %s", err)}
109109
}
110110

111+
type GetIdentityFlagsOpts struct {
112+
Transient bool `json:"transient,omitempty"`
113+
}
114+
111115
// Returns `Flags` struct holding all the flags for the current environment for
112116
// a given identity.
113117
//
@@ -118,13 +122,13 @@ func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) {
118122
// If local evaluation is enabled this function will not call the Flagsmith API
119123
// directly, but instead read the asynchronously updated local environment or
120124
// use the default flag handler in case it has not yet been updated.
121-
func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits []*Trait) (f Flags, err error) {
125+
func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits []*Trait, opts *GetIdentityFlagsOpts) (f Flags, err error) {
122126
if c.config.localEvaluation || c.config.offlineMode {
123127
if f, err = c.getIdentityFlagsFromEnvironment(identifier, traits); err == nil {
124128
return f, nil
125129
}
126130
} else {
127-
if f, err = c.GetIdentityFlagsFromAPI(ctx, identifier, traits); err == nil {
131+
if f, err = c.GetIdentityFlagsFromAPI(ctx, identifier, traits, opts); err == nil {
128132
return f, nil
129133
}
130134
}
@@ -191,11 +195,15 @@ func (c *Client) GetEnvironmentFlagsFromAPI(ctx context.Context) (Flags, error)
191195

192196
// GetIdentityFlagsFromAPI tries to contact the Flagsmith API to get the latest identity flags.
193197
// Will return an error in case of failure or unexpected response.
194-
func (c *Client) GetIdentityFlagsFromAPI(ctx context.Context, identifier string, traits []*Trait) (Flags, error) {
198+
func (c *Client) GetIdentityFlagsFromAPI(ctx context.Context, identifier string, traits []*Trait, opts *GetIdentityFlagsOpts) (Flags, error) {
195199
body := struct {
196200
Identifier string `json:"identifier"`
197201
Traits []*Trait `json:"traits,omitempty"`
202+
GetIdentityFlagsOpts
198203
}{Identifier: identifier, Traits: traits}
204+
if opts != nil {
205+
body.Transient = opts.Transient
206+
}
199207
resp, err := c.client.NewRequest().
200208
SetBody(&body).
201209
SetContext(ctx).

client_test.go

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func TestGetIdentityFlagsUseslocalEnvironmentWhenAvailable(t *testing.T) {
231231
// Then
232232
assert.NoError(t, err)
233233

234-
flags, err := client.GetIdentityFlags(ctx, "test_identity", nil)
234+
flags, err := client.GetIdentityFlags(ctx, "test_identity", nil, nil)
235235

236236
assert.NoError(t, err)
237237

@@ -257,7 +257,7 @@ func TestGetIdentityFlagsUseslocalOverridesWhenAvailable(t *testing.T) {
257257
// Then
258258
assert.NoError(t, err)
259259

260-
flags, err := client.GetIdentityFlags(ctx, "overridden-id", nil)
260+
flags, err := client.GetIdentityFlags(ctx, "overridden-id", nil, nil)
261261

262262
assert.NoError(t, err)
263263

@@ -272,55 +272,78 @@ func TestGetIdentityFlagsUseslocalOverridesWhenAvailable(t *testing.T) {
272272

273273
func TestGetIdentityFlagsCallsAPIWhenLocalEnvironmentNotAvailableWithTraits(t *testing.T) {
274274
// Given
275+
stringTrait := flagsmith.Trait{TraitKey: "stringTrait", TraitValue: "trait_value"}
276+
intTrait := flagsmith.Trait{TraitKey: "intTrait", TraitValue: 1}
277+
floatTrait := flagsmith.Trait{TraitKey: "floatTrait", TraitValue: 1.11}
278+
boolTrait := flagsmith.Trait{TraitKey: "boolTrait", TraitValue: true}
279+
nillTrait := flagsmith.Trait{TraitKey: "NoneTrait", TraitValue: nil}
280+
transientTrait := flagsmith.Trait{TraitKey: "TransientTrait", TraitValue: "not_persisted", Transient: true}
281+
282+
testCases := []struct {
283+
Identifier string
284+
Traits []*flagsmith.Trait
285+
Opts *flagsmith.GetIdentityFlagsOpts
286+
ExpectedRequestBody string
287+
}{
288+
{
289+
"test_identity",
290+
[]*flagsmith.Trait{&stringTrait, &intTrait, &floatTrait, &boolTrait, &nillTrait, &transientTrait},
291+
nil,
292+
`{"identifier":"test_identity","traits":[{"trait_key":"stringTrait","trait_value":"trait_value"},` +
293+
`{"trait_key":"intTrait","trait_value":1},` +
294+
`{"trait_key":"floatTrait","trait_value":1.11},` +
295+
`{"trait_key":"boolTrait","trait_value":true},` +
296+
`{"trait_key":"NoneTrait","trait_value":null},` +
297+
`{"trait_key":"TransientTrait","trait_value":"not_persisted","transient":true}]}`,
298+
},
299+
{
300+
"test_transient_identity",
301+
[]*flagsmith.Trait{},
302+
&flagsmith.GetIdentityFlagsOpts{Transient: true},
303+
`{"identifier":"test_transient_identity","transient":true}`,
304+
},
305+
}
306+
275307
ctx := context.Background()
276-
expectedRequestBody := `{"identifier":"test_identity","traits":[{"trait_key":"stringTrait","trait_value":"trait_value"},` +
277-
`{"trait_key":"intTrait","trait_value":1},` +
278-
`{"trait_key":"floatTrait","trait_value":1.11},` +
279-
`{"trait_key":"boolTrait","trait_value":true},` +
280-
`{"trait_key":"NoneTrait","trait_value":null}]}`
281308

282-
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
283-
assert.Equal(t, req.URL.Path, "/api/v1/identities/")
284-
assert.Equal(t, fixtures.EnvironmentAPIKey, req.Header.Get("X-Environment-Key"))
309+
for _, tc := range testCases {
310+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
311+
assert.Equal(t, req.URL.Path, "/api/v1/identities/")
312+
assert.Equal(t, fixtures.EnvironmentAPIKey, req.Header.Get("X-Environment-Key"))
285313

286-
// Test that we sent the correct body
287-
rawBody, err := io.ReadAll(req.Body)
288-
assert.NoError(t, err)
289-
assert.Equal(t, expectedRequestBody, string(rawBody))
314+
// Test that we sent the correct body
315+
rawBody, err := io.ReadAll(req.Body)
316+
assert.NoError(t, err)
317+
assert.Equal(t, tc.ExpectedRequestBody, string(rawBody))
290318

291-
rw.Header().Set("Content-Type", "application/json")
319+
rw.Header().Set("Content-Type", "application/json")
292320

293-
rw.WriteHeader(http.StatusOK)
294-
_, err = io.WriteString(rw, fixtures.IdentityResponseJson)
321+
rw.WriteHeader(http.StatusOK)
322+
_, err = io.WriteString(rw, fixtures.IdentityResponseJson)
295323

296-
assert.NoError(t, err)
297-
}))
298-
defer server.Close()
299-
// When
300-
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
301-
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
324+
assert.NoError(t, err)
325+
}))
326+
defer server.Close()
327+
// When
328+
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
329+
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
302330

303-
stringTrait := flagsmith.Trait{TraitKey: "stringTrait", TraitValue: "trait_value"}
304-
intTrait := flagsmith.Trait{TraitKey: "intTrait", TraitValue: 1}
305-
floatTrait := flagsmith.Trait{TraitKey: "floatTrait", TraitValue: 1.11}
306-
boolTrait := flagsmith.Trait{TraitKey: "boolTrait", TraitValue: true}
307-
nillTrait := flagsmith.Trait{TraitKey: "NoneTrait", TraitValue: nil}
331+
// When
308332

309-
traits := []*flagsmith.Trait{&stringTrait, &intTrait, &floatTrait, &boolTrait, &nillTrait}
310-
// When
333+
flags, err := client.GetIdentityFlags(ctx, tc.Identifier, tc.Traits, tc.Opts)
311334

312-
flags, err := client.GetIdentityFlags(ctx, "test_identity", traits)
335+
// Then
336+
assert.NoError(t, err)
313337

314-
// Then
315-
assert.NoError(t, err)
338+
allFlags := flags.AllFlags()
316339

317-
allFlags := flags.AllFlags()
340+
assert.Equal(t, 1, len(allFlags))
318341

319-
assert.Equal(t, 1, len(allFlags))
342+
assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName)
343+
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
344+
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
320345

321-
assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName)
322-
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
323-
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
346+
}
324347
}
325348

326349
func TestDefaultHandlerIsUsedWhenNoMatchingEnvironmentFlagReturned(t *testing.T) {
@@ -608,7 +631,7 @@ func TestOfflineMode(t *testing.T) {
608631
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
609632

610633
// And GetIdentityFlags works as well
611-
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil)
634+
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil, nil)
612635
assert.NoError(t, err)
613636

614637
allFlags = flags.AllFlags()
@@ -650,7 +673,7 @@ func TestOfflineHandlerIsUsedWhenRequestFails(t *testing.T) {
650673
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
651674

652675
// And GetIdentityFlags works as well
653-
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil)
676+
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil, nil)
654677
assert.NoError(t, err)
655678

656679
allFlags = flags.AllFlags()

models.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ type Flag struct {
2020
type Trait struct {
2121
TraitKey string `json:"trait_key"`
2222
TraitValue interface{} `json:"trait_value"`
23+
Transient bool `json:"transient,omitempty"`
2324
}
2425

2526
type IdentityTraits struct {
2627
Identifier string `json:"identifier"`
2728
Traits []*Trait `json:"traits"`
29+
Transient bool `json:"transient,omitempty"`
2830
}
2931

3032
func (t *Trait) ToTraitModel() *traits.TraitModel {

0 commit comments

Comments
 (0)