Launch Darkly Restful API

Caveat: More of an opinion, I feel using Launch Darkly REST API as part of application logic is trashington but the API exists and I wanted to know how to use it. Potentially in the future I’ll have a valid use case.

My team needed to programatically remove keys from a Launch Darkly segment and it tickled my interest as this cannot be done with the SDK however Launch Darkly does provide a REST API (which is what I suspect SDK Client is probabaly using under the hood). I had a play in the past with Launch Darkly SDK Clients.

Complete code for this post is at https://github.com/carlpaton/LaunchDarklyDemo/tree/main/FeatureClient2

Test Data And SDK Tests

Launch Darkly segments are groups of values that can be applied to more than one rule, so you can have sweet segment 3 with the guids listed below applied to n feature flags. For my tests I just added the guids below to the feature sweet-feature-name-3

1
2
3
3f43cd8d-c2f1-4a5a-b07b-a6fc2495dabf
e3dbd64b-f51d-4aff-a4e5-f334960b9045
85d223cc-7dc6-4b89-ad99-a17a612856ea
  1. Create the segment using the GUI and add the values

Create Segment

  1. Create the flag and apply the segment

Create Flag

  1. Using the code described in this SDK post and pushed here, query Launch Darkly. The expected output is as follows
1
CheckByUserKey: featureName=sweet-feature-name-3 userId=e3dbd64b-f51d-4aff-a4e5-f334960b9045 returned allowed=True

Restful API - Change Segment From Code

The REST API uses an access token (Generate from the GUI -> Account settings -> Authorization -> Access tokens). This is not the same as an SDK Key which is associated with the project when you set it up.

These are the steps I followed to make use of the Restful API and change the segment described above from code.

  1. Create the HTTP client and set its headers, in a real world application this will be injected into the pipeline with Microsoft.Extensions.DependencyInjection.AddHttpClient instead of being new HttpClient(); - remember new is glue :D
1
2
3
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", "api-00000000-0000-0000-0000-000000000000");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");

Additionally I setup some serializer options - I really dont understand why this is not the default

1
2
3
4
var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

Lastly I created a user variable - this can be any string that represents your entity. Guids are the most common so thats what I used to represent a user.

1
var userId = "e3dbd64b-f51d-4aff-a4e5-f334960b9045";
  1. We need to generate the segment URL which has a predefined format as seen at https://apidocs.launchdarkly.com/tag/Segments#operation/getSegment
1
2
3
4
5
6
7
8
9
private static string GetSegmentUri()
{
var projectKey = "default";
var environmentKey = "production";
var segmentKey = "sweet-segment-3";
var baseUrl = "https://app.launchdarkly.com/api/v2";

return $"{baseUrl}/segments/{projectKey}/{environmentKey}/{segmentKey}";
}
  1. Get the segment, this will be the JSON representation of the segment created above. It was called sweet-segment-3. This code would need some defensive checks for null on responseBody but you knew that already 😊
1
2
3
4
5
6
7
8
private static async Task<Segment> GetSegment(HttpClient httpClient, JsonSerializerOptions options)
{
var response = await httpClient.GetAsync(GetSegmentUri());
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();

return JsonSerializer.Deserialize<Segment>(responseBody, options);
}

This is what the JSON response from the server looked like when I tested

Complete JSON response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
"name": "sweet segment 3",
"tags": [],
"creationDate": 1687318631256,
"lastModifiedDate": 1687318969195,
"key": "sweet-segment-3",
"included": [],
"excluded": [],
"includedContexts": [],
"excludedContexts": [],
"_links": {
"parent": {
"href": "/api/v2/segments/default/production",
"type": "application/json"
},
"self": {
"href": "/api/v2/segments/default/production/sweet-segment-3",
"type": "application/json"
},
"site": {
"href": "/default/production/segments/sweet-segment-3",
"type": "text/html"
}
},
"rules": [{
"_id": "da4d32e6-de8c-4533-870c-1423dcd466fd",
"clauses": [{
"_id": "771ee791-6820-48d5-902a-0d997b11582c",
"attribute": "key",
"op": "in",
"values": ["85d223cc-7dc6-4b89-ad99-a17a612856ea", "e3dbd64b-f51d-4aff-a4e5-f334960b9045", "3f43cd8d-c2f1-4a5a-b07b-a6fc2495dabf"],
"contextKind": "user",
"negate": false
}],
"rolloutContextKind": "user"
}],
"version": 4,
"deleted": false,
"_flags": [{
"name": "sweet feature name 3",
"key": "sweet-feature-name-3",
"_links": {
"parent": {
"href": "/api/v2/flags/default",
"type": "application/json"
},
"self": {
"href": "/api/v2/flags/default/sweet-feature-name-3",
"type": "application/json"
}
},
"_site": {
"href": "/default/production/features/sweet-feature-name-3",
"type": "text/html"
}
}],
"generation": 1
}
  1. Get the patch operations, here the assumption is there is only one rule we care about. From the above its
1
2
3
4
5
6
7
8
9
10
11
12
"rules": [{
"_id": "da4d32e6-de8c-4533-870c-1423dcd466fd",
"clauses": [{
"_id": "771ee791-6820-48d5-902a-0d997b11582c",
"attribute": "key",
"op": "in",
"values": ["85d223cc-7dc6-4b89-ad99-a17a612856ea", "e3dbd64b-f51d-4aff-a4e5-f334960b9045", "3f43cd8d-c2f1-4a5a-b07b-a6fc2495dabf"],
"contextKind": "user",
"negate": false
}],
"rolloutContextKind": "user"
}],

From there we itterate over the clauses and interrogate the values using ToHashSet. Should the userId exist in that hash we create a LdPatchOperation. From code this look like the below. The Op is an operation type requested. Example remove. This can be neatly wrapped up on a LaunchDarklyDto class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static List<LdPatchOperation> GetPatchOperation(string userId, Segment segment)
{
var firstRule = segment.Rules.First();
var patchOperations = new List<LdPatchOperation>();

for (int clauseIndex = 0; clauseIndex < firstRule.Clauses.Count; clauseIndex++)
{
var currentClause = firstRule.Clauses[clauseIndex];
var guidsInClause = currentClause.Values.ToHashSet();
var exists = guidsInClause.Contains(userId);

if (exists)
{
var idIndex = currentClause.Values.IndexOf(userId);
patchOperations.Add(new LdPatchOperation()
{
Op = "remove",
Path = $"/rules/0/clauses/{clauseIndex}/values/{idIndex}",
});
}
}

return patchOperations;
}
  1. Update the segment by passing the operations as a Patch property. Here we must use the HTTP verb PATCH which we get implicitly by calling PatchAsync.

Its very important here that we check EnsureSuccessStatusCode as if we get something like 429 (Rate Limit) then the update was unsuccessful - potentially this is possible if you are using a script thats iterating over a large set of userIds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static async Task UpdateSegment(List<LdPatchOperation> operations, string userId, HttpClient httpClient, JsonSerializerOptions options)
{
if (!operations.Any())
return;

var body = new LdPatchSegmentPayload()
{
Patch = operations,
Comment = $"Removing userId : {userId}"
};
var content = JsonSerializer.Serialize(body, options);
var stringContent = new StringContent(content, Encoding.UTF8, "application/json");

var response = await httpClient.PatchAsync(GetSegmentUri(), stringContent);
response.EnsureSuccessStatusCode();
}
  1. Now using the same checks from Test Data And SDK Tests above, if we look for e3dbd64b-f51d-4aff-a4e5-f334960b9045 it would have been removed
1
CheckByUserKey: featureName=sweet-feature-name-3 userId=e3dbd64b-f51d-4aff-a4e5-f334960b9045 returned allowed=False

This can be confirmed from the GUI

This can be confirmed from the GUI

  1. The complete code call stack could look as follows
1
2
3
4
5
var userId = "e3dbd64b-f51d-4aff-a4e5-f334960b9045";
var segment = await GetSegment(httpClient, jsonOptions);
var operations = GetPatchOperation(userId, segment);

await UpdateSegment(operations, userId, httpClient, jsonOptions);

References