diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
index 94db1a39eb..3e8039e5f2 100644
--- a/apps/web/Dockerfile
+++ b/apps/web/Dockerfile
@@ -40,7 +40,6 @@ RUN echo '#!/bin/sh' > /tmp/read-secrets.sh && \
echo 'exec "$@"' >> /tmp/read-secrets.sh && \
chmod +x /tmp/read-secrets.sh
-ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN
# Increase Node.js memory limit as a regular build argument
diff --git a/apps/web/app/layout.test.tsx b/apps/web/app/layout.test.tsx
index 51abc5195b..495ec8f9ce 100644
--- a/apps/web/app/layout.test.tsx
+++ b/apps/web/app/layout.test.tsx
@@ -29,6 +29,7 @@ vi.mock("@formbricks/lib/constants", () => ({
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
WEBAPP_URL: "test-webapp-url",
IS_PRODUCTION: false,
+ SENTRY_DSN: "mock-sentry-dsn",
}));
vi.mock("@/tolgee/language", () => ({
@@ -69,6 +70,15 @@ vi.mock("@/tolgee/client", () => ({
),
}));
+vi.mock("@/app/sentry/SentryProvider", () => ({
+ SentryProvider: ({ children, sentryDsn }: { children: React.ReactNode; sentryDsn?: string }) => (
+
+ SentryProvider: {sentryDsn}
+ {children}
+
+ ),
+}));
+
describe("RootLayout", () => {
beforeEach(() => {
cleanup();
@@ -97,6 +107,7 @@ describe("RootLayout", () => {
expect(screen.getByTestId("speed-insights")).toBeInTheDocument();
expect(screen.getByTestId("ph-provider")).toBeInTheDocument();
expect(screen.getByTestId("tolgee-next-provider")).toBeInTheDocument();
+ expect(screen.getByTestId("sentry-provider")).toBeInTheDocument();
expect(screen.getByTestId("child")).toHaveTextContent("Child Content");
});
});
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index ede4738cd7..bc235ff730 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,3 +1,4 @@
+import { SentryProvider } from "@/app/sentry/SentryProvider";
import { PHProvider } from "@/modules/ui/components/post-hog-client";
import { TolgeeNextProvider } from "@/tolgee/client";
import { getLocale } from "@/tolgee/language";
@@ -6,7 +7,7 @@ import { TolgeeStaticData } from "@tolgee/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Metadata } from "next";
import React from "react";
-import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
+import { IS_POSTHOG_CONFIGURED, SENTRY_DSN } from "@formbricks/lib/constants";
import "../modules/ui/globals.css";
export const metadata: Metadata = {
@@ -27,11 +28,13 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
{process.env.VERCEL === "1" && }
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
);
diff --git a/apps/web/app/sentry/SentryProvider.test.tsx b/apps/web/app/sentry/SentryProvider.test.tsx
new file mode 100644
index 0000000000..40b58e7165
--- /dev/null
+++ b/apps/web/app/sentry/SentryProvider.test.tsx
@@ -0,0 +1,101 @@
+import * as Sentry from "@sentry/nextjs";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { SentryProvider } from "./SentryProvider";
+
+vi.mock("@sentry/nextjs", async () => {
+ const actual = await vi.importActual("@sentry/nextjs");
+ return {
+ ...actual,
+ replayIntegration: (options: any) => {
+ return {
+ name: "Replay",
+ id: "Replay",
+ options,
+ };
+ },
+ };
+});
+
+describe("SentryProvider", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("calls Sentry.init when sentryDsn is provided", () => {
+ const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0";
+ const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
+
+ render(
+
+ Test Content
+
+ );
+
+ // The useEffect runs after mount, so Sentry.init should have been called.
+ expect(initSpy).toHaveBeenCalled();
+ expect(initSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ dsn: sentryDsn,
+ tracesSampleRate: 1,
+ debug: false,
+ replaysOnErrorSampleRate: 1.0,
+ replaysSessionSampleRate: 0.1,
+ integrations: expect.any(Array),
+ beforeSend: expect.any(Function),
+ })
+ );
+ });
+
+ it("does not call Sentry.init when sentryDsn is not provided", () => {
+ const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
+
+ render(
+
+ Test Content
+
+ );
+
+ expect(initSpy).not.toHaveBeenCalled();
+ });
+
+ it("renders children", () => {
+ const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0";
+ render(
+
+ Test Content
+
+ );
+ expect(screen.getByTestId("child")).toHaveTextContent("Test Content");
+ });
+
+ it("processes beforeSend correctly", () => {
+ const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0";
+ const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
+
+ render(
+
+ Test Content
+
+ );
+
+ const config = initSpy.mock.calls[0][0];
+ expect(config).toHaveProperty("beforeSend");
+ const beforeSend = config.beforeSend;
+
+ if (!beforeSend) {
+ throw new Error("beforeSend is not defined");
+ }
+
+ const dummyEvent = { some: "event" } as unknown as Sentry.ErrorEvent;
+
+ const hintWithNextNotFound = { originalException: { digest: "NEXT_NOT_FOUND" } };
+ expect(beforeSend(dummyEvent, hintWithNextNotFound)).toBeNull();
+
+ const hintWithOtherError = { originalException: { digest: "OTHER_ERROR" } };
+ expect(beforeSend(dummyEvent, hintWithOtherError)).toEqual(dummyEvent);
+
+ const hintWithoutError = { originalException: undefined };
+ expect(beforeSend(dummyEvent, hintWithoutError)).toEqual(dummyEvent);
+ });
+});
diff --git a/apps/web/app/sentry/SentryProvider.tsx b/apps/web/app/sentry/SentryProvider.tsx
new file mode 100644
index 0000000000..b01e71dfc4
--- /dev/null
+++ b/apps/web/app/sentry/SentryProvider.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import * as Sentry from "@sentry/nextjs";
+import { useEffect } from "react";
+
+interface SentryProviderProps {
+ children: React.ReactNode;
+ sentryDsn?: string;
+}
+
+export const SentryProvider = ({ children, sentryDsn }: SentryProviderProps) => {
+ useEffect(() => {
+ if (sentryDsn) {
+ Sentry.init({
+ dsn: sentryDsn,
+
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+
+ replaysOnErrorSampleRate: 1.0,
+
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
+
+ // You can remove this option if you're not planning to use the Sentry Session Replay feature:
+ integrations: [
+ Sentry.replayIntegration({
+ // Additional Replay configuration goes in here, for example:
+ maskAllText: true,
+ blockAllMedia: true,
+ }),
+ ],
+
+ beforeSend(event, hint) {
+ const error = hint.originalException as Error;
+
+ // @ts-expect-error
+ if (error && error.digest === "NEXT_NOT_FOUND") {
+ return null;
+ }
+
+ return event;
+ },
+ });
+ }
+ }, []);
+
+ return <>{children}>;
+};
diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts
index 0b527429c2..e86284efd3 100644
--- a/apps/web/instrumentation.ts
+++ b/apps/web/instrumentation.ts
@@ -1,8 +1,14 @@
-import { env } from "@formbricks/lib/env";
+import { PROMETHEUS_ENABLED, SENTRY_DSN } from "@formbricks/lib/constants";
// instrumentation.ts
export const register = async () => {
- if (process.env.NEXT_RUNTIME === "nodejs" && env.PROMETHEUS_ENABLED) {
+ if (process.env.NEXT_RUNTIME === "nodejs" && PROMETHEUS_ENABLED) {
await import("./instrumentation-node");
}
+ if (process.env.NEXT_RUNTIME === "nodejs" && SENTRY_DSN) {
+ await import("./sentry.server.config");
+ }
+ if (process.env.NEXT_RUNTIME === "edge" && SENTRY_DSN) {
+ await import("./sentry.edge.config");
+ }
};
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index eb1eed2cfa..7eb8d1a5f8 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -321,7 +321,7 @@ const sentryConfig = {
disableLogger: true,
};
-const exportConfig = process.env.NEXT_PUBLIC_SENTRY_DSN
+const exportConfig = process.env.SENTRY_DSN
? withSentryConfig(nextConfig, sentryOptions)
: nextConfig;
diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts
deleted file mode 100644
index b5765e8e3c..0000000000
--- a/apps/web/sentry.client.config.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// This file configures the initialization of Sentry on the client.
-// The config you add here will be used whenever a users loads a page in their browser.
-// https://docs.sentry.io/platforms/javascript/guides/nextjs/
-import * as Sentry from "@sentry/nextjs";
-
-Sentry.init({
- dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
-
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1,
-
- // Setting this option to true will print useful information to the console while you're setting up Sentry.
- debug: false,
-
- replaysOnErrorSampleRate: 1.0,
-
- // This sets the sample rate to be 10%. You may want this to be 100% while
- // in development and sample at a lower rate in production
- replaysSessionSampleRate: 0.1,
-
- // You can remove this option if you're not planning to use the Sentry Session Replay feature:
- integrations: [
- Sentry.replayIntegration({
- // Additional Replay configuration goes in here, for example:
- maskAllText: true,
- blockAllMedia: true,
- }),
- ],
-
- beforeSend(event, hint) {
- const error = hint.originalException as Error;
-
- // @ts-expect-error
- if (error && error.digest === "NEXT_NOT_FOUND") {
- return null;
- }
-
- return event;
- },
-});
diff --git a/apps/web/sentry.edge.config.ts b/apps/web/sentry.edge.config.ts
index 45a2a3a210..c882e14790 100644
--- a/apps/web/sentry.edge.config.ts
+++ b/apps/web/sentry.edge.config.ts
@@ -3,13 +3,20 @@
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
+import { SENTRY_DSN } from "@formbricks/lib/constants";
-Sentry.init({
- dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+if (SENTRY_DSN) {
+ console.log("Sentry DSN found, enabling Sentry on the edge");
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1,
+ Sentry.init({
+ dsn: SENTRY_DSN,
- // Setting this option to true will print useful information to the console while you're setting up Sentry.
- debug: false,
-});
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+ });
+} else {
+ console.warn("Sentry DSN not found, Sentry will be disabled on the edge");
+}
diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts
index f91917df8a..c7a59cf053 100644
--- a/apps/web/sentry.server.config.ts
+++ b/apps/web/sentry.server.config.ts
@@ -2,27 +2,34 @@
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
+import { SENTRY_DSN } from "@formbricks/lib/constants";
-Sentry.init({
- dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+if (SENTRY_DSN) {
+ console.log("Sentry DSN found, enabling Sentry on the server");
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1,
+ Sentry.init({
+ dsn: SENTRY_DSN,
- // Setting this option to true will print useful information to the console while you're setting up Sentry.
- debug: false,
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
- // uncomment the line below to enable Spotlight (https://spotlightjs.com)
- // spotlight: process.env.NODE_ENV === 'development',
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
- beforeSend(event, hint) {
- const error = hint.originalException as Error;
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: process.env.NODE_ENV === 'development',
- // @ts-expect-error
- if (error && error.digest === "NEXT_NOT_FOUND") {
- return null;
- }
+ beforeSend(event, hint) {
+ const error = hint.originalException as Error;
- return event;
- },
-});
+ // @ts-expect-error
+ if (error && error.digest === "NEXT_NOT_FOUND") {
+ return null;
+ }
+
+ return event;
+ },
+ });
+} else {
+ console.warn("Sentry DSN not found, Sentry will be disabled on the server");
+}
diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts
index 25297608c9..38b8557e90 100644
--- a/apps/web/vite.config.mts
+++ b/apps/web/vite.config.mts
@@ -32,14 +32,13 @@ export default defineConfig({
"app/(app)/environments/**/components/PosthogIdentify.tsx",
"app/(app)/(onboarding)/organizations/**/layout.tsx",
"app/(app)/(survey-editor)/environments/**/layout.tsx",
- "modules/ee/sso/lib/**/*.ts",
- "modules/ee/contacts/lib/**/*.ts",
- "modules/survey/link/lib/**/*.ts",
"app/(auth)/layout.tsx",
"app/(app)/layout.tsx",
"app/layout.tsx",
- "app/(app)/environments/**/surveys/**/(analysis)/summary/components/SurveyAnalysisCTA.tsx",
"app/intercom/*.tsx",
+ "app/sentry/*.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/SurveyAnalysisCTA.tsx",
+ "modules/ee/sso/lib/**/*.ts",
"app/lib/**/*.ts",
"app/api/(internal)/insights/lib/**/*.ts",
"modules/ee/role-management/*.ts",
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index 23e218c78d..3601858249 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -289,3 +289,7 @@ export const IS_TURNSTILE_CONFIGURED = Boolean(env.NEXT_PUBLIC_TURNSTILE_SITE_KE
export const IS_PRODUCTION = env.NODE_ENV === "production";
export const IS_DEVELOPMENT = env.NODE_ENV === "development";
+
+export const SENTRY_DSN = env.SENTRY_DSN;
+
+export const PROMETHEUS_ENABLED = env.PROMETHEUS_ENABLED === "1";
diff --git a/packages/lib/env.ts b/packages/lib/env.ts
index 069e77eb0a..e3318e6bd9 100644
--- a/packages/lib/env.ts
+++ b/packages/lib/env.ts
@@ -81,6 +81,7 @@ export const env = createEnv({
S3_ENDPOINT_URL: z.string().optional(),
S3_FORCE_PATH_STYLE: z.enum(["1", "0"]).optional(),
SAML_DATABASE_URL: z.string().optional(),
+ SENTRY_DSN: z.string().optional(),
SIGNUP_DISABLED: z.enum(["1", "0"]).optional(),
SLACK_CLIENT_ID: z.string().optional(),
SLACK_CLIENT_SECRET: z.string().optional(),
@@ -126,7 +127,6 @@ export const env = createEnv({
.or(z.string().refine((str) => str === "")),
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: z.string().optional(),
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: z.string().optional(),
- NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
},
/*
@@ -184,9 +184,9 @@ export const env = createEnv({
NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID,
+ SENTRY_DSN: process.env.SENTRY_DSN,
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY,
POSTHOG_API_HOST: process.env.POSTHOG_API_HOST,
- NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
diff --git a/packages/react-native/src/lib/survey/action.ts b/packages/react-native/src/lib/survey/action.ts
index 1e3c6b3578..cb5a4bf325 100644
--- a/packages/react-native/src/lib/survey/action.ts
+++ b/packages/react-native/src/lib/survey/action.ts
@@ -103,6 +103,9 @@ export const track = async (
return trackAction(actionClass.name, code);
} catch (error) {
+ const logger = Logger.getInstance();
+ logger.error(`Error tracking action ${error as string}`);
+
return err({
code: "error",
message: "Error tracking action",
diff --git a/sonar-project.properties b/sonar-project.properties
index a77ad428ae..3e6fde6dd1 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
sonar.sourceEncoding=UTF-8
# Coverage
-sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/**,scripts/merge-client-endpoints.ts
-sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts,scripts/merge-client-endpoints.ts
+sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/**, **/instrumentation.ts, scripts/merge-client-endpoints.ts
+sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts, **/instrumentation.ts, scripts/merge-client-endpoints.ts
\ No newline at end of file
diff --git a/turbo.json b/turbo.json
index a3339cfc5e..edf8e38705 100644
--- a/turbo.json
+++ b/turbo.json
@@ -159,7 +159,6 @@
"NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID",
"NEXT_PUBLIC_FORMBRICKS_PMF_FORM_ID",
"NEXT_PUBLIC_FORMBRICKS_URL",
- "NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_FORMBRICKS_COM_API_HOST",
"NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID",
"NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID",