chore: Remove old telemetry & usage tracking (#6844)

Co-authored-by: Matti Nannt <matti@formbricks.com>
This commit is contained in:
Johannes
2025-11-25 04:57:43 -08:00
committed by GitHub
parent f07092595f
commit aab6798b29
62 changed files with 36 additions and 878 deletions

View File

@@ -1,13 +1,8 @@
---
description: >
This rule provides comprehensive knowledge about the Formbricks database structure, relationships,
and data patterns. It should be used **only when the agent explicitly requests database schema-level
details** to support tasks such as: writing/debugging Prisma queries, designing/reviewing data models,
investigating multi-tenancy behavior, creating API endpoints, or understanding data relationships.
globs: []
alwaysApply: agent-requested
globs: schema.prisma
alwaysApply: false
---
# Formbricks Database Schema Reference
This rule provides a reference to the Formbricks database structure. For the most up-to-date and complete schema definitions, please refer to the schema.prisma file directly.

View File

@@ -1,5 +0,0 @@
---
description:
globs:
alwaysApply: false
---

View File

@@ -1,5 +0,0 @@
---
description:
globs:
alwaysApply: false
---

View File

@@ -17,7 +17,6 @@ on:
workflow_dispatch:
env:
TELEMETRY_DISABLED: 1
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

View File

@@ -1,8 +1,6 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { AuthorizationError } from "@formbricks/types/errors";
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
@@ -40,14 +38,6 @@ const ProjectOnboardingLayout = async (props) => {
return (
<div className="flex-1 bg-slate-50">
<PosthogIdentify
session={session}
user={user}
organizationId={organization.id}
organizationName={organization.name}
organizationBilling={organization.billing}
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
/>
<ToasterClient />
{children}
</div>

View File

@@ -1,14 +1,13 @@
import { redirect } from "next/navigation";
import { getEnvironment } from "@/lib/environment/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
const SurveyEditorEnvironmentLayout = async (props) => {
const params = await props.params;
const { children } = props;
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
const { t, session, user } = await environmentIdLayoutChecks(params.environmentId);
if (!session) {
return redirect(`/auth/login`);
@@ -25,15 +24,9 @@ const SurveyEditorEnvironmentLayout = async (props) => {
}
return (
<EnvironmentIdBaseLayout
environmentId={params.environmentId}
session={session}
user={user}
organization={organization}>
<div className="flex h-screen flex-col">
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
</div>
</EnvironmentIdBaseLayout>
<div className="flex h-screen flex-col">
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
</div>
);
};

View File

@@ -1,61 +0,0 @@
"use client";
import type { Session } from "next-auth";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
import { TOrganizationBilling } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
interface PosthogIdentifyProps {
session: Session;
user: TUser;
environmentId?: string;
organizationId?: string;
organizationName?: string;
organizationBilling?: TOrganizationBilling;
isPosthogEnabled: boolean;
}
export const PosthogIdentify = ({
session,
user,
environmentId,
organizationId,
organizationName,
organizationBilling,
isPosthogEnabled,
}: PosthogIdentifyProps) => {
const posthog = usePostHog();
useEffect(() => {
if (isPosthogEnabled && session.user && posthog) {
posthog.identify(session.user.id, {
name: user.name,
email: user.email,
});
if (environmentId) {
posthog.group("environment", environmentId, { name: environmentId });
}
if (organizationId) {
posthog.group("organization", organizationId, {
name: organizationName,
plan: organizationBilling?.plan,
responseLimit: organizationBilling?.limits.monthly.responses,
miuLimit: organizationBilling?.limits.monthly.miu,
});
}
}
}, [
posthog,
session.user,
environmentId,
organizationId,
organizationName,
organizationBilling,
user.name,
user.email,
isPosthogEnabled,
]);
return null;
};

View File

@@ -4,7 +4,6 @@ import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/comp
import { EnvironmentContextWrapper } from "@/app/(app)/environments/[environmentId]/context/environment-context";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getEnvironmentLayoutData } from "@/modules/environments/lib/utils";
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
const EnvLayout = async (props: {
@@ -24,11 +23,7 @@ const EnvLayout = async (props: {
const layoutData = await getEnvironmentLayoutData(params.environmentId, session.user.id);
return (
<EnvironmentIdBaseLayout
environmentId={params.environmentId}
session={layoutData.session}
user={layoutData.user}
organization={layoutData.organization}>
<>
<EnvironmentStorageHandler environmentId={params.environmentId} />
<EnvironmentContextWrapper
environment={layoutData.environment}
@@ -36,7 +31,7 @@ const EnvLayout = async (props: {
organization={layoutData.organization}>
<EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>
</EnvironmentContextWrapper>
</EnvironmentIdBaseLayout>
</>
);
};

View File

@@ -1,5 +1,6 @@
import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
@@ -25,7 +26,7 @@ export const generateMetadata = async (props: Props): Promise<Metadata> => {
};
const SurveyLayout = async ({ children }) => {
return <>{children}</>;
return <ResponseFilterProvider>{children}</ResponseFilterProvider>;
};
export default SurveyLayout;

View File

@@ -8,8 +8,8 @@ import { TResponseWithQuotas } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TTag } from "@formbricks/types/tags";
import { TUser, TUserLocale } from "@formbricks/types/user";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getResponsesAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import { ResponseDataView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView";
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
import { getFormattedFilters } from "@/app/lib/surveys/surveys";

View File

@@ -12,11 +12,11 @@ import {
} from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { TUserLocale } from "@formbricks/types/user";
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import {
SelectedFilterValue,
useResponseFilter,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary";
import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
import { ConsentSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";

View File

@@ -5,8 +5,8 @@ import { useEffect, useMemo, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getSurveySummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";

View File

@@ -25,7 +25,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import {
DateRange,
useResponseFilter,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { downloadResponsesFile } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/utils";
import { getFormattedFilters, getTodayDate } from "@/app/lib/surveys/surveys";

View File

@@ -9,7 +9,7 @@ import {
SelectedFilterValue,
TResponseStatus,
useResponseFilter,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";

View File

@@ -1,12 +1,9 @@
import { getServerSession } from "next-auth";
import { Suspense } from "react";
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
import { IS_POSTHOG_CONFIGURED, POSTHOG_API_HOST, POSTHOG_API_KEY } from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { ClientLogout } from "@/modules/ui/components/client-logout";
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
const AppLayout = async ({ children }) => {
@@ -21,20 +18,9 @@ const AppLayout = async ({ children }) => {
return (
<>
<NoMobileOverlay />
<Suspense>
<PostHogPageview
posthogEnabled={IS_POSTHOG_CONFIGURED}
postHogApiHost={POSTHOG_API_HOST}
postHogApiKey={POSTHOG_API_KEY}
/>
</Suspense>
<PHProvider posthogEnabled={IS_POSTHOG_CONFIGURED}>
<>
<IntercomClientWrapper user={user} />
<ToasterClient />
{children}
</>
</PHProvider>
<IntercomClientWrapper user={user} />
<ToasterClient />
{children}
</>
);
};

View File

@@ -1,34 +0,0 @@
import { Organization } from "@prisma/client";
import { logger } from "@formbricks/logger";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
export const handleBillingLimitsCheck = async (
environmentId: string,
organizationId: string,
organizationBilling: Organization["billing"]
): Promise<void> => {
if (!IS_FORMBRICKS_CLOUD) return;
const responsesCount = await getMonthlyOrganizationResponseCount(organizationId);
const responsesLimit = organizationBilling.limits.monthly.responses;
if (responsesLimit && responsesCount >= responsesLimit) {
try {
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
plan: organizationBilling.plan,
limits: {
projects: null,
monthly: {
responses: responsesLimit,
miu: null,
},
},
});
} catch (err) {
// Log error but do not throw
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
};

View File

@@ -18,10 +18,6 @@ import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import {
capturePosthogEnvironmentEvent,
sendPlanLimitsReachedEventToPosthogWeekly,
} from "@/lib/posthogServer";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
@@ -58,20 +54,6 @@ const checkResponseLimit = async (environmentId: string): Promise<boolean> => {
const monthlyResponseLimit = organization.billing.limits.monthly.responses;
const isLimitReached = monthlyResponseLimit !== null && currentResponseCount >= monthlyResponseLimit;
if (isLimitReached) {
try {
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
plan: organization.billing.plan,
limits: {
projects: null,
monthly: { responses: monthlyResponseLimit, miu: null },
},
});
} catch (error) {
logger.error({ error }, `Error sending plan limits reached event to Posthog`);
}
}
return isLimitReached;
};
@@ -111,10 +93,7 @@ export const GET = withV1ApiWrapper({
}
if (!environment.appSetupCompleted) {
await Promise.all([
updateEnvironment(environment.id, { appSetupCompleted: true }),
capturePosthogEnvironmentEvent(environmentId, "app setup completed"),
]);
await updateEnvironment(environment.id, { appSetupCompleted: true });
}
// check organization subscriptions and response limits

View File

@@ -5,7 +5,6 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { createDisplay } from "./lib/display";
@@ -59,7 +58,6 @@ export const POST = withV1ApiWrapper({
try {
const response = await createDisplay(inputValidation.data);
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
return {
response: responses.successResponse(response, true),
};

View File

@@ -8,16 +8,11 @@ import { TOrganization } from "@formbricks/types/organizations";
import { TSurvey } from "@formbricks/types/surveys/types";
import { cache } from "@/lib/cache";
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
import {
capturePosthogEnvironmentEvent,
sendPlanLimitsReachedEventToPosthogWeekly,
} from "@/lib/posthogServer";
import { EnvironmentStateData, getEnvironmentStateData } from "./data";
import { getEnvironmentState } from "./environmentState";
// Mock dependencies
vi.mock("@/lib/organization/service");
vi.mock("@/lib/posthogServer");
vi.mock("@/lib/cache", () => ({
cache: {
withCache: vi.fn(),
@@ -43,7 +38,6 @@ vi.mock("@/lib/constants", () => ({
RECAPTCHA_SECRET_KEY: "mock_recaptcha_secret_key",
IS_RECAPTCHA_CONFIGURED: true,
IS_PRODUCTION: true,
IS_POSTHOG_CONFIGURED: false,
ENTERPRISE_LICENSE_KEY: "mock_enterprise_license_key",
}));
@@ -188,9 +182,7 @@ describe("getEnvironmentState", () => {
expect(result.data).toEqual(expectedData);
expect(getEnvironmentStateData).toHaveBeenCalledWith(environmentId);
expect(prisma.environment.update).not.toHaveBeenCalled();
expect(capturePosthogEnvironmentEvent).not.toHaveBeenCalled();
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(mockOrganization.id);
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
});
test("should throw ResourceNotFoundError if environment not found", async () => {
@@ -226,7 +218,6 @@ describe("getEnvironmentState", () => {
where: { id: environmentId },
data: { appSetupCompleted: true },
});
expect(capturePosthogEnvironmentEvent).toHaveBeenCalledWith(environmentId, "app setup completed");
expect(result.data).toBeDefined();
});
@@ -237,16 +228,6 @@ describe("getEnvironmentState", () => {
expect(result.data.surveys).toEqual([]);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(mockOrganization.id);
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalledWith(environmentId, {
plan: mockOrganization.billing.plan,
limits: {
projects: null,
monthly: {
miu: null,
responses: mockOrganization.billing.limits.monthly.responses,
},
},
});
});
test("should return surveys if monthly response limit not reached (Cloud)", async () => {
@@ -256,21 +237,6 @@ describe("getEnvironmentState", () => {
expect(result.data.surveys).toEqual(mockSurveys);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(mockOrganization.id);
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
});
test("should handle error when sending Posthog limit reached event", async () => {
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
const posthogError = new Error("Posthog failed");
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
const result = await getEnvironmentState(environmentId);
expect(result.data.surveys).toEqual([]);
expect(logger.error).toHaveBeenCalledWith(
posthogError,
"Error sending plan limits reached event to Posthog"
);
});
test("should include recaptchaSiteKey if recaptcha variables are set", async () => {
@@ -313,7 +279,6 @@ describe("getEnvironmentState", () => {
// Should return surveys even with high count since limit is null (unlimited)
expect(result.data.surveys).toEqual(mockSurveys);
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
});
test("should propagate database update errors", async () => {
@@ -331,21 +296,6 @@ describe("getEnvironmentState", () => {
await expect(getEnvironmentState(environmentId)).rejects.toThrow("Database error");
});
test("should propagate PostHog event capture errors", async () => {
const incompleteEnvironmentData = {
...mockEnvironmentStateData,
environment: {
...mockEnvironmentStateData.environment,
appSetupCompleted: false,
},
};
vi.mocked(getEnvironmentStateData).mockResolvedValue(incompleteEnvironmentData);
vi.mocked(capturePosthogEnvironmentEvent).mockRejectedValue(new Error("PostHog error"));
// Should throw error since Promise.all will fail if PostHog event capture fails
await expect(getEnvironmentState(environmentId)).rejects.toThrow("PostHog error");
});
test("should include recaptchaSiteKey when IS_RECAPTCHA_CONFIGURED is true", async () => {
const result = await getEnvironmentState(environmentId);

View File

@@ -1,15 +1,10 @@
import "server-only";
import { createCacheKey } from "@formbricks/cache";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { TJsEnvironmentState } from "@formbricks/types/js";
import { cache } from "@/lib/cache";
import { IS_FORMBRICKS_CLOUD, IS_RECAPTCHA_CONFIGURED, RECAPTCHA_SITE_KEY } from "@/lib/constants";
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
import {
capturePosthogEnvironmentEvent,
sendPlanLimitsReachedEventToPosthogWeekly,
} from "@/lib/posthogServer";
import { getEnvironmentStateData } from "./data";
/**
@@ -33,13 +28,10 @@ export const getEnvironmentState = async (
// Handle app setup completion update if needed
// This is a one-time setup flag that can tolerate TTL-based cache expiration
if (!environment.appSetupCompleted) {
await Promise.all([
prisma.environment.update({
where: { id: environmentId },
data: { appSetupCompleted: true },
}),
capturePosthogEnvironmentEvent(environmentId, "app setup completed"),
]);
await prisma.environment.update({
where: { id: environmentId },
data: { appSetupCompleted: true },
});
}
// Check monthly response limits for Formbricks Cloud
@@ -49,24 +41,6 @@ export const getEnvironmentState = async (
const currentResponseCount = await getMonthlyOrganizationResponseCount(organization.id);
isMonthlyResponsesLimitReached =
monthlyResponseLimit !== null && currentResponseCount >= monthlyResponseLimit;
// Send plan limits event if needed
if (isMonthlyResponsesLimitReached) {
try {
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
plan: organization.billing.plan,
limits: {
projects: null,
monthly: {
miu: null,
responses: organization.billing.limits.monthly.responses,
},
},
});
} catch (err) {
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
}
// Build the response data

View File

@@ -1,15 +1,10 @@
import { Prisma } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurveyQuota } from "@formbricks/types/quota";
import { TResponseInput } from "@formbricks/types/responses";
import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
import { createResponse, createResponseWithQuotaEvaluation } from "./response";
@@ -24,22 +19,13 @@ vi.mock("@/lib/constants", () => ({
}));
vi.mock("@/lib/organization/service", () => ({
getMonthlyOrganizationResponseCount: vi.fn(),
getOrganizationByEnvironmentId: vi.fn(),
}));
vi.mock("@/lib/posthogServer", () => ({
sendPlanLimitsReachedEventToPosthogWeekly: vi.fn(),
}));
vi.mock("@/lib/response/utils", () => ({
calculateTtcTotal: vi.fn((ttc) => ttc),
}));
vi.mock("@/lib/telemetry", () => ({
captureTelemetry: vi.fn(),
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
@@ -138,35 +124,6 @@ describe("createResponse", () => {
);
});
test("should check response limits if IS_FORMBRICKS_CLOUD is true", async () => {
mockIsFormbricksCloud = true;
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
await createResponse(mockResponseInput, prisma);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
});
test("should send limit reached event if IS_FORMBRICKS_CLOUD is true and limit reached", async () => {
mockIsFormbricksCloud = true;
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
await createResponse(mockResponseInput, prisma);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalledWith(environmentId, {
plan: "free",
limits: {
projects: null,
monthly: {
responses: 100,
miu: null,
},
},
});
});
test("should throw ResourceNotFoundError if organization not found", async () => {
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
await expect(createResponse(mockResponseInput, prisma)).rejects.toThrow(ResourceNotFoundError);
@@ -186,20 +143,6 @@ describe("createResponse", () => {
vi.mocked(prisma.response.create).mockRejectedValue(genericError);
await expect(createResponse(mockResponseInput)).rejects.toThrow(genericError);
});
test("should log error but not throw if sendPlanLimitsReachedEventToPosthogWeekly fails", async () => {
mockIsFormbricksCloud = true;
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
const posthogError = new Error("PostHog error");
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
await createResponse(mockResponseInput);
expect(logger.error).toHaveBeenCalledWith(
posthogError,
"Error sending plan limits reached event to Posthog"
);
});
});
describe("createResponseWithQuotaEvaluation", () => {

View File

@@ -6,11 +6,9 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { handleBillingLimitsCheck } from "@/app/api/lib/utils";
import { buildPrismaResponseData } from "@/app/api/v1/lib/utils";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { captureTelemetry } from "@/lib/telemetry";
import { validateInputs } from "@/lib/utils/validate";
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
import { getContactByUserId } from "./contact";
@@ -83,7 +81,6 @@ export const createResponse = async (
tx: Prisma.TransactionClient
): Promise<TResponse> => {
validateInputs([responseInput, ZResponseInput]);
captureTelemetry("response created");
const { environmentId, userId, finished, ttc: initialTtc } = responseInput;
@@ -121,8 +118,6 @@ export const createResponse = async (
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
await handleBillingLimitsCheck(environmentId, organization.id, organization.billing);
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -10,7 +10,6 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { sendToPipeline } from "@/app/lib/pipelines";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getSurvey } from "@/lib/survey/service";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
@@ -172,11 +171,6 @@ export const POST = withV1ApiWrapper({
});
}
await capturePosthogEnvironmentEvent(survey.environmentId, "response created", {
surveyId: responseData.surveyId,
surveyType: survey.type,
});
const quotaObj = createQuotaFullObject(quotaFull);
const responseDataWithQuota = {

View File

@@ -4,11 +4,7 @@ import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse, TResponseInput } from "@formbricks/types/responses";
import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getResponseContact } from "@/lib/response/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { validateInputs } from "@/lib/utils/validate";
@@ -96,9 +92,6 @@ const mockTransformedResponses = [mockResponse, { ...mockResponse, id: "response
// Mock dependencies
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: true,
POSTHOG_API_KEY: "mock-posthog-api-key",
POSTHOG_HOST: "mock-posthog-host",
IS_POSTHOG_CONFIGURED: true,
ENCRYPTION_KEY: "mock-encryption-key",
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
GITHUB_ID: "mock-github-id",
@@ -118,10 +111,8 @@ vi.mock("@/lib/constants", () => ({
SENTRY_DSN: "mock-sentry-dsn",
}));
vi.mock("@/lib/organization/service");
vi.mock("@/lib/posthogServer");
vi.mock("@/lib/response/service");
vi.mock("@/lib/response/utils");
vi.mock("@/lib/telemetry");
vi.mock("@/lib/utils/validate");
vi.mock("@formbricks/database", () => ({
prisma: {
@@ -162,7 +153,6 @@ describe("Response Lib Tests", () => {
vi.mocked(mockTx.response.create).mockResolvedValue({
...mockResponsePrisma,
});
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
const response = await createResponse(mockResponseInputWithUserId, mockTx);
@@ -217,68 +207,6 @@ describe("Response Lib Tests", () => {
await expect(createResponse(mockResponseInput, mockTx)).rejects.toThrow(genericError);
});
describe("Cloud specific tests", () => {
test("should check response limit and send event if limit reached", async () => {
// IS_FORMBRICKS_CLOUD is true by default from the top-level mock
const limit = 100;
const mockOrgWithBilling = {
...mockOrganization,
billing: { limits: { monthly: { responses: limit } } },
} as any;
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrgWithBilling);
vi.mocked(calculateTtcTotal).mockReturnValue({ total: 10 });
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma);
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(limit); // Limit reached
await createResponse(mockResponseInput, mockTx);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalled();
});
test("should check response limit and not send event if limit not reached", async () => {
const limit = 100;
const mockOrgWithBilling = {
...mockOrganization,
billing: { limits: { monthly: { responses: limit } } },
} as any;
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrgWithBilling);
vi.mocked(calculateTtcTotal).mockReturnValue({ total: 10 });
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma);
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(limit - 1); // Limit not reached
await createResponse(mockResponseInput, mockTx);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
});
test("should log error if sendPlanLimitsReachedEventToPosthogWeekly fails", async () => {
const limit = 100;
const mockOrgWithBilling = {
...mockOrganization,
billing: { limits: { monthly: { responses: limit } } },
} as any;
const posthogError = new Error("Posthog error");
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrgWithBilling);
vi.mocked(calculateTtcTotal).mockReturnValue({ total: 10 });
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma);
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(limit); // Limit reached
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
// Expecting successful response creation despite PostHog error
const response = await createResponse(mockResponseInput, mockTx);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith(
posthogError,
"Error sending plan limits reached event to Posthog"
);
expect(response).toEqual(mockResponse); // Should still return the created response
});
});
});
describe("getResponsesByEnvironmentIds", () => {

View File

@@ -8,14 +8,12 @@ import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { handleBillingLimitsCheck } from "@/app/api/lib/utils";
import { buildPrismaResponseData } from "@/app/api/v1/lib/utils";
import { RESPONSES_PER_PAGE } from "@/lib/constants";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getResponseContact } from "@/lib/response/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { getSurvey } from "@/lib/survey/service";
import { captureTelemetry } from "@/lib/telemetry";
import { validateInputs } from "@/lib/utils/validate";
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
import { getContactByUserId } from "./contact";
@@ -93,7 +91,6 @@ export const createResponse = async (
tx?: Prisma.TransactionClient
): Promise<TResponse> => {
validateInputs([responseInput, ZResponseInput]);
captureTelemetry("response created");
const { environmentId, userId, finished, ttc: initialTtc } = responseInput;
@@ -131,8 +128,6 @@ export const createResponse = async (
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
await handleBillingLimitsCheck(environmentId, organization.id, organization.billing);
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -3,7 +3,6 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displays/types/display";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { createDisplay } from "./lib/display";
@@ -49,7 +48,6 @@ export const POST = async (request: Request, context: Context): Promise<Response
try {
const response = await createDisplay(inputValidation.data);
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
return responses.successResponse(response, true);
} catch (error) {
if (error instanceof ResourceNotFoundError) {

View File

@@ -8,13 +8,8 @@ import { TResponseWithQuotaFull, TSurveyQuota } from "@formbricks/types/quota";
import { TResponse } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { captureTelemetry } from "@/lib/telemetry";
import { validateInputs } from "@/lib/utils/validate";
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
import { getContact } from "./contact";
@@ -49,9 +44,7 @@ vi.mock("@/lib/constants", () => ({
}));
vi.mock("@/lib/organization/service");
vi.mock("@/lib/posthogServer");
vi.mock("@/lib/response/utils");
vi.mock("@/lib/telemetry");
vi.mock("@/lib/utils/validate");
vi.mock("@/modules/ee/quotas/lib/evaluation-service");
vi.mock("@formbricks/database", () => ({
@@ -166,9 +159,6 @@ describe("createResponse V2", () => {
...ttc,
_total: Object.values(ttc).reduce((a, b) => a + b, 0),
}));
vi.mocked(captureTelemetry).mockResolvedValue(undefined);
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockResolvedValue(undefined);
vi.mocked(evaluateResponseQuotas).mockResolvedValue({
shouldEndSurvey: false,
quotaFull: null,
@@ -179,32 +169,6 @@ describe("createResponse V2", () => {
mockIsFormbricksCloud = false;
});
test("should check response limits if IS_FORMBRICKS_CLOUD is true", async () => {
mockIsFormbricksCloud = true;
await createResponse(mockResponseInput, mockTx);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
});
test("should send limit reached event if IS_FORMBRICKS_CLOUD is true and limit reached", async () => {
mockIsFormbricksCloud = true;
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
await createResponse(mockResponseInput, mockTx);
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalledWith(environmentId, {
plan: "free",
limits: {
projects: null,
monthly: {
responses: 100,
miu: null,
},
},
});
});
test("should throw ResourceNotFoundError if organization not found", async () => {
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
await expect(createResponse(mockResponseInput, mockTx)).rejects.toThrow(ResourceNotFoundError);
@@ -225,20 +189,6 @@ describe("createResponse V2", () => {
await expect(createResponse(mockResponseInput, mockTx)).rejects.toThrow(genericError);
});
test("should log error but not throw if sendPlanLimitsReachedEventToPosthogWeekly fails", async () => {
mockIsFormbricksCloud = true;
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
const posthogError = new Error("PostHog error");
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
await createResponse(mockResponseInput, mockTx); // Should not throw
expect(logger.error).toHaveBeenCalledWith(
posthogError,
"Error sending plan limits reached event to Posthog"
);
});
test("should correctly map prisma tags to response tags", async () => {
const mockTag: TTag = { id: "tag1", name: "Tag 1", environmentId };
const prismaResponseWithTags = {
@@ -269,7 +219,6 @@ describe("createResponseWithQuotaEvaluation V2", () => {
...ttc,
_total: Object.values(ttc).reduce((a, b) => a + b, 0),
}));
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
vi.mocked(evaluateResponseQuotas).mockResolvedValue({
shouldEndSurvey: false,
quotaFull: null,

View File

@@ -6,12 +6,10 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { handleBillingLimitsCheck } from "@/app/api/lib/utils";
import { responseSelection } from "@/app/api/v1/client/[environmentId]/responses/lib/response";
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { captureTelemetry } from "@/lib/telemetry";
import { validateInputs } from "@/lib/utils/validate";
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
import { getContact } from "./contact";
@@ -91,7 +89,6 @@ export const createResponse = async (
tx?: Prisma.TransactionClient
): Promise<TResponse> => {
validateInputs([responseInput, ZResponseInput]);
captureTelemetry("response created");
const { environmentId, contactId, finished, ttc: initialTtc } = responseInput;
@@ -129,8 +126,6 @@ export const createResponse = async (
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
await handleBillingLimitsCheck(environmentId, organization.id, organization.billing);
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -8,7 +8,6 @@ import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/respons
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getSurvey } from "@/lib/survey/service";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
@@ -148,11 +147,6 @@ export const POST = async (request: Request, context: Context): Promise<Response
});
}
await capturePosthogEnvironmentEvent(environmentId, "response created", {
surveyId: responseData.surveyId,
surveyType: survey.type,
});
const quotaObj = createQuotaFullObject(quotaFull);
const responseDataWithQuota = {

View File

@@ -12,7 +12,7 @@ import { TTag } from "@formbricks/types/tags";
import {
DateRange,
SelectedFilterValue,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { generateQuestionAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";

View File

@@ -12,7 +12,7 @@ import {
DateRange,
FilterValue,
SelectedFilterValue,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import {
OptionsType,
QuestionOption,

View File

@@ -218,10 +218,6 @@ export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY;
export const INTERCOM_APP_ID = env.INTERCOM_APP_ID;
export const IS_INTERCOM_CONFIGURED = Boolean(env.INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
export const POSTHOG_API_KEY = env.POSTHOG_API_KEY;
export const POSTHOG_API_HOST = env.POSTHOG_API_HOST;
export const IS_POSTHOG_CONFIGURED = Boolean(POSTHOG_API_KEY && POSTHOG_API_HOST);
export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;
export const TURNSTILE_SITE_KEY = env.TURNSTILE_SITE_KEY;
export const IS_TURNSTILE_CONFIGURED = Boolean(env.TURNSTILE_SITE_KEY && TURNSTILE_SECRET_KEY);

View File

@@ -59,8 +59,6 @@ export const env = createEnv({
? z.string().optional()
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
POSTHOG_API_HOST: z.string().optional(),
POSTHOG_API_KEY: z.string().optional(),
PRIVACY_URL: z
.string()
.url()
@@ -103,7 +101,6 @@ export const env = createEnv({
}
)
.optional(),
TELEMETRY_DISABLED: z.enum(["1", "0"]).optional(),
TERMS_URL: z
.string()
.url()
@@ -172,8 +169,6 @@ export const env = createEnv({
MAIL_FROM_NAME: process.env.MAIL_FROM_NAME,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
SENTRY_DSN: process.env.SENTRY_DSN,
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY,
POSTHOG_API_HOST: process.env.POSTHOG_API_HOST,
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
@@ -206,7 +201,6 @@ export const env = createEnv({
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
PUBLIC_URL: process.env.PUBLIC_URL,
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED,
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
TURNSTILE_SITE_KEY: process.env.TURNSTILE_SITE_KEY,
RECAPTCHA_SITE_KEY: process.env.RECAPTCHA_SITE_KEY,

View File

@@ -17,7 +17,6 @@ import {
} from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import { getOrganizationsByUserId } from "../organization/service";
import { capturePosthogEnvironmentEvent } from "../posthogServer";
import { getUserProjects } from "../project/service";
import { validateInputs } from "../utils/validate";
@@ -173,10 +172,6 @@ export const createEnvironment = async (
},
});
await capturePosthogEnvironmentEvent(environment.id, "environment created", {
environmentType: environment.type,
});
return environment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -1,56 +0,0 @@
import { PostHog } from "posthog-node";
import { createCacheKey } from "@formbricks/cache";
import { logger } from "@formbricks/logger";
import { TOrganizationBillingPlan, TOrganizationBillingPlanLimits } from "@formbricks/types/organizations";
import { cache } from "@/lib/cache";
import { IS_POSTHOG_CONFIGURED, IS_PRODUCTION, POSTHOG_API_HOST, POSTHOG_API_KEY } from "./constants";
const enabled = IS_PRODUCTION && IS_POSTHOG_CONFIGURED;
export const capturePosthogEnvironmentEvent = async (
environmentId: string,
eventName: string,
properties: any = {}
) => {
if (!enabled || typeof POSTHOG_API_HOST !== "string" || typeof POSTHOG_API_KEY !== "string") {
return;
}
try {
const client = new PostHog(POSTHOG_API_KEY, {
host: POSTHOG_API_HOST,
});
client.capture({
// workaround with a static string as exaplained in PostHog docs: https://posthog.com/docs/product-analytics/group-analytics
distinctId: "environmentEvents",
event: eventName,
groups: { environment: environmentId },
properties,
});
await client.shutdown();
} catch (error) {
logger.error(error, "error sending posthog event");
}
};
export const sendPlanLimitsReachedEventToPosthogWeekly = async (
environmentId: string,
billing: {
plan: TOrganizationBillingPlan;
limits: TOrganizationBillingPlanLimits;
}
) =>
await cache.withCache(
async () => {
try {
await capturePosthogEnvironmentEvent(environmentId, "plan limit reached", {
...billing,
});
return "success";
} catch (error) {
logger.error(error, "error sending plan limits reached event to posthog weekly");
throw error;
}
},
createCacheKey.custom("analytics", environmentId, `plan_limits_${billing.plan}`),
60 * 60 * 24 * 7 * 1000 // 7 days in milliseconds
);

View File

@@ -13,7 +13,6 @@ import {
getOrganizationByEnvironmentId,
subscribeOrganizationMembersToSurveyResponses,
} from "@/lib/organization/service";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { evaluateLogic } from "@/lib/surveyLogic/utils";
import {
mockActionClass,
@@ -44,11 +43,6 @@ vi.mock("@/lib/organization/service", () => ({
subscribeOrganizationMembersToSurveyResponses: vi.fn(),
}));
// Mock posthogServer
vi.mock("@/lib/posthogServer", () => ({
capturePosthogEnvironmentEvent: vi.fn(),
}));
// Mock actionClass service
vi.mock("@/lib/actionClass/service", () => ({
getActionClasses: vi.fn(),
@@ -646,7 +640,6 @@ describe("Tests for createSurvey", () => {
expect(prisma.survey.create).toHaveBeenCalled();
expect(result.name).toEqual(mockSurveyOutput.name);
expect(subscribeOrganizationMembersToSurveyResponses).toHaveBeenCalled();
expect(capturePosthogEnvironmentEvent).toHaveBeenCalled();
});
test("creates a private segment for app surveys", async () => {

View File

@@ -13,7 +13,6 @@ import {
} from "@/lib/organization/service";
import { getActionClasses } from "../actionClass/service";
import { ITEMS_PER_PAGE } from "../constants";
import { capturePosthogEnvironmentEvent } from "../posthogServer";
import { validateInputs } from "../utils/validate";
import { checkForInvalidImagesInQuestions, transformPrismaSurvey } from "./utils";
@@ -673,11 +672,6 @@ export const createSurvey = async (
await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy, organization.id);
}
await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
surveyId: survey.id,
surveyType: survey.type,
});
return transformedSurvey;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -1,38 +0,0 @@
/* We use this telemetry service to better understand how Formbricks is being used
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";
const crypto = require("crypto");
// We are using the hashed CRON_SECRET as the distinct identifier for the instance for telemetry.
// The hash cannot be traced back to the original value or the instance itself.
// This is to ensure that the telemetry data is anonymous but still unique to the instance.
const getTelemetryId = (): string => {
return crypto.createHash("sha256").update(env.CRON_SECRET).digest("hex");
};
export const captureTelemetry = async (eventName: string, properties = {}) => {
if (env.TELEMETRY_DISABLED !== "1" && IS_PRODUCTION) {
try {
await fetch("https://telemetry.formbricks.com/capture/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: "phc_SoIFUJ8b9ufDm0YOnoOxJf6PXyuHpO7N6RztxFdZTy", // NOSONAR // This is a public API key for telemetry and not a secret
event: eventName,
properties: {
distinct_id: getTelemetryId(),
...properties,
},
timestamp: new Date().toISOString(),
}),
});
} catch (error) {
logger.error(error, "error sending telemetry");
}
}
};

View File

@@ -1,13 +1,10 @@
import "server-only";
import { Prisma, Response } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { calculateTtcTotal } from "@/lib/response/utils";
import { captureTelemetry } from "@/lib/telemetry";
import { getContactByUserId } from "@/modules/api/v2/management/responses/lib/contact";
import {
getMonthlyOrganizationResponseCount,
@@ -51,8 +48,6 @@ export const createResponse = async (
responseInput: TResponseInput,
tx?: Prisma.TransactionClient
): Promise<Result<Response, ApiErrorResponseV2>> => {
captureTelemetry("response created");
const {
surveyId,
displayId,
@@ -126,7 +121,6 @@ export const createResponse = async (
if (!billing.ok) {
return err(billing.error as ApiErrorResponseV2);
}
const billingData = billing.data;
const prismaClient = tx ?? prisma;
@@ -140,26 +134,7 @@ export const createResponse = async (
return err(responsesCountResult.error as ApiErrorResponseV2);
}
const responsesCount = responsesCountResult.data;
const responsesLimit = billingData.limits?.monthly.responses;
if (responsesLimit && responsesCount >= responsesLimit) {
try {
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
plan: billingData.plan,
limits: {
projects: null,
monthly: {
responses: responsesLimit,
miu: null,
},
},
});
} catch (err) {
// Log error but do not throw it
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
// Limit check completed
}
return ok(response);

View File

@@ -12,7 +12,6 @@ import {
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { err, ok } from "@formbricks/types/error-handlers";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import {
getMonthlyOrganizationResponseCount,
getOrganizationBilling,
@@ -20,10 +19,6 @@ import {
} from "@/modules/api/v2/management/responses/lib/organization";
import { createResponse, getResponses } from "../response";
vi.mock("@/lib/posthogServer", () => ({
sendPlanLimitsReachedEventToPosthogWeekly: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/modules/api/v2/management/responses/lib/organization", () => ({
getOrganizationIdFromEnvironmentId: vi.fn(),
getOrganizationBilling: vi.fn(),
@@ -150,11 +145,8 @@ describe("Response Lib", () => {
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(ok(100));
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockImplementation(() => Promise.resolve(""));
const result = await createResponse(environmentId, responseInput);
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalled();
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual(response);
@@ -191,10 +183,6 @@ describe("Response Lib", () => {
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(ok(100));
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(
new Error("Error sending plan limits")
);
const result = await createResponse(environmentId, responseInput);
expect(result.ok).toBe(true);
if (result.ok) {

View File

@@ -1,7 +1,6 @@
import { WebhookSource } from "@prisma/client";
import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { captureTelemetry } from "@/lib/telemetry";
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
import { createWebhook, getWebhooks } from "../webhook";
@@ -16,10 +15,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
vi.mock("@/lib/telemetry", () => ({
captureTelemetry: vi.fn(),
}));
describe("getWebhooks", () => {
const environmentId = "env1";
const params = {
@@ -86,7 +81,6 @@ describe("createWebhook", () => {
vi.mocked(prisma.webhook.create).mockResolvedValueOnce(createdWebhook);
const result = await createWebhook(inputWebhook);
expect(captureTelemetry).toHaveBeenCalledWith("webhook_created");
expect(prisma.webhook.create).toHaveBeenCalled();
expect(result.ok).toBe(true);

View File

@@ -1,7 +1,6 @@
import { Prisma, Webhook } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { captureTelemetry } from "@/lib/telemetry";
import { getWebhooksQuery } from "@/modules/api/v2/management/webhooks/lib/utils";
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
@@ -47,8 +46,6 @@ export const getWebhooks = async (
};
export const createWebhook = async (webhook: TWebhookInput): Promise<Result<Webhook, ApiErrorResponseV2>> => {
captureTelemetry("webhook_created");
const { environmentId, name, url, source, triggers, surveyIds } = webhook;
try {

View File

@@ -2,7 +2,6 @@ import { ProjectTeam } from "@prisma/client";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { captureTelemetry } from "@/lib/telemetry";
import { getProjectTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils";
import {
TGetProjectTeamsFilter,
@@ -44,8 +43,6 @@ export const getProjectTeams = async (
export const createProjectTeam = async (
teamInput: TProjectTeamInput
): Promise<Result<ProjectTeam, ApiErrorResponseV2>> => {
captureTelemetry("project team created");
const { teamId, projectId, permission } = teamInput;
try {

View File

@@ -2,7 +2,6 @@ import "server-only";
import { Team } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { captureTelemetry } from "@/lib/telemetry";
import { getTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/utils";
import {
TGetTeamsFilter,
@@ -15,8 +14,6 @@ export const createTeam = async (
teamInput: TTeamInput,
organizationId: string
): Promise<Result<Team, ApiErrorResponseV2>> => {
captureTelemetry("team created");
const { name } = teamInput;
try {

View File

@@ -2,7 +2,6 @@ import { OrganizationRole, Prisma, TeamUserRole } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { TUser } from "@formbricks/database/zod/users";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { captureTelemetry } from "@/lib/telemetry";
import { getUsersQuery } from "@/modules/api/v2/organizations/[organizationId]/users/lib/utils";
import {
TGetUsersFilter,
@@ -73,8 +72,6 @@ export const createUser = async (
userInput: TUserInput,
organizationId
): Promise<Result<TUser, ApiErrorResponseV2>> => {
captureTelemetry("user created");
const { name, email, role, teams, isActive } = userInput;
try {
@@ -150,8 +147,6 @@ export const updateUser = async (
userInput: TUserInputPatch,
organizationId: string
): Promise<Result<TUser, ApiErrorResponseV2>> => {
captureTelemetry("user updated");
const { name, email, role, teams, isActive } = userInput;
let existingTeams: string[] = [];
let newTeams;

View File

@@ -13,7 +13,7 @@ import { ActionClientCtx } from "@/lib/utils/action-client/types/context";
import { createUser, updateUser } from "@/modules/auth/lib/user";
import { deleteInvite, getInvite } from "@/modules/auth/signup/lib/invite";
import { createTeamMembership } from "@/modules/auth/signup/lib/team";
import { captureFailedSignup, verifyTurnstileToken } from "@/modules/auth/signup/lib/utils";
import { verifyTurnstileToken } from "@/modules/auth/signup/lib/utils";
import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
@@ -46,21 +46,15 @@ const ZCreateUserAction = z.object({
),
});
async function verifyTurnstileIfConfigured(
turnstileToken: string | undefined,
email: string,
name: string
): Promise<void> {
async function verifyTurnstileIfConfigured(turnstileToken: string | undefined): Promise<void> {
if (!IS_TURNSTILE_CONFIGURED) return;
if (!turnstileToken || !TURNSTILE_SECRET_KEY) {
captureFailedSignup(email, name);
throw new UnknownError("Server configuration error");
}
const isHuman = await verifyTurnstileToken(TURNSTILE_SECRET_KEY, turnstileToken);
if (!isHuman) {
captureFailedSignup(email, name);
throw new UnknownError("reCAPTCHA verification failed");
}
}
@@ -180,7 +174,7 @@ export const createUserAction = actionClient.schema(ZCreateUserAction).action(
"user",
async ({ ctx, parsedInput }: { ctx: ActionClientCtx; parsedInput: Record<string, any> }) => {
await applyIPRateLimit(rateLimitConfigs.auth.signup);
await verifyTurnstileIfConfigured(parsedInput.turnstileToken, parsedInput.email, parsedInput.name);
await verifyTurnstileIfConfigured(parsedInput.turnstileToken);
const hashedPassword = await hashPassword(parsedInput.password);
const { user, userAlreadyExisted } = await createUserSafely(

View File

@@ -13,7 +13,6 @@ import { TUserLocale, ZUserName, ZUserPassword } from "@formbricks/types/user";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createUserAction } from "@/modules/auth/signup/actions";
import { TermsPrivacyLinks } from "@/modules/auth/signup/components/terms-privacy-links";
import { captureFailedSignup } from "@/modules/auth/signup/lib/utils";
import { SSOOptions } from "@/modules/ee/sso/components/sso-options";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
@@ -236,7 +235,6 @@ export const SignupForm = ({
onError={() => {
setTurnstileToken(undefined);
toast.error(t("auth.signup.captcha_failed"));
captureFailedSignup(form.getValues("email"), form.getValues("name"));
}}
/>
)}

View File

@@ -1,6 +1,5 @@
import posthog from "posthog-js";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { captureFailedSignup, verifyTurnstileToken } from "./utils";
import { verifyTurnstileToken } from "./utils";
beforeEach(() => {
global.fetch = vi.fn();
@@ -62,18 +61,3 @@ describe("verifyTurnstileToken", () => {
expect(result).toBe(false);
});
});
describe("captureFailedSignup", () => {
test("should capture TELEMETRY_FAILED_SIGNUP event with email and name", () => {
const captureSpy = vi.spyOn(posthog, "capture");
const email = "test@example.com";
const name = "Test User";
captureFailedSignup(email, name);
expect(captureSpy).toHaveBeenCalledWith("TELEMETRY_FAILED_SIGNUP", {
email,
name,
});
});
});

View File

@@ -1,5 +1,3 @@
import posthog from "posthog-js";
export const verifyTurnstileToken = async (secretKey: string, token: string): Promise<boolean> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
@@ -29,10 +27,3 @@ export const verifyTurnstileToken = async (secretKey: string, token: string): Pr
clearTimeout(timeoutId);
}
};
export const captureFailedSignup = (email: string, name: string) => {
posthog.capture("TELEMETRY_FAILED_SIGNUP", {
email,
name,
});
};

View File

@@ -56,9 +56,6 @@ vi.mock("@/lib/constants", () => ({
ITEMS_PER_PAGE: 2,
ENCRYPTION_KEY: "test-encryption-key-32-chars-long!",
IS_PRODUCTION: false,
IS_POSTHOG_CONFIGURED: false,
POSTHOG_API_HOST: "test-posthog-host",
POSTHOG_API_KEY: "test-posthog-key",
}));
const environmentId = "cm123456789012345678901237";

View File

@@ -9,16 +9,11 @@ import {
getOrganizationByEnvironmentId,
subscribeOrganizationMembersToSurveyResponses,
} from "@/lib/organization/service";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getActionClasses } from "@/modules/survey/lib/action-class";
import { selectSurvey } from "@/modules/survey/lib/survey";
import { createSurvey, handleTriggerUpdates } from "./survey";
// Mock dependencies
vi.mock("@/lib/posthogServer", () => ({
capturePosthogEnvironmentEvent: vi.fn(),
}));
vi.mock("@/lib/survey/utils", () => ({
checkForInvalidImagesInQuestions: vi.fn(),
}));
@@ -121,11 +116,6 @@ describe("survey module", () => {
"user-123",
"org-123"
);
expect(capturePosthogEnvironmentEvent).toHaveBeenCalledWith(
environmentId,
"survey created",
expect.objectContaining({ surveyId: "survey-123" })
);
expect(result).toBeDefined();
expect(result.id).toBe("survey-123");
});

View File

@@ -7,7 +7,6 @@ import {
getOrganizationByEnvironmentId,
subscribeOrganizationMembersToSurveyResponses,
} from "@/lib/organization/service";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { checkForInvalidImagesInQuestions } from "@/lib/survey/utils";
import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
import { getActionClasses } from "@/modules/survey/lib/action-class";
@@ -122,11 +121,6 @@ export const createSurvey = async (
await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy, organization.id);
}
await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
surveyId: survey.id,
surveyType: survey.type,
});
return transformedSurvey;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -1,39 +0,0 @@
import { Session } from "next-auth";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
interface EnvironmentIdBaseLayoutProps {
children: React.ReactNode;
environmentId: string;
session: Session;
user: TUser;
organization: TOrganization;
}
export const EnvironmentIdBaseLayout = async ({
children,
environmentId,
session,
user,
organization,
}: EnvironmentIdBaseLayoutProps) => {
return (
<ResponseFilterProvider>
<PosthogIdentify
session={session}
user={user}
environmentId={environmentId}
organizationId={organization.id}
organizationName={organization.name}
organizationBilling={organization.billing}
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
/>
<ToasterClient />
{children}
</ResponseFilterProvider>
);
};

View File

@@ -1,56 +0,0 @@
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import React, { type JSX, useEffect } from "react";
interface PostHogPageviewProps {
posthogEnabled: boolean;
postHogApiHost?: string;
postHogApiKey?: string;
}
export const PostHogPageview = ({
posthogEnabled,
postHogApiHost,
postHogApiKey,
}: PostHogPageviewProps): JSX.Element => {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (!posthogEnabled) return;
try {
if (!postHogApiHost) {
throw new Error("Posthog API host is required");
}
if (!postHogApiKey) {
throw new Error("Posthog key is required");
}
posthog.init(postHogApiKey, { api_host: postHogApiHost });
} catch (error) {
console.error("Failed to initialize PostHog:", error);
}
}, []);
useEffect(() => {
if (!posthogEnabled) return;
let url = window.origin + pathname;
if (searchParams?.toString()) {
url += `?${searchParams.toString()}`;
}
posthog.capture("$pageview", { $current_url: url });
}, [pathname, searchParams, posthogEnabled]);
return <></>;
};
interface PHPProviderProps {
children: React.ReactNode;
posthogEnabled: boolean;
}
export const PHProvider = ({ children, posthogEnabled }: PHPProviderProps) => {
return posthogEnabled ? <PostHogProvider client={posthog}>{children}</PostHogProvider> : children;
};

View File

@@ -108,8 +108,6 @@
"nodemailer": "7.0.9",
"otplib": "12.0.1",
"papaparse": "5.5.2",
"posthog-js": "1.240.0",
"posthog-node": "5.9.2",
"prismjs": "1.30.0",
"qr-code-styling": "1.9.2",
"qrcode": "1.5.4",

View File

@@ -57,7 +57,6 @@ export default defineConfig({
"**/actions.ts", // Server actions (plural)
"**/action.ts", // Server actions (singular)
"lib/env.ts", // Environment configuration
"lib/posthogServer.ts", // PostHog server integration
"**/cache.ts", // Cache files
"**/cache/**", // Cache directories

View File

@@ -186,9 +186,6 @@ export const testInputValidation = async (service: Function, ...args: any[]): Pr
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
POSTHOG_API_KEY: "mock-posthog-api-key",
POSTHOG_HOST: "mock-posthog-host",
IS_POSTHOG_CONFIGURED: true,
ENCRYPTION_KEY: "mock-encryption-key",
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
GITHUB_ID: "mock-github-id",

View File

@@ -52,7 +52,6 @@ These variables are present inside your machine's docker-compose file. Restart t
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
| 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_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |

52
pnpm-lock.yaml generated
View File

@@ -377,12 +377,6 @@ importers:
papaparse:
specifier: 5.5.2
version: 5.5.2
posthog-js:
specifier: 1.240.0
version: 1.240.0
posthog-node:
specifier: 5.9.2
version: 5.9.2
prismjs:
specifier: 1.30.0
version: 1.30.0
@@ -2882,9 +2876,6 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@posthog/core@1.2.2':
resolution: {integrity: sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg==}
'@preact/preset-vite@2.10.1':
resolution: {integrity: sha512-59lyGBXNfZIr5OOuBUB4/IB8AqF/ULbvYnyItgK/2BJnsGJqaeaJobRVtMp1129obHQuj8oZ/dVxB9inmH8Xig==}
peerDependencies:
@@ -5687,9 +5678,6 @@ packages:
core-js-compat@3.46.0:
resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==}
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -6462,9 +6450,6 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.7.4:
resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
@@ -8246,21 +8231,6 @@ packages:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
posthog-js@1.240.0:
resolution: {integrity: sha512-zZhedVycGracBMWVRvWJMkB2EiB/dUoe/eM+CsFCnda/PN3Se+V7a6CLGuLZKLF9EfHswCxxU/PIxgDrhbAgjQ==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
posthog-node@5.9.2:
resolution: {integrity: sha512-oU7FbFcH5cn40nhP04cBeT67zE76EiGWjKKzDvm6IOm5P83sqM0Ij0wMJQSHp+QI6ZN7MLzb+4xfMPUEZ4q6CA==}
engines: {node: '>=20'}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
@@ -9838,9 +9808,6 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -13121,8 +13088,6 @@ snapshots:
dependencies:
playwright: 1.56.1
'@posthog/core@1.2.2': {}
'@preact/preset-vite@2.10.1(@babel/core@7.28.5)(preact@10.26.6)(vite@6.4.1(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.5
@@ -16287,8 +16252,6 @@ snapshots:
dependencies:
browserslist: 4.27.0
core-js@3.46.0: {}
core-util-is@1.0.3: {}
create-require@1.1.1: {}
@@ -17251,8 +17214,6 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.4.8: {}
fflate@0.7.4: {}
file-entry-cache@6.0.1:
@@ -19128,17 +19089,6 @@ snapshots:
dependencies:
xtend: 4.0.2
posthog-js@1.240.0:
dependencies:
core-js: 3.46.0
fflate: 0.4.8
preact: 10.26.6
web-vitals: 4.2.4
posthog-node@5.9.2:
dependencies:
'@posthog/core': 1.2.2
preact-render-to-string@5.2.6(preact@10.26.6):
dependencies:
preact: 10.26.6
@@ -20863,8 +20813,6 @@ snapshots:
web-streams-polyfill@3.3.3: {}
web-vitals@4.2.4: {}
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}

View File

@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
sonar.sourceEncoding=UTF-8
# Coverage
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,packages/js-core/src/index.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,packages/js-core/src/index.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,packages/js-core/src/index.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,packages/js-core/src/index.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**

View File

@@ -172,8 +172,6 @@
"OIDC_SIGNING_ALGORITHM",
"PASSWORD_RESET_DISABLED",
"PLAYWRIGHT_CI",
"POSTHOG_API_HOST",
"POSTHOG_API_KEY",
"PRIVACY_URL",
"RATE_LIMITING_DISABLED",
"REDIS_URL",
@@ -203,7 +201,6 @@
"SURVEYS_PACKAGE_MODE",
"SURVEYS_PACKAGE_BUILD",
"PUBLIC_URL",
"TELEMETRY_DISABLED",
"TURNSTILE_SECRET_KEY",
"TURNSTILE_SITE_KEY",
"RECAPTCHA_SITE_KEY",