Skip to content

Commit d92865c

Browse files
committed
fix(json): handle dotted attribute names as literal keys in filterFields
getNestedValue now checks for the path as a literal property name before falling back to dot-separated nested traversal. This fixes --fields gen_ai.usage.input_tokens on span list which stores custom attributes as flat dotted keys, not nested objects.
1 parent 054af1f commit d92865c

File tree

1 file changed

+33
-4
lines changed

1 file changed

+33
-4
lines changed

src/lib/formatters/json.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,32 @@ import type { Writer } from "../../types/index.js";
1010
/**
1111
* Get a nested value from an object using a dot-notated path.
1212
*
13+
* First checks for the path as a literal property name (e.g.,
14+
* `"gen_ai.usage.input_tokens"` as a flat key), then falls back to
15+
* dot-separated nested traversal (e.g., `"contexts.trace.traceId"`).
16+
* This ensures custom span attributes with dotted names are found.
17+
*
1318
* Returns `{ found: true, value }` when the path resolves, even if the
1419
* leaf value is `undefined` or `null`. Returns `{ found: false }` when
1520
* any intermediate segment is not an object (or is missing).
1621
*
1722
* @param obj - Source object to traverse
18-
* @param path - Dot-separated key path (e.g. `"contexts.trace.traceId"`)
23+
* @param path - Key path: literal property name or dot-separated nesting
1924
*/
2025
function getNestedValue(
2126
obj: unknown,
2227
path: string
2328
): { found: true; value: unknown } | { found: false } {
29+
if (obj === null || obj === undefined || typeof obj !== "object") {
30+
return { found: false };
31+
}
32+
33+
// Fast path: literal property name (handles dotted keys like "gen_ai.usage.input_tokens")
34+
if (Object.hasOwn(obj, path)) {
35+
return { found: true, value: (obj as Record<string, unknown>)[path] };
36+
}
37+
38+
// Fall back to dot-separated nested traversal
2439
let current: unknown = obj;
2540
for (const segment of path.split(".")) {
2641
if (
@@ -41,15 +56,25 @@ function getNestedValue(
4156
/**
4257
* Set a nested value in an object, creating intermediate objects as needed.
4358
*
59+
* When `literalKey` is true, the path is used as a literal property name
60+
* (no dot splitting). This preserves dotted attribute names like
61+
* `"gen_ai.usage.input_tokens"` as flat keys in the output.
62+
*
4463
* @param target - Target object to write into (mutated in place)
45-
* @param path - Dot-separated key path
64+
* @param path - Key path: literal name or dot-separated nesting
4665
* @param value - Value to set at the leaf
66+
* @param literalKey - When true, skip dot splitting
4767
*/
4868
function setNestedValue(
4969
target: Record<string, unknown>,
5070
path: string,
51-
value: unknown
71+
value: unknown,
72+
literalKey = false
5273
): void {
74+
if (literalKey) {
75+
target[path] = value;
76+
return;
77+
}
5378
const segments = path.split(".");
5479
let current: Record<string, unknown> = target;
5580
for (let i = 0; i < segments.length - 1; i++) {
@@ -104,7 +129,11 @@ export function filterFields<T>(data: T, fields: string[]): Partial<T> {
104129
for (const field of fields) {
105130
const lookup = getNestedValue(data, field);
106131
if (lookup.found) {
107-
setNestedValue(result, field, lookup.value);
132+
// Use literal key when the field name exists as a direct property
133+
// (e.g., "gen_ai.usage.input_tokens" as a flat key)
134+
const isLiteral =
135+
typeof data === "object" && data !== null && Object.hasOwn(data, field);
136+
setNestedValue(result, field, lookup.value, isLiteral);
108137
}
109138
}
110139

0 commit comments

Comments
 (0)