feat: distributed tracing - 3 (#25092)

* feat: distributed tracing 2

* feat: distributed tracing 2

* refactor: feedback

* refactor: feedback

* fix: type error

* fix: trpc error

* feat: distributed tracing - 3

* chore: translation

* refactor: improvements

* fix: feedback

* chore: remove
This commit is contained in:
Udit Takkar
2025-12-04 05:51:26 +00:00
committed by GitHub
parent 741144bff2
commit 843329ccce
7 changed files with 210 additions and 24 deletions
@@ -32,6 +32,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.",
@@ -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>
<code className="ml-1 select-all break-all font-mono">{error.data.traceId}</code>
</div>
)}
</>
) : (
<>{t("can_you_try_again")}</>
@@ -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) {
@@ -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({
@@ -580,7 +580,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "email_verification_required",
data: { traceId: traceContext.traceId },
});
}
@@ -590,7 +589,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "invalid_verification_code",
data: { traceId: traceContext.traceId },
});
}
}
@@ -608,14 +606,12 @@ async function handler(
throw new HttpError({
statusCode: 404,
message: "event_type_not_found",
data: { traceId: traceContext.traceId },
});
if (eventType.seatsPerTimeSlot && eventType.recurringEvent) {
throw new HttpError({
statusCode: 400,
message: "recurring_event_seats_error",
data: { traceId: traceContext.traceId },
});
}
@@ -757,7 +753,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "Missing routingFormResponseId",
data: { traceId: traceContext.traceId },
});
}
routingFormResponse = await deps.prismaClient.app_RoutingForms_FormResponse.findUnique({
@@ -1858,7 +1853,6 @@ async function handler(
throw new HttpError({
statusCode: 409,
message: ErrorCode.BookingConflict,
data: { traceId: traceContext.traceId },
});
}
throw err;
@@ -2105,7 +2099,6 @@ async function handler(
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking failed",
data: { traceId: traceContext.traceId },
};
tracingLogger.error(
@@ -2329,7 +2322,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "Missing payment credentials",
data: { traceId: traceContext.traceId },
});
}
+1 -1
View File
@@ -61,7 +61,7 @@ export function defaultResponder<T>(
const statusCode = getHTTPStatusCodeFromError(err);
error = new HttpError({ statusCode, message: err.message });
} else {
error = getServerErrorFromUnknown(err);
error = getServerErrorFromUnknown(tracedError);
}
// we don't want to report Bad Request errors to Sentry / console
if (!(error.statusCode >= 400 && error.statusCode < 500)) {
@@ -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 = [
@@ -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}`);
@@ -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;
@@ -40,61 +44,85 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
* For client-side code, use getErrorFromUnknown from @calcom/lib/errors instead.
*/
export function getServerErrorFromUnknown(cause: unknown): HttpError {
let traceId: string | undefined;
let tracedData: Record<string, unknown> | undefined;
if (cause instanceof TracedError) {
traceId = cause.traceId;
tracedData = cause.data;
cause = cause.originalError;
}
if (cause instanceof TRPCError) {
const statusCode = getHTTPStatusCodeFromError(cause);
return new HttpError({
statusCode,
message: cause.message,
cause,
data: traceId ? { ...tracedData, traceId } : undefined,
});
}
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.",
cause,
data: traceId ? { ...tracedData, traceId } : undefined,
});
}
if (isPrismaError(cause)) {
return getServerErrorFromPrismaError(cause);
return getServerErrorFromPrismaError(cause, traceId, tracedData);
}
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";
return getHttpError({ statusCode: 400, cause: stripeErrorObj, traceId, tracedData });
}
if (cause instanceof ErrorWithCode) {
const statusCode = getHttpStatusCode(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);
return {
...redactedCause,
name: cause.name,
const originalData = cause.data;
return new HttpError({
statusCode: cause.statusCode,
message: cause.message ?? "",
cause: cause.cause,
url: cause.url,
statusCode: cause.statusCode,
method: cause.method,
};
data: traceId ? { ...originalData, ...tracedData, traceId } : originalData,
});
}
if (cause instanceof Error) {
const statusCode = getHttpStatusCode(cause);
return getHttpError({ statusCode, cause });
return getHttpError({ statusCode, cause, traceId, tracedData });
}
if (typeof cause === "string") {
return new HttpError({
statusCode: 500,
message: cause,
data: traceId ? { ...tracedData, traceId } : undefined,
});
}
return new HttpError({
statusCode: 500,
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
data: traceId ? { ...tracedData, traceId } : tracedData,
});
}
@@ -152,14 +180,33 @@ export function getHttpStatusCode(cause: Error | ErrorWithCode): number {
}
}
function getHttpError<T extends Error>({ statusCode, cause }: { statusCode: number; cause: T }) {
function getHttpError<T extends Error>({
statusCode,
cause,
traceId,
tracedData,
}: {
statusCode: number;
cause: T;
traceId?: string;
tracedData?: Record<string, unknown>;
}) {
const redacted = redactError(cause);
return new HttpError({ statusCode, message: redacted.message, cause: redacted });
return new HttpError({
statusCode,
message: redacted.message,
cause: redacted,
data: traceId ? { ...tracedData, traceId } : undefined,
});
}
function getServerErrorFromPrismaError(cause: Prisma.PrismaClientKnownRequestError) {
function getServerErrorFromPrismaError(
cause: Prisma.PrismaClientKnownRequestError,
traceId?: string,
tracedData?: Record<string, unknown>
) {
if (cause.code === "P2025") {
return getHttpError({ statusCode: 404, cause });
return getHttpError({ statusCode: 404, cause, traceId, tracedData });
}
return getHttpError({ statusCode: 400, cause });
return getHttpError({ statusCode: 400, cause, traceId, tracedData });
}