From 5527f184b78079fd9a9f42fe210b8980caf94a73 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Fri, 21 Mar 2025 18:39:13 +0530 Subject: [PATCH 01/54] feat: adds configurable logging (#4914) Co-authored-by: Matthias Nannt Co-authored-by: pandeymangg --- .env.example | 3 + .github/workflows/sonarqube.yml | 4 - apps/web/Dockerfile | 10 +- .../xm-templates/lib/xm-templates.ts | 9 +- .../integrations/airtable/lib/airtable.ts | 4 +- .../integrations/google-sheets/lib/google.ts | 5 +- .../integrations/lib/surveys.ts | 3 +- .../integrations/notion/lib/notion.ts | 5 +- .../integrations/slack/lib/slack.ts | 5 +- apps/web/app/[shortUrlId]/page.tsx | 3 +- .../api/(internal)/csv-conversion/route.ts | 3 +- .../app/api/(internal)/insights/lib/utils.ts | 3 +- apps/web/app/api/(internal)/insights/route.ts | 3 +- .../pipeline/lib/handleIntegrations.ts | 11 +- .../pipeline/lib/survey-follow-up.ts | 3 +- apps/web/app/api/(internal)/pipeline/route.ts | 21 +- .../app/sync/[userId]/route.ts | 8 +- .../[environmentId]/app/sync/lib/survey.ts | 3 +- .../client/[environmentId]/displays/route.ts | 3 +- .../environment/lib/environmentState.ts | 3 +- .../environment/lib/project.ts | 3 +- .../[environmentId]/environment/lib/survey.ts | 3 +- .../[environmentId]/environment/route.ts | 10 +- .../responses/[responseId]/route.ts | 11 +- .../[environmentId]/responses/lib/response.ts | 3 +- .../client/[environmentId]/responses/route.ts | 3 +- .../[environmentId]/storage/local/route.ts | 3 +- .../integrations/airtable/callback/route.ts | 3 +- .../action-classes/[actionClassId]/route.ts | 3 +- .../api/v1/management/action-classes/route.ts | 3 +- .../responses/[responseId]/route.ts | 3 +- .../v1/management/responses/lib/response.ts | 3 +- .../app/api/v1/management/responses/route.ts | 5 +- .../app/api/v1/management/storage/route.ts | 3 +- .../surveys/[surveyId]/lib/surveys.ts | 3 +- .../v1/management/surveys/[surveyId]/route.ts | 3 +- .../app/api/v1/management/surveys/route.ts | 3 +- .../app/api/v1/webhooks/[webhookId]/route.ts | 5 +- .../client/[environmentId]/displays/route.ts | 3 +- .../[environmentId]/responses/lib/response.ts | 3 +- .../client/[environmentId]/responses/route.ts | 3 +- apps/web/app/lib/pipelines.ts | 3 +- apps/web/app/middleware/rate-limit.ts | 4 +- apps/web/instrumentation-node.ts | 3 +- apps/web/lib/utils/action-client.ts | 3 +- apps/web/middleware.ts | 1 + apps/web/modules/api/v2/lib/rate-limit.ts | 3 +- .../api/v2/lib/tests/rate-limit.test.ts | 21 +- .../modules/api/v2/lib/tests/utils.test.ts | 102 ++++++-- apps/web/modules/api/v2/lib/utils.ts | 27 ++- .../api/v2/management/auth/api-wrapper.ts | 1 - .../auth/authenticated-api-client.ts | 9 +- .../[responseId]/lib/tests/utils.test.ts | 13 +- .../responses/[responseId]/lib/utils.ts | 3 +- .../v2/management/responses/lib/response.ts | 3 +- apps/web/modules/api/v2/openapi-document.ts | 1 + apps/web/modules/auth/invite/page.tsx | 3 +- apps/web/modules/auth/lib/authOptions.ts | 5 +- apps/web/modules/auth/lib/brevo.test.ts | 9 +- apps/web/modules/auth/lib/brevo.ts | 6 +- .../ee/auth/saml/lib/preload-connection.ts | 5 +- .../saml/lib/tests/preload-connection.test.ts | 15 +- .../ee/billing/api/lib/create-subscription.ts | 3 +- .../api/lib/is-subscription-cancelled.ts | 3 +- .../ee/billing/api/lib/stripe-webhook.ts | 3 +- .../lib/subscription-created-or-updated.ts | 9 +- .../billing/api/lib/subscription-deleted.ts | 3 +- .../contacts/[userId]/attributes/route.ts | 3 +- .../identify/contacts/[userId]/route.ts | 5 +- .../api/client/[environmentId]/user/route.ts | 5 +- .../[contactAttributeKeyId]/route.ts | 3 +- .../contact-attribute-keys/route.ts | 3 +- .../modules/ee/insights/experience/actions.ts | 13 +- .../ee/insights/experience/lib/insights.ts | 3 +- .../ee/insights/experience/lib/stats.ts | 3 +- .../web/modules/ee/license-check/lib/utils.ts | 9 +- apps/web/modules/ee/teams/lib/roles.ts | 3 +- .../modules/ee/teams/team-list/lib/project.ts | 3 +- .../whitelabel/remove-branding/lib/project.ts | 3 +- .../modules/email/emails/survey/follow-up.tsx | 1 - apps/web/modules/email/index.tsx | 5 +- .../settings/teams/lib/membership.ts | 3 +- .../modules/projects/settings/lib/project.ts | 7 +- .../projects/settings/look/lib/project.ts | 3 +- .../components/template-list/lib/survey.ts | 3 +- apps/web/modules/survey/editor/lib/project.ts | 3 +- apps/web/modules/survey/editor/lib/survey.ts | 5 +- apps/web/modules/survey/lib/environment.ts | 3 +- apps/web/modules/survey/lib/membership.ts | 3 +- apps/web/modules/survey/lib/project.ts | 3 +- apps/web/modules/survey/link/lib/project.ts | 3 +- apps/web/modules/survey/link/lib/survey.ts | 3 +- .../modules/survey/list/lib/environment.ts | 3 +- apps/web/modules/survey/list/lib/project.ts | 3 +- apps/web/modules/survey/list/lib/survey.ts | 13 +- .../modules/survey/templates/lib/survey.ts | 3 +- .../ui/components/input-combo-box/stories.ts | 3 +- apps/web/next.config.mjs | 7 +- apps/web/package.json | 1 + .../api/management/responses.spec.ts | 3 +- .../playwright/api/management/survey.spec.ts | 3 +- apps/web/playwright/utils/helper.ts | 3 +- apps/web/scripts/merge-client-endpoints.ts | 5 +- docker/docker-compose.yml | 4 +- .../configuration/environment-variables.mdx | 1 + package.json | 1 + .../migration.ts | 18 +- .../migration.ts | 3 +- .../migration.ts | 3 +- .../migration.ts | 5 +- .../migration.ts | 5 +- .../migration.ts | 5 +- packages/database/package.json | 1 + .../database/src/scripts/apply-migrations.ts | 3 +- .../database/src/scripts/create-migration.ts | 11 +- .../src/scripts/create-saml-database.ts | 7 +- .../src/scripts/generate-data-migration.ts | 15 +- .../database/src/scripts/migration-runner.ts | 37 +-- packages/database/tsconfig.json | 4 + packages/lib/airtable/service.ts | 3 +- packages/lib/env.ts | 2 + packages/lib/environment/service.ts | 5 +- packages/lib/integration/service.ts | 3 +- packages/lib/jwt.ts | 3 +- packages/lib/language/service.ts | 11 +- packages/lib/membership/service.ts | 3 +- packages/lib/organization/service.ts | 3 +- packages/lib/package.json | 1 + packages/lib/posthogServer.ts | 5 +- packages/lib/project/service.ts | 3 +- packages/lib/response/service.ts | 3 +- packages/lib/responseNote/service.ts | 11 +- packages/lib/storage/service.ts | 3 +- packages/lib/storage/utils.ts | 6 +- packages/lib/survey/service.ts | 16 +- packages/lib/telemetry.ts | 3 +- packages/lib/utils/fileConversion.ts | 3 +- packages/lib/utils/validate.ts | 6 +- packages/logger/.eslintrc.cjs | 7 + packages/logger/.gitignore | 4 + packages/logger/package.json | 41 ++++ packages/logger/src/index.ts | 1 + packages/logger/src/logger.test.ts | 224 ++++++++++++++++++ packages/logger/src/logger.ts | 111 +++++++++ packages/logger/src/vite-env.d.ts | 1 + packages/logger/tsconfig.json | 12 + packages/logger/types/logger.ts | 7 + packages/logger/vite.config.ts | 32 +++ pnpm-lock.yaml | 182 ++++++++++++++ turbo.json | 18 ++ 150 files changed, 1167 insertions(+), 292 deletions(-) create mode 100644 packages/logger/.eslintrc.cjs create mode 100644 packages/logger/.gitignore create mode 100644 packages/logger/package.json create mode 100644 packages/logger/src/index.ts create mode 100644 packages/logger/src/logger.test.ts create mode 100644 packages/logger/src/logger.ts create mode 100644 packages/logger/src/vite-env.d.ts create mode 100644 packages/logger/tsconfig.json create mode 100644 packages/logger/types/logger.ts create mode 100644 packages/logger/vite.config.ts diff --git a/.env.example b/.env.example index b4f51e2def..a90fd8b07c 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,9 @@ NEXTAUTH_SECRET= # You can use: `openssl rand -hex 32` to generate a secure one CRON_SECRET= +# Set the minimum log level(debug, info, warn, error, fatal) +LOG_LEVEL=info + ############## # DATABASE # ############## diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 65d26fe7b7..b7dfe776df 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -46,11 +46,7 @@ jobs: - name: Run tests with coverage run: | - cd apps/web pnpm test:coverage - cd ../../ - # The Vitest coverage config is in your vite.config.mts - - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 env: diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index b16f185c9f..cf30c09ef2 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -85,6 +85,8 @@ COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src +COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules +COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist # Copy Prisma-specific generated files COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client @@ -93,14 +95,16 @@ COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_mod COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt . COPY /docker/cronjobs /app/docker/cronjobs -# Copy only @paralleldrive/cuid2 and @noble/hashes +# Copy required dependencies COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2 COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes +COPY --from=installer /app/node_modules/zod ./node_modules/zod -RUN npm install -g tsx typescript prisma +RUN npm install -g tsx typescript prisma pino-pretty EXPOSE 3000 ENV HOSTNAME "0.0.0.0" +ENV NODE_ENV="production" # USER nextjs # Prepare volume for uploads @@ -119,4 +123,4 @@ CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \ fi; \ (cd packages/database && npm run db:migrate:deploy) && \ (cd packages/database && npm run db:create-saml-database:deploy) && \ - exec node apps/web/server.js + exec node apps/web/server.js \ No newline at end of file diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts index c6f5e0c82b..fa11c26f11 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts @@ -1,13 +1,10 @@ import { getDefaultEndingCard } from "@/app/lib/templates"; import { createId } from "@paralleldrive/cuid2"; import { TFnType } from "@tolgee/react"; +import { logger } from "@formbricks/logger"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TXMTemplate } from "@formbricks/types/templates"; -function logError(error: Error, context: string) { - console.error(`Error in ${context}:`, error); -} - export const getXMSurveyDefault = (t: TFnType): TXMTemplate => { try { return { @@ -19,7 +16,7 @@ export const getXMSurveyDefault = (t: TFnType): TXMTemplate => { }, }; } catch (error) { - logError(error, "getXMSurveyDefault"); + logger.error(error, "Failed to create default XM survey template"); throw error; // Re-throw after logging } }; @@ -449,7 +446,7 @@ export const getXMTemplates = (t: TFnType): TXMTemplate[] => { enpsSurvey(t), ]; } catch (error) { - logError(error, "getXMTemplates"); + logger.error(error, "Unable to load XM templates, returning empty array"); return []; // Return an empty array or handle as needed } }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts index cbeff6da6c..9604a13f12 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable"; export const fetchTables = async (environmentId: string, baseId: string) => { @@ -17,7 +18,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch airtable config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts index dd7d6b03b4..267d4fed7a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const authorize = async (environmentId: string, apiHost: string): Promise => { const res = await fetch(`${apiHost}/api/google-sheet`, { method: "GET", @@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch google sheet config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts index 3039099837..7e9127267a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts @@ -7,6 +7,7 @@ import { surveyCache } from "@formbricks/lib/survey/cache"; import { selectSurvey } from "@formbricks/lib/survey/service"; import { transformPrismaSurvey } from "@formbricks/lib/survey/utils"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -34,7 +35,7 @@ export const getSurveys = reactCache( return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey(surveyPrisma)); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error({ error }, "getSurveys: Could not fetch surveys"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts index 5eab694413..2aaec82d5e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/lib/notion.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const authorize = async (environmentId: string, apiHost: string): Promise => { const res = await fetch(`${apiHost}/api/v1/integrations/notion`, { method: "GET", @@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch notion config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts index 252bd36bac..74f7edb2f3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/lib/slack.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const authorize = async (environmentId: string, apiHost: string): Promise => { const res = await fetch(`${apiHost}/api/v1/integrations/slack`, { method: "GET", @@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise }); if (!res.ok) { - console.error(res.text); + const errorText = await res.text(); + logger.error({ errorText }, "authorize: Could not fetch slack config"); throw new Error("Could not create response"); } const resJSON = await res.json(); diff --git a/apps/web/app/[shortUrlId]/page.tsx b/apps/web/app/[shortUrlId]/page.tsx index eb26877f77..24894cc4ec 100644 --- a/apps/web/app/[shortUrlId]/page.tsx +++ b/apps/web/app/[shortUrlId]/page.tsx @@ -2,6 +2,7 @@ import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata"; import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; import { getShortUrl } from "@formbricks/lib/shortUrl/service"; +import { logger } from "@formbricks/logger"; import { TShortUrl, ZShortUrlId } from "@formbricks/types/short-url"; export const generateMetadata = async (props): Promise => { @@ -44,7 +45,7 @@ const Page = async (props) => { try { shortUrl = await getShortUrl(params.shortUrlId); } catch (error) { - console.error(error); + logger.error(error, "Could not fetch short url"); notFound(); } diff --git a/apps/web/app/api/(internal)/csv-conversion/route.ts b/apps/web/app/api/(internal)/csv-conversion/route.ts index 5e668022c9..0c823b4bc6 100755 --- a/apps/web/app/api/(internal)/csv-conversion/route.ts +++ b/apps/web/app/api/(internal)/csv-conversion/route.ts @@ -3,6 +3,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { AsyncParser } from "@json2csv/node"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; +import { logger } from "@formbricks/logger"; export const POST = async (request: NextRequest) => { const session = await getServerSession(authOptions); @@ -28,7 +29,7 @@ export const POST = async (request: NextRequest) => { try { csv = await parser.parse(json).promise(); } catch (err) { - console.error(err); + logger.error({ error: err, url: request.url }, "Failed to convert to CSV"); throw new Error("Failed to convert to CSV"); } diff --git a/apps/web/app/api/(internal)/insights/lib/utils.ts b/apps/web/app/api/(internal)/insights/lib/utils.ts index a4438acc40..53faeb9e0c 100644 --- a/apps/web/app/api/(internal)/insights/lib/utils.ts +++ b/apps/web/app/api/(internal)/insights/lib/utils.ts @@ -4,6 +4,7 @@ import { surveyCache } from "@formbricks/lib/survey/cache"; import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { doesSurveyHasOpenTextQuestion } from "@formbricks/lib/survey/utils"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse } from "@formbricks/types/responses"; @@ -80,7 +81,7 @@ export const generateInsightsEnabledForSurveyQuestions = async ( return { success: false }; } catch (error) { - console.error("Error generating insights for surveys:", error); + logger.error(error, "Error generating insights for surveys"); throw error; } }; diff --git a/apps/web/app/api/(internal)/insights/route.ts b/apps/web/app/api/(internal)/insights/route.ts index 0d7037a618..c4a2c8f47d 100644 --- a/apps/web/app/api/(internal)/insights/route.ts +++ b/apps/web/app/api/(internal)/insights/route.ts @@ -5,6 +5,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { headers } from "next/headers"; import { z } from "zod"; import { CRON_SECRET } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; import { generateInsightsEnabledForSurveyQuestions } from "./lib/utils"; export const maxDuration = 300; // This function can run for a maximum of 300 seconds @@ -25,7 +26,7 @@ export const POST = async (request: Request) => { const inputValidation = ZGenerateInsightsInput.safeParse(jsonInput); if (!inputValidation.success) { - console.error(inputValidation.error); + logger.error({ error: inputValidation.error, url: request.url }, "Error in POST /api/insights"); return responses.badRequestResponse( "Fields are missing or incorrectly formatted", transformErrorToDetails(inputValidation.error), diff --git a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts index aa0cacb4a8..5eea313aaa 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts @@ -9,6 +9,7 @@ import { writeDataToSlack } from "@formbricks/lib/slack/service"; import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime"; import { parseRecallInfo } from "@formbricks/lib/utils/recall"; import { truncateText } from "@formbricks/lib/utils/strings"; +import { logger } from "@formbricks/logger"; import { Result } from "@formbricks/types/error-handlers"; import { TIntegration, TIntegrationType } from "@formbricks/types/integration"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; @@ -83,13 +84,13 @@ export const handleIntegrations = async ( survey ); if (!googleResult.ok) { - console.error("Error in google sheets integration: ", googleResult.error); + logger.error(googleResult.error, "Error in google sheets integration"); } break; case "slack": const slackResult = await handleSlackIntegration(integration as TIntegrationSlack, data, survey); if (!slackResult.ok) { - console.error("Error in slack integration: ", slackResult.error); + logger.error(slackResult.error, "Error in slack integration"); } break; case "airtable": @@ -99,13 +100,13 @@ export const handleIntegrations = async ( survey ); if (!airtableResult.ok) { - console.error("Error in airtable integration: ", airtableResult.error); + logger.error(airtableResult.error, "Error in airtable integration"); } break; case "notion": const notionResult = await handleNotionIntegration(integration as TIntegrationNotion, data, survey); if (!notionResult.ok) { - console.error("Error in notion integration: ", notionResult.error); + logger.error(notionResult.error, "Error in notion integration"); } break; } @@ -418,7 +419,7 @@ const getValue = (colType: string, value: string | string[] | Date | number | Re return typeof value === "string" ? value : (value as string[]).join(", "); } } catch (error) { - console.error(error); + logger.error(error, "Payload build failed!"); throw new Error("Payload build failed!"); } }; diff --git a/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts b/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts index 240b0d09ff..e2d1115116 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts @@ -1,6 +1,7 @@ import { sendFollowUpEmail } from "@/modules/email"; import { z } from "zod"; import { TSurveyFollowUpAction } from "@formbricks/database/types/survey-follow-up"; +import { logger } from "@formbricks/logger"; import { TOrganization } from "@formbricks/types/organizations"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -89,6 +90,6 @@ export const sendSurveyFollowUps = async ( .map((result) => `FollowUp ${result.followUpId} failed: ${result.error}`); if (errors.length > 0) { - console.error("Follow-up processing errors:", errors); + logger.error(errors, "Follow-up processing errors"); } }; diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts index c2739e4877..e98ac5208a 100644 --- a/apps/web/app/api/(internal)/pipeline/route.ts +++ b/apps/web/app/api/(internal)/pipeline/route.ts @@ -19,6 +19,7 @@ import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { convertDatesInObject } from "@formbricks/lib/time"; import { getPromptText } from "@formbricks/lib/utils/ai"; import { parseRecallInfo } from "@formbricks/lib/utils/recall"; +import { logger } from "@formbricks/logger"; import { handleIntegrations } from "./lib/handleIntegrations"; export const POST = async (request: Request) => { @@ -34,7 +35,10 @@ export const POST = async (request: Request) => { const inputValidation = ZPipelineInput.safeParse(convertedJsonInput); if (!inputValidation.success) { - console.error(inputValidation.error); + logger.error( + { error: inputValidation.error, url: request.url }, + "Error in POST /api/(internal)/pipeline" + ); return responses.badRequestResponse( "Fields are missing or incorrectly formatted", transformErrorToDetails(inputValidation.error), @@ -87,7 +91,7 @@ export const POST = async (request: Request) => { data: response, }), }).catch((error) => { - console.error(`Webhook call to ${webhook.url} failed:`, error); + logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`); }) ); @@ -100,7 +104,7 @@ export const POST = async (request: Request) => { ]); if (!survey) { - console.error(`Survey with id ${surveyId} not found`); + logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`); return new Response("Survey not found", { status: 404 }); } @@ -172,7 +176,10 @@ export const POST = async (request: Request) => { const emailPromises = usersWithNotifications.map((user) => sendResponseFinishedEmail(user.email, environmentId, survey, response, responseCount).catch((error) => { - console.error(`Failed to send email to ${user.email}:`, error); + logger.error( + { error, url: request.url, userEmail: user.email }, + `Failed to send email to ${user.email}` + ); }) ); @@ -188,7 +195,7 @@ export const POST = async (request: Request) => { const results = await Promise.allSettled([...webhookPromises, ...emailPromises]); results.forEach((result) => { if (result.status === "rejected") { - console.error("Promise rejected:", result.reason); + logger.error({ error: result.reason, url: request.url }, "Promise rejected"); } }); @@ -228,7 +235,7 @@ export const POST = async (request: Request) => { text, }); } catch (e) { - console.error(e); + logger.error({ error: e, url: request.url }, "Error creating document and assigning insight"); } } } @@ -240,7 +247,7 @@ export const POST = async (request: Request) => { const results = await Promise.allSettled(webhookPromises); results.forEach((result) => { if (result.status === "rejected") { - console.error("Promise rejected:", result.reason); + logger.error({ error: result.reason, url: request.url }, "Promise rejected"); } }); } diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts index a0f90078ea..306a488ae5 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts @@ -21,6 +21,7 @@ import { } from "@formbricks/lib/posthogServer"; import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants"; +import { logger } from "@formbricks/logger"; import { ZJsPeopleUserIdInput } from "@formbricks/types/js"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -103,7 +104,7 @@ export const GET = async ( }, }); } catch (error) { - console.error(`Error sending plan limits reached event to Posthog: ${error}`); + logger.error({ error, url: request.url }, `Error sending plan limits reached event to Posthog`); } } } @@ -187,7 +188,10 @@ export const GET = async ( return responses.successResponse({ ...state }, true); } catch (error) { - console.error(error); + logger.error( + { error, url: request.url }, + "Error in GET /api/v1/client/[environmentId]/app/sync/[userId]" + ); return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true); } }; diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts index fe269b75a8..949c0d6ea1 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts @@ -14,6 +14,7 @@ import { getSurveys } from "@formbricks/lib/survey/service"; import { anySurveyHasFilters } from "@formbricks/lib/survey/utils"; import { diffInDays } from "@formbricks/lib/utils/datetime"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -150,7 +151,7 @@ export const getSyncSurveys = reactCache( return surveys; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error); throw new DatabaseError(error.message); } diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts index 12e526fb68..478ea47041 100644 --- a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts @@ -2,6 +2,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; +import { logger } from "@formbricks/logger"; import { ZDisplayCreateInput } from "@formbricks/types/displays"; import { InvalidInputError } from "@formbricks/types/errors"; import { createDisplay } from "./lib/display"; @@ -48,7 +49,7 @@ export const POST = async (request: Request, context: Context): Promise transformPrismaSurvey(survey)); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys for environment state"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts index a57d36109e..0f99348595 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts @@ -3,6 +3,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { NextRequest } from "next/server"; import { environmentCache } from "@formbricks/lib/environment/cache"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsSyncInput } from "@formbricks/types/js"; @@ -11,7 +12,7 @@ export const OPTIONS = async (): Promise => { }; export const GET = async ( - _: NextRequest, + request: NextRequest, props: { params: Promise<{ environmentId: string; @@ -58,11 +59,14 @@ export const GET = async ( return responses.notFoundResponse(err.resourceType, err.resourceId); } - console.error(err); + logger.error( + { error: err, url: request.url }, + "Error in GET /api/v1/client/[environmentId]/environment" + ); return responses.internalServerErrorResponse(err.message, true); } } catch (error) { - console.error(error); + logger.error({ error, url: request.url }, "Error in GET /api/v1/client/[environmentId]/environment"); return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true); } }; diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts index 94ee53a57c..bc54dcb4d7 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts @@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { sendToPipeline } from "@/app/lib/pipelines"; import { updateResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { ZResponseUpdateInput } from "@formbricks/types/responses"; @@ -45,7 +46,10 @@ export const PUT = async ( return responses.badRequestResponse(error.message); } if (error instanceof DatabaseError) { - console.error(error); + logger.error( + { error, url: request.url }, + "Error in PUT /api/v1/client/[environmentId]/responses/[responseId]" + ); return responses.internalServerErrorResponse(error.message); } } @@ -59,7 +63,10 @@ export const PUT = async ( return responses.badRequestResponse(error.message); } if (error instanceof DatabaseError) { - console.error(error); + logger.error( + { error, url: request.url }, + "Error in PUT /api/v1/client/[environmentId]/responses/[responseId]" + ); return responses.internalServerErrorResponse(error.message); } } diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts b/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts index 56ffb86327..d961371381 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts @@ -12,6 +12,7 @@ import { calculateTtcTotal } from "@formbricks/lib/response/utils"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { captureTelemetry } from "@formbricks/lib/telemetry"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses"; @@ -178,7 +179,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise { const req_ = await fetch("https://api.airtable.com/v0/meta/whoami", { @@ -77,7 +78,7 @@ export const GET = async (req: NextRequest) => { await createOrUpdateIntegration(environmentId, airtableIntegrationInput); return Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/airtable`); } catch (error) { - console.error(error); + logger.error({ error, url: req.url }, "Error in GET /api/v1/integrations/airtable/callback"); responses.internalServerErrorResponse(error); } responses.badRequestResponse("unknown error occurred"); diff --git a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts index c9ab0f9ba8..6811814800 100644 --- a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts +++ b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service"; +import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; @@ -54,7 +55,7 @@ export const PUT = async ( try { actionClassUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/action-classes/route.ts b/apps/web/app/api/v1/management/action-classes/route.ts index 6a032ee6e3..4bfc2922f8 100644 --- a/apps/web/app/api/v1/management/action-classes/route.ts +++ b/apps/web/app/api/v1/management/action-classes/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { createActionClass, getActionClasses } from "@formbricks/lib/actionClass/service"; +import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; import { DatabaseError } from "@formbricks/types/errors"; @@ -28,7 +29,7 @@ export const POST = async (request: Request): Promise => { try { actionClassInput = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 2eeefb829b..28f7c7d304 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -4,6 +4,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { TResponse, ZResponseUpdateInput } from "@formbricks/types/responses"; const fetchAndValidateResponse = async (authentication: any, responseId: string): Promise => { @@ -77,7 +78,7 @@ export const PUT = async ( try { responseUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/responses/lib/response.ts b/apps/web/app/api/v1/management/responses/lib/response.ts index 56ffb86327..d961371381 100644 --- a/apps/web/app/api/v1/management/responses/lib/response.ts +++ b/apps/web/app/api/v1/management/responses/lib/response.ts @@ -12,6 +12,7 @@ import { calculateTtcTotal } from "@formbricks/lib/response/utils"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { captureTelemetry } from "@formbricks/lib/telemetry"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses"; @@ -178,7 +179,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise => { try { jsonInput = await request.json(); } catch (err) { - console.error(`Error parsing JSON input: ${err}`); + logger.error({ error: err, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } @@ -92,7 +93,7 @@ export const POST = async (request: Request): Promise => { if (error instanceof InvalidInputError) { return responses.badRequestResponse(error.message); } else { - console.error(error); + logger.error({ error, url: request.url }, "Error in POST /api/v1/management/responses"); return responses.internalServerErrorResponse(error.message); } } diff --git a/apps/web/app/api/v1/management/storage/route.ts b/apps/web/app/api/v1/management/storage/route.ts index 1f0ecdc86b..9a5060b2be 100644 --- a/apps/web/app/api/v1/management/storage/route.ts +++ b/apps/web/app/api/v1/management/storage/route.ts @@ -3,6 +3,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { logger } from "@formbricks/logger"; import { getSignedUrlForPublicFile } from "./lib/getSignedUrl"; // api endpoint for uploading public files @@ -17,7 +18,7 @@ export const POST = async (req: NextRequest): Promise => { try { storageInput = await req.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: req.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts index bcf696a893..c70179f17b 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts @@ -5,6 +5,7 @@ import { segmentCache } from "@formbricks/lib/cache/segment"; import { responseCache } from "@formbricks/lib/response/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const deleteSurvey = async (surveyId: string) => { @@ -67,7 +68,7 @@ export const deleteSurvey = async (surveyId: string) => { return deletedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error({ error, surveyId }, "Error deleting survey"); throw new DatabaseError(error.message); } diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts index 4c44d923ad..34749304bc 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts @@ -6,6 +6,7 @@ import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { TSurvey, ZSurveyUpdateInput } from "@formbricks/types/surveys/types"; const fetchAndAuthorizeSurvey = async (authentication: any, surveyId: string): Promise => { @@ -79,7 +80,7 @@ export const PUT = async ( try { surveyUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts index 029c5c29fc..06ee2cee4d 100644 --- a/apps/web/app/api/v1/management/surveys/route.ts +++ b/apps/web/app/api/v1/management/surveys/route.ts @@ -5,6 +5,7 @@ import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { createSurvey, getSurveys } from "@formbricks/lib/survey/service"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { ZSurveyCreateInput } from "@formbricks/types/surveys/types"; @@ -41,7 +42,7 @@ export const POST = async (request: Request): Promise => { try { surveyInput = await request.json(); } catch (error) { - console.error(`Error parsing JSON: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index a5a9ed9f43..9dd12a4c47 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -2,6 +2,7 @@ import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key"; import { deleteWebhook, getWebhook } from "@/app/api/v1/webhooks/[webhookId]/lib/webhook"; import { responses } from "@/app/lib/api/response"; import { headers } from "next/headers"; +import { logger } from "@formbricks/logger"; export const GET = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => { const params = await props.params; @@ -26,7 +27,7 @@ export const GET = async (_: Request, props: { params: Promise<{ webhookId: stri return responses.successResponse(webhook); }; -export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => { +export const DELETE = async (request: Request, props: { params: Promise<{ webhookId: string }> }) => { const params = await props.params; const headersList = await headers(); const apiKey = headersList.get("x-api-key"); @@ -52,7 +53,7 @@ export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: s const webhook = await deleteWebhook(params.webhookId); return responses.successResponse(webhook); } catch (e) { - console.error(e.message); + logger.error({ error: e, url: request.url }, "Error deleting webhook"); return responses.notFoundResponse("Webhook", params.webhookId); } }; diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts index bddc7cb7de..f91d3f1347 100644 --- a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts @@ -3,6 +3,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; +import { logger } from "@formbricks/logger"; import { InvalidInputError } from "@formbricks/types/errors"; import { createDisplay } from "./lib/display"; @@ -48,7 +49,7 @@ export const POST = async (request: Request, context: Context): Promise { return fetch(`${WEBAPP_URL}/api/pipeline`, { @@ -15,6 +16,6 @@ export const sendToPipeline = async ({ event, surveyId, environmentId, response response, }), }).catch((error) => { - console.error(`Error sending event to pipeline: ${error}`); + logger.error(error, "Error sending event to pipeline"); }); }; diff --git a/apps/web/app/middleware/rate-limit.ts b/apps/web/app/middleware/rate-limit.ts index 4025ea63ff..4c9dc467a7 100644 --- a/apps/web/app/middleware/rate-limit.ts +++ b/apps/web/app/middleware/rate-limit.ts @@ -1,5 +1,6 @@ import { LRUCache } from "lru-cache"; import { ENTERPRISE_LICENSE_KEY, REDIS_HTTP_URL } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; interface Options { interval: number; @@ -28,8 +29,7 @@ const redisRateLimiter = (options: Options) => async (token: string) => { } const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`); if (!tokenCountResponse.ok) { - // eslint-disable-next-line no-console -- need for debugging - console.error("Failed to increment token count in Redis", tokenCountResponse); + logger.error({ tokenCountResponse }, "Failed to increment token count in Redis"); return; } diff --git a/apps/web/instrumentation-node.ts b/apps/web/instrumentation-node.ts index a1abee1ca5..55eeac233f 100644 --- a/apps/web/instrumentation-node.ts +++ b/apps/web/instrumentation-node.ts @@ -13,6 +13,7 @@ import { } from "@opentelemetry/resources"; import { MeterProvider } from "@opentelemetry/sdk-metrics"; import { env } from "@formbricks/lib/env"; +import { logger } from "@formbricks/logger"; const exporter = new PrometheusExporter({ port: env.PROMETHEUS_EXPORTER_PORT ? parseInt(env.PROMETHEUS_EXPORTER_PORT) : 9464, @@ -51,7 +52,7 @@ process.on("SIGTERM", async () => { await meterProvider.shutdown(); // Possibly close other instrumentation resources } catch (e) { - console.error("Error during graceful shutdown:", e); + logger.error(e, "Error during graceful shutdown"); } finally { process.exit(0); } diff --git a/apps/web/lib/utils/action-client.ts b/apps/web/lib/utils/action-client.ts index 97d073fa47..ba73be13de 100644 --- a/apps/web/lib/utils/action-client.ts +++ b/apps/web/lib/utils/action-client.ts @@ -2,6 +2,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from "next-safe-action"; import { getUser } from "@formbricks/lib/user/service"; +import { logger } from "@formbricks/logger"; import { AuthenticationError, AuthorizationError, @@ -25,7 +26,7 @@ export const actionClient = createSafeActionClient({ } // eslint-disable-next-line no-console -- This error needs to be logged for debugging server-side errors - console.error("SERVER ERROR: ", e); + logger.error(e, "SERVER ERROR"); return DEFAULT_SERVER_ERROR_MESSAGE; }, }); diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 080beca153..88a612cd33 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -85,6 +85,7 @@ export const middleware = async (originalRequest: NextRequest) => { }); request.headers.set("x-request-id", uuidv4()); + request.headers.set("x-start-time", Date.now().toString()); // Create a new NextResponse object to forward the new request with headers const nextResponseWithCustomHeader = NextResponse.next({ diff --git a/apps/web/modules/api/v2/lib/rate-limit.ts b/apps/web/modules/api/v2/lib/rate-limit.ts index 2747b447b6..2ca3d695eb 100644 --- a/apps/web/modules/api/v2/lib/rate-limit.ts +++ b/apps/web/modules/api/v2/lib/rate-limit.ts @@ -1,6 +1,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { type LimitOptions, Ratelimit, type RatelimitResponse } from "@unkey/ratelimit"; import { MANAGEMENT_API_RATE_LIMIT, RATE_LIMITING_DISABLED, UNKEY_ROOT_KEY } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; import { Result, err, okVoid } from "@formbricks/types/error-handlers"; export type RateLimitHelper = { @@ -18,7 +19,7 @@ let warningDisplayed = false; /** Prevent flooding the logs while testing/building */ function logOnce(message: string) { if (warningDisplayed) return; - console.warn(message); + logger.warn(message); warningDisplayed = true; } diff --git a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts index 7048ee1aa1..323854abc3 100644 --- a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts +++ b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts @@ -1,4 +1,11 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; +import { logger } from "@formbricks/logger"; + +vi.mock("@formbricks/logger", () => ({ + logger: { + warn: vi.fn(), + }, +})); vi.mock("@unkey/ratelimit", () => ({ Ratelimit: vi.fn(), @@ -16,18 +23,18 @@ describe("when rate limiting is disabled", () => { }); test("should log a warning once and return a stubbed response", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(logger, "warn"); const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit"); const res1 = await rateLimiter()({ identifier: "test-id" }); expect(res1).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 }); - expect(warnSpy).toHaveBeenCalledWith("Rate limiting disabled"); + expect(loggerSpy).toHaveBeenCalled(); // Subsequent calls won't log again. await rateLimiter()({ identifier: "another-id" }); - expect(warnSpy).toHaveBeenCalledTimes(1); - warnSpy.mockRestore(); + expect(loggerSpy).toHaveBeenCalledTimes(1); + loggerSpy.mockRestore(); }); }); @@ -44,14 +51,14 @@ describe("when UNKEY_ROOT_KEY is missing", () => { }); test("should log a warning about missing UNKEY_ROOT_KEY and return stub response", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(logger, "warn"); const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit"); const limiterFunc = rateLimiter(); const res = await limiterFunc({ identifier: "test-id" }); expect(res).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 }); - expect(warnSpy).toHaveBeenCalledWith("Disabled due to not finding UNKEY_ROOT_KEY env variable"); - warnSpy.mockRestore(); + expect(loggerSpy).toHaveBeenCalled(); + loggerSpy.mockRestore(); }); }); diff --git a/apps/web/modules/api/v2/lib/tests/utils.test.ts b/apps/web/modules/api/v2/lib/tests/utils.test.ts index ebcb82a9b8..0885a565cd 100644 --- a/apps/web/modules/api/v2/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/lib/tests/utils.test.ts @@ -1,6 +1,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { describe, expect, test, vi } from "vitest"; import { ZodError } from "zod"; +import { logger } from "@formbricks/logger"; import { formatZodError, handleApiError, logApiError, logApiRequest } from "../utils"; const mockRequest = new Request("http://localhost"); @@ -128,38 +129,77 @@ describe("utils", () => { describe("logApiRequest", () => { test("logs API request details", () => { - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Mock the withContext method and its returned info method + const infoMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + info: infoMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test?apikey=123&token=abc&safeParam=value"); mockRequest.headers.set("x-request-id", "123"); + mockRequest.headers.set("x-start-time", Date.now().toString()); - logApiRequest(mockRequest, 200, 100); + logApiRequest(mockRequest, 200); - expect(consoleLogSpy).toHaveBeenCalledWith( - `[API REQUEST DETAILS] GET /api/test - 200 - 100ms\n correlationId: 123\n queryParams: {"safeParam":"value"}` - ); + // Verify withContext was called + expect(withContextMock).toHaveBeenCalled(); + // Verify info was called on the child logger + expect(infoMock).toHaveBeenCalledWith("API Request Details"); - consoleLogSpy.mockRestore(); + // Restore the original method + logger.withContext = originalWithContext; }); test("logs API request details without correlationId and without safe query params", () => { - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Mock the withContext method and its returned info method + const infoMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + info: infoMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test?apikey=123&token=abc"); mockRequest.headers.delete("x-request-id"); + mockRequest.headers.set("x-start-time", (Date.now() - 100).toString()); - logApiRequest(mockRequest, 200, 100); - expect(consoleLogSpy).toHaveBeenCalledWith( - `[API REQUEST DETAILS] GET /api/test - 200 - 100ms\n queryParams: {}` + logApiRequest(mockRequest, 200); + + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: "GET", + path: "/api/test", + responseStatus: 200, + queryParams: {}, + }) ); - consoleLogSpy.mockRestore(); + // Verify info was called on the child logger + expect(infoMock).toHaveBeenCalledWith("API Request Details"); + + // Restore the original method + logger.withContext = originalWithContext; }); }); describe("logApiError", () => { test("logs API error details", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + // Mock the withContext method and its returned error method + const errorMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + error: errorMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test"); mockRequest.headers.set("x-request-id", "123"); @@ -171,15 +211,29 @@ describe("utils", () => { logApiError(mockRequest, error); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `[API ERROR DETAILS]\n correlationId: 123\n error: ${JSON.stringify(error, null, 2)}` - ); + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith({ + correlationId: "123", + error, + }); - consoleErrorSpy.mockRestore(); + // Verify error was called on the child logger + expect(errorMock).toHaveBeenCalledWith("API Error Details"); + + // Restore the original method + logger.withContext = originalWithContext; }); test("logs API error details without correlationId", () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + // Mock the withContext method and its returned error method + const errorMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + error: errorMock, + }); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; const mockRequest = new Request("http://localhost/api/test"); mockRequest.headers.delete("x-request-id"); @@ -191,11 +245,17 @@ describe("utils", () => { logApiError(mockRequest, error); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `[API ERROR DETAILS]\n error: ${JSON.stringify(error, null, 2)}` - ); + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith({ + correlationId: "", + error, + }); - consoleErrorSpy.mockRestore(); + // Verify error was called on the child logger + expect(errorMock).toHaveBeenCalledWith("API Error Details"); + + // Restore the original method + logger.withContext = originalWithContext; }); }); }); diff --git a/apps/web/modules/api/v2/lib/utils.ts b/apps/web/modules/api/v2/lib/utils.ts index 80f60e06a6..f429c70240 100644 --- a/apps/web/modules/api/v2/lib/utils.ts +++ b/apps/web/modules/api/v2/lib/utils.ts @@ -1,6 +1,7 @@ import { responses } from "@/modules/api/v2/lib/response"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { ZodError } from "zod"; +import { logger } from "@formbricks/logger"; export const handleApiError = (request: Request, err: ApiErrorResponseV2): Response => { logApiError(request, err); @@ -40,11 +41,12 @@ export const formatZodError = (error: ZodError) => { })); }; -export const logApiRequest = (request: Request, responseStatus: number, duration: number): void => { +export const logApiRequest = (request: Request, responseStatus: number): void => { const method = request.method; const url = new URL(request.url); const path = url.pathname; const correlationId = request.headers.get("x-request-id") || ""; + const startTime = request.headers.get("x-start-time") || ""; const queryParams = Object.fromEntries(url.searchParams.entries()); const sensitiveParams = ["apikey", "token", "secret"]; @@ -52,14 +54,25 @@ export const logApiRequest = (request: Request, responseStatus: number, duration Object.entries(queryParams).filter(([key]) => !sensitiveParams.includes(key.toLowerCase())) ); - console.log( - `[API REQUEST DETAILS] ${method} ${path} - ${responseStatus} - ${duration}ms${correlationId ? `\n correlationId: ${correlationId}` : ""}\n queryParams: ${JSON.stringify(safeQueryParams)}` - ); + // Info: Conveys general, operational messages about system progress and state. + logger + .withContext({ + method, + path, + responseStatus, + duration: `${Date.now() - parseInt(startTime)} ms`, + correlationId, + queryParams: safeQueryParams, + }) + .info("API Request Details"); }; export const logApiError = (request: Request, error: ApiErrorResponseV2): void => { const correlationId = request.headers.get("x-request-id") || ""; - console.error( - `[API ERROR DETAILS]${correlationId ? `\n correlationId: ${correlationId}` : ""}\n error: ${JSON.stringify(error, null, 2)}` - ); + logger + .withContext({ + correlationId, + error, + }) + .error("API Error Details"); }; diff --git a/apps/web/modules/api/v2/management/auth/api-wrapper.ts b/apps/web/modules/api/v2/management/auth/api-wrapper.ts index 16862ce1b2..0ff89bbafb 100644 --- a/apps/web/modules/api/v2/management/auth/api-wrapper.ts +++ b/apps/web/modules/api/v2/management/auth/api-wrapper.ts @@ -75,7 +75,6 @@ export const apiWrapper = async ({ if (schemas?.params) { const paramsObject = (await externalParams) || {}; - console.log("paramsObject: ", paramsObject); const paramsResult = schemas.params.safeParse(paramsObject); if (!paramsResult.success) { throw err({ diff --git a/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts b/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts index 9971582f32..1b2aff7c2b 100644 --- a/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts +++ b/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts @@ -14,8 +14,6 @@ export const authenticatedApiClient = async ({ rateLimit?: boolean; handler: HandlerFn>; }): Promise => { - const startTime = Date.now(); - const response = await apiWrapper({ request, schemas, @@ -23,10 +21,9 @@ export const authenticatedApiClient = async ({ rateLimit, handler, }); - - const duration = Date.now() - startTime; - - logApiRequest(request, response.status, duration); + if (response.ok) { + logApiRequest(request, response.status); + } return response; }; diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts index a5e3ad6488..b1908799b8 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts @@ -1,9 +1,16 @@ import { environmentId, fileUploadQuestion, openTextQuestion, responseData } from "./__mocks__/utils.mock"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { deleteFile } from "@formbricks/lib/storage/service"; +import { logger } from "@formbricks/logger"; import { okVoid } from "@formbricks/types/error-handlers"; import { findAndDeleteUploadedFilesInResponse } from "../utils"; +vi.mock("@formbricks/logger", () => ({ + logger: { + error: vi.fn(), + }, +})); + vi.mock("@formbricks/lib/storage/service", () => ({ deleteFile: vi.fn(), })); @@ -37,15 +44,15 @@ describe("findAndDeleteUploadedFilesInResponse", () => { [fileUploadQuestion.id]: [invalidFileUrl], }; - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(logger, "error"); const result = await findAndDeleteUploadedFilesInResponse(responseData, [fileUploadQuestion]); expect(deleteFile).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(loggerSpy).toHaveBeenCalled(); expect(result).toEqual(okVoid()); - consoleErrorSpy.mockRestore(); + loggerSpy.mockRestore(); }); test("process multiple file URLs", async () => { diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts index 005c9de21e..11655b2e09 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts @@ -1,6 +1,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Response, Survey } from "@prisma/client"; import { deleteFile } from "@formbricks/lib/storage/service"; +import { logger } from "@formbricks/logger"; import { Result, okVoid } from "@formbricks/types/error-handlers"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; @@ -26,7 +27,7 @@ export const findAndDeleteUploadedFilesInResponse = async ( } return deleteFile(environmentId, accessType as "private" | "public", fileName); } catch (error) { - console.error(`Failed to delete file ${fileUrl}:`, error); + logger.error({ error, fileUrl }, "Failed to delete file"); } }); diff --git a/apps/web/modules/api/v2/management/responses/lib/response.ts b/apps/web/modules/api/v2/management/responses/lib/response.ts index 0c1ccf841a..f48eb413d8 100644 --- a/apps/web/modules/api/v2/management/responses/lib/response.ts +++ b/apps/web/modules/api/v2/management/responses/lib/response.ts @@ -16,6 +16,7 @@ import { responseCache } from "@formbricks/lib/response/cache"; import { calculateTtcTotal } from "@formbricks/lib/response/utils"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { captureTelemetry } from "@formbricks/lib/telemetry"; +import { logger } from "@formbricks/logger"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const createResponse = async ( @@ -110,7 +111,7 @@ export const createResponse = async ( }); } catch (err) { // Log error but do not throw it - console.error(`Error sending plan limits reached event to Posthog: ${err}`); + logger.error(err, "Error sending plan limits reached event to Posthog"); } } } diff --git a/apps/web/modules/api/v2/openapi-document.ts b/apps/web/modules/api/v2/openapi-document.ts index 2c5b179450..5581b16549 100644 --- a/apps/web/modules/api/v2/openapi-document.ts +++ b/apps/web/modules/api/v2/openapi-document.ts @@ -80,4 +80,5 @@ const document = createDocument({ ], }); +// do not replace this with logger.info console.log(yaml.stringify(document)); diff --git a/apps/web/modules/auth/invite/page.tsx b/apps/web/modules/auth/invite/page.tsx index b569a2304d..b91402f5af 100644 --- a/apps/web/modules/auth/invite/page.tsx +++ b/apps/web/modules/auth/invite/page.tsx @@ -11,6 +11,7 @@ import { WEBAPP_URL } from "@formbricks/lib/constants"; import { verifyInviteToken } from "@formbricks/lib/jwt"; import { createMembership } from "@formbricks/lib/membership/service"; import { getUser, updateUser } from "@formbricks/lib/user/service"; +import { logger } from "@formbricks/logger"; import { ContentLayout } from "./components/content-layout"; interface InvitePageProps { @@ -131,7 +132,7 @@ export const InvitePage = async (props: InvitePageProps) => { ); } catch (e) { - console.error(e); + logger.error(e, "Error in InvitePage"); return ( ({ @@ -35,17 +36,17 @@ describe("createBrevoCustomer", () => { }); it("should log an error if fetch fails", async () => { - const consoleSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Fetch failed")); await createBrevoCustomer({ id: "123", email: "test@example.com" }); - expect(consoleSpy).toHaveBeenCalledWith("Error sending user to Brevo:", expect.any(Error)); + expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error sending user to Brevo"); }); it("should log the error response if fetch status is not 200", async () => { - const consoleSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); vi.mocked(global.fetch).mockResolvedValueOnce( new Response("Bad Request", { status: 400, statusText: "Bad Request" }) @@ -53,6 +54,6 @@ describe("createBrevoCustomer", () => { await createBrevoCustomer({ id: "123", email: "test@example.com" }); - expect(consoleSpy).toHaveBeenCalledWith("Error sending user to Brevo:", "Bad Request"); + expect(loggerSpy).toHaveBeenCalledWith({ errorText: "Bad Request" }, "Error sending user to Brevo"); }); }); diff --git a/apps/web/modules/auth/lib/brevo.ts b/apps/web/modules/auth/lib/brevo.ts index 4308f4857b..6fd9e4a06c 100644 --- a/apps/web/modules/auth/lib/brevo.ts +++ b/apps/web/modules/auth/lib/brevo.ts @@ -1,5 +1,6 @@ import { BREVO_API_KEY, BREVO_LIST_ID } from "@formbricks/lib/constants"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { TUserEmail, ZUserEmail } from "@formbricks/types/user"; @@ -34,9 +35,10 @@ export const createBrevoCustomer = async ({ id, email }: { id: string; email: TU }); if (res.status !== 200) { - console.error("Error sending user to Brevo:", await res.text()); + const errorText = await res.text(); + logger.error({ errorText }, "Error sending user to Brevo"); } } catch (error) { - console.error("Error sending user to Brevo:", error); + logger.error(error, "Error sending user to Brevo"); } }; diff --git a/apps/web/modules/ee/auth/saml/lib/preload-connection.ts b/apps/web/modules/ee/auth/saml/lib/preload-connection.ts index 1a7e7f8c17..5a140971a7 100644 --- a/apps/web/modules/ee/auth/saml/lib/preload-connection.ts +++ b/apps/web/modules/ee/auth/saml/lib/preload-connection.ts @@ -3,6 +3,7 @@ import { ConnectionAPIController } from "@boxyhq/saml-jackson/dist/controller/ap import fs from "fs/promises"; import path from "path"; import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; const getPreloadedConnectionFile = async () => { const preloadedConnections = await fs.readdir(path.join(SAML_XML_DIR)); @@ -41,7 +42,7 @@ export const preloadConnection = async (connectionController: ConnectionAPIContr const preloadedConnectionMetadata = await getPreloadedConnectionMetadata(); if (!preloadedConnectionMetadata) { - console.log("No preloaded connection metadata found"); + logger.info("No preloaded connection metadata found"); return; } @@ -68,6 +69,6 @@ export const preloadConnection = async (connectionController: ConnectionAPIContr }); } } catch (error) { - console.error("Error preloading connection:", error.message); + logger.error(error, "Error preloading connection"); } }; diff --git a/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts b/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts index 16663333e1..5bb8c60f45 100644 --- a/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts +++ b/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts @@ -2,6 +2,7 @@ import fs from "fs/promises"; import path from "path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@formbricks/lib/constants"; +import { logger } from "@formbricks/logger"; import { preloadConnection } from "../preload-connection"; vi.mock("@formbricks/lib/constants", () => ({ @@ -114,14 +115,11 @@ describe("SAML Preload Connection", () => { test("handle case when no XML files are found", async () => { vi.mocked(fs.readdir).mockResolvedValue(["other-file.txt"] as any); - const consoleErrorSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); await preloadConnection(mockConnectionController as any); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error preloading connection:", - expect.stringContaining("No preloaded connection file found") - ); + expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error preloading connection"); expect(mockConnectionController.createSAMLConnection).not.toHaveBeenCalled(); }); @@ -130,13 +128,10 @@ describe("SAML Preload Connection", () => { const errorMessage = "Invalid metadata"; mockConnectionController.createSAMLConnection.mockRejectedValue(new Error(errorMessage)); - const consoleErrorSpy = vi.spyOn(console, "error"); + const loggerSpy = vi.spyOn(logger, "error"); await preloadConnection(mockConnectionController as any); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error preloading connection:", - expect.stringContaining(errorMessage) - ); + expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error preloading connection"); }); }); diff --git a/apps/web/modules/ee/billing/api/lib/create-subscription.ts b/apps/web/modules/ee/billing/api/lib/create-subscription.ts index fdbe261836..64681c19e5 100644 --- a/apps/web/modules/ee/billing/api/lib/create-subscription.ts +++ b/apps/web/modules/ee/billing/api/lib/create-subscription.ts @@ -3,6 +3,7 @@ import { STRIPE_API_VERSION, WEBAPP_URL } from "@formbricks/lib/constants"; import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { apiVersion: STRIPE_API_VERSION, @@ -96,7 +97,7 @@ export const createSubscription = async ( url: "", }; } catch (err) { - console.error(err); + logger.error(err, "Error creating subscription"); return { status: 500, newPlan: true, diff --git a/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts b/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts index 1929054054..8f584ffb81 100644 --- a/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts +++ b/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts @@ -2,6 +2,7 @@ import Stripe from "stripe"; import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { apiVersion: STRIPE_API_VERSION, @@ -44,7 +45,7 @@ export const isSubscriptionCancelled = async ( date: null, }; } catch (err) { - console.error(err); + logger.error(err, "Error checking if subscription is cancelled"); return { cancelled: false, date: null, diff --git a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts index 948ce1f3f2..8103599f58 100644 --- a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts +++ b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts @@ -5,6 +5,7 @@ import { handleSubscriptionDeleted } from "@/modules/ee/billing/api/lib/subscrip import Stripe from "stripe"; import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; +import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { apiVersion: STRIPE_API_VERSION, @@ -19,7 +20,7 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin event = stripe.webhooks.constructEvent(requestBody, stripeSignature, webhookSecret); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; - if (err! instanceof Error) console.error(err); + if (err! instanceof Error) logger.error(err, "Error in Stripe webhook handler"); return { status: 400, message: `Webhook Error: ${errorMessage}` }; } diff --git a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts index 514b6b5564..11fd9c81f5 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts @@ -2,6 +2,7 @@ import Stripe from "stripe"; import { PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TOrganizationBillingPeriod, @@ -27,7 +28,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } if (!organizationId) { - console.error("No organizationId found in subscription"); + logger.error({ event, organizationId }, "No organizationId found in subscription"); return { status: 400, message: "skipping, no organizationId found" }; } @@ -60,7 +61,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } else if (parseInt(product.metadata.responses) > 0) { responses = parseInt(product.metadata.responses); } else { - console.error("Invalid responses metadata in product: ", product.metadata.responses); + logger.error({ responses: product.metadata.responses }, "Invalid responses metadata in product"); throw new Error("Invalid responses metadata in product"); } @@ -69,7 +70,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } else if (parseInt(product.metadata.miu) > 0) { miu = parseInt(product.metadata.miu); } else { - console.error("Invalid miu metadata in product: ", product.metadata.miu); + logger.error({ miu: product.metadata.miu }, "Invalid miu metadata in product"); throw new Error("Invalid miu metadata in product"); } @@ -78,7 +79,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => } else if (parseInt(product.metadata.projects) > 0) { projects = parseInt(product.metadata.projects); } else { - console.error("Invalid projects metadata in product: ", product.metadata.projects); + logger.error({ projects: product.metadata.projects }, "Invalid projects metadata in product"); throw new Error("Invalid projects metadata in product"); } diff --git a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts index d81299c4be..3b6af9e808 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts @@ -1,13 +1,14 @@ import Stripe from "stripe"; import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@formbricks/lib/constants"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; export const handleSubscriptionDeleted = async (event: Stripe.Event) => { const stripeSubscriptionObject = event.data.object as Stripe.Subscription; const organizationId = stripeSubscriptionObject.metadata.organizationId; if (!organizationId) { - console.error("No organizationId found in subscription"); + logger.error({ event, organizationId }, "No organizationId found in subscription"); return { status: 400, message: "skipping, no organizationId found" }; } diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts index a71891c8d3..797afcd152 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route.ts @@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { updateAttributes } from "@/modules/ee/contacts/lib/attributes"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { NextRequest } from "next/server"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsContactsUpdateAttributeInput } from "@formbricks/types/js"; import { getContactByUserIdWithAttributes } from "./lib/contact"; @@ -89,7 +90,7 @@ export const PUT = async ( true ); } catch (err) { - console.error(err); + logger.error({ err, url: req.url }, "Error updating attributes"); if (err.statusCode === 403) { return responses.forbiddenResponse(err.message || "Forbidden", true, { ignore: true }); } diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts index ea0bfaf3e2..ead57b3447 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route.ts @@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator"; import { contactCache } from "@/lib/cache/contact"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { NextRequest, userAgent } from "next/server"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsUserIdentifyInput } from "@formbricks/types/js"; import { getPersonState } from "./lib/personState"; @@ -62,11 +63,11 @@ export const GET = async ( return responses.notFoundResponse(err.resourceType, err.resourceId); } - console.error(err); + logger.error({ err, url: request.url }, "Error fetching person state"); return responses.internalServerErrorResponse(err.message ?? "Unable to fetch person state", true); } } catch (error) { - console.error(error); + logger.error({ error, url: request.url }, "Error fetching person state"); return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true); } }; diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts index db59c34ea2..c054e1eb06 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts @@ -2,6 +2,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { NextRequest, userAgent } from "next/server"; +import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TJsPersonState, ZJsUserIdentifyInput, ZJsUserUpdateInput } from "@formbricks/types/js"; @@ -94,11 +95,11 @@ export const POST = async ( return responses.notFoundResponse(err.resourceType, err.resourceId); } - console.error(err); + logger.error({ err, url: request.url }, "Error in POST /api/v1/client/[environmentId]/user"); return responses.internalServerErrorResponse(err.message ?? "Unable to fetch person state", true); } } catch (error) { - console.error(error); + logger.error({ error, url: request.url }, "Error in POST /api/v1/client/[environmentId]/user"); return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true); } }; diff --git a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts index f1145d6519..800442c8d2 100644 --- a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts +++ b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { logger } from "@formbricks/logger"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; import { @@ -109,7 +110,7 @@ export const PUT = async ( try { contactAttributeKeyUpdate = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts index 7cdbeeb8e1..34928ba0e8 100644 --- a/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts +++ b/apps/web/modules/ee/contacts/api/management/contact-attribute-keys/route.ts @@ -2,6 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { ZContactAttributeKeyCreateInput } from "./[contactAttributeKeyId]/types/contact-attribute-keys"; import { createContactAttributeKey, getContactAttributeKeys } from "./lib/contact-attribute-keys"; @@ -40,7 +41,7 @@ export const POST = async (request: Request): Promise => { try { contactAttibuteKeyInput = await request.json(); } catch (error) { - console.error(`Error parsing JSON input: ${error}`); + logger.error({ error, url: request.url }, "Error parsing JSON input"); return responses.badRequestResponse("Malformed JSON input, please check your request body"); } diff --git a/apps/web/modules/ee/insights/experience/actions.ts b/apps/web/modules/ee/insights/experience/actions.ts index 0e705b170a..4f50bc0a8d 100644 --- a/apps/web/modules/ee/insights/experience/actions.ts +++ b/apps/web/modules/ee/insights/experience/actions.ts @@ -12,6 +12,7 @@ import { checkAIPermission } from "@/modules/ee/insights/actions"; import { ZInsightFilterCriteria } from "@/modules/ee/insights/experience/types/insights"; import { z } from "zod"; import { ZInsight } from "@formbricks/database/zod/insights"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { getInsights, updateInsight } from "./lib/insights"; import { getStats } from "./lib/stats"; @@ -113,10 +114,14 @@ export const updateInsightAction = authenticatedActionClient return await updateInsight(parsedInput.insightId, parsedInput.data); } catch (error) { - console.error("Error updating insight:", { - insightId: parsedInput.insightId, - error, - }); + logger.error( + { + insightId: parsedInput.insightId, + error, + }, + "Error updating insight" + ); + if (error instanceof Error) { throw new Error(`Failed to update insight: ${error.message}`); } diff --git a/apps/web/modules/ee/insights/experience/lib/insights.ts b/apps/web/modules/ee/insights/experience/lib/insights.ts index 4fdcf8962b..ed26260036 100644 --- a/apps/web/modules/ee/insights/experience/lib/insights.ts +++ b/apps/web/modules/ee/insights/experience/lib/insights.ts @@ -11,6 +11,7 @@ import { cache } from "@formbricks/lib/cache"; import { INSIGHTS_PER_PAGE } from "@formbricks/lib/constants"; import { responseCache } from "@formbricks/lib/response/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId, ZOptionalNumber } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; @@ -121,7 +122,7 @@ export const updateInsight = async (insightId: string, updates: Partial } } } catch (error) { - console.error("Error in updateInsight:", error); + logger.error(error, "Error in updateInsight"); if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); } diff --git a/apps/web/modules/ee/insights/experience/lib/stats.ts b/apps/web/modules/ee/insights/experience/lib/stats.ts index dc9cbff9ea..f70872d452 100644 --- a/apps/web/modules/ee/insights/experience/lib/stats.ts +++ b/apps/web/modules/ee/insights/experience/lib/stats.ts @@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { responseCache } from "@formbricks/lib/response/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { TStats } from "../types/stats"; @@ -88,7 +89,7 @@ export const getStats = reactCache( }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching stats"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts index 88beff7ffd..c5ad8cfc8f 100644 --- a/apps/web/modules/ee/license-check/lib/utils.ts +++ b/apps/web/modules/ee/license-check/lib/utils.ts @@ -19,6 +19,7 @@ import { } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { hashString } from "@formbricks/lib/hashString"; +import { logger } from "@formbricks/logger"; const hashedKey = ENTERPRISE_LICENSE_KEY ? hashString(ENTERPRISE_LICENSE_KEY) : undefined; const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const; @@ -98,12 +99,12 @@ const fetchLicenseForE2ETesting = async (): Promise<{ return newResult; } else if (currentTime.getTime() - previousResult.lastChecked.getTime() > 60 * 60 * 1000) { // Fail after 1 hour - console.log("E2E_TESTING is enabled. Enterprise license was revoked after 1 hour."); + logger.info("E2E_TESTING is enabled. Enterprise license was revoked after 1 hour."); return null; } return previousResult; } catch (error) { - console.error("Error fetching license: ", error); + logger.error(error, "Error fetching license"); return null; } }; @@ -191,7 +192,7 @@ export const getEnterpriseLicense = async (): Promise<{ } // Log error only after 72 hours - console.error("Error while checking license: The license check failed"); + logger.error("Error while checking license: The license check failed"); return { active: false, @@ -254,7 +255,7 @@ export const fetchLicense = reactCache( return null; } catch (error) { - console.error("Error while checking license: ", error); + logger.error(error, "Error while checking license"); return null; } }, diff --git a/apps/web/modules/ee/teams/lib/roles.ts b/apps/web/modules/ee/teams/lib/roles.ts index 030779db2f..5b74f1aa6e 100644 --- a/apps/web/modules/ee/teams/lib/roles.ts +++ b/apps/web/modules/ee/teams/lib/roles.ts @@ -8,6 +8,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { membershipCache } from "@formbricks/lib/membership/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId, ZString } from "@formbricks/types/common"; import { DatabaseError, UnknownError } from "@formbricks/types/errors"; @@ -51,7 +52,7 @@ export const getProjectPermissionByUserId = reactCache( return highestPermission; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project permission by user id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/ee/teams/team-list/lib/project.ts b/apps/web/modules/ee/teams/team-list/lib/project.ts index 2f655a174b..06ec7d370c 100644 --- a/apps/web/modules/ee/teams/team-list/lib/project.ts +++ b/apps/web/modules/ee/teams/team-list/lib/project.ts @@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZString } from "@formbricks/types/common"; import { DatabaseError, UnknownError } from "@formbricks/types/errors"; @@ -32,7 +33,7 @@ export const getProjectsByOrganizationId = reactCache( })); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching projects by organization id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts b/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts index 8a42f69617..3d160120dd 100644 --- a/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts +++ b/apps/web/modules/ee/whitelabel/remove-branding/lib/project.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import { prisma } from "@formbricks/database"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { ValidationError } from "@formbricks/types/errors"; @@ -49,7 +50,7 @@ export const updateProjectBranding = async ( return true; } catch (error) { if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + logger.error(error.errors, "Error updating project branding"); } throw new ValidationError("Data validation of project failed"); } diff --git a/apps/web/modules/email/emails/survey/follow-up.tsx b/apps/web/modules/email/emails/survey/follow-up.tsx index fed887ea88..d81dba0ee9 100644 --- a/apps/web/modules/email/emails/survey/follow-up.tsx +++ b/apps/web/modules/email/emails/survey/follow-up.tsx @@ -14,7 +14,6 @@ interface FollowUpEmailProps { export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Promise { const t = await getTranslate(); - console.log(t("emails.imprint")); const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl; return ( diff --git a/apps/web/modules/email/index.tsx b/apps/web/modules/email/index.tsx index b99074d768..d4dd11b2eb 100644 --- a/apps/web/modules/email/index.tsx +++ b/apps/web/modules/email/index.tsx @@ -18,6 +18,7 @@ import { } from "@formbricks/lib/constants"; import { createInviteToken, createToken, createTokenForLinkSurvey } from "@formbricks/lib/jwt"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; +import { logger } from "@formbricks/logger"; import type { TLinkSurveyEmailData } from "@formbricks/types/email"; import { InvalidInputError } from "@formbricks/types/errors"; import type { TResponse } from "@formbricks/types/responses"; @@ -76,7 +77,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise return true; } catch (error) { - console.error("Error in sendEmail:", error); + logger.error(error, "Error in sendEmail"); throw new InvalidInputError("Incorrect SMTP credentials"); } }; @@ -104,7 +105,7 @@ export const sendVerificationEmail = async ({ html, }); } catch (error) { - console.error("Error in sendVerificationEmail:", error); + logger.error(error, "Error in sendVerificationEmail"); throw error; // Re-throw the error to maintain the original behavior } }; diff --git a/apps/web/modules/organization/settings/teams/lib/membership.ts b/apps/web/modules/organization/settings/teams/lib/membership.ts index 57252051d6..c15981448d 100644 --- a/apps/web/modules/organization/settings/teams/lib/membership.ts +++ b/apps/web/modules/organization/settings/teams/lib/membership.ts @@ -9,6 +9,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { DatabaseError, UnknownError } from "@formbricks/types/errors"; import { TMember } from "@formbricks/types/memberships"; @@ -51,7 +52,7 @@ export const getMembershipByOrganizationId = reactCache( return members; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching membership by organization id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/projects/settings/lib/project.ts b/apps/web/modules/projects/settings/lib/project.ts index 877618348d..d0733b3d21 100644 --- a/apps/web/modules/projects/settings/lib/project.ts +++ b/apps/web/modules/projects/settings/lib/project.ts @@ -11,6 +11,7 @@ import { deleteS3FilesByEnvironmentId, } from "@formbricks/lib/storage/service"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId, ZString } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ValidationError } from "@formbricks/types/errors"; import { TProject, TProjectUpdateInput, ZProject, ZProjectUpdateInput } from "@formbricks/types/project"; @@ -79,7 +80,7 @@ export const updateProject = async ( return project; } catch (error) { if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + logger.error(error.errors, "Error updating project"); } throw new ValidationError("Data validation of project failed"); } @@ -174,7 +175,7 @@ export const deleteProject = async (projectId: string): Promise => { await Promise.all(s3FilesPromises); } catch (err) { // fail silently because we don't want to throw an error if the files are not deleted - console.error(err); + logger.error(err, "Error deleting S3 files"); } } else { const localFilesPromises = project.environments.map(async (environment) => { @@ -185,7 +186,7 @@ export const deleteProject = async (projectId: string): Promise => { await Promise.all(localFilesPromises); } catch (err) { // fail silently because we don't want to throw an error if the files are not deleted - console.error(err); + logger.error(err, "Error deleting local files"); } } diff --git a/apps/web/modules/projects/settings/look/lib/project.ts b/apps/web/modules/projects/settings/look/lib/project.ts index 7384edfe56..82e99a7f78 100644 --- a/apps/web/modules/projects/settings/look/lib/project.ts +++ b/apps/web/modules/projects/settings/look/lib/project.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getProjectByEnvironmentId = reactCache( @@ -29,7 +30,7 @@ export const getProjectByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/components/template-list/lib/survey.ts b/apps/web/modules/survey/components/template-list/lib/survey.ts index cd40045c90..15562ed497 100644 --- a/apps/web/modules/survey/components/template-list/lib/survey.ts +++ b/apps/web/modules/survey/components/template-list/lib/survey.ts @@ -11,6 +11,7 @@ import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TSurveyCreateInput } from "@formbricks/types/surveys/types"; @@ -177,7 +178,7 @@ export const createSurvey = async ( return transformedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating survey"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/editor/lib/project.ts b/apps/web/modules/survey/editor/lib/project.ts index 78c1df8d9a..d07a794a77 100644 --- a/apps/web/modules/survey/editor/lib/project.ts +++ b/apps/web/modules/survey/editor/lib/project.ts @@ -3,6 +3,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const getProject = reactCache( @@ -19,7 +20,7 @@ export const getProject = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/editor/lib/survey.ts b/apps/web/modules/survey/editor/lib/survey.ts index e594428698..2c99b6d9a8 100644 --- a/apps/web/modules/survey/editor/lib/survey.ts +++ b/apps/web/modules/survey/editor/lib/survey.ts @@ -9,6 +9,7 @@ import { Prisma, Survey } from "@prisma/client"; import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSegment, ZSegmentFilters } from "@formbricks/types/segment"; import { TSurvey, TSurveyOpenTextQuestion } from "@formbricks/types/surveys/types"; @@ -118,7 +119,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => segmentCache.revalidate({ id: updatedSegment.id, environmentId: updatedSegment.environmentId }); updatedSegment.surveys.map((survey) => surveyCache.revalidate({ id: survey.id })); } catch (error) { - console.error(error); + logger.error(error, "Error updating survey"); throw new Error("Error updating survey"); } } else { @@ -372,7 +373,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => return modifiedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error updating survey"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/lib/environment.ts b/apps/web/modules/survey/lib/environment.ts index a43e35a0a5..045042fef1 100644 --- a/apps/web/modules/survey/lib/environment.ts +++ b/apps/web/modules/survey/lib/environment.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { environmentCache } from "@formbricks/lib/environment/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getEnvironment = reactCache( @@ -27,7 +28,7 @@ export const getEnvironment = reactCache( return environment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching environment"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/lib/membership.ts b/apps/web/modules/survey/lib/membership.ts index 2353e9e3e1..8d78354919 100644 --- a/apps/web/modules/survey/lib/membership.ts +++ b/apps/web/modules/survey/lib/membership.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { membershipCache } from "@formbricks/lib/membership/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { AuthorizationError, DatabaseError, UnknownError } from "@formbricks/types/errors"; export const getMembershipRoleByUserIdOrganizationId = reactCache( @@ -30,7 +31,7 @@ export const getMembershipRoleByUserIdOrganizationId = reactCache( return membership.role; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching membership role by user id and organization id"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/lib/project.ts b/apps/web/modules/survey/lib/project.ts index c12ab8a498..1f6d3857de 100644 --- a/apps/web/modules/survey/lib/project.ts +++ b/apps/web/modules/survey/lib/project.ts @@ -5,6 +5,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getProjectByEnvironmentId = reactCache( @@ -27,7 +28,7 @@ export const getProjectByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/link/lib/project.ts b/apps/web/modules/survey/link/lib/project.ts index 00237b4729..c169d5b4c9 100644 --- a/apps/web/modules/survey/link/lib/project.ts +++ b/apps/web/modules/survey/link/lib/project.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; @@ -35,7 +36,7 @@ export const getProjectByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching project by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/link/lib/survey.ts b/apps/web/modules/survey/link/lib/survey.ts index 32eaafd232..7a54acd089 100644 --- a/apps/web/modules/survey/link/lib/survey.ts +++ b/apps/web/modules/survey/link/lib/survey.ts @@ -4,6 +4,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const getSurveyMetadata = reactCache(async (surveyId: string) => @@ -31,7 +32,7 @@ export const getSurveyMetadata = reactCache(async (surveyId: string) => return survey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey metadata"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/list/lib/environment.ts b/apps/web/modules/survey/list/lib/environment.ts index 90a9d274e2..3752019037 100644 --- a/apps/web/modules/survey/list/lib/environment.ts +++ b/apps/web/modules/survey/list/lib/environment.ts @@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { environmentCache } from "@formbricks/lib/environment/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const doesEnvironmentExist = reactCache( @@ -80,7 +81,7 @@ export const getEnvironment = reactCache( return environment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error fetching environment"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/list/lib/project.ts b/apps/web/modules/survey/list/lib/project.ts index 7adea354cb..6b124c5045 100644 --- a/apps/web/modules/survey/list/lib/project.ts +++ b/apps/web/modules/survey/list/lib/project.ts @@ -6,6 +6,7 @@ import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; import { projectCache } from "@formbricks/lib/project/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const getProjectWithLanguagesByEnvironmentId = reactCache( @@ -30,7 +31,7 @@ export const getProjectWithLanguagesByEnvironmentId = reactCache( return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting project with languages by environment id"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/survey/list/lib/survey.ts b/apps/web/modules/survey/list/lib/survey.ts index 336b1a0dcc..d0ab6c2f62 100644 --- a/apps/web/modules/survey/list/lib/survey.ts +++ b/apps/web/modules/survey/list/lib/survey.ts @@ -15,6 +15,7 @@ import { projectCache } from "@formbricks/lib/project/cache"; import { responseCache } from "@formbricks/lib/response/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurveyFilterCriteria } from "@formbricks/types/surveys/types"; @@ -72,7 +73,7 @@ export const getSurveys = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys"); throw new DatabaseError(error.message); } throw error; @@ -161,7 +162,7 @@ export const getSurveysSortedByRelevance = reactCache( return surveys; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys sorted by relevance"); throw new DatabaseError(error.message); } throw error; @@ -193,7 +194,7 @@ export const getSurvey = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey"); throw new DatabaseError(error.message); } throw error; @@ -283,7 +284,7 @@ export const deleteSurvey = async (surveyId: string): Promise => { return true; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error deleting survey"); throw new DatabaseError(error.message); } @@ -606,7 +607,7 @@ export const copySurveyToOtherEnvironment = async ( return newSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error copying survey to other environment"); throw new DatabaseError(error.message); } throw error; @@ -628,7 +629,7 @@ export const getSurveyCount = reactCache( return surveyCount; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey count"); throw new DatabaseError(error.message); } diff --git a/apps/web/modules/survey/templates/lib/survey.ts b/apps/web/modules/survey/templates/lib/survey.ts index 3ee89261bf..d8967d3e79 100644 --- a/apps/web/modules/survey/templates/lib/survey.ts +++ b/apps/web/modules/survey/templates/lib/survey.ts @@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; import { surveyCache } from "@formbricks/lib/survey/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; export const createSurvey = async ( @@ -101,7 +102,7 @@ export const createSurvey = async ( return { id: survey.id }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating survey"); throw new DatabaseError(error.message); } throw error; diff --git a/apps/web/modules/ui/components/input-combo-box/stories.ts b/apps/web/modules/ui/components/input-combo-box/stories.ts index 54ae84aedf..5ab19be468 100644 --- a/apps/web/modules/ui/components/input-combo-box/stories.ts +++ b/apps/web/modules/ui/components/input-combo-box/stories.ts @@ -1,5 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { FileIcon, FolderIcon, ImageIcon } from "lucide-react"; +import { logger } from "@formbricks/logger"; import { InputCombobox } from "./index"; const meta = { @@ -41,7 +42,7 @@ export const Default: Story = { searchPlaceholder: "Search...", options: commonOptions, value: null, - onChangeValue: (value, option) => console.log("Selected:", value, option), + onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"), clearable: true, withInput: false, allowMultiSelect: false, diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 3593620524..eb1eed2cfa 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -18,7 +18,7 @@ const nextConfig = { assetPrefix: process.env.ASSET_PREFIX_URL || undefined, output: "standalone", poweredByHeader: false, - serverExternalPackages: ["@aws-sdk"], + serverExternalPackages: ["@aws-sdk", "@opentelemetry/instrumentation", "pino", "pino-pretty"], outputFileTracingIncludes: { "app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"], }, @@ -27,10 +27,7 @@ const nextConfig = { localeDetection: false, defaultLocale: "en-US", }, - experimental: { - instrumentationHook: true, - serverComponentsExternalPackages: ["@opentelemetry/instrumentation"], - }, + experimental: {}, transpilePackages: ["@formbricks/database", "@formbricks/lib"], images: { remotePatterns: [ diff --git a/apps/web/package.json b/apps/web/package.json index 408a8f0bcb..0b67048bac 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,7 @@ "@formbricks/js-core": "workspace:*", "@formbricks/lib": "workspace:*", "@formbricks/surveys": "workspace:*", + "@formbricks/logger": "workspace:*", "@formbricks/types": "workspace:*", "@hookform/resolvers": "3.9.1", "@intercom/messenger-js-sdk": "0.0.14", diff --git a/apps/web/playwright/api/management/responses.spec.ts b/apps/web/playwright/api/management/responses.spec.ts index ff4fc062ec..17fbac8da8 100644 --- a/apps/web/playwright/api/management/responses.spec.ts +++ b/apps/web/playwright/api/management/responses.spec.ts @@ -1,4 +1,5 @@ import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; import { test } from "../../lib/fixtures"; import { loginAndGetApiKey } from "../../lib/utils"; import { RESPONSES_API_URL, SURVEYS_API_URL } from "../constants"; @@ -10,7 +11,7 @@ test.describe("API Tests for Responses", () => { try { ({ environmentId, apiKey } = await loginAndGetApiKey(page, users)); } catch (error) { - console.error("Error during login and getting API key:", error); + logger.error(error, "Error during login and getting API key"); throw error; } diff --git a/apps/web/playwright/api/management/survey.spec.ts b/apps/web/playwright/api/management/survey.spec.ts index 985950dfba..eb446c87a6 100644 --- a/apps/web/playwright/api/management/survey.spec.ts +++ b/apps/web/playwright/api/management/survey.spec.ts @@ -1,4 +1,5 @@ import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; import { test } from "../../lib/fixtures"; import { loginAndGetApiKey } from "../../lib/utils"; import { SURVEYS_API_URL } from "../constants"; @@ -10,7 +11,7 @@ test.describe("API Tests", () => { try { ({ environmentId, apiKey } = await loginAndGetApiKey(page, users)); } catch (error) { - console.error("Error during login and getting API key:", error); + logger.error(error, "Error during login and getting API key"); throw error; } diff --git a/apps/web/playwright/utils/helper.ts b/apps/web/playwright/utils/helper.ts index 05aaedd324..2b767a5d45 100644 --- a/apps/web/playwright/utils/helper.ts +++ b/apps/web/playwright/utils/helper.ts @@ -2,6 +2,7 @@ import { CreateSurveyParams, CreateSurveyWithLogicParams } from "@/playwright/ut import { expect } from "@playwright/test"; import { readFileSync, writeFileSync } from "fs"; import { Page } from "playwright"; +import { logger } from "@formbricks/logger"; import { TProjectConfigChannel } from "@formbricks/types/project"; export const signUpAndLogin = async ( @@ -96,7 +97,7 @@ export const uploadFileForFileUploadQuestion = async (page: Page) => { }, ]); } catch (error) { - console.error("Error uploading files:", error); + logger.error(error, "Error uploading files"); } }; diff --git a/apps/web/scripts/merge-client-endpoints.ts b/apps/web/scripts/merge-client-endpoints.ts index e1bff09938..2a19276301 100644 --- a/apps/web/scripts/merge-client-endpoints.ts +++ b/apps/web/scripts/merge-client-endpoints.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as yaml from "yaml"; +import { logger } from "@formbricks/logger"; // Define the v1 (now v2) client endpoints to be merged const v1ClientEndpoints = { @@ -314,8 +315,8 @@ const updatedOpenapiContent = yaml.stringify(openapiDoc); // Write the updated content back to the openapi.yml file try { fs.writeFileSync(openapiFilePath, updatedOpenapiContent); - console.log("Merged v1 client endpoints into the generated v2 documentation."); + logger.info("Merged v1 client endpoints into the generated v2 documentation."); } catch (error) { - console.error("Error writing to OpenAPI file:", error); + logger.error(error, "Error writing to OpenAPI file"); process.exit(1); } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index afe005913a..09d604bf5a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -26,6 +26,9 @@ x-environment: &environment # You can use: $(openssl rand -hex 32) to generate a secure one CRON_SECRET: + # Set the minimum log level(debug, info, warn, error, fatal) + # LOG_LEVEL: info + ############################################# OPTIONAL (ENTERPRISE EDITION) ############################################# # Enterprise License Key (More info at: https://formbricks.com/docs/self-hosting/license) @@ -162,7 +165,6 @@ x-environment: &environment # REDIS_URL: # REDIS_DEFAULT_TTL: - # Set the below to use for Rate Limiting (default us In-Memory LRU Cache) # REDIS_HTTP_URL: diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 7783a9793c..3b23dd7b4e 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -16,6 +16,7 @@ These variables are present inside your machine’s docker-compose file. Restart | NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | | ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | | CRON_SECRET | API Secret for running cron jobs. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | +| LOG_LEVEL | Minimum log level (debug, info, warn, error, fatal) | optional | info | | UPLOADS_DIR | Local directory for storing uploads. | optional | ./uploads | | S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) | | S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) | diff --git a/package.json b/package.json index 072c87cba1..349fca8569 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lint": "turbo run lint", "release": "turbo run build --filter=@formbricks/js... && changeset publish", "test": "turbo run test --no-cache", + "test:coverage": "turbo run test:coverage --no-cache", "test:e2e": "playwright test", "test-e2e:azure": "pnpm test:e2e -c playwright.service.config.ts --workers=20", "prepare": "husky install", diff --git a/packages/database/migration/20241209104738_xm_user_identification/migration.ts b/packages/database/migration/20241209104738_xm_user_identification/migration.ts index 36f2cdc438..6dd745701f 100644 --- a/packages/database/migration/20241209104738_xm_user_identification/migration.ts +++ b/packages/database/migration/20241209104738_xm_user_identification/migration.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ import { createId } from "@paralleldrive/cuid2"; import { Prisma } from "@prisma/client"; +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; export const xmUserIdentification: MigrationScript = { @@ -23,7 +24,7 @@ export const xmUserIdentification: MigrationScript = { // If no contacts have a userId, migration is already complete if (totalContacts > 0 && contactsWithUserId === 0) { - console.log("Migration already completed. No contacts with userId found."); + logger.info("Migration already completed. No contacts with userId found."); return; } @@ -40,7 +41,7 @@ export const xmUserIdentification: MigrationScript = { break; } - console.log("Processing attributeKeys for", environments.length, "environments"); + logger.info(`Processing attributeKeys for ${environments.length.toString()} environments`); // Process each environment for (const env of environments) { @@ -80,7 +81,7 @@ export const xmUserIdentification: MigrationScript = { SELECT COUNT(*)::integer AS deleted_count FROM deleted `; - console.log("Deleted userId attributes for", deletedCount, "contacts"); + logger.info(`Deleted userId attributes for ${deletedCount.toString()} contacts`); while (true) { // Fetch contacts with userId in batches @@ -164,7 +165,7 @@ export const xmUserIdentification: MigrationScript = { processedContacts += contacts.length; if (processedContacts > 0) { - console.log(`Processed ${processedContacts.toString()} contacts`); + logger.info(`Processed ${processedContacts.toString()} contacts`); } } @@ -186,13 +187,12 @@ export const xmUserIdentification: MigrationScript = { ) `; - console.log("Total contacts after migration:", totalContactsAfterMigration); - console.log("Total attributes with userId now:", totalUserIdAttributes); + logger.info(`Total contacts after migration: ${totalContactsAfterMigration.toString()}`); + logger.info(`Total attributes with userId now: ${totalUserIdAttributes.toString()}`); if (totalContactsAfterMigration !== totalUserIdAttributes) { - console.log( - "Difference between total contacts and total attributes with userId: ", - totalContactsAfterMigration - totalUserIdAttributes + logger.info( + `Difference between total contacts and total attributes with userId: ${(totalContactsAfterMigration - totalUserIdAttributes).toString()}` ); } }, diff --git a/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts b/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts index ac1034a816..db894d9067 100644 --- a/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts +++ b/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ import { Prisma } from "@prisma/client"; +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; export const xmAttributeRemoval: MigrationScript = { @@ -83,6 +84,6 @@ export const xmAttributeRemoval: MigrationScript = { ), }; - console.log("Data migration completed. Summary: ", summary); + logger.info(summary, "Data migration completed. Summary: "); }, }; diff --git a/packages/database/migration/20241209111525_update_org_limits/migration.ts b/packages/database/migration/20241209111525_update_org_limits/migration.ts index 75e7610245..0f34212015 100644 --- a/packages/database/migration/20241209111525_update_org_limits/migration.ts +++ b/packages/database/migration/20241209111525_update_org_limits/migration.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; type Plan = "free" | "startup" | "scale"; @@ -94,6 +95,6 @@ export const updateOrgLimits: MigrationScript = { await Promise.all(updationPromises); - console.log(`Updated ${organizations.length.toString()} organizations`); + logger.info(`Updated ${organizations.length.toString()} organizations`); }, }; diff --git a/packages/database/migration/20241209111725_product_revamp/migration.ts b/packages/database/migration/20241209111725_product_revamp/migration.ts index c4f9a2e67b..b9abf57847 100644 --- a/packages/database/migration/20241209111725_product_revamp/migration.ts +++ b/packages/database/migration/20241209111725_product_revamp/migration.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; type Plan = "free" | "startup" | "scale" | "enterprise"; @@ -46,7 +47,7 @@ export const productRevamp: MigrationScript = { await Promise.all(updateOrganizationPromises); - console.log(`Updated ${updateOrganizationPromises.length.toString()} organizations`); + logger.info(`Updated ${updateOrganizationPromises.length.toString()} organizations`); const updatedEmptyConfigProjects: number | undefined = await tx.$executeRaw` UPDATE "Project" @@ -57,7 +58,7 @@ export const productRevamp: MigrationScript = { WHERE config = '{}'; `; - console.log( + logger.info( `Updated ${updatedEmptyConfigProjects ? updatedEmptyConfigProjects.toString() : "0"} projects with empty config` ); }, diff --git a/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts b/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts index 44d0993db2..27ddc3a6d3 100644 --- a/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts +++ b/packages/database/migration/20250103060634_add_placeholder_to_contact_and_address_question/migration.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- field.placeholder can be undefined for surveys created before this migration */ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; interface Field { @@ -63,7 +64,7 @@ export const addPlaceholderToContactAndAddressQuestion: MigrationScript = { `; if (surveys.length === 0) { - console.log("No surveys found"); + logger.info("No surveys found"); return; } let surveyUpdateCount = 0; @@ -194,6 +195,6 @@ export const addPlaceholderToContactAndAddressQuestion: MigrationScript = { } await Promise.all(updatePromises); - console.log(`Updated ${surveyUpdateCount.toString()} surveys`); + logger.info(`Updated ${surveyUpdateCount.toString()} surveys`); }, }; diff --git a/packages/database/migration/20250211050118_removed_new_session_event/migration.ts b/packages/database/migration/20250211050118_removed_new_session_event/migration.ts index ac9b4bb620..a0e4450775 100644 --- a/packages/database/migration/20250211050118_removed_new_session_event/migration.ts +++ b/packages/database/migration/20250211050118_removed_new_session_event/migration.ts @@ -1,3 +1,4 @@ +import { logger } from "@formbricks/logger"; import type { MigrationScript } from "../../src/scripts/migration-runner"; export const removedNewSessionEvent: MigrationScript = { @@ -17,7 +18,7 @@ export const removedNewSessionEvent: MigrationScript = { ) `; - console.log(`Updated ${updatedActions.toString()} automatic actions`); + logger.info(`Updated ${updatedActions.toString()} automatic actions`); // Delete actions that are not referenced in SurveyTrigger const deletedActions = await tx.$executeRaw` @@ -29,6 +30,6 @@ export const removedNewSessionEvent: MigrationScript = { WHERE "actionClassId" = "ActionClass".id ) `; - console.log(`Deleted ${deletedActions.toString()} automatic actions`); + logger.info(`Deleted ${deletedActions.toString()} automatic actions`); }, }; diff --git a/packages/database/package.json b/packages/database/package.json index 96f6896a01..43232fbc42 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -26,6 +26,7 @@ "@prisma/client": "6.0.1", "@prisma/extension-accelerate": "1.2.1", "dotenv-cli": "7.4.4", + "@formbricks/logger": "workspace:*", "zod-openapi": "4.2.3" }, "devDependencies": { diff --git a/packages/database/src/scripts/apply-migrations.ts b/packages/database/src/scripts/apply-migrations.ts index 3573d5310e..fcaee8af88 100644 --- a/packages/database/src/scripts/apply-migrations.ts +++ b/packages/database/src/scripts/apply-migrations.ts @@ -1,6 +1,7 @@ +import { logger } from "@formbricks/logger"; import { applyMigrations } from "./migration-runner"; applyMigrations().catch((error: unknown) => { - console.error("Migration failed:", error); + logger.fatal(error, "Migration failed"); process.exit(1); }); diff --git a/packages/database/src/scripts/create-migration.ts b/packages/database/src/scripts/create-migration.ts index 44384b3fab..ab044a6cc1 100644 --- a/packages/database/src/scripts/create-migration.ts +++ b/packages/database/src/scripts/create-migration.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; import { promisify } from "node:util"; +import { logger } from "@formbricks/logger"; import { applyMigrations } from "./migration-runner"; const execAsync = promisify(exec); @@ -15,11 +16,11 @@ function promptForMigrationName(): Promise { return new Promise((resolve) => { rl.question("Enter the name of the migration (please use spaces): ", (name) => { if (!name.trim()) { - console.error("Migration name cannot be empty."); + logger.fatal("Migration name cannot be empty."); process.exit(1); } if (/[^a-zA-Z0-9\s]/.test(name)) { - console.error( + logger.fatal( "Migration name contains invalid characters. Only letters, numbers, and spaces are allowed." ); process.exit(1); @@ -47,7 +48,7 @@ async function main(): Promise { // Create migration await execAsync(`pnpm prisma migrate dev --name "${migrationName}" --create-only`); - console.log(`Migration created: ${migrationName}`); + logger.info(`Migration created: ${migrationName}`); // Find the newly created migration const migrationToCopy = await fs @@ -89,7 +90,7 @@ async function main(): Promise { try { await applyMigrations(); } catch (err) { - console.error("Error applying migrations: ", err); + logger.fatal(err, "Error applying migrations: "); // delete the created migration directories: await fs.rm(destPath, { recursive: true, force: true }); process.exit(1); @@ -97,6 +98,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error("Migration creation failed:", error); + logger.fatal(error, "Migration creation failed"); process.exit(1); }); diff --git a/packages/database/src/scripts/create-saml-database.ts b/packages/database/src/scripts/create-saml-database.ts index cc1d7de99e..e2fbf5b29d 100644 --- a/packages/database/src/scripts/create-saml-database.ts +++ b/packages/database/src/scripts/create-saml-database.ts @@ -1,4 +1,5 @@ import { PrismaClient } from "@prisma/client"; +import { logger } from "@formbricks/logger"; const createSamlDatabase = async (): Promise => { const samlDatabaseUrl = process.env.SAML_DATABASE_URL; @@ -32,9 +33,9 @@ const createSamlDatabase = async (): Promise => { await prisma.$executeRawUnsafe(`CREATE DATABASE "${dbName}"`); - console.log(`Database '${dbName}' created successfully.`); + logger.info(`Database '${dbName}' created successfully.`); } catch (error) { - console.error(`Error creating database '${dbName}':`, error); + logger.error(error, `Error creating database '${dbName}'`); return; } finally { await prisma.$disconnect(); @@ -46,5 +47,5 @@ createSamlDatabase() process.exit(0); }) .catch((error: unknown) => { - console.error("Error creating SAML database:", error); + logger.error(error, "Error creating SAML database"); }); diff --git a/packages/database/src/scripts/generate-data-migration.ts b/packages/database/src/scripts/generate-data-migration.ts index dfd923829b..b78d9f578c 100644 --- a/packages/database/src/scripts/generate-data-migration.ts +++ b/packages/database/src/scripts/generate-data-migration.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; import { createId } from "@paralleldrive/cuid2"; +import { logger } from "@formbricks/logger"; const rl = readline.createInterface({ input: process.stdin, @@ -12,7 +13,7 @@ const migrationsDir = path.resolve(__dirname, "../../migration"); async function createMigration(): Promise { // Log the full path to verify directory location - console.log("Migrations Directory Full Path:", migrationsDir); + logger.info(migrationsDir, "Migrations Directory Full Path"); // Check if migrations directory exists, create if not const hasAccess = await fs @@ -22,7 +23,7 @@ async function createMigration(): Promise { if (!hasAccess) { await fs.mkdir(migrationsDir, { recursive: true }); - console.log(`Created migrations directory: ${migrationsDir}`); + logger.info(`Created migrations directory: ${migrationsDir}`); } const migrationNameSpaced = await promptForMigrationName(); @@ -55,22 +56,22 @@ async function createMigration(): Promise { // Create the migration directory await fs.mkdir(fullMigrationPath, { recursive: true }); - console.log("Created migration directory:", fullMigrationPath); + logger.info(fullMigrationPath, "Created migration directory"); // Create the migration file await fs.writeFile(filePath, getTemplateContent(migrationFunctionName, migrationNameTimestamped)); - console.log(`New migration created: ${filePath}`); + logger.info(filePath, "New migration created"); } function promptForMigrationName(): Promise { return new Promise((resolve) => { rl.question("Enter the name of the migration (please use spaces): ", (name) => { if (!name.trim()) { - console.error("Migration name cannot be empty."); + logger.error("Migration name cannot be empty."); process.exit(1); } if (/[^a-zA-Z0-9\s]/.test(name)) { - console.error( + logger.error( "Migration name contains invalid characters. Only letters, numbers, and spaces are allowed." ); process.exit(1); @@ -107,6 +108,6 @@ export const ${migrationName}: MigrationScript = { } createMigration().catch((error: unknown) => { - console.error("An error occurred while creating the migration:", error); + logger.fatal(error, "An error occurred while creating the migration"); process.exit(1); }); diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts index b0d020ec6b..2f43c4b986 100644 --- a/packages/database/src/scripts/migration-runner.ts +++ b/packages/database/src/scripts/migration-runner.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import { type Prisma, PrismaClient } from "@prisma/client"; +import { logger } from "@formbricks/logger"; const execAsync = promisify(exec); @@ -27,7 +28,7 @@ const MIGRATIONS_DIR = path.resolve(__dirname, "../../migration"); const PRISMA_MIGRATIONS_DIR = path.resolve(__dirname, "../../migrations"); const runMigrations = async (migrations: MigrationScript[]): Promise => { - console.log(`Starting migrations: ${migrations.length.toString()} to run`); + logger.info(`Starting migrations: ${migrations.length.toString()} to run`); const startTime = Date.now(); // empty the prisma migrations directory @@ -38,12 +39,12 @@ const runMigrations = async (migrations: MigrationScript[]): Promise => { } const endTime = Date.now(); - console.log(`All migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); + logger.info(`All migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); }; const runSingleMigration = async (migration: MigrationScript, index: number): Promise => { if (migration.type === "data") { - console.log(`Running data migration: ${migration.name}`); + logger.info(`Running data migration: ${migration.name}`); try { await prisma.$transaction( @@ -56,9 +57,9 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr `; if (existingMigration?.[0]?.status === "pending") { - console.log(`Data migration ${migration.name} is pending.`); - console.log("Either there is another migration which is currently running or this is an error."); - console.log( + logger.info(`Data migration ${migration.name} is pending.`); + logger.info("Either there is another migration which is currently running or this is an error."); + logger.info( "If you are sure that there is no migration running, you need to manually resolve the issue." ); @@ -66,12 +67,12 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr } if (existingMigration?.[0]?.status === "applied") { - console.log(`Data migration ${migration.name} already completed. Skipping...`); + logger.info(`Data migration ${migration.name} already completed. Skipping...`); return; } if (existingMigration?.[0]?.status === "failed") { - console.log(`Data migration ${migration.name} failed previously. Retrying...`); + logger.info(`Data migration ${migration.name} failed previously. Retrying...`); } else { // create a new data migration entry with pending status await prisma.$executeRaw`INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'pending')`; @@ -92,13 +93,13 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr `; } - console.log(`Data migration ${migration.name} completed successfully`); + logger.info(`Data migration ${migration.name} completed successfully`); }, { timeout: TRANSACTION_TIMEOUT } ); } catch (error) { // Record migration failure - console.error(`Data migration ${migration.name} failed:`, error); + logger.error(error, `Data migration ${migration.name} failed`); // Mark migration as failed await prisma.$queryRaw` INSERT INTO "DataMigration" (id, name, status) @@ -110,7 +111,7 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr } } else { try { - console.log(`Running schema migration: ${migration.name}`); + logger.info(`Running schema migration: ${migration.name}`); let copyOnly = false; @@ -138,7 +139,7 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr .then((files) => files.find((dir) => dir.includes(migration.name))); if (!migrationToCopy) { - console.error(`Schema migration not found: ${migration.name}`); + logger.error(`Schema migration not found: ${migration.name}`); return; } @@ -149,16 +150,16 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr await fs.cp(sourcePath, destPath, { recursive: true }); if (copyOnly) { - console.log(`Schema migration ${migration.name} copied to migrations directory`); + logger.info(`Schema migration ${migration.name} copied to migrations directory`); return; } // Run Prisma migrate // throws when migrate deploy fails await execAsync("pnpm prisma migrate deploy"); - console.log(`Successfully applied schema migration: ${migration.name}`); + logger.info(`Successfully applied schema migration: ${migration.name}`); } catch (err) { - console.error(`Schema migration ${migration.name} failed:`, err); + logger.error(err, `Schema migration ${migration.name} failed`); throw err; } } @@ -238,7 +239,7 @@ const loadMigrations = async (): Promise => { } } } else { - console.warn( + logger.warn( `Migration directory ${dirName} doesn't have migration.sql or data-migration.ts. Skipping...` ); } @@ -265,7 +266,7 @@ const loadMigrations = async (): Promise => { export async function applyMigrations(): Promise { try { const allMigrations = await loadMigrations(); - console.log(`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR}`); + logger.info(`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR}`); await runMigrations(allMigrations); } catch (error) { await prisma.$disconnect(); @@ -284,7 +285,7 @@ async function isSchemaMigrationApplied(migrationName: string, prismaClient: Pri `; return applied.length > 0; } catch (error: unknown) { - console.error(`Failed to check migration status: ${error as string}`); + logger.error(error, `Failed to check migration status`); throw new Error(`Could not verify migration status: ${error as string}`); } } diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json index 2853bd4d55..fa8f001737 100644 --- a/packages/database/tsconfig.json +++ b/packages/database/tsconfig.json @@ -1,4 +1,8 @@ { + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler" + }, "exclude": ["node_modules", "dist"], "extends": "@formbricks/config-typescript/node16.json", "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "tsup.config.ts"] diff --git a/packages/lib/airtable/service.ts b/packages/lib/airtable/service.ts index a9229ce9ad..07ceb4a25e 100644 --- a/packages/lib/airtable/service.ts +++ b/packages/lib/airtable/service.ts @@ -1,4 +1,5 @@ import { Prisma } from "@prisma/client"; +import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { TIntegrationItem } from "@formbricks/types/integration"; import { @@ -60,7 +61,7 @@ export const fetchAirtableAuthToken = async (formData: Record) => { const parsedToken = ZIntegrationAirtableTokenSchema.safeParse(tokenRes); if (!parsedToken.success) { - console.error(parsedToken.error); + logger.error(parsedToken.error, "Error parsing airtable token"); throw new Error(parsedToken.error.message); } const { access_token, refresh_token, expires_in } = parsedToken.data; diff --git a/packages/lib/env.ts b/packages/lib/env.ts index cb03557f6b..086622b850 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -50,6 +50,7 @@ export const env = createEnv({ INTERCOM_SECRET_KEY: z.string().optional(), INTERCOM_APP_ID: z.string().optional(), IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(), + LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "fatal"]).optional(), MAIL_FROM: z.string().email().optional(), MAIL_FROM_NAME: z.string().optional(), NEXTAUTH_SECRET: z.string().min(1), @@ -176,6 +177,7 @@ export const env = createEnv({ INVITE_DISABLED: process.env.INVITE_DISABLED, INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY, IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD, + LOG_LEVEL: process.env.LOG_LEVEL, MAIL_FROM: process.env.MAIL_FROM, MAIL_FROM_NAME: process.env.MAIL_FROM_NAME, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts index b14b6ba22d..6662846164 100644 --- a/packages/lib/environment/service.ts +++ b/packages/lib/environment/service.ts @@ -3,6 +3,7 @@ import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import type { TEnvironment, @@ -37,7 +38,7 @@ export const getEnvironment = reactCache( return environment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting environment"); throw new DatabaseError(error.message); } @@ -87,7 +88,7 @@ export const getEnvironments = reactCache( return environments; } catch (error) { if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + logger.error(error, "Error getting environments"); } throw new ValidationError("Data validation of environments array failed"); } diff --git a/packages/lib/integration/service.ts b/packages/lib/integration/service.ts index 3d977971aa..ac96c57969 100644 --- a/packages/lib/integration/service.ts +++ b/packages/lib/integration/service.ts @@ -2,6 +2,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; @@ -55,7 +56,7 @@ export const createOrUpdateIntegration = async ( return integration; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating or updating integration"); throw new DatabaseError(error.message); } throw error; diff --git a/packages/lib/jwt.ts b/packages/lib/jwt.ts index c81b405688..235d263ea0 100644 --- a/packages/lib/jwt.ts +++ b/packages/lib/jwt.ts @@ -1,5 +1,6 @@ import jwt, { JwtPayload } from "jsonwebtoken"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { symmetricDecrypt, symmetricEncrypt } from "./crypto"; import { env } from "./env"; @@ -112,7 +113,7 @@ export const verifyInviteToken = (token: string): { inviteId: string; email: str email: decryptedEmail, }; } catch (error) { - console.error(`Error verifying invite token: ${error}`); + logger.error(error, "Error verifying invite token"); throw new Error("Invalid or expired invite token"); } }; diff --git a/packages/lib/language/service.ts b/packages/lib/language/service.ts index c4dd911c6f..382fb4f49c 100644 --- a/packages/lib/language/service.ts +++ b/packages/lib/language/service.ts @@ -1,6 +1,7 @@ import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors"; import { @@ -40,7 +41,7 @@ export const getLanguage = async (languageId: string): Promise { return true; } catch (error) { - console.error(`Failed to access S3 bucket: ${error}`); + logger.error(error, "Failed to access S3 bucket"); throw new Error(`S3 Bucket Access Test Failed: ${error}`); } }; diff --git a/packages/lib/storage/utils.ts b/packages/lib/storage/utils.ts index 9de8323e42..193978ccb0 100644 --- a/packages/lib/storage/utils.ts +++ b/packages/lib/storage/utils.ts @@ -1,3 +1,5 @@ +import { logger } from "@formbricks/logger"; + export const getOriginalFileNameFromUrl = (fileURL: string) => { try { const fileNameFromURL = fileURL.startsWith("/storage/") @@ -16,7 +18,7 @@ export const getOriginalFileNameFromUrl = (fileURL: string) => { const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : ""; return fileName; } catch (error) { - console.error(`Error parsing file URL: ${error}`); + logger.error(error, "Error parsing file URL"); } }; @@ -28,6 +30,6 @@ export const getFileNameWithIdFromUrl = (fileURL: string) => { return fileNameFromURL ? decodeURIComponent(fileNameFromURL || "") : ""; } catch (error) { - console.error("Error parsing file URL:", error); + logger.error(error, "Error parsing file URL"); } }; diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index b9c2027e40..aea8f2ce76 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -2,6 +2,7 @@ import "server-only"; import { ActionClass, Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; +import { logger } from "@formbricks/logger"; import { ZOptionalNumber } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; @@ -198,7 +199,7 @@ export const getSurvey = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey"); throw new DatabaseError(error.message); } throw error; @@ -241,7 +242,7 @@ export const getSurveysByActionClassId = reactCache( }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys by action class id"); throw new DatabaseError(error.message); } @@ -286,7 +287,7 @@ export const getSurveys = reactCache( return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey(surveyPrisma)); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting surveys"); throw new DatabaseError(error.message); } throw error; @@ -314,7 +315,7 @@ export const getSurveyCount = reactCache( return surveyCount; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error getting survey count"); throw new DatabaseError(error.message); } @@ -435,7 +436,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => segmentCache.revalidate({ id: updatedSegment.id, environmentId: updatedSegment.environmentId }); updatedSegment.surveys.map((survey) => surveyCache.revalidate({ id: survey.id })); } catch (error) { - console.error(error); + logger.error(error, "Error updating survey"); throw new Error("Error updating survey"); } } else { @@ -688,7 +689,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => return modifiedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error updating survey"); throw new DatabaseError(error.message); } @@ -862,7 +863,7 @@ export const createSurvey = async ( return transformedSurvey; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); + logger.error(error, "Error creating survey"); throw new DatabaseError(error.message); } throw error; @@ -890,6 +891,7 @@ export const getSurveyIdByResultShareKey = reactCache( return survey.id; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { + logger.error(error, "Error getting survey id by result share key"); throw new DatabaseError(error.message); } diff --git a/packages/lib/telemetry.ts b/packages/lib/telemetry.ts index 6550da5a4a..4a06f18b20 100644 --- a/packages/lib/telemetry.ts +++ b/packages/lib/telemetry.ts @@ -2,6 +2,7 @@ and how we can improve it. All data including the IP address is collected anonymously and we cannot trace anything back to you or your customers. If you still want to disable telemetry, set the environment variable TELEMETRY_DISABLED=1 */ +import { logger } from "@formbricks/logger"; import { IS_PRODUCTION } from "./constants"; import { env } from "./env"; @@ -31,7 +32,7 @@ export const captureTelemetry = async (eventName: string, properties = {}) => { }), }); } catch (error) { - console.error(`error sending telemetry: ${error}`); + logger.error(error, "error sending telemetry"); } } }; diff --git a/packages/lib/utils/fileConversion.ts b/packages/lib/utils/fileConversion.ts index c7b0bf7813..5c4236cc4f 100644 --- a/packages/lib/utils/fileConversion.ts +++ b/packages/lib/utils/fileConversion.ts @@ -1,5 +1,6 @@ import { AsyncParser } from "@json2csv/node"; import * as xlsx from "xlsx"; +import { logger } from "@formbricks/logger"; export const convertToCsv = async (fields: string[], jsonData: Record[]) => { let csv: string = ""; @@ -11,7 +12,7 @@ export const convertToCsv = async (fields: string[], jsonData: Record = [T, z.ZodType]; @@ -11,8 +12,9 @@ export function validateInputs[]>( for (const [value, schema] of pairs) { const inputValidation = schema.safeParse(value); if (!inputValidation.success) { - console.error( - `Validation failed for ${JSON.stringify(value).substring(0, 100)} and ${JSON.stringify(schema)}: ${inputValidation.error.message}` + logger.error( + inputValidation.error, + `Validation failed for ${JSON.stringify(value).substring(0, 100)} and ${JSON.stringify(schema)}` ); throw new ValidationError("Validation failed"); } diff --git a/packages/logger/.eslintrc.cjs b/packages/logger/.eslintrc.cjs new file mode 100644 index 0000000000..6459e6fb42 --- /dev/null +++ b/packages/logger/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ["@formbricks/eslint-config/library.js"], + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/logger/.gitignore b/packages/logger/.gitignore new file mode 100644 index 0000000000..a6ea004855 --- /dev/null +++ b/packages/logger/.gitignore @@ -0,0 +1,4 @@ +node_modules +.vscode +build +dist \ No newline at end of file diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000000..b3b095e0b5 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,41 @@ +{ + "name": "@formbricks/logger", + "private": true, + "type": "module", + "version": "0.1.0", + "homepage": "https://formbricks.com", + "license": "MIT", + "description": "Logger for Formbricks", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/formbricks/formbricks" + }, + "keywords": [ + "Formbricks", + "logger", + "logging" + ], + "scripts": { + "clean": "rimraf .turbo node_modules coverage dist", + "lint": "eslint . --ext .ts,.js,.tsx,.jsx", + "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", + "lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json", + "build": "tsc && vite build", + "test": "vitest" + }, + "author": "Formbricks ", + "dependencies": { + "zod": "3.24.1", + "pino": "^8.0.0", + "pino-pretty": "^10.0.0" + }, + "devDependencies": { + "vite": "^6.2.0", + "@formbricks/config-typescript": "workspace:*", + "vitest": "3.0.7", + "@formbricks/eslint-config": "workspace:*", + "vite-plugin-dts": "4.3.0" + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000000..41c7bf273e --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1 @@ +export * from "./logger"; diff --git a/packages/logger/src/logger.test.ts b/packages/logger/src/logger.test.ts new file mode 100644 index 0000000000..c3cf137d74 --- /dev/null +++ b/packages/logger/src/logger.test.ts @@ -0,0 +1,224 @@ +// Import pino after the mock is defined +import Pino from "pino"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { LOG_LEVELS } from "../types/logger"; + +// Store original environment variables outside any function +const originalNodeEnv = process.env.NODE_ENV; +const originalLogLevel = process.env.LOG_LEVEL; +const originalNextRuntime = process.env.NEXT_RUNTIME; + +// Define the mock before any imports that use it +// Move mock outside of any function to avoid hoisting issues +vi.mock("pino", () => { + // Create a factory function that returns the mock + return { + default: vi.fn(() => ({ + debug: vi.fn().mockReturnThis(), + info: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + error: vi.fn().mockReturnThis(), + fatal: vi.fn().mockReturnThis(), + child: vi.fn().mockReturnThis(), + flush: vi.fn(), + })), + stdSerializers: { + err: vi.fn(), + req: vi.fn(), + res: vi.fn(), + }, + }; +}); + +describe("Logger", () => { + beforeEach(() => { + // Clear module cache to reset logger for each test + vi.resetModules(); + + // Reset mocks + vi.clearAllMocks(); + + // Set default environment for tests + process.env.NODE_ENV = "development"; + process.env.LOG_LEVEL = "info"; + process.env.NEXT_RUNTIME = "nodejs"; + }); + + afterEach(() => { + // Restore process.env + process.env.NODE_ENV = originalNodeEnv; + process.env.LOG_LEVEL = originalLogLevel; + process.env.NEXT_RUNTIME = originalNextRuntime; + }); + + test("logger is created with development config when NODE_ENV is not production", async () => { + process.env.NODE_ENV = "development"; + const { logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + transport: expect.objectContaining({ + target: "pino-pretty", + }) as Pino.TransportSingleOptions, + }) + ); + + expect(logger).toBeDefined(); + }); + + test("logger is created with production config when NODE_ENV is production", async () => { + process.env.NODE_ENV = "production"; + const { logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.not.objectContaining({ + transport: expect.any(Object) as Pino.TransportSingleOptions, + }) + ); + + expect(logger).toBeDefined(); + }); + + test("getLogLevel defaults to 'info' in development mode", async () => { + process.env.NODE_ENV = "development"; + process.env.LOG_LEVEL = undefined; + + const { logger: _logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + level: "info", + }) + ); + }); + + test("getLogLevel defaults to 'warn' in production mode", async () => { + process.env.NODE_ENV = "production"; + process.env.LOG_LEVEL = undefined; + + const { logger: _logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + level: "warn", + }) + ); + }); + + test("getLogLevel respects LOG_LEVEL env variable when valid", async () => { + process.env.LOG_LEVEL = "debug"; + + const { logger: _logger } = await import("./logger"); + + expect(Pino).toHaveBeenCalledWith( + expect.objectContaining({ + level: "debug", + }) + ); + }); + + test("withContext creates a child logger with provided context", async () => { + // Clear cache to get a fresh instance + vi.resetModules(); + + // Create a child spy before importing the logger + const childSpy = vi.fn().mockReturnThis(); + + // Set up the mock to capture the child call + vi.mocked(Pino).mockReturnValue({ + debug: vi.fn().mockReturnThis(), + info: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + error: vi.fn().mockReturnThis(), + fatal: vi.fn().mockReturnThis(), + child: childSpy, + flush: vi.fn(), + } as unknown as Pino.Logger); + + // Now import the logger with our updated mock + const { logger } = await import("./logger"); + + const context = { requestId: "123", userId: "456" }; + logger.withContext(context); + + // Check that the child method was called with the context + expect(childSpy).toHaveBeenCalledWith(context); + }); + + test("request creates a child logger with HTTP request info", async () => { + // Clear cache to get a fresh instance + vi.resetModules(); + + // Create a child spy before importing the logger + const childSpy = vi.fn().mockReturnThis(); + + // Set up the mock to capture the child call + vi.mocked(Pino).mockReturnValue({ + debug: vi.fn().mockReturnThis(), + info: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + error: vi.fn().mockReturnThis(), + fatal: vi.fn().mockReturnThis(), + child: childSpy, + flush: vi.fn(), + } as unknown as Pino.Logger); + + // Now import the logger with our updated mock + const { logger } = await import("./logger"); + + const req = { + method: "GET", + url: "https://example.com/test", + }; + + logger.request(req as unknown as Request); + + // Check that the child method was called with the expected object + expect(childSpy).toHaveBeenCalledWith({ + method: "GET", + url: "https://example.com/test", + }); + }); + + test("logger has all expected log level methods", async () => { + const { logger } = await import("./logger"); + + LOG_LEVELS.forEach((level) => { + expect(typeof logger[level]).toBe("function"); + }); + }); + + test("process handlers are attached in Node.js environment", async () => { + const processSpy = vi.spyOn(process, "on"); + processSpy.mockImplementation(() => process); // Return process for chaining + + process.env.NEXT_RUNTIME = "nodejs"; + + await import("./logger"); + + // Check that process handlers were attached + expect(processSpy).toHaveBeenCalledWith("uncaughtException", expect.any(Function)); + expect(processSpy).toHaveBeenCalledWith("unhandledRejection", expect.any(Function)); + expect(processSpy).toHaveBeenCalledWith("SIGTERM", expect.any(Function)); + expect(processSpy).toHaveBeenCalledWith("SIGINT", expect.any(Function)); + + processSpy.mockRestore(); + }); + + test("process handlers are not attached outside Node.js environment", async () => { + const processSpy = vi.spyOn(process, "on"); + processSpy.mockImplementation(() => process); // Return process for chaining + + process.env.NEXT_RUNTIME = "edge"; + + await import("./logger"); + + // No handlers should be attached for particular events + expect(processSpy).not.toHaveBeenCalledWith("uncaughtException", expect.any(Function)); + expect(processSpy).not.toHaveBeenCalledWith("unhandledRejection", expect.any(Function)); + expect(processSpy).not.toHaveBeenCalledWith("SIGTERM", expect.any(Function)); + expect(processSpy).not.toHaveBeenCalledWith("SIGINT", expect.any(Function)); + + processSpy.mockRestore(); + }); +}); diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 0000000000..525356cc94 --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,111 @@ +import Pino, { type Logger, type LoggerOptions, stdSerializers } from "pino"; +import { LOG_LEVELS, type TLogLevel, ZLogLevel } from "../types/logger"; + +const IS_PRODUCTION = !process.env.NODE_ENV || process.env.NODE_ENV === "production"; +const getLogLevel = (): TLogLevel => { + let logLevel: TLogLevel = "info"; + + if (IS_PRODUCTION) logLevel = "warn"; + + const envLogLevel = process.env.LOG_LEVEL; + + const logLevelResult = ZLogLevel.safeParse(envLogLevel); + if (logLevelResult.success) logLevel = logLevelResult.data; + + return logLevel; +}; + +const baseLoggerConfig: LoggerOptions = { + level: getLogLevel(), + serializers: { + err: stdSerializers.err, + req: stdSerializers.req, + res: stdSerializers.res, + }, + customLevels: { + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, + }, + useOnlyCustomLevels: true, + timestamp: true, + formatters: { + level: (label) => { + return { level: label }; + }, + }, + name: "formbricks", +}; + +const developmentConfig: LoggerOptions = { + ...baseLoggerConfig, + transport: { + target: "pino-pretty", + options: { + colorize: true, + levelFirst: true, + translateTime: "SYS:standard", + ignore: "pid,hostname,ip,requestId", + }, + }, +}; + +const productionConfig: LoggerOptions = { + ...baseLoggerConfig, +}; + +const logger: Logger = IS_PRODUCTION ? Pino(productionConfig) : Pino(developmentConfig); + +LOG_LEVELS.forEach((level) => { + logger[level] = logger[level].bind(logger); +}); + +const extendedLogger = { + ...logger, + withContext: (context: Record) => logger.child(context), + request: (req: Request) => + logger.child({ + method: req.method, + url: req.url, + }), +}; + +const handleShutdown = (event: string, err?: Error): void => { + if (err) { + logger.error(err, `Error during shutdown (${event})`); + } + logger.info({ event }, "Process is exiting"); + + logger.flush(); +}; + +// Create a separate function for attaching Node.js process handlers +const attachNodeProcessHandlers = (): void => { + // Only attach handlers if we're in a Node.js environment with full process support + if (process.env.NEXT_RUNTIME === "nodejs") { + process.on("uncaughtException", (err) => { + handleShutdown("uncaughtException", err); + }); + process.on("unhandledRejection", (err) => { + handleShutdown("unhandledRejection", err as Error); + }); + process.on("SIGTERM", () => { + handleShutdown("SIGTERM"); + }); + process.on("SIGINT", () => { + handleShutdown("SIGINT"); + }); + } +}; + +if (process.env.NEXT_RUNTIME === "nodejs") { + try { + attachNodeProcessHandlers(); + } catch (e) { + logger.error(e, "Error attaching process event handlers"); + } +} + +export { extendedLogger as logger }; diff --git a/packages/logger/src/vite-env.d.ts b/packages/logger/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/logger/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000000..3e48f73289 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true, + "strict": true + }, + "exclude": ["node_modules"], + "extends": "@formbricks/config-typescript/js-library.json", + "include": ["src", "package.json", "types"] +} diff --git a/packages/logger/types/logger.ts b/packages/logger/types/logger.ts new file mode 100644 index 0000000000..26cbbdc587 --- /dev/null +++ b/packages/logger/types/logger.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const LOG_LEVELS = ["debug", "info", "warn", "error", "fatal"] as const; + +export const ZLogLevel = z.enum(LOG_LEVELS); + +export type TLogLevel = z.infer; diff --git a/packages/logger/vite.config.ts b/packages/logger/vite.config.ts new file mode 100644 index 0000000000..91e81fc9e2 --- /dev/null +++ b/packages/logger/vite.config.ts @@ -0,0 +1,32 @@ +import { resolve } from "node:path"; +import { PluginOption, defineConfig } from "vite"; +import dts from "vite-plugin-dts"; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "formbricksLogger", + fileName: "index", + formats: ["es", "cjs"], + }, + rollupOptions: { + external: ["pino", "pino-pretty", "zod"], + output: { + exports: "named", + globals: { + pino: "pino", + "pino-pretty": "pinoPretty", + zod: "zod", + }, + }, + }, + sourcemap: true, + emptyOutDir: false, + }, + plugins: [ + dts({ + rollupTypes: true, + }) as PluginOption, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a296c60684..baabdb3df2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: '@formbricks/lib': specifier: workspace:* version: link:../../packages/lib + '@formbricks/logger': + specifier: workspace:* + version: link:../../packages/logger '@formbricks/surveys': specifier: workspace:* version: link:../../packages/surveys @@ -697,6 +700,9 @@ importers: packages/database: dependencies: + '@formbricks/logger': + specifier: workspace:* + version: link:../logger '@prisma/client': specifier: 6.0.1 version: 6.0.1(prisma@6.0.1) @@ -803,6 +809,9 @@ importers: '@formbricks/database': specifier: workspace:* version: link:../database + '@formbricks/logger': + specifier: workspace:* + version: link:../logger '@formbricks/types': specifier: workspace:* version: link:../types @@ -880,6 +889,34 @@ importers: specifier: 2.0.2 version: 2.0.2(typescript@5.8.2)(vitest@2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)) + packages/logger: + dependencies: + pino: + specifier: ^8.0.0 + version: 8.21.0 + pino-pretty: + specifier: ^10.0.0 + version: 10.3.1 + zod: + specifier: 3.24.1 + version: 3.24.1 + devDependencies: + '@formbricks/config-typescript': + specifier: workspace:* + version: link:../config-typescript + '@formbricks/eslint-config': + specifier: workspace:* + version: link:../config-eslint + vite: + specifier: ^6.2.0 + version: 6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vite-plugin-dts: + specifier: 4.3.0 + version: 4.3.0(@types/node@22.10.2)(rollup@4.36.0)(typescript@5.8.2)(vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vitest: + specifier: 3.0.7 + version: 3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + packages/react-native: dependencies: '@react-native-async-storage/async-storage': @@ -6714,6 +6751,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -7574,6 +7615,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -8356,6 +8400,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} @@ -8376,6 +8423,13 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} @@ -8872,6 +8926,9 @@ packages: help-me@3.0.0: resolution: {integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hermes-estree@0.19.1: resolution: {integrity: sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==} @@ -10822,6 +10879,10 @@ packages: resolution: {integrity: sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==} engines: {node: ^10.13.0 || >=12.0.0} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -11157,6 +11218,20 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + + pino-pretty@10.3.1: + resolution: {integrity: sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==} + hasBin: true + + pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + + pino@8.21.0: + resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -11467,6 +11542,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -11565,6 +11643,9 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -11837,6 +11918,10 @@ packages: readline@1.3.0: resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recast@0.21.5: resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} engines: {node: '>= 4'} @@ -12083,6 +12168,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -12316,6 +12405,9 @@ packages: resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonic-boom@3.8.1: + resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} + sort-object-keys@1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} @@ -12778,6 +12870,9 @@ packages: resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} engines: {node: '>=0.2.6'} + thread-stream@2.7.0: + resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} + throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -21634,6 +21729,8 @@ snapshots: at-least-node@1.0.0: {} + atomic-sleep@1.0.0: {} + autoprefixer@10.4.20(postcss@8.4.49): dependencies: browserslist: 4.24.4 @@ -22645,6 +22742,8 @@ snapshots: date-fns@4.1.0: {} + dateformat@4.6.3: {} + dayjs@1.11.13: {} de-indent@1.0.2: {} @@ -23705,6 +23804,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-copy@3.0.2: {} + fast-deep-equal@2.0.1: {} fast-deep-equal@3.1.3: {} @@ -23729,6 +23830,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + fast-shallow-equal@1.0.0: {} fast-uri@3.0.6: {} @@ -24302,6 +24407,8 @@ snapshots: glob: 7.2.3 readable-stream: 3.6.2 + help-me@5.0.0: {} + hermes-estree@0.19.1: {} hermes-estree@0.23.1: {} @@ -26670,6 +26777,8 @@ snapshots: oidc-token-hash@5.1.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -27012,6 +27121,44 @@ snapshots: pify@4.0.1: {} + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.7.0 + split2: 4.2.0 + + pino-pretty@10.3.1: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pump: 3.0.2 + readable-stream: 4.7.0 + secure-json-parse: 2.7.0 + sonic-boom: 3.8.1 + strip-json-comments: 3.1.1 + + pino-std-serializers@6.2.2: {} + + pino@8.21.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 3.8.1 + thread-stream: 2.7.0 + pirates@4.0.6: {} pkg-dir@3.0.0: @@ -27255,6 +27402,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@3.0.0: {} + process@0.11.10: {} progress@2.0.3: {} @@ -27356,6 +27505,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -27766,6 +27917,8 @@ snapshots: readline@1.3.0: {} + real-require@0.2.0: {} + recast@0.21.5: dependencies: ast-types: 0.15.2 @@ -28077,6 +28230,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} satori@0.12.0: @@ -28405,6 +28560,10 @@ snapshots: smart-buffer: 4.2.0 optional: true + sonic-boom@3.8.1: + dependencies: + atomic-sleep: 1.0.0 + sort-object-keys@1.1.3: {} sort-package-json@2.15.1: @@ -28922,6 +29081,10 @@ snapshots: thirty-two@1.0.2: {} + thread-stream@2.7.0: + dependencies: + real-require: 0.2.0 + throat@5.0.0: {} throttle-debounce@3.0.1: {} @@ -29535,6 +29698,25 @@ snapshots: - rollup - supports-color + vite-plugin-dts@4.3.0(@types/node@22.10.2)(rollup@4.36.0)(typescript@5.8.2)(vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@microsoft/api-extractor': 7.52.1(@types/node@22.10.2) + '@rollup/pluginutils': 5.1.4(rollup@4.36.0) + '@volar/typescript': 2.4.12 + '@vue/language-core': 2.1.6(typescript@5.8.2) + compare-versions: 6.1.1 + debug: 4.4.0 + kolorist: 1.8.0 + local-pkg: 0.5.1 + magic-string: 0.30.17 + typescript: 5.8.2 + optionalDependencies: + vite: 6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-node-polyfills@0.22.0(rollup@4.36.0)(vite@6.0.9(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.36.0) diff --git a/turbo.json b/turbo.json index 6d0ab28b60..de4e5ca8b8 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,9 @@ "dependsOn": ["@formbricks/api#build"], "persistent": true }, + "@formbricks/database#lint": { + "dependsOn": ["@formbricks/logger#build"] + }, "@formbricks/database#setup": { "dependsOn": ["db:up"] }, @@ -37,6 +40,7 @@ "@formbricks/js#lint": { "dependsOn": ["@formbricks/js-core#build"] }, + "@formbricks/js-core#go": { "cache": false, "dependsOn": ["@formbricks/database#db:setup", "@formbricks/api#build", "@formbricks/js#build"], @@ -48,6 +52,12 @@ "@formbricks/js-core#test": { "dependsOn": ["@formbricks/api#build"] }, + "@formbricks/lib#lint": { + "dependsOn": ["@formbricks/logger#build"] + }, + "@formbricks/lib#test": { + "dependsOn": ["@formbricks/logger#build"] + }, "@formbricks/react-native#build": { "dependsOn": ["^build"], "outputs": ["dist/**"] @@ -81,6 +91,12 @@ "dependsOn": ["@formbricks/database#db:setup", "@formbricks/js#build"], "persistent": true }, + "@formbricks/web#test": { + "dependsOn": ["@formbricks/logger#build"] + }, + "@formbricks/web#test:coverage": { + "dependsOn": ["@formbricks/logger#build"] + }, "build": { "dependsOn": ["^build"], "env": [ @@ -131,6 +147,7 @@ "IS_FORMBRICKS_CLOUD", "INTERCOM_APP_ID", "INTERCOM_SECRET_KEY", + "LOG_LEVEL", "MAIL_FROM", "MAIL_FROM_NAME", "NEXT_PUBLIC_LAYER_API_KEY", @@ -232,6 +249,7 @@ }, "db:setup": { "cache": false, + "dependsOn": ["@formbricks/logger#build"], "outputs": [] }, "db:start": { From 43d5d3d7193e9011f36d0872246b20b88a437821 Mon Sep 17 00:00:00 2001 From: Harsh Shrikant Bhat <90265455+harshsbhat@users.noreply.github.com> Date: Fri, 21 Mar 2025 18:44:56 +0530 Subject: [PATCH 02/54] chore: small email tweaks (#5019) --- apps/web/modules/email/components/email-template.tsx | 2 +- .../emails/general/email-customization-preview-email.tsx | 6 ++++-- .../modules/email/emails/invite/invite-accepted-email.tsx | 8 +++++--- apps/web/modules/email/emails/invite/invite-email.tsx | 8 +++++--- .../email/emails/survey/embed-survey-preview-email.tsx | 6 +++--- .../web/modules/email/emails/survey/link-survey-email.tsx | 6 +++--- .../email/emails/survey/response-finished-email.tsx | 4 ++-- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/web/modules/email/components/email-template.tsx b/apps/web/modules/email/components/email-template.tsx index 92ad28ab29..922e073e3f 100644 --- a/apps/web/modules/email/components/email-template.tsx +++ b/apps/web/modules/email/components/email-template.tsx @@ -30,7 +30,7 @@ export async function EmailTemplate({
{isDefaultLogo ? ( - Logo + Logo ) : ( - {t("emails.email_customization_preview_email_heading", { userName })} - {t("emails.email_customization_preview_email_text")} + + {t("emails.email_customization_preview_email_heading", { userName })} + + {t("emails.email_customization_preview_email_text")} ); diff --git a/apps/web/modules/email/emails/invite/invite-accepted-email.tsx b/apps/web/modules/email/emails/invite/invite-accepted-email.tsx index 841fcf015b..976f311858 100644 --- a/apps/web/modules/email/emails/invite/invite-accepted-email.tsx +++ b/apps/web/modules/email/emails/invite/invite-accepted-email.tsx @@ -1,5 +1,5 @@ import { getTranslate } from "@/tolgee/server"; -import { Container, Text } from "@react-email/components"; +import { Container, Heading, Text } from "@react-email/components"; import React from "react"; import { EmailFooter } from "../../components/email-footer"; import { EmailTemplate } from "../../components/email-template"; @@ -17,8 +17,10 @@ export async function InviteAcceptedEmail({ return ( - {t("emails.invite_accepted_email_heading", { inviterName })} - + + {t("emails.invite_accepted_email_heading", { inviterName })} {inviterName} + + {t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "} {t("emails.invite_accepted_email_text_par2")} diff --git a/apps/web/modules/email/emails/invite/invite-email.tsx b/apps/web/modules/email/emails/invite/invite-email.tsx index 55f0bf8011..1e87f50204 100644 --- a/apps/web/modules/email/emails/invite/invite-email.tsx +++ b/apps/web/modules/email/emails/invite/invite-email.tsx @@ -1,5 +1,5 @@ import { getTranslate } from "@/tolgee/server"; -import { Container, Text } from "@react-email/components"; +import { Container, Heading, Text } from "@react-email/components"; import React from "react"; import { EmailButton } from "../../components/email-button"; import { EmailFooter } from "../../components/email-footer"; @@ -20,8 +20,10 @@ export async function InviteEmail({ return ( - {t("emails.invite_email_heading", { inviteeName })} - + + {t("emails.invite_email_heading", { inviteeName })} {inviteeName} + + {t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "} {t("emails.invite_email_text_par2")} diff --git a/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx b/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx index 31762ea54d..b88c460a14 100644 --- a/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx +++ b/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx @@ -18,9 +18,9 @@ export async function EmbedSurveyPreviewEmail({ return ( - {t("emails.embed_survey_preview_email_heading")} - {t("emails.embed_survey_preview_email_text")} - + {t("emails.embed_survey_preview_email_heading")} + {t("emails.embed_survey_preview_email_text")} + {t("emails.embed_survey_preview_email_didnt_request")}{" "} {t("emails.embed_survey_preview_email_fight_spam")} diff --git a/apps/web/modules/email/emails/survey/link-survey-email.tsx b/apps/web/modules/email/emails/survey/link-survey-email.tsx index 82628d26e8..d59e2df77d 100644 --- a/apps/web/modules/email/emails/survey/link-survey-email.tsx +++ b/apps/web/modules/email/emails/survey/link-survey-email.tsx @@ -20,9 +20,9 @@ export async function LinkSurveyEmail({ return ( - {t("emails.verification_email_hey")} - {t("emails.verification_email_thanks")} - {t("emails.verification_email_to_fill_survey")} + {t("emails.verification_email_hey")} + {t("emails.verification_email_thanks")} + {t("emails.verification_email_to_fill_survey")} {t("emails.verification_email_survey_name")}: {surveyName} diff --git a/apps/web/modules/email/emails/survey/response-finished-email.tsx b/apps/web/modules/email/emails/survey/response-finished-email.tsx index 8e3426cd15..e8891d1130 100644 --- a/apps/web/modules/email/emails/survey/response-finished-email.tsx +++ b/apps/web/modules/email/emails/survey/response-finished-email.tsx @@ -96,8 +96,8 @@ export async function ResponseFinishedEmail({ - {t("emails.survey_response_finished_email_hey")} - + {t("emails.survey_response_finished_email_hey")} + {t("emails.survey_response_finished_email_congrats", { surveyName: survey.name, })} From 6a2a8b74c8f2b133b315e020e1382df388e710d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Mar 2025 04:24:15 +0100 Subject: [PATCH 03/54] chore(deps): bump the npm_and_yarn group across 3 directories with 1 update (#5035) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthias Nannt --- apps/demo/package.json | 2 +- .../components/template-list/lib/survey.ts | 78 +++++++- .../components/template-list/lib/utils.ts | 72 +------- apps/web/modules/survey/editor/lib/survey.ts | 73 +++++++- apps/web/modules/survey/editor/lib/utils.tsx | 70 ------- apps/web/package.json | 2 +- pnpm-lock.yaml | 173 +++++++++++------- 7 files changed, 254 insertions(+), 216 deletions(-) diff --git a/apps/demo/package.json b/apps/demo/package.json index ce27d6244b..544e4755b8 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -13,7 +13,7 @@ "dependencies": { "@formbricks/js": "workspace:*", "lucide-react": "0.468.0", - "next": "15.1.2", + "next": "15.2.3", "react": "19.0.0", "react-dom": "19.0.0" }, diff --git a/apps/web/modules/survey/components/template-list/lib/survey.ts b/apps/web/modules/survey/components/template-list/lib/survey.ts index 15562ed497..d8ae98fcc8 100644 --- a/apps/web/modules/survey/components/template-list/lib/survey.ts +++ b/apps/web/modules/survey/components/template-list/lib/survey.ts @@ -1,20 +1,18 @@ import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { subscribeOrganizationMembersToSurveyResponses } from "@/modules/survey/components/template-list/lib/organization"; -import { handleTriggerUpdates } from "@/modules/survey/components/template-list/lib/utils"; +import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger"; import { getActionClasses } from "@/modules/survey/lib/action-class"; import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization"; import { selectSurvey } from "@/modules/survey/lib/survey"; -import { getInsightsEnabled } from "@/modules/survey/lib/utils"; -import { doesSurveyHasOpenTextQuestion } from "@/modules/survey/lib/utils"; -import { Prisma } from "@prisma/client"; +import { doesSurveyHasOpenTextQuestion, getInsightsEnabled } from "@/modules/survey/lib/utils"; +import { ActionClass, Prisma } from "@prisma/client"; 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"; +import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { TSurvey, TSurveyCreateInput } from "@formbricks/types/surveys/types"; export const createSurvey = async ( environmentId: string, @@ -184,3 +182,69 @@ export const createSurvey = async ( throw error; } }; + +const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => { + if (!triggers) return; + + // check if all the triggers are valid + triggers.forEach((trigger) => { + if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) { + throw new InvalidInputError("Invalid trigger id"); + } + }); + + // check if all the triggers are unique + const triggerIds = triggers.map((trigger) => trigger.actionClass.id); + + if (new Set(triggerIds).size !== triggerIds.length) { + throw new InvalidInputError("Duplicate trigger id"); + } +}; + +export const handleTriggerUpdates = ( + updatedTriggers: TSurvey["triggers"], + currentTriggers: TSurvey["triggers"], + actionClasses: ActionClass[] +) => { + if (!updatedTriggers) return {}; + checkTriggersValidity(updatedTriggers, actionClasses); + + const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id); + const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id); + + // added triggers are triggers that are not in the current triggers and are there in the new triggers + const addedTriggers = updatedTriggers.filter( + (trigger) => !currentTriggerIds.includes(trigger.actionClass.id) + ); + + // deleted triggers are triggers that are not in the new triggers and are there in the current triggers + const deletedTriggers = currentTriggers.filter( + (trigger) => !updatedTriggerIds.includes(trigger.actionClass.id) + ); + + // Construct the triggers update object + const triggersUpdate: TriggerUpdate = {}; + + if (addedTriggers.length > 0) { + triggersUpdate.create = addedTriggers.map((trigger) => ({ + actionClassId: trigger.actionClass.id, + })); + } + + if (deletedTriggers.length > 0) { + // disconnect the public triggers from the survey + triggersUpdate.deleteMany = { + actionClassId: { + in: deletedTriggers.map((trigger) => trigger.actionClass.id), + }, + }; + } + + [...addedTriggers, ...deletedTriggers].forEach((trigger) => { + surveyCache.revalidate({ + actionClassId: trigger.actionClass.id, + }); + }); + + return triggersUpdate; +}; diff --git a/apps/web/modules/survey/components/template-list/lib/utils.ts b/apps/web/modules/survey/components/template-list/lib/utils.ts index 4f19a09b09..da5e3fa70e 100644 --- a/apps/web/modules/survey/components/template-list/lib/utils.ts +++ b/apps/web/modules/survey/components/template-list/lib/utils.ts @@ -1,12 +1,8 @@ -import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger"; -import { ActionClass } from "@prisma/client"; import { TFnType } from "@tolgee/react"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { InvalidInputError } from "@formbricks/types/errors"; import { TProject, TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project"; -import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types"; +import { TSurveyQuestion } from "@formbricks/types/surveys/types"; import { TTemplate, TTemplateRole } from "@formbricks/types/templates"; export const replaceQuestionPresetPlaceholders = ( @@ -60,69 +56,3 @@ export const getRoleMapping = (t: TFnType): { value: TTemplateRole; label: strin { value: "sales", label: t("common.sales") }, { value: "peopleManager", label: t("common.people_manager") }, ]; - -const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => { - if (!triggers) return; - - // check if all the triggers are valid - triggers.forEach((trigger) => { - if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) { - throw new InvalidInputError("Invalid trigger id"); - } - }); - - // check if all the triggers are unique - const triggerIds = triggers.map((trigger) => trigger.actionClass.id); - - if (new Set(triggerIds).size !== triggerIds.length) { - throw new InvalidInputError("Duplicate trigger id"); - } -}; - -export const handleTriggerUpdates = ( - updatedTriggers: TSurvey["triggers"], - currentTriggers: TSurvey["triggers"], - actionClasses: ActionClass[] -) => { - if (!updatedTriggers) return {}; - checkTriggersValidity(updatedTriggers, actionClasses); - - const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id); - const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id); - - // added triggers are triggers that are not in the current triggers and are there in the new triggers - const addedTriggers = updatedTriggers.filter( - (trigger) => !currentTriggerIds.includes(trigger.actionClass.id) - ); - - // deleted triggers are triggers that are not in the new triggers and are there in the current triggers - const deletedTriggers = currentTriggers.filter( - (trigger) => !updatedTriggerIds.includes(trigger.actionClass.id) - ); - - // Construct the triggers update object - const triggersUpdate: TriggerUpdate = {}; - - if (addedTriggers.length > 0) { - triggersUpdate.create = addedTriggers.map((trigger) => ({ - actionClassId: trigger.actionClass.id, - })); - } - - if (deletedTriggers.length > 0) { - // disconnect the public triggers from the survey - triggersUpdate.deleteMany = { - actionClassId: { - in: deletedTriggers.map((trigger) => trigger.actionClass.id), - }, - }; - } - - [...addedTriggers, ...deletedTriggers].forEach((trigger) => { - surveyCache.revalidate({ - actionClassId: trigger.actionClass.id, - }); - }); - - return triggersUpdate; -}; diff --git a/apps/web/modules/survey/editor/lib/survey.ts b/apps/web/modules/survey/editor/lib/survey.ts index 2c99b6d9a8..17e3e4c809 100644 --- a/apps/web/modules/survey/editor/lib/survey.ts +++ b/apps/web/modules/survey/editor/lib/survey.ts @@ -1,11 +1,10 @@ import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; -import { handleTriggerUpdates } from "@/modules/survey/editor/lib/utils"; +import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger"; import { getActionClasses } from "@/modules/survey/lib/action-class"; import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization"; import { getSurvey, selectSurvey } from "@/modules/survey/lib/survey"; -import { getInsightsEnabled } from "@/modules/survey/lib/utils"; -import { doesSurveyHasOpenTextQuestion } from "@/modules/survey/lib/utils"; -import { Prisma, Survey } from "@prisma/client"; +import { doesSurveyHasOpenTextQuestion, getInsightsEnabled } from "@/modules/survey/lib/utils"; +import { ActionClass, Prisma, Survey } from "@prisma/client"; import { prisma } from "@formbricks/database"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { surveyCache } from "@formbricks/lib/survey/cache"; @@ -380,3 +379,69 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => throw error; } }; + +const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => { + if (!triggers) return; + + // check if all the triggers are valid + triggers.forEach((trigger) => { + if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) { + throw new InvalidInputError("Invalid trigger id"); + } + }); + + // check if all the triggers are unique + const triggerIds = triggers.map((trigger) => trigger.actionClass.id); + + if (new Set(triggerIds).size !== triggerIds.length) { + throw new InvalidInputError("Duplicate trigger id"); + } +}; + +const handleTriggerUpdates = ( + updatedTriggers: TSurvey["triggers"], + currentTriggers: TSurvey["triggers"], + actionClasses: ActionClass[] +) => { + if (!updatedTriggers) return {}; + checkTriggersValidity(updatedTriggers, actionClasses); + + const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id); + const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id); + + // added triggers are triggers that are not in the current triggers and are there in the new triggers + const addedTriggers = updatedTriggers.filter( + (trigger) => !currentTriggerIds.includes(trigger.actionClass.id) + ); + + // deleted triggers are triggers that are not in the new triggers and are there in the current triggers + const deletedTriggers = currentTriggers.filter( + (trigger) => !updatedTriggerIds.includes(trigger.actionClass.id) + ); + + // Construct the triggers update object + const triggersUpdate: TriggerUpdate = {}; + + if (addedTriggers.length > 0) { + triggersUpdate.create = addedTriggers.map((trigger) => ({ + actionClassId: trigger.actionClass.id, + })); + } + + if (deletedTriggers.length > 0) { + // disconnect the public triggers from the survey + triggersUpdate.deleteMany = { + actionClassId: { + in: deletedTriggers.map((trigger) => trigger.actionClass.id), + }, + }; + } + + [...addedTriggers, ...deletedTriggers].forEach((trigger) => { + surveyCache.revalidate({ + actionClassId: trigger.actionClass.id, + }); + }); + + return triggersUpdate; +}; diff --git a/apps/web/modules/survey/editor/lib/utils.tsx b/apps/web/modules/survey/editor/lib/utils.tsx index 5e06a1cf50..8f89c80dbe 100644 --- a/apps/web/modules/survey/editor/lib/utils.tsx +++ b/apps/web/modules/survey/editor/lib/utils.tsx @@ -1,15 +1,11 @@ -import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger"; import { getQuestionTypes } from "@/modules/survey/lib/questions"; import { TComboboxGroupedOption, TComboboxOption } from "@/modules/ui/components/input-combo-box"; -import { ActionClass } from "@prisma/client"; import { TFnType } from "@tolgee/react"; import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react"; import { HTMLInputTypeAttribute } from "react"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils"; import { recallToHeadline } from "@formbricks/lib/utils/recall"; -import { InvalidInputError } from "@formbricks/types/errors"; import { TConditionGroup, TLeftOperand, @@ -1177,69 +1173,3 @@ export const findEndingCardUsedInLogic = (survey: TSurvey, endingCardId: string) (question) => question.logicFallback === endingCardId || question.logic?.some(isUsedInLogicRule) ); }; - -const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => { - if (!triggers) return; - - // check if all the triggers are valid - triggers.forEach((trigger) => { - if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) { - throw new InvalidInputError("Invalid trigger id"); - } - }); - - // check if all the triggers are unique - const triggerIds = triggers.map((trigger) => trigger.actionClass.id); - - if (new Set(triggerIds).size !== triggerIds.length) { - throw new InvalidInputError("Duplicate trigger id"); - } -}; - -export const handleTriggerUpdates = ( - updatedTriggers: TSurvey["triggers"], - currentTriggers: TSurvey["triggers"], - actionClasses: ActionClass[] -) => { - if (!updatedTriggers) return {}; - checkTriggersValidity(updatedTriggers, actionClasses); - - const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id); - const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id); - - // added triggers are triggers that are not in the current triggers and are there in the new triggers - const addedTriggers = updatedTriggers.filter( - (trigger) => !currentTriggerIds.includes(trigger.actionClass.id) - ); - - // deleted triggers are triggers that are not in the new triggers and are there in the current triggers - const deletedTriggers = currentTriggers.filter( - (trigger) => !updatedTriggerIds.includes(trigger.actionClass.id) - ); - - // Construct the triggers update object - const triggersUpdate: TriggerUpdate = {}; - - if (addedTriggers.length > 0) { - triggersUpdate.create = addedTriggers.map((trigger) => ({ - actionClassId: trigger.actionClass.id, - })); - } - - if (deletedTriggers.length > 0) { - // disconnect the public triggers from the survey - triggersUpdate.deleteMany = { - actionClassId: { - in: deletedTriggers.map((trigger) => trigger.actionClass.id), - }, - }; - } - - [...addedTriggers, ...deletedTriggers].forEach((trigger) => { - surveyCache.revalidate({ - actionClassId: trigger.actionClass.id, - }); - }); - - return triggersUpdate; -}; diff --git a/apps/web/package.json b/apps/web/package.json index 0b67048bac..ed460a07de 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -105,7 +105,7 @@ "lru-cache": "11.0.2", "lucide-react": "0.468.0", "mime": "4.0.4", - "next": "15.1.2", + "next": "15.2.3", "next-auth": "4.24.11", "next-safe-action": "7.10.2", "node-fetch": "3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baabdb3df2..12f45034fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ importers: specifier: 0.468.0 version: 0.468.0(react@19.0.0) next: - specifier: 15.1.2 - version: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 15.2.3 + version: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -338,7 +338,7 @@ importers: version: 0.0.31(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@sentry/nextjs': specifier: 8.52.0 - version: 8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1) + version: 8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1) '@tailwindcss/forms': specifier: 0.5.9 version: 0.5.9(tailwindcss@3.4.16(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2))) @@ -374,7 +374,7 @@ importers: version: 1.10.0(@opentelemetry/api-logs@0.56.0)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)) '@vercel/speed-insights': specifier: 1.1.0 - version: 1.1.0(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 1.1.0(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) ai: specifier: 4.1.17 version: 4.1.17(react@19.0.0)(zod@3.24.1) @@ -445,14 +445,14 @@ importers: specifier: 4.0.4 version: 4.0.4 next: - specifier: 15.1.2 - version: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 15.2.3 + version: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-auth: specifier: 4.24.11 - version: 4.24.11(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.24.11(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-safe-action: specifier: 7.10.2 - version: 7.10.2(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1) + version: 7.10.2(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1) node-fetch: specifier: 3.3.2 version: 3.3.2 @@ -558,7 +558,7 @@ importers: version: link:../../packages/config-eslint '@neshca/cache-handler': specifier: 1.9.0 - version: 1.9.0(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0) + version: 1.9.0(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0) '@testing-library/react': specifier: 16.2.0 version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -844,7 +844,7 @@ importers: version: 5.0.9 next-auth: specifier: 4.24.11 - version: 4.24.11(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.24.11(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) posthog-node: specifier: 4.4.1 version: 4.4.1 @@ -1454,6 +1454,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-define-polyfill-provider@0.6.4': + resolution: {integrity: sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-environment-visitor@7.24.7': resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} @@ -3391,56 +3396,56 @@ packages: next: '>= 13.5.1 < 15' redis: '>= 4.6' - '@next/env@15.1.2': - resolution: {integrity: sha512-Hm3jIGsoUl6RLB1vzY+dZeqb+/kWPZ+h34yiWxW0dV87l8Im/eMOwpOA+a0L78U0HM04syEjXuRlCozqpwuojQ==} + '@next/env@15.2.3': + resolution: {integrity: sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==} '@next/eslint-plugin-next@15.1.0': resolution: {integrity: sha512-+jPT0h+nelBT6HC9ZCHGc7DgGVy04cv4shYdAe6tKlEbjQUtwU3LzQhzbDHQyY2m6g39m6B0kOFVuLGBrxxbGg==} - '@next/swc-darwin-arm64@15.1.2': - resolution: {integrity: sha512-b9TN7q+j5/7+rGLhFAVZiKJGIASuo8tWvInGfAd8wsULjB1uNGRCj1z1WZwwPWzVQbIKWFYqc+9L7W09qwt52w==} + '@next/swc-darwin-arm64@15.2.3': + resolution: {integrity: sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.1.2': - resolution: {integrity: sha512-caR62jNDUCU+qobStO6YJ05p9E+LR0EoXh1EEmyU69cYydsAy7drMcOlUlRtQihM6K6QfvNwJuLhsHcCzNpqtA==} + '@next/swc-darwin-x64@15.2.3': + resolution: {integrity: sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.1.2': - resolution: {integrity: sha512-fHHXBusURjBmN6VBUtu6/5s7cCeEkuGAb/ZZiGHBLVBXMBy4D5QpM8P33Or8JD1nlOjm/ZT9sEE5HouQ0F+hUA==} + '@next/swc-linux-arm64-gnu@15.2.3': + resolution: {integrity: sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.1.2': - resolution: {integrity: sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==} + '@next/swc-linux-arm64-musl@15.2.3': + resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.1.2': - resolution: {integrity: sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==} + '@next/swc-linux-x64-gnu@15.2.3': + resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.1.2': - resolution: {integrity: sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==} + '@next/swc-linux-x64-musl@15.2.3': + resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.1.2': - resolution: {integrity: sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==} + '@next/swc-win32-arm64-msvc@15.2.3': + resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.1.2': - resolution: {integrity: sha512-D3cNA8NoT3aWISWmo7HF5Eyko/0OdOO+VagkoJuiTk7pyX3P/b+n8XA/MYvyR+xSVcbKn68B1rY9fgqjNISqzQ==} + '@next/swc-win32-x64-msvc@15.2.3': + resolution: {integrity: sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -6811,6 +6816,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs2@0.4.13: + resolution: {integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.11.1: resolution: {integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==} peerDependencies: @@ -6821,6 +6831,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.4: + resolution: {integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-native-web@0.19.13: resolution: {integrity: sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==} @@ -10578,6 +10593,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@5.0.9: resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} engines: {node: ^18 || >=20} @@ -10645,8 +10665,8 @@ packages: zod: optional: true - next@15.1.2: - resolution: {integrity: sha512-nLJDV7peNy+0oHlmY2JZjzMfJ8Aj0/dd3jCwSZS8ZiO5nkQfcZRqDrRN3U5rJtqVTQneIOGZzb6LCNrk7trMCQ==} + next@15.2.3: + resolution: {integrity: sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -15043,6 +15063,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-plugin-utils': 7.26.5 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + '@babel/helper-environment-visitor@7.24.7': dependencies: '@babel/types': 7.26.10 @@ -15814,9 +15845,9 @@ snapshots: '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) - babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.26.0) babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.26.0) - babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.26.0) core-js-compat: 3.41.0 semver: 6.3.1 transitivePeerDependencies: @@ -17371,41 +17402,41 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@neshca/cache-handler@1.9.0(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0)': + '@neshca/cache-handler@1.9.0(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0)': dependencies: cluster-key-slot: 1.1.2 lru-cache: 10.4.3 - next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) redis: 4.7.0 - '@next/env@15.1.2': {} + '@next/env@15.2.3': {} '@next/eslint-plugin-next@15.1.0': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.1.2': + '@next/swc-darwin-arm64@15.2.3': optional: true - '@next/swc-darwin-x64@15.1.2': + '@next/swc-darwin-x64@15.2.3': optional: true - '@next/swc-linux-arm64-gnu@15.1.2': + '@next/swc-linux-arm64-gnu@15.2.3': optional: true - '@next/swc-linux-arm64-musl@15.1.2': + '@next/swc-linux-arm64-musl@15.2.3': optional: true - '@next/swc-linux-x64-gnu@15.1.2': + '@next/swc-linux-x64-gnu@15.2.3': optional: true - '@next/swc-linux-x64-musl@15.1.2': + '@next/swc-linux-x64-musl@15.2.3': optional: true - '@next/swc-win32-arm64-msvc@15.1.2': + '@next/swc-win32-arm64-msvc@15.2.3': optional: true - '@next/swc-win32-x64-msvc@15.1.2': + '@next/swc-win32-x64-msvc@15.2.3': optional: true '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': @@ -19537,7 +19568,7 @@ snapshots: '@sentry/core@8.52.0': {} - '@sentry/nextjs@8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1)': + '@sentry/nextjs@8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.30.0 @@ -19550,7 +19581,7 @@ snapshots: '@sentry/vercel-edge': 8.52.0 '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1) chalk: 3.0.0 - next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) resolve: 1.22.8 rollup: 3.29.5 stacktrace-parser: 0.1.11 @@ -20962,9 +20993,9 @@ snapshots: '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@vercel/speed-insights@1.1.0(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@vercel/speed-insights@1.1.0(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': optionalDependencies: - next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.8.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))': @@ -21825,6 +21856,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.26.0): + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -21840,6 +21880,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + babel-plugin-react-native-web@0.19.13: {} babel-plugin-syntax-hermes-parser@0.23.1: @@ -26465,6 +26512,8 @@ snapshots: nanoid@3.3.10: {} + nanoid@3.3.11: {} + nanoid@5.0.9: {} napi-build-utils@2.0.0: {} @@ -26483,13 +26532,13 @@ snapshots: new-github-issue-url@0.2.1: {} - next-auth@4.24.11(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next-auth@4.24.11(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@babel/runtime': 7.26.10 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.25.2 @@ -26500,17 +26549,17 @@ snapshots: optionalDependencies: nodemailer: 6.9.16 - next-safe-action@7.10.2(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1): + next-safe-action@7.10.2(next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1): dependencies: - next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: zod: 3.24.1 - next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.1.2 + '@next/env': 15.2.3 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -26520,14 +26569,14 @@ snapshots: react-dom: 19.0.0(react@19.0.0) styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.1.2 - '@next/swc-darwin-x64': 15.1.2 - '@next/swc-linux-arm64-gnu': 15.1.2 - '@next/swc-linux-arm64-musl': 15.1.2 - '@next/swc-linux-x64-gnu': 15.1.2 - '@next/swc-linux-x64-musl': 15.1.2 - '@next/swc-win32-arm64-msvc': 15.1.2 - '@next/swc-win32-x64-msvc': 15.1.2 + '@next/swc-darwin-arm64': 15.2.3 + '@next/swc-darwin-x64': 15.2.3 + '@next/swc-linux-arm64-gnu': 15.2.3 + '@next/swc-linux-arm64-musl': 15.2.3 + '@next/swc-linux-x64-gnu': 15.2.3 + '@next/swc-linux-x64-musl': 15.2.3 + '@next/swc-win32-arm64-msvc': 15.2.3 + '@next/swc-win32-x64-msvc': 15.2.3 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.49.1 sharp: 0.33.5 @@ -27259,7 +27308,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.10 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 From d98eb5b46ff82a1096382f3dd1ba86208b036c0f Mon Sep 17 00:00:00 2001 From: Peter Pesti-Varga Date: Sun, 23 Mar 2025 01:45:53 +0100 Subject: [PATCH 04/54] fix: Trust server in the iOS webview to allow to load the survey package (#5024) --- .../FormbricksSDK/WebView/SurveyWebView.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift b/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift index f5abf2cde6..e5297c8fdb 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift @@ -26,6 +26,7 @@ struct SurveyWebView: UIViewRepresentable { webView.isOpaque = false webView.backgroundColor = UIColor.clear webView.isInspectable = true + webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator return webView } @@ -51,7 +52,7 @@ struct SurveyWebView: UIViewRepresentable { } extension SurveyWebView { - class Coordinator: NSObject, WKUIDelegate { + class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate { // webView function handles Javascipt alert func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert) @@ -59,6 +60,14 @@ extension SurveyWebView { UIApplication.safeKeyWindow?.rootViewController?.presentedViewController?.present(alertController, animated: true) completionHandler() } + + func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if let serverTrust = challenge.protectionSpace.serverTrust { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } else { + completionHandler(.useCredential, nil) + } + } } } From 71ebde06f43408d665b8b257d87713367d7ea56b Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Mon, 24 Mar 2025 04:15:31 +0100 Subject: [PATCH 05/54] docs: add prometheus to monitoring docs (#5042) --- docs/self-hosting/setup/monitoring.mdx | 146 ++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 17 deletions(-) diff --git a/docs/self-hosting/setup/monitoring.mdx b/docs/self-hosting/setup/monitoring.mdx index 05bcceab57..127f944617 100644 --- a/docs/self-hosting/setup/monitoring.mdx +++ b/docs/self-hosting/setup/monitoring.mdx @@ -8,7 +8,52 @@ icon: "magnifying-glass-chart" Formbricks follows Next.js best practices with all logs being written to stdout/stderr, making it easy to collect and forward logs to your preferred logging solution. -### Docker Container Logs +### Log Levels + +- `debug`: Detailed information for debugging purposes. +- `info`: General information about the system's operation. +- `warn`: Potential issues that may require attention. +- `error`: Errors that occur during the operation of the system. +- `fatal`: Critical errors that cause the system to crash. + +### Log Format + +Formbricks uses JSON format for logs, which is structured and easy to parse. Each log entry includes: + +- `level`: The log level (e.g., info, error). +- `time`: Timestamp of the log entry in milliseconds since epoch. +- `pid`: Process ID of the application. +- `hostname`: Hostname of the server where the log was generated. +- `requestId`: Unique identifier for the request (if applicable). +- `userId`: Unique identifier for the user (if applicable). +- `msg`: The log message. +- `stack`: Stack trace (if applicable). +- `data`: Additional data related to the log entry. + +### Example Log Entry + +```json +{ + "hostname": "server-1", + "level": 30, + "msg": "User logged in successfully", + "pid": 12345, + "requestId": "abc-123", + "time": 1710000000000, + "userId": "user-789" +} +``` + +### Configuring Log Levels + +You can configure the minimum log level using the `LOG_LEVEL` environment variable. Valid values: `debug`, `info`, `warn`, `error`, `fatal`. +The default log level in production environments is `warn`, while in development environments it is `debug`. + +```env +LOG_LEVEL=debug +``` + +### AccessDocker Container Logs ```bash # One-Click setup @@ -20,7 +65,7 @@ docker logs docker logs -f # Follow logs ``` -### Kubernetes Pod Logs +### Access Kubernetes Pod Logs ```bash kubectl logs -n @@ -37,22 +82,29 @@ Since all logs are written to stdout/stderr, you can integrate with various logg - Splunk - CloudWatch Logs (AWS) -## OpenTelemetry Integration (Beta) +## OpenTelemetry Integration -Formbricks leverages Next.js's built-in OpenTelemetry instrumentation for comprehensive observability. When enabled, it automatically instruments various aspects of your application. +Formbricks offers two complementary observability approaches: -Set the following environment variables: +1. **Next.js OpenTelemetry** - For tracing and APM integration +2. **Prometheus Integration** - For metrics collection and monitoring + +### Next.js OpenTelemetry for Tracing + +Formbricks leverages Next.js's built-in OpenTelemetry instrumentation for tracing and APM integration. When enabled, it automatically instruments various aspects of your application, providing detailed insights into request flows and performance. + +To enable Next.js OpenTelemetry, set the following environment variables: ```env OTEL_ENABLED=true -OTEL_ENDPOINT= +OTEL_ENDPOINT= # e.g., http://localhost:4318/v1/traces for OTLP HTTP OTEL_SERVICE_NAME=formbricks NEXT_OTEL_VERBOSE=1 # Optional: enables detailed tracing ``` -### Default Instrumentation +#### Default Next.js Instrumentation -The OpenTelemetry integration automatically tracks: +The Next.js OpenTelemetry integration automatically tracks: - HTTP requests and responses - Route rendering @@ -61,24 +113,84 @@ The OpenTelemetry integration automatically tracks: - Database queries - External API calls -### Supported Backends +#### Supported Backends for Tracing -OpenTelemetry can export data to: +OpenTelemetry trace data can be exported to various observability platforms: + +##### Tracing Backends - Jaeger - Zipkin -- Prometheus +- Tempo + +##### APM & Full-Stack Observability Platforms + - New Relic - Datadog +- Dynatrace - Azure Monitor +- AWS X-Ray +- Google Cloud Trace -### Key Metrics +## Prometheus Integration -- HTTP request duration -- Database performance -- Memory usage -- Response times -- Error rates +Formbricks implements a dedicated Prometheus metrics exporter using OpenTelemetry. This integration runs a metrics server on a separate port and exposes metrics in Prometheus format for scraping. It focuses specifically on host and runtime metrics rather than application-specific traces. + +### Configuration + +To enable and configure the Prometheus exporter, set the following environment variables: + +```env +PROMETHEUS_ENABLED=1 +PROMETHEUS_EXPORTER_PORT=9464 # Optional, defaults to 9464 +``` + +The `PROMETHEUS_ENABLED` environment variable must be set to `1` to enable metrics collection. The `PROMETHEUS_EXPORTER_PORT` variable is optional and defaults to 9464 if not specified. + +The exporter listens on all network interfaces (0.0.0.0) and exposes metrics at the `/metrics` endpoint. + +### Available Metrics + +The metrics exported by the Prometheus integration include: + +- **Host Metrics**: + + - CPU usage (user, system, idle) + - Memory usage (used, free, cached) + - Disk I/O (reads, writes) + - Network I/O (bytes in/out, packets in/out) + +- **HTTP Metrics**: + + - Request counts + - Request durations + - Error rates + +- **Runtime Metrics**: + - Garbage collection frequency and duration + - Event loop lag + - Heap statistics (size, used, available) + +### Collecting Metrics + +You can scrape metrics from your Prometheus server by adding the following to your Prometheus configuration: + +```yaml +scrape_configs: + - job_name: "formbricks" + static_configs: + - targets: ["your-formbricks-host:9464"] +``` + +### Resource Attributes + +The metrics include resource attributes automatically detected from: + +- Environment variables +- Process information +- Host information + +These attributes can help you filter and group metrics in your dashboards. ## Health Checks From 8649522b5b7aac65a370780f9cc4e093a111edef Mon Sep 17 00:00:00 2001 From: Piyush Jain <122745947+d3vb0ox@users.noreply.github.com> Date: Mon, 24 Mar 2025 09:21:32 +0530 Subject: [PATCH 06/54] chore(actions): Update github actions to follow new release pattern (#5037) --- .github/workflows/deploy-formbricks-cloud.yml | 64 +++++++ .github/workflows/formbricks-deploy.yml | 31 ++++ .github/workflows/release-docker-github.yml | 13 +- .github/workflows/release-docker.yml | 59 ------- .github/workflows/release-helm-chart.yml | 15 +- infra/formbricks-cloud-helm/helmfile.yaml | 17 ++ .../formbricks-cloud-helm/values.yaml.gotmpl | 164 ++++++++++++++++++ infra/terraform/rds.tf | 9 + 8 files changed, 303 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/deploy-formbricks-cloud.yml create mode 100644 .github/workflows/formbricks-deploy.yml delete mode 100644 .github/workflows/release-docker.yml create mode 100644 infra/formbricks-cloud-helm/helmfile.yaml create mode 100644 infra/formbricks-cloud-helm/values.yaml.gotmpl diff --git a/.github/workflows/deploy-formbricks-cloud.yml b/.github/workflows/deploy-formbricks-cloud.yml new file mode 100644 index 0000000000..bff5c196e3 --- /dev/null +++ b/.github/workflows/deploy-formbricks-cloud.yml @@ -0,0 +1,64 @@ +name: Formbricks Cloud Deployment + +on: + workflow_dispatch: + inputs: + VERSION: + description: 'The version of the Docker image to release' + required: true + type: string + REPOSITORY: + description: 'The repository to use for the Docker image' + required: false + type: string + default: 'ghcr.io/formbricks/formbricks' + workflow_call: + inputs: + VERSION: + description: 'The version of the Docker image to release' + required: true + type: string + REPOSITORY: + description: 'The repository to use for the Docker image' + required: false + type: string + default: 'ghcr.io/formbricks/formbricks' + +permissions: + id-token: write + contents: write + +jobs: + helmfile-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} + aws-region: "eu-central-1" + + - name: Setup Cluster Access + run: | + aws eks update-kubeconfig --name formbricks-prod-eks --region eu-central-1 + env: + AWS_REGION: eu-central-1 + + - uses: helmfile/helmfile-action@v2 + env: + VERSION: ${{ inputs.VERSION }} + REPOSITORY: ${{ inputs.REPOSITORY }} + FORMBRICKS_S3_BUCKET: ${{ secrets.FORMBRICKS_S3_BUCKET }} + FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }} + FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }} + with: + helm-plugins: > + https://github.com/databus23/helm-diff, + https://github.com/jkroepke/helm-secrets + helmfile-args: apply + helmfile-auto-init: "false" + helmfile-workdirectory: infra/formbricks-cloud-helm + diff --git a/.github/workflows/formbricks-deploy.yml b/.github/workflows/formbricks-deploy.yml new file mode 100644 index 0000000000..fb2fdb206c --- /dev/null +++ b/.github/workflows/formbricks-deploy.yml @@ -0,0 +1,31 @@ +name: Build and Deploy Formbricks + +on: + workflow_dispatch: + push: + tags: + - "v*" + +jobs: + docker-build: + name: Build stable docker image + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/workflows/release-docker-github.yml + + helm-chart-release: + name: Release Helm Chart + uses: ./.github/workflows/release-helm-chart.yml + needs: + - docker-build + with: + VERSION: ${{ needs.docker-build.outputs.VERSION }} + + deploy-formbricks-cloud: + name: Deploy Helm Chart + secrets: inherit + uses: ./.github/workflows/deploy-formbricks-cloud.yml + needs: + - docker-build + - helm-chart-release + with: + VERSION: ${{ needs.docker-build.outputs.VERSION }} diff --git a/.github/workflows/release-docker-github.yml b/.github/workflows/release-docker-github.yml index 837dcfe4b5..c09d66d553 100644 --- a/.github/workflows/release-docker-github.yml +++ b/.github/workflows/release-docker-github.yml @@ -6,10 +6,11 @@ name: Docker Release to Github # documentation. on: - workflow_dispatch: - push: - tags: - - "v*" + workflow_call: + outputs: + VERSION: + description: release version + value: ${{ jobs.build.outputs.VERSION }} env: # Use docker.io for Docker Hub if empty @@ -33,6 +34,9 @@ jobs: # with sigstore/fulcio when running outside of PRs. id-token: write + outputs: + VERSION: ${{ steps.extract_release_tag.outputs.VERSION }} + steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 @@ -48,6 +52,7 @@ jobs: TAG=${{ github.ref }} TAG=${TAG#refs/tags/v} echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV + echo "VERSION=$TAG" >> $GITHUB_OUTPUT - name: Update package.json version run: | diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml deleted file mode 100644 index d3367333ca..0000000000 --- a/.github/workflows/release-docker.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release on Dockerhub - -on: - push: - tags: - - "v*" - -permissions: - contents: read - -jobs: - release-image-on-dockerhub: - name: Release on Dockerhub - permissions: - contents: read - runs-on: ubuntu-latest - env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public" - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 - with: - egress-policy: audit - - - name: Checkout Repo - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - - - name: Get Release Tag - id: extract_release_tag - run: | - TAG=${{ github.ref }} - TAG=${TAG#refs/tags/v} - echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV - - - name: Update package.json version - run: | - sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json - cat ./apps/web/package.json | grep version - - - name: Log in to Docker Hub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - - - name: Build and push Docker image - uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1 - with: - context: . - file: ./apps/web/Dockerfile - push: true - tags: | - ${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }} - ${{ secrets.DOCKER_USERNAME }}/formbricks:latest diff --git a/.github/workflows/release-helm-chart.yml b/.github/workflows/release-helm-chart.yml index 85cfe16894..fbe39e160d 100644 --- a/.github/workflows/release-helm-chart.yml +++ b/.github/workflows/release-helm-chart.yml @@ -1,9 +1,12 @@ name: Publish Helm Chart on: - release: - types: - - published + workflow_call: + inputs: + VERSION: + description: 'The version of the Helm chart to release' + required: true + type: string permissions: contents: read @@ -39,8 +42,8 @@ jobs: - name: Update Chart.yaml with new version run: | - yq -i ".version = \"${VERSION#v}\"" helm-chart/Chart.yaml - yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml + yq -i ".version = \"${{ inputs.VERSION }}\"" helm-chart/Chart.yaml + yq -i ".appVersion = \"v${{ inputs.VERSION }}\"" helm-chart/Chart.yaml - name: Package Helm chart run: | @@ -48,4 +51,4 @@ jobs: - name: Push Helm chart to GitHub Container Registry run: | - helm push formbricks-${VERSION#v}.tgz oci://ghcr.io/formbricks/helm-charts + helm push formbricks-${{ inputs.VERSION }}.tgz oci://ghcr.io/formbricks/helm-charts diff --git a/infra/formbricks-cloud-helm/helmfile.yaml b/infra/formbricks-cloud-helm/helmfile.yaml new file mode 100644 index 0000000000..c986cf6ad2 --- /dev/null +++ b/infra/formbricks-cloud-helm/helmfile.yaml @@ -0,0 +1,17 @@ +repositories: + - name: helm-charts + url: ghcr.io/formbricks/helm-charts + oci: true + +releases: + - name: formbricks + namespace: formbricks + chart: helm-charts/formbricks + version: ^3.0.0 + values: + - values.yaml.gotmpl + set: + - name: deployment.image.tag + value: v{{ requiredEnv "VERSION" }} + - name: deployment.image.repository + value: {{ requiredEnv "REPOSITORY" }} diff --git a/infra/formbricks-cloud-helm/values.yaml.gotmpl b/infra/formbricks-cloud-helm/values.yaml.gotmpl new file mode 100644 index 0000000000..ec5e571ddf --- /dev/null +++ b/infra/formbricks-cloud-helm/values.yaml.gotmpl @@ -0,0 +1,164 @@ +cronJob: + enabled: true + jobs: + ping: + args: + - /bin/sh + - -c + - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" + "$WEBAPP_URL/api/cron/ping"' + env: + CRON_SECRET: + valueFrom: + secretKeyRef: + key: CRON_SECRET + name: formbricks-app-env + WEBAPP_URL: + valueFrom: + secretKeyRef: + key: WEBAPP_URL + name: formbricks-app-env + image: + imagePullPolicy: IfNotPresent + repository: curlimages/curl + tag: latest + schedule: 0 9 * * * + successfulJobsHistoryLimit: 0 + survey-status: + args: + - /bin/sh + - -c + - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" + "$WEBAPP_URL/api/cron/survey-status"' + env: + CRON_SECRET: + valueFrom: + secretKeyRef: + key: CRON_SECRET + name: formbricks-app-env + WEBAPP_URL: + valueFrom: + secretKeyRef: + key: WEBAPP_URL + name: formbricks-app-env + image: + imagePullPolicy: IfNotPresent + repository: curlimages/curl + tag: latest + schedule: 0 0 * * * + successfulJobsHistoryLimit: 0 + weekely-summary: + args: + - /bin/sh + - -c + - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" + "$WEBAPP_URL/api/cron/weekly-summary"' + env: + CRON_SECRET: + valueFrom: + secretKeyRef: + key: CRON_SECRET + name: formbricks-app-env + WEBAPP_URL: + valueFrom: + secretKeyRef: + key: WEBAPP_URL + name: formbricks-app-env + image: + imagePullPolicy: IfNotPresent + repository: curlimages/curl + tag: latest + schedule: 0 8 * * 1 + successfulJobsHistoryLimit: 0 + +## Deployment & Autoscaling +deployment: + env: + DOCKER_CRON_ENABLED: + value: "0" + RATE_LIMITING_DISABLED: + value: "1" + S3_BUCKET_NAME: + value: {{ requiredEnv "FORMBRICKS_S3_BUCKET" }} + envFrom: + app-env: + nameSuffix: app-env + type: secret + nodeSelector: + karpenter.sh/capacity-type: on-demand + reloadOnChange: true +autoscaling: + enabled: true + maxReplicas: 10 + minReplicas: 3 + metrics: + - resource: + name: cpu + target: + averageUtilization: 60 + type: Utilization + type: Resource + - resource: + name: memory + target: + averageUtilization: 60 + type: Utilization + type: Resource + +### Secrets +secret: + enabled: false +externalSecret: + enabled: true + files: + app-env: + dataFrom: + key: prod/formbricks/environment + app-secrets: + dataFrom: + key: prod/formbricks/secrets + refreshInterval: 1m + secretStore: + kind: ClusterSecretStore + name: aws-secrets-manager + +## Ingress +ingress: + annotations: + alb.ingress.kubernetes.io/certificate-arn: {{ requiredEnv "FORMBRICKS_INGRESS_CERT_ARN" }} + alb.ingress.kubernetes.io/group.name: formbricks + alb.ingress.kubernetes.io/healthcheck-path: /health + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06 + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/target-type: ip + enabled: true + hosts: + - host: app.k8s.formbricks.com + paths: + - path: / + pathType: Prefix + serviceName: formbricks + - host: app.formbricks.com + paths: + - path: / + pathType: Prefix + serviceName: formbricks + ingressClassName: alb + +## RBAC +rbac: + enabled: true + serviceAccount: + annotations: + eks.amazonaws.com/role-arn: {{ requiredEnv "FORMBRICKS_ROLE_ARN" }} + additionalLabels: {} + enabled: true + name: formbricks + +## Dependencies +postgresql: + enabled: false +redis: + enabled: false diff --git a/infra/terraform/rds.tf b/infra/terraform/rds.tf index 0049018e7f..2455b737b0 100644 --- a/infra/terraform/rds.tf +++ b/infra/terraform/rds.tf @@ -23,6 +23,15 @@ module "rds-aurora" { master_username = "formbricks" master_password = random_password.postgres.result manage_master_user_password = false + create_db_cluster_parameter_group = true + db_cluster_parameter_group_family = data.aws_rds_engine_version.postgresql.parameter_group_family + db_cluster_parameter_group_parameters = [ + { + name = "shared_preload_libraries" + value = "pglogical" + apply_method = "pending-reboot" + } + ] vpc_id = module.vpc.vpc_id db_subnet_group_name = module.vpc.database_subnet_group_name From 777210ec42a23ba0d9102a102a763b933e06693e Mon Sep 17 00:00:00 2001 From: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Date: Mon, 24 Mar 2025 02:39:17 -0300 Subject: [PATCH 07/54] fix: refactored the code to create a new link on click for single-use link surveys (#5038) Co-authored-by: Piyush Gupta --- .../(organization)/general/page.test.tsx | 6 + .../summary/components/SurveyAnalysisCTA.tsx | 10 +- .../tests/SurveyAnalysisCTA.test.tsx | 129 ++++++++++++++++++ .../survey/hooks/useSingleUseId.test.tsx | 105 ++++++++++++++ .../modules/survey/hooks/useSingleUseId.tsx | 38 ++++++ apps/web/modules/survey/lib/client-utils.ts | 5 + .../survey/lib/tests/client-utils.test.ts | 17 +++ apps/web/modules/survey/link/page.tsx | 1 + .../survey/list/components/survey-card.tsx | 59 +++----- .../list/components/survey-dropdown-menu.tsx | 39 ++++-- .../components/tests/survey-card.test.tsx | 112 +++++++++++++++ .../tests/survey-dropdown-menu.test.tsx | 122 +++++++++++++++++ apps/web/vite.config.mts | 6 +- packages/lib/aiModels.ts | 23 ++-- packages/lib/constants.ts | 5 + packages/lib/vitestSetup.ts | 44 ++++-- 16 files changed, 647 insertions(+), 74 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/tests/SurveyAnalysisCTA.test.tsx create mode 100644 apps/web/modules/survey/hooks/useSingleUseId.test.tsx create mode 100644 apps/web/modules/survey/hooks/useSingleUseId.tsx create mode 100644 apps/web/modules/survey/lib/client-utils.ts create mode 100644 apps/web/modules/survey/lib/tests/client-utils.test.ts create mode 100644 apps/web/modules/survey/list/components/tests/survey-card.test.tsx create mode 100644 apps/web/modules/survey/list/components/tests/survey-dropdown-menu.test.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx index b7f6303685..097a427527 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx @@ -37,6 +37,12 @@ vi.mock("@formbricks/lib/constants", () => ({ WEBAPP_URL: "mock-webapp-url", SMTP_HOST: "mock-smtp-host", SMTP_PORT: "mock-smtp-port", + AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name", + AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key", + AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id", + AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name", + AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key", + AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id", })); vi.mock("next-auth", () => ({ diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx index 1ba4b154c0..2b005e3a44 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx @@ -3,6 +3,8 @@ import { ShareEmbedSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey"; import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage"; import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown"; +import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId"; +import { copySurveyLink } from "@/modules/survey/lib/client-utils"; import { Badge } from "@/modules/ui/components/badge"; import { IconBar } from "@/modules/ui/components/iconbar"; import { useTranslate } from "@tolgee/react"; @@ -49,6 +51,7 @@ export const SurveyAnalysisCTA = ({ }); const surveyUrl = useMemo(() => `${webAppUrl}/s/${survey.id}`, [survey.id, webAppUrl]); + const { refreshSingleUseId } = useSingleUseId(survey); const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted; @@ -71,8 +74,11 @@ export const SurveyAnalysisCTA = ({ }; const handleCopyLink = () => { - navigator.clipboard - .writeText(surveyUrl) + refreshSingleUseId() + .then((newId) => { + const linkToCopy = copySurveyLink(surveyUrl, newId); + return navigator.clipboard.writeText(linkToCopy); + }) .then(() => { toast.success(t("common.copied_to_clipboard")); }) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/tests/SurveyAnalysisCTA.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/tests/SurveyAnalysisCTA.test.tsx new file mode 100644 index 0000000000..5c51495c33 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/tests/SurveyAnalysisCTA.test.tsx @@ -0,0 +1,129 @@ +import { render, cleanup, fireEvent, waitFor, screen } from "@testing-library/react"; +import { describe, it, vi, afterEach, expect } from "vitest"; +import { SurveyAnalysisCTA } from "../SurveyAnalysisCTA"; +import toast from "react-hot-toast"; +import { TSurvey } from "@formbricks/types/surveys/types"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TUser } from "@formbricks/types/user"; +import "@testing-library/jest-dom/vitest"; + +// Mock constants +vi.mock("@formbricks/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + ENCRYPTION_KEY: "test", + ENTERPRISE_LICENSE_KEY: "test", + GITHUB_ID: "test", + GITHUB_SECRET: "test", + GOOGLE_CLIENT_ID: "test", + GOOGLE_CLIENT_SECRET: "test", + AZUREAD_CLIENT_ID: "mock-azuread-client-id", + AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", + AZUREAD_TENANT_ID: "mock-azuread-tenant-id", + OIDC_CLIENT_ID: "mock-oidc-client-id", + OIDC_CLIENT_SECRET: "mock-oidc-client-secret", + OIDC_ISSUER: "mock-oidc-issuer", + OIDC_DISPLAY_NAME: "mock-oidc-display-name", + OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", + WEBAPP_URL: "mock-webapp-url", + AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name", + AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key", + AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id", + AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name", + AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key", + AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id", + IS_PRODUCTION: true, + FB_LOGO_URL: "https://example.com/mock-logo.png", + SMTP_HOST: "mock-smtp-host", + SMTP_PORT: "mock-smtp-port", +})); + +// Create a spy for refreshSingleUseId so we can override it in tests +const refreshSingleUseIdSpy = vi.fn(() => Promise.resolve("newSingleUseId")); + +// Mock useSingleUseId hook +vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({ + useSingleUseId: () => ({ + refreshSingleUseId: refreshSingleUseIdSpy, + }), +})); + +const mockSearchParams = new URLSearchParams(); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn() }), + useSearchParams: () => mockSearchParams, // Reuse the same object + usePathname: () => "/current", +})); + +// Mock copySurveyLink to return a predictable string +vi.mock("@/modules/survey/lib/client-utils", () => ({ + copySurveyLink: vi.fn((url: string, id: string) => `${url}?id=${id}`), +})); + +vi.spyOn(toast, "success"); +vi.spyOn(toast, "error"); + +// Set up a fake clipboard +const writeTextMock = vi.fn(() => Promise.resolve()); +Object.assign(navigator, { + clipboard: { writeText: writeTextMock }, +}); + +const dummySurvey = { + id: "survey123", + type: "link", + environmentId: "env123", + status: "active", +} as unknown as TSurvey; +const dummyEnvironment = { id: "env123", appSetupCompleted: true } as TEnvironment; +const dummyUser = { id: "user123", name: "Test User" } as TUser; +const webAppUrl = "http://example.com"; + +describe("SurveyAnalysisCTA - handleCopyLink", () => { + afterEach(() => { + cleanup(); + }); + + it("calls copySurveyLink and clipboard.writeText on success", async () => { + render( + + ); + + const copyButton = screen.getByRole("button", { name: "common.copy_link" }); + fireEvent.click(copyButton); + + await waitFor(() => { + expect(refreshSingleUseIdSpy).toHaveBeenCalled(); + expect(writeTextMock).toHaveBeenCalledWith("http://example.com/s/survey123?id=newSingleUseId"); + expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard"); + }); + }); + + it("shows error toast on failure", async () => { + refreshSingleUseIdSpy.mockImplementationOnce(() => Promise.reject(new Error("fail"))); + render( + + ); + + const copyButton = screen.getByRole("button", { name: "common.copy_link" }); + fireEvent.click(copyButton); + + await waitFor(() => { + expect(refreshSingleUseIdSpy).toHaveBeenCalled(); + expect(writeTextMock).not.toHaveBeenCalled(); + expect(toast.error).toHaveBeenCalledWith("environments.surveys.summary.failed_to_copy_link"); + }); + }); +}); diff --git a/apps/web/modules/survey/hooks/useSingleUseId.test.tsx b/apps/web/modules/survey/hooks/useSingleUseId.test.tsx new file mode 100644 index 0000000000..e88a1655b5 --- /dev/null +++ b/apps/web/modules/survey/hooks/useSingleUseId.test.tsx @@ -0,0 +1,105 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import toast from "react-hot-toast"; + +import { useSingleUseId } from "./useSingleUseId"; +import { generateSingleUseIdAction } from "@/modules/survey/list/actions"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { TSurvey } from "@formbricks/types/surveys/types"; + +// Mock external functions +vi.mock("@/modules/survey/list/actions", () => ({ + generateSingleUseIdAction: vi.fn(), +})); + +vi.mock("@/lib/utils/helper", () => ({ + getFormattedErrorMessage: vi.fn(() => "Formatted error"), +})); + +describe("useSingleUseId", () => { + const mockSurvey = { + id: "survey123", + singleUse: { + enabled: true, + isEncrypted: true, + }, + } as TSurvey; + + it("should initialize singleUseId to undefined", () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ data: "mockSingleUseId" }); + + const { result } = renderHook(() => useSingleUseId(mockSurvey)); + + // Right after mount, before the async effect resolves, singleUseId should be undefined + expect(result.current.singleUseId).toBeUndefined(); + }); + + it("should fetch and set singleUseId if singleUse is enabled", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ data: "mockSingleUseId" }); + + const { result, rerender } = renderHook((props) => useSingleUseId(props), { + initialProps: mockSurvey, + }); + + // Wait for the effect to run + await new Promise((r) => setTimeout(r, 0)); + + expect(generateSingleUseIdAction).toHaveBeenCalledWith({ + surveyId: "survey123", + isEncrypted: true, + }); + expect(result.current.singleUseId).toBe("mockSingleUseId"); + + // Re-render with the same props to ensure it doesn't break + rerender(mockSurvey); + + // The singleUseId remains the same unless we explicitly refresh + expect(result.current.singleUseId).toBe("mockSingleUseId"); + }); + + it("should return undefined and not call the API if singleUse is disabled", async () => { + const disabledSurvey = { + ...mockSurvey, + singleUse: { + enabled: false, + }, + } as TSurvey; + + const { result } = renderHook(() => useSingleUseId(disabledSurvey)); + + await new Promise((r) => setTimeout(r, 0)); + + expect(generateSingleUseIdAction).not.toHaveBeenCalled(); + expect(result.current.singleUseId).toBeUndefined(); + }); + + it("should show toast error if the API call fails", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ serverError: "Something went wrong" }); + + const { result } = renderHook(() => useSingleUseId(mockSurvey)); + + await new Promise((r) => setTimeout(r, 0)); + + expect(getFormattedErrorMessage).toHaveBeenCalledWith({ serverError: "Something went wrong" }); + expect(toast.error).toHaveBeenCalledWith("Formatted error"); + expect(result.current.singleUseId).toBeUndefined(); + }); + + it("should refreshSingleUseId on demand", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ data: "initialId" }); + const { result } = renderHook(() => useSingleUseId(mockSurvey)); + + // Wait for initial + await new Promise((r) => setTimeout(r, 0)); + expect(result.current.singleUseId).toBe("initialId"); + + vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ data: "refreshedId" }); + + await act(async () => { + const val = await result.current.refreshSingleUseId(); + expect(val).toBe("refreshedId"); + }); + + expect(result.current.singleUseId).toBe("refreshedId"); + }); +}); \ No newline at end of file diff --git a/apps/web/modules/survey/hooks/useSingleUseId.tsx b/apps/web/modules/survey/hooks/useSingleUseId.tsx new file mode 100644 index 0000000000..b506e20d5f --- /dev/null +++ b/apps/web/modules/survey/hooks/useSingleUseId.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { generateSingleUseIdAction } from "@/modules/survey/list/actions"; +import { TSurvey as TSurveyList } from "@/modules/survey/list/types/surveys"; +import { useCallback, useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { TSurvey } from "@formbricks/types/surveys/types"; + +export const useSingleUseId = (survey: TSurvey | TSurveyList) => { + const [singleUseId, setSingleUseId] = useState(undefined); + + const refreshSingleUseId = useCallback(async () => { + if (survey.singleUse?.enabled) { + const response = await generateSingleUseIdAction({ + surveyId: survey.id, + isEncrypted: !!survey.singleUse?.isEncrypted, + }); + if (response?.data) { + setSingleUseId(response.data); + return response.data; + } else { + const errorMessage = getFormattedErrorMessage(response); + toast.error(errorMessage); + return undefined; + } + } else { + setSingleUseId(undefined); + return undefined; + } + }, [survey]); + + useEffect(() => { + refreshSingleUseId(); + }, [survey, refreshSingleUseId]); + + return { singleUseId, refreshSingleUseId }; +}; diff --git a/apps/web/modules/survey/lib/client-utils.ts b/apps/web/modules/survey/lib/client-utils.ts new file mode 100644 index 0000000000..ceb7721277 --- /dev/null +++ b/apps/web/modules/survey/lib/client-utils.ts @@ -0,0 +1,5 @@ +"use client"; + +export const copySurveyLink = (surveyUrl: string, singleUseId?: string): string => { + return singleUseId ? `${surveyUrl}?suId=${singleUseId}` : surveyUrl; +}; diff --git a/apps/web/modules/survey/lib/tests/client-utils.test.ts b/apps/web/modules/survey/lib/tests/client-utils.test.ts new file mode 100644 index 0000000000..d24906214a --- /dev/null +++ b/apps/web/modules/survey/lib/tests/client-utils.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; +import { copySurveyLink } from "../client-utils"; + +describe("copySurveyLink", () => { + it("appends singleUseId when provided", () => { + const surveyUrl = "http://example.com/survey"; + const singleUseId = "12345"; + const result = copySurveyLink(surveyUrl, singleUseId); + expect(result).toBe("http://example.com/survey?suId=12345"); + }); + + it("returns original surveyUrl when singleUseId is not provided", () => { + const surveyUrl = "http://example.com/survey"; + const result = copySurveyLink(surveyUrl); + expect(result).toBe(surveyUrl); + }); +}); diff --git a/apps/web/modules/survey/link/page.tsx b/apps/web/modules/survey/link/page.tsx index 622ad52102..5c4215b77d 100644 --- a/apps/web/modules/survey/link/page.tsx +++ b/apps/web/modules/survey/link/page.tsx @@ -47,6 +47,7 @@ export const LinkSurveyPage = async (props: LinkSurveyPageProps) => { const isSingleUseSurveyEncrypted = survey?.singleUse?.isEncrypted; let singleUseId: string | undefined = undefined; + if (isSingleUseSurvey) { // check if the single use id is present for single use surveys if (!suId) { diff --git a/apps/web/modules/survey/list/components/survey-card.tsx b/apps/web/modules/survey/list/components/survey-card.tsx index 55daff5e61..a6bddfb24b 100644 --- a/apps/web/modules/survey/list/components/survey-card.tsx +++ b/apps/web/modules/survey/list/components/survey-card.tsx @@ -1,14 +1,12 @@ "use client"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { generateSingleUseIdAction } from "@/modules/survey/list/actions"; +import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId"; import { SurveyTypeIndicator } from "@/modules/survey/list/components/survey-type-indicator"; import { TSurvey } from "@/modules/survey/list/types/surveys"; import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator"; import { useTranslate } from "@tolgee/react"; import Link from "next/link"; -import { useEffect, useMemo, useState } from "react"; -import toast from "react-hot-toast"; +import { useMemo } from "react"; import { cn } from "@formbricks/lib/cn"; import { convertDateString, timeSince } from "@formbricks/lib/time"; import { TUserLocale } from "@formbricks/types/user"; @@ -33,43 +31,26 @@ export const SurveyCard = ({ locale, }: SurveyCardProps) => { const { t } = useTranslate(); - const surveyStatusLabel = - survey.status === "inProgress" - ? t("common.in_progress") - : survey.status === "scheduled" - ? t("common.scheduled") - : survey.status === "completed" - ? t("common.completed") - : survey.status === "draft" - ? t("common.draft") - : survey.status === "paused" - ? t("common.paused") - : undefined; + const surveyStatusLabel = (() => { + switch (survey.status) { + case "inProgress": + return t("common.in_progress"); + case "scheduled": + return t("common.scheduled"); + case "completed": + return t("common.completed"); + case "draft": + return t("common.draft"); + case "paused": + return t("common.paused"); + default: + return undefined; + } + })(); const isSurveyCreationDeletionDisabled = isReadOnly; - const [singleUseId, setSingleUseId] = useState(); - - useEffect(() => { - const fetchSingleUseId = async () => { - if (survey.singleUse?.enabled) { - const generateSingleUseIdResponse = await generateSingleUseIdAction({ - surveyId: survey.id, - isEncrypted: !!survey.singleUse?.isEncrypted, - }); - if (generateSingleUseIdResponse?.data) { - setSingleUseId(generateSingleUseIdResponse.data); - } else { - const errorMessage = getFormattedErrorMessage(generateSingleUseIdResponse); - toast.error(errorMessage); - } - } else { - setSingleUseId(undefined); - } - }; - - fetchSingleUseId(); - }, [survey]); + const { refreshSingleUseId } = useSingleUseId(survey); const linkHref = useMemo(() => { return survey.status === "draft" @@ -123,7 +104,7 @@ export const SurveyCard = ({ environmentId={environmentId} webAppUrl={WEBAPP_URL} disabled={isDraftAndReadOnly} - singleUseId={singleUseId} + refreshSingleUseId={refreshSingleUseId} isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled} duplicateSurvey={duplicateSurvey} deleteSurvey={deleteSurvey} diff --git a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx index 30b59b497e..09805c761e 100644 --- a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx +++ b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx @@ -1,6 +1,7 @@ "use client"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { copySurveyLink } from "@/modules/survey/lib/client-utils"; import { copySurveyToOtherEnvironmentAction, deleteSurveyAction, @@ -36,7 +37,7 @@ interface SurveyDropDownMenuProps { environmentId: string; survey: TSurvey; webAppUrl: string; - singleUseId?: string; + refreshSingleUseId: () => Promise; disabled?: boolean; isSurveyCreationDeletionDisabled?: boolean; duplicateSurvey: (survey: TSurvey) => void; @@ -47,7 +48,7 @@ export const SurveyDropDownMenu = ({ environmentId, survey, webAppUrl, - singleUseId, + refreshSingleUseId, disabled, isSurveyCreationDeletionDisabled, deleteSurvey, @@ -76,6 +77,21 @@ export const SurveyDropDownMenu = ({ setLoading(false); }; + const handleCopyLink = async (e: React.MouseEvent) => { + try { + e.preventDefault(); + setIsDropDownOpen(false); + const newId = await refreshSingleUseId(); + const copiedLink = copySurveyLink(surveyUrl, newId); + navigator.clipboard.writeText(copiedLink); + toast.success(t("common.copied_to_clipboard")); + router.refresh(); + } + catch (error) { + toast.error(t("environments.surveys.summary.failed_to_copy_link")); + } + }; + const duplicateSurveyAndRefresh = async (surveyId: string) => { setLoading(true); try { @@ -105,6 +121,7 @@ export const SurveyDropDownMenu = ({ return (
e.stopPropagation()}> @@ -168,11 +185,12 @@ export const SurveyDropDownMenu = ({
{ + onClick={async (e) => { e.preventDefault(); setIsDropDownOpen(false); - const previewUrl = singleUseId - ? `/s/${survey.id}?suId=${singleUseId}&preview=true` + const newId = await refreshSingleUseId(); + const previewUrl = newId + ? `/s/${survey.id}?suId=${newId}&preview=true` : `/s/${survey.id}?preview=true`; window.open(previewUrl, "_blank"); }}> @@ -183,16 +201,9 @@ export const SurveyDropDownMenu = ({ diff --git a/apps/web/modules/survey/list/components/tests/survey-card.test.tsx b/apps/web/modules/survey/list/components/tests/survey-card.test.tsx new file mode 100644 index 0000000000..fc9b482aa8 --- /dev/null +++ b/apps/web/modules/survey/list/components/tests/survey-card.test.tsx @@ -0,0 +1,112 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import { SurveyCard } from "../survey-card"; +import { TSurvey } from "@/modules/survey/list/types/surveys"; + +// Mock constants +vi.mock("@formbricks/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + ENCRYPTION_KEY: "test", + ENTERPRISE_LICENSE_KEY: "test", + GITHUB_ID: "test", + GITHUB_SECRET: "test", + GOOGLE_CLIENT_ID: "test", + GOOGLE_CLIENT_SECRET: "test", + AZUREAD_CLIENT_ID: "mock-azuread-client-id", + AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", + AZUREAD_TENANT_ID: "mock-azuread-tenant-id", + OIDC_CLIENT_ID: "mock-oidc-client-id", + OIDC_CLIENT_SECRET: "mock-oidc-client-secret", + OIDC_ISSUER: "mock-oidc-issuer", + OIDC_DISPLAY_NAME: "mock-oidc-display-name", + OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", + WEBAPP_URL: "mock-webapp-url", + AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name", + AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key", + AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id", + AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name", + AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key", + AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id", + IS_PRODUCTION: true, + FB_LOGO_URL: "https://example.com/mock-logo.png", + SMTP_HOST: "mock-smtp-host", + SMTP_PORT: "mock-smtp-port", +})); + +describe("SurveyCard", () => { + const dummySurvey = { + id: "survey123", + name: "Test Survey", + status: "draft", + responseCount: 0, + type: "link", + createdAt: new Date().toString(), + updatedAt: new Date().toString(), + }; + const environmentId = "env123"; + const WEBAPP_URL = "http://example.com"; + const mockDeleteSurvey = vi.fn(); + const mockDuplicateSurvey = vi.fn(); + + afterEach(() => { + cleanup(); + }); + + it("renders survey card with a draft link when not readOnly", () => { + render( + // ...existing code for test wrapper if needed... + + ); + // Draft survey => link should point to edit + const link = screen.getByRole("link"); + expect(link).toHaveAttribute( + "href", + `/environments/${environmentId}/surveys/${dummySurvey.id}/edit` + ); + }); + + it("displays no clickable link when readOnly and survey is draft", () => { + render( + + ); + // When it's read only and draft, we expect no link + const link = screen.queryByRole("link"); + expect(link).toBeNull(); + }); + + it("renders summary link when survey status is not draft", () => { + render( + + ); + // For non-draft => link to summary + const link = screen.getByRole("link"); + expect(link).toHaveAttribute( + "href", + `/environments/${environmentId}/surveys/${dummySurvey.id}/summary` + ); + }); +}); diff --git a/apps/web/modules/survey/list/components/tests/survey-dropdown-menu.test.tsx b/apps/web/modules/survey/list/components/tests/survey-dropdown-menu.test.tsx new file mode 100644 index 0000000000..031fb2195f --- /dev/null +++ b/apps/web/modules/survey/list/components/tests/survey-dropdown-menu.test.tsx @@ -0,0 +1,122 @@ +import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import { SurveyDropDownMenu } from "../survey-dropdown-menu"; +import { TSurvey } from "@/modules/survey/list/types/surveys"; +import userEvent from "@testing-library/user-event"; + +// Mock constants +vi.mock("@formbricks/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + ENCRYPTION_KEY: "test", + ENTERPRISE_LICENSE_KEY: "test", + GITHUB_ID: "test", + GITHUB_SECRET: "test", + GOOGLE_CLIENT_ID: "test", + GOOGLE_CLIENT_SECRET: "test", + AZUREAD_CLIENT_ID: "mock-azuread-client-id", + AZUREAD_CLIENT_SECRET: "mock-azure-client-secret", + AZUREAD_TENANT_ID: "mock-azuread-tenant-id", + OIDC_CLIENT_ID: "mock-oidc-client-id", + OIDC_CLIENT_SECRET: "mock-oidc-client-secret", + OIDC_ISSUER: "mock-oidc-issuer", + OIDC_DISPLAY_NAME: "mock-oidc-display-name", + OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm", + WEBAPP_URL: "mock-webapp-url", + AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name", + AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key", + AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id", + AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name", + AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key", + AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id", + IS_PRODUCTION: true, + FB_LOGO_URL: "https://example.com/mock-logo.png", + SMTP_HOST: "mock-smtp-host", + SMTP_PORT: "mock-smtp-port", +})); + +// Mock external dependencies +vi.mock("@/modules/survey/lib/client-utils", () => ({ + copySurveyLink: vi.fn((url: string, suId?: string) => (suId ? `${url}?suId=${suId}` : url)), +})); + +const fakeSurvey = { + id: "testSurvey", + name: "Test Survey", + status: "inProgress", + type: "link", + creator: { name: "Test User" }, +} as unknown as TSurvey ; + +describe("SurveyDropDownMenu", () => { + afterEach(() => { + cleanup(); + }); + + it("calls copySurveyLink when copy link is clicked", async () => { + const mockRefresh = vi.fn().mockResolvedValue("fakeSingleUseId"); + const mockDeleteSurvey = vi.fn(); + const mockDuplicateSurvey = vi.fn(); + + render( + + ); + + // Find the menu wrapper + const menuWrapper = screen.getByTestId("survey-dropdown-menu"); + + // Inside that wrapper, find the actual trigger (div, button, etc.) + // By default, the trigger is the first clickable child + const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement; + expect(triggerElement).toBeInTheDocument(); + + // Use userEvent to mimic real user interaction + await userEvent.click(triggerElement); + + // Click copy link + const copyLinkButton = screen.getByTestId("copy-link"); + fireEvent.click(copyLinkButton); + + await waitFor(() => { + expect(mockRefresh).toHaveBeenCalled(); + }); + }); + + it("shows edit and delete items when not disabled", async () => { + render( + + ); + + // Find the menu wrapper + const menuWrapper = screen.getByTestId("survey-dropdown-menu"); + + // Inside that wrapper, find the actual trigger (div, button, etc.) + // By default, the trigger is the first clickable child + const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement; + expect(triggerElement).toBeInTheDocument(); + + // Use userEvent to mimic real user interaction + await userEvent.click(triggerElement); + + const editItem = screen.getByText("common.edit"); + const deleteItem = screen.getByText("common.delete"); + + expect(editItem).toBeInTheDocument(); + expect(deleteItem).toBeInTheDocument(); + }); +}); diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index 93c95f3d0e..8bed253586 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -8,7 +8,6 @@ export default defineConfig({ test: { environment: "node", environmentMatchGlobs: [ - ["**/page.test.tsx", "node"], // page files use node environment because it uses server-side rendering ["**/*.test.tsx", "jsdom"], ], exclude: ["playwright/**", "node_modules/**"], @@ -34,9 +33,14 @@ export default defineConfig({ "modules/survey/link/lib/**/*.ts", "app/(auth)/layout.tsx", "app/(app)/layout.tsx", + "app/(app)/environments/**/surveys/**/(analysis)/summary/components/SurveyAnalysisCTA.tsx", "app/intercom/*.tsx", "modules/ee/role-management/*.ts", "modules/organization/settings/teams/actions.ts", + "modules/survey/hooks/*.tsx", + "modules/survey/lib/client-utils.ts", + "modules/survey/list/components/survey-card.tsx", + "modules/survey/list/components/survey-dropdown-menu.tsx" ], exclude: [ "**/.next/**", diff --git a/packages/lib/aiModels.ts b/packages/lib/aiModels.ts index 2c814be408..c2bfcf0156 100644 --- a/packages/lib/aiModels.ts +++ b/packages/lib/aiModels.ts @@ -1,14 +1,21 @@ import { createAzure } from "@ai-sdk/azure"; -import { env } from "./env"; +import { + AI_AZURE_EMBEDDINGS_API_KEY, + AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID, + AI_AZURE_EMBEDDINGS_RESSOURCE_NAME, + AI_AZURE_LLM_API_KEY, + AI_AZURE_LLM_DEPLOYMENT_ID, + AI_AZURE_LLM_RESSOURCE_NAME, +} from "./constants"; export const llmModel = createAzure({ - resourceName: env.AI_AZURE_LLM_RESSOURCE_NAME, // Azure resource name - apiKey: env.AI_AZURE_LLM_API_KEY, // Azure API key -})(env.AI_AZURE_LLM_DEPLOYMENT_ID || "llm"); + resourceName: AI_AZURE_LLM_RESSOURCE_NAME, // Azure resource name + apiKey: AI_AZURE_LLM_API_KEY, // Azure API key +})(AI_AZURE_LLM_DEPLOYMENT_ID || "llm"); export const embeddingsModel = createAzure({ - resourceName: env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME, // Azure resource name - apiKey: env.AI_AZURE_EMBEDDINGS_API_KEY, // Azure API key -}).embedding(env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID || "embeddings", { + resourceName: AI_AZURE_EMBEDDINGS_RESSOURCE_NAME, // Azure resource name + apiKey: AI_AZURE_EMBEDDINGS_API_KEY, // Azure API key +}).embedding(AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID || "embeddings", { dimensions: 512, -}); +}); \ No newline at end of file diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 15d3e3e4bd..a58bda2e60 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -268,6 +268,11 @@ export const IS_AI_CONFIGURED = Boolean( env.AI_AZURE_LLM_DEPLOYMENT_ID && env.AI_AZURE_LLM_RESSOURCE_NAME ); +export const AI_AZURE_LLM_API_KEY = env.AI_AZURE_LLM_API_KEY; +export const AI_AZURE_LLM_DEPLOYMENT_ID = env.AI_AZURE_LLM_DEPLOYMENT_ID; +export const AI_AZURE_EMBEDDINGS_RESSOURCE_NAME = env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME; +export const AI_AZURE_EMBEDDINGS_API_KEY = env.AI_AZURE_EMBEDDINGS_API_KEY; +export const AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID = env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID; export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY; export const INTERCOM_APP_ID = env.INTERCOM_APP_ID; diff --git a/packages/lib/vitestSetup.ts b/packages/lib/vitestSetup.ts index 17a2d6b519..d4fefc99a3 100644 --- a/packages/lib/vitestSetup.ts +++ b/packages/lib/vitestSetup.ts @@ -3,6 +3,21 @@ import "@testing-library/jest-dom/vitest"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { ValidationError } from "@formbricks/types/errors"; +// mock react toast + +vi.mock("react-hot-toast", () => ({ + default: { + success: vi.fn(), + error: vi.fn(), + }, + toast: { + success: vi.fn(), + error: vi.fn(), + }, + success: vi.fn(), + error: vi.fn(), +})); + // mock next cache vi.mock("next/cache", () => ({ @@ -37,16 +52,25 @@ vi.mock("@tolgee/react", () => ({ // mock next/router navigation -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: vi.fn(), - replace: vi.fn(), - back: vi.fn(), - forward: vi.fn(), - prefetch: vi.fn(), - refresh: vi.fn(), - }), -})); +vi.mock("next/navigation", async () => { + const actual = await vi.importActual("next/navigation"); + + return { + ...actual, + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + prefetch: vi.fn(), + refresh: vi.fn(), + }), + notFound: vi.fn(), + redirect: vi.fn(), + useSearchParams: vi.fn(), + usePathname: vi.fn(), + }; +}); // mock server-only vi.mock("server-only", () => { From 60d056348739281dbb9e49070e82ae60ba1ca6d5 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:13:57 +0530 Subject: [PATCH 08/54] fix: adds missing storage api docs and fixes api key auth docs (#5031) --- .../[environmentId]/storage/local/route.ts | 16 +- .../client/[environmentId]/storage/route.ts | 25 +- docs/api-v2-reference/openapi.yml | 524 +++++++++++++++++- packages/types/storage.ts | 11 +- 4 files changed, 554 insertions(+), 22 deletions(-) diff --git a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts index 330f615c88..d209325c67 100644 --- a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts @@ -36,13 +36,13 @@ export const POST = async (req: NextRequest, context: Context): Promise Surveys description: Operations for managing surveys. -security: - - apiKeyAuth: [] + - name: Management API > Storage + description: Operations for managing storage. + - name: Client API > File Upload + description: Operations for uploading files. + paths: - /responses/{responseId}: + /{environmentId}/responses/{responseId}: put: description: Update an existing response for example when you want to mark a response as finished or you want to change an existing response's value. parameters: + - in: path + name: environmentId + required: true + schema: + type: string - in: path name: responseId required: true @@ -280,8 +288,246 @@ paths: servers: - url: https://app.formbricks.com/api/v2/client description: Formbricks Client + /{environmentId}/storage: + post: + summary: Upload Private File + description: > + API endpoint for uploading private files. Uploaded files are kept + private so that only users with access to the specified environment can + retrieve them. The endpoint validates the survey ID, file name, and file + type from the request body, and returns a signed URL for S3 uploads + along with a local upload URL. + tags: + - Client API > File Upload + parameters: + - in: path + name: environmentId + required: true + schema: + type: string + description: The ID of the environment. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + surveyId: + type: string + description: The ID of the survey associated with the file. + fileName: + type: string + description: The name of the file to be uploaded. + fileType: + type: string + description: The MIME type of the file. + required: + - surveyId + - fileName + - fileType + example: + surveyId: cm7pr0x2y004o192zmit8cjvb + fileName: example.jpg + fileType: image/jpeg + responses: + "200": + description: OK - Returns the signed URL, signing data, updated file name, and file URL. + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + signedUrl: + type: string + description: Signed URL for uploading the file to local storage. + signingData: + type: object + properties: + signature: + type: string + description: Signature for verifying the upload. + timestamp: + type: number + description: Timestamp used in the signature. + uuid: + type: string + description: Unique identifier for the signed upload. + updatedFileName: + type: string + description: The updated file name after processing. + fileUrl: + type: string + description: URL where the uploaded file can be accessed. + example: + data: + signedUrl: "http://localhost:3000/api/v1/client/cm1ubebtj000614kqe4hs3c67/storage/local" + signingData: + signature: "3e51c6f441e646a0c9a47fdcdd25eee9bfac26d5506461d811b9c55cbdd90914" + timestamp: 1741693207760 + uuid: "f48bcb1aad904f574069a253388024af" + updatedFileName: "halle--fid--b153ba3e-6602-4bb3-bed9-211b5b1ae463.jpg" + fileUrl: "http://localhost:3000/storage/cm1ubebtj000614kqe4hs3c67/private/halle--fid--b153ba3e-6602-4bb3-bed9-211b5b1ae463.jpg" + "400": + description: Bad Request - One or more required fields are missing. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: fileName is required + "404": + description: Not Found - The specified survey or organization does not exist. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: Survey survey123 not found + servers: + - url: https://app.formbricks.com/api/v2/client + description: Formbricks API Server + /{environmentId}/storage/local: + post: + summary: Upload Private File to Local Storage + description: > + API endpoint for uploading private files to local storage. The request must include a valid signature, + UUID, and timestamp to verify the upload. The file is provided as a Base64 encoded string in the request body. + The "Content-Type" header must be set to a valid MIME type, and the file data must be a valid file object (buffer). + tags: + - Client API > File Upload + parameters: + - in: path + name: environmentId + required: true + schema: + type: string + description: The ID of the environment. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + surveyId: + type: string + description: The ID of the survey associated with the file. + fileName: + type: string + description: The URI encoded file name. + fileType: + type: string + description: The MIME type of the file. + signature: + type: string + description: Signed signature for verifying the file upload. + uuid: + type: string + description: Unique identifier used in the signature validation. + timestamp: + type: string + description: Timestamp used in the signature validation. + fileBase64String: + type: string + description: > + Base64 encoded string of the file. It should include data type information, + e.g. "data:;base64,". + required: + - surveyId + - fileName + - fileType + - signature + - uuid + - timestamp + - fileBase64String + example: + surveyId: "survey123" + fileName: "example.jpg" + fileType: "image/jpeg" + signature: "signedSignatureValue" + uuid: "uniqueUuidValue" + timestamp: "1627891234567" + fileBase64String: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/..." + responses: + "200": + description: OK - File uploaded successfully. + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Success message. + example: + message: "File uploaded successfully" + "400": + description: Bad Request - One or more required fields are missing or the file is too large. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "fileName is required" + "401": + description: Unauthorized - Signature validation failed or required signature fields are missing. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "Unauthorized" + "404": + description: Not Found - The specified survey or organization does not exist. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "Survey survey123 not found" + "500": + description: Internal Server Error - File upload failed. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "File upload failed" + servers: + - url: https://app.formbricks.com/api/v2 + description: Formbricks API Server /responses: get: + security: + - apiKeyAuth: [] operationId: getResponses summary: Get responses description: Gets responses from the database. @@ -347,6 +593,8 @@ paths: items: $ref: "#/components/schemas/response" post: + security: + - apiKeyAuth: [] operationId: createResponse summary: Create a response description: Creates a response in the database. @@ -368,6 +616,8 @@ paths: $ref: "#/components/schemas/response" /responses/{id}: get: + security: + - apiKeyAuth: [] operationId: getResponse summary: Get a response description: Gets a response from the database. @@ -388,6 +638,8 @@ paths: schema: $ref: "#/components/schemas/response" put: + security: + - apiKeyAuth: [] operationId: updateResponse summary: Update a response description: Updates a response in the database. @@ -491,6 +743,8 @@ paths: schema: $ref: "#/components/schemas/response" delete: + security: + - apiKeyAuth: [] operationId: deleteResponse summary: Delete a response description: Deletes a response from the database. @@ -512,6 +766,8 @@ paths: $ref: "#/components/schemas/response" /contacts: get: + security: + - apiKeyAuth: [] operationId: getContacts summary: Get contacts description: Gets contacts from the database. @@ -565,6 +821,8 @@ paths: items: $ref: "#/components/schemas/contact" post: + security: + - apiKeyAuth: [] operationId: createContact summary: Create a contact description: Creates a contact in the database. @@ -586,6 +844,8 @@ paths: $ref: "#/components/schemas/contact" /contacts/{id}: get: + security: + - apiKeyAuth: [] operationId: getContact summary: Get a contact description: Gets a contact from the database. @@ -605,6 +865,8 @@ paths: schema: $ref: "#/components/schemas/contact" put: + security: + - apiKeyAuth: [] operationId: updateContact summary: Update a contact description: Updates a contact in the database. @@ -631,6 +893,8 @@ paths: schema: $ref: "#/components/schemas/contact" delete: + security: + - apiKeyAuth: [] operationId: deleteContact summary: Delete a contact description: Deletes a contact from the database. @@ -651,6 +915,8 @@ paths: $ref: "#/components/schemas/contact" /contact-attributes: get: + security: + - apiKeyAuth: [] operationId: getContactAttributes summary: Get contact attributes description: Gets contact attributes from the database. @@ -704,6 +970,8 @@ paths: items: $ref: "#/components/schemas/contactAttribute" post: + security: + - apiKeyAuth: [] operationId: createContactAttribute summary: Create a contact attribute description: Creates a contact attribute in the database. @@ -721,6 +989,8 @@ paths: description: Contact attribute created successfully. /contact-attributes/{id}: get: + security: + - apiKeyAuth: [] operationId: getContactAttribute summary: Get a contact attribute description: Gets a contact attribute from the database. @@ -740,6 +1010,8 @@ paths: schema: $ref: "#/components/schemas/contactAttribute" put: + security: + - apiKeyAuth: [] operationId: updateContactAttribute summary: Update a contact attribute description: Updates a contact attribute in the database. @@ -766,6 +1038,8 @@ paths: schema: $ref: "#/components/schemas/contactAttribute" delete: + security: + - apiKeyAuth: [] operationId: deleteContactAttribute summary: Delete a contact attribute description: Deletes a contact attribute from the database. @@ -786,6 +1060,8 @@ paths: $ref: "#/components/schemas/contactAttribute" /contact-attribute-keys: get: + security: + - apiKeyAuth: [] operationId: getContactAttributeKeys summary: Get contact attribute keys description: Gets contact attribute keys from the database. @@ -839,6 +1115,8 @@ paths: items: $ref: "#/components/schemas/contactAttributeKey" post: + security: + - apiKeyAuth: [] operationId: createContactAttributeKey summary: Create a contact attribute key description: Creates a contact attribute key in the database. @@ -856,6 +1134,8 @@ paths: description: Contact attribute key created successfully. /contact-attribute-keys/{id}: get: + security: + - apiKeyAuth: [] operationId: getContactAttributeKey summary: Get a contact attribute key description: Gets a contact attribute key from the database. @@ -875,6 +1155,8 @@ paths: schema: $ref: "#/components/schemas/contactAttributeKey" put: + security: + - apiKeyAuth: [] operationId: updateContactAttributeKey summary: Update a contact attribute key description: Updates a contact attribute key in the database. @@ -901,6 +1183,8 @@ paths: schema: $ref: "#/components/schemas/contactAttributeKey" delete: + security: + - apiKeyAuth: [] operationId: deleteContactAttributeKey summary: Delete a contact attribute key description: Deletes a contact attribute key from the database. @@ -921,6 +1205,8 @@ paths: $ref: "#/components/schemas/contactAttributeKey" /surveys: get: + security: + - apiKeyAuth: [] operationId: getSurveys summary: Get surveys description: Gets surveys from the database. @@ -991,6 +1277,8 @@ paths: items: $ref: "#/components/schemas/survey" post: + security: + - apiKeyAuth: [] operationId: createSurvey summary: Create a survey description: Creates a survey in the database. @@ -1012,6 +1300,8 @@ paths: $ref: "#/components/schemas/survey" /surveys/{id}: get: + security: + - apiKeyAuth: [] operationId: getSurvey summary: Get a survey description: Gets a survey from the database. @@ -1032,6 +1322,8 @@ paths: schema: $ref: "#/components/schemas/survey" put: + security: + - apiKeyAuth: [] operationId: updateSurvey summary: Update a survey description: Updates a survey in the database. @@ -1059,6 +1351,8 @@ paths: schema: $ref: "#/components/schemas/survey" delete: + security: + - apiKeyAuth: [] operationId: deleteSurvey summary: Delete a survey description: Deletes a survey from the database. @@ -1078,6 +1372,230 @@ paths: application/json: schema: $ref: "#/components/schemas/survey" + /api/v2/management/storage: + post: + security: + - apiKeyAuth: [] + summary: Upload Public File + description: > + API endpoint for uploading public files. Uploaded files are public and accessible by anyone. + This endpoint requires authentication. It accepts a JSON body with fileName, fileType, environmentId, + and optionally allowedFileExtensions to restrict file types. On success, it returns a signed URL for uploading + the file to S3 along with a local upload URL. + tags: + - Management API > Storage + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + fileName: + type: string + description: The name of the file to be uploaded. + fileType: + type: string + description: The MIME type of the file. + environmentId: + type: string + description: The ID of the environment. + allowedFileExtensions: + type: array + items: + type: string + description: Optional. List of allowed file extensions. + required: + - fileName + - fileType + - environmentId + example: + fileName: "profile.png" + fileType: "image/png" + environmentId: "env123" + allowedFileExtensions: ["png", "jpg", "jpeg"] + responses: + "200": + description: OK - Returns the signed URL, updated file name, and file URL. + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + signedUrl: + type: string + description: Signed URL for uploading the file to S3. + localUrl: + type: string + description: URL for uploading the file to local storage. + updatedFileName: + type: string + description: The updated file name after processing. + fileUrl: + type: string + description: URL where the uploaded file can be accessed. + example: + data: + signedUrl: "http://localhost:3000/api/v1/client/cm1ubebtj000614kqe4hs3c67/storage/public" + localUrl: "http://localhost:3000/storage/cm1ubebtj000614kqe4hs3c67/public/profile.png" + updatedFileName: "profile--fid--abc123.png" + fileUrl: "http://localhost:3000/storage/cm1ubebtj000614kqe4hs3c67/public/profile--fid--abc123.png" + "400": + description: Bad Request - Missing required fields or invalid file extension. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "fileName is required" + "401": + description: Unauthorized - Authentication failed or user not logged in. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "Not authenticated" + "403": + description: Forbidden - User does not have access to the specified environment. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "User does not have access to environment env123" + /api/v2/management/storage/local: + put: + summary: Upload Public File to Local Storage + security: + - apiKeyAuth: [] + description: > + Management API endpoint for uploading public files to local storage. This endpoint requires authentication. + File metadata is provided via headers (X-File-Type, X-File-Name, X-Environment-ID, X-Signature, X-UUID, X-Timestamp) + and the file is provided as a multipart/form-data file field named "file". The "Content-Type" header must be set to a valid MIME type. + tags: + - Management API > Storage + parameters: + - in: header + name: X-File-Type + required: true + schema: + type: string + description: "MIME type of the file. Must be a valid MIME type." + - in: header + name: X-File-Name + required: true + schema: + type: string + description: "URI encoded file name." + - in: header + name: X-Environment-ID + required: true + schema: + type: string + description: "ID of the environment." + - in: header + name: X-Signature + required: true + schema: + type: string + description: "Signature for verifying the request." + - in: header + name: X-UUID + required: true + schema: + type: string + description: "Unique identifier for the signed upload." + - in: header + name: X-Timestamp + required: true + schema: + type: string + description: "Timestamp used for the signature." + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: "The file to be uploaded as a valid file object (buffer)." + required: + - file + responses: + "200": + description: OK - File uploaded successfully. + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + message: + type: string + description: Success message. + example: + data: + message: "File uploaded successfully" + "400": + description: Bad Request - Missing required fields, invalid header values, or file issues. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "fileType is required" + "401": + description: Unauthorized - Authentication failed, invalid signature, or user not authorized. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "Not authenticated" + "500": + description: Internal Server Error - File upload failed due to server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Detailed error message. + example: + error: "File upload failed" + servers: + - url: https://app.formbricks.com/api/v2 + description: Formbricks API Server components: securitySchemes: apiKeyAuth: diff --git a/packages/types/storage.ts b/packages/types/storage.ts index 974fef89f6..634cbc5ecd 100644 --- a/packages/types/storage.ts +++ b/packages/types/storage.ts @@ -16,7 +16,16 @@ export const ZUploadFileConfig = z.object({ export type TUploadFileConfig = z.infer; -const ZUploadFileResponse = z.object({ +export const ZUploadFileRequest = z.object({ + fileName: z.string(), + fileType: z.string(), + surveyId: z.string().cuid2(), + environmentId: z.string().cuid2(), +}); + +export type TUploadFileRequest = z.infer; + +export const ZUploadFileResponse = z.object({ data: z.object({ signedUrl: z.string(), fileUrl: z.string(), From 306f65461734400519b3882fbfd0d452378598b5 Mon Sep 17 00:00:00 2001 From: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Date: Mon, 24 Mar 2025 08:49:04 -0300 Subject: [PATCH 09/54] fix: add membership checks in remaining routes (#5026) --- .../[organizationId]/landing/page.tsx | 14 +- .../projects/new/channel/page.tsx | 9 +- .../projects/new/mode/page.tsx | 9 +- .../projects/new/settings/page.tsx | 21 +-- .../environments/[environmentId]/page.tsx | 17 +-- .../(organization)/general/page.test.tsx | 69 ++++------ .../settings/(organization)/general/page.tsx | 23 +--- .../[surveyId]/(analysis)/responses/page.tsx | 44 +------ .../[surveyId]/(analysis)/summary/page.tsx | 43 +----- apps/web/modules/organization/lib/utils.ts | 49 +++++++ .../organization/settings/teams/page.tsx | 15 +-- .../organization/types/organization-auth.ts | 19 +++ apps/web/modules/survey/editor/page.tsx | 43 ++---- apps/web/modules/survey/list/page.tsx | 124 ++++++++---------- apps/web/modules/survey/templates/page.tsx | 33 +---- 15 files changed, 203 insertions(+), 329 deletions(-) create mode 100644 apps/web/modules/organization/lib/utils.ts create mode 100644 apps/web/modules/organization/types/organization-auth.ts diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx index 5dea2c22d6..5bc2b635e7 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx @@ -1,27 +1,25 @@ import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar"; -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils"; +import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { Header } from "@/modules/ui/components/header"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { notFound, redirect } from "next/navigation"; -import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service"; +import { getOrganizationsByUserId } from "@formbricks/lib/organization/service"; import { getUser } from "@formbricks/lib/user/service"; const Page = async (props) => { const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); - if (!session || !session.user) { + + const { session, organization } = await getOrganizationAuth(params.organizationId); + + if (!session?.user) { return redirect(`/auth/login`); } const user = await getUser(session.user.id); if (!user) return notFound(); - const organization = await getOrganization(params.organizationId); - if (!organization) return notFound(); - const organizations = await getOrganizationsByUserId(session.user.id); const { features } = await getEnterpriseLicense(); diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.tsx index 4ac6aa42bb..4309addd10 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/channel/page.tsx @@ -1,10 +1,9 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; -import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { getTranslate } from "@/tolgee/server"; import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react"; -import { getServerSession } from "next-auth"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getUserProjects } from "@formbricks/lib/project/service"; @@ -17,8 +16,10 @@ interface ChannelPageProps { const Page = async (props: ChannelPageProps) => { const params = await props.params; - const session = await getServerSession(authOptions); - if (!session || !session.user) { + + const { session } = await getOrganizationAuth(params.organizationId); + + if (!session?.user) { return redirect(`/auth/login`); } diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.tsx index c3574c0a9c..a570a6ed89 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/mode/page.tsx @@ -1,10 +1,9 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; -import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { getTranslate } from "@/tolgee/server"; import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react"; -import { getServerSession } from "next-auth"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getUserProjects } from "@formbricks/lib/project/service"; @@ -17,8 +16,10 @@ interface ModePageProps { const Page = async (props: ModePageProps) => { const params = await props.params; - const session = await getServerSession(authOptions); - if (!session || !session.user) { + + const { session } = await getOrganizationAuth(params.organizationId); + + if (!session?.user) { return redirect(`/auth/login`); } diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx index 9c7d4f856c..5a6098b3d3 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx @@ -1,16 +1,14 @@ import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding"; import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings"; -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; +import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { getTranslate } from "@/tolgee/server"; import { XIcon } from "lucide-react"; -import { getServerSession } from "next-auth"; import Link from "next/link"; import { redirect } from "next/navigation"; import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; -import { getOrganization } from "@formbricks/lib/organization/service"; import { getUserProjects } from "@formbricks/lib/project/service"; import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project"; @@ -29,25 +27,20 @@ const Page = async (props: ProjectSettingsPageProps) => { const searchParams = await props.searchParams; const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); - if (!session || !session.user) { + const { session, organization } = await getOrganizationAuth(params.organizationId); + + if (!session?.user) { return redirect(`/auth/login`); } - const channel = searchParams.channel || null; - const industry = searchParams.industry || null; - const mode = searchParams.mode || "surveys"; + const channel = searchParams.channel ?? null; + const industry = searchParams.industry ?? null; + const mode = searchParams.mode ?? "surveys"; const projects = await getUserProjects(session.user.id, params.organizationId); const organizationTeams = await getTeamsByOrganizationId(params.organizationId); - const organization = await getOrganization(params.organizationId); - - if (!organization) { - throw new Error(t("common.organization_not_found")); - } - const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); if (!organizationTeams) { diff --git a/apps/web/app/(app)/environments/[environmentId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/page.tsx index 96739edc0c..062dfe781e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/page.tsx @@ -1,24 +1,11 @@ -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { redirect } from "next/navigation"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; const EnvironmentPage = async (props) => { const params = await props.params; - const session = await getServerSession(authOptions); - const t = await getTranslate(); - const organization = await getOrganizationByEnvironmentId(params.environmentId); - - if (!session) { - return redirect(`/auth/login`); - } - - if (!organization) { - throw new Error(t("common.organization_not_found")); - } + const { session, organization } = await getEnvironmentAuth(params.environmentId); const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); const { isBilling } = getAccessFlags(currentUserMembership?.role); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx index 097a427527..b31e6432d6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.test.tsx @@ -3,15 +3,11 @@ import { getIsOrganizationAIReady, getWhiteLabelPermission, } from "@/modules/ee/license-check/lib/utils"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; +import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getUser } from "@formbricks/lib/user/service"; -import { TMembership } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; import { TUser } from "@formbricks/types/user"; import Page from "./page"; @@ -45,10 +41,6 @@ vi.mock("@formbricks/lib/constants", () => ({ AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id", })); -vi.mock("next-auth", () => ({ - getServerSession: vi.fn(), -})); - vi.mock("@/tolgee/server", () => ({ getTranslate: vi.fn(), })); @@ -57,16 +49,8 @@ vi.mock("@formbricks/lib/user/service", () => ({ getUser: vi.fn(), })); -vi.mock("@formbricks/lib/organization/service", () => ({ - getOrganizationByEnvironmentId: vi.fn(), -})); - -vi.mock("@formbricks/lib/membership/service", () => ({ - getMembershipByUserIdOrganizationId: vi.fn(), -})); - -vi.mock("@formbricks/lib/membership/utils", () => ({ - getAccessFlags: vi.fn(), +vi.mock("@/modules/environments/lib/utils", () => ({ + getEnvironmentAuth: vi.fn(), })); vi.mock("@/modules/ee/license-check/lib/utils", () => ({ @@ -76,26 +60,21 @@ vi.mock("@/modules/ee/license-check/lib/utils", () => ({ })); describe("Page", () => { - const mockParams = { environmentId: "test-environment-id" }; - const mockSession = { user: { id: "test-user-id" } }; + let mockEnvironmentAuth = { + session: { user: { id: "test-user-id" } }, + currentUserMembership: { role: "owner" }, + organization: { id: "test-organization-id", billing: { plan: "free" } }, + isOwner: true, + isManager: false, + } as unknown as TEnvironmentAuth; + const mockUser = { id: "test-user-id" } as TUser; - const mockOrganization = { id: "test-organization-id", billing: { plan: "free" } } as TOrganization; - const mockMembership = { role: "owner" } as TMembership; const mockTranslate = vi.fn((key) => key); beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(getServerSession).mockResolvedValue(mockSession); vi.mocked(getTranslate).mockResolvedValue(mockTranslate); vi.mocked(getUser).mockResolvedValue(mockUser); - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization); - vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); - vi.mocked(getAccessFlags).mockReturnValue({ - isOwner: true, - isManager: false, - isBilling: false, - isMember: false, - }); + vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth); vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); vi.mocked(getIsOrganizationAIReady).mockResolvedValue(true); vi.mocked(getWhiteLabelPermission).mockResolvedValue(true); @@ -111,8 +90,10 @@ describe("Page", () => { expect(result).toBeTruthy(); }); - it("renders if session user id is null", async () => { - vi.mocked(getServerSession).mockResolvedValue({ user: { id: null } }); + it("renders if session user id empty", async () => { + mockEnvironmentAuth.session.user.id = ""; + + vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth); const props = { params: Promise.resolve({ environmentId: "env-123" }), @@ -123,17 +104,13 @@ describe("Page", () => { expect(result).toBeTruthy(); }); - it("throws an error if the session is not found", async () => { - vi.mocked(getServerSession).mockResolvedValue(null); + it("handles getEnvironmentAuth error", async () => { + vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error")); - await expect(Page({ params: Promise.resolve(mockParams) })).rejects.toThrow("common.session_not_found"); - }); + const props = { + params: Promise.resolve({ environmentId: "env-123" }), + }; - it("throws an error if the organization is not found", async () => { - vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null); - - await expect(Page({ params: Promise.resolve(mockParams) })).rejects.toThrow( - "common.organization_not_found" - ); + await expect(Page(props)).rejects.toThrow("Authentication error"); }); }); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx index f926cd6580..d50c058260 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx @@ -1,21 +1,17 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle"; -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsMultiOrgEnabled, getIsOrganizationAIReady, getWhiteLabelPermission, } from "@/modules/ee/license-check/lib/utils"; import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { SettingsId } from "@/modules/ui/components/settings-id"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getUser } from "@formbricks/lib/user/service"; import { SettingsCard } from "../../components/SettingsCard"; import { DeleteOrganization } from "./components/DeleteOrganization"; @@ -24,20 +20,13 @@ import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm" const Page = async (props: { params: Promise<{ environmentId: string }> }) => { const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); - if (!session) { - throw new Error(t("common.session_not_found")); - } + + const { session, currentUserMembership, organization, isOwner, isManager } = await getEnvironmentAuth( + params.environmentId + ); + const user = session?.user?.id ? await getUser(session.user.id) : null; - const organization = await getOrganizationByEnvironmentId(params.environmentId); - - if (!organization) { - throw new Error(t("common.organization_not_found")); - } - - const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); - const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role); const isMultiOrgEnabled = await getIsMultiOrgEnabled(); const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.billing.plan); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index d087986bfb..eb653e4003 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -3,24 +3,16 @@ import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { MAX_RESPONSES_FOR_INSIGHT_GENERATION, RESPONSES_PER_PAGE, WEBAPP_URL, } from "@formbricks/lib/constants"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; @@ -30,47 +22,25 @@ import { findMatchingLocale } from "@formbricks/lib/utils/locale"; const Page = async (props) => { const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); - if (!session) { - throw new Error(t("common.session_not_found")); - } - const [survey, environment] = await Promise.all([ - getSurvey(params.surveyId), - getEnvironment(params.environmentId), - ]); - if (!environment) { - throw new Error(t("common.environment_not_found")); - } + const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId); + + const survey = await getSurvey(params.surveyId); + if (!survey) { throw new Error(t("common.survey_not_found")); } - const project = await getProjectByEnvironmentId(environment.id); - if (!project) { - throw new Error(t("common.project_not_found")); - } const user = await getUser(session.user.id); + if (!user) { throw new Error(t("common.user_not_found")); } + const tags = await getTagsByEnvironmentId(params.environmentId); - const organization = await getOrganizationByEnvironmentId(params.environmentId); - if (!organization) { - throw new Error(t("common.organization_not_found")); - } - - const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); const totalResponseCount = await getResponseCountBySurveyId(params.surveyId); - const { isMember } = getAccessFlags(currentUserMembership?.role); - - const permission = await getProjectPermissionByUserId(session.user.id, project.id); - const { hasReadAccess } = getTeamPermissionFlags(permission); - - const isReadOnly = isMember && hasReadAccess; - const isAIEnabled = await getIsAIEnabled({ isAIEnabled: organization.isAIEnabled, billing: organization.billing, diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index 6557fe7643..018953705f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -3,14 +3,11 @@ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/s import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { notFound } from "next/navigation"; import { DEFAULT_LOCALE, @@ -18,11 +15,6 @@ import { MAX_RESPONSES_FOR_INSIGHT_GENERATION, WEBAPP_URL, } from "@formbricks/lib/constants"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; import { getUser } from "@formbricks/lib/user/service"; @@ -30,10 +22,8 @@ import { getUser } from "@formbricks/lib/user/service"; const SurveyPage = async (props: { params: Promise<{ environmentId: string; surveyId: string }> }) => { const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); - if (!session) { - throw new Error(t("common.session_not_found")); - } + + const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId); const surveyId = params.surveyId; @@ -41,41 +31,20 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv return notFound(); } - const [survey, environment] = await Promise.all([ - getSurvey(params.surveyId), - getEnvironment(params.environmentId), - ]); - if (!environment) { - throw new Error(t("common.environment_not_found")); - } + const survey = await getSurvey(params.surveyId); + if (!survey) { throw new Error(t("common.survey_not_found")); } - const project = await getProjectByEnvironmentId(environment.id); - if (!project) { - throw new Error(t("common.project_not_found")); - } - const user = await getUser(session.user.id); + if (!user) { throw new Error(t("common.user_not_found")); } - const organization = await getOrganizationByEnvironmentId(params.environmentId); - - if (!organization) { - throw new Error(t("common.organization_not_found")); - } - const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); const totalResponseCount = await getResponseCountBySurveyId(params.surveyId); - const { isMember } = getAccessFlags(currentUserMembership?.role); - const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id); - const { hasReadAccess } = getTeamPermissionFlags(projectPermission); - - const isReadOnly = isMember && hasReadAccess; - // I took this out cause it's cloud only right? // const { active: isEnterpriseEdition } = await getEnterpriseLicense(); diff --git a/apps/web/modules/organization/lib/utils.ts b/apps/web/modules/organization/lib/utils.ts new file mode 100644 index 0000000000..f5041b256d --- /dev/null +++ b/apps/web/modules/organization/lib/utils.ts @@ -0,0 +1,49 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getTranslate } from "@/tolgee/server"; +import { getServerSession } from "next-auth"; +import { cache } from "react"; +import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; +import { getAccessFlags } from "@formbricks/lib/membership/utils"; +import { getOrganization } from "@formbricks/lib/organization/service"; +import { TOrganizationAuth } from "../types/organization-auth"; + +/** + * Common utility to fetch organization data and perform authorization checks + * + * Usage: + * const { session, organization, ... } = await getOrganizationAuth(params.organizationId); + */ +export const getOrganizationAuth = cache(async (organizationId: string): Promise => { + const t = await getTranslate(); + + // Perform all fetches in parallel + const [session, organization] = await Promise.all([ + getServerSession(authOptions), + getOrganization(organizationId), + ]); + + if (!session) { + throw new Error(t("common.session_not_found")); + } + + if (!organization) { + throw new Error(t("common.organization_not_found")); + } + + const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); + if (!currentUserMembership) { + throw new Error(t("common.membership_not_found")); + } + + const { isMember, isOwner, isManager, isBilling } = getAccessFlags(currentUserMembership?.role); + + return { + organization, + session, + currentUserMembership, + isMember, + isOwner, + isManager, + isBilling, + }; +}); diff --git a/apps/web/modules/organization/settings/teams/page.tsx b/apps/web/modules/organization/settings/teams/page.tsx index 7ca9c69c58..24684ea1ca 100644 --- a/apps/web/modules/organization/settings/teams/page.tsx +++ b/apps/web/modules/organization/settings/teams/page.tsx @@ -1,31 +1,20 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { MembersView } from "@/modules/organization/settings/teams/components/members-view"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; export const TeamsPage = async (props) => { const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); - if (!session) { - throw new Error(t("common.session_not_found")); - } - const organization = await getOrganizationByEnvironmentId(params.environmentId); - if (!organization) { - throw new Error(t("common.organization_not_found")); - } + const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId); const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); - const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); return ( diff --git a/apps/web/modules/organization/types/organization-auth.ts b/apps/web/modules/organization/types/organization-auth.ts new file mode 100644 index 0000000000..34a78c709a --- /dev/null +++ b/apps/web/modules/organization/types/organization-auth.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; +import { ZMembership } from "@formbricks/types/memberships"; +import { ZOrganization } from "@formbricks/types/organizations"; +import { ZUser } from "@formbricks/types/user"; + +export const ZOrganizationAuth = z.object({ + organization: ZOrganization, + session: z.object({ + user: ZUser.pick({ id: true }), + expires: z.string(), + }), + currentUserMembership: ZMembership, + isMember: z.boolean(), + isOwner: z.boolean(), + isManager: z.boolean(), + isBilling: z.boolean(), +}); + +export type TOrganizationAuth = z.infer; diff --git a/apps/web/modules/survey/editor/page.tsx b/apps/web/modules/survey/editor/page.tsx index dc3215adef..3c6bec8396 100644 --- a/apps/web/modules/survey/editor/page.tsx +++ b/apps/web/modules/survey/editor/page.tsx @@ -1,21 +1,16 @@ -import { authOptions } from "@/modules/auth/lib/authOptions"; import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys"; import { getSegments } from "@/modules/ee/contacts/segments/lib/segments"; import { getIsContactsEnabled, getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { getProjectLanguages } from "@/modules/survey/editor/lib/project"; import { getUserEmail } from "@/modules/survey/editor/lib/user"; import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { getActionClasses } from "@/modules/survey/lib/action-class"; -import { getEnvironment } from "@/modules/survey/lib/environment"; -import { getMembershipRoleByUserIdOrganizationId } from "@/modules/survey/lib/membership"; import { getProjectByEnvironmentId } from "@/modules/survey/lib/project"; import { getResponseCountBySurveyId } from "@/modules/survey/lib/response"; import { getOrganizationBilling, getSurvey } from "@/modules/survey/lib/survey"; import { ErrorComponent } from "@/modules/ui/components/error-component"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { DEFAULT_LOCALE, IS_FORMBRICKS_CLOUD, @@ -23,7 +18,6 @@ import { SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY, } from "@formbricks/lib/constants"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { SurveyEditor } from "./components/survey-editor"; import { getUserLocale } from "./lib/user"; @@ -38,31 +32,20 @@ export const generateMetadata = async (props) => { export const SurveyEditorPage = async (props) => { const searchParams = await props.searchParams; const params = await props.params; + + const { session, isMember, environment, hasReadAccess, currentUserMembership, projectPermission } = + await getEnvironmentAuth(params.environmentId); + const t = await getTranslate(); - const [ - survey, - project, - environment, - actionClasses, - contactAttributeKeys, - responseCount, - session, - segments, - ] = await Promise.all([ + const [survey, project, actionClasses, contactAttributeKeys, responseCount, segments] = await Promise.all([ getSurvey(params.surveyId), getProjectByEnvironmentId(params.environmentId), - getEnvironment(params.environmentId), getActionClasses(params.environmentId), getContactAttributeKeys(params.environmentId), getResponseCountBySurveyId(params.surveyId), - getServerSession(authOptions), getSegments(params.environmentId), ]); - if (!session) { - throw new Error(t("common.session_not_found")); - } - if (!project) { throw new Error(t("common.project_not_found")); } @@ -72,16 +55,6 @@ export const SurveyEditorPage = async (props) => { throw new Error(t("common.organization_not_found")); } - const membershipRole = await getMembershipRoleByUserIdOrganizationId( - session?.user.id, - project.organizationId - ); - const { isMember } = getAccessFlags(membershipRole); - - const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id); - - const { hasReadAccess } = getTeamPermissionFlags(projectPermission); - const isSurveyCreationDeletionDisabled = isMember && hasReadAccess; const locale = session.user.id ? await getUserLocale(session.user.id) : undefined; @@ -115,7 +88,7 @@ export const SurveyEditorPage = async (props) => { actionClasses={actionClasses} contactAttributeKeys={contactAttributeKeys} responseCount={responseCount} - membershipRole={membershipRole} + membershipRole={currentUserMembership.role} projectPermission={projectPermission} colors={SURVEY_BG_COLORS} segments={segments} @@ -124,7 +97,7 @@ export const SurveyEditorPage = async (props) => { projectLanguages={projectLanguages} plan={organizationBilling.plan} isFormbricksCloud={IS_FORMBRICKS_CLOUD} - isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false} + isUnsplashConfigured={!!UNSPLASH_ACCESS_KEY} isCxMode={isCxMode} locale={locale ?? DEFAULT_LOCALE} mailFrom={MAIL_FROM ?? "hola@formbricks.com"} diff --git a/apps/web/modules/survey/list/page.tsx b/apps/web/modules/survey/list/page.tsx index c2f47a5217..e7690de1bc 100644 --- a/apps/web/modules/survey/list/page.tsx +++ b/apps/web/modules/survey/list/page.tsx @@ -1,12 +1,7 @@ -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { TemplateList } from "@/modules/survey/components/template-list"; -import { getMembershipRoleByUserIdOrganizationId } from "@/modules/survey/lib/membership"; import { getProjectByEnvironmentId } from "@/modules/survey/lib/project"; import { SurveysList } from "@/modules/survey/list/components/survey-list"; -import { getEnvironment } from "@/modules/survey/list/lib/environment"; -import { getOrganizationIdByEnvironmentId } from "@/modules/survey/list/lib/organization"; import { getSurveyCount } from "@/modules/survey/list/lib/survey"; import { Button } from "@/modules/ui/components/button"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -14,11 +9,9 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { PlusIcon } from "lucide-react"; import { Metadata } from "next"; -import { getServerSession } from "next-auth"; import Link from "next/link"; import { redirect } from "next/navigation"; import { DEFAULT_LOCALE, SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getUserLocale } from "@formbricks/lib/user/service"; import { TTemplateRole } from "@formbricks/types/templates"; @@ -41,42 +34,22 @@ export const SurveysPage = async ({ }: SurveyTemplateProps) => { const searchParams = await searchParamsProps; const params = await paramsProps; - - const session = await getServerSession(authOptions); - const project = await getProjectByEnvironmentId(params.environmentId); - const organizationId = await getOrganizationIdByEnvironmentId(params.environmentId); const t = await getTranslate(); - if (!session) { - throw new Error(t("common.session_not_found")); - } + + const project = await getProjectByEnvironmentId(params.environmentId); if (!project) { throw new Error(t("common.project_not_found")); } - if (!organizationId) { - throw new Error(t("common.organization_not_found")); - } + const { session, isBilling, environment, isReadOnly } = await getEnvironmentAuth(params.environmentId); const prefilledFilters = [project?.config.channel, project.config.industry, searchParams.role ?? null]; - const membershipRole = await getMembershipRoleByUserIdOrganizationId(session?.user.id, organizationId); - const { isMember, isBilling } = getAccessFlags(membershipRole); - - const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id); - const { hasReadAccess } = getTeamPermissionFlags(projectPermission); - - const isReadOnly = isMember && hasReadAccess; - if (isBilling) { return redirect(`/environments/${params.environmentId}/settings/billing`); } - const environment = await getEnvironment(params.environmentId); - if (!environment) { - throw new Error(t("common.environment_not_found")); - } - const surveyCount = await getSurveyCount(params.environmentId); const currentProjectChannel = project.config.channel ?? null; @@ -92,44 +65,55 @@ export const SurveysPage = async ({ ); }; - return ( - - {surveyCount > 0 ? ( - <> - : } /> - - - ) : isReadOnly ? ( - <> -

- {t("environments.surveys.no_surveys_created_yet")} -

+ const projectWithRequiredProps = { + ...project, + brandColor: project.styling?.brandColor?.light ?? null, + highlightBorderColor: null, + }; -

- {t("environments.surveys.read_only_user_not_allowed_to_create_survey_warning")} -

- - ) : ( - <> -

- {t("environments.surveys.all_set_time_to_create_first_survey")} -

- - - )} -
- ); + let content; + if (surveyCount > 0) { + content = ( + <> + : } /> + + + ); + } else if (isReadOnly) { + content = ( + <> +

+ {t("environments.surveys.no_surveys_created_yet")} +

+ +

+ {t("environments.surveys.read_only_user_not_allowed_to_create_survey_warning")} +

+ + ); + } else { + content = ( + <> +

+ {t("environments.surveys.all_set_time_to_create_first_survey")} +

+ + + ); + } + + return {content}; }; diff --git a/apps/web/modules/survey/templates/page.tsx b/apps/web/modules/survey/templates/page.tsx index b3b9bffc4b..1e09e335d9 100644 --- a/apps/web/modules/survey/templates/page.tsx +++ b/apps/web/modules/survey/templates/page.tsx @@ -1,13 +1,7 @@ -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; -import { getEnvironment } from "@/modules/survey/lib/environment"; -import { getMembershipRoleByUserIdOrganizationId } from "@/modules/survey/lib/membership"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { getProjectByEnvironmentId } from "@/modules/survey/lib/project"; import { getTranslate } from "@/tolgee/server"; -import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project"; import { TTemplateRole } from "@formbricks/types/templates"; import { TemplateContainerWithPreview } from "./components/template-container"; @@ -25,37 +19,18 @@ interface SurveyTemplateProps { export const SurveyTemplatesPage = async (props: SurveyTemplateProps) => { const searchParams = await props.searchParams; - const params = await props.params; const t = await getTranslate(); - const session = await getServerSession(authOptions); + const params = await props.params; const environmentId = params.environmentId; - if (!session) { - throw new Error(t("common.session_not_found")); - } + const { session, environment, isReadOnly } = await getEnvironmentAuth(environmentId); - const [environment, project] = await Promise.all([ - getEnvironment(environmentId), - getProjectByEnvironmentId(environmentId), - ]); + const project = await getProjectByEnvironmentId(environmentId); if (!project) { throw new Error(t("common.project_not_found")); } - if (!environment) { - throw new Error(t("common.environment_not_found")); - } - const membershipRole = await getMembershipRoleByUserIdOrganizationId( - session?.user.id, - project.organizationId - ); - const { isMember } = getAccessFlags(membershipRole); - - const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id); - const { hasReadAccess } = getTeamPermissionFlags(projectPermission); - - const isReadOnly = isMember && hasReadAccess; if (isReadOnly) { return redirect(`/environments/${environment.id}/surveys`); } From 010784c2b2285dccf48549e155ba9954da3f7c20 Mon Sep 17 00:00:00 2001 From: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Date: Mon, 24 Mar 2025 09:12:37 -0300 Subject: [PATCH 10/54] chore: added new script with onload (#4987) Co-authored-by: pandeymangg --- .../components/OnboardingSetupInstructions.tsx | 4 ++-- .../(setup)/components/setup-instructions.tsx | 2 +- apps/web/playwright/js.spec.ts | 15 ++++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx index 33822ca1c9..7ceb44322a 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx @@ -36,7 +36,7 @@ export const OnboardingSetupInstructions = ({ !function(){ var appUrl = "${webAppUrl}"; var environmentId = "${environmentId}"; - var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.setup({environmentId: environmentId, appUrl: appUrl})},500)}(); + var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}(); `; @@ -46,7 +46,7 @@ export const OnboardingSetupInstructions = ({ !function(){ var appUrl = "${webAppUrl}"; var environmentId = "${environmentId}"; - var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.setup({environmentId: environmentId, appUrl: appUrl })},500)}(); + var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}(); `; diff --git a/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx b/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx index 1f25876959..7035729423 100644 --- a/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx +++ b/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx @@ -129,7 +129,7 @@ if (typeof window !== "undefined") {

{` `}

Step 2: Debug mode

diff --git a/apps/web/playwright/js.spec.ts b/apps/web/playwright/js.spec.ts index d3a05ca059..8c3979a890 100644 --- a/apps/web/playwright/js.spec.ts +++ b/apps/web/playwright/js.spec.ts @@ -8,13 +8,14 @@ const HTML_TEMPLATE = ` var t = document.createElement("script"); (t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/js/formbricks.umd.cjs"); var e = document.getElementsByTagName("script")[0]; - e.parentNode.insertBefore(t, e), - setTimeout(function () { - formbricks.setup({ - environmentId: "ENVIRONMENT_ID", - appUrl: "http://localhost:3000", - }); - }, 500); + t.onload = function(){ + if (window.formbricks) { + window.formbricks.setup({environmentId: "ENVIRONMENT_ID", appUrl: "http://localhost:3000"}); + } else { + console.error("Formbricks library failed to load properly. The formbricks object is not available."); + } + }; + e.parentNode.insertBefore(t, e); })(); From b3f336c959dc16b02bf379bf9b119906601c47a8 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:29:31 +0530 Subject: [PATCH 11/54] fix: invite user bug (#5043) Co-authored-by: pandeymangg --- .../ee/role-management/components/add-member-role.tsx | 6 +++--- .../role-management/components/edit-membership-role.tsx | 5 +++-- .../components/invite-member/individual-invite-tab.tsx | 2 +- packages/logger/package.json | 8 ++++++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/web/modules/ee/role-management/components/add-member-role.tsx b/apps/web/modules/ee/role-management/components/add-member-role.tsx index 3682070ea3..cfba5eb3a2 100644 --- a/apps/web/modules/ee/role-management/components/add-member-role.tsx +++ b/apps/web/modules/ee/role-management/components/add-member-role.tsx @@ -37,7 +37,7 @@ export function AddMemberRole({ let rolesArray = ["member"]; if (isOwner) { - rolesArray.push("owner", "manager"); + rolesArray.push("manager", "owner"); if (isFormbricksCloud) { rolesArray.push("billing"); } @@ -62,7 +62,7 @@ export function AddMemberRole({