Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thirty-trainers-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---

Add a maxDepth to flatten/unflattenAttributes to prevent possible issues
4 changes: 2 additions & 2 deletions .github/workflows/changesets-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

- name: Install dependencies
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
- name: Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0

- name: Install and update lockfile
run: pnpm install --no-frozen-lockfile
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

- name: 📥 Download deps
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0

- name: 📥 Download deps
run: pnpm install --frozen-lockfile --filter trigger.dev...
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
- name: Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

# npm v11.5.1 or newer is required for OIDC support
Expand Down Expand Up @@ -154,7 +154,7 @@ jobs:
- name: Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

# npm v11.5.1 or newer is required for OIDC support
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

- name: 📥 Download deps
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests-internal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

# ..to avoid rate limits when pulling images
Expand Down Expand Up @@ -127,7 +127,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
# no cache enabled, we're not installing deps

- name: Download blob reports from GitHub Actions Artifacts
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

# ..to avoid rate limits when pulling images
Expand Down Expand Up @@ -127,7 +127,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
# no cache enabled, we're not installing deps

- name: Download blob reports from GitHub Actions Artifacts
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests-webapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
cache: "pnpm"

# ..to avoid rate limits when pulling images
Expand Down Expand Up @@ -135,7 +135,7 @@ jobs:
- name: ⎔ Setup node
uses: buildjet/setup-node@v4
with:
node-version: 20.19.0
node-version: 20.20.0
# no cache enabled, we're not installing deps

- name: Download blob reports from GitHub Actions Artifacts
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.19.0
v20.20.0
2 changes: 1 addition & 1 deletion apps/supervisor/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v22.12.0
v22.22.0
2 changes: 1 addition & 1 deletion apps/supervisor/Containerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:22-alpine@sha256:9bef0ef1e268f60627da9ba7d7605e8831d5b56ad07487d24d1aa386336d1944 AS node-22-alpine
FROM node:22.22.0-alpine@sha256:bcccf7410b77ca7447d292f616c7b0a89deff87e335fe91352ea04ce8babf50f AS node-22-alpine

WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG NODE_IMAGE=node:20.11.1-bullseye-slim@sha256:5a5a92b3a8d392691c983719dbdc65d9f30085d6dcd65376e7a32e6fe9bf4cbe
ARG NODE_IMAGE=node:20.20.0-bullseye-slim@sha256:f52726bba3d47831859be141b4a57d3f7b93323f8fddfbd8375386e2c3b72319

FROM golang:1.23-alpine AS goose_builder
RUN go install github.com/pressly/goose/v3/cmd/goose@latest
Expand Down
43 changes: 30 additions & 13 deletions packages/core/src/v3/utils/flattenAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { Attributes } from "@opentelemetry/api";
export const NULL_SENTINEL = "$@null((";
export const CIRCULAR_REFERENCE_SENTINEL = "$@circular((";

const DEFAULT_MAX_DEPTH = 128;

export function flattenAttributes(
obj: unknown,
prefix?: string,
maxAttributeCount?: number
maxAttributeCount?: number,
maxDepth: number = DEFAULT_MAX_DEPTH
): Attributes {
const flattener = new AttributeFlattener(maxAttributeCount);
flattener.doFlatten(obj, prefix);
const flattener = new AttributeFlattener(maxAttributeCount, maxDepth);
flattener.doFlatten(obj, prefix, 0);
return flattener.attributes;
}

Expand All @@ -18,7 +21,10 @@ class AttributeFlattener {
private attributeCounter: number = 0;
private result: Attributes = {};

constructor(private maxAttributeCount?: number) {}
constructor(
private maxAttributeCount?: number,
private maxDepth: number = DEFAULT_MAX_DEPTH
) {}

get attributes(): Attributes {
return this.result;
Expand All @@ -37,11 +43,16 @@ class AttributeFlattener {
return true;
}

doFlatten(obj: unknown, prefix?: string) {
doFlatten(obj: unknown, prefix?: string, depth: number = 0) {
if (!this.canAddMoreAttributes()) {
return;
}

// Check depth limit to prevent stack overflow
if (depth > this.maxDepth) {
return;
}

// Check if obj is null or undefined
if (obj === undefined) {
return;
Expand Down Expand Up @@ -94,7 +105,7 @@ class AttributeFlattener {
let index = 0;
for (const item of obj) {
if (!this.canAddMoreAttributes()) break;
this.#processValue(item, `${prefix || "set"}.[${index}]`);
this.#processValue(item, `${prefix || "set"}.[${index}]`, depth);
index++;
}
return;
Expand All @@ -106,7 +117,7 @@ class AttributeFlattener {
if (!this.canAddMoreAttributes()) break;
// Use the key directly if it's a string, otherwise convert it
const keyStr = typeof key === "string" ? key : String(key);
this.#processValue(value, `${prefix || "map"}.${keyStr}`);
this.#processValue(value, `${prefix || "map"}.${keyStr}`, depth);
}
return;
}
Expand Down Expand Up @@ -196,15 +207,15 @@ class AttributeFlattener {
if (!this.canAddMoreAttributes()) {
break;
}
this.#processValue(value[i], `${newPrefix}.[${i}]`);
this.#processValue(value[i], `${newPrefix}.[${i}]`, depth);
}
} else {
this.#processValue(value, newPrefix);
this.#processValue(value, newPrefix, depth);
}
}
}

#processValue(value: unknown, prefix: string) {
#processValue(value: unknown, prefix: string, depth: number) {
if (!this.canAddMoreAttributes()) {
return;
}
Expand All @@ -224,9 +235,9 @@ class AttributeFlattener {
return;
}

// Handle non-primitive values by recursing
// Handle non-primitive values by recursing (increment depth)
if (typeof value === "object" || typeof value === "function") {
this.doFlatten(value as any, prefix);
this.doFlatten(value as any, prefix, depth + 1);
} else {
// Convert other types to strings (bigint, symbol, etc.)
this.addAttribute(prefix, String(value));
Expand All @@ -240,7 +251,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {

export function unflattenAttributes(
obj: Attributes,
filteredKeys?: string[]
filteredKeys?: string[],
maxDepth: number = DEFAULT_MAX_DEPTH
): Record<string, unknown> | string | number | boolean | null | undefined {
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
return obj;
Expand Down Expand Up @@ -285,6 +297,11 @@ export function unflattenAttributes(
[] as (string | number)[]
);

// Skip keys that exceed max depth to prevent memory exhaustion
if (parts.length > maxDepth) {
continue;
}

let current: any = result;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
Expand Down
86 changes: 86 additions & 0 deletions packages/core/test/flattenAttributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,53 @@ describe("flattenAttributes", () => {
expect(result["bigint"]).toBe("999");
expect(typeof result["symbol"]).toBe("string");
});

it("respects maxDepth limit", () => {
const obj = {
a: {
b: {
c: {
d: {
e: {
f: "deep",
},
},
},
},
},
};

// With maxDepth of 3, should not include keys deeper than 3 levels
const result = flattenAttributes(obj, undefined, undefined, 3);

// a.b.c should exist (depth 3)
expect(result["a.b.c"]).toBeUndefined(); // c is an object, not a leaf
// a.b.c.d should not exist (would require going to depth 4)
expect(result["a.b.c.d"]).toBeUndefined();
expect(result["a.b.c.d.e.f"]).toBeUndefined();
});

it("does not crash with deeply nested objects", () => {
// Create object iteratively with 500 levels of nesting
let deepObj: any = { value: "leaf" };
for (let i = 0; i < 500; i++) {
deepObj = { n: deepObj };
}

// Should complete without stack overflow
expect(() => flattenAttributes(deepObj)).not.toThrow();
});

it("does not crash with deeply nested arrays", () => {
// Create deeply nested array structure iteratively
let deepArray: any = ["leaf"];
for (let i = 0; i < 500; i++) {
deepArray = [deepArray];
}

// Should complete without stack overflow
expect(() => flattenAttributes({ arr: deepArray })).not.toThrow();
});
});

describe("unflattenAttributes", () => {
Expand Down Expand Up @@ -581,4 +628,43 @@ describe("unflattenAttributes", () => {
blogPosts: [{ title: "Post 1", author: "[Circular Reference]" }],
});
});

it("respects maxDepth limit and skips overly deep keys", () => {
// Create a flattened object with keys at various depths
const flattened = {
"a.b.c": "shallow", // depth 3 - should be included
"a.b.c.d.e.f.g": "deep", // depth 7 - should be skipped with maxDepth=5
"x.y": "also shallow", // depth 2 - should be included
};

const result = unflattenAttributes(flattened, undefined, 5);

// Shallow keys should be included
expect(result).toHaveProperty("a.b.c", "shallow");
expect(result).toHaveProperty("x.y", "also shallow");

// Deep key should be skipped (not create the nested structure)
expect((result as any)?.a?.b?.c?.d?.e?.f?.g).toBeUndefined();
});

it("uses default maxDepth of 128", () => {
// Create a key with 129 parts - should be skipped
const deepKey = Array(129).fill("x").join(".");
const flattened = {
[deepKey]: "too deep",
"a.b": "shallow",
};

const result = unflattenAttributes(flattened);

// Shallow key should work
expect(result).toHaveProperty("a.b", "shallow");

// Deep key should be skipped
let current: any = result;
for (let i = 0; i < 129 && current; i++) {
current = current?.x;
}
expect(current).toBeUndefined();
});
});
2 changes: 1 addition & 1 deletion references/prisma-7/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.19.0
20.20.0
Loading