• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

moconnell / yolo / 28329529092

28 Jun 2026 05:01PM UTC coverage: 87.429% (+4.5%) from 82.93%
28329529092

Pull #131

github

web-flow
Merge c250324f8 into 58d55311c
Pull Request #131: Feat: Telemetry

425 of 459 branches covered (92.59%)

Branch coverage included in aggregate %.

956 of 1065 new or added lines in 16 files covered. (89.77%)

4 existing lines in 2 files now uncovered.

3727 of 4290 relevant lines covered (86.88%)

22.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.72
/src/YoloFunk/Functions/StorageQueryFunctions.cs
1
using System.Net;
2
using Azure;
3
using Azure.Data.Tables;
4
using Azure.Storage.Blobs;
5
using Microsoft.Azure.Functions.Worker;
6
using Microsoft.Azure.Functions.Worker.Http;
7
using Microsoft.Extensions.DependencyInjection;
8
using Microsoft.Extensions.Logging;
9
using YoloFunk.Dto;
10
using YoloFunk.Infrastructure;
11

12
namespace YoloFunk.Functions;
13

14
public sealed class StorageQueryFunctions(
6✔
15
    IServiceProvider serviceProvider,
6✔
16
    ILogger<StorageQueryFunctions> logger)
6✔
17
{
18
    private const string TradeExecutionsTableName = "tradeexecutions";
19
    private const string RebalanceEventsTableName = "rebalanceevents";
20
    private const string HttpRequestsTableName = "httprequestsindex";
21
    private const string HttpRequestsContainerName = "http-requests";
22

23
    [Function(nameof(GetTradeExecutions))]
24
    public async Task<HttpResponseData> GetTradeExecutions(
25
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/trade-executions")]
26
        HttpRequestData req,
27
        CancellationToken cancellationToken)
28
    {
3✔
29
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
3✔
30
            return await ServiceUnavailableAsync(req, cancellationToken);
1✔
31

32
        var query = HttpQueryParameters.Parse(req.Url);
2✔
33
        var page = query.GetInt32("page", 1, 1, 10_000);
2✔
34
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
2✔
35
        var orderBy = query.GetString("orderBy") ?? "submittedAt";
2✔
36
        var direction = NormalizeDirection(query.GetString("direction"));
2✔
37
        var continuationToken = query.GetString("continuationToken");
2✔
38

39
        try
40
        {
2✔
41
            var pageResult = await QueryTradeExecutionsAsync(
2✔
42
                tableServiceClient,
2✔
43
                query,
2✔
44
                pageSize,
2✔
45
                continuationToken,
2✔
46
                cancellationToken);
2✔
47
            var items = ApplyTradeExecutionSort(pageResult.Items, orderBy, direction);
2✔
48

49
            return await WritePagedResponseAsync(
2✔
50
                req,
2✔
51
                items,
2✔
52
                page,
2✔
53
                pageSize,
2✔
54
                orderBy,
2✔
55
                direction,
2✔
56
                cancellationToken,
2✔
57
                pageResult.NextContinuationToken,
2✔
58
                skipItems: false);
2✔
59
        }
NEW
60
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
61
        {
×
NEW
62
            return await WritePagedResponseAsync(
×
NEW
63
                req,
×
NEW
64
                Array.Empty<TradeExecutionQueryItem>(),
×
NEW
65
                page,
×
NEW
66
                pageSize,
×
NEW
67
                orderBy,
×
NEW
68
                direction,
×
NEW
69
                cancellationToken);
×
70
        }
NEW
71
        catch (Exception ex)
×
NEW
72
        {
×
NEW
73
            logger.LogError(ex, "Failed to query trade executions");
×
NEW
74
            return await ErrorAsync(req, "Failed to query trade executions", cancellationToken);
×
75
        }
76
    }
3✔
77

78
    [Function(nameof(GetRebalanceEvents))]
79
    public async Task<HttpResponseData> GetRebalanceEvents(
80
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/rebalance-events")]
81
        HttpRequestData req,
82
        CancellationToken cancellationToken)
83
    {
1✔
84
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
1✔
NEW
85
            return await ServiceUnavailableAsync(req, cancellationToken);
×
86

87
        var query = HttpQueryParameters.Parse(req.Url);
1✔
88
        var page = query.GetInt32("page", 1, 1, 10_000);
1✔
89
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
1✔
90
        var orderBy = query.GetString("orderBy") ?? "timestampUtc";
1✔
91
        var direction = NormalizeDirection(query.GetString("direction"));
1✔
92

93
        try
94
        {
1✔
95
            var items = await QueryRebalanceEventsAsync(tableServiceClient, query, cancellationToken);
1✔
96
            items = ApplyRebalanceEventSort(items, orderBy, direction);
1✔
97

98
            return await WritePagedResponseAsync(req, items, page, pageSize, orderBy, direction, cancellationToken);
1✔
99
        }
NEW
100
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
101
        {
×
NEW
102
            return await WritePagedResponseAsync(
×
NEW
103
                req,
×
NEW
104
                Array.Empty<RebalanceEventQueryItem>(),
×
NEW
105
                page,
×
NEW
106
                pageSize,
×
NEW
107
                orderBy,
×
NEW
108
                direction,
×
NEW
109
                cancellationToken);
×
110
        }
NEW
111
        catch (Exception ex)
×
NEW
112
        {
×
NEW
113
            logger.LogError(ex, "Failed to query rebalance events");
×
NEW
114
            return await ErrorAsync(req, "Failed to query rebalance events", cancellationToken);
×
115
        }
116
    }
1✔
117

118
    [Function(nameof(GetHttpRequestCaptures))]
119
    public async Task<HttpResponseData> GetHttpRequestCaptures(
120
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests")]
121
        HttpRequestData req,
122
        CancellationToken cancellationToken)
123
    {
1✔
124
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
1✔
NEW
125
            return await ServiceUnavailableAsync(req, cancellationToken);
×
126

127
        var query = HttpQueryParameters.Parse(req.Url);
1✔
128
        var page = query.GetInt32("page", 1, 1, 10_000);
1✔
129
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
1✔
130
        var orderBy = query.GetString("orderBy") ?? "requestTimeUtc";
1✔
131
        var direction = NormalizeDirection(query.GetString("direction"));
1✔
132

133
        try
134
        {
1✔
135
            var items = await QueryHttpRequestCapturesAsync(tableServiceClient, query, cancellationToken);
1✔
136
            items = ApplyHttpRequestCaptureSort(items, orderBy, direction);
1✔
137

138
            return await WritePagedResponseAsync(req, items, page, pageSize, orderBy, direction, cancellationToken);
1✔
139
        }
NEW
140
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
141
        {
×
NEW
142
            return await WritePagedResponseAsync(
×
NEW
143
                req,
×
NEW
144
                Array.Empty<HttpRequestCaptureQueryItem>(),
×
NEW
145
                page,
×
NEW
146
                pageSize,
×
NEW
147
                orderBy,
×
NEW
148
                direction,
×
NEW
149
                cancellationToken);
×
150
        }
NEW
151
        catch (Exception ex)
×
NEW
152
        {
×
NEW
153
            logger.LogError(ex, "Failed to query HTTP request captures");
×
NEW
154
            return await ErrorAsync(req, "Failed to query HTTP request captures", cancellationToken);
×
155
        }
156
    }
1✔
157

158
    [Function(nameof(GetHttpRequestPayload))]
159
    public async Task<HttpResponseData> GetHttpRequestPayload(
160
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests/payload")]
161
        HttpRequestData req,
162
        CancellationToken cancellationToken)
163
    {
1✔
164
        if (!TryGetBlobServiceClient(req, out var blobServiceClient))
1✔
NEW
165
            return await ServiceUnavailableAsync(req, cancellationToken);
×
166

167
        var blobName = HttpQueryParameters.Parse(req.Url).GetString("blobName");
1✔
168
        if (string.IsNullOrWhiteSpace(blobName))
1✔
169
        {
1✔
170
            var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
1✔
171
            await badRequest.WriteAsJsonAsync(
1✔
172
                new { Error = "Missing required query parameter: blobName" },
1✔
173
                cancellationToken);
1✔
174
            return badRequest;
1✔
175
        }
176

177
        try
NEW
178
        {
×
NEW
179
            var blobClient = blobServiceClient
×
NEW
180
                .GetBlobContainerClient(HttpRequestsContainerName)
×
NEW
181
                .GetBlobClient(blobName);
×
NEW
182
            var download = await blobClient.DownloadContentAsync(cancellationToken);
×
183

NEW
184
            var response = req.CreateResponse(HttpStatusCode.OK);
×
NEW
185
            response.Headers.Add("Content-Type", "application/json");
×
NEW
186
            await response.WriteStringAsync(download.Value.Content.ToString(), cancellationToken);
×
NEW
187
            return response;
×
188
        }
NEW
189
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
190
        {
×
NEW
191
            var notFound = req.CreateResponse(HttpStatusCode.NotFound);
×
NEW
192
            await notFound.WriteAsJsonAsync(
×
NEW
193
                new { Error = "HTTP request payload not found", BlobName = blobName },
×
NEW
194
                cancellationToken);
×
NEW
195
            return notFound;
×
196
        }
NEW
197
        catch (Exception ex)
×
NEW
198
        {
×
NEW
199
            logger.LogError(ex, "Failed to get HTTP request payload {BlobName}", blobName);
×
NEW
200
            return await ErrorAsync(req, "Failed to get HTTP request payload", cancellationToken);
×
201
        }
202
    }
1✔
203

204
    private bool TryGetTableServiceClient(
205
        HttpRequestData req,
206
        out TableServiceClient tableServiceClient)
207
    {
5✔
208
        tableServiceClient = serviceProvider.GetService<TableServiceClient>() ??
5✔
209
                             req.FunctionContext.InstanceServices.GetService<TableServiceClient>()!;
5✔
210

211
        return tableServiceClient is not null;
5✔
212
    }
5✔
213

214
    private bool TryGetBlobServiceClient(
215
        HttpRequestData req,
216
        out BlobServiceClient blobServiceClient)
217
    {
1✔
218
        blobServiceClient = serviceProvider.GetService<BlobServiceClient>() ??
1✔
219
                            req.FunctionContext.InstanceServices.GetService<BlobServiceClient>()!;
1✔
220

221
        return blobServiceClient is not null;
1✔
222
    }
1✔
223

224
    private static async Task<QueryPageResult<TradeExecutionQueryItem>> QueryTradeExecutionsAsync(
225
        TableServiceClient tableServiceClient,
226
        HttpQueryParameters query,
227
        int pageSize,
228
        string? continuationToken,
229
        CancellationToken cancellationToken)
230
    {
2✔
231
        var strategy = query.GetString("strategy");
2✔
232
        var coin = query.GetString("coin");
2✔
233
        var runId = query.GetString("runId");
2✔
234
        var status = query.GetString("status");
2✔
235
        var from = query.GetDateTimeOffset("from");
2✔
236
        var to = query.GetDateTimeOffset("to");
2✔
237

238
        var tableClient = tableServiceClient.GetTableClient(TradeExecutionsTableName);
2✔
239
        var page = await QueryEntitiesPageAsync(
2✔
240
            tableClient,
2✔
241
            BuildPartitionFilter(strategy),
2✔
242
            pageSize,
2✔
243
            continuationToken,
2✔
244
            cancellationToken);
2✔
245

246
        var items = page.Items
2✔
247
            .Select(ToTradeExecutionQueryItem)
2✔
248
            .Where(item => Matches(item.StrategyName, strategy))
3✔
249
            .Where(item => Matches(item.Coin, coin))
3✔
250
            .Where(item => Matches(item.RunId, runId))
2✔
251
            .Where(item => Matches(item.Status, status))
2✔
252
            .Where(item => !from.HasValue || (item.SubmittedAt ?? item.RecordedAt) >= from.Value)
2✔
253
            .Where(item => !to.HasValue || (item.SubmittedAt ?? item.RecordedAt) <= to.Value)
2✔
254
            .ToArray();
2✔
255

256
        return new QueryPageResult<TradeExecutionQueryItem>(items, page.NextContinuationToken);
2✔
257
    }
2✔
258

259
    private static async Task<IReadOnlyList<HttpRequestCaptureQueryItem>> QueryHttpRequestCapturesAsync(
260
        TableServiceClient tableServiceClient,
261
        HttpQueryParameters query,
262
        CancellationToken cancellationToken)
263
    {
1✔
264
        var host = query.GetString("host");
1✔
265
        var endpoint = query.GetString("endpoint");
1✔
266
        var method = query.GetString("method");
1✔
267
        var statusCode = query.GetString("statusCode");
1✔
268
        var contentHash = query.GetString("contentHash");
1✔
269
        var from = query.GetDateTimeOffset("from");
1✔
270
        var to = query.GetDateTimeOffset("to");
1✔
271

272
        var tableClient = tableServiceClient.GetTableClient(HttpRequestsTableName);
1✔
273
        var entities = await QueryEntitiesAsync(tableClient, BuildPartitionFilter(host), cancellationToken);
1✔
274

275
        return [.. entities
1✔
276
            .Select(ToHttpRequestCaptureQueryItem)
1✔
277
            .Where(item => Matches(item.Host, host))
1✔
278
            .Where(item => Contains(item.Endpoint, endpoint))
1✔
279
            .Where(item => Matches(item.Method, method))
1✔
280
            .Where(item => !int.TryParse(statusCode, out var expectedStatusCode) || item.StatusCode == expectedStatusCode)
1✔
281
            .Where(item => Matches(item.ContentHash, contentHash))
1✔
282
            .Where(item => !from.HasValue || item.RequestTimeUtc >= from.Value)
1✔
283
            .Where(item => !to.HasValue || item.RequestTimeUtc <= to.Value)];
2✔
284
    }
1✔
285

286
    private static async Task<IReadOnlyList<RebalanceEventQueryItem>> QueryRebalanceEventsAsync(
287
        TableServiceClient tableServiceClient,
288
        HttpQueryParameters query,
289
        CancellationToken cancellationToken)
290
    {
1✔
291
        var strategy = query.GetString("strategy");
1✔
292
        var runId = query.GetString("runId");
1✔
293
        var eventType = query.GetString("eventType");
1✔
294
        var level = query.GetString("level");
1✔
295
        var coin = query.GetString("coin");
1✔
296
        var clientOrderId = query.GetString("clientOrderId");
1✔
297
        var from = query.GetDateTimeOffset("from");
1✔
298
        var to = query.GetDateTimeOffset("to");
1✔
299

300
        var tableClient = tableServiceClient.GetTableClient(RebalanceEventsTableName);
1✔
301
        var partitionFilter = !string.IsNullOrWhiteSpace(strategy) && !string.IsNullOrWhiteSpace(runId)
1✔
302
            ? BuildPartitionFilter($"{strategy}|{runId}")
1✔
303
            : null;
1✔
304
        var entities = await QueryEntitiesAsync(tableClient, partitionFilter, cancellationToken);
1✔
305

306
        return [.. entities
1✔
307
            .Select(ToRebalanceEventQueryItem)
1✔
308
            .Where(item => Matches(item.StrategyName, strategy))
2✔
309
            .Where(item => Matches(item.RunId, runId))
2✔
310
            .Where(item => Matches(item.EventType, eventType))
2✔
311
            .Where(item => Matches(item.Level, level))
1✔
312
            .Where(item => Matches(item.Coin, coin))
1✔
313
            .Where(item => Matches(item.ClientOrderId, clientOrderId))
1✔
314
            .Where(item => !from.HasValue || item.TimestampUtc >= from.Value)
1✔
315
            .Where(item => !to.HasValue || item.TimestampUtc <= to.Value)];
2✔
316
    }
1✔
317

318
    private static async Task<IReadOnlyList<TableEntity>> QueryEntitiesAsync(
319
        TableClient tableClient,
320
        string? filter,
321
        CancellationToken cancellationToken)
322
    {
2✔
323
        var entities = new List<TableEntity>();
2✔
324
        await foreach (var entity in tableClient.QueryAsync<TableEntity>(
12✔
325
                           filter,
2✔
326
                           maxPerPage: 1_000,
2✔
327
                           cancellationToken: cancellationToken))
2✔
328
        {
3✔
329
            entities.Add(entity);
3✔
330
        }
3✔
331

332
        return entities;
2✔
333
    }
2✔
334

335
    private static async Task<QueryPageResult<TableEntity>> QueryEntitiesPageAsync(
336
        TableClient tableClient,
337
        string? filter,
338
        int pageSize,
339
        string? continuationToken,
340
        CancellationToken cancellationToken)
341
    {
2✔
342
        await foreach (var page in tableClient
8✔
343
                           .QueryAsync<TableEntity>(
2✔
344
                               filter,
2✔
345
                               maxPerPage: pageSize,
2✔
346
                               cancellationToken: cancellationToken)
2✔
347
                           .AsPages(continuationToken, pageSize))
2✔
348
        {
2✔
349
            return new QueryPageResult<TableEntity>([.. page.Values], page.ContinuationToken);
2✔
350
        }
351

NEW
352
        return new QueryPageResult<TableEntity>([], null);
×
353
    }
2✔
354

355
    private static string? BuildPartitionFilter(string? partitionKey)
356
    {
4✔
357
        return string.IsNullOrWhiteSpace(partitionKey)
4✔
358
            ? null
4✔
359
            : $"PartitionKey eq '{SanitizeTableKey(partitionKey).Replace("'", "''")}'";
4✔
360
    }
4✔
361

362
    private static IReadOnlyList<TradeExecutionQueryItem> ApplyTradeExecutionSort(
363
        IReadOnlyList<TradeExecutionQueryItem> items,
364
        string orderBy,
365
        string direction)
366
    {
2✔
367
        Func<TradeExecutionQueryItem, object?> key = orderBy.ToLowerInvariant() switch
2✔
368
        {
2✔
NEW
369
            "coin" => item => item.Coin,
×
NEW
370
            "completedat" => item => item.CompletedAt,
×
NEW
371
            "recordedat" => item => item.RecordedAt,
×
NEW
372
            "runid" => item => item.RunId,
×
NEW
373
            "status" => item => item.Status,
×
NEW
374
            "strategy" or "strategyname" => item => item.StrategyName,
×
375
            _ => item => item.SubmittedAt
4✔
376
        };
2✔
377

378
        return Sort(items, key, direction);
2✔
379
    }
2✔
380

381
    private static IReadOnlyList<HttpRequestCaptureQueryItem> ApplyHttpRequestCaptureSort(
382
        IReadOnlyList<HttpRequestCaptureQueryItem> items,
383
        string orderBy,
384
        string direction)
385
    {
1✔
386
        Func<HttpRequestCaptureQueryItem, object?> key = orderBy.ToLowerInvariant() switch
1✔
387
        {
1✔
NEW
388
            "endpoint" => item => item.Endpoint,
×
NEW
389
            "host" => item => item.Host,
×
NEW
390
            "method" => item => item.Method,
×
NEW
391
            "statuscode" => item => item.StatusCode,
×
392
            _ => item => item.RequestTimeUtc
2✔
393
        };
1✔
394

395
        return Sort(items, key, direction);
1✔
396
    }
1✔
397

398
    private static IReadOnlyList<RebalanceEventQueryItem> ApplyRebalanceEventSort(
399
        IReadOnlyList<RebalanceEventQueryItem> items,
400
        string orderBy,
401
        string direction)
402
    {
1✔
403
        Func<RebalanceEventQueryItem, object?> key = orderBy.ToLowerInvariant() switch
1✔
404
        {
1✔
NEW
405
            "eventtype" => item => item.EventType,
×
NEW
406
            "level" => item => item.Level,
×
NEW
407
            "runid" => item => item.RunId,
×
NEW
408
            "sequence" => item => item.Sequence,
×
NEW
409
            "strategy" or "strategyname" => item => item.StrategyName,
×
410
            _ => item => item.TimestampUtc
2✔
411
        };
1✔
412

413
        return Sort(items, key, direction);
1✔
414
    }
1✔
415

416
    private static IReadOnlyList<T> Sort<T>(
417
        IReadOnlyList<T> items,
418
        Func<T, object?> key,
419
        string direction)
420
    {
4✔
421
        return direction == "asc"
4✔
422
            ? [.. items.OrderBy(key)]
4✔
423
            : [.. items.OrderByDescending(key)];
4✔
424
    }
4✔
425

426
    private static async Task<HttpResponseData> WritePagedResponseAsync<T>(
427
        HttpRequestData req,
428
        IReadOnlyList<T> items,
429
        int page,
430
        int pageSize,
431
        string? orderBy,
432
        string direction,
433
        CancellationToken cancellationToken,
434
        string? nextContinuationToken = null,
435
        bool skipItems = true)
436
    {
4✔
437
        var response = req.CreateResponse(HttpStatusCode.OK);
4✔
438
        var pageItems = skipItems
4✔
439
            ? items
4✔
440
                .Skip((page - 1) * pageSize)
4✔
441
                .Take(pageSize)
4✔
442
                .ToArray()
4✔
443
            : [.. items.Take(pageSize)];
4✔
444

445
        await response.WriteAsJsonAsync(
4✔
446
            new PagedQueryResponse<T>(pageItems, page, pageSize, items.Count, orderBy, direction, nextContinuationToken),
4✔
447
            cancellationToken);
4✔
448
        return response;
4✔
449
    }
4✔
450

451
    private static TradeExecutionQueryItem ToTradeExecutionQueryItem(TableEntity entity)
452
    {
3✔
453
        return new TradeExecutionQueryItem(
3✔
454
            GetString(entity, "ExecutionId"),
3✔
455
            GetString(entity, "RunId"),
3✔
456
            GetString(entity, "StrategyName"),
3✔
457
            GetNullableString(entity, "WalletAddress"),
3✔
458
            GetNullableString(entity, "VaultAddress"),
3✔
459
            GetString(entity, "Coin"),
3✔
460
            GetString(entity, "Side"),
3✔
461
            GetNullableString(entity, "TargetPosition"),
3✔
462
            GetNullableString(entity, "CurrentPosition"),
3✔
463
            GetNullableString(entity, "IntendedDelta"),
3✔
464
            GetNullableString(entity, "ArrivalMid"),
3✔
465
            GetNullableString(entity, "ArrivalBid"),
3✔
466
            GetNullableString(entity, "ArrivalAsk"),
3✔
467
            GetNullableString(entity, "SpreadBps"),
3✔
468
            GetNullableString(entity, "OrderId"),
3✔
469
            GetString(entity, "OrderType"),
3✔
470
            GetNullableBool(entity, "PostOnly"),
3✔
471
            GetNullableBool(entity, "ReduceOnly"),
3✔
472
            GetNullableString(entity, "LimitPrice"),
3✔
473
            GetNullableDateTimeOffset(entity, "SubmittedAt"),
3✔
474
            GetNullableString(entity, "FilledQty"),
3✔
475
            GetNullableString(entity, "AvgFillPrice"),
3✔
476
            GetNullableString(entity, "Fees"),
3✔
477
            GetNullableString(entity, "MakerQty"),
3✔
478
            GetNullableString(entity, "MakerAvgFillPrice"),
3✔
479
            GetNullableString(entity, "MakerFees"),
3✔
480
            GetNullableString(entity, "TakerQty"),
3✔
481
            GetNullableString(entity, "TakerAvgFillPrice"),
3✔
482
            GetNullableString(entity, "TakerFees"),
3✔
483
            GetNullableString(entity, "CancelledQty"),
3✔
484
            GetNullableDateTimeOffset(entity, "CompletedAt"),
3✔
485
            GetNullableString(entity, "Status"),
3✔
486
            GetNullableString(entity, "Error"),
3✔
487
            GetNullableDateTimeOffset(entity, "RecordedAt"));
3✔
488
    }
3✔
489

490
    private static HttpRequestCaptureQueryItem ToHttpRequestCaptureQueryItem(TableEntity entity)
491
    {
1✔
492
        return new HttpRequestCaptureQueryItem(
1✔
493
            GetString(entity, "Host"),
1✔
494
            GetString(entity, "Endpoint"),
1✔
495
            GetString(entity, "Url"),
1✔
496
            GetString(entity, "Method"),
1✔
497
            GetInt32(entity, "StatusCode"),
1✔
498
            GetString(entity, "BlobContainer"),
1✔
499
            GetString(entity, "BlobName"),
1✔
500
            GetString(entity, "ContentHash"),
1✔
501
            GetDateTimeOffset(entity, "RequestTimeUtc"),
1✔
502
            GetString(entity, "QueryParametersJson"));
1✔
503
    }
1✔
504

505
    private static RebalanceEventQueryItem ToRebalanceEventQueryItem(TableEntity entity)
506
    {
2✔
507
        return new RebalanceEventQueryItem(
2✔
508
            GetString(entity, "RunId"),
2✔
509
            GetString(entity, "StrategyName"),
2✔
510
            GetDateTimeOffset(entity, "TimestampUtc"),
2✔
511
            GetInt32(entity, "Sequence"),
2✔
512
            GetString(entity, "EventType"),
2✔
513
            GetString(entity, "Level"),
2✔
514
            GetString(entity, "Summary"),
2✔
515
            GetNullableString(entity, "WalletAddress"),
2✔
516
            GetNullableString(entity, "VaultAddress"),
2✔
517
            GetNullableString(entity, "Coin"),
2✔
518
            GetNullableString(entity, "ClientOrderId"),
2✔
519
            GetNullableString(entity, "OrderId"),
2✔
520
            GetString(entity, "PayloadJson"));
2✔
521
    }
2✔
522

523
    private static bool Matches(string? actual, string? expected) =>
524
        string.IsNullOrWhiteSpace(expected) ||
22✔
525
        string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase);
22✔
526

527
    private static bool Contains(string actual, string? expected) =>
528
        string.IsNullOrWhiteSpace(expected) ||
1✔
529
        actual.Contains(expected, StringComparison.OrdinalIgnoreCase);
1✔
530

531
    private static string NormalizeDirection(string? direction) =>
532
        string.Equals(direction, "asc", StringComparison.OrdinalIgnoreCase) ? "asc" : "desc";
4✔
533

534
    private static string SanitizeTableKey(string value)
535
    {
4✔
536
        return value
4✔
537
            .Replace("/", "|")
4✔
538
            .Replace("\\", "|")
4✔
539
            .Replace("#", "_")
4✔
540
            .Replace("?", "_");
4✔
541
    }
4✔
542

543
    private static string GetString(TableEntity entity, string property) =>
544
        entity.TryGetValue(property, out var value) ? value?.ToString() ?? string.Empty : string.Empty;
38✔
545

546
    private static string? GetNullableString(TableEntity entity, string property) =>
547
        entity.TryGetValue(property, out var value) ? value?.ToString() : null;
79✔
548

549
    private static int GetInt32(TableEntity entity, string property) =>
550
        entity.TryGetValue(property, out var value) && value is int parsed ? parsed : 0;
3✔
551

552
    private static bool? GetNullableBool(TableEntity entity, string property) =>
553
        entity.TryGetValue(property, out var value) && value is bool parsed ? parsed : null;
6✔
554

555
    private static DateTimeOffset GetDateTimeOffset(TableEntity entity, string property) =>
556
        GetNullableDateTimeOffset(entity, property) ?? DateTimeOffset.MinValue;
3✔
557

558
    private static DateTimeOffset? GetNullableDateTimeOffset(TableEntity entity, string property) =>
559
        entity.TryGetValue(property, out var value) && value is DateTimeOffset parsed ? parsed : null;
12✔
560

561
    private static async Task<HttpResponseData> ServiceUnavailableAsync(
562
        HttpRequestData req,
563
        CancellationToken cancellationToken)
564
    {
1✔
565
        var response = req.CreateResponse(HttpStatusCode.ServiceUnavailable);
1✔
566
        await response.WriteAsJsonAsync(
1✔
567
            new { Error = "Azure storage is not configured. Set AzureWebJobsStorage to enable storage query endpoints." },
1✔
568
            cancellationToken);
1✔
569
        return response;
1✔
570
    }
1✔
571

572
    private static async Task<HttpResponseData> ErrorAsync(
573
        HttpRequestData req,
574
        string message,
575
        CancellationToken cancellationToken)
NEW
576
    {
×
NEW
577
        var response = req.CreateResponse(HttpStatusCode.InternalServerError);
×
NEW
578
        await response.WriteAsJsonAsync(new { Error = message }, cancellationToken);
×
NEW
579
        return response;
×
NEW
580
    }
×
581

582
    private sealed record QueryPageResult<T>(IReadOnlyList<T> Items, string? NextContinuationToken);
12✔
583
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc