Skip to content

Commit 9e53829

Browse files
committed
Improve the display of non-object return types in the run trace viewer
1 parent ed03f4b commit 9e53829

File tree

10 files changed

+196
-51
lines changed

10 files changed

+196
-51
lines changed

.changeset/six-ligers-exist.md

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+
Improve the display of non-object return types in the run trace viewer

apps/webapp/app/models/message.server.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { json, Session } from "@remix-run/node";
2-
import { redirect } from "remix-typedjson";
1+
import { json, redirect, Session } from "@remix-run/node";
32
import { createCookieSessionStorage } from "@remix-run/node";
43
import { env } from "~/env.server";
54

apps/webapp/app/presenters/v3/SpanPresenter.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class SpanPresenter {
4242
const output =
4343
span.outputType === "application/store"
4444
? `/resources/packets/${span.environmentId}/${span.output}`
45-
: typeof span.output !== "undefined" && span.output !== null
45+
: typeof span.output !== "undefined"
4646
? await prettyPrintPacket(span.output, span.outputType ?? undefined)
4747
: undefined;
4848

apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export const action: ActionFunction = async ({ request, params }) => {
6363
{ spanId: newRun.spanId }
6464
);
6565

66+
logger.debug("Replayed run", {
67+
taskRunId: taskRun.id,
68+
taskRunFriendlyId: taskRun.friendlyId,
69+
newRunId: newRun.id,
70+
newRunFriendlyId: newRun.friendlyId,
71+
runPath,
72+
});
73+
6674
return redirectWithSuccessMessage(runPath, request, `Replaying run`);
6775
} catch (error) {
6876
if (error instanceof Error) {

apps/webapp/app/v3/eventRepository.server.ts

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
correctErrorStackTrace,
1414
createPacketAttributesAsJson,
1515
flattenAttributes,
16+
NULL_SENTINEL,
1617
isExceptionSpanEvent,
1718
omit,
1819
unflattenAttributes,
@@ -438,21 +439,10 @@ export class EventRepository {
438439
return;
439440
}
440441

441-
const output = isEmptyJson(fullEvent.output)
442-
? null
443-
: unflattenAttributes(fullEvent.output as Attributes);
442+
const output = rehydrateJson(fullEvent.output);
443+
const payload = rehydrateJson(fullEvent.payload);
444444

445-
const payload = isEmptyJson(fullEvent.payload)
446-
? null
447-
: unflattenAttributes(fullEvent.payload as Attributes);
448-
449-
const show = unflattenAttributes(
450-
filteredAttributes(fullEvent.properties as Attributes, SemanticInternalAttributes.SHOW)
451-
)[SemanticInternalAttributes.SHOW] as
452-
| {
453-
actions?: boolean;
454-
}
455-
| undefined;
445+
const show = rehydrateShow(fullEvent.properties);
456446

457447
const properties = sanitizedAttributes(fullEvent.properties);
458448

@@ -1046,7 +1036,7 @@ function isEmptyJson(json: Prisma.JsonValue) {
10461036
return false;
10471037
}
10481038

1049-
function sanitizedAttributes(json: Prisma.JsonValue): Record<string, unknown> | undefined {
1039+
function sanitizedAttributes(json: Prisma.JsonValue) {
10501040
if (json === null || json === undefined) {
10511041
return;
10521042
}
@@ -1143,3 +1133,57 @@ function getNowInNanoseconds(): bigint {
11431133
function getDateFromNanoseconds(nanoseconds: bigint) {
11441134
return new Date(Number(nanoseconds) / 1_000_000);
11451135
}
1136+
1137+
function rehydrateJson(json: Prisma.JsonValue): any {
1138+
if (json === null) {
1139+
return undefined;
1140+
}
1141+
1142+
if (json === NULL_SENTINEL) {
1143+
return null;
1144+
}
1145+
1146+
if (typeof json === "string") {
1147+
return json;
1148+
}
1149+
1150+
if (typeof json === "number") {
1151+
return json;
1152+
}
1153+
1154+
if (typeof json === "boolean") {
1155+
return json;
1156+
}
1157+
1158+
if (Array.isArray(json)) {
1159+
return json.map((item) => rehydrateJson(item));
1160+
}
1161+
1162+
if (typeof json === "object") {
1163+
return unflattenAttributes(json as Attributes);
1164+
}
1165+
1166+
return null;
1167+
}
1168+
1169+
function rehydrateShow(properties: Prisma.JsonValue): { actions?: boolean } | undefined {
1170+
if (properties === null || properties === undefined) {
1171+
return;
1172+
}
1173+
1174+
if (typeof properties !== "object") {
1175+
return;
1176+
}
1177+
1178+
if (Array.isArray(properties)) {
1179+
return;
1180+
}
1181+
1182+
const actions = properties[SemanticInternalAttributes.SHOW_ACTIONS];
1183+
1184+
if (typeof actions === "boolean") {
1185+
return { actions };
1186+
}
1187+
1188+
return;
1189+
}

packages/core/src/v3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export {
2828
flattenAttributes,
2929
primitiveValueOrflattenedAttributes,
3030
unflattenAttributes,
31+
NULL_SENTINEL,
3132
} from "./utils/flattenAttributes";
3233
export { omit } from "./utils/omit";
3334
export {

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

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { Attributes } from "@opentelemetry/api";
22

3+
export const NULL_SENTINEL = "$@null((";
4+
35
export function flattenAttributes(
46
obj: Record<string, unknown> | Array<unknown> | string | boolean | number | null | undefined,
57
prefix?: string
68
): Attributes {
79
const result: Attributes = {};
810

911
// Check if obj is null or undefined
10-
if (!obj) {
12+
if (obj === undefined) {
13+
return result;
14+
}
15+
16+
if (obj === null) {
17+
result[prefix || ""] = NULL_SENTINEL;
1118
return result;
1219
}
1320

@@ -27,14 +34,18 @@ export function flattenAttributes(
2734
}
2835

2936
for (const [key, value] of Object.entries(obj)) {
30-
const newPrefix = `${prefix ? `${prefix}.` : ""}${key}`;
37+
const newPrefix = `${prefix ? `${prefix}.` : ""}${Array.isArray(obj) ? `[${key}]` : key}`;
3138
if (Array.isArray(value)) {
3239
for (let i = 0; i < value.length; i++) {
3340
if (typeof value[i] === "object" && value[i] !== null) {
3441
// update null check here as well
3542
Object.assign(result, flattenAttributes(value[i], `${newPrefix}.[${i}]`));
3643
} else {
37-
result[`${newPrefix}.[${i}]`] = value[i];
44+
if (value[i] === null) {
45+
result[`${newPrefix}.[${i}]`] = NULL_SENTINEL;
46+
} else {
47+
result[`${newPrefix}.[${i}]`] = value[i];
48+
}
3849
}
3950
}
4051
} else if (isRecord(value)) {
@@ -43,6 +54,8 @@ export function flattenAttributes(
4354
} else {
4455
if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
4556
result[newPrefix] = value;
57+
} else if (value === null) {
58+
result[newPrefix] = NULL_SENTINEL;
4659
}
4760
}
4861
}
@@ -54,53 +67,67 @@ function isRecord(value: unknown): value is Record<string, unknown> {
5467
return value !== null && typeof value === "object" && !Array.isArray(value);
5568
}
5669

57-
export function unflattenAttributes(obj: Attributes): Record<string, unknown> {
70+
export function unflattenAttributes(
71+
obj: Attributes
72+
): Record<string, unknown> | string | number | boolean | null | undefined {
5873
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
5974
return obj;
6075
}
6176

77+
if (
78+
typeof obj === "object" &&
79+
obj !== null &&
80+
Object.keys(obj).length === 1 &&
81+
Object.keys(obj)[0] === ""
82+
) {
83+
return rehydrateNull(obj[""]) as any;
84+
}
85+
86+
if (Object.keys(obj).length === 0) {
87+
return;
88+
}
89+
6290
const result: Record<string, unknown> = {};
6391

6492
for (const [key, value] of Object.entries(obj)) {
6593
const parts = key.split(".").reduce((acc, part) => {
66-
// Splitting array indices as separate parts
67-
if (detectIsArrayIndex(part)) {
68-
acc.push(part);
94+
if (part.includes("[")) {
95+
// Handling nested array indices
96+
const subparts = part.split(/\[|\]/).filter((p) => p !== "");
97+
acc.push(...subparts);
6998
} else {
70-
acc.push(...part.split(/\.\[(.*?)\]/).filter(Boolean));
99+
acc.push(part);
71100
}
72101
return acc;
73102
}, [] as string[]);
74103

75-
let current: Record<string, unknown> = result;
104+
let current: any = result;
76105
for (let i = 0; i < parts.length - 1; i++) {
77106
const part = parts[i];
78-
const isArray = detectIsArrayIndex(part);
79-
const cleanPart = isArray ? part.substring(1, part.length - 1) : part;
80-
const nextIsArray = detectIsArrayIndex(parts[i + 1]);
81-
if (!current[cleanPart]) {
82-
current[cleanPart] = nextIsArray ? [] : {};
107+
const nextPart = parts[i + 1];
108+
const isArray = /^\d+$/.test(nextPart);
109+
if (isArray && !Array.isArray(current[part])) {
110+
current[part] = [];
111+
} else if (!isArray && current[part] === undefined) {
112+
current[part] = {};
83113
}
84-
current = current[cleanPart] as Record<string, unknown>;
114+
current = current[part];
85115
}
86116
const lastPart = parts[parts.length - 1];
87-
const cleanLastPart = detectIsArrayIndex(lastPart)
88-
? parseInt(lastPart.substring(1, lastPart.length - 1), 10)
89-
: lastPart;
90-
current[cleanLastPart] = value;
117+
current[lastPart] = rehydrateNull(value);
91118
}
92119

93-
return result;
94-
}
95-
96-
function detectIsArrayIndex(key: string): boolean {
97-
const match = key.match(/^\[(\d+)\]$/);
98-
99-
if (match) {
100-
return true;
120+
// Convert the result to an array if all top-level keys are numeric indices
121+
if (Object.keys(result).every((k) => /^\d+$/.test(k))) {
122+
const maxIndex = Math.max(...Object.keys(result).map((k) => parseInt(k)));
123+
const arrayResult = Array(maxIndex + 1);
124+
for (const key in result) {
125+
arrayResult[parseInt(key)] = result[key];
126+
}
127+
return arrayResult as any;
101128
}
102129

103-
return false;
130+
return result;
104131
}
105132

106133
export function primitiveValueOrflattenedAttributes(
@@ -129,3 +156,11 @@ export function primitiveValueOrflattenedAttributes(
129156

130157
return attributes;
131158
}
159+
160+
function rehydrateNull(value: any): any {
161+
if (value === NULL_SENTINEL) {
162+
return null;
163+
}
164+
165+
return value;
166+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,13 @@ export async function createPacketAttributes(
216216
const parsed = parse(packet.data) as any;
217217
const jsonified = JSON.parse(JSON.stringify(parsed, safeReplacer));
218218

219-
return {
219+
const result = {
220220
...flattenAttributes(jsonified, dataKey),
221221
[dataTypeKey]: "application/json",
222222
};
223-
} catch {
223+
224+
return result;
225+
} catch (e) {
224226
return;
225227
}
226228

packages/core/test/flattenAttributes.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
11
import { flattenAttributes, unflattenAttributes } from "../src/v3/utils/flattenAttributes";
22

33
describe("flattenAttributes", () => {
4-
it("handles null and undefined gracefully", () => {
5-
expect(flattenAttributes(null)).toEqual({});
6-
expect(flattenAttributes(undefined)).toEqual({});
4+
it("handles null correctly", () => {
5+
expect(flattenAttributes(null)).toEqual({ "": "$@null((" });
6+
expect(unflattenAttributes({ "": "$@null((" })).toEqual(null);
7+
8+
expect(flattenAttributes(null, "$output")).toEqual({ $output: "$@null((" });
9+
expect(flattenAttributes({ foo: null })).toEqual({ foo: "$@null((" });
10+
expect(unflattenAttributes({ foo: "$@null((" })).toEqual({ foo: null });
11+
12+
expect(flattenAttributes({ foo: [null] })).toEqual({ "foo.[0]": "$@null((" });
13+
expect(unflattenAttributes({ "foo.[0]": "$@null((" })).toEqual({ foo: [null] });
14+
15+
expect(flattenAttributes([null])).toEqual({ "[0]": "$@null((" });
16+
expect(unflattenAttributes({ "[0]": "$@null((" })).toEqual([null]);
717
});
818

919
it("flattens string attributes correctly", () => {
1020
const result = flattenAttributes("testString");
1121
expect(result).toEqual({ "": "testString" });
22+
expect(unflattenAttributes(result)).toEqual("testString");
1223
});
1324

1425
it("flattens number attributes correctly", () => {
1526
const result = flattenAttributes(12345);
1627
expect(result).toEqual({ "": 12345 });
28+
expect(unflattenAttributes(result)).toEqual(12345);
1729
});
1830

1931
it("flattens boolean attributes correctly", () => {
2032
const result = flattenAttributes(true);
2133
expect(result).toEqual({ "": true });
34+
expect(unflattenAttributes(result)).toEqual(true);
35+
});
36+
37+
it("flattens boolean attributes correctly", () => {
38+
const result = flattenAttributes(true, "$output");
39+
expect(result).toEqual({ $output: true });
40+
expect(unflattenAttributes(result)).toEqual({ $output: true });
41+
});
42+
43+
it("flattens array attributes correctly", () => {
44+
const input = [1, 2, 3];
45+
const result = flattenAttributes(input);
46+
expect(result).toEqual({ "[0]": 1, "[1]": 2, "[2]": 3 });
47+
expect(unflattenAttributes(result)).toEqual(input);
2248
});
2349

2450
it("flattens complex objects correctly", () => {

0 commit comments

Comments
 (0)