|
5 | 5 | using PostHog; |
6 | 6 | using PostHog.Versioning; |
7 | 7 |
|
| 8 | +const int StaleEventDrainDelayMs = 100; |
| 9 | + |
8 | 10 | var builder = WebApplication.CreateBuilder(args); |
9 | 11 |
|
10 | 12 | // Configure JSON options for consistent serialization |
|
102 | 104 | return Results.Ok(new { success = true }); |
103 | 105 | }); |
104 | 106 |
|
| 107 | +app.MapPost("/get_feature_flag", async (FeatureFlagRequest request) => |
| 108 | +{ |
| 109 | + if (state.Client is null) |
| 110 | + { |
| 111 | + return Results.BadRequest(new { error = "SDK not initialized" }); |
| 112 | + } |
| 113 | + |
| 114 | + if (string.IsNullOrEmpty(request.Key) || string.IsNullOrEmpty(request.DistinctId)) |
| 115 | + { |
| 116 | + return Results.BadRequest(new { error = "key and distinct_id are required" }); |
| 117 | + } |
| 118 | + |
| 119 | + try |
| 120 | + { |
| 121 | + var options = new FeatureFlagOptions |
| 122 | + { |
| 123 | + PersonProperties = request.PersonProperties, |
| 124 | + Groups = ToGroupCollection(request.Groups, request.GroupProperties), |
| 125 | + FlagKeysToEvaluate = [request.Key], |
| 126 | + OnlyEvaluateLocally = request.ForceRemote == false, |
| 127 | + DisableGeoIp = request.DisableGeoIp ?? false |
| 128 | + }; |
| 129 | + |
| 130 | +#pragma warning disable CS0618 |
| 131 | + var flag = await state.Client.GetFeatureFlagAsync( |
| 132 | + request.Key, |
| 133 | + request.DistinctId, |
| 134 | + options, |
| 135 | + CancellationToken.None); |
| 136 | +#pragma warning restore CS0618 |
| 137 | + |
| 138 | + // Feature-flag evaluation captures a documented $feature_flag_called side-effect event. |
| 139 | + // Flush it in the same adapter action so a later /reset does not send stale events into |
| 140 | + // the next harness test's freshly-reset mock server. Keep a named drain delay aligned with |
| 141 | + // the /flush endpoint so async send completion timing is explicit and tunable. |
| 142 | + await state.Client.FlushAsync(); |
| 143 | + await Task.Delay(StaleEventDrainDelayMs); |
| 144 | + |
| 145 | + return Results.Ok(new |
| 146 | + { |
| 147 | + success = true, |
| 148 | + value = flag?.VariantKey ?? (object?)(flag?.IsEnabled ?? false) |
| 149 | + }); |
| 150 | + } |
| 151 | + catch (Exception ex) |
| 152 | + { |
| 153 | + Console.Error.WriteLine($"Feature flag error: {ex}"); |
| 154 | + state.RecordError(ex.Message); |
| 155 | + return Results.StatusCode(500); |
| 156 | + } |
| 157 | +}); |
| 158 | + |
105 | 159 | app.MapPost("/flush", async () => |
106 | 160 | { |
107 | 161 | if (state.Client is null) |
|
113 | 167 | { |
114 | 168 | await state.Client.FlushAsync(); |
115 | 169 |
|
116 | | - // Wait a bit for any pending requests to complete |
117 | | - await Task.Delay(100); |
| 170 | + // Wait a bit for any pending requests to complete. |
| 171 | + await Task.Delay(StaleEventDrainDelayMs); |
118 | 172 | } |
119 | 173 | catch (Exception ex) |
120 | 174 | { |
|
143 | 197 | return Results.Ok(new { success = true }); |
144 | 198 | }); |
145 | 199 |
|
| 200 | +GroupCollection? ToGroupCollection( |
| 201 | + Dictionary<string, object?>? groups, |
| 202 | + Dictionary<string, Dictionary<string, object?>>? groupProperties) |
| 203 | +{ |
| 204 | + if ((groups is null || groups.Count == 0) && (groupProperties is null || groupProperties.Count == 0)) |
| 205 | + { |
| 206 | + return null; |
| 207 | + } |
| 208 | + |
| 209 | + var collection = new GroupCollection(); |
| 210 | + if (groups is null) |
| 211 | + { |
| 212 | + return collection; |
| 213 | + } |
| 214 | + |
| 215 | + foreach (var (groupType, groupKeyValue) in groups) |
| 216 | + { |
| 217 | + var groupKey = ToStringValue(groupKeyValue); |
| 218 | + if (groupKey is null) |
| 219 | + { |
| 220 | + continue; |
| 221 | + } |
| 222 | + |
| 223 | + var properties = groupProperties is not null && groupProperties.TryGetValue(groupType, out var props) |
| 224 | + ? props |
| 225 | + : []; |
| 226 | + collection.Add(new Group(groupType, groupKey, properties)); |
| 227 | + } |
| 228 | + |
| 229 | + return collection; |
| 230 | +} |
| 231 | + |
| 232 | +static string? ToStringValue(object? value) => value switch |
| 233 | +{ |
| 234 | + null => null, |
| 235 | + string s => s, |
| 236 | + JsonElement { ValueKind: JsonValueKind.Null } => null, |
| 237 | + JsonElement { ValueKind: JsonValueKind.String } json => json.GetString(), |
| 238 | + JsonElement json => json.ToString(), |
| 239 | + _ => value.ToString() |
| 240 | +}; |
| 241 | + |
146 | 242 | app.Run(); |
147 | 243 |
|
148 | 244 | // --- Models --- |
@@ -170,6 +266,16 @@ record CaptureRequest( |
170 | 266 | [property: JsonPropertyName("timestamp")] string? Timestamp = null |
171 | 267 | ); |
172 | 268 |
|
| 269 | +record FeatureFlagRequest( |
| 270 | + [property: JsonPropertyName("key")] string Key, |
| 271 | + [property: JsonPropertyName("distinct_id")] string DistinctId, |
| 272 | + [property: JsonPropertyName("person_properties")] Dictionary<string, object?>? PersonProperties = null, |
| 273 | + [property: JsonPropertyName("groups")] Dictionary<string, object?>? Groups = null, |
| 274 | + [property: JsonPropertyName("group_properties")] Dictionary<string, Dictionary<string, object?>>? GroupProperties = null, |
| 275 | + [property: JsonPropertyName("disable_geoip")] bool? DisableGeoIp = null, |
| 276 | + [property: JsonPropertyName("force_remote")] bool? ForceRemote = null |
| 277 | +); |
| 278 | + |
173 | 279 | record StateResponse( |
174 | 280 | [property: JsonPropertyName("pending_events")] int PendingEvents, |
175 | 281 | [property: JsonPropertyName("total_events_captured")] int TotalEventsCaptured, |
|
0 commit comments