@oneuptime/common 12.0.24 → 12.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/Span.ts +101 -0
- package/Server/API/BaseAPI.ts +0 -24
- package/Server/API/SlackAPI.ts +0 -2
- package/Server/Middleware/SlackAuthorization.ts +96 -18
- package/Server/Utils/Telemetry/LlmMetricSpend.ts +56 -5
- package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
- package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
- package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
- package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
- package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
- package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
- package/Tests/Server/API/BaseAPI.test.ts +41 -0
- package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +37 -18
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +143 -2
- package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +391 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +298 -0
- package/Types/Telemetry/LlmConventions.ts +255 -0
- package/Types/Telemetry/LlmMetricConventions.ts +212 -7
- package/Utils/Telemetry/LlmMetricQuery.ts +83 -0
- package/build/dist/Models/AnalyticsModels/Span.js +89 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +3 -19
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +0 -2
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +52 -3
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +197 -5
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js +57 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import LlmSpanDisplayUtil, {
|
|
2
|
+
LlmSpanDisplay,
|
|
3
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Utils/LlmSpanDisplay";
|
|
4
|
+
import LlmSpanUtil, {
|
|
5
|
+
LlmSpanFields,
|
|
6
|
+
} from "../../../Server/Utils/Telemetry/LlmSpan";
|
|
7
|
+
import { AttributeType } from "../../../Server/Utils/Telemetry/Telemetry";
|
|
8
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
9
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
10
|
+
import {
|
|
11
|
+
LlmEndUserAttributeKeys,
|
|
12
|
+
LlmTeamAttributeKeys,
|
|
13
|
+
LlmUserEmailAttributeKeys,
|
|
14
|
+
LlmUserIdAttributeKeys,
|
|
15
|
+
} from "../../../Types/Telemetry/LlmConventions";
|
|
16
|
+
import { describe, expect, test } from "@jest/globals";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* ---------------------------------------------------------------------------
|
|
20
|
+
* The client half of the LLM convention pair
|
|
21
|
+
* ---------------------------------------------------------------------------
|
|
22
|
+
*
|
|
23
|
+
* LlmSpanDisplay.ts and Common/Server/Utils/Telemetry/LlmSpan.ts read the SAME
|
|
24
|
+
* key lists out of Common/Types/Telemetry/LlmConventions.ts, and that shared
|
|
25
|
+
* import is the only thing keeping the span panel and the denormalized
|
|
26
|
+
* columns naming the same person for the same span. Nothing about that
|
|
27
|
+
* arrangement is enforced by the type system: dropping a list from the
|
|
28
|
+
* client's import, or re-declaring a key inline, compiles perfectly and shows
|
|
29
|
+
* a blank "User" on exactly the emitters whose rows the table can filter.
|
|
30
|
+
*
|
|
31
|
+
* So the highest-value test in this file is the drift guard at the bottom,
|
|
32
|
+
* which walks every recognized key and asserts the two parsers agree on it.
|
|
33
|
+
* The rest pin the behaviours a reader of the panel depends on: preference
|
|
34
|
+
* order, the employee / downstream-customer separation, and the empty cases.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// A span whose attributes reach the client as a plain JSON map.
|
|
38
|
+
type Attributes = JSONObject;
|
|
39
|
+
|
|
40
|
+
// The server extractor's flattened attribute dictionary.
|
|
41
|
+
type ServerAttributes = Dictionary<AttributeType | Array<AttributeType>>;
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* Every case below carries an LLM marker. Both parsers gate identity on the
|
|
45
|
+
* span being an LLM span at all, so identity keys on their own would prove
|
|
46
|
+
* nothing about the key lists — see the gating test further down, which pins
|
|
47
|
+
* that gate on purpose.
|
|
48
|
+
*/
|
|
49
|
+
const LLM_MARKER: Attributes = { "gen_ai.system": "openai" };
|
|
50
|
+
|
|
51
|
+
type ParseFunction = (attributes: Attributes) => LlmSpanDisplay;
|
|
52
|
+
|
|
53
|
+
const parse: ParseFunction = (attributes: Attributes): LlmSpanDisplay => {
|
|
54
|
+
return LlmSpanDisplayUtil.parse({ attributes: attributes });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type WithMarkerFunction = (attributes: Attributes) => Attributes;
|
|
58
|
+
|
|
59
|
+
const withMarker: WithMarkerFunction = (attributes: Attributes): Attributes => {
|
|
60
|
+
return { ...LLM_MARKER, ...attributes };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("LlmSpanDisplayUtil.parse — employee identity keys", () => {
|
|
64
|
+
/*
|
|
65
|
+
* Each list is ordered preferred-first and the parser returns the first key
|
|
66
|
+
* present. A key that is listed but never read is worse than one that is
|
|
67
|
+
* missing: the list is the documentation, and both the ingest extractor and
|
|
68
|
+
* this parser are read as the answer to "does OneUptime understand my
|
|
69
|
+
* instrumentation?".
|
|
70
|
+
*/
|
|
71
|
+
test.each(LlmUserIdAttributeKeys)(
|
|
72
|
+
"user id key %s is recognized on its own",
|
|
73
|
+
(key: string) => {
|
|
74
|
+
expect(parse(withMarker({ [key]: "employee-42" })).userId).toBe(
|
|
75
|
+
"employee-42",
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
test.each(LlmUserEmailAttributeKeys)(
|
|
81
|
+
"user email key %s is recognized on its own",
|
|
82
|
+
(key: string) => {
|
|
83
|
+
expect(parse(withMarker({ [key]: "ada@example.com" })).userEmail).toBe(
|
|
84
|
+
"ada@example.com",
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
test.each(LlmTeamAttributeKeys)(
|
|
90
|
+
"team key %s is recognized on its own",
|
|
91
|
+
(key: string) => {
|
|
92
|
+
expect(parse(withMarker({ [key]: "platform" })).team).toBe("platform");
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
test("the whole preference order is honoured, most-preferred first", () => {
|
|
97
|
+
/*
|
|
98
|
+
* A single span carrying EVERY recognized spelling: a gateway that stamps
|
|
99
|
+
* its own metadata onto a span an SDK already annotated produces exactly
|
|
100
|
+
* this. Walking the list from the back and removing one key at a time
|
|
101
|
+
* asserts the full ordering rather than only its first element.
|
|
102
|
+
*/
|
|
103
|
+
const attributes: Attributes = withMarker({});
|
|
104
|
+
|
|
105
|
+
for (const key of LlmUserIdAttributeKeys) {
|
|
106
|
+
attributes[key] = `value-of-${key}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (
|
|
110
|
+
let index: number = 0;
|
|
111
|
+
index < LlmUserIdAttributeKeys.length;
|
|
112
|
+
index++
|
|
113
|
+
) {
|
|
114
|
+
const expectedKey: string = LlmUserIdAttributeKeys[index]!;
|
|
115
|
+
|
|
116
|
+
expect(parse(attributes).userId).toBe(`value-of-${expectedKey}`);
|
|
117
|
+
|
|
118
|
+
delete attributes[expectedKey];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Every key removed: nothing is left to resolve.
|
|
122
|
+
expect(parse(attributes).userId).toBe("");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("user.id and user.email are read from nested attribute objects", () => {
|
|
126
|
+
/*
|
|
127
|
+
* The API returns attributes as they were stored, and OTLP producers that
|
|
128
|
+
* send `user: { id, email }` as a nested object arrive that way. The
|
|
129
|
+
* server extractor sees an already-flattened dictionary; the client has
|
|
130
|
+
* to flatten first, so this path is client-only and would otherwise be
|
|
131
|
+
* untested.
|
|
132
|
+
*/
|
|
133
|
+
const display: LlmSpanDisplay = parse(
|
|
134
|
+
withMarker({
|
|
135
|
+
user: { id: "acct-9f2", email: "grace@example.com" },
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(display.userId).toBe("acct-9f2");
|
|
140
|
+
expect(display.userEmail).toBe("grace@example.com");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("LlmSpanDisplayUtil.parse — employee vs downstream customer", () => {
|
|
145
|
+
/*
|
|
146
|
+
* The load-bearing separation. gen_ai.user / llm.user carry OpenAI's `user`
|
|
147
|
+
* REQUEST parameter and litellm.metadata.user_api_key_end_user_id is
|
|
148
|
+
* LiteLLM's end-user id: on a SaaS product's spans all three name the
|
|
149
|
+
* product's OWN CUSTOMER. Reading one of them as the employee would
|
|
150
|
+
* manufacture a phantom employee per customer and leave the engineer who
|
|
151
|
+
* actually owns the spend looking like they spent nothing.
|
|
152
|
+
*/
|
|
153
|
+
test.each(LlmEndUserAttributeKeys)(
|
|
154
|
+
"end-user key %s yields no employee",
|
|
155
|
+
(key: string) => {
|
|
156
|
+
const display: LlmSpanDisplay = parse(
|
|
157
|
+
withMarker({ [key]: "customer-1234" }),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(display.userId).toBe("");
|
|
161
|
+
expect(display.userEmail).toBe("");
|
|
162
|
+
expect(display.endUser).toBe("customer-1234");
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
test("a span carrying ONLY end-user keys has no employee at all", () => {
|
|
167
|
+
const attributes: Attributes = withMarker({});
|
|
168
|
+
|
|
169
|
+
for (const key of LlmEndUserAttributeKeys) {
|
|
170
|
+
attributes[key] = "customer-1234";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const display: LlmSpanDisplay = parse(attributes);
|
|
174
|
+
|
|
175
|
+
expect(display.userId).toBe("");
|
|
176
|
+
expect(display.userEmail).toBe("");
|
|
177
|
+
expect(display.team).toBe("");
|
|
178
|
+
expect(display.endUser).toBe("customer-1234");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("employee and end user are both surfaced, separately, on one span", () => {
|
|
182
|
+
// The realistic LiteLLM shape: key owner is staff, end user is a customer.
|
|
183
|
+
const display: LlmSpanDisplay = parse(
|
|
184
|
+
withMarker({
|
|
185
|
+
"litellm.metadata.user_api_key_user_id": "employee-7",
|
|
186
|
+
"litellm.metadata.user_api_key_end_user_id": "customer-1234",
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(display.userId).toBe("employee-7");
|
|
191
|
+
expect(display.endUser).toBe("customer-1234");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("no end-user key is also an employee key", () => {
|
|
195
|
+
/*
|
|
196
|
+
* The exclusion pinned as a set relation rather than through parse(), so
|
|
197
|
+
* appending an end-user spelling to an employee list fails here even if
|
|
198
|
+
* the parser's own behaviour is unchanged.
|
|
199
|
+
*/
|
|
200
|
+
for (const key of LlmEndUserAttributeKeys) {
|
|
201
|
+
expect(LlmUserIdAttributeKeys).not.toContain(key);
|
|
202
|
+
expect(LlmUserEmailAttributeKeys).not.toContain(key);
|
|
203
|
+
expect(LlmTeamAttributeKeys).not.toContain(key);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("LlmSpanDisplayUtil.parse — absent and empty identity", () => {
|
|
209
|
+
test("an LLM span with no identity attributes reports none", () => {
|
|
210
|
+
const display: LlmSpanDisplay = parse(
|
|
211
|
+
withMarker({ "gen_ai.request.model": "gpt-4o" }),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(display.isLlmSpan).toBe(true);
|
|
215
|
+
expect(display.userId).toBe("");
|
|
216
|
+
expect(display.userEmail).toBe("");
|
|
217
|
+
expect(display.team).toBe("");
|
|
218
|
+
expect(display.endUser).toBe("");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("whitespace-only values are treated as absent, not as a person", () => {
|
|
222
|
+
/*
|
|
223
|
+
* An emitter that sets the attribute from an unset environment variable
|
|
224
|
+
* sends "" or " ". Rendering that as the employee would produce a blank
|
|
225
|
+
* chip and a blank table cell that read as a real, nameless person.
|
|
226
|
+
*/
|
|
227
|
+
const display: LlmSpanDisplay = parse(
|
|
228
|
+
withMarker({
|
|
229
|
+
"user.id": " ",
|
|
230
|
+
"user.email": "\t\n",
|
|
231
|
+
"team.id": " ",
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(display.userId).toBe("");
|
|
236
|
+
expect(display.userEmail).toBe("");
|
|
237
|
+
expect(display.team).toBe("");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a whitespace-only preferred key falls through to the next one", () => {
|
|
241
|
+
const display: LlmSpanDisplay = parse(
|
|
242
|
+
withMarker({ "user.id": " ", "enduser.id": "employee-42" }),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(display.userId).toBe("employee-42");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("values are trimmed before display", () => {
|
|
249
|
+
expect(
|
|
250
|
+
parse(withMarker({ "user.email": " ada@example.com " })).userEmail,
|
|
251
|
+
).toBe("ada@example.com");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("identity alone does not make a span an LLM span", () => {
|
|
255
|
+
/*
|
|
256
|
+
* user.id / user.email / team.id are GENERIC OTel general-semconv keys
|
|
257
|
+
* that RUM browser spans and ordinary HTTP spans carry. If they counted
|
|
258
|
+
* as LLM evidence, the AI / LLM panel would open on a plain page-load
|
|
259
|
+
* span; and because the ingest extractor gates the llmUser* columns the
|
|
260
|
+
* same way, the panel would be claiming an employee the stored row does
|
|
261
|
+
* not have.
|
|
262
|
+
*/
|
|
263
|
+
const display: LlmSpanDisplay = parse({
|
|
264
|
+
"http.method": "GET",
|
|
265
|
+
"user.id": "acct-9f2",
|
|
266
|
+
"user.email": "ada@example.com",
|
|
267
|
+
"team.id": "platform",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(display.isLlmSpan).toBe(false);
|
|
271
|
+
expect(display.userId).toBe("");
|
|
272
|
+
expect(display.userEmail).toBe("");
|
|
273
|
+
expect(display.team).toBe("");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("LlmSpanDisplay and the ingest extractor cannot drift", () => {
|
|
278
|
+
/*
|
|
279
|
+
* The guard this file exists for.
|
|
280
|
+
*
|
|
281
|
+
* The span panel parses attributes in the browser; the LLM calls table
|
|
282
|
+
* filters and sorts the columns the ingest extractor wrote. Both are
|
|
283
|
+
* supposed to be reading the identical key lists. Every way of breaking
|
|
284
|
+
* that — dropping a list from one side's import, re-declaring the strings
|
|
285
|
+
* inline, adding a key to only one parser — type-checks and renders, and
|
|
286
|
+
* shows up in production as a panel and a table row that disagree about who
|
|
287
|
+
* made a call.
|
|
288
|
+
*
|
|
289
|
+
* So each case below feeds ONE recognized key to BOTH parsers and requires
|
|
290
|
+
* the same answer. Adding a key to LlmConventions.ts extends this suite for
|
|
291
|
+
* free; wiring it into only one parser fails it.
|
|
292
|
+
*/
|
|
293
|
+
type ExtractFunction = (attributes: Attributes) => LlmSpanFields;
|
|
294
|
+
|
|
295
|
+
const extract: ExtractFunction = (attributes: Attributes): LlmSpanFields => {
|
|
296
|
+
return LlmSpanUtil.extract(attributes as ServerAttributes);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
test.each(LlmUserIdAttributeKeys)(
|
|
300
|
+
"client and server resolve the same employee id from %s",
|
|
301
|
+
(key: string) => {
|
|
302
|
+
const attributes: Attributes = withMarker({ [key]: "employee-42" });
|
|
303
|
+
|
|
304
|
+
expect(parse(attributes).userId).toBe(extract(attributes).llmUserId);
|
|
305
|
+
expect(parse(attributes).userId).toBe("employee-42");
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
test.each(LlmUserEmailAttributeKeys)(
|
|
310
|
+
"client and server resolve the same employee email from %s",
|
|
311
|
+
(key: string) => {
|
|
312
|
+
const attributes: Attributes = withMarker({ [key]: "ada@example.com" });
|
|
313
|
+
|
|
314
|
+
expect(parse(attributes).userEmail).toBe(
|
|
315
|
+
extract(attributes).llmUserEmail,
|
|
316
|
+
);
|
|
317
|
+
expect(parse(attributes).userEmail).toBe("ada@example.com");
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
test.each(LlmTeamAttributeKeys)(
|
|
322
|
+
"client and server resolve the same team from %s",
|
|
323
|
+
(key: string) => {
|
|
324
|
+
const attributes: Attributes = withMarker({ [key]: "platform" });
|
|
325
|
+
|
|
326
|
+
expect(parse(attributes).team).toBe(extract(attributes).llmTeam);
|
|
327
|
+
expect(parse(attributes).team).toBe("platform");
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
test.each(LlmEndUserAttributeKeys)(
|
|
332
|
+
"neither client nor server reads %s as the employee",
|
|
333
|
+
(key: string) => {
|
|
334
|
+
const attributes: Attributes = withMarker({ [key]: "customer-1234" });
|
|
335
|
+
|
|
336
|
+
const display: LlmSpanDisplay = parse(attributes);
|
|
337
|
+
const fields: LlmSpanFields = extract(attributes);
|
|
338
|
+
|
|
339
|
+
expect(display.userId).toBe("");
|
|
340
|
+
expect(display.userEmail).toBe("");
|
|
341
|
+
expect(fields.llmUserId).toBe("");
|
|
342
|
+
expect(fields.llmUserEmail).toBe("");
|
|
343
|
+
|
|
344
|
+
/*
|
|
345
|
+
* The client alone surfaces the end user, under its own label. That
|
|
346
|
+
* asymmetry is deliberate — the value has no column — so it is asserted
|
|
347
|
+
* rather than left to look like an oversight.
|
|
348
|
+
*/
|
|
349
|
+
expect(display.endUser).toBe("customer-1234");
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
test("both parsers agree on a fully-populated coding-agent span", () => {
|
|
354
|
+
// The shape Claude Code / Codex / Gemini CLI actually export.
|
|
355
|
+
const attributes: Attributes = withMarker({
|
|
356
|
+
"gen_ai.request.model": "claude-opus-4",
|
|
357
|
+
"user.id": "acct-9f2",
|
|
358
|
+
"user.email": "ada@example.com",
|
|
359
|
+
"team.id": "platform",
|
|
360
|
+
cost_center: "RD-114",
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const display: LlmSpanDisplay = parse(attributes);
|
|
364
|
+
const fields: LlmSpanFields = extract(attributes);
|
|
365
|
+
|
|
366
|
+
expect(display.userId).toBe(fields.llmUserId);
|
|
367
|
+
expect(display.userEmail).toBe(fields.llmUserEmail);
|
|
368
|
+
expect(display.team).toBe(fields.llmTeam);
|
|
369
|
+
|
|
370
|
+
// team.id outranks cost_center in the shared list, on both sides.
|
|
371
|
+
expect(display.team).toBe("platform");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("both parsers gate identity on the span being an LLM span", () => {
|
|
375
|
+
const attributes: Attributes = {
|
|
376
|
+
"http.method": "GET",
|
|
377
|
+
"user.id": "acct-9f2",
|
|
378
|
+
"user.email": "ada@example.com",
|
|
379
|
+
"team.id": "platform",
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const display: LlmSpanDisplay = parse(attributes);
|
|
383
|
+
const fields: LlmSpanFields = extract(attributes);
|
|
384
|
+
|
|
385
|
+
expect(display.isLlmSpan).toBe(fields.isLlmSpan);
|
|
386
|
+
expect(display.isLlmSpan).toBe(false);
|
|
387
|
+
expect(display.userId).toBe(fields.llmUserId);
|
|
388
|
+
expect(display.userEmail).toBe(fields.llmUserEmail);
|
|
389
|
+
expect(display.team).toBe(fields.llmTeam);
|
|
390
|
+
});
|
|
391
|
+
});
|