diff --git a/apps/webapp/app/utils/taskEvent.ts b/apps/webapp/app/utils/taskEvent.ts index 5fa2713e16..e6e643d134 100644 --- a/apps/webapp/app/utils/taskEvent.ts +++ b/apps/webapp/app/utils/taskEvent.ts @@ -5,6 +5,7 @@ import { isExceptionSpanEvent, millisecondsToNanoseconds, NULL_SENTINEL, + EMPTY_ARRAY_SENTINEL, SemanticInternalAttributes, SpanEvent, SpanEvents, @@ -446,6 +447,10 @@ export function rehydrateJson(json: Prisma.JsonValue): any { return null; } + if (json === EMPTY_ARRAY_SENTINEL) { + return []; + } + if (typeof json === "string") { return json; } diff --git a/apps/webapp/app/v3/eventRepository.server.ts b/apps/webapp/app/v3/eventRepository.server.ts index 990c6127ef..13761c7d82 100644 --- a/apps/webapp/app/v3/eventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository.server.ts @@ -5,6 +5,7 @@ import { ExceptionEventProperties, ExceptionSpanEvent, NULL_SENTINEL, + EMPTY_ARRAY_SENTINEL, PRIMARY_VARIANT, SemanticInternalAttributes, SpanEvent, @@ -1593,6 +1594,10 @@ function rehydrateJson(json: Prisma.JsonValue): any { return null; } + if (json === EMPTY_ARRAY_SENTINEL) { + return []; + } + if (typeof json === "string") { return json; } diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts index 7706842c14..72b8c8ed40 100644 --- a/packages/core/src/v3/index.ts +++ b/packages/core/src/v3/index.ts @@ -44,6 +44,7 @@ export { primitiveValueOrflattenedAttributes, unflattenAttributes, NULL_SENTINEL, + EMPTY_ARRAY_SENTINEL, } from "./utils/flattenAttributes.js"; export { omit } from "./utils/omit.js"; export { diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index e2791f21ac..450a298336 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -1,6 +1,8 @@ import { Attributes } from "@opentelemetry/api"; +import { debug } from "node:util"; export const NULL_SENTINEL = "$@null(("; +export const EMPTY_ARRAY_SENTINEL = "$@empty_array(("; export const CIRCULAR_REFERENCE_SENTINEL = "$@circular(("; export function flattenAttributes( @@ -20,6 +22,11 @@ export function flattenAttributes( return result; } + if (Array.isArray(obj) && obj.length === 0) { + result[prefix || ""] = EMPTY_ARRAY_SENTINEL; + return result; + } + if (typeof obj === "string") { result[prefix || ""] = obj; return result; @@ -66,6 +73,10 @@ export function flattenAttributes( } } } + + if (!value.length) { + result[newPrefix] = EMPTY_ARRAY_SENTINEL; + } } else if (isRecord(value)) { // update null check here Object.assign(result, flattenAttributes(value, newPrefix, seen)); @@ -98,7 +109,7 @@ export function unflattenAttributes( Object.keys(obj).length === 1 && Object.keys(obj)[0] === "" ) { - return rehydrateNull(obj[""]) as any; + return rehydrateEmptyValues(obj[""]) as any; } if (Object.keys(obj).length === 0) { @@ -150,7 +161,7 @@ export function unflattenAttributes( const lastPart = parts[parts.length - 1]; if (lastPart !== undefined) { - current[lastPart] = rehydrateNull(rehydrateCircular(value)); + current[lastPart] = rehydrateEmptyValues(rehydrateCircular(value)); } } @@ -201,10 +212,14 @@ export function primitiveValueOrflattenedAttributes( return attributes; } -function rehydrateNull(value: any): any { +function rehydrateEmptyValues(value: any): any { if (value === NULL_SENTINEL) { return null; } + if (value === EMPTY_ARRAY_SENTINEL) { + return []; + } + return value; } diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 4b00995163..f7b004cee1 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -61,6 +61,22 @@ describe("flattenAttributes", () => { expect(unflattenAttributes(result)).toEqual(input); }); + it("flattens empty array attributes correctly", () => { + const input: number[] = []; + + const result = flattenAttributes(input); + expect(result).toEqual({ "": "$@empty_array((" }); + expect(unflattenAttributes(result)).toEqual(input); + }); + + it("flattens empty array child attributes correctly", () => { + const input: number[] = []; + + const result = flattenAttributes({ input }); + expect(result).toEqual({ input: "$@empty_array((" }); + expect(unflattenAttributes(result)).toEqual({ input }); + }); + it("flattens complex objects correctly", () => { const obj = { level1: { @@ -157,13 +173,13 @@ describe("flattenAttributes", () => { expect(flattenAttributes(obj, "retry.byStatus")).toEqual(expected); }); - it("handles circular references correctly", () => { + 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", + name: "Alice", "blogPosts.[0].title": "Post 1", "blogPosts.[0].author": "$@circular((", }); @@ -175,7 +191,7 @@ describe("flattenAttributes", () => { const result = flattenAttributes(user); expect(result).toEqual({ - "name": "Bob", + name: "Bob", "friends.[0]": "$@circular((", }); }); @@ -246,10 +262,24 @@ describe("unflattenAttributes", () => { }; expect(unflattenAttributes(flattened)).toEqual(expected); }); - + + it("correctly reconstructs empty arrays", () => { + const flattened = { + "": "$@empty_array((", + array1: "$@empty_array((", + "array2.[0]": "$@empty_array((", + }; + const expected = { + "": [], + array1: [], + array2: [[]], + }; + expect(unflattenAttributes(flattened)).toEqual(expected); + }); + it("rehydrates circular references correctly", () => { const flattened = { - "name": "Alice", + name: "Alice", "blogPosts.[0].title": "Post 1", "blogPosts.[0].author": "$@circular((", };