diff --git a/.changeset/brown-laws-rest.md b/.changeset/brown-laws-rest.md new file mode 100644 index 0000000000..757ba89d87 --- /dev/null +++ b/.changeset/brown-laws-rest.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Fix: Handle circular references in flattenAttributes function diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 7ef0a20d12..545b0184e6 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -1,10 +1,12 @@ import { Attributes } from "@opentelemetry/api"; export const NULL_SENTINEL = "$@null(("; +export const CIRCULAR_REFERENCE_SENTINEL = "$@circular(("; export function flattenAttributes( obj: Record | Array | string | boolean | number | null | undefined, - prefix?: string + prefix?: string , + seen: WeakSet = new WeakSet() ): Attributes { const result: Attributes = {}; @@ -38,13 +40,25 @@ export function flattenAttributes( return result; } + // Check for circular reference + if (obj !== null && typeof obj === "object" && seen.has(obj)) { + result[prefix || ""] = CIRCULAR_REFERENCE_SENTINEL; + return result; + } + + // Add object to seen set + if (obj !== null && typeof obj === "object") { + seen.add(obj); + } + + for (const [key, value] of Object.entries(obj)) { const newPrefix = `${prefix ? `${prefix}.` : ""}${Array.isArray(obj) ? `[${key}]` : key}`; if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { if (typeof value[i] === "object" && value[i] !== null) { // update null check here as well - Object.assign(result, flattenAttributes(value[i], `${newPrefix}.[${i}]`)); + Object.assign(result, flattenAttributes(value[i], `${newPrefix}.[${i}]`,seen)); } else { if (value[i] === null) { result[`${newPrefix}.[${i}]`] = NULL_SENTINEL; @@ -55,7 +69,7 @@ export function flattenAttributes( } } else if (isRecord(value)) { // update null check here - Object.assign(result, flattenAttributes(value, newPrefix)); + Object.assign(result, flattenAttributes(value, newPrefix, seen)); } else { if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") { result[newPrefix] = value; @@ -135,8 +149,10 @@ export function unflattenAttributes( } const lastPart = parts[parts.length - 1]; + if (lastPart !== undefined) { - current[lastPart] = rehydrateNull(value); + current[lastPart] = rehydrateNull(rehydrateCircular(value)); + } } @@ -153,6 +169,13 @@ export function unflattenAttributes( return result; } +function rehydrateCircular(value: any): any { + if (value === CIRCULAR_REFERENCE_SENTINEL) { + return "[Circular Reference]"; + } + return value; +} + export function primitiveValueOrflattenedAttributes( obj: Record | Array | string | boolean | number | undefined, prefix: string | undefined diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 139d2311e3..4b00995163 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -156,6 +156,29 @@ describe("flattenAttributes", () => { expect(flattenAttributes(obj, "retry.byStatus")).toEqual(expected); }); + + it("handles circular references correctly", () => { + const user = { name: "Alice" }; + user["blogPosts"] = [{ title: "Post 1", author: user }]; // Circular reference + + const result = flattenAttributes(user); + expect(result).toEqual({ + "name": "Alice", + "blogPosts.[0].title": "Post 1", + "blogPosts.[0].author": "$@circular((", + }); + }); + + it("handles nested circular references correctly", () => { + const user = { name: "Bob" }; + user["friends"] = [user]; // Circular reference + + const result = flattenAttributes(user); + expect(result).toEqual({ + "name": "Bob", + "friends.[0]": "$@circular((", + }); + }); }); describe("unflattenAttributes", () => { @@ -223,4 +246,18 @@ describe("unflattenAttributes", () => { }; expect(unflattenAttributes(flattened)).toEqual(expected); }); + + it("rehydrates circular references correctly", () => { + const flattened = { + "name": "Alice", + "blogPosts.[0].title": "Post 1", + "blogPosts.[0].author": "$@circular((", + }; + + const result = unflattenAttributes(flattened); + expect(result).toEqual({ + name: "Alice", + blogPosts: [{ title: "Post 1", author: "[Circular Reference]" }], + }); + }); });