Skip to content

Commit 4cdbd5b

Browse files
committed
fix(core): handle conflicting dotted paths in unflattenAttributes
When OTLP input contains both `a.b = "scalar"` and `a.b.c = "value"` in the same attribute map, the path-walk loop crashed with TypeError because `current[part]` could be a primitive/null from a prior key. Apply last-write-wins, matching AttributeFlattener.addAttribute. refs TRI-10121
1 parent 37eeaa3 commit 4cdbd5b

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Fix `TypeError` in `unflattenAttributes` when the input attribute map contains conflicting dotted key paths (e.g. both `a.b` set to a scalar and `a.b.c` set to a value). The path-walk loop now applies last-write-wins when a prior key wrote a primitive, null, or array at an intermediate slot, matching the existing precedent in `AttributeFlattener.addAttribute`. Callers no longer crash when handed malformed external attribute inputs.

packages/core/src/v3/utils/flattenAttributes.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,16 @@ export function unflattenAttributes(
312312
}
313313

314314
if (typeof nextPart === "number") {
315-
// Ensure we create an array for numeric indices
316-
current[part] = Array.isArray(current[part]) ? current[part] : [];
317-
} else if (current[part] === undefined) {
318-
// Create an object for non-numeric paths
315+
if (!Array.isArray(current[part])) {
316+
current[part] = [];
317+
}
318+
} else if (
319+
current[part] === null ||
320+
typeof current[part] !== "object" ||
321+
Array.isArray(current[part])
322+
) {
323+
// Last-write-wins when a prior key wrote a primitive, null, or array
324+
// at this slot — keeps unflatten total for conflicting OTLP inputs.
319325
current[part] = {};
320326
}
321327

packages/core/test/flattenAttributes.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,4 +667,54 @@ describe("unflattenAttributes", () => {
667667
}
668668
expect(current).toBeUndefined();
669669
});
670+
671+
// Defends against external OTLP producers that emit both a leaf value and a
672+
// nested path through the same prefix in one attribute map (e.g. AI SDK
673+
// telemetry on certain models). The flattener can't produce these, but the
674+
// unflattener used to crash with TypeError when it tried to descend into a
675+
// primitive sibling.
676+
it("does not throw when a scalar precedes a deeper path at the same prefix", () => {
677+
expect(() =>
678+
unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" })
679+
).not.toThrow();
680+
expect(unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" })).toEqual({
681+
a: { b: { c: "value" } },
682+
});
683+
});
684+
685+
it("does not throw when a deeper path precedes a scalar at the same prefix", () => {
686+
expect(() =>
687+
unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" })
688+
).not.toThrow();
689+
expect(unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" })).toEqual({
690+
a: { b: "scalar" },
691+
});
692+
});
693+
694+
it("treats an intermediate null sentinel as overwritable when a deeper path follows", () => {
695+
expect(() =>
696+
unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" })
697+
).not.toThrow();
698+
expect(unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" })).toEqual({
699+
a: { b: { c: "value" } },
700+
});
701+
});
702+
703+
it("does not throw when a scalar prefix conflicts with a numeric-index path", () => {
704+
expect(() =>
705+
unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" })
706+
).not.toThrow();
707+
expect(unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" })).toEqual({
708+
a: { b: ["indexed"] },
709+
});
710+
});
711+
712+
it("converts an existing object slot to an array when a numeric-index path follows", () => {
713+
expect(() =>
714+
unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" })
715+
).not.toThrow();
716+
expect(unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" })).toEqual({
717+
a: { b: ["indexed"] },
718+
});
719+
});
670720
});

0 commit comments

Comments
 (0)