chore: remove unused handleBillingLimitsCheck function and utils file

- Delete apps/web/app/api/lib/utils.ts as it only contained a no-op function
- Remove handleBillingLimitsCheck calls from all response creation endpoints
- Function was a placeholder with no actual implementation
This commit is contained in:
Johannes
2025-11-18 14:51:04 +01:00
parent 6999abba3b
commit 58ab40ab8e
47 changed files with 20 additions and 708 deletions

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

@@ -8,7 +8,7 @@ const SurveyEditorEnvironmentLayout = async (props) => {
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,11 +25,7 @@ const SurveyEditorEnvironmentLayout = async (props) => {
}
return (
<EnvironmentIdBaseLayout
environmentId={params.environmentId}
session={session}
user={user}
organization={organization}>
<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

@@ -24,11 +24,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}>
<EnvironmentIdBaseLayout>
<EnvironmentStorageHandler environmentId={params.environmentId} />
<EnvironmentContextWrapper
environment={layoutData.environment}

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,19 +54,7 @@ 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`);
}
}
// Limit check completed
return isLimitReached;
};
@@ -111,10 +95,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
@@ -50,23 +42,7 @@ export const getEnvironmentState = async (
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");
}
}
// Limit check completed
}
// Build the response data

View File

@@ -9,7 +9,6 @@ import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { calculateTtcTotal } from "@/lib/response/utils";
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
import { createResponse, createResponseWithQuotaEvaluation } from "./response";
@@ -28,18 +27,10 @@ vi.mock("@/lib/organization/service", () => ({
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(),
}));
@@ -145,26 +136,6 @@ describe("createResponse", () => {
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 () => {
@@ -186,20 +157,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

@@ -8,7 +8,6 @@ import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { getResponseContact } from "@/lib/response/service";
import { calculateTtcTotal } from "@/lib/response/utils";
import { validateInputs } from "@/lib/utils/validate";
@@ -96,9 +95,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 +114,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: {
@@ -234,10 +228,9 @@ describe("Response Lib Tests", () => {
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 () => {
test("should check response limit if limit not reached", async () => {
const limit = 100;
const mockOrgWithBilling = {
...mockOrganization,
@@ -251,32 +244,6 @@ describe("Response Lib Tests", () => {
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
});
});
});

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

@@ -12,9 +12,7 @@ import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
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 +47,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 +162,7 @@ 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,
@@ -183,26 +177,6 @@ describe("createResponse V2", () => {
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 () => {
@@ -225,20 +199,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 = {

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

@@ -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,37 +1,13 @@
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) => {
export const EnvironmentIdBaseLayout = async ({ children }: 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 | |

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