diff --git a/.env.example b/.env.example
index 664abd3bf7..b3ed82c802 100644
--- a/.env.example
+++ b/.env.example
@@ -210,6 +210,8 @@ UNKEY_ROOT_KEY=
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
# It's used automatically by Sentry during the build for authentication when uploading source maps.
# SENTRY_AUTH_TOKEN=
+# The SENTRY_ENVIRONMENT is the environment which the error will belong to in the Sentry dashboard
+# SENTRY_ENVIRONMENT=
# Configure the minimum role for user management from UI(owner, manager, disabled)
# USER_MANAGEMENT_MINIMUM_ROLE="manager"
diff --git a/.github/actions/upload-sentry-sourcemaps/action.yml b/.github/actions/upload-sentry-sourcemaps/action.yml
new file mode 100644
index 0000000000..e8510aa2f2
--- /dev/null
+++ b/.github/actions/upload-sentry-sourcemaps/action.yml
@@ -0,0 +1,121 @@
+name: 'Upload Sentry Sourcemaps'
+description: 'Extract sourcemaps from Docker image and upload to Sentry'
+
+inputs:
+ docker_image:
+ description: 'Docker image to extract sourcemaps from'
+ required: true
+ release_version:
+ description: 'Sentry release version (e.g., v1.2.3)'
+ required: true
+ sentry_auth_token:
+ description: 'Sentry authentication token'
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Validate Sentry auth token
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "๐ Validating Sentry authentication token..."
+
+ # Assign token to local variable for secure handling
+ SENTRY_TOKEN="${{ inputs.sentry_auth_token }}"
+
+ # Test the token by making a simple API call to Sentry
+ response=$(curl -s -w "%{http_code}" -o /tmp/sentry_response.json \
+ -H "Authorization: Bearer $SENTRY_TOKEN" \
+ "https://sentry.io/api/0/organizations/formbricks/")
+
+ http_code=$(echo "$response" | tail -n1)
+
+ if [ "$http_code" != "200" ]; then
+ echo "โ Error: Invalid Sentry auth token (HTTP $http_code)"
+ echo "Please check your SENTRY_AUTH_TOKEN is correct and has the necessary permissions."
+ if [ -f /tmp/sentry_response.json ]; then
+ echo "Response body:"
+ cat /tmp/sentry_response.json
+ fi
+ exit 1
+ fi
+
+ echo "โ
Sentry auth token validated successfully"
+
+ # Clean up temp file
+ rm -f /tmp/sentry_response.json
+
+ - name: Extract sourcemaps from Docker image
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "๐ฆ Extracting sourcemaps from Docker image: ${{ inputs.docker_image }}"
+
+ # Create temporary container from the image and capture its ID
+ echo "Creating temporary container..."
+ CONTAINER_ID=$(docker create "${{ inputs.docker_image }}")
+ echo "Container created with ID: $CONTAINER_ID"
+
+ # Set up cleanup function to ensure container is removed on script exit
+ cleanup_container() {
+ # Capture the current exit code to preserve it
+ local original_exit_code=$?
+
+ echo "๐งน Cleaning up Docker container..."
+
+ # Remove the container if it exists (ignore errors if already removed)
+ if [ -n "$CONTAINER_ID" ]; then
+ docker rm -f "$CONTAINER_ID" 2>/dev/null || true
+ echo "Container $CONTAINER_ID removed"
+ fi
+
+ # Exit with the original exit code to preserve script success/failure status
+ exit $original_exit_code
+ }
+
+ # Register cleanup function to run on script exit (success or failure)
+ trap cleanup_container EXIT
+
+ # Extract .next directory containing sourcemaps
+ docker cp "$CONTAINER_ID:/home/nextjs/apps/web/.next" ./extracted-next
+
+ # Verify sourcemaps exist
+ if [ ! -d "./extracted-next/static/chunks" ]; then
+ echo "โ Error: .next/static/chunks directory not found in Docker image"
+ echo "Expected structure: /home/nextjs/apps/web/.next/static/chunks/"
+ exit 1
+ fi
+
+ sourcemap_count=$(find ./extracted-next/static/chunks -name "*.map" | wc -l)
+ echo "โ
Found $sourcemap_count sourcemap files"
+
+ if [ "$sourcemap_count" -eq 0 ]; then
+ echo "โ Error: No sourcemap files found. Check that productionBrowserSourceMaps is enabled."
+ exit 1
+ fi
+
+ - name: Create Sentry release and upload sourcemaps
+ uses: getsentry/action-release@v3
+ env:
+ SENTRY_AUTH_TOKEN: ${{ inputs.sentry_auth_token }}
+ SENTRY_ORG: formbricks
+ SENTRY_PROJECT: formbricks-cloud
+ with:
+ environment: production
+ version: ${{ inputs.release_version }}
+ sourcemaps: './extracted-next/'
+
+ - name: Clean up extracted files
+ shell: bash
+ if: always()
+ run: |
+ set -euo pipefail
+ # Clean up extracted files
+ rm -rf ./extracted-next
+ echo "๐งน Cleaned up extracted files"
diff --git a/.github/workflows/formbricks-release.yml b/.github/workflows/formbricks-release.yml
index 68f45a88b5..6df33e7dd8 100644
--- a/.github/workflows/formbricks-release.yml
+++ b/.github/workflows/formbricks-release.yml
@@ -32,3 +32,25 @@ jobs:
with:
VERSION: v${{ needs.docker-build.outputs.VERSION }}
ENVIRONMENT: "prod"
+
+ upload-sentry-sourcemaps:
+ name: Upload Sentry Sourcemaps
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ needs:
+ - docker-build
+ - deploy-formbricks-cloud
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.2.2
+ with:
+ fetch-depth: 0
+
+ - name: Upload Sentry Sourcemaps
+ uses: ./.github/actions/upload-sentry-sourcemaps
+ continue-on-error: true
+ with:
+ docker_image: ghcr.io/formbricks/formbricks:v${{ needs.docker-build.outputs.VERSION }}
+ release_version: v${{ needs.docker-build.outputs.VERSION }}
+ sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
diff --git a/.github/workflows/upload-sentry-sourcemaps.yml b/.github/workflows/upload-sentry-sourcemaps.yml
new file mode 100644
index 0000000000..7af92ebc10
--- /dev/null
+++ b/.github/workflows/upload-sentry-sourcemaps.yml
@@ -0,0 +1,46 @@
+name: Upload Sentry Sourcemaps (Manual)
+
+on:
+ workflow_dispatch:
+ inputs:
+ docker_image:
+ description: "Docker image to extract sourcemaps from"
+ required: true
+ type: string
+ release_version:
+ description: "Release version (e.g., v1.2.3)"
+ required: true
+ type: string
+ tag_version:
+ description: "Docker image tag (leave empty to use release_version)"
+ required: false
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ upload-sourcemaps:
+ name: Upload Sourcemaps to Sentry
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.2.2
+ with:
+ fetch-depth: 0
+
+ - name: Set Docker Image
+ run: |
+ if [ -n "${{ inputs.tag_version }}" ]; then
+ echo "DOCKER_IMAGE=${{ inputs.docker_image }}:${{ inputs.tag_version }}" >> $GITHUB_ENV
+ else
+ echo "DOCKER_IMAGE=${{ inputs.docker_image }}:${{ inputs.release_version }}" >> $GITHUB_ENV
+ fi
+
+ - name: Upload Sourcemaps to Sentry
+ uses: ./.github/actions/upload-sentry-sourcemaps
+ with:
+ docker_image: ${{ env.DOCKER_IMAGE }}
+ release_version: ${{ inputs.release_version }}
+ sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
\ No newline at end of file
diff --git a/apps/web/app/layout.test.tsx b/apps/web/app/layout.test.tsx
index 1cad3293ea..4c3f4031d5 100644
--- a/apps/web/app/layout.test.tsx
+++ b/apps/web/app/layout.test.tsx
@@ -31,6 +31,8 @@ vi.mock("@/lib/constants", () => ({
WEBAPP_URL: "test-webapp-url",
IS_PRODUCTION: false,
SENTRY_DSN: "mock-sentry-dsn",
+ SENTRY_RELEASE: "mock-sentry-release",
+ SENTRY_ENVIRONMENT: "mock-sentry-environment",
}));
vi.mock("@/tolgee/language", () => ({
@@ -59,9 +61,18 @@ vi.mock("@/tolgee/client", () => ({
}));
vi.mock("@/app/sentry/SentryProvider", () => ({
- SentryProvider: ({ children, sentryDsn }: { children: React.ReactNode; sentryDsn?: string }) => (
+ SentryProvider: ({
+ children,
+ sentryDsn,
+ sentryRelease,
+ }: {
+ children: React.ReactNode;
+ sentryDsn?: string;
+ sentryRelease?: string;
+ }) => (
SentryProvider: {sentryDsn}
+ {sentryRelease && ` - Release: ${sentryRelease}`}
{children}
),
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index ee7b027e7c..eb6eaef0aa 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,5 +1,5 @@
import { SentryProvider } from "@/app/sentry/SentryProvider";
-import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants";
+import { IS_PRODUCTION, SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "@/lib/constants";
import { TolgeeNextProvider } from "@/tolgee/client";
import { getLocale } from "@/tolgee/language";
import { getTolgee } from "@/tolgee/server";
@@ -25,7 +25,11 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
return (
-
+
{children}
diff --git a/apps/web/app/sentry/SentryProvider.test.tsx b/apps/web/app/sentry/SentryProvider.test.tsx
index 1756efe45a..6be78aeab0 100644
--- a/apps/web/app/sentry/SentryProvider.test.tsx
+++ b/apps/web/app/sentry/SentryProvider.test.tsx
@@ -48,6 +48,24 @@ describe("SentryProvider", () => {
);
});
+ test("calls Sentry.init with sentryRelease when provided", () => {
+ const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
+ const testRelease = "v1.2.3";
+
+ render(
+
+ Test Content
+
+ );
+
+ expect(initSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ dsn: sentryDsn,
+ release: testRelease,
+ })
+ );
+ });
+
test("does not call Sentry.init when sentryDsn is not provided", () => {
const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined);
diff --git a/apps/web/app/sentry/SentryProvider.tsx b/apps/web/app/sentry/SentryProvider.tsx
index d67b399135..da65b2cbf0 100644
--- a/apps/web/app/sentry/SentryProvider.tsx
+++ b/apps/web/app/sentry/SentryProvider.tsx
@@ -6,14 +6,24 @@ import { useEffect } from "react";
interface SentryProviderProps {
children: React.ReactNode;
sentryDsn?: string;
+ sentryRelease?: string;
+ sentryEnvironment?: string;
isEnabled?: boolean;
}
-export const SentryProvider = ({ children, sentryDsn, isEnabled }: SentryProviderProps) => {
+export const SentryProvider = ({
+ children,
+ sentryDsn,
+ sentryRelease,
+ sentryEnvironment,
+ isEnabled,
+}: SentryProviderProps) => {
useEffect(() => {
if (sentryDsn && isEnabled) {
Sentry.init({
dsn: sentryDsn,
+ release: sentryRelease,
+ environment: sentryEnvironment,
// No tracing while Sentry doesn't update to telemetry 2.0.0 - https://github.com/getsentry/sentry-javascript/issues/15737
tracesSampleRate: 0,
diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts
index db50e9f8da..f877709ba6 100644
--- a/apps/web/lib/constants.ts
+++ b/apps/web/lib/constants.ts
@@ -273,6 +273,24 @@ export const RECAPTCHA_SITE_KEY = env.RECAPTCHA_SITE_KEY;
export const RECAPTCHA_SECRET_KEY = env.RECAPTCHA_SECRET_KEY;
export const IS_RECAPTCHA_CONFIGURED = Boolean(RECAPTCHA_SITE_KEY && RECAPTCHA_SECRET_KEY);
+// Use the app version for Sentry release (updated during build in production)
+// Fallback to environment variable if package.json is not accessible
+export const SENTRY_RELEASE = (() => {
+ if (process.env.NODE_ENV !== "production") {
+ return undefined;
+ }
+
+ // Try to read from package.json with proper error handling
+ try {
+ const pkg = require("../package.json");
+ return pkg.version === "0.0.0" ? undefined : `v${pkg.version}`;
+ } catch {
+ // If package.json can't be read (e.g., in some deployment scenarios),
+ // return undefined and let Sentry work without release tracking
+ return undefined;
+ }
+})();
+export const SENTRY_ENVIRONMENT = env.SENTRY_ENVIRONMENT;
export const SENTRY_DSN = env.SENTRY_DSN;
export const PROMETHEUS_ENABLED = env.PROMETHEUS_ENABLED === "1";
diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts
index 8efb312673..3c39603ab4 100644
--- a/apps/web/lib/env.ts
+++ b/apps/web/lib/env.ts
@@ -127,6 +127,7 @@ export const env = createEnv({
.string()
.transform((val) => parseInt(val))
.optional(),
+ SENTRY_ENVIRONMENT: z.string().optional(),
},
/*
@@ -225,5 +226,6 @@ export const env = createEnv({
AUDIT_LOG_ENABLED: process.env.AUDIT_LOG_ENABLED,
AUDIT_LOG_GET_USER_IP: process.env.AUDIT_LOG_GET_USER_IP,
SESSION_MAX_AGE: process.env.SESSION_MAX_AGE,
+ SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT,
},
});
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index 88ba2efb5d..7821e62b4b 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -20,7 +20,7 @@ const nextConfig = {
cacheMaxMemorySize: 0, // disable default in-memory caching
output: "standalone",
poweredByHeader: false,
- productionBrowserSourceMaps: false,
+ productionBrowserSourceMaps: true,
serverExternalPackages: ["@aws-sdk", "@opentelemetry/instrumentation", "pino", "pino-pretty"],
outputFileTracingIncludes: {
"/api/auth/**/*": ["../../node_modules/jose/**/*"],
@@ -425,6 +425,7 @@ const sentryOptions = {
org: "formbricks",
project: "formbricks-cloud",
+ environment: process.env.SENTRY_ENVIRONMENT,
// Only print logs for uploading source maps in CI
silent: true,
@@ -434,11 +435,23 @@ const sentryOptions = {
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
+
+ // Disable automatic release management
+ automaticVercelMonitors: false,
+ autoUploadSourceMaps: false,
+ hideSourceMaps: false,
+
+ // Don't automatically create releases - we handle this in GitHub Actions
+ release: {
+ create: false,
+ deploy: false,
+ setCommits: false,
+ },
};
const exportConfig =
- process.env.SENTRY_DSN && process.env.NODE_ENV === "production"
- ? withSentryConfig(nextConfig, sentryOptions)
- : nextConfig;
+ (process.env.SENTRY_DSN && process.env.NODE_ENV === "production")
+ ? withSentryConfig(nextConfig, sentryOptions) :
+ nextConfig;
export default exportConfig;
diff --git a/apps/web/sentry.edge.config.ts b/apps/web/sentry.edge.config.ts
index 576498b3e1..fabe1ea681 100644
--- a/apps/web/sentry.edge.config.ts
+++ b/apps/web/sentry.edge.config.ts
@@ -2,7 +2,7 @@
// The config you add here will be used whenever one of the edge features is loaded.
// 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 { SENTRY_DSN } from "@/lib/constants";
+import { SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "@/lib/constants";
import * as Sentry from "@sentry/nextjs";
import { logger } from "@formbricks/logger";
@@ -11,6 +11,8 @@ if (SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN,
+ release: SENTRY_RELEASE,
+ environment: SENTRY_ENVIRONMENT,
// No tracing while Sentry doesn't update to telemetry 2.0.0 - https://github.com/getsentry/sentry-javascript/issues/15737
tracesSampleRate: 0,
diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts
index c91683ff0a..321a9c594a 100644
--- a/apps/web/sentry.server.config.ts
+++ b/apps/web/sentry.server.config.ts
@@ -1,7 +1,7 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
-import { SENTRY_DSN } from "@/lib/constants";
+import { SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "@/lib/constants";
import * as Sentry from "@sentry/nextjs";
import { logger } from "@formbricks/logger";
@@ -10,6 +10,8 @@ if (SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN,
+ release: SENTRY_RELEASE,
+ environment: SENTRY_ENVIRONMENT,
// No tracing while Sentry doesn't update to telemetry 2.0.0 - https://github.com/getsentry/sentry-javascript/issues/15737
tracesSampleRate: 0,
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index b73224cd3d..8e32a42f9c 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -82,6 +82,8 @@ x-environment: &environment
# SENTRY_DSN:
# It's used for authentication when uploading source maps to Sentry, to make errors more readable.
# SENTRY_AUTH_TOKEN:
+ # The SENTRY_ENVIRONMENT is used to identify the environment in Sentry.
+ # SENTRY_ENVIRONMENT:
################################################### OPTIONAL (STORAGE) ###################################################
diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx
index 0308fe3ebf..0e4c227431 100644
--- a/docs/self-hosting/configuration/environment-variables.mdx
+++ b/docs/self-hosting/configuration/environment-variables.mdx
@@ -54,7 +54,7 @@ These variables are present inside your machine's docker-compose file. Restart t
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | |
-| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
+| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
@@ -68,6 +68,7 @@ These variables are present inside your machine's docker-compose file. Restart t
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
| DEFAULT_TEAM_ID | Default team ID for new users. | optional | |
| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional | |
+| SENTRY_ENVIRONMENT | Set this to identify the environment in Sentry | optional | |
| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional | |
| SESSION_MAX_AGE | Configure the maximum age for the session in seconds. | optional | 86400 (24 hours) |
| USER_MANAGEMENT_MINIMUM_ROLE | Set this to control which roles can access user management features. Accepted values: "owner", "manager", "disabled" | optional | manager |
diff --git a/turbo.json b/turbo.json
index b0b923df36..d4abde199a 100644
--- a/turbo.json
+++ b/turbo.json
@@ -146,6 +146,7 @@
"SAML_DATABASE_URL",
"SESSION_MAX_AGE",
"SENTRY_DSN",
+ "SENTRY_ENVIRONMENT",
"SLACK_CLIENT_ID",
"SLACK_CLIENT_SECRET",
"SMTP_HOST",