Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"verify_email_change_success_toast": "Updated your email to {{email}}",
"verify_email_change_failure_toast": "Failed to update email.",
"change_of_email": "Verify your new {{appName}} email address",
"trace_reference_id": "Trace reference ID",
"change_of_email_toast": "We have sent a verification link to {{email}}. We will update your email address once you click this link.",
"copy_somewhere_safe": "Save this API key somewhere safe. You will not be able to view it again.",
"verify_email_email_body": "Please verify your email address by clicking the button below.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@ const getError = ({
return error?.message ? (
<>
{responseVercelIdHeader ?? ""} {t(messageKey, { date, count })}
{error.data?.traceId && (
<div className="text-subtle mt-2 text-xs">
<span className="font-medium">{t("trace_reference_id")}:</span>
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t("trace_reference_id") introduces a new translation key, but no locale entry exists for it, so the UI will render the raw key text instead of a localized label. Please add the string to the locale files when adding a new t() usage.

Prompt for AI agents
Address the following comment on packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx at line 328:

<comment>`t(&quot;trace_reference_id&quot;)` introduces a new translation key, but no locale entry exists for it, so the UI will render the raw key text instead of a localized label. Please add the string to the locale files when adding a new `t()` usage.</comment>

<file context>
@@ -323,6 +323,12 @@ const getError = ({
       {responseVercelIdHeader ?? &quot;&quot;} {t(messageKey, { date, count })}
+      {error.data?.traceId &amp;&amp; (
+        &lt;div className=&quot;text-subtle mt-2 text-xs&quot;&gt;
+          &lt;span className=&quot;font-medium&quot;&gt;{t(&quot;trace_reference_id&quot;)}:&lt;/span&gt;
+          &lt;code className=&quot;ml-1 select-all break-all font-mono&quot;&gt;{error.data.traceId}&lt;/code&gt;
+        &lt;/div&gt;
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new trace reference label calls an undefined translation key, so users will see the literal key text instead of a localized string. Add the entry to the locale files to keep the UI consistent.

Prompt for AI agents
Address the following comment on packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx at line 328:

<comment>The new trace reference label calls an undefined translation key, so users will see the literal key text instead of a localized string. Add the entry to the locale files to keep the UI consistent.</comment>

<file context>
@@ -323,6 +323,12 @@ const getError = ({
       {responseVercelIdHeader ?? &quot;&quot;} {t(messageKey, { date, count })}
+      {error.data?.traceId &amp;&amp; (
+        &lt;div className=&quot;text-subtle mt-2 text-xs&quot;&gt;
+          &lt;span className=&quot;font-medium&quot;&gt;{t(&quot;trace_reference_id&quot;)}:&lt;/span&gt;
+          &lt;code className=&quot;ml-1 select-all break-all font-mono&quot;&gt;{error.data.traceId}&lt;/code&gt;
+        &lt;/div&gt;
</file context>

[internal] Confidence score: 7/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new label calls t("trace_reference_id"), but that translation key does not exist in the repo, so the UI will show the literal key instead of localized text. Define the string in the locale files or reuse an existing key before rendering it.

Prompt for AI agents
Address the following comment on packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx at line 328:

<comment>The new label calls `t(&quot;trace_reference_id&quot;)`, but that translation key does not exist in the repo, so the UI will show the literal key instead of localized text. Define the string in the locale files or reuse an existing key before rendering it.</comment>

<file context>
@@ -323,6 +323,12 @@ const getError = ({
       {responseVercelIdHeader ?? &quot;&quot;} {t(messageKey, { date, count })}
+      {error.data?.traceId &amp;&amp; (
+        &lt;div className=&quot;text-subtle mt-2 text-xs&quot;&gt;
+          &lt;span className=&quot;font-medium&quot;&gt;{t(&quot;trace_reference_id&quot;)}:&lt;/span&gt;
+          &lt;code className=&quot;ml-1 select-all break-all font-mono&quot;&gt;{error.data.traceId}&lt;/code&gt;
+        &lt;/div&gt;
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

<code className="ml-1 select-all break-all font-mono">{error.data.traceId}</code>
</div>
)}
</>
) : (
<>{t("can_you_try_again")}</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin

const error = err as Error & {
data: { rescheduleUid: string; startTime: string; attendees: string[] };
traceId?: string;
};

if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
Expand Down Expand Up @@ -521,6 +522,11 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin
: event?.data?.forwardParamsSuccessRedirect,
});
},
onError: (err, _, ctx) => {
console.error("Error creating recurring booking", err);
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- It is only called when user takes an action in embed
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
},
});

const handleBookEvent = useHandleBookEvent({
Expand Down
134 changes: 134 additions & 0 deletions packages/lib/server/getServerErrorFromUnknown.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Prisma } from "@calcom/prisma/client";
import { describe, expect, test } from "vitest";
import { ZodError } from "zod";

import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";

import { TRPCError } from "@trpc/server";

import { HttpError } from "../http-error";
import { TracedError } from "../tracing/error";
import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown";

const test400Codes = [
Expand Down Expand Up @@ -113,6 +118,135 @@ describe("getServerErrorFromUnknown", () => {
});
});

describe("TracedError handling", () => {
test("should extract traceId and tracedData from TracedError", () => {
const originalError = new Error("Original error message");
const tracedData = { userId: "123", operation: "booking" };
const traceContext = {
traceId: "trace_abc123",
spanId: "span_123",
operation: "test_operation",
};

const tracedError = new TracedError(originalError, traceContext, tracedData);

const result = getServerErrorFromUnknown(tracedError);

expect(result.statusCode).toBe(500);
expect(result.message).toBe("Original error message");
expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
});

test("should handle TracedError wrapping ErrorWithCode", () => {
const originalError = new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
const tracedData = { bookingId: "456" };
const traceContext = {
traceId: "trace_def456",
spanId: "span_456",
operation: "booking_lookup",
};

const tracedError = new TracedError(originalError, traceContext, tracedData);

const result = getServerErrorFromUnknown(tracedError);

expect(result.statusCode).toBe(404);
expect(result.message).toBe("Booking not found");
expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
});
});

describe("TRPCError handling", () => {
test("should handle TRPCError with BAD_REQUEST", () => {
const trpcError = new TRPCError({
code: "BAD_REQUEST",
message: "Invalid input data",
});

const result = getServerErrorFromUnknown(trpcError);

expect(result.statusCode).toBe(400);
expect(result.message).toBe("Invalid input data");
expect(result.data).toBeUndefined();
});

test("should handle TracedError wrapping TRPCError", () => {
const trpcError = new TRPCError({
code: "NOT_FOUND",
message: "Resource not found",
});
const tracedData = { resourceId: "789" };
const traceContext = {
traceId: "trace_trpc123",
spanId: "span_trpc123",
operation: "resource_lookup",
};

const tracedError = new TracedError(trpcError, traceContext, tracedData);

const result = getServerErrorFromUnknown(tracedError);

expect(result.statusCode).toBe(404);
expect(result.message).toBe("Resource not found");
expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
});
});

describe("ZodError handling", () => {
test("should handle ZodError with validation issues", () => {
const zodError = new ZodError([
{
code: "invalid_type",
expected: "string",
received: "number",
path: ["name"],
message: "Expected string, received number",
},
]);

const result = getServerErrorFromUnknown(zodError);

expect(result.statusCode).toBe(400);
expect(result.message).toBe("invalid_type in 'name': Expected string, received number");
expect(result.cause).toBe(zodError);
});
});

describe("Prisma error handling", () => {
test("should handle Prisma P2025 error (record not found)", () => {
const prismaError = new Error("Record to delete does not exist.") as any;
prismaError.code = "P2025";
prismaError.clientVersion = "5.0.0";
Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype);

const result = getServerErrorFromUnknown(prismaError);

expect(result.statusCode).toBe(404);
expect(result.message).toBe("Record to delete does not exist.");
expect(result.cause).toBe(prismaError);
});

test("should handle other Prisma errors as 400", () => {
const prismaError = new Error("Foreign key constraint failed") as any;
prismaError.code = "P2003";
prismaError.clientVersion = "5.0.0";
Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype);

const result = getServerErrorFromUnknown(prismaError);

expect(result.statusCode).toBe(400);
expect(result.message).toBe("Foreign key constraint failed");
});
});

test("should handle unknown type fallback", () => {
const result = getServerErrorFromUnknown({ someProperty: "value" });

expect(result.statusCode).toBe(500);
expect(result.message).toBe("Unhandled error of type 'object'. Please reach out for our customer support.");
expect(result.data).toBeUndefined();
});

test400Codes.forEach((errorCode) => {
test(`${errorCode} should return 400 Bad Request`, () => {
const error = new ErrorWithCode(errorCode, `Test message for ${errorCode}`);
Expand Down
63 changes: 57 additions & 6 deletions packages/lib/server/getServerErrorFromUnknown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import { Prisma } from "@calcom/prisma/client";

import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";

import { HttpError } from "../http-error";
import { redactError } from "../redactError";
import { stripeInvalidRequestErrorSchema } from "../stripe-error";
import { TracedError } from "../tracing/error";

function hasName(cause: unknown): cause is { name: string } {
return !!cause && typeof cause === "object" && "name" in cause;
Expand All @@ -34,37 +38,71 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
}

export function getServerErrorFromUnknown(cause: unknown): HttpError {
let traceId: string | undefined;
let tracedData: Record<string, unknown> | undefined;

if (cause instanceof TracedError) {
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unwrapping logic for TracedError is not robust enough to handle nested instances. A single if statement is used, but a while loop would be required to recursively unwrap multiple layers of TracedError, preventing the traceId from being lost and ensuring the original error is correctly processed.

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: The code unwraps a TracedError once, but there is no evidence in the repository or documentation that TracedError instances can be nested. Without a demonstrated scenario where multiple layers occur, the suggestion to switch to a while loop is speculative; it may also be an intentional design choice. Given the lack of concrete impact, this is too uncertain to report.

Prompt for AI agents
Address the following comment on packages/lib/server/getServerErrorFromUnknown.ts at line 44:

<comment>The unwrapping logic for `TracedError` is not robust enough to handle nested instances. A single `if` statement is used, but a `while` loop would be required to recursively unwrap multiple layers of `TracedError`, preventing the `traceId` from being lost and ensuring the original error is correctly processed.

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: The code unwraps a `TracedError` once, but there is no evidence in the repository or documentation that `TracedError` instances can be nested. Without a demonstrated scenario where multiple layers occur, the suggestion to switch to a `while` loop is speculative; it may also be an intentional design choice. Given the lack of concrete impact, this is too uncertain to report.</comment>

<file context>
@@ -34,37 +38,71 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
+  let traceId: string | undefined;
+  let tracedData: Record&lt;string, unknown&gt; | undefined;
+
+  if (cause instanceof TracedError) {
+    traceId = cause.traceId;
+    tracedData = cause.data;
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: System Design Agent

Fix with Cubic

traceId = cause.traceId;
tracedData = cause.data;
cause = cause.originalError;
}

if (cause instanceof TRPCError) {
const statusCode = getHTTPStatusCodeFromError(cause);
return new HttpError({
statusCode,
message: cause.message,
data: traceId ? { ...tracedData, traceId } : undefined,
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traceId augmentation spreads optional objects like tracedData/prismaError.data without defaulting to {}, so traced errors without attached data will throw before an HttpError can be returned. Wrap each spread target in ?? {} (or guard) before merging trace metadata.

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: Object spread syntax gracefully ignores undefined/null operands instead of throwing. Running this scenario in Node (console.log({ ...undefined })) produces {} without errors, so spreading tracedData, prismaError.data, etc. when they are undefined is safe. The reported TypeError cannot occur, making this claim incorrect.

Prompt for AI agents
Address the following comment on packages/lib/server/getServerErrorFromUnknown.ts at line 55:

<comment>`traceId` augmentation spreads optional objects like `tracedData`/`prismaError.data` without defaulting to `{}`, so traced errors without attached data will throw before an `HttpError` can be returned. Wrap each spread target in `?? {}` (or guard) before merging trace metadata.

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: Object spread syntax gracefully ignores `undefined`/`null` operands instead of throwing. Running this scenario in Node (`console.log({ ...undefined })`) produces `{}` without errors, so spreading `tracedData`, `prismaError.data`, etc. when they are undefined is safe. The reported TypeError cannot occur, making this claim incorrect.</comment>

<file context>
@@ -34,37 +38,71 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
+    return new HttpError({
+      statusCode,
+      message: cause.message,
+      data: traceId ? { ...tracedData, traceId } : undefined,
+    });
+  }
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Suggested change
data: traceId ? { ...tracedData, traceId } : undefined,
data: traceId ? { ...(tracedData ?? {}), traceId } : undefined,
Fix with Cubic

});
}
if (isZodError(cause)) {
return new HttpError({
statusCode: 400,
message: parseZodErrorIssues(cause.issues),
cause,
data: traceId ? { ...tracedData, traceId } : undefined,
});
}
if (cause instanceof SyntaxError) {
return new HttpError({
statusCode: 500,
message: "Unexpected error, please reach out for our customer support.",
data: traceId ? { ...tracedData, traceId } : undefined,
});
}
if (isPrismaError(cause)) {
return getServerErrorFromPrismaError(cause);
const prismaError = getServerErrorFromPrismaError(cause);
return new HttpError({
statusCode: prismaError.statusCode,
message: prismaError.message,
cause: prismaError.cause,
data: traceId ? { ...tracedData, ...prismaError.data, traceId } : prismaError.data,
});
}
const parsedStripeError = stripeInvalidRequestErrorSchema.safeParse(cause);
if (parsedStripeError.success) {
return getHttpError({ statusCode: 400, cause: parsedStripeError.data });
const stripeErrorObj = new Error(parsedStripeError.data.message || "Stripe error");
stripeErrorObj.name = parsedStripeError.data.type || "StripeInvalidRequestError";
const stripeError = getHttpError({ statusCode: 400, cause: stripeErrorObj });
return new HttpError({
statusCode: stripeError.statusCode,
message: stripeError.message,
cause: stripeError.cause,
data: traceId ? { ...tracedData, ...stripeError.data, traceId } : stripeError.data,
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to spread stripeError.data will crash runtime for traced Stripe errors because that property is undefined on the HttpError created by getHttpError.

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: Spreading stripeError.data cannot crash because JS object spread simply skips undefined sources. { ...undefined } evaluates to {}, so the trace branch remains safe even when stripeError.data is undefined. The reported runtime error cannot occur, making the violation invalid.

Prompt for AI agents
Address the following comment on packages/lib/server/getServerErrorFromUnknown.ts at line 91:

<comment>Trying to spread `stripeError.data` will crash runtime for traced Stripe errors because that property is undefined on the HttpError created by getHttpError.

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: Spreading `stripeError.data` cannot crash because JS object spread simply skips `undefined` sources. `{ ...undefined }` evaluates to `{}`, so the trace branch remains safe even when `stripeError.data` is undefined. The reported runtime error cannot occur, making the violation invalid.</comment>

<file context>
@@ -34,37 +38,71 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
+      statusCode: stripeError.statusCode,
+      message: stripeError.message,
+      cause: stripeError.cause,
+      data: traceId ? { ...tracedData, ...stripeError.data, traceId } : stripeError.data,
+    });
   }
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Suggested change
data: traceId ? { ...tracedData, ...stripeError.data, traceId } : stripeError.data,
data: traceId ? { ...(tracedData ?? {}), ...(stripeError.data ?? {}), traceId } : stripeError.data,
Fix with Cubic

});
}
if (cause instanceof ErrorWithCode) {
const statusCode = getStatusCode(cause);
return new HttpError({
statusCode,
message: cause.message ?? "",
data: cause.data,
data: traceId ? { ...cause.data, ...tracedData, traceId } : cause.data,
cause,
});
}
if (cause instanceof HttpError) {
const redactedCause = redactError(cause);
const originalData = cause.data;
return {
...redactedCause,
name: cause.name,
Expand All @@ -73,22 +111,35 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError {
url: cause.url,
statusCode: cause.statusCode,
method: cause.method,
data: traceId ? { ...originalData, ...tracedData, traceId } : originalData,
};
}
if (cause instanceof Error) {
const statusCode = getStatusCode(cause);
return getHttpError({ statusCode, cause });
const error = getHttpError({ statusCode, cause });
return new HttpError({
statusCode: error.statusCode,
message: error.message,
cause: error.cause,
data: traceId ? { ...tracedData, ...error.data, traceId } : error.data,
});
}
if (typeof cause === "string") {
// @ts-expect-error https://github.com/tc39/proposal-error-cause
const error = new Error(cause, { cause });
const httpError = getHttpError({ statusCode: 500, cause: error });
return new HttpError({
statusCode: 500,
message: cause,
statusCode: httpError.statusCode,
message: httpError.message,
cause: httpError.cause,
data: traceId ? { ...tracedData, ...httpError.data, traceId } : httpError.data,
});
}

return new HttpError({
statusCode: 500,
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
data: traceId ? { ...tracedData, traceId } : tracedData,
});
}

Expand Down
Loading