@@ -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 */
2025function 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 */
4868function 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