From 21422fe027303d4ff894ffda3cb9acce471ef6a1 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:05:21 +0300 Subject: [PATCH] feat: enhance API v2 exception filters with user context for improved Axiom observability (#22157) * feat: enhance API v2 exception filters with user context for improved Axiom observability - Add extractUserContext utility to safely extract user/org/team context from request - Enhance all 5 exception filters (HTTP, TRPC, Prisma, Zod, Calendar) with user context - Include userId, userEmail, organizationId, teamId in Axiom logs when available - Improve observability for debugging and monitoring authenticated requests Co-Authored-By: morgan@cal.com * chore: update yarn.lock from lint-staged hooks Co-Authored-By: morgan@cal.com * fix: use request.organization instead of request.organizationId - Address GitHub comment feedback from ThyMinimalDev - Extract organization ID from organization object for consistency - Follows same pattern as user and team extraction Co-Authored-By: morgan@cal.com * fix: revert to using request.organizationId as requested in GitHub comment - Change back from request.organization to request.organizationId - This aligns with how auth strategies actually populate the request object - Addresses GitHub comment from ThyMinimalDev Co-Authored-By: morgan@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: morgan@cal.com Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> --- .../calendar-service-exception.filter.ts | 3 ++ .../v2/src/filters/http-exception.filter.ts | 3 ++ .../v2/src/filters/prisma-exception.filter.ts | 3 ++ .../v2/src/filters/trpc-exception.filter.ts | 3 ++ .../v2/src/filters/zod-exception.filter.ts | 3 ++ apps/api/v2/src/lib/extract-user-context.ts | 34 +++++++++++++++++++ 6 files changed, 49 insertions(+) create mode 100644 apps/api/v2/src/lib/extract-user-context.ts diff --git a/apps/api/v2/src/filters/calendar-service-exception.filter.ts b/apps/api/v2/src/filters/calendar-service-exception.filter.ts index 05868c0575..0a86486ee0 100644 --- a/apps/api/v2/src/filters/calendar-service-exception.filter.ts +++ b/apps/api/v2/src/filters/calendar-service-exception.filter.ts @@ -1,3 +1,4 @@ +import { extractUserContext } from "@/lib/extract-user-context"; import { filterReqHeaders } from "@/lib/filterReqHeaders"; import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common"; import { Request } from "express"; @@ -39,6 +40,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter { const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; response.setHeader("X-Request-Id", requestId.toString()); + const userContext = extractUserContext(request); this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, { exception, body: request.body, @@ -46,6 +48,7 @@ export class CalendarServiceExceptionFilter implements ExceptionFilter { url: request.url, method: request.method, requestId, + ...userContext, }); response.status(400).json({ diff --git a/apps/api/v2/src/filters/http-exception.filter.ts b/apps/api/v2/src/filters/http-exception.filter.ts index 904942b9f2..e2ef0afb7e 100644 --- a/apps/api/v2/src/filters/http-exception.filter.ts +++ b/apps/api/v2/src/filters/http-exception.filter.ts @@ -1,3 +1,4 @@ +import { extractUserContext } from "@/lib/extract-user-context"; import { filterReqHeaders } from "@/lib/filterReqHeaders"; import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common"; import { Request } from "express"; @@ -16,6 +17,7 @@ export class HttpExceptionFilter implements ExceptionFilter { const statusCode = exception.getStatus(); const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; response.setHeader("X-Request-Id", requestId.toString()); + const userContext = extractUserContext(request); this.logger.error(`Http Exception Filter: ${exception?.message}`, { exception, body: request.body, @@ -23,6 +25,7 @@ export class HttpExceptionFilter implements ExceptionFilter { url: request.url, method: request.method, requestId, + ...userContext, }); response.status(statusCode).json({ diff --git a/apps/api/v2/src/filters/prisma-exception.filter.ts b/apps/api/v2/src/filters/prisma-exception.filter.ts index 86bec00a98..4c77c354fd 100644 --- a/apps/api/v2/src/filters/prisma-exception.filter.ts +++ b/apps/api/v2/src/filters/prisma-exception.filter.ts @@ -1,3 +1,4 @@ +import { extractUserContext } from "@/lib/extract-user-context"; import { filterReqHeaders } from "@/lib/filterReqHeaders"; import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common"; import { Catch, HttpStatus, Logger } from "@nestjs/common"; @@ -43,6 +44,7 @@ export class PrismaExceptionFilter implements ExceptionFilter { const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; response.setHeader("X-Request-Id", requestId.toString()); + const userContext = extractUserContext(request); this.logger.error(`PrismaError: ${error.message}`, { error, body: request.body, @@ -50,6 +52,7 @@ export class PrismaExceptionFilter implements ExceptionFilter { url: request.url, method: request.method, requestId, + ...userContext, }); let message = "There was an error, please try again later."; diff --git a/apps/api/v2/src/filters/trpc-exception.filter.ts b/apps/api/v2/src/filters/trpc-exception.filter.ts index 4a77cba3b1..02bf3aab2d 100644 --- a/apps/api/v2/src/filters/trpc-exception.filter.ts +++ b/apps/api/v2/src/filters/trpc-exception.filter.ts @@ -1,3 +1,4 @@ +import { extractUserContext } from "@/lib/extract-user-context"; import { filterReqHeaders } from "@/lib/filterReqHeaders"; import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common"; import { Request } from "express"; @@ -120,6 +121,7 @@ export class TRPCExceptionFilter implements ExceptionFilter { const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; response.setHeader("X-Request-Id", requestId.toString()); + const userContext = extractUserContext(request); this.logger.error(`TRPC Exception Filter: ${exception?.message}`, { exception, body: request.body, @@ -127,6 +129,7 @@ export class TRPCExceptionFilter implements ExceptionFilter { url: request.url, method: request.method, requestId, + ...userContext, }); response.status(statusCode).json({ diff --git a/apps/api/v2/src/filters/zod-exception.filter.ts b/apps/api/v2/src/filters/zod-exception.filter.ts index 3a8bb41771..95ba3c9fe8 100644 --- a/apps/api/v2/src/filters/zod-exception.filter.ts +++ b/apps/api/v2/src/filters/zod-exception.filter.ts @@ -1,3 +1,4 @@ +import { extractUserContext } from "@/lib/extract-user-context"; import { filterReqHeaders } from "@/lib/filterReqHeaders"; import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common"; import { Catch, HttpStatus, Logger } from "@nestjs/common"; @@ -18,6 +19,7 @@ export class ZodExceptionFilter implements ExceptionFilter { const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; response.setHeader("X-Request-Id", requestId.toString()); + const userContext = extractUserContext(request); this.logger.error(`ZodError: ${error.message}`, { error, body: request.body, @@ -25,6 +27,7 @@ export class ZodExceptionFilter implements ExceptionFilter { url: request.url, method: request.method, requestId, + ...userContext, }); response.status(HttpStatus.BAD_REQUEST).json({ diff --git a/apps/api/v2/src/lib/extract-user-context.ts b/apps/api/v2/src/lib/extract-user-context.ts new file mode 100644 index 0000000000..5b2bfe8287 --- /dev/null +++ b/apps/api/v2/src/lib/extract-user-context.ts @@ -0,0 +1,34 @@ +import { Request } from "express"; + +import { Team } from "@calcom/prisma/client"; + +import { ApiAuthGuardUser } from "../modules/auth/strategies/api-auth/api-auth.strategy"; + +export interface UserContext { + userId?: number; + userEmail?: string; + organizationId?: number; + teamId?: number; +} + +export function extractUserContext(request: Request): UserContext { + const context: UserContext = {}; + + const user = (request as any).user as ApiAuthGuardUser | undefined; + if (user) { + context.userId = user.id; + context.userEmail = user.email; + } + + const organizationId = (request as any).organizationId as number | undefined; + if (organizationId) { + context.organizationId = organizationId; + } + + const team = (request as any).team as Team | undefined; + if (team) { + context.teamId = team.id; + } + + return context; +}