Commit 485976e
authored
feat: Add TrackDurationOf, TrackMetricsOf, TrackJudgeResult, TrackToolCall (#287)
## Summary
Adds the **compound tracking methods** and **new event types** to
`ILdAiConfigTracker`. Callers can now measure operation duration via a
callable wrapper, extract metrics from an operation result in one call,
record judge evaluation outcomes, and track tool invocations. The legacy
`TrackDurationOfTask` and `TrackRequest` methods are preserved but
marked `[Obsolete]`.
Six new methods on `ILdAiConfigTracker` (and implementations on
`LdAiConfigTracker`):
### `TrackDurationOf<T>`
```csharp
public Task<T> TrackDurationOf<T>(Func<Task<T>> operation);
```
Accepts a **callable** (not a pre-started task) so the tracker controls
when execution begins — duration measurement starts at invocation, not
at some earlier `Task.Run` call site. Uses `Stopwatch` for wall-clock
precision. Duration is recorded even if the operation throws (via
`finally`). Emits `$ld:ai:duration:total`.
Replaces `TrackDurationOfTask<T>(Task<T>)`, which is now `[Obsolete("Use
TrackDurationOf instead.")]`.
### `TrackMetricsOf<T>`
```csharp
public Task<T> TrackMetricsOf<T>(Func<T, AiMetrics> metricsExtractor, Func<Task<T>> operation);
```
All-in-one wrapper that tracks duration, success/error, and optional
token usage from a single operation. Flow:
1. Starts a stopwatch, invokes `operation`.
2. On success: stops timer → `TrackDuration` → calls
`metricsExtractor(result)` → `TrackSuccess`/`TrackError` based on
`AiMetrics.Success` → `TrackTokens` if `AiMetrics.Tokens` is non-null.
3. On exception: stops timer → `TrackDuration` → `TrackError` →
re-throws.
Replaces `TrackRequest(Task<Response>)`, which is now `[Obsolete("Use
TrackMetricsOf instead.")]`.
### `TrackJudgeResult`
```csharp
public void TrackJudgeResult(JudgeResult result);
```
Records a judge evaluation outcome. The event is **silently dropped**
when `result.Sampled == false` or `result.Success == false` — this
prevents noisy/invalid scores from polluting metrics.
When emitted, the event uses `result.MetricKey` as the track event name
and `result.Score` as the metric value. If `result.JudgeConfigKey` is
non-null, it's merged into the track data alongside the standard
`runId`/`configKey`/`variationKey`/`version` fields.
### `TrackToolCall`
```csharp
public void TrackToolCall(string toolKey);
```
Emits a `$ld:ai:tool_call` event with `toolKey` merged into the track
data. Unlike most tracker methods, this is **not** at-most-once — it may
be called multiple times to record every tool invocation in a run (each
emits a separate event with metric value `1`).
### `TrackToolCalls`
```csharp
public void TrackToolCalls(IEnumerable<string> toolKeys);
```
Convenience batch method — iterates `toolKeys` and calls `TrackToolCall`
for each.
### New types
**`AiMetrics`** — immutable record holding `Success` (bool) and optional
`Tokens` (Usage?) for use with `TrackMetricsOf`.
**`JudgeResult`** — immutable record holding `MetricKey`, `Score`,
`Sampled`, `Success`, and optional `JudgeConfigKey` for use with
`TrackJudgeResult`.
### `AgentConfigs` event fix
`AgentConfigs` now fires **only** the aggregate
`$ld:ai:usage:agent-configs` event. It calls the private
`BuildAgentConfig` path internally — it does NOT call the public
`AgentConfig()` method and does NOT fire individual
`$ld:ai:usage:agent-config` events. Tests updated to assert
`Times.Never` on individual events.
## Test plan
- [ ] `dotnet build` succeeds across `netstandard2.0`, `net462`,
`net8.0`
- [ ] `dotnet test --framework net8.0` passes
- [ ] `LdAiConfigTrackerTest` covers the new tracker surface:
- `TrackDurationOf_MeasuresDuration` — verifies wall-clock measurement
via a 50ms delay
- `TrackMetricsOf_SuccessPath_TracksAllMetrics` — verifies duration +
success + tokens all emitted
- `TrackMetricsOf_ErrorPath_TracksErrorAndRethrows` — verifies duration
+ error emitted, exception propagated
- `TrackJudgeResult_SampledFalse_NoEventEmitted` — verifies silent drop
- `TrackJudgeResult_SuccessFalse_NoEventEmitted` — verifies silent drop
- `TrackJudgeResult_SuccessPath_EmitsCorrectEvent` — verifies metric
key, score, judgeConfigKey in data
- `TrackToolCall_DataIncludesToolKey` — verifies `$ld:ai:tool_call`
event with toolKey in data
- `TrackToolCall_NoAtMostOnce_EmitsMultipleEvents` — verifies repeated
calls emit separate events
- `DeprecatedShims_StillCallable` — verifies `[Obsolete]` methods remain
functional
- [ ] `LdAiClientAgentJudgeTest.AgentConfigs_FiresOnlyAggregateEvent` —
verifies NO individual `$ld:ai:usage:agent-config` events
- [ ] `LdAiClientTest.AgentConfigs_OnlyBatchEventFired` — same assertion
from the client-level test
- [ ] Reviewer confirms method signatures, event names, and at-most-once
semantics match the cross-SDK contract (AITRACK §1.1.4, §1.1.12,
§1.1.13, §1.1.15)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes public tracker contracts and telemetry event shapes; behavior
is well-tested but callers migrating off obsolete APIs need to adopt the
new wrappers.
>
> **Overview**
> Extends **`ILdAiConfigTracker`** with compound wrappers and new event
types for the server AI SDK.
>
> **`TrackDurationOf`** times a `Func<Task<T>>` (so measurement starts
at invocation), records **`$ld:ai:duration:total`** even on failure, and
supersedes **`TrackDurationOfTask`**, which stays but is
**`[Obsolete]`**. **`TrackMetricsOf`** runs an operation, records
duration, then applies success/error and optional tokens via a new
**`AiMetrics`** extractor; **`TrackRequest`** is obsolete in favor of
this pattern.
>
> **`TrackJudgeResult`** emits judge scores under a caller metric key
when sampled and successful, optionally merging **`judgeConfigKey`**
into track data. **`TrackToolCall`** / **`TrackToolCalls`** emit
**`$ld:ai:tool_call`** with **`toolKey`** and are **not** at-most-once.
Supporting types **`AiMetrics`** and **`JudgeResult`** are added;
**`MergeTrackData`** enriches events for judge and tool tracking.
>
> Tests document **`AgentConfigs`** aggregate-only usage events (no
per-key **`$ld:ai:usage:agent-config`**) and cover the new tracker APIs
plus deprecated shims.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1e44b8d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 34293ca commit 485976e
6 files changed
Lines changed: 435 additions & 1 deletion
File tree
- pkgs/sdk/server-ai
- src
- Interfaces
- Tracking
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
41 | 51 | | |
42 | 52 | | |
43 | 53 | | |
| |||
49 | 59 | | |
50 | 60 | | |
51 | 61 | | |
| 62 | + | |
52 | 63 | | |
53 | 64 | | |
54 | 65 | | |
| |||
84 | 95 | | |
85 | 96 | | |
86 | 97 | | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
87 | 111 | | |
88 | 112 | | |
89 | 113 | | |
| |||
122 | 146 | | |
123 | 147 | | |
124 | 148 | | |
| 149 | + | |
125 | 150 | | |
126 | 151 | | |
127 | 152 | | |
| |||
130 | 155 | | |
131 | 156 | | |
132 | 157 | | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
133 | 178 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
| |||
144 | 145 | | |
145 | 146 | | |
146 | 147 | | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
147 | 164 | | |
148 | 165 | | |
149 | 166 | | |
| |||
217 | 234 | | |
218 | 235 | | |
219 | 236 | | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
220 | 276 | | |
221 | 277 | | |
222 | 278 | | |
| |||
274 | 330 | | |
275 | 331 | | |
276 | 332 | | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
277 | 371 | | |
278 | 372 | | |
279 | 373 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1035 | 1035 | | |
1036 | 1036 | | |
1037 | 1037 | | |
| 1038 | + | |
1038 | 1039 | | |
1039 | 1040 | | |
1040 | 1041 | | |
| |||
1079 | 1080 | | |
1080 | 1081 | | |
1081 | 1082 | | |
1082 | | - | |
| 1083 | + | |
1083 | 1084 | | |
1084 | 1085 | | |
| 1086 | + | |
1085 | 1087 | | |
1086 | 1088 | | |
1087 | 1089 | | |
1088 | 1090 | | |
1089 | 1091 | | |
1090 | 1092 | | |
| 1093 | + | |
1091 | 1094 | | |
1092 | 1095 | | |
1093 | 1096 | | |
| |||
0 commit comments