feat: adds configurable logging (#4914)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2025-03-21 18:39:13 +05:30
committed by GitHub
parent 7dd5cf8b6e
commit 5527f184b7
150 changed files with 1167 additions and 292 deletions

View File

@@ -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 #
##############

View File

@@ -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:

View File

@@ -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

View File

@@ -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
}
};

View File

@@ -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();

View File

@@ -1,3 +1,5 @@
import { logger } from "@formbricks/logger";
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
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();

View File

@@ -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<TSurvey>(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;

View File

@@ -1,3 +1,5 @@
import { logger } from "@formbricks/logger";
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
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();

View File

@@ -1,3 +1,5 @@
import { logger } from "@formbricks/logger";
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
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();

View File

@@ -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<Metadata> => {
@@ -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();
}

View File

@@ -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");
}

View File

@@ -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;
}
};

View File

@@ -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),

View File

@@ -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!");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
}
});
}

View File

@@ -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);
}
};

View File

@@ -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);
}

View File

@@ -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<Response
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error in POST /api/v1/client/[environmentId]/displays");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -15,6 +15,7 @@ import {
} from "@formbricks/lib/posthogServer";
import { projectCache } from "@formbricks/lib/project/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TJsEnvironmentState } from "@formbricks/types/js";
import { getActionClassesForEnvironmentState } from "./actionClass";
@@ -89,7 +90,7 @@ export const getEnvironmentState = async (
},
});
} catch (err) {
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}

View File

@@ -4,6 +4,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";
import { TJsEnvironmentStateProject } from "@formbricks/types/js";
@@ -35,7 +36,7 @@ export const getProjectForEnvironmentState = reactCache(
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error(error, "Error getting project for environment state");
throw new DatabaseError(error.message);
}
throw error;

View File

@@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { surveyCache } from "@formbricks/lib/survey/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 { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
@@ -80,7 +81,7 @@ export const getSurveysForEnvironmentState = reactCache(
return surveysPrisma.map((survey) => transformPrismaSurvey<TJsEnvironmentStateSurvey>(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;

View File

@@ -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<Response> => {
};
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);
}
};

View File

@@ -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);
}
}

View File

@@ -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<TRe
});
} catch (err) {
// Log error but do not throw
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
}

View File

@@ -6,6 +6,7 @@ import { headers } from "next/headers";
import { UAParser } from "ua-parser-js";
import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
@@ -107,7 +108,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error creating response");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -9,6 +9,7 @@ import { validateLocalSignedUrl } from "@formbricks/lib/crypto";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { putFileToLocalStorage } from "@formbricks/lib/storage/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
interface Context {
params: Promise<{
@@ -125,7 +126,7 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
message: "File uploaded successfully",
});
} catch (err) {
console.error("err: ", err);
logger.error({ error: err, url: req.url }, "Error in POST /api/v1/client/[environmentId]/upload");
if (err.name === "FileTooLargeError") {
return responses.badRequestResponse(err.message);
}

View File

@@ -7,6 +7,7 @@ import { fetchAirtableAuthToken } from "@formbricks/lib/airtable/service";
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { createOrUpdateIntegration } from "@formbricks/lib/integration/service";
import { logger } from "@formbricks/logger";
const getEmail = async (token: string) => {
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");

View File

@@ -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");
}

View File

@@ -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<Response> => {
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");
}

View File

@@ -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<TResponse> => {
@@ -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");
}

View File

@@ -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<TRe
});
} catch (err) {
// Log error but do not throw
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
}

View File

@@ -4,6 +4,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { NextRequest } from "next/server";
import { getResponses, getResponsesByEnvironmentId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
import { createResponse } from "./lib/response";
@@ -45,7 +46,7 @@ export const POST = async (request: Request): Promise<Response> => {
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<Response> => {
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);
}
}

View File

@@ -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<Response> => {
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");
}

View File

@@ -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);
}

View File

@@ -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<TSurvey | null> => {
@@ -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");
}

View File

@@ -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<Response> => {
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");
}

View File

@@ -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);
}
};

View File

@@ -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<Response
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error creating display");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -14,6 +14,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, ZResponseInput } from "@formbricks/types/responses";
@@ -129,7 +130,7 @@ export const createResponse = async (responseInput: TResponseInputV2): Promise<T
});
} catch (err) {
// Log error but do not throw
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
}

View File

@@ -6,6 +6,7 @@ import { headers } from "next/headers";
import { UAParser } from "ua-parser-js";
import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponse } from "@formbricks/types/responses";
@@ -108,7 +109,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error creating response");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -1,5 +1,6 @@
import { TPipelineInput } from "@/app/lib/types/pipelines";
import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { logger } from "@formbricks/logger";
export const sendToPipeline = async ({ event, surveyId, environmentId, response }: TPipelineInput) => {
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");
});
};

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
},
});

View File

@@ -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({

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -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;
});
});
});

View File

@@ -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");
};

View File

@@ -75,7 +75,6 @@ export const apiWrapper = async <S extends ExtendedSchemas>({
if (schemas?.params) {
const paramsObject = (await externalParams) || {};
console.log("paramsObject: ", paramsObject);
const paramsResult = schemas.params.safeParse(paramsObject);
if (!paramsResult.success) {
throw err({

View File

@@ -14,8 +14,6 @@ export const authenticatedApiClient = async <S extends ExtendedSchemas>({
rateLimit?: boolean;
handler: HandlerFn<ParsedSchemas<S>>;
}): Promise<Response> => {
const startTime = Date.now();
const response = await apiWrapper({
request,
schemas,
@@ -23,10 +21,9 @@ export const authenticatedApiClient = async <S extends ExtendedSchemas>({
rateLimit,
handler,
});
const duration = Date.now() - startTime;
logApiRequest(request, response.status, duration);
if (response.ok) {
logApiRequest(request, response.status);
}
return response;
};

View File

@@ -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 () => {

View File

@@ -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");
}
});

View File

@@ -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");
}
}
}

View File

@@ -80,4 +80,5 @@ const document = createDocument({
],
});
// do not replace this with logger.info
console.log(yaml.stringify(document));

View File

@@ -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) => {
</ContentLayout>
);
} catch (e) {
console.error(e);
logger.error(e, "Error in InvitePage");
return (
<ContentLayout
headline={t("auth.invite.invite_not_found")}

View File

@@ -12,6 +12,7 @@ import {
} from "@formbricks/lib/constants";
import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto";
import { verifyToken } from "@formbricks/lib/jwt";
import { logger } from "@formbricks/logger";
import { TUser } from "@formbricks/types/user";
import { createBrevoCustomer } from "./brevo";
@@ -51,7 +52,7 @@ export const authOptions: NextAuthOptions = {
},
});
} catch (e) {
console.error(e);
logger.error(e, "Error in CredentialsProvider authorize");
throw Error("Internal server error. Please try again later");
}
if (!user) {
@@ -69,7 +70,7 @@ export const authOptions: NextAuthOptions = {
if (user.twoFactorEnabled && credentials.backupCode) {
if (!ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
logger.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error("Internal Server Error");
}

View File

@@ -1,6 +1,7 @@
import { Response } from "node-fetch";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { createBrevoCustomer } from "./brevo";
vi.mock("@formbricks/lib/constants", () => ({
@@ -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");
});
});

View File

@@ -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");
}
};

View File

@@ -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");
}
};

View File

@@ -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");
});
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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}` };
}

View File

@@ -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");
}

View File

@@ -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" };
}

View File

@@ -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 });
}

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -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");
}

View File

@@ -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<Response> => {
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");
}

View File

@@ -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}`);
}

View File

@@ -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<Insight>
}
}
} catch (error) {
console.error("Error in updateInsight:", error);
logger.error(error, "Error in updateInsight");
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}

View File

@@ -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;

View File

@@ -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;
}
},

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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");
}

View File

@@ -14,7 +14,6 @@ interface FollowUpEmailProps {
export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Promise<React.JSX.Element> {
const t = await getTranslate();
console.log(t("emails.imprint"));
const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl;
return (

View File

@@ -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<boolean>
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
}
};

View File

@@ -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);
}

View File

@@ -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<TProject> => {
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<TProject> => {
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");
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<TSurvey> =>
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<TSurvey> =>
return modifiedSurvey;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error(error, "Error updating survey");
throw new DatabaseError(error.message);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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<boolean> => {
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);
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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: [

View File

@@ -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",

View File

@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More