diff --git a/.env.example b/.env.example index b4f51e2def..a90fd8b07c 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,9 @@ NEXTAUTH_SECRET= # You can use: `openssl rand -hex 32` to generate a secure one CRON_SECRET= +# Set the minimum log level(debug, info, warn, error, fatal) +LOG_LEVEL=info + ############## # DATABASE # ############## diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 65d26fe7b7..b7dfe776df 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -46,11 +46,7 @@ jobs: - name: Run tests with coverage run: | - cd apps/web pnpm test:coverage - cd ../../ - # The Vitest coverage config is in your vite.config.mts - - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 env: diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index b16f185c9f..cf30c09ef2 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -85,6 +85,8 @@ COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src +COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules +COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist # Copy Prisma-specific generated files COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client @@ -93,14 +95,16 @@ COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_mod COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt . COPY /docker/cronjobs /app/docker/cronjobs -# Copy only @paralleldrive/cuid2 and @noble/hashes +# Copy required dependencies COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2 COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes +COPY --from=installer /app/node_modules/zod ./node_modules/zod -RUN npm install -g tsx typescript prisma +RUN npm install -g tsx typescript prisma pino-pretty EXPOSE 3000 ENV HOSTNAME "0.0.0.0" +ENV NODE_ENV="production" # USER nextjs # Prepare volume for uploads @@ -119,4 +123,4 @@ CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \ fi; \ (cd packages/database && npm run db:migrate:deploy) && \ (cd packages/database && npm run db:create-saml-database:deploy) && \ - exec node apps/web/server.js + exec node apps/web/server.js \ No newline at end of file diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts index c6f5e0c82b..fa11c26f11 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts @@ -1,13 +1,10 @@ import { getDefaultEndingCard } from "@/app/lib/templates"; import { createId } from "@paralleldrive/cuid2"; import { TFnType } from "@tolgee/react"; +import { logger } from "@formbricks/logger"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TXMTemplate } from "@formbricks/types/templates"; -function logError(error: Error, context: string) { - console.error(`Error in ${context}:`, error); -} - export const getXMSurveyDefault = (t: TFnType): TXMTemplate => { try { return { @@ -19,7 +16,7 @@ export const getXMSurveyDefault = (t: TFnType): TXMTemplate => { }, }; } catch (error) { - logError(error, "getXMSurveyDefault"); + logger.error(error, "Failed to create default XM survey template"); throw error; // Re-throw after logging } }; @@ -449,7 +446,7 @@ export const getXMTemplates = (t: TFnType): TXMTemplate[] => { enpsSurvey(t), ]; } catch (error) { - logError(error, "getXMTemplates"); + logger.error(error, "Unable to load XM templates, returning empty array"); return []; // Return an empty array or handle as needed } }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts index cbeff6da6c..9604a13f12 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable"; export const fetchTables = async (environmentId: string, baseId: string) => { @@ -17,7 +18,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch airtable config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts index dd7d6b03b4..267d4fed7a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const authorize = async (environmentId: string, apiHost: string): Promise => { const res = await fetch(`${apiHost}/api/google-sheet`, { method: "GET", @@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch google sheet config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts index 3039099837..7e9127267a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts @@ -7,6 +7,7 @@ import { surveyCache } from "@formbricks/lib/survey/cache"; import { selectSurvey } from "@formbricks/lib/survey/service"; import { transformPrismaSurvey } from "@formbricks/lib/survey/utils"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -34,7 +35,7 @@ export const getSurveys = reactCache( return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey(surveyPrisma)); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error({ error }, "getSurveys: Could not fetch surveys"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts index 5eab694413..2aaec82d5e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const authorize = async (environmentId: string, apiHost: string): Promise => { const res = await fetch(`${apiHost}/api/v1/integrations/notion`, { method: "GET", @@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch notion config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts index 252bd36bac..74f7edb2f3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const authorize = async (environmentId: string, apiHost: string): Promise => { const res = await fetch(`${apiHost}/api/v1/integrations/slack`, { method: "GET", @@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch slack config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/[shortUrlId]/page.tsx b/apps/web/app/[shortUrlId]/page.tsx index eb26877f77..24894cc4ec 100644 --- a/apps/web/app/[shortUrlId]/page.tsx +++ b/apps/web/app/[shortUrlId]/page.tsx @@ -2,6 +2,7 @@ import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata"; import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; import { getShortUrl } from "@formbricks/lib/shortUrl/service"; +import { logger } from "@formbricks/logger"; import { TShortUrl, ZShortUrlId } from "@formbricks/types/short-url"; export const generateMetadata = async (props): Promise => { @@ -44,7 +45,7 @@ const Page = async (props) => { try { shortUrl = await getShortUrl(params.shortUrlId); } catch (error) { - console.error(error); + logger.error(error, "Could not fetch short url"); notFound(); } diff --git a/apps/web/app/api/(internal)/csv-conversion/route.ts b/apps/web/app/api/(internal)/csv-conversion/route.ts index 5e668022c9..0c823b4bc6 100755 --- a/apps/web/app/api/(internal)/csv-conversion/route.ts +++ b/apps/web/app/api/(internal)/csv-conversion/route.ts @@ -3,6 +3,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { AsyncParser } from "@json2csv/node"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; +import { logger } from "@formbricks/logger"; export const POST = async (request: NextRequest) => { const session = await getServerSession(authOptions); @@ -28,7 +29,7 @@ export const POST = async (request: NextRequest) => { try { csv = await parser.parse(json).promise(); } catch (err) { - console.error(err); + logger.error({ error: err, url: request.url }, "Failed to convert to CSV"); throw new Error("Failed to convert to CSV"); } diff --git a/apps/web/app/api/(internal)/insights/lib/utils.ts b/apps/web/app/api/(internal)/insights/lib/utils.ts index a4438acc40..53faeb9e0c 100644 --- a/apps/web/app/api/(internal)/insights/lib/utils.ts +++ b/apps/web/app/api/(internal)/insights/lib/utils.ts @@ -4,6 +4,7 @@ import { surveyCache } from "@formbricks/lib/survey/cache"; import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { doesSurveyHasOpenTextQuestion } from "@formbricks/lib/survey/utils"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse } from "@formbricks/types/responses"; @@ -80,7 +81,7 @@ export const generateInsightsEnabledForSurveyQuestions = async ( return { success: false }; } catch (error) { - console.error("Error generating insights for surveys:", error); + logger.error(error, "Error generating insights for surveys"); throw error; } }; diff --git a/apps/web/app/api/(internal)/insights/route.ts b/apps/web/app/api/(internal)/insights/route.ts index 0d7037a618..c4a2c8f47d 100644 --- a/apps/web/app/api/(internal)/insights/route.ts +++ b/apps/web/app/api/(internal)/insights/route.ts @@ -5,6 +5,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { headers } from "next/headers"; import { z } from "zod"; import { CRON_SECRET } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; import { generateInsightsEnabledForSurveyQuestions } from "./lib/utils"; export const maxDuration = 300; // This function can run for a maximum of 300 seconds @@ -25,7 +26,7 @@ export const POST = async (request: Request) => { const inputValidation = ZGenerateInsightsInput.safeParse(jsonInput); if (!inputValidation.success) { - console.error(inputValidation.error); + logger.error({ error: inputValidation.error, url: request.url }, "Error in POST /api/insights"); return responses.badRequestResponse( "Fields are missing or incorrectly formatted", transformErrorToDetails(inputValidation.error), diff --git a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts index aa0cacb4a8..5eea313aaa 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts @@ -9,6 +9,7 @@ import { writeDataToSlack } from "@formbricks/lib/slack/service"; import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime"; import { parseRecallInfo } from "@formbricks/lib/utils/recall"; import { truncateText } from "@formbricks/lib/utils/strings"; +import { logger } from "@formbricks/logger"; import { Result } from "@formbricks/types/error-handlers"; import { TIntegration, TIntegrationType } from "@formbricks/types/integration"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; @@ -83,13 +84,13 @@ export const handleIntegrations = async ( survey ); if (!googleResult.ok) { - console.error("Error in google sheets integration: ", googleResult.error); + logger.error(googleResult.error, "Error in google sheets integration"); } break; case "slack": const slackResult = await handleSlackIntegration(integration as TIntegrationSlack, data, survey); if (!slackResult.ok) { - console.error("Error in slack integration: ", slackResult.error); + logger.error(slackResult.error, "Error in slack integration"); } break; case "airtable": @@ -99,13 +100,13 @@ export const handleIntegrations = async ( survey ); if (!airtableResult.ok) { - console.error("Error in airtable integration: ", airtableResult.error); + logger.error(airtableResult.error, "Error in airtable integration"); } break; case "notion": const notionResult = await handleNotionIntegration(integration as TIntegrationNotion, data, survey); if (!notionResult.ok) { - console.error("Error in notion integration: ", notionResult.error); + logger.error(notionResult.error, "Error in notion integration"); } break; } @@ -418,7 +419,7 @@ const getValue = (colType: string, value: string | string[] | Date | number | Re return typeof value === "string" ? value : (value as string[]).join(", "); } } catch (error) { - console.error(error); + logger.error(error, "Payload build failed!"); throw new Error("Payload build failed!"); } }; diff --git a/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts b/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts index 240b0d09ff..e2d1115116 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts @@ -1,6 +1,7 @@ import { sendFollowUpEmail } from "@/modules/email"; import { z } from "zod"; import { TSurveyFollowUpAction } from "@formbricks/database/types/survey-follow-up"; +import { logger } from "@formbricks/logger"; import { TOrganization } from "@formbricks/types/organizations"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -89,6 +90,6 @@ export const sendSurveyFollowUps = async ( .map((result) => `FollowUp ${result.followUpId} failed: ${result.error}`); if (errors.length > 0) { - console.error("Follow-up processing errors:", errors); + logger.error(errors, "Follow-up processing errors"); } }; diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts index c2739e4877..e98ac5208a 100644 --- a/apps/web/app/api/(internal)/pipeline/route.ts +++ b/apps/web/app/api/(internal)/pipeline/route.ts @@ -19,6 +19,7 @@ import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { convertDatesInObject } from "@formbricks/lib/time"; import { getPromptText } from "@formbricks/lib/utils/ai"; import { parseRecallInfo } from "@formbricks/lib/utils/recall"; +import { logger } from "@formbricks/logger"; import { handleIntegrations } from "./lib/handleIntegrations"; export const POST = async (request: Request) => { @@ -34,7 +35,10 @@ export const POST = async (request: Request) => { const inputValidation = ZPipelineInput.safeParse(convertedJsonInput); if (!inputValidation.success) { - console.error(inputValidation.error); + logger.error( + { error: inputValidation.error, url: request.url }, + "Error in POST /api/(internal)/pipeline" + ); return responses.badRequestResponse( "Fields are missing or incorrectly formatted", transformErrorToDetails(inputValidation.error), @@ -87,7 +91,7 @@ export const POST = async (request: Request) => { data: response, }), }).catch((error) => { - console.error(`Webhook call to ${webhook.url} failed:`, error); + logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`); }) ); @@ -100,7 +104,7 @@ export const POST = async (request: Request) => { ]); if (!survey) { - console.error(`Survey with id ${surveyId} not found`); + logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`); return new Response("Survey not found", { status: 404 }); } @@ -172,7 +176,10 @@ export const POST = async (request: Request) => { const emailPromises = usersWithNotifications.map((user) => sendResponseFinishedEmail(user.email, environmentId, survey, response, responseCount).catch((error) => { - console.error(`Failed to send email to ${user.email}:`, error); + logger.error( + { error, url: request.url, userEmail: user.email }, + `Failed to send email to ${user.email}` + ); }) ); @@ -188,7 +195,7 @@ export const POST = async (request: Request) => { const results = await Promise.allSettled([...webhookPromises, ...emailPromises]); results.forEach((result) => { if (result.status === "rejected") { - console.error("Promise rejected:", result.reason); + logger.error({ error: result.reason, url: request.url }, "Promise rejected"); } }); @@ -228,7 +235,7 @@ export const POST = async (request: Request) => { text, }); } catch (e) { - console.error(e); + logger.error({ error: e, url: request.url }, "Error creating document and assigning insight"); } } } @@ -240,7 +247,7 @@ export const POST = async (request: Request) => { const results = await Promise.allSettled(webhookPromises); results.forEach((result) => { if (result.status === "rejected") { - console.error("Promise rejected:", result.reason); + logger.error({ error: result.reason, url: request.url }, "Promise rejected"); } }); } diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts index a0f90078ea..306a488ae5 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts @@ -21,6 +21,7 @@ import { } from "@formbricks/lib/posthogServer"; import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants"; +import { logger } from "@formbricks/logger"; import { ZJsPeopleUserIdInput } from "@formbricks/types/js"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -103,7 +104,7 @@ export const GET = async ( }, }); } catch (error) { - console.error(`Error sending plan limits reached event to Posthog: ${error}`); + logger.error({ error, url: request.url }, `Error sending plan limits reached event to Posthog`); } } } @@ -187,7 +188,10 @@ export const GET = async ( return responses.successResponse({ ...state }, true); } catch (error) { - console.error(error); + logger.error( + { error, url: request.url }, + "Error in GET /api/v1/client/[environmentId]/app/sync/[userId]" + ); return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true); } }; diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts index fe269b75a8..949c0d6ea1 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts @@ -14,6 +14,7 @@ import { getSurveys } from "@formbricks/lib/survey/service"; import { anySurveyHasFilters } from "@formbricks/lib/survey/utils"; import { diffInDays } from "@formbricks/lib/utils/datetime"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -150,7 +151,7 @@ export const getSyncSurveys = reactCache( return surveys; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error); throw new DatabaseError(error.message); } diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts index 12e526fb68..478ea47041 100644 --- a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts @@ -2,6 +2,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; +import { logger } from "@formbricks/logger"; import { ZDisplayCreateInput } from "@formbricks/types/displays"; import { InvalidInputError } from "@formbricks/types/errors"; import { createDisplay } from "./lib/display"; @@ -48,7 +49,7 @@ export const POST = async (request: Request, context: Context): Promise transformPrismaSurvey(survey)); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys for environment state"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts index a57d36109e..0f99348595 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts @@ -3,6 +3,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { NextRequest } from "next/server"; import { environmentCache } from "@formbricks/lib/environment/cache"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsSyncInput } from "@formbricks/types/js"; @@ -11,7 +12,7 @@ export const OPTIONS = async (): Promise => { }; export const GET = async ( - _: NextRequest, + request: NextRequest, props: { params: Promise<{ environmentId: string; @@ -58,11 +59,14 @@ export const GET = async ( return responses.notFoundResponse(err.resourceType, err.resourceId); } - console.error(err); + logger.error( + { error: err, url: request.url }, + "Error in GET /api/v1/client/[environmentId]/environment" + ); return responses.internalServerErrorResponse(err.message, true); } } catch (error) { - console.error(error); + logger.error({ error, url: request.url }, "Error in GET /api/v1/client/[environmentId]/environment"); return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true); } }; diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts index 94ee53a57c..bc54dcb4d7 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts @@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { sendToPipeline } from "@/app/lib/pipelines"; import { updateResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { ZResponseUpdateInput } from "@formbricks/types/responses"; @@ -45,7 +46,10 @@ export const PUT = async ( return responses.badRequestResponse(error.message); } if (error instanceof DatabaseError) { - console.error(error); + logger.error( + { error, url: request.url }, + "Error in PUT /api/v1/client/[environmentId]/responses/[responseId]" + ); return responses.internalServerErrorResponse(error.message); } } @@ -59,7 +63,10 @@ export const PUT = async ( return responses.badRequestResponse(error.message); } if (error instanceof DatabaseError) { - console.error(error); + logger.error( + { error, url: request.url }, + "Error in PUT /api/v1/client/[environmentId]/responses/[responseId]" + ); return responses.internalServerErrorResponse(error.message); } } diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts b/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts index 56ffb86327..d961371381 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts @@ -12,6 +12,7 @@ import { calculateTtcTotal } from "@formbricks/lib/response/utils"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { captureTelemetry } from "@formbricks/lib/telemetry"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses"; @@ -178,7 +179,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise { const req_ = await fetch("https://api.airtable.com/v0/meta/whoami", { @@ -77,7 +78,7 @@ export const GET = async (req: NextRequest) => { await createOrUpdateIntegration(environmentId, airtableIntegrationInput); return Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/airtable`); } catch (error) { - console.error(error); + logger.error({ error, url: req.url }, "Error in GET /api/v1/integrations/airtable/callback"); responses.internalServerErrorResponse(error); } responses.badRequestResponse("unknown error occurred"); diff --git a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts index c9ab0f9ba8..6811814800 100644 --- a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts +++ b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service"; +import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; @@ -54,7 +55,7 @@ export const PUT = async ( try { actionClassUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/action-classes/route.ts b/apps/web/app/api/v1/management/action-classes/route.ts index 6a032ee6e3..4bfc2922f8 100644 --- a/apps/web/app/api/v1/management/action-classes/route.ts +++ b/apps/web/app/api/v1/management/action-classes/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { createActionClass, getActionClasses } from "@formbricks/lib/actionClass/service"; +import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; import { DatabaseError } from "@formbricks/types/errors"; @@ -28,7 +29,7 @@ export const POST = async (request: Request): Promise => { try { actionClassInput = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 2eeefb829b..28f7c7d304 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -4,6 +4,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { TResponse, ZResponseUpdateInput } from "@formbricks/types/responses"; const fetchAndValidateResponse = async (authentication: any, responseId: string): Promise => { @@ -77,7 +78,7 @@ export const PUT = async ( try { responseUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/responses/lib/response.ts b/apps/web/app/api/v1/management/responses/lib/response.ts index 56ffb86327..d961371381 100644 --- a/apps/web/app/api/v1/management/responses/lib/response.ts +++ b/apps/web/app/api/v1/management/responses/lib/response.ts @@ -12,6 +12,7 @@ import { calculateTtcTotal } from "@formbricks/lib/response/utils"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { captureTelemetry } from "@formbricks/lib/telemetry"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses"; @@ -178,7 +179,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise => { try { jsonInput = await request.json(); } catch (err) { - console.error(`Error parsing JSON input: ${err}`); + logger.error({ error: err, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } @@ -92,7 +93,7 @@ export const POST = async (request: Request): Promise => { if (error instanceof InvalidInputError) { return responses.badRequestResponse(error.message); } else { - console.error(error); + logger.error({ error, url: request.url }, "Error in POST /api/v1/management/responses"); return responses.internalServerErrorResponse(error.message); } } diff --git a/apps/web/app/api/v1/management/storage/route.ts b/apps/web/app/api/v1/management/storage/route.ts index 1f0ecdc86b..9a5060b2be 100644 --- a/apps/web/app/api/v1/management/storage/route.ts +++ b/apps/web/app/api/v1/management/storage/route.ts @@ -3,6 +3,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { logger } from "@formbricks/logger"; import { getSignedUrlForPublicFile } from "./lib/getSignedUrl"; // api endpoint for uploading public files @@ -17,7 +18,7 @@ export const POST = async (req: NextRequest): Promise => { try { storageInput = await req.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: req.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts index bcf696a893..c70179f17b 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts @@ -5,6 +5,7 @@ import { segmentCache } from "@formbricks/lib/cache/segment"; import { responseCache } from "@formbricks/lib/response/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const deleteSurvey = async (surveyId: string) => { @@ -67,7 +68,7 @@ export const deleteSurvey = async (surveyId: string) => { return deletedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error({ error, surveyId }, "Error deleting survey"); throw new DatabaseError(error.message); } diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts index 4c44d923ad..34749304bc 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts @@ -6,6 +6,7 @@ import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { TSurvey, ZSurveyUpdateInput } from "@formbricks/types/surveys/types"; const fetchAndAuthorizeSurvey = async (authentication: any, surveyId: string): Promise => { @@ -79,7 +80,7 @@ export const PUT = async ( try { surveyUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts index 029c5c29fc..06ee2cee4d 100644 --- a/apps/web/app/api/v1/management/surveys/route.ts +++ b/apps/web/app/api/v1/management/surveys/route.ts @@ -5,6 +5,7 @@ import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { createSurvey, getSurveys } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { ZSurveyCreateInput } from "@formbricks/types/surveys/types"; @@ -41,7 +42,7 @@ export const POST = async (request: Request): Promise => { try { surveyInput = await request.json(); } catch (error) { - console.error(`Error parsing JSON: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index a5a9ed9f43..9dd12a4c47 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -2,6 +2,7 @@ import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key"; import { deleteWebhook, getWebhook } from "@/app/api/v1/webhooks/[webhookId]/lib/webhook"; import { responses } from "@/app/lib/api/response"; import { headers } from "next/headers"; +import { logger } from "@formbricks/logger"; export const GET = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => { const params = await props.params; @@ -26,7 +27,7 @@ export const GET = async (_: Request, props: { params: Promise<{ webhookId: stri return responses.successResponse(webhook); }; -export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => { +export const DELETE = async (request: Request, props: { params: Promise<{ webhookId: string }> }) => { const params = await props.params; const headersList = await headers(); const apiKey = headersList.get("x-api-key"); @@ -52,7 +53,7 @@ export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: s const webhook = await deleteWebhook(params.webhookId); return responses.successResponse(webhook); } catch (e) { - console.error(e.message); + logger.error({ error: e, url: request.url }, "Error deleting webhook"); return responses.notFoundResponse("Webhook", params.webhookId); } }; diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts index bddc7cb7de..f91d3f1347 100644 --- a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts @@ -3,6 +3,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; +import { logger } from "@formbricks/logger"; import { InvalidInputError } from "@formbricks/types/errors"; import { createDisplay } from "./lib/display"; @@ -48,7 +49,7 @@ export const POST = async (request: Request, context: Context): Promise { return fetch(`${WEBAPP_URL}/api/pipeline`, { @@ -15,6 +16,6 @@ export const sendToPipeline = async ({ event, surveyId, environmentId, response response, }), }).catch((error) => { - console.error(`Error sending event to pipeline: ${error}`); + logger.error(error, "Error sending event to pipeline"); }); }; diff --git a/apps/web/app/middleware/rate-limit.ts b/apps/web/app/middleware/rate-limit.ts index 4025ea63ff..4c9dc467a7 100644 --- a/apps/web/app/middleware/rate-limit.ts +++ b/apps/web/app/middleware/rate-limit.ts @@ -1,5 +1,6 @@ import { LRUCache } from "lru-cache"; import { ENTERPRISE_LICENSE_KEY, REDIS_HTTP_URL } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; interface Options { interval: number; @@ -28,8 +29,7 @@ const redisRateLimiter = (options: Options) => async (token: string) => { } const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`); if (!tokenCountResponse.ok) { - // eslint-disable-next-line no-console -- need for debugging - console.error("Failed to increment token count in Redis", tokenCountResponse); + logger.error({ tokenCountResponse }, "Failed to increment token count in Redis"); return; } diff --git a/apps/web/instrumentation-node.ts b/apps/web/instrumentation-node.ts index a1abee1ca5..55eeac233f 100644 --- a/apps/web/instrumentation-node.ts +++ b/apps/web/instrumentation-node.ts @@ -13,6 +13,7 @@ import { } from "@opentelemetry/resources"; import { MeterProvider } from "@opentelemetry/sdk-metrics"; import { env } from "@formbricks/lib/env"; +import { logger } from "@formbricks/logger"; const exporter = new PrometheusExporter({ port: env.PROMETHEUS_EXPORTER_PORT ? parseInt(env.PROMETHEUS_EXPORTER_PORT) : 9464, @@ -51,7 +52,7 @@ process.on("SIGTERM", async () => { await meterProvider.shutdown(); // Possibly close other instrumentation resources } catch (e) { - console.error("Error during graceful shutdown:", e); + logger.error(e, "Error during graceful shutdown"); } finally { process.exit(0); } diff --git a/apps/web/lib/utils/action-client.ts b/apps/web/lib/utils/action-client.ts index 97d073fa47..ba73be13de 100644 --- a/apps/web/lib/utils/action-client.ts +++ b/apps/web/lib/utils/action-client.ts @@ -2,6 +2,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from "next-safe-action"; import { getUser } from "@formbricks/lib/user/service"; +import { logger } from "@formbricks/logger"; import { AuthenticationError, AuthorizationError, @@ -25,7 +26,7 @@ export const actionClient = createSafeActionClient({ } // eslint-disable-next-line no-console -- This error needs to be logged for debugging server-side errors - console.error("SERVER ERROR: ", e); + logger.error(e, "SERVER ERROR"); return DEFAULT_SERVER_ERROR_MESSAGE; }, }); diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 080beca153..88a612cd33 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -85,6 +85,7 @@ export const middleware = async (originalRequest: NextRequest) => { }); request.headers.set("x-request-id", uuidv4()); + request.headers.set("x-start-time", Date.now().toString()); // Create a new NextResponse object to forward the new request with headers const nextResponseWithCustomHeader = NextResponse.next({ diff --git a/apps/web/modules/api/v2/lib/rate-limit.ts b/apps/web/modules/api/v2/lib/rate-limit.ts index 2747b447b6..2ca3d695eb 100644 --- a/apps/web/modules/api/v2/lib/rate-limit.ts +++ b/apps/web/modules/api/v2/lib/rate-limit.ts @@ -1,6 +1,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { type LimitOptions, Ratelimit, type RatelimitResponse } from "@unkey/ratelimit"; import { MANAGEMENT_API_RATE_LIMIT, RATE_LIMITING_DISABLED, UNKEY_ROOT_KEY } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; import { Result, err, okVoid } from "@formbricks/types/error-handlers"; export type RateLimitHelper = { @@ -18,7 +19,7 @@ let warningDisplayed = false; /** Prevent flooding the logs while testing/building */ function logOnce(message: string) { if (warningDisplayed) return; - console.warn(message); + logger.warn(message); warningDisplayed = true; } diff --git a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts index 7048ee1aa1..323854abc3 100644 --- a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts +++ b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts @@ -1,4 +1,11 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; +import { logger } from "@formbricks/logger"; + +vi.mock("@formbricks/logger", () => ({ + logger: { + warn: vi.fn(), + }, +})); vi.mock("@unkey/ratelimit", () => ({ Ratelimit: vi.fn(), @@ -16,18 +23,18 @@ describe("when rate limiting is disabled", () => { }); test("should log a warning once and return a stubbed response", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(logger, "warn"); const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit"); const res1 = await rateLimiter()({ identifier: "test-id" }); expect(res1).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 }); - expect(warnSpy).toHaveBeenCalledWith("Rate limiting disabled"); + expect(loggerSpy).toHaveBeenCalled(); // Subsequent calls won't log again. await rateLimiter()({ identifier: "another-id" }); - expect(warnSpy).toHaveBeenCalledTimes(1); - warnSpy.mockRestore(); + expect(loggerSpy).toHaveBeenCalledTimes(1); + loggerSpy.mockRestore(); }); }); @@ -44,14 +51,14 @@ describe("when UNKEY_ROOT_KEY is missing", () => { }); test("should log a warning about missing UNKEY_ROOT_KEY and return stub response", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(logger, "warn"); const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit"); const limiterFunc = rateLimiter(); const res = await limiterFunc({ identifier: "test-id" }); expect(res).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 }); - expect(warnSpy).toHaveBeenCalledWith("Disabled due to not finding UNKEY_ROOT_KEY env variable"); - warnSpy.mockRestore(); + expect(loggerSpy).toHaveBeenCalled(); + loggerSpy.mockRestore(); }); }); diff --git a/apps/web/modules/api/v2/lib/tests/utils.test.ts b/apps/web/modules/api/v2/lib/tests/utils.test.ts index ebcb82a9b8..0885a565cd 100644 --- a/apps/web/modules/api/v2/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/lib/tests/utils.test.ts @@ -1,6 +1,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { describe, expect, test, vi } from "vitest"; import { ZodError } from "zod"; +import { logger } from "@formbricks/logger"; import { formatZodError, handleApiError, logApiError, logApiRequest } from "../utils"; const mockRequest = new Request("http://localhost"); @@ -128,38 +129,77 @@ describe("utils", () => { describe("logApiRequest", () => { test("logs API request details", () => { - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Mock the withContext method and its returned info method + const infoMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + info: infoMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test?apikey=123&token=abc&safeParam=value"); mockRequest.headers.set("x-request-id", "123"); + mockRequest.headers.set("x-start-time", Date.now().toString()); - logApiRequest(mockRequest, 200, 100); + logApiRequest(mockRequest, 200); - expect(consoleLogSpy).toHaveBeenCalledWith( - `[API REQUEST DETAILS] GET /api/test - 200 - 100ms\n correlationId: 123\n queryParams: {"safeParam":"value"}` - ); + // Verify withContext was called + expect(withContextMock).toHaveBeenCalled(); + // Verify info was called on the child logger + expect(infoMock).toHaveBeenCalledWith("API Request Details"); - consoleLogSpy.mockRestore(); + // Restore the original method + logger.withContext = originalWithContext; }); test("logs API request details without correlationId and without safe query params", () => { - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Mock the withContext method and its returned info method + const infoMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + info: infoMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test?apikey=123&token=abc"); mockRequest.headers.delete("x-request-id"); + mockRequest.headers.set("x-start-time", (Date.now() - 100).toString()); - logApiRequest(mockRequest, 200, 100); - expect(consoleLogSpy).toHaveBeenCalledWith( - `[API REQUEST DETAILS] GET /api/test - 200 - 100ms\n queryParams: {}` + logApiRequest(mockRequest, 200); + + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: "GET", + path: "/api/test", + responseStatus: 200, + queryParams: {}, + }) ); - consoleLogSpy.mockRestore(); + // Verify info was called on the child logger + expect(infoMock).toHaveBeenCalledWith("API Request Details"); + + // Restore the original method + logger.withContext = originalWithContext; }); }); describe("logApiError", () => { test("logs API error details", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + // Mock the withContext method and its returned error method + const errorMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + error: errorMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test"); mockRequest.headers.set("x-request-id", "123"); @@ -171,15 +211,29 @@ describe("utils", () => { logApiError(mockRequest, error); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `[API ERROR DETAILS]\n correlationId: 123\n error: ${JSON.stringify(error, null, 2)}` - ); + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith({ + correlationId: "123", + error, + }); - consoleErrorSpy.mockRestore(); + // Verify error was called on the child logger + expect(errorMock).toHaveBeenCalledWith("API Error Details"); + + // Restore the original method + logger.withContext = originalWithContext; }); test("logs API error details without correlationId", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + // Mock the withContext method and its returned error method + const errorMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + error: errorMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test"); mockRequest.headers.delete("x-request-id"); @@ -191,11 +245,17 @@ describe("utils", () => { logApiError(mockRequest, error); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `[API ERROR DETAILS]\n error: ${JSON.stringify(error, null, 2)}` - ); + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith({ + correlationId: "", + error, + }); - consoleErrorSpy.mockRestore(); + // Verify error was called on the child logger + expect(errorMock).toHaveBeenCalledWith("API Error Details"); + + // Restore the original method + logger.withContext = originalWithContext; }); }); }); diff --git a/apps/web/modules/api/v2/lib/utils.ts b/apps/web/modules/api/v2/lib/utils.ts index 80f60e06a6..f429c70240 100644 --- a/apps/web/modules/api/v2/lib/utils.ts +++ b/apps/web/modules/api/v2/lib/utils.ts @@ -1,6 +1,7 @@ import { responses } from "@/modules/api/v2/lib/response"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { ZodError } from "zod"; +import { logger } from "@formbricks/logger"; export const handleApiError = (request: Request, err: ApiErrorResponseV2): Response => { logApiError(request, err); @@ -40,11 +41,12 @@ export const formatZodError = (error: ZodError) => { })); }; -export const logApiRequest = (request: Request, responseStatus: number, duration: number): void => { +export const logApiRequest = (request: Request, responseStatus: number): void => { const method = request.method; const url = new URL(request.url); const path = url.pathname; const correlationId = request.headers.get("x-request-id") || ""; + const startTime = request.headers.get("x-start-time") || ""; const queryParams = Object.fromEntries(url.searchParams.entries()); const sensitiveParams = ["apikey", "token", "secret"]; @@ -52,14 +54,25 @@ export const logApiRequest = (request: Request, responseStatus: number, duration Object.entries(queryParams).filter(([key]) => !sensitiveParams.includes(key.toLowerCase())) ); - console.log( - `[API REQUEST DETAILS] ${method} ${path} - ${responseStatus} - ${duration}ms${correlationId ? `\n correlationId: ${correlationId}` : ""}\n queryParams: ${JSON.stringify(safeQueryParams)}` - ); + // Info: Conveys general, operational messages about system progress and state. + logger + .withContext({ + method, + path, + responseStatus, + duration: `${Date.now() - parseInt(startTime)} ms`, + correlationId, + queryParams: safeQueryParams, + }) + .info("API Request Details"); }; export const logApiError = (request: Request, error: ApiErrorResponseV2): void => { const correlationId = request.headers.get("x-request-id") || ""; - console.error( - `[API ERROR DETAILS]${correlationId ? `\n correlationId: ${correlationId}` : ""}\n error: ${JSON.stringify(error, null, 2)}` - ); + logger + .withContext({ + correlationId, + error, + }) + .error("API Error Details"); }; diff --git a/apps/web/modules/api/v2/management/auth/api-wrapper.ts b/apps/web/modules/api/v2/management/auth/api-wrapper.ts index 16862ce1b2..0ff89bbafb 100644 --- a/apps/web/modules/api/v2/management/auth/api-wrapper.ts +++ b/apps/web/modules/api/v2/management/auth/api-wrapper.ts @@ -75,7 +75,6 @@ export const apiWrapper = async ({ if (schemas?.params) { const paramsObject = (await externalParams) || {}; - console.log("paramsObject: ", paramsObject); const paramsResult = schemas.params.safeParse(paramsObject); if (!paramsResult.success) { throw err({ diff --git a/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts b/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts index 9971582f32..1b2aff7c2b 100644 --- a/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts +++ b/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts @@ -14,8 +14,6 @@ export const authenticatedApiClient = async ({ rateLimit?: boolean; handler: HandlerFn>; }): Promise => { - const startTime = Date.now(); - const response = await apiWrapper({ request, schemas, @@ -23,10 +21,9 @@ export const authenticatedApiClient = async ({ rateLimit, handler, }); - - const duration = Date.now() - startTime; - - logApiRequest(request, response.status, duration); + if (response.ok) { + logApiRequest(request, response.status); + } return response; }; diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts index a5e3ad6488..b1908799b8 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts @@ -1,9 +1,16 @@ import { environmentId, fileUploadQuestion, openTextQuestion, responseData } from "./__mocks__/utils.mock"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { deleteFile } from "@formbricks/lib/storage/service"; +import { logger } from "@formbricks/logger"; import { okVoid } from "@formbricks/types/error-handlers"; import { findAndDeleteUploadedFilesInResponse } from "../utils"; +vi.mock("@formbricks/logger", () => ({ + logger: { + error: vi.fn(), + }, +})); + vi.mock("@formbricks/lib/storage/service", () => ({ deleteFile: vi.fn(), })); @@ -37,15 +44,15 @@ describe("findAndDeleteUploadedFilesInResponse", () => { [fileUploadQuestion.id]: [invalidFileUrl], }; - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(logger, "error"); const result = await findAndDeleteUploadedFilesInResponse(responseData, [fileUploadQuestion]); expect(deleteFile).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(loggerSpy).toHaveBeenCalled(); expect(result).toEqual(okVoid()); - consoleErrorSpy.mockRestore(); + loggerSpy.mockRestore(); }); test("process multiple file URLs", async () => { diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts index 005c9de21e..11655b2e09 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts @@ -1,6 +1,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Response, Survey } from "@prisma/client"; import { deleteFile } from "@formbricks/lib/storage/service"; +import { logger } from "@formbricks/logger"; import { Result, okVoid } from "@formbricks/types/error-handlers"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; @@ -26,7 +27,7 @@ export const findAndDeleteUploadedFilesInResponse = async ( } return deleteFile(environmentId, accessType as "private" | "public", fileName); } catch (error) { - console.error(`Failed to delete file ${fileUrl}:`, error); + logger.error({ error, fileUrl }, "Failed to delete file"); } }); diff --git a/apps/web/modules/api/v2/management/responses/lib/response.ts b/apps/web/modules/api/v2/management/responses/lib/response.ts index 0c1ccf841a..f48eb413d8 100644 --- a/apps/web/modules/api/v2/management/responses/lib/response.ts +++ b/apps/web/modules/api/v2/management/responses/lib/response.ts @@ -16,6 +16,7 @@ import { responseCache } from "@formbricks/lib/response/cache"; import { calculateTtcTotal } from "@formbricks/lib/response/utils"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { captureTelemetry } from "@formbricks/lib/telemetry"; +import { logger } from "@formbricks/logger"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const createResponse = async ( @@ -110,7 +111,7 @@ export const createResponse = async ( }); } catch (err) { // Log error but do not throw it - console.error(`Error sending plan limits reached event to Posthog: ${err}`); + logger.error(err, "Error sending plan limits reached event to Posthog"); } } } diff --git a/apps/web/modules/api/v2/openapi-document.ts b/apps/web/modules/api/v2/openapi-document.ts index 2c5b179450..5581b16549 100644 --- a/apps/web/modules/api/v2/openapi-document.ts +++ b/apps/web/modules/api/v2/openapi-document.ts @@ -80,4 +80,5 @@ const document = createDocument({ ], }); +// do not replace this with logger.info console.log(yaml.stringify(document)); diff --git a/apps/web/modules/auth/invite/page.tsx b/apps/web/modules/auth/invite/page.tsx index b569a2304d..b91402f5af 100644 --- a/apps/web/modules/auth/invite/page.tsx +++ b/apps/web/modules/auth/invite/page.tsx @@ -11,6 +11,7 @@ import { WEBAPP_URL } from "@formbricks/lib/constants"; import { verifyInviteToken } from "@formbricks/lib/jwt"; import { createMembership } from "@formbricks/lib/membership/service"; import { getUser, updateUser } from "@formbricks/lib/user/service"; +import { logger } from "@formbricks/logger"; import { ContentLayout } from "./components/content-layout"; interface InvitePageProps { @@ -131,7 +132,7 @@ export const InvitePage = async (props: InvitePageProps) => { ); } catch (e) { - console.error(e); + logger.error(e, "Error in InvitePage"); return ( ({ @@ -35,17 +36,17 @@ describe("createBrevoCustomer", () => { }); it("should log an error if fetch fails", async () => { - const consoleSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Fetch failed")); await createBrevoCustomer({ id: "123", email: "test@example.com" }); - expect(consoleSpy).toHaveBeenCalledWith("Error sending user to Brevo:", expect.any(Error)); + expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error sending user to Brevo"); }); it("should log the error response if fetch status is not 200", async () => { - const consoleSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); vi.mocked(global.fetch).mockResolvedValueOnce( new Response("Bad Request", { status: 400, statusText: "Bad Request" }) @@ -53,6 +54,6 @@ describe("createBrevoCustomer", () => { await createBrevoCustomer({ id: "123", email: "test@example.com" }); - expect(consoleSpy).toHaveBeenCalledWith("Error sending user to Brevo:", "Bad Request"); + expect(loggerSpy).toHaveBeenCalledWith({ errorText: "Bad Request" }, "Error sending user to Brevo"); }); }); diff --git a/apps/web/modules/auth/lib/brevo.ts b/apps/web/modules/auth/lib/brevo.ts index 4308f4857b..6fd9e4a06c 100644 --- a/apps/web/modules/auth/lib/brevo.ts +++ b/apps/web/modules/auth/lib/brevo.ts @@ -1,5 +1,6 @@ import { BREVO_API_KEY, BREVO_LIST_ID } from "@formbricks/lib/constants"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { TUserEmail, ZUserEmail } from "@formbricks/types/user"; @@ -34,9 +35,10 @@ export const createBrevoCustomer = async ({ id, email }: { id: string; email: TU }); if (res.status !== 200) { - console.error("Error sending user to Brevo:", await res.text()); + const errorText = await res.text(); + logger.error({ errorText }, "Error sending user to Brevo"); } } catch (error) { - console.error("Error sending user to Brevo:", error); + logger.error(error, "Error sending user to Brevo"); } }; diff --git a/apps/web/modules/ee/auth/saml/lib/preload-connection.ts b/apps/web/modules/ee/auth/saml/lib/preload-connection.ts index 1a7e7f8c17..5a140971a7 100644 --- a/apps/web/modules/ee/auth/saml/lib/preload-connection.ts +++ b/apps/web/modules/ee/auth/saml/lib/preload-connection.ts @@ -3,6 +3,7 @@ import { ConnectionAPIController } from "@boxyhq/saml-jackson/dist/controller/ap import fs from "fs/promises"; import path from "path"; import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; const getPreloadedConnectionFile = async () => { const preloadedConnections = await fs.readdir(path.join(SAML_XML_DIR)); @@ -41,7 +42,7 @@ export const preloadConnection = async (connectionController: ConnectionAPIContr const preloadedConnectionMetadata = await getPreloadedConnectionMetadata(); if (!preloadedConnectionMetadata) { - console.log("No preloaded connection metadata found"); + logger.info("No preloaded connection metadata found"); return; } @@ -68,6 +69,6 @@ export const preloadConnection = async (connectionController: ConnectionAPIContr }); } } catch (error) { - console.error("Error preloading connection:", error.message); + logger.error(error, "Error preloading connection"); } }; diff --git a/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts b/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts index 16663333e1..5bb8c60f45 100644 --- a/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts +++ b/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts @@ -2,6 +2,7 @@ import fs from "fs/promises"; import path from "path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; import { preloadConnection } from "../preload-connection"; vi.mock("@formbricks/lib/constants", () => ({ @@ -114,14 +115,11 @@ describe("SAML Preload Connection", () => { test("handle case when no XML files are found", async () => { vi.mocked(fs.readdir).mockResolvedValue(["other-file.txt"] as any); - const consoleErrorSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); await preloadConnection(mockConnectionController as any); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error preloading connection:", - expect.stringContaining("No preloaded connection file found") - ); + expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error preloading connection"); expect(mockConnectionController.createSAMLConnection).not.toHaveBeenCalled(); }); @@ -130,13 +128,10 @@ describe("SAML Preload Connection", () => { const errorMessage = "Invalid metadata"; mockConnectionController.createSAMLConnection.mockRejectedValue(new Error(errorMessage)); - const consoleErrorSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); await preloadConnection(mockConnectionController as any); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error preloading connection:", - expect.stringContaining(errorMessage) - ); + expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error preloading connection"); }); }); diff --git a/apps/web/modules/ee/billing/api/lib/create-subscription.ts b/apps/web/modules/ee/billing/api/lib/create-subscription.ts index fdbe261836..64681c19e5 100644 --- a/apps/web/modules/ee/billing/api/lib/create-subscription.ts +++ b/apps/web/modules/ee/billing/api/lib/create-subscription.ts @@ -3,6 +3,7 @@ import { STRIPE_API_VERSION, WEBAPP_URL } from "@formbricks/lib/constants"; import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { apiVersion: STRIPE_API_VERSION, @@ -96,7 +97,7 @@ export const createSubscription = async ( url: "", }; } catch (err) { - console.error(err); + logger.error(err, "Error creating subscription"); return { status: 500, newPlan: true, diff --git a/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts b/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts index 1929054054..8f584ffb81 100644 --- a/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts +++ b/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts @@ -2,6 +2,7 @@ import Stripe from "stripe"; import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { apiVersion: STRIPE_API_VERSION, @@ -44,7 +45,7 @@ export const isSubscriptionCancelled = async ( date: null, }; } catch (err) { - console.error(err); + logger.error(err, "Error checking if subscription is cancelled"); return { cancelled: false, date: null, diff --git a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts index 948ce1f3f2..8103599f58 100644 --- a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts +++ b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts @@ -5,6 +5,7 @@ import { handleSubscriptionDeleted } from "@/modules/ee/billing/api/lib/subscrip import Stripe from "stripe"; import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; +import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { apiVersion: STRIPE_API_VERSION, @@ -19,7 +20,7 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin event = stripe.webhooks.constructEvent(requestBody, stripeSignature, webhookSecret); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; - if (err! instanceof Error) console.error(err); + if (err! instanceof Error) logger.error(err, "Error in Stripe webhook handler"); return { status: 400, message: `Webhook Error: ${errorMessage}` }; } diff --git a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts index 514b6b5564..11fd9c81f5 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts @@ -2,6 +2,7 @@ import Stripe from "stripe"; import { PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TOrganizationBillingPeriod, @@ -27,7 +28,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } if (!organizationId) { - console.error("No organizationId found in subscription"); + logger.error({ event, organizationId }, "No organizationId found in subscription"); return { status: 400, message: "skipping, no organizationId found" }; } @@ -60,7 +61,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } else if (parseInt(product.metadata.responses) > 0) { responses = parseInt(product.metadata.responses); } else { - console.error("Invalid responses metadata in product: ", product.metadata.responses); + logger.error({ responses: product.metadata.responses }, "Invalid responses metadata in product"); throw new Error("Invalid responses metadata in product"); } @@ -69,7 +70,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } else if (parseInt(product.metadata.miu) > 0) { miu = parseInt(product.metadata.miu); } else { - console.error("Invalid miu metadata in product: ", product.metadata.miu); + logger.error({ miu: product.metadata.miu }, "Invalid miu metadata in product"); throw new Error("Invalid miu metadata in product"); } @@ -78,7 +79,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } else if (parseInt(product.metadata.projects) > 0) { projects = parseInt(product.metadata.projects); } else { - console.error("Invalid projects metadata in product: ", product.metadata.projects); + logger.error({ projects: product.metadata.projects }, "Invalid projects metadata in product"); throw new Error("Invalid projects metadata in product"); } diff --git a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts index d81299c4be..3b6af9e808 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts @@ -1,13 +1,14 @@ import Stripe from "stripe"; import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@formbricks/lib/constants"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; export const handleSubscriptionDeleted = async (event: Stripe.Event) => { const stripeSubscriptionObject = event.data.object as Stripe.Subscription; const organizationId = stripeSubscriptionObject.metadata.organizationId; if (!organizationId) { - console.error("No organizationId found in subscription"); + logger.error({ event, organizationId }, "No organizationId found in subscription"); return { status: 400, message: "skipping, no organizationId found" }; } diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts index a71891c8d3..797afcd152 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts @@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { updateAttributes } from "@/modules/ee/contacts/lib/attributes"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { NextRequest } from "next/server"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsContactsUpdateAttributeInput } from "@formbricks/types/js"; import { getContactByUserIdWithAttributes } from "./lib/contact"; @@ -89,7 +90,7 @@ export const PUT = async ( true ); } catch (err) { - console.error(err); + logger.error({ err, url: req.url }, "Error updating attributes"); if (err.statusCode === 403) { return responses.forbiddenResponse(err.message || "Forbidden", true, { ignore: true }); } diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts index ea0bfaf3e2..ead57b3447 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts @@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { contactCache } from "@/lib/cache/contact"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { NextRequest, userAgent } from "next/server"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsUserIdentifyInput } from "@formbricks/types/js"; import { getPersonState } from "./lib/personState"; @@ -62,11 +63,11 @@ export const GET = async ( return responses.notFoundResponse(err.resourceType, err.resourceId); } - console.error(err); + logger.error({ err, url: request.url }, "Error fetching person state"); return responses.internalServerErrorResponse(err.message ?? "Unable to fetch person state", true); } } catch (error) { - console.error(error); + logger.error({ error, url: request.url }, "Error fetching person state"); return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true); } }; diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts index db59c34ea2..c054e1eb06 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts @@ -2,6 +2,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { NextRequest, userAgent } from "next/server"; +import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TJsPersonState, ZJsUserIdentifyInput, ZJsUserUpdateInput } from "@formbricks/types/js"; @@ -94,11 +95,11 @@ export const POST = async ( return responses.notFoundResponse(err.resourceType, err.resourceId); } - console.error(err); + logger.error({ err, url: request.url }, "Error in POST /api/v1/client/[environmentId]/user"); return responses.internalServerErrorResponse(err.message ?? "Unable to fetch person state", true); } } catch (error) { - console.error(error); + logger.error({ error, url: request.url }, "Error in POST /api/v1/client/[environmentId]/user"); return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true); } }; diff --git a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts index f1145d6519..800442c8d2 100644 --- a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts +++ b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { logger } from "@formbricks/logger"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; import { @@ -109,7 +110,7 @@ export const PUT = async ( try { contactAttributeKeyUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts index 7cdbeeb8e1..34928ba0e8 100644 --- a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts +++ b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { ZContactAttributeKeyCreateInput } from "./[contactAttributeKeyId]/types/contact-attribute-keys"; import { createContactAttributeKey, getContactAttributeKeys } from "./lib/contact-attribute-keys"; @@ -40,7 +41,7 @@ export const POST = async (request: Request): Promise => { try { contactAttibuteKeyInput = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/modules/ee/insights/experience/actions.ts b/apps/web/modules/ee/insights/experience/actions.ts index 0e705b170a..4f50bc0a8d 100644 --- a/apps/web/modules/ee/insights/experience/actions.ts +++ b/apps/web/modules/ee/insights/experience/actions.ts @@ -12,6 +12,7 @@ import { checkAIPermission } from "@/modules/ee/insights/actions"; import { ZInsightFilterCriteria } from "@/modules/ee/insights/experience/types/insights"; import { z } from "zod"; import { ZInsight } from "@formbricks/database/zod/insights"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { getInsights, updateInsight } from "./lib/insights"; import { getStats } from "./lib/stats"; @@ -113,10 +114,14 @@ export const updateInsightAction = authenticatedActionClient return await updateInsight(parsedInput.insightId, parsedInput.data); } catch (error) { - console.error("Error updating insight:", { - insightId: parsedInput.insightId, - error, - }); + logger.error( + { + insightId: parsedInput.insightId, + error, + }, + "Error updating insight" + ); + if (error instanceof Error) { throw new Error(`Failed to update insight: ${error.message}`); } diff --git a/apps/web/modules/ee/insights/experience/lib/insights.ts b/apps/web/modules/ee/insights/experience/lib/insights.ts index 4fdcf8962b..ed26260036 100644 --- a/apps/web/modules/ee/insights/experience/lib/insights.ts +++ b/apps/web/modules/ee/insights/experience/lib/insights.ts @@ -11,6 +11,7 @@ import { cache } from "@formbricks/lib/cache"; import { INSIGHTS_PER_PAGE } from "@formbricks/lib/constants"; import { responseCache } from "@formbricks/lib/response/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId, ZOptionalNumber } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; @@ -121,7 +122,7 @@ export const updateInsight = async (insightId: string, updates: Partial } } } catch (error) { - console.error("Error in updateInsight:", error); + logger.error(error, "Error in updateInsight"); if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); } diff --git a/apps/web/modules/ee/insights/experience/lib/stats.ts b/apps/web/modules/ee/insights/experience/lib/stats.ts index dc9cbff9ea..f70872d452 100644 --- a/apps/web/modules/ee/insights/experience/lib/stats.ts +++ b/apps/web/modules/ee/insights/experience/lib/stats.ts @@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { responseCache } from "@formbricks/lib/response/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { TStats } from "../types/stats"; @@ -88,7 +89,7 @@ export const getStats = reactCache( }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching stats"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts index 88beff7ffd..c5ad8cfc8f 100644 --- a/apps/web/modules/ee/license-check/lib/utils.ts +++ b/apps/web/modules/ee/license-check/lib/utils.ts @@ -19,6 +19,7 @@ import { } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { hashString } from "@formbricks/lib/hashString"; +import { logger } from "@formbricks/logger"; const hashedKey = ENTERPRISE_LICENSE_KEY ? hashString(ENTERPRISE_LICENSE_KEY) : undefined; const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const; @@ -98,12 +99,12 @@ const fetchLicenseForE2ETesting = async (): Promise<{ return newResult; } else if (currentTime.getTime() - previousResult.lastChecked.getTime() > 60 * 60 * 1000) { // Fail after 1 hour - console.log("E2E_TESTING is enabled. Enterprise license was revoked after 1 hour."); + logger.info("E2E_TESTING is enabled. Enterprise license was revoked after 1 hour."); return null; } return previousResult; } catch (error) { - console.error("Error fetching license: ", error); + logger.error(error, "Error fetching license"); return null; } }; @@ -191,7 +192,7 @@ export const getEnterpriseLicense = async (): Promise<{ } // Log error only after 72 hours - console.error("Error while checking license: The license check failed"); + logger.error("Error while checking license: The license check failed"); return { active: false, @@ -254,7 +255,7 @@ export const fetchLicense = reactCache( return null; } catch (error) { - console.error("Error while checking license: ", error); + logger.error(error, "Error while checking license"); return null; } }, diff --git a/apps/web/modules/ee/teams/lib/roles.ts b/apps/web/modules/ee/teams/lib/roles.ts index 030779db2f..5b74f1aa6e 100644 --- a/apps/web/modules/ee/teams/lib/roles.ts +++ b/apps/web/modules/ee/teams/lib/roles.ts @@ -8,6 +8,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { membershipCache } from "@formbricks/lib/membership/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId, ZString } from "@formbricks/types/common"; import { DatabaseError, UnknownError } from "@formbricks/types/errors"; @@ -51,7 +52,7 @@ export const getProjectPermissionByUserId = reactCache( return highestPermission; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project permission by user id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/ee/teams/team-list/lib/project.ts b/apps/web/modules/ee/teams/team-list/lib/project.ts index 2f655a174b..06ec7d370c 100644 --- a/apps/web/modules/ee/teams/team-list/lib/project.ts +++ b/apps/web/modules/ee/teams/team-list/lib/project.ts @@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZString } from "@formbricks/types/common"; import { DatabaseError, UnknownError } from "@formbricks/types/errors"; @@ -32,7 +33,7 @@ export const getProjectsByOrganizationId = reactCache( })); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching projects by organization id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts b/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts index 8a42f69617..3d160120dd 100644 --- a/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts +++ b/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import { prisma } from "@formbricks/database"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { ValidationError } from "@formbricks/types/errors"; @@ -49,7 +50,7 @@ export const updateProjectBranding = async ( return true; } catch (error) { if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + logger.error(error.errors, "Error updating project branding"); } throw new ValidationError("Data validation of project failed"); } diff --git a/apps/web/modules/email/emails/survey/follow-up.tsx b/apps/web/modules/email/emails/survey/follow-up.tsx index fed887ea88..d81dba0ee9 100644 --- a/apps/web/modules/email/emails/survey/follow-up.tsx +++ b/apps/web/modules/email/emails/survey/follow-up.tsx @@ -14,7 +14,6 @@ interface FollowUpEmailProps { export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Promise { const t = await getTranslate(); - console.log(t("emails.imprint")); const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl; return ( diff --git a/apps/web/modules/email/index.tsx b/apps/web/modules/email/index.tsx index b99074d768..d4dd11b2eb 100644 --- a/apps/web/modules/email/index.tsx +++ b/apps/web/modules/email/index.tsx @@ -18,6 +18,7 @@ import { } from "@formbricks/lib/constants"; import { createInviteToken, createToken, createTokenForLinkSurvey } from "@formbricks/lib/jwt"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; import type { TLinkSurveyEmailData } from "@formbricks/types/email"; import { InvalidInputError } from "@formbricks/types/errors"; import type { TResponse } from "@formbricks/types/responses"; @@ -76,7 +77,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise return true; } catch (error) { - console.error("Error in sendEmail:", error); + logger.error(error, "Error in sendEmail"); throw new InvalidInputError("Incorrect SMTP credentials"); } }; @@ -104,7 +105,7 @@ export const sendVerificationEmail = async ({ html, }); } catch (error) { - console.error("Error in sendVerificationEmail:", error); + logger.error(error, "Error in sendVerificationEmail"); throw error; // Re-throw the error to maintain the original behavior } }; diff --git a/apps/web/modules/organization/settings/teams/lib/membership.ts b/apps/web/modules/organization/settings/teams/lib/membership.ts index 57252051d6..c15981448d 100644 --- a/apps/web/modules/organization/settings/teams/lib/membership.ts +++ b/apps/web/modules/organization/settings/teams/lib/membership.ts @@ -9,6 +9,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { DatabaseError, UnknownError } from "@formbricks/types/errors"; import { TMember } from "@formbricks/types/memberships"; @@ -51,7 +52,7 @@ export const getMembershipByOrganizationId = reactCache( return members; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching membership by organization id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/projects/settings/lib/project.ts b/apps/web/modules/projects/settings/lib/project.ts index 877618348d..d0733b3d21 100644 --- a/apps/web/modules/projects/settings/lib/project.ts +++ b/apps/web/modules/projects/settings/lib/project.ts @@ -11,6 +11,7 @@ import { deleteS3FilesByEnvironmentId, } from "@formbricks/lib/storage/service"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId, ZString } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ValidationError } from "@formbricks/types/errors"; import { TProject, TProjectUpdateInput, ZProject, ZProjectUpdateInput } from "@formbricks/types/project"; @@ -79,7 +80,7 @@ export const updateProject = async ( return project; } catch (error) { if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + logger.error(error.errors, "Error updating project"); } throw new ValidationError("Data validation of project failed"); } @@ -174,7 +175,7 @@ export const deleteProject = async (projectId: string): Promise => { await Promise.all(s3FilesPromises); } catch (err) { // fail silently because we don't want to throw an error if the files are not deleted - console.error(err); + logger.error(err, "Error deleting S3 files"); } } else { const localFilesPromises = project.environments.map(async (environment) => { @@ -185,7 +186,7 @@ export const deleteProject = async (projectId: string): Promise => { await Promise.all(localFilesPromises); } catch (err) { // fail silently because we don't want to throw an error if the files are not deleted - console.error(err); + logger.error(err, "Error deleting local files"); } } diff --git a/apps/web/modules/projects/settings/look/lib/project.ts b/apps/web/modules/projects/settings/look/lib/project.ts index 7384edfe56..82e99a7f78 100644 --- a/apps/web/modules/projects/settings/look/lib/project.ts +++ b/apps/web/modules/projects/settings/look/lib/project.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getProjectByEnvironmentId = reactCache( @@ -29,7 +30,7 @@ export const getProjectByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/components/template-list/lib/survey.ts b/apps/web/modules/survey/components/template-list/lib/survey.ts index cd40045c90..15562ed497 100644 --- a/apps/web/modules/survey/components/template-list/lib/survey.ts +++ b/apps/web/modules/survey/components/template-list/lib/survey.ts @@ -11,6 +11,7 @@ import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TSurveyCreateInput } from "@formbricks/types/surveys/types"; @@ -177,7 +178,7 @@ export const createSurvey = async ( return transformedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating survey"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/editor/lib/project.ts b/apps/web/modules/survey/editor/lib/project.ts index 78c1df8d9a..d07a794a77 100644 --- a/apps/web/modules/survey/editor/lib/project.ts +++ b/apps/web/modules/survey/editor/lib/project.ts @@ -3,6 +3,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const getProject = reactCache( @@ -19,7 +20,7 @@ export const getProject = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/editor/lib/survey.ts b/apps/web/modules/survey/editor/lib/survey.ts index e594428698..2c99b6d9a8 100644 --- a/apps/web/modules/survey/editor/lib/survey.ts +++ b/apps/web/modules/survey/editor/lib/survey.ts @@ -9,6 +9,7 @@ import { Prisma, Survey } from "@prisma/client"; import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSegment, ZSegmentFilters } from "@formbricks/types/segment"; import { TSurvey, TSurveyOpenTextQuestion } from "@formbricks/types/surveys/types"; @@ -118,7 +119,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => segmentCache.revalidate({ id: updatedSegment.id, environmentId: updatedSegment.environmentId }); updatedSegment.surveys.map((survey) => surveyCache.revalidate({ id: survey.id })); } catch (error) { - console.error(error); + logger.error(error, "Error updating survey"); throw new Error("Error updating survey"); } } else { @@ -372,7 +373,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => return modifiedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error updating survey"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/lib/environment.ts b/apps/web/modules/survey/lib/environment.ts index a43e35a0a5..045042fef1 100644 --- a/apps/web/modules/survey/lib/environment.ts +++ b/apps/web/modules/survey/lib/environment.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { environmentCache } from "@formbricks/lib/environment/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getEnvironment = reactCache( @@ -27,7 +28,7 @@ export const getEnvironment = reactCache( return environment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching environment"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/lib/membership.ts b/apps/web/modules/survey/lib/membership.ts index 2353e9e3e1..8d78354919 100644 --- a/apps/web/modules/survey/lib/membership.ts +++ b/apps/web/modules/survey/lib/membership.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { membershipCache } from "@formbricks/lib/membership/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { AuthorizationError, DatabaseError, UnknownError } from "@formbricks/types/errors"; export const getMembershipRoleByUserIdOrganizationId = reactCache( @@ -30,7 +31,7 @@ export const getMembershipRoleByUserIdOrganizationId = reactCache( return membership.role; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching membership role by user id and organization id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/lib/project.ts b/apps/web/modules/survey/lib/project.ts index c12ab8a498..1f6d3857de 100644 --- a/apps/web/modules/survey/lib/project.ts +++ b/apps/web/modules/survey/lib/project.ts @@ -5,6 +5,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getProjectByEnvironmentId = reactCache( @@ -27,7 +28,7 @@ export const getProjectByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/link/lib/project.ts b/apps/web/modules/survey/link/lib/project.ts index 00237b4729..c169d5b4c9 100644 --- a/apps/web/modules/survey/link/lib/project.ts +++ b/apps/web/modules/survey/link/lib/project.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; @@ -35,7 +36,7 @@ export const getProjectByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/link/lib/survey.ts b/apps/web/modules/survey/link/lib/survey.ts index 32eaafd232..7a54acd089 100644 --- a/apps/web/modules/survey/link/lib/survey.ts +++ b/apps/web/modules/survey/link/lib/survey.ts @@ -4,6 +4,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const getSurveyMetadata = reactCache(async (surveyId: string) => @@ -31,7 +32,7 @@ export const getSurveyMetadata = reactCache(async (surveyId: string) => return survey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey metadata"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/list/lib/environment.ts b/apps/web/modules/survey/list/lib/environment.ts index 90a9d274e2..3752019037 100644 --- a/apps/web/modules/survey/list/lib/environment.ts +++ b/apps/web/modules/survey/list/lib/environment.ts @@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { environmentCache } from "@formbricks/lib/environment/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const doesEnvironmentExist = reactCache( @@ -80,7 +81,7 @@ export const getEnvironment = reactCache( return environment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching environment"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/list/lib/project.ts b/apps/web/modules/survey/list/lib/project.ts index 7adea354cb..6b124c5045 100644 --- a/apps/web/modules/survey/list/lib/project.ts +++ b/apps/web/modules/survey/list/lib/project.ts @@ -6,6 +6,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getProjectWithLanguagesByEnvironmentId = reactCache( @@ -30,7 +31,7 @@ export const getProjectWithLanguagesByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting project with languages by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/list/lib/survey.ts b/apps/web/modules/survey/list/lib/survey.ts index 336b1a0dcc..d0ab6c2f62 100644 --- a/apps/web/modules/survey/list/lib/survey.ts +++ b/apps/web/modules/survey/list/lib/survey.ts @@ -15,6 +15,7 @@ import { projectCache } from "@formbricks/lib/project/cache"; import { responseCache } from "@formbricks/lib/response/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurveyFilterCriteria } from "@formbricks/types/surveys/types"; @@ -72,7 +73,7 @@ export const getSurveys = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys"); throw new DatabaseError(error.message); } throw error; @@ -161,7 +162,7 @@ export const getSurveysSortedByRelevance = reactCache( return surveys; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys sorted by relevance"); throw new DatabaseError(error.message); } throw error; @@ -193,7 +194,7 @@ export const getSurvey = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey"); throw new DatabaseError(error.message); } throw error; @@ -283,7 +284,7 @@ export const deleteSurvey = async (surveyId: string): Promise => { return true; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error deleting survey"); throw new DatabaseError(error.message); } @@ -606,7 +607,7 @@ export const copySurveyToOtherEnvironment = async ( return newSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error copying survey to other environment"); throw new DatabaseError(error.message); } throw error; @@ -628,7 +629,7 @@ export const getSurveyCount = reactCache( return surveyCount; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey count"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/templates/lib/survey.ts b/apps/web/modules/survey/templates/lib/survey.ts index 3ee89261bf..d8967d3e79 100644 --- a/apps/web/modules/survey/templates/lib/survey.ts +++ b/apps/web/modules/survey/templates/lib/survey.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const createSurvey = async ( @@ -101,7 +102,7 @@ export const createSurvey = async ( return { id: survey.id }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating survey"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/ui/components/input-combo-box/stories.ts b/apps/web/modules/ui/components/input-combo-box/stories.ts index 54ae84aedf..5ab19be468 100644 --- a/apps/web/modules/ui/components/input-combo-box/stories.ts +++ b/apps/web/modules/ui/components/input-combo-box/stories.ts @@ -1,5 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { FileIcon, FolderIcon, ImageIcon } from "lucide-react"; +import { logger } from "@formbricks/logger"; import { InputCombobox } from "./index"; const meta = { @@ -41,7 +42,7 @@ export const Default: Story = { searchPlaceholder: "Search...", options: commonOptions, value: null, - onChangeValue: (value, option) => console.log("Selected:", value, option), + onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"), clearable: true, withInput: false, allowMultiSelect: false, diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 3593620524..eb1eed2cfa 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -18,7 +18,7 @@ const nextConfig = { assetPrefix: process.env.ASSET_PREFIX_URL || undefined, output: "standalone", poweredByHeader: false, - serverExternalPackages: ["@aws-sdk"], + serverExternalPackages: ["@aws-sdk", "@opentelemetry/instrumentation", "pino", "pino-pretty"], outputFileTracingIncludes: { "app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"], }, @@ -27,10 +27,7 @@ const nextConfig = { localeDetection: false, defaultLocale: "en-US", }, - experimental: { - instrumentationHook: true, - serverComponentsExternalPackages: ["@opentelemetry/instrumentation"], - }, + experimental: {}, transpilePackages: ["@formbricks/database", "@formbricks/lib"], images: { remotePatterns: [ diff --git a/apps/web/package.json b/apps/web/package.json index 408a8f0bcb..0b67048bac 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,7 @@ "@formbricks/js-core": "workspace:*", "@formbricks/lib": "workspace:*", "@formbricks/surveys": "workspace:*", + "@formbricks/logger": "workspace:*", "@formbricks/types": "workspace:*", "@hookform/resolvers": "3.9.1", "@intercom/messenger-js-sdk": "0.0.14", diff --git a/apps/web/playwright/api/management/responses.spec.ts b/apps/web/playwright/api/management/responses.spec.ts index ff4fc062ec..17fbac8da8 100644 --- a/apps/web/playwright/api/management/responses.spec.ts +++ b/apps/web/playwright/api/management/responses.spec.ts @@ -1,4 +1,5 @@ import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; import { test } from "../../lib/fixtures"; import { loginAndGetApiKey } from "../../lib/utils"; import { RESPONSES_API_URL, SURVEYS_API_URL } from "../constants"; @@ -10,7 +11,7 @@ test.describe("API Tests for Responses", () => { try { ({ environmentId, apiKey } = await loginAndGetApiKey(page, users)); } catch (error) { - console.error("Error during login and getting API key:", error); + logger.error(error, "Error during login and getting API key"); throw error; } diff --git a/apps/web/playwright/api/management/survey.spec.ts b/apps/web/playwright/api/management/survey.spec.ts index 985950dfba..eb446c87a6 100644 --- a/apps/web/playwright/api/management/survey.spec.ts +++ b/apps/web/playwright/api/management/survey.spec.ts @@ -1,4 +1,5 @@ import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; import { test } from "../../lib/fixtures"; import { loginAndGetApiKey } from "../../lib/utils"; import { SURVEYS_API_URL } from "../constants"; @@ -10,7 +11,7 @@ test.describe("API Tests", () => { try { ({ environmentId, apiKey } = await loginAndGetApiKey(page, users)); } catch (error) { - console.error("Error during login and getting API key:", error); + logger.error(error, "Error during login and getting API key"); throw error; } diff --git a/apps/web/playwright/utils/helper.ts b/apps/web/playwright/utils/helper.ts index 05aaedd324..2b767a5d45 100644 --- a/apps/web/playwright/utils/helper.ts +++ b/apps/web/playwright/utils/helper.ts @@ -2,6 +2,7 @@ import { CreateSurveyParams, CreateSurveyWithLogicParams } from "@/playwright/ut import { expect } from "@playwright/test"; import { readFileSync, writeFileSync } from "fs"; import { Page } from "playwright"; +import { logger } from "@formbricks/logger"; import { TProjectConfigChannel } from "@formbricks/types/project"; export const signUpAndLogin = async ( @@ -96,7 +97,7 @@ export const uploadFileForFileUploadQuestion = async (page: Page) => { }, ]); } catch (error) { - console.error("Error uploading files:", error); + logger.error(error, "Error uploading files"); } }; diff --git a/apps/web/scripts/merge-client-endpoints.ts b/apps/web/scripts/merge-client-endpoints.ts index e1bff09938..2a19276301 100644 --- a/apps/web/scripts/merge-client-endpoints.ts +++ b/apps/web/scripts/merge-client-endpoints.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as yaml from "yaml"; +import { logger } from "@formbricks/logger"; // Define the v1 (now v2) client endpoints to be merged const v1ClientEndpoints = { @@ -314,8 +315,8 @@ const updatedOpenapiContent = yaml.stringify(openapiDoc); // Write the updated content back to the openapi.yml file try { fs.writeFileSync(openapiFilePath, updatedOpenapiContent); - console.log("Merged v1 client endpoints into the generated v2 documentation."); + logger.info("Merged v1 client endpoints into the generated v2 documentation."); } catch (error) { - console.error("Error writing to OpenAPI file:", error); + logger.error(error, "Error writing to OpenAPI file"); process.exit(1); } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index afe005913a..09d604bf5a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -26,6 +26,9 @@ x-environment: &environment # You can use: $(openssl rand -hex 32) to generate a secure one CRON_SECRET: + # Set the minimum log level(debug, info, warn, error, fatal) + # LOG_LEVEL: info + ############################################# OPTIONAL (ENTERPRISE EDITION) ############################################# # Enterprise License Key (More info at: https://formbricks.com/docs/self-hosting/license) @@ -162,7 +165,6 @@ x-environment: &environment # REDIS_URL: # REDIS_DEFAULT_TTL: - # Set the below to use for Rate Limiting (default us In-Memory LRU Cache) # REDIS_HTTP_URL: diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 7783a9793c..3b23dd7b4e 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -16,6 +16,7 @@ These variables are present inside your machine’s docker-compose file. Restart | NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | | ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | | CRON_SECRET | API Secret for running cron jobs. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | +| LOG_LEVEL | Minimum log level (debug, info, warn, error, fatal) | optional | info | | UPLOADS_DIR | Local directory for storing uploads. | optional | ./uploads | | S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) | | S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) | diff --git a/package.json b/package.json index 072c87cba1..349fca8569 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lint": "turbo run lint", "release": "turbo run build --filter=@formbricks/js... && changeset publish", "test": "turbo run test --no-cache", + "test:coverage": "turbo run test:coverage --no-cache", "test:e2e": "playwright test", "test-e2e:azure": "pnpm test:e2e -c playwright.service.config.ts --workers=20", "prepare": "husky install", diff --git a/packages/database/migration/20241209104738_xm_user_identification/migration.ts b/packages/database/migration/20241209104738_xm_user_identification/migration.ts index 36f2cdc438..6dd745701f 100644 --- a/packages/database/migration/20241209104738_xm_user_identification/migration.ts +++ b/packages/database/migration/20241209104738_xm_user_identification/migration.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ import { createId } from "@paralleldrive/cuid2"; import { Prisma } from "@prisma/client"; +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; export const xmUserIdentification: MigrationScript = { @@ -23,7 +24,7 @@ export const xmUserIdentification: MigrationScript = { // If no contacts have a userId, migration is already complete if (totalContacts > 0 && contactsWithUserId === 0) { - console.log("Migration already completed. No contacts with userId found."); + logger.info("Migration already completed. No contacts with userId found."); return; } @@ -40,7 +41,7 @@ export const xmUserIdentification: MigrationScript = { break; } - console.log("Processing attributeKeys for", environments.length, "environments"); + logger.info(`Processing attributeKeys for ${environments.length.toString()} environments`); // Process each environment for (const env of environments) { @@ -80,7 +81,7 @@ export const xmUserIdentification: MigrationScript = { SELECT COUNT(*)::integer AS deleted_count FROM deleted `; - console.log("Deleted userId attributes for", deletedCount, "contacts"); + logger.info(`Deleted userId attributes for ${deletedCount.toString()} contacts`); while (true) { // Fetch contacts with userId in batches @@ -164,7 +165,7 @@ export const xmUserIdentification: MigrationScript = { processedContacts += contacts.length; if (processedContacts > 0) { - console.log(`Processed ${processedContacts.toString()} contacts`); + logger.info(`Processed ${processedContacts.toString()} contacts`); } } @@ -186,13 +187,12 @@ export const xmUserIdentification: MigrationScript = { ) `; - console.log("Total contacts after migration:", totalContactsAfterMigration); - console.log("Total attributes with userId now:", totalUserIdAttributes); + logger.info(`Total contacts after migration: ${totalContactsAfterMigration.toString()}`); + logger.info(`Total attributes with userId now: ${totalUserIdAttributes.toString()}`); if (totalContactsAfterMigration !== totalUserIdAttributes) { - console.log( - "Difference between total contacts and total attributes with userId: ", - totalContactsAfterMigration - totalUserIdAttributes + logger.info( + `Difference between total contacts and total attributes with userId: ${(totalContactsAfterMigration - totalUserIdAttributes).toString()}` ); } }, diff --git a/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts b/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts index ac1034a816..db894d9067 100644 --- a/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts +++ b/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ import { Prisma } from "@prisma/client"; +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; export const xmAttributeRemoval: MigrationScript = { @@ -83,6 +84,6 @@ export const xmAttributeRemoval: MigrationScript = { ), }; - console.log("Data migration completed. Summary: ", summary); + logger.info(summary, "Data migration completed. Summary: "); }, }; diff --git a/packages/database/migration/20241209111525_update_org_limits/migration.ts b/packages/database/migration/20241209111525_update_org_limits/migration.ts index 75e7610245..0f34212015 100644 --- a/packages/database/migration/20241209111525_update_org_limits/migration.ts +++ b/packages/database/migration/20241209111525_update_org_limits/migration.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; type Plan = "free" | "startup" | "scale"; @@ -94,6 +95,6 @@ export const updateOrgLimits: MigrationScript = { await Promise.all(updationPromises); - console.log(`Updated ${organizations.length.toString()} organizations`); + logger.info(`Updated ${organizations.length.toString()} organizations`); }, }; diff --git a/packages/database/migration/20241209111725_product_revamp/migration.ts b/packages/database/migration/20241209111725_product_revamp/migration.ts index c4f9a2e67b..b9abf57847 100644 --- a/packages/database/migration/20241209111725_product_revamp/migration.ts +++ b/packages/database/migration/20241209111725_product_revamp/migration.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; type Plan = "free" | "startup" | "scale" | "enterprise"; @@ -46,7 +47,7 @@ export const productRevamp: MigrationScript = { await Promise.all(updateOrganizationPromises); - console.log(`Updated ${updateOrganizationPromises.length.toString()} organizations`); + logger.info(`Updated ${updateOrganizationPromises.length.toString()} organizations`); const updatedEmptyConfigProjects: number | undefined = await tx.$executeRaw` UPDATE "Project" @@ -57,7 +58,7 @@ export const productRevamp: MigrationScript = { WHERE config = '{}'; `; - console.log( + logger.info( `Updated ${updatedEmptyConfigProjects ? updatedEmptyConfigProjects.toString() : "0"} projects with empty config` ); }, diff --git a/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts b/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts index 44d0993db2..27ddc3a6d3 100644 --- a/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts +++ b/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- field.placeholder can be undefined for surveys created before this migration */ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; interface Field { @@ -63,7 +64,7 @@ export const addPlaceholderToContactAndAddressQuestion: MigrationScript = { `; if (surveys.length === 0) { - console.log("No surveys found"); + logger.info("No surveys found"); return; } let surveyUpdateCount = 0; @@ -194,6 +195,6 @@ export const addPlaceholderToContactAndAddressQuestion: MigrationScript = { } await Promise.all(updatePromises); - console.log(`Updated ${surveyUpdateCount.toString()} surveys`); + logger.info(`Updated ${surveyUpdateCount.toString()} surveys`); }, }; diff --git a/packages/database/migration/20250211050118_removed_new_session_event/migration.ts b/packages/database/migration/20250211050118_removed_new_session_event/migration.ts index ac9b4bb620..a0e4450775 100644 --- a/packages/database/migration/20250211050118_removed_new_session_event/migration.ts +++ b/packages/database/migration/20250211050118_removed_new_session_event/migration.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; export const removedNewSessionEvent: MigrationScript = { @@ -17,7 +18,7 @@ export const removedNewSessionEvent: MigrationScript = { ) `; - console.log(`Updated ${updatedActions.toString()} automatic actions`); + logger.info(`Updated ${updatedActions.toString()} automatic actions`); // Delete actions that are not referenced in SurveyTrigger const deletedActions = await tx.$executeRaw` @@ -29,6 +30,6 @@ export const removedNewSessionEvent: MigrationScript = { WHERE "actionClassId" = "ActionClass".id ) `; - console.log(`Deleted ${deletedActions.toString()} automatic actions`); + logger.info(`Deleted ${deletedActions.toString()} automatic actions`); }, }; diff --git a/packages/database/package.json b/packages/database/package.json index 96f6896a01..43232fbc42 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -26,6 +26,7 @@ "@prisma/client": "6.0.1", "@prisma/extension-accelerate": "1.2.1", "dotenv-cli": "7.4.4", + "@formbricks/logger": "workspace:*", "zod-openapi": "4.2.3" }, "devDependencies": { diff --git a/packages/database/src/scripts/apply-migrations.ts b/packages/database/src/scripts/apply-migrations.ts index 3573d5310e..fcaee8af88 100644 --- a/packages/database/src/scripts/apply-migrations.ts +++ b/packages/database/src/scripts/apply-migrations.ts @@ -1,6 +1,7 @@ +import { logger } from "@formbricks/logger"; import { applyMigrations } from "./migration-runner"; applyMigrations().catch((error: unknown) => { - console.error("Migration failed:", error); + logger.fatal(error, "Migration failed"); process.exit(1); }); diff --git a/packages/database/src/scripts/create-migration.ts b/packages/database/src/scripts/create-migration.ts index 44384b3fab..ab044a6cc1 100644 --- a/packages/database/src/scripts/create-migration.ts +++ b/packages/database/src/scripts/create-migration.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; import { promisify } from "node:util"; +import { logger } from "@formbricks/logger"; import { applyMigrations } from "./migration-runner"; const execAsync = promisify(exec); @@ -15,11 +16,11 @@ function promptForMigrationName(): Promise { return new Promise((resolve) => { rl.question("Enter the name of the migration (please use spaces): ", (name) => { if (!name.trim()) { - console.error("Migration name cannot be empty."); + logger.fatal("Migration name cannot be empty."); process.exit(1); } if (/[^a-zA-Z0-9\s]/.test(name)) { - console.error( + logger.fatal( "Migration name contains invalid characters. Only letters, numbers, and spaces are allowed." ); process.exit(1); @@ -47,7 +48,7 @@ async function main(): Promise { // Create migration await execAsync(`pnpm prisma migrate dev --name "${migrationName}" --create-only`); - console.log(`Migration created: ${migrationName}`); + logger.info(`Migration created: ${migrationName}`); // Find the newly created migration const migrationToCopy = await fs @@ -89,7 +90,7 @@ async function main(): Promise { try { await applyMigrations(); } catch (err) { - console.error("Error applying migrations: ", err); + logger.fatal(err, "Error applying migrations: "); // delete the created migration directories: await fs.rm(destPath, { recursive: true, force: true }); process.exit(1); @@ -97,6 +98,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error("Migration creation failed:", error); + logger.fatal(error, "Migration creation failed"); process.exit(1); }); diff --git a/packages/database/src/scripts/create-saml-database.ts b/packages/database/src/scripts/create-saml-database.ts index cc1d7de99e..e2fbf5b29d 100644 --- a/packages/database/src/scripts/create-saml-database.ts +++ b/packages/database/src/scripts/create-saml-database.ts @@ -1,4 +1,5 @@ import { PrismaClient } from "@prisma/client"; +import { logger } from "@formbricks/logger"; const createSamlDatabase = async (): Promise => { const samlDatabaseUrl = process.env.SAML_DATABASE_URL; @@ -32,9 +33,9 @@ const createSamlDatabase = async (): Promise => { await prisma.$executeRawUnsafe(`CREATE DATABASE "${dbName}"`); - console.log(`Database '${dbName}' created successfully.`); + logger.info(`Database '${dbName}' created successfully.`); } catch (error) { - console.error(`Error creating database '${dbName}':`, error); + logger.error(error, `Error creating database '${dbName}'`); return; } finally { await prisma.$disconnect(); @@ -46,5 +47,5 @@ createSamlDatabase() process.exit(0); }) .catch((error: unknown) => { - console.error("Error creating SAML database:", error); + logger.error(error, "Error creating SAML database"); }); diff --git a/packages/database/src/scripts/generate-data-migration.ts b/packages/database/src/scripts/generate-data-migration.ts index dfd923829b..b78d9f578c 100644 --- a/packages/database/src/scripts/generate-data-migration.ts +++ b/packages/database/src/scripts/generate-data-migration.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; import { createId } from "@paralleldrive/cuid2"; +import { logger } from "@formbricks/logger"; const rl = readline.createInterface({ input: process.stdin, @@ -12,7 +13,7 @@ const migrationsDir = path.resolve(__dirname, "../../migration"); async function createMigration(): Promise { // Log the full path to verify directory location - console.log("Migrations Directory Full Path:", migrationsDir); + logger.info(migrationsDir, "Migrations Directory Full Path"); // Check if migrations directory exists, create if not const hasAccess = await fs @@ -22,7 +23,7 @@ async function createMigration(): Promise { if (!hasAccess) { await fs.mkdir(migrationsDir, { recursive: true }); - console.log(`Created migrations directory: ${migrationsDir}`); + logger.info(`Created migrations directory: ${migrationsDir}`); } const migrationNameSpaced = await promptForMigrationName(); @@ -55,22 +56,22 @@ async function createMigration(): Promise { // Create the migration directory await fs.mkdir(fullMigrationPath, { recursive: true }); - console.log("Created migration directory:", fullMigrationPath); + logger.info(fullMigrationPath, "Created migration directory"); // Create the migration file await fs.writeFile(filePath, getTemplateContent(migrationFunctionName, migrationNameTimestamped)); - console.log(`New migration created: ${filePath}`); + logger.info(filePath, "New migration created"); } function promptForMigrationName(): Promise { return new Promise((resolve) => { rl.question("Enter the name of the migration (please use spaces): ", (name) => { if (!name.trim()) { - console.error("Migration name cannot be empty."); + logger.error("Migration name cannot be empty."); process.exit(1); } if (/[^a-zA-Z0-9\s]/.test(name)) { - console.error( + logger.error( "Migration name contains invalid characters. Only letters, numbers, and spaces are allowed." ); process.exit(1); @@ -107,6 +108,6 @@ export const ${migrationName}: MigrationScript = { } createMigration().catch((error: unknown) => { - console.error("An error occurred while creating the migration:", error); + logger.fatal(error, "An error occurred while creating the migration"); process.exit(1); }); diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts index b0d020ec6b..2f43c4b986 100644 --- a/packages/database/src/scripts/migration-runner.ts +++ b/packages/database/src/scripts/migration-runner.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import { type Prisma, PrismaClient } from "@prisma/client"; +import { logger } from "@formbricks/logger"; const execAsync = promisify(exec); @@ -27,7 +28,7 @@ const MIGRATIONS_DIR = path.resolve(__dirname, "../../migration"); const PRISMA_MIGRATIONS_DIR = path.resolve(__dirname, "../../migrations"); const runMigrations = async (migrations: MigrationScript[]): Promise => { - console.log(`Starting migrations: ${migrations.length.toString()} to run`); + logger.info(`Starting migrations: ${migrations.length.toString()} to run`); const startTime = Date.now(); // empty the prisma migrations directory @@ -38,12 +39,12 @@ const runMigrations = async (migrations: MigrationScript[]): Promise => { } const endTime = Date.now(); - console.log(`All migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); + logger.info(`All migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); }; const runSingleMigration = async (migration: MigrationScript, index: number): Promise => { if (migration.type === "data") { - console.log(`Running data migration: ${migration.name}`); + logger.info(`Running data migration: ${migration.name}`); try { await prisma.$transaction( @@ -56,9 +57,9 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr `; if (existingMigration?.[0]?.status === "pending") { - console.log(`Data migration ${migration.name} is pending.`); - console.log("Either there is another migration which is currently running or this is an error."); - console.log( + logger.info(`Data migration ${migration.name} is pending.`); + logger.info("Either there is another migration which is currently running or this is an error."); + logger.info( "If you are sure that there is no migration running, you need to manually resolve the issue." ); @@ -66,12 +67,12 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr } if (existingMigration?.[0]?.status === "applied") { - console.log(`Data migration ${migration.name} already completed. Skipping...`); + logger.info(`Data migration ${migration.name} already completed. Skipping...`); return; } if (existingMigration?.[0]?.status === "failed") { - console.log(`Data migration ${migration.name} failed previously. Retrying...`); + logger.info(`Data migration ${migration.name} failed previously. Retrying...`); } else { // create a new data migration entry with pending status await prisma.$executeRaw`INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'pending')`; @@ -92,13 +93,13 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr `; } - console.log(`Data migration ${migration.name} completed successfully`); + logger.info(`Data migration ${migration.name} completed successfully`); }, { timeout: TRANSACTION_TIMEOUT } ); } catch (error) { // Record migration failure - console.error(`Data migration ${migration.name} failed:`, error); + logger.error(error, `Data migration ${migration.name} failed`); // Mark migration as failed await prisma.$queryRaw` INSERT INTO "DataMigration" (id, name, status) @@ -110,7 +111,7 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr } } else { try { - console.log(`Running schema migration: ${migration.name}`); + logger.info(`Running schema migration: ${migration.name}`); let copyOnly = false; @@ -138,7 +139,7 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr .then((files) => files.find((dir) => dir.includes(migration.name))); if (!migrationToCopy) { - console.error(`Schema migration not found: ${migration.name}`); + logger.error(`Schema migration not found: ${migration.name}`); return; } @@ -149,16 +150,16 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr await fs.cp(sourcePath, destPath, { recursive: true }); if (copyOnly) { - console.log(`Schema migration ${migration.name} copied to migrations directory`); + logger.info(`Schema migration ${migration.name} copied to migrations directory`); return; } // Run Prisma migrate // throws when migrate deploy fails await execAsync("pnpm prisma migrate deploy"); - console.log(`Successfully applied schema migration: ${migration.name}`); + logger.info(`Successfully applied schema migration: ${migration.name}`); } catch (err) { - console.error(`Schema migration ${migration.name} failed:`, err); + logger.error(err, `Schema migration ${migration.name} failed`); throw err; } } @@ -238,7 +239,7 @@ const loadMigrations = async (): Promise => { } } } else { - console.warn( + logger.warn( `Migration directory ${dirName} doesn't have migration.sql or data-migration.ts. Skipping...` ); } @@ -265,7 +266,7 @@ const loadMigrations = async (): Promise => { export async function applyMigrations(): Promise { try { const allMigrations = await loadMigrations(); - console.log(`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR}`); + logger.info(`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR}`); await runMigrations(allMigrations); } catch (error) { await prisma.$disconnect(); @@ -284,7 +285,7 @@ async function isSchemaMigrationApplied(migrationName: string, prismaClient: Pri `; return applied.length > 0; } catch (error: unknown) { - console.error(`Failed to check migration status: ${error as string}`); + logger.error(error, `Failed to check migration status`); throw new Error(`Could not verify migration status: ${error as string}`); } } diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json index 2853bd4d55..fa8f001737 100644 --- a/packages/database/tsconfig.json +++ b/packages/database/tsconfig.json @@ -1,4 +1,8 @@ { + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler" + }, "exclude": ["node_modules", "dist"], "extends": "@formbricks/config-typescript/node16.json", "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "tsup.config.ts"] diff --git a/packages/lib/airtable/service.ts b/packages/lib/airtable/service.ts index a9229ce9ad..07ceb4a25e 100644 --- a/packages/lib/airtable/service.ts +++ b/packages/lib/airtable/service.ts @@ -1,4 +1,5 @@ import { Prisma } from "@prisma/client"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { TIntegrationItem } from "@formbricks/types/integration"; import { @@ -60,7 +61,7 @@ export const fetchAirtableAuthToken = async (formData: Record) => { const parsedToken = ZIntegrationAirtableTokenSchema.safeParse(tokenRes); if (!parsedToken.success) { - console.error(parsedToken.error); + logger.error(parsedToken.error, "Error parsing airtable token"); throw new Error(parsedToken.error.message); } const { access_token, refresh_token, expires_in } = parsedToken.data; diff --git a/packages/lib/env.ts b/packages/lib/env.ts index cb03557f6b..086622b850 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -50,6 +50,7 @@ export const env = createEnv({ INTERCOM_SECRET_KEY: z.string().optional(), INTERCOM_APP_ID: z.string().optional(), IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(), + LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "fatal"]).optional(), MAIL_FROM: z.string().email().optional(), MAIL_FROM_NAME: z.string().optional(), NEXTAUTH_SECRET: z.string().min(1), @@ -176,6 +177,7 @@ export const env = createEnv({ INVITE_DISABLED: process.env.INVITE_DISABLED, INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY, IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD, + LOG_LEVEL: process.env.LOG_LEVEL, MAIL_FROM: process.env.MAIL_FROM, MAIL_FROM_NAME: process.env.MAIL_FROM_NAME, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts index b14b6ba22d..6662846164 100644 --- a/packages/lib/environment/service.ts +++ b/packages/lib/environment/service.ts @@ -3,6 +3,7 @@ import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import type { TEnvironment, @@ -37,7 +38,7 @@ export const getEnvironment = reactCache( return environment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting environment"); throw new DatabaseError(error.message); } @@ -87,7 +88,7 @@ export const getEnvironments = reactCache( return environments; } catch (error) { if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + logger.error(error, "Error getting environments"); } throw new ValidationError("Data validation of environments array failed"); } diff --git a/packages/lib/integration/service.ts b/packages/lib/integration/service.ts index 3d977971aa..ac96c57969 100644 --- a/packages/lib/integration/service.ts +++ b/packages/lib/integration/service.ts @@ -2,6 +2,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; @@ -55,7 +56,7 @@ export const createOrUpdateIntegration = async ( return integration; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating or updating integration"); throw new DatabaseError(error.message); } throw error; diff --git a/packages/lib/jwt.ts b/packages/lib/jwt.ts index c81b405688..235d263ea0 100644 --- a/packages/lib/jwt.ts +++ b/packages/lib/jwt.ts @@ -1,5 +1,6 @@ import jwt, { JwtPayload } from "jsonwebtoken"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { symmetricDecrypt, symmetricEncrypt } from "./crypto"; import { env } from "./env"; @@ -112,7 +113,7 @@ export const verifyInviteToken = (token: string): { inviteId: string; email: str email: decryptedEmail, }; } catch (error) { - console.error(`Error verifying invite token: ${error}`); + logger.error(error, "Error verifying invite token"); throw new Error("Invalid or expired invite token"); } }; diff --git a/packages/lib/language/service.ts b/packages/lib/language/service.ts index c4dd911c6f..382fb4f49c 100644 --- a/packages/lib/language/service.ts +++ b/packages/lib/language/service.ts @@ -1,6 +1,7 @@ import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors"; import { @@ -40,7 +41,7 @@ export const getLanguage = async (languageId: string): Promise { return true; } catch (error) { - console.error(`Failed to access S3 bucket: ${error}`); + logger.error(error, "Failed to access S3 bucket"); throw new Error(`S3 Bucket Access Test Failed: ${error}`); } }; diff --git a/packages/lib/storage/utils.ts b/packages/lib/storage/utils.ts index 9de8323e42..193978ccb0 100644 --- a/packages/lib/storage/utils.ts +++ b/packages/lib/storage/utils.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const getOriginalFileNameFromUrl = (fileURL: string) => { try { const fileNameFromURL = fileURL.startsWith("/storage/") @@ -16,7 +18,7 @@ export const getOriginalFileNameFromUrl = (fileURL: string) => { const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : ""; return fileName; } catch (error) { - console.error(`Error parsing file URL: ${error}`); + logger.error(error, "Error parsing file URL"); } }; @@ -28,6 +30,6 @@ export const getFileNameWithIdFromUrl = (fileURL: string) => { return fileNameFromURL ? decodeURIComponent(fileNameFromURL || "") : ""; } catch (error) { - console.error("Error parsing file URL:", error); + logger.error(error, "Error parsing file URL"); } }; diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index b9c2027e40..aea8f2ce76 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -2,6 +2,7 @@ import "server-only"; import { ActionClass, Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZOptionalNumber } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; @@ -198,7 +199,7 @@ export const getSurvey = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey"); throw new DatabaseError(error.message); } throw error; @@ -241,7 +242,7 @@ export const getSurveysByActionClassId = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys by action class id"); throw new DatabaseError(error.message); } @@ -286,7 +287,7 @@ export const getSurveys = reactCache( return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey(surveyPrisma)); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys"); throw new DatabaseError(error.message); } throw error; @@ -314,7 +315,7 @@ export const getSurveyCount = reactCache( return surveyCount; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey count"); throw new DatabaseError(error.message); } @@ -435,7 +436,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => segmentCache.revalidate({ id: updatedSegment.id, environmentId: updatedSegment.environmentId }); updatedSegment.surveys.map((survey) => surveyCache.revalidate({ id: survey.id })); } catch (error) { - console.error(error); + logger.error(error, "Error updating survey"); throw new Error("Error updating survey"); } } else { @@ -688,7 +689,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => return modifiedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error updating survey"); throw new DatabaseError(error.message); } @@ -862,7 +863,7 @@ export const createSurvey = async ( return transformedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating survey"); throw new DatabaseError(error.message); } throw error; @@ -890,6 +891,7 @@ export const getSurveyIdByResultShareKey = reactCache( return survey.id; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { + logger.error(error, "Error getting survey id by result share key"); throw new DatabaseError(error.message); } diff --git a/packages/lib/telemetry.ts b/packages/lib/telemetry.ts index 6550da5a4a..4a06f18b20 100644 --- a/packages/lib/telemetry.ts +++ b/packages/lib/telemetry.ts @@ -2,6 +2,7 @@ and how we can improve it. All data including the IP address is collected anonymously and we cannot trace anything back to you or your customers. If you still want to disable telemetry, set the environment variable TELEMETRY_DISABLED=1 */ +import { logger } from "@formbricks/logger"; import { IS_PRODUCTION } from "./constants"; import { env } from "./env"; @@ -31,7 +32,7 @@ export const captureTelemetry = async (eventName: string, properties = {}) => { }), }); } catch (error) { - console.error(`error sending telemetry: ${error}`); + logger.error(error, "error sending telemetry"); } } }; diff --git a/packages/lib/utils/fileConversion.ts b/packages/lib/utils/fileConversion.ts index c7b0bf7813..5c4236cc4f 100644 --- a/packages/lib/utils/fileConversion.ts +++ b/packages/lib/utils/fileConversion.ts @@ -1,5 +1,6 @@ import { AsyncParser } from "@json2csv/node"; import * as xlsx from "xlsx"; +import { logger } from "@formbricks/logger"; export const convertToCsv = async (fields: string[], jsonData: Record[]) => { let csv: string = ""; @@ -11,7 +12,7 @@ export const convertToCsv = async (fields: string[], jsonData: Record = [T, z.ZodType]; @@ -11,8 +12,9 @@ export function validateInputs[]>( for (const [value, schema] of pairs) { const inputValidation = schema.safeParse(value); if (!inputValidation.success) { - console.error( - `Validation failed for ${JSON.stringify(value).substring(0, 100)} and ${JSON.stringify(schema)}: ${inputValidation.error.message}` + logger.error( + inputValidation.error, + `Validation failed for ${JSON.stringify(value).substring(0, 100)} and ${JSON.stringify(schema)}` ); throw new ValidationError("Validation failed"); } diff --git a/packages/logger/.eslintrc.cjs b/packages/logger/.eslintrc.cjs new file mode 100644 index 0000000000..6459e6fb42 --- /dev/null +++ b/packages/logger/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ["@formbricks/eslint-config/library.js"], + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/logger/.gitignore b/packages/logger/.gitignore new file mode 100644 index 0000000000..a6ea004855 --- /dev/null +++ b/packages/logger/.gitignore @@ -0,0 +1,4 @@ +node_modules +.vscode +build +dist \ No newline at end of file diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000000..b3b095e0b5 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,41 @@ +{ + "name": "@formbricks/logger", + "private": true, + "type": "module", + "version": "0.1.0", + "homepage": "https://formbricks.com", + "license": "MIT", + "description": "Logger for Formbricks", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/formbricks/formbricks" + }, + "keywords": [ + "Formbricks", + "logger", + "logging" + ], + "scripts": { + "clean": "rimraf .turbo node_modules coverage dist", + "lint": "eslint . --ext .ts,.js,.tsx,.jsx", + "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", + "lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json", + "build": "tsc && vite build", + "test": "vitest" + }, + "author": "Formbricks ", + "dependencies": { + "zod": "3.24.1", + "pino": "^8.0.0", + "pino-pretty": "^10.0.0" + }, + "devDependencies": { + "vite": "^6.2.0", + "@formbricks/config-typescript": "workspace:*", + "vitest": "3.0.7", + "@formbricks/eslint-config": "workspace:*", + "vite-plugin-dts": "4.3.0" + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000000..41c7bf273e --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1 @@ +export * from "./logger"; diff --git a/packages/logger/src/logger.test.ts b/packages/logger/src/logger.test.ts new file mode 100644 index 0000000000..c3cf137d74 --- /dev/null +++ b/packages/logger/src/logger.test.ts @@ -0,0 +1,224 @@ +// Import pino after the mock is defined +import Pino from "pino"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { LOG_LEVELS } from "../types/logger"; + +// Store original environment variables outside any function +const originalNodeEnv = process.env.NODE_ENV; +const originalLogLevel = process.env.LOG_LEVEL; +const originalNextRuntime = process.env.NEXT_RUNTIME; + +// Define the mock before any imports that use it +// Move mock outside of any function to avoid hoisting issues +vi.mock("pino", () => { + // Create a factory function that returns the mock + return { + default: vi.fn(() => ({ + debug: vi.fn().mockReturnThis(), + info: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + error: vi.fn().mockReturnThis(), + fatal: vi.fn().mockReturnThis(), + child: vi.fn().mockReturnThis(), + flush: vi.fn(), + })), + stdSerializers: { + err: vi.fn(), + req: vi.fn(), + res: vi.fn(), + }, + }; +}); + +describe("Logger", () => { + beforeEach(() => { + // Clear module cache to reset logger for each test + vi.resetModules(); + + // Reset mocks + vi.clearAllMocks(); + + // Set default environment for tests + process.env.NODE_ENV = "development"; + process.env.LOG_LEVEL = "info"; + process.env.NEXT_RUNTIME = "nodejs"; + }); + + afterEach(() => { + // Restore process.env + process.env.NODE_ENV = originalNodeEnv; + process.env.LOG_LEVEL = originalLogLevel; + process.env.NEXT_RUNTIME = originalNextRuntime; + }); + + test("logger is created with development config when NODE_ENV is not production", async () => { + process.env.NODE_ENV = "development"; + const { logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + transport: expect.objectContaining({ + target: "pino-pretty", + }) as Pino.TransportSingleOptions, + }) + ); + + expect(logger).toBeDefined(); + }); + + test("logger is created with production config when NODE_ENV is production", async () => { + process.env.NODE_ENV = "production"; + const { logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.not.objectContaining({ + transport: expect.any(Object) as Pino.TransportSingleOptions, + }) + ); + + expect(logger).toBeDefined(); + }); + + test("getLogLevel defaults to 'info' in development mode", async () => { + process.env.NODE_ENV = "development"; + process.env.LOG_LEVEL = undefined; + + const { logger: _logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + level: "info", + }) + ); + }); + + test("getLogLevel defaults to 'warn' in production mode", async () => { + process.env.NODE_ENV = "production"; + process.env.LOG_LEVEL = undefined; + + const { logger: _logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + level: "warn", + }) + ); + }); + + test("getLogLevel respects LOG_LEVEL env variable when valid", async () => { + process.env.LOG_LEVEL = "debug"; + + const { logger: _logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + level: "debug", + }) + ); + }); + + test("withContext creates a child logger with provided context", async () => { + // Clear cache to get a fresh instance + vi.resetModules(); + + // Create a child spy before importing the logger + const childSpy = vi.fn().mockReturnThis(); + + // Set up the mock to capture the child call + vi.mocked(Pino).mockReturnValue({ + debug: vi.fn().mockReturnThis(), + info: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + error: vi.fn().mockReturnThis(), + fatal: vi.fn().mockReturnThis(), + child: childSpy, + flush: vi.fn(), + } as unknown as Pino.Logger); + + // Now import the logger with our updated mock + const { logger } = await import("./logger"); + + const context = { requestId: "123", userId: "456" }; + logger.withContext(context); + + // Check that the child method was called with the context + expect(childSpy).toHaveBeenCalledWith(context); + }); + + test("request creates a child logger with HTTP request info", async () => { + // Clear cache to get a fresh instance + vi.resetModules(); + + // Create a child spy before importing the logger + const childSpy = vi.fn().mockReturnThis(); + + // Set up the mock to capture the child call + vi.mocked(Pino).mockReturnValue({ + debug: vi.fn().mockReturnThis(), + info: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + error: vi.fn().mockReturnThis(), + fatal: vi.fn().mockReturnThis(), + child: childSpy, + flush: vi.fn(), + } as unknown as Pino.Logger); + + // Now import the logger with our updated mock + const { logger } = await import("./logger"); + + const req = { + method: "GET", + url: "https://example.com/test", + }; + + logger.request(req as unknown as Request); + + // Check that the child method was called with the expected object + expect(childSpy).toHaveBeenCalledWith({ + method: "GET", + url: "https://example.com/test", + }); + }); + + test("logger has all expected log level methods", async () => { + const { logger } = await import("./logger"); + + LOG_LEVELS.forEach((level) => { + expect(typeof logger[level]).toBe("function"); + }); + }); + + test("process handlers are attached in Node.js environment", async () => { + const processSpy = vi.spyOn(process, "on"); + processSpy.mockImplementation(() => process); // Return process for chaining + + process.env.NEXT_RUNTIME = "nodejs"; + + await import("./logger"); + + // Check that process handlers were attached + expect(processSpy).toHaveBeenCalledWith("uncaughtException", expect.any(Function)); + expect(processSpy).toHaveBeenCalledWith("unhandledRejection", expect.any(Function)); + expect(processSpy).toHaveBeenCalledWith("SIGTERM", expect.any(Function)); + expect(processSpy).toHaveBeenCalledWith("SIGINT", expect.any(Function)); + + processSpy.mockRestore(); + }); + + test("process handlers are not attached outside Node.js environment", async () => { + const processSpy = vi.spyOn(process, "on"); + processSpy.mockImplementation(() => process); // Return process for chaining + + process.env.NEXT_RUNTIME = "edge"; + + await import("./logger"); + + // No handlers should be attached for particular events + expect(processSpy).not.toHaveBeenCalledWith("uncaughtException", expect.any(Function)); + expect(processSpy).not.toHaveBeenCalledWith("unhandledRejection", expect.any(Function)); + expect(processSpy).not.toHaveBeenCalledWith("SIGTERM", expect.any(Function)); + expect(processSpy).not.toHaveBeenCalledWith("SIGINT", expect.any(Function)); + + processSpy.mockRestore(); + }); +}); diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 0000000000..525356cc94 --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,111 @@ +import Pino, { type Logger, type LoggerOptions, stdSerializers } from "pino"; +import { LOG_LEVELS, type TLogLevel, ZLogLevel } from "../types/logger"; + +const IS_PRODUCTION = !process.env.NODE_ENV || process.env.NODE_ENV === "production"; +const getLogLevel = (): TLogLevel => { + let logLevel: TLogLevel = "info"; + + if (IS_PRODUCTION) logLevel = "warn"; + + const envLogLevel = process.env.LOG_LEVEL; + + const logLevelResult = ZLogLevel.safeParse(envLogLevel); + if (logLevelResult.success) logLevel = logLevelResult.data; + + return logLevel; +}; + +const baseLoggerConfig: LoggerOptions = { + level: getLogLevel(), + serializers: { + err: stdSerializers.err, + req: stdSerializers.req, + res: stdSerializers.res, + }, + customLevels: { + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, + }, + useOnlyCustomLevels: true, + timestamp: true, + formatters: { + level: (label) => { + return { level: label }; + }, + }, + name: "formbricks", +}; + +const developmentConfig: LoggerOptions = { + ...baseLoggerConfig, + transport: { + target: "pino-pretty", + options: { + colorize: true, + levelFirst: true, + translateTime: "SYS:standard", + ignore: "pid,hostname,ip,requestId", + }, + }, +}; + +const productionConfig: LoggerOptions = { + ...baseLoggerConfig, +}; + +const logger: Logger = IS_PRODUCTION ? Pino(productionConfig) : Pino(developmentConfig); + +LOG_LEVELS.forEach((level) => { + logger[level] = logger[level].bind(logger); +}); + +const extendedLogger = { + ...logger, + withContext: (context: Record) => logger.child(context), + request: (req: Request) => + logger.child({ + method: req.method, + url: req.url, + }), +}; + +const handleShutdown = (event: string, err?: Error): void => { + if (err) { + logger.error(err, `Error during shutdown (${event})`); + } + logger.info({ event }, "Process is exiting"); + + logger.flush(); +}; + +// Create a separate function for attaching Node.js process handlers +const attachNodeProcessHandlers = (): void => { + // Only attach handlers if we're in a Node.js environment with full process support + if (process.env.NEXT_RUNTIME === "nodejs") { + process.on("uncaughtException", (err) => { + handleShutdown("uncaughtException", err); + }); + process.on("unhandledRejection", (err) => { + handleShutdown("unhandledRejection", err as Error); + }); + process.on("SIGTERM", () => { + handleShutdown("SIGTERM"); + }); + process.on("SIGINT", () => { + handleShutdown("SIGINT"); + }); + } +}; + +if (process.env.NEXT_RUNTIME === "nodejs") { + try { + attachNodeProcessHandlers(); + } catch (e) { + logger.error(e, "Error attaching process event handlers"); + } +} + +export { extendedLogger as logger }; diff --git a/packages/logger/src/vite-env.d.ts b/packages/logger/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/logger/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000000..3e48f73289 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true, + "strict": true + }, + "exclude": ["node_modules"], + "extends": "@formbricks/config-typescript/js-library.json", + "include": ["src", "package.json", "types"] +} diff --git a/packages/logger/types/logger.ts b/packages/logger/types/logger.ts new file mode 100644 index 0000000000..26cbbdc587 --- /dev/null +++ b/packages/logger/types/logger.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const LOG_LEVELS = ["debug", "info", "warn", "error", "fatal"] as const; + +export const ZLogLevel = z.enum(LOG_LEVELS); + +export type TLogLevel = z.infer; diff --git a/packages/logger/vite.config.ts b/packages/logger/vite.config.ts new file mode 100644 index 0000000000..91e81fc9e2 --- /dev/null +++ b/packages/logger/vite.config.ts @@ -0,0 +1,32 @@ +import { resolve } from "node:path"; +import { PluginOption, defineConfig } from "vite"; +import dts from "vite-plugin-dts"; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "formbricksLogger", + fileName: "index", + formats: ["es", "cjs"], + }, + rollupOptions: { + external: ["pino", "pino-pretty", "zod"], + output: { + exports: "named", + globals: { + pino: "pino", + "pino-pretty": "pinoPretty", + zod: "zod", + }, + }, + }, + sourcemap: true, + emptyOutDir: false, + }, + plugins: [ + dts({ + rollupTypes: true, + }) as PluginOption, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a296c60684..baabdb3df2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: '@formbricks/lib': specifier: workspace:* version: link:../../packages/lib + '@formbricks/logger': + specifier: workspace:* + version: link:../../packages/logger '@formbricks/surveys': specifier: workspace:* version: link:../../packages/surveys @@ -697,6 +700,9 @@ importers: packages/database: dependencies: + '@formbricks/logger': + specifier: workspace:* + version: link:../logger '@prisma/client': specifier: 6.0.1 version: 6.0.1(prisma@6.0.1) @@ -803,6 +809,9 @@ importers: '@formbricks/database': specifier: workspace:* version: link:../database + '@formbricks/logger': + specifier: workspace:* + version: link:../logger '@formbricks/types': specifier: workspace:* version: link:../types @@ -880,6 +889,34 @@ importers: specifier: 2.0.2 version: 2.0.2(typescript@5.8.2)(vitest@2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)) + packages/logger: + dependencies: + pino: + specifier: ^8.0.0 + version: 8.21.0 + pino-pretty: + specifier: ^10.0.0 + version: 10.3.1 + zod: + specifier: 3.24.1 + version: 3.24.1 + devDependencies: + '@formbricks/config-typescript': + specifier: workspace:* + version: link:../config-typescript + '@formbricks/eslint-config': + specifier: workspace:* + version: link:../config-eslint + vite: + specifier: ^6.2.0 + version: 6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vite-plugin-dts: + specifier: 4.3.0 + version: 4.3.0(@types/node@22.10.2)(rollup@4.36.0)(typescript@5.8.2)(vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vitest: + specifier: 3.0.7 + version: 3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + packages/react-native: dependencies: '@react-native-async-storage/async-storage': @@ -6714,6 +6751,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -7574,6 +7615,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -8356,6 +8400,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} @@ -8376,6 +8423,13 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} @@ -8872,6 +8926,9 @@ packages: help-me@3.0.0: resolution: {integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hermes-estree@0.19.1: resolution: {integrity: sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==} @@ -10822,6 +10879,10 @@ packages: resolution: {integrity: sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==} engines: {node: ^10.13.0 || >=12.0.0} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -11157,6 +11218,20 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + + pino-pretty@10.3.1: + resolution: {integrity: sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==} + hasBin: true + + pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + + pino@8.21.0: + resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -11467,6 +11542,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -11565,6 +11643,9 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -11837,6 +11918,10 @@ packages: readline@1.3.0: resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recast@0.21.5: resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} engines: {node: '>= 4'} @@ -12083,6 +12168,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -12316,6 +12405,9 @@ packages: resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonic-boom@3.8.1: + resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} + sort-object-keys@1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} @@ -12778,6 +12870,9 @@ packages: resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} engines: {node: '>=0.2.6'} + thread-stream@2.7.0: + resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} + throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -21634,6 +21729,8 @@ snapshots: at-least-node@1.0.0: {} + atomic-sleep@1.0.0: {} + autoprefixer@10.4.20(postcss@8.4.49): dependencies: browserslist: 4.24.4 @@ -22645,6 +22742,8 @@ snapshots: date-fns@4.1.0: {} + dateformat@4.6.3: {} + dayjs@1.11.13: {} de-indent@1.0.2: {} @@ -23705,6 +23804,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-copy@3.0.2: {} + fast-deep-equal@2.0.1: {} fast-deep-equal@3.1.3: {} @@ -23729,6 +23830,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + fast-shallow-equal@1.0.0: {} fast-uri@3.0.6: {} @@ -24302,6 +24407,8 @@ snapshots: glob: 7.2.3 readable-stream: 3.6.2 + help-me@5.0.0: {} + hermes-estree@0.19.1: {} hermes-estree@0.23.1: {} @@ -26670,6 +26777,8 @@ snapshots: oidc-token-hash@5.1.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -27012,6 +27121,44 @@ snapshots: pify@4.0.1: {} + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.7.0 + split2: 4.2.0 + + pino-pretty@10.3.1: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pump: 3.0.2 + readable-stream: 4.7.0 + secure-json-parse: 2.7.0 + sonic-boom: 3.8.1 + strip-json-comments: 3.1.1 + + pino-std-serializers@6.2.2: {} + + pino@8.21.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 3.8.1 + thread-stream: 2.7.0 + pirates@4.0.6: {} pkg-dir@3.0.0: @@ -27255,6 +27402,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@3.0.0: {} + process@0.11.10: {} progress@2.0.3: {} @@ -27356,6 +27505,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -27766,6 +27917,8 @@ snapshots: readline@1.3.0: {} + real-require@0.2.0: {} + recast@0.21.5: dependencies: ast-types: 0.15.2 @@ -28077,6 +28230,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} satori@0.12.0: @@ -28405,6 +28560,10 @@ snapshots: smart-buffer: 4.2.0 optional: true + sonic-boom@3.8.1: + dependencies: + atomic-sleep: 1.0.0 + sort-object-keys@1.1.3: {} sort-package-json@2.15.1: @@ -28922,6 +29081,10 @@ snapshots: thirty-two@1.0.2: {} + thread-stream@2.7.0: + dependencies: + real-require: 0.2.0 + throat@5.0.0: {} throttle-debounce@3.0.1: {} @@ -29535,6 +29698,25 @@ snapshots: - rollup - supports-color + vite-plugin-dts@4.3.0(@types/node@22.10.2)(rollup@4.36.0)(typescript@5.8.2)(vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@microsoft/api-extractor': 7.52.1(@types/node@22.10.2) + '@rollup/pluginutils': 5.1.4(rollup@4.36.0) + '@volar/typescript': 2.4.12 + '@vue/language-core': 2.1.6(typescript@5.8.2) + compare-versions: 6.1.1 + debug: 4.4.0 + kolorist: 1.8.0 + local-pkg: 0.5.1 + magic-string: 0.30.17 + typescript: 5.8.2 + optionalDependencies: + vite: 6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-node-polyfills@0.22.0(rollup@4.36.0)(vite@6.0.9(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.36.0) diff --git a/turbo.json b/turbo.json index 6d0ab28b60..de4e5ca8b8 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,9 @@ "dependsOn": ["@formbricks/api#build"], "persistent": true }, + "@formbricks/database#lint": { + "dependsOn": ["@formbricks/logger#build"] + }, "@formbricks/database#setup": { "dependsOn": ["db:up"] }, @@ -37,6 +40,7 @@ "@formbricks/js#lint": { "dependsOn": ["@formbricks/js-core#build"] }, + "@formbricks/js-core#go": { "cache": false, "dependsOn": ["@formbricks/database#db:setup", "@formbricks/api#build", "@formbricks/js#build"], @@ -48,6 +52,12 @@ "@formbricks/js-core#test": { "dependsOn": ["@formbricks/api#build"] }, + "@formbricks/lib#lint": { + "dependsOn": ["@formbricks/logger#build"] + }, + "@formbricks/lib#test": { + "dependsOn": ["@formbricks/logger#build"] + }, "@formbricks/react-native#build": { "dependsOn": ["^build"], "outputs": ["dist/**"] @@ -81,6 +91,12 @@ "dependsOn": ["@formbricks/database#db:setup", "@formbricks/js#build"], "persistent": true }, + "@formbricks/web#test": { + "dependsOn": ["@formbricks/logger#build"] + }, + "@formbricks/web#test:coverage": { + "dependsOn": ["@formbricks/logger#build"] + }, "build": { "dependsOn": ["^build"], "env": [ @@ -131,6 +147,7 @@ "IS_FORMBRICKS_CLOUD", "INTERCOM_APP_ID", "INTERCOM_SECRET_KEY", + "LOG_LEVEL", "MAIL_FROM", "MAIL_FROM_NAME", "NEXT_PUBLIC_LAYER_API_KEY", @@ -232,6 +249,7 @@ }, "db:setup": { "cache": false, + "dependsOn": ["@formbricks/logger#build"], "outputs": [] }, "db:start": {