- {truncateMiddle(getAttributeValue(person, "userId"), 24)}
+ {truncateMiddle(getAttributeValue(person, "userId"), 24) || person.userId}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/setup/components/SetupInstructions.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/setup/components/SetupInstructions.tsx
index 03880784ec..5a7f420421 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/setup/components/SetupInstructions.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/setup/components/SetupInstructions.tsx
@@ -108,7 +108,7 @@ if (typeof window !== "undefined") {
{`
`}
You're done π
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx
index b76a7fc634..083d882db2 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HowToSendCard.tsx
@@ -49,7 +49,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
const options = [
{
id: "web",
- name: "Web App",
+ name: "In-App Survey",
icon: ComputerDesktopIcon,
description: "Embed a survey in your web app to collect responses.",
comingSoon: false,
@@ -65,7 +65,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
},
{
id: "mobile",
- name: "Mobile app",
+ name: "Mobile App Survey",
icon: DevicePhoneMobileIcon,
description: "Survey users inside a mobile app (iOS & Android).",
comingSoon: true,
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx
index 774af989ec..d732b885cc 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx
@@ -1,6 +1,6 @@
"use client";
-import AddNoCodeActionModal from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/AddNoCodeActionModal";
+import AddNoCodeActionModal from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/AddActionModal";
import { cn } from "@formbricks/lib/cn";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TSurvey } from "@formbricks/types/surveys";
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhoToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhoToSendCard.tsx
index 71e054a0af..ad27ebefb5 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhoToSendCard.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhoToSendCard.tsx
@@ -3,12 +3,14 @@
import { cn } from "@formbricks/lib/cn";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TSurvey } from "@formbricks/types/surveys";
+import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
import { Badge } from "@formbricks/ui/Badge";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import { CheckCircleIcon, FunnelIcon, PlusIcon, TrashIcon, UserGroupIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
+import { Info } from "lucide-react";
import { useEffect, useState } from "react"; /* */
const filterConditions = [
@@ -99,6 +101,24 @@ export default function WhoToSendCard({ localSurvey, setLocalSurvey, attributeCl
+
+
+
+ User Identification
+
+ To target your audience you need to identify your users within your app. You can read more
+ about how to do this in our{" "}
+
+ docs
+
+ .
+
+
+
+
{localSurvey.attributeFilters?.length === 0 ? (
diff --git a/apps/web/app/api/v1/(legacy)/js/actions/route.ts b/apps/web/app/api/v1/(legacy)/js/actions/route.ts
new file mode 100644
index 0000000000..91d1dbdc2b
--- /dev/null
+++ b/apps/web/app/api/v1/(legacy)/js/actions/route.ts
@@ -0,0 +1,10 @@
+import { responses } from "@/app/lib/api/response";
+import { NextResponse } from "next/server";
+
+export async function OPTIONS(): Promise
{
+ return responses.successResponse({}, true);
+}
+
+export async function POST(): Promise {
+ return responses.successResponse({}, true);
+}
diff --git a/apps/web/app/api/v1/(legacy)/js/lib/surveys.ts b/apps/web/app/api/v1/(legacy)/js/lib/surveys.ts
new file mode 100644
index 0000000000..d27f299460
--- /dev/null
+++ b/apps/web/app/api/v1/(legacy)/js/lib/surveys.ts
@@ -0,0 +1,116 @@
+import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
+import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
+import { displayCache } from "@formbricks/lib/display/cache";
+import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
+import { getSurveys } from "@formbricks/lib/survey/service";
+import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
+import { productCache } from "@formbricks/lib/product/cache";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { TSurveyWithTriggers } from "@formbricks/types/js";
+import { TPerson } from "@formbricks/types/people";
+import { unstable_cache } from "next/cache";
+
+// Helper function to calculate difference in days between two dates
+const diffInDays = (date1: Date, date2: Date) => {
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
+ return Math.floor(diffTime / (1000 * 60 * 60 * 24));
+};
+
+export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
+ unstable_cache(
+ async () => {
+ return await getSyncSurveys(environmentId, person);
+ },
+ [`getSyncSurveysCached-${environmentId}`],
+ {
+ tags: [
+ displayCache.tag.byPersonId(person.id),
+ surveyCache.tag.byEnvironmentId(environmentId),
+ productCache.tag.byEnvironmentId(environmentId),
+ ],
+ revalidate: SERVICES_REVALIDATION_INTERVAL,
+ }
+ )();
+
+export const getSyncSurveys = async (
+ environmentId: string,
+ person: TPerson
+): Promise => {
+ // get recontactDays from product
+ const product = await getProductByEnvironmentId(environmentId);
+
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
+ let surveys = await getSurveys(environmentId);
+
+ // filtered surveys for running and web
+ surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
+
+ const displays = await getDisplaysByPersonId(person.id);
+
+ // filter surveys that meet the displayOption criteria
+ surveys = surveys.filter((survey) => {
+ if (survey.displayOption === "respondMultiple") {
+ return true;
+ } else if (survey.displayOption === "displayOnce") {
+ return displays.filter((display) => display.surveyId === survey.id).length === 0;
+ } else if (survey.displayOption === "displayMultiple") {
+ return (
+ displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
+ 0
+ );
+ } else {
+ throw Error("Invalid displayOption");
+ }
+ });
+
+ const attributeClasses = await getAttributeClasses(environmentId);
+
+ // filter surveys that meet the attributeFilters criteria
+ const potentialSurveysWithAttributes = surveys.filter((survey) => {
+ const attributeFilters = survey.attributeFilters;
+ if (attributeFilters.length === 0) {
+ return true;
+ }
+ // check if meets all attribute filters criterias
+ return attributeFilters.every((attributeFilter) => {
+ const attributeClassName = attributeClasses.find(
+ (attributeClass) => attributeClass.id === attributeFilter.attributeClassId
+ )?.name;
+ if (!attributeClassName) {
+ throw Error("Invalid attribute filter class");
+ }
+ const personAttributeValue = person.attributes[attributeClassName];
+ if (attributeFilter.condition === "equals") {
+ return personAttributeValue === attributeFilter.value;
+ } else if (attributeFilter.condition === "notEquals") {
+ return personAttributeValue !== attributeFilter.value;
+ } else {
+ throw Error("Invalid attribute filter condition");
+ }
+ });
+ });
+
+ const latestDisplay = displays[0];
+
+ // filter surveys that meet the recontactDays criteria
+ surveys = potentialSurveysWithAttributes.filter((survey) => {
+ if (!latestDisplay) {
+ return true;
+ } else if (survey.recontactDays !== null) {
+ const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
+ if (!lastDisplaySurvey) {
+ return true;
+ }
+ return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
+ } else if (product.recontactDays !== null) {
+ return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
+ } else {
+ return true;
+ }
+ });
+
+ return surveys;
+};
diff --git a/apps/web/app/api/v1/(legacy)/js/lib/sync.ts b/apps/web/app/api/v1/(legacy)/js/lib/sync.ts
new file mode 100644
index 0000000000..f4e5101c09
--- /dev/null
+++ b/apps/web/app/api/v1/(legacy)/js/lib/sync.ts
@@ -0,0 +1,133 @@
+import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/lib/surveys";
+import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
+import { getActionClasses } from "@formbricks/lib/actionClass/service";
+import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
+import { getPerson } from "@formbricks/lib/person/service";
+import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
+import { captureTelemetry } from "@formbricks/lib/telemetry";
+import { TEnvironment } from "@formbricks/types/environment";
+import { TJsLegacyState } from "@formbricks/types/js";
+import { getSurveys } from "@formbricks/lib/survey/service";
+import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
+
+const captureNewSessionTelemetry = async (jsVersion?: string): Promise => {
+ await captureTelemetry("state update", { jsVersion: jsVersion ?? "unknown" });
+};
+
+export const getUpdatedState = async (
+ environmentId: string,
+ personId: string,
+ jsVersion?: string
+): Promise => {
+ let environment: TEnvironment | null;
+
+ if (jsVersion) {
+ captureNewSessionTelemetry(jsVersion);
+ }
+
+ // check if environment exists
+ environment = await getEnvironment(environmentId);
+
+ if (!environment) {
+ throw new Error("Environment does not exist");
+ }
+
+ if (!environment?.widgetSetupCompleted) {
+ await updateEnvironment(environment.id, { widgetSetupCompleted: true });
+ }
+
+ // check team subscriptons
+ const team = await getTeamByEnvironmentId(environmentId);
+
+ if (!team) {
+ throw new Error("Team does not exist");
+ }
+
+ // check if Monthly Active Users limit is reached
+ if (IS_FORMBRICKS_CLOUD) {
+ const hasUserTargetingSubscription =
+ team?.billing?.features.userTargeting.status &&
+ team?.billing?.features.userTargeting.status in ["active", "canceled"];
+ const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
+ const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
+
+ if (isMauLimitReached) {
+ const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
+ throw new Error(errorMessage);
+
+ // if (!personId) {
+ // // don't allow new people
+ // throw new Error(errorMessage);
+ // }
+ // const session = await getSession(sessionId);
+ // if (!session) {
+ // // don't allow new sessions
+ // throw new Error(errorMessage);
+ // }
+ // // check if session was created this month (user already active this month)
+ // const now = new Date();
+ // const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
+ // if (new Date(session.createdAt) < firstDayOfMonth) {
+ // throw new Error(errorMessage);
+ // }
+ }
+ }
+
+ const person = await getPerson(personId);
+
+ if (!person) {
+ throw new Error("Person not found");
+ }
+
+ const [surveys, noCodeActionClasses, product] = await Promise.all([
+ getSyncSurveysCached(environmentId, person),
+ getActionClasses(environmentId),
+ getProductByEnvironmentId(environmentId),
+ ]);
+
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
+ // return state
+ const state: TJsLegacyState = {
+ person: person!,
+ session: {},
+ surveys,
+ noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
+ product,
+ };
+
+ return state;
+};
+
+export const getPublicUpdatedState = async (environmentId: string) => {
+ // check if environment exists
+ const environment = await getEnvironment(environmentId);
+
+ if (!environment) {
+ throw new Error("Environment does not exist");
+ }
+
+ // TODO: check if Monthly Active Users limit is reached
+
+ const [surveys, noCodeActionClasses, product] = await Promise.all([
+ getSurveys(environmentId),
+ getActionClasses(environmentId),
+ getProductByEnvironmentId(environmentId),
+ ]);
+
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
+ const state: TJsLegacyState = {
+ surveys,
+ session: {},
+ noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
+ product,
+ person: null,
+ };
+
+ return state;
+};
diff --git a/apps/web/app/api/v1/(legacy)/js/people/[personId]/set-attribute/route.ts b/apps/web/app/api/v1/(legacy)/js/people/[personId]/set-attribute/route.ts
new file mode 100644
index 0000000000..931299e81d
--- /dev/null
+++ b/apps/web/app/api/v1/(legacy)/js/people/[personId]/set-attribute/route.ts
@@ -0,0 +1,74 @@
+import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
+import { personCache } from "@formbricks/lib/person/cache";
+import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
+import { surveyCache } from "@formbricks/lib/survey/cache";
+import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
+import { NextResponse } from "next/server";
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function POST(req: Request, { params }): Promise {
+ try {
+ const { personId } = params;
+
+ if (!personId || personId === "legacy") {
+ return responses.internalServerErrorResponse("setAttribute requires an identified user", true);
+ }
+
+ const jsonInput = await req.json();
+
+ // validate using zod
+ const inputValidation = ZJsPeopleLegacyAttributeInput.safeParse(jsonInput);
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ const { environmentId, key, value } = inputValidation.data;
+
+ const existingPerson = await getPerson(personId);
+
+ if (!existingPerson) {
+ return responses.notFoundResponse("Person", personId, true);
+ }
+
+ let attributeClass = await getAttributeClassByName(environmentId, key);
+
+ // create new attribute class if not found
+ if (attributeClass === null) {
+ attributeClass = await createAttributeClass(environmentId, key, "code");
+ }
+
+ if (!attributeClass) {
+ return responses.internalServerErrorResponse("Unable to create attribute class", true);
+ }
+
+ // upsert attribute (update or create)
+ await updatePersonAttribute(personId, attributeClass.id, value);
+
+ personCache.revalidate({
+ id: personId,
+ environmentId,
+ });
+
+ surveyCache.revalidate({
+ environmentId,
+ });
+
+ const state = await getUpdatedState(environmentId, personId);
+
+ return responses.successResponse({ ...state }, true);
+ } catch (error) {
+ console.error(error);
+ return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
+ }
+}
diff --git a/apps/web/app/api/v1/(legacy)/js/people/[personId]/set-user-id/route.ts b/apps/web/app/api/v1/(legacy)/js/people/[personId]/set-user-id/route.ts
new file mode 100644
index 0000000000..9a7d932e0f
--- /dev/null
+++ b/apps/web/app/api/v1/(legacy)/js/people/[personId]/set-user-id/route.ts
@@ -0,0 +1,38 @@
+import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
+import { ZJsPeopleUserIdInput } from "@formbricks/types/js";
+import { NextResponse } from "next/server";
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function POST(req: Request): Promise {
+ try {
+ const jsonInput = await req.json();
+
+ // validate using zod
+ const inputValidation = ZJsPeopleUserIdInput.safeParse(jsonInput);
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ const { environmentId, userId } = inputValidation.data;
+
+ const personWithUserId = await getOrCreatePersonByUserId(userId, environmentId);
+
+ const state = await getUpdatedState(environmentId, personWithUserId.id);
+
+ return responses.successResponse({ ...state }, true);
+ } catch (error) {
+ console.error(error);
+ return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
+ }
+}
diff --git a/apps/web/app/api/v1/(legacy)/js/people/route.ts b/apps/web/app/api/v1/(legacy)/js/people/route.ts
new file mode 100644
index 0000000000..6bfcdc993a
--- /dev/null
+++ b/apps/web/app/api/v1/(legacy)/js/people/route.ts
@@ -0,0 +1,32 @@
+import { responses } from "@/app/lib/api/response";
+import { createPerson } from "@formbricks/lib/person/service";
+import { NextRequest } from "next/server";
+
+export async function OPTIONS() {
+ // cors headers
+
+ return responses.successResponse({}, true);
+}
+
+export async function POST(req: NextRequest) {
+ // we need to create a new person
+ // call the createPerson service from here
+
+ const { environmentId, userId } = await req.json();
+
+ if (!environmentId) {
+ return responses.badRequestResponse("environmentId is required", { environmentId }, true);
+ }
+
+ if (!userId) {
+ return responses.badRequestResponse("userId is required", { environmentId }, true);
+ }
+
+ try {
+ const person = await createPerson(environmentId, userId);
+
+ return responses.successResponse({ status: "success", person }, true);
+ } catch (err) {
+ return responses.internalServerErrorResponse("Something went wrong", true);
+ }
+}
diff --git a/apps/web/app/api/v1/js/sync/lib/surveys.ts b/apps/web/app/api/v1/(legacy)/js/sync/lib/surveys.ts
similarity index 100%
rename from apps/web/app/api/v1/js/sync/lib/surveys.ts
rename to apps/web/app/api/v1/(legacy)/js/sync/lib/surveys.ts
diff --git a/apps/web/app/api/v1/js/sync/lib/sync.ts b/apps/web/app/api/v1/(legacy)/js/sync/lib/sync.ts
similarity index 52%
rename from apps/web/app/api/v1/js/sync/lib/sync.ts
rename to apps/web/app/api/v1/(legacy)/js/sync/lib/sync.ts
index 1181229377..2d834d0520 100644
--- a/apps/web/app/api/v1/js/sync/lib/sync.ts
+++ b/apps/web/app/api/v1/(legacy)/js/sync/lib/sync.ts
@@ -1,4 +1,4 @@
-import { getSyncSurveysCached } from "@/app/api/v1/js/sync/lib/surveys";
+import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/sync/lib/surveys";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import {
IS_FORMBRICKS_CLOUD,
@@ -7,33 +7,22 @@ import {
PRICING_USERTARGETING_FREE_MTU,
} from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
-import { createPerson, getPerson } from "@formbricks/lib/person/service";
+import { getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
-import { createSession, extendSession, getSession } from "@formbricks/lib/session/service";
+import { getSurveys } from "@formbricks/lib/survey/service";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamByEnvironmentId,
} from "@formbricks/lib/team/service";
-import { captureTelemetry } from "@formbricks/lib/telemetry";
import { TEnvironment } from "@formbricks/types/environment";
-import { TJsState } from "@formbricks/types/js";
+import { TJsLegacyState } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
-import { TSession } from "@formbricks/types/sessions";
-const captureNewSessionTelemetry = async (jsVersion?: string): Promise => {
- await captureTelemetry("session created", { jsVersion: jsVersion ?? "unknown" });
-};
-
-export const getUpdatedState = async (
- environmentId: string,
- personId?: string,
- sessionId?: string,
- jsVersion?: string
-): Promise => {
+export const getUpdatedState = async (environmentId: string, personId?: string): Promise => {
let environment: TEnvironment | null;
- let person: TPerson;
- let session: TSession | null;
+ let person: TPerson | {};
+ const session = {};
// check if environment exists
environment = await getEnvironment(environmentId);
@@ -58,64 +47,23 @@ export const getUpdatedState = async (
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
if (isMauLimitReached) {
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
- if (!personId || !sessionId) {
+ if (!personId) {
// don't allow new people or sessions
throw new Error(errorMessage);
}
- const session = await getSession(sessionId);
- if (!session) {
- // don't allow new sessions
- throw new Error(errorMessage);
- }
- // check if session was created this month (user already active this month)
- const now = new Date();
- const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
- if (new Date(session.createdAt) < firstDayOfMonth) {
- throw new Error(errorMessage);
- }
}
}
if (!personId) {
// create a new person
- person = await createPerson(environmentId);
- // create a new session
- session = await createSession(person.id);
+ person = { id: "legacy" };
} else {
// check if person exists
const existingPerson = await getPerson(personId);
- if (!existingPerson) {
- // create a new person
- person = await createPerson(environmentId);
- } else {
+ if (existingPerson) {
person = existingPerson;
- }
- }
- if (!sessionId) {
- // create a new session
- session = await createSession(person.id);
- } else {
- // check validity of person & session
- session = await getSession(sessionId);
- if (!session) {
- // create a new session
- session = await createSession(person.id);
- captureNewSessionTelemetry(jsVersion);
} else {
- // check if session is expired
- if (session.expiresAt < new Date()) {
- // create a new session
- session = await createSession(person.id);
- captureNewSessionTelemetry(jsVersion);
- } else {
- // extend session (if about to expire)
- const isSessionAboutToExpire =
- new Date(session.expiresAt).getTime() - new Date().getTime() < 1000 * 60 * 10;
-
- if (isSessionAboutToExpire) {
- session = await extendSession(sessionId);
- }
- }
+ person = { id: "legacy" };
}
}
// check if App Survey limit is reached
@@ -131,9 +79,20 @@ export const getUpdatedState = async (
monthlyResponsesCount >= PRICING_APPSURVEYS_FREE_RESPONSES;
}
+ const isPerson = Object.keys(person).length > 0;
+
+ let surveys;
+ if (isAppSurveyLimitReached) {
+ surveys = [];
+ } else if (isPerson) {
+ surveys = await getSyncSurveysCached(environmentId, person as TPerson);
+ } else {
+ surveys = await getSurveys(environmentId);
+ surveys = surveys.filter((survey) => survey.type === "web");
+ }
+
// get/create rest of the state
- const [surveys, noCodeActionClasses, product] = await Promise.all([
- !isAppSurveyLimitReached ? getSyncSurveysCached(environmentId, person) : [],
+ const [noCodeActionClasses, product] = await Promise.all([
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
@@ -143,8 +102,8 @@ export const getUpdatedState = async (
}
// return state
- const state: TJsState = {
- person: person!,
+ const state: TJsLegacyState = {
+ person,
session,
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
diff --git a/apps/web/app/api/v1/js/sync/route.ts b/apps/web/app/api/v1/(legacy)/js/sync/route.ts
similarity index 70%
rename from apps/web/app/api/v1/js/sync/route.ts
rename to apps/web/app/api/v1/(legacy)/js/sync/route.ts
index 7da235a2fe..22602b8607 100644
--- a/apps/web/app/api/v1/js/sync/route.ts
+++ b/apps/web/app/api/v1/(legacy)/js/sync/route.ts
@@ -1,7 +1,7 @@
-import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
+import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
-import { ZJsSyncInput } from "@formbricks/types/js";
+import { ZJsSyncLegacyInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise {
@@ -13,7 +13,7 @@ export async function POST(req: Request): Promise {
const jsonInput = await req.json();
// validate using zod
- const inputValidation = ZJsSyncInput.safeParse(jsonInput);
+ const inputValidation = ZJsSyncLegacyInput.safeParse(jsonInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -23,9 +23,9 @@ export async function POST(req: Request): Promise {
);
}
- const { environmentId, personId, sessionId } = inputValidation.data;
+ const { environmentId, personId } = inputValidation.data;
- const state = await getUpdatedState(environmentId, personId, sessionId, inputValidation.data.jsVersion);
+ const state = await getUpdatedState(environmentId, personId);
return responses.successResponse({ ...state }, true);
} catch (error) {
diff --git a/apps/web/app/api/v1/client/displays/[displayId]/responded/route.ts b/apps/web/app/api/v1/client/(legacy)/displays/[displayId]/responded/route.ts
similarity index 100%
rename from apps/web/app/api/v1/client/displays/[displayId]/responded/route.ts
rename to apps/web/app/api/v1/client/(legacy)/displays/[displayId]/responded/route.ts
diff --git a/apps/web/app/api/v1/client/displays/[displayId]/route.ts b/apps/web/app/api/v1/client/(legacy)/displays/[displayId]/route.ts
similarity index 100%
rename from apps/web/app/api/v1/client/displays/[displayId]/route.ts
rename to apps/web/app/api/v1/client/(legacy)/displays/[displayId]/route.ts
diff --git a/apps/web/app/api/v1/client/displays/route.ts b/apps/web/app/api/v1/client/(legacy)/displays/route.ts
similarity index 76%
rename from apps/web/app/api/v1/client/displays/route.ts
rename to apps/web/app/api/v1/client/(legacy)/displays/route.ts
index 5225f40fb9..eef6819cbc 100644
--- a/apps/web/app/api/v1/client/displays/route.ts
+++ b/apps/web/app/api/v1/client/(legacy)/displays/route.ts
@@ -1,11 +1,11 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
-import { InvalidInputError } from "@formbricks/types/errors";
+import { createDisplayLegacy } from "@formbricks/lib/display/service";
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
-import { createDisplay } from "@formbricks/lib/display/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
-import { TDisplay, ZDisplayCreateInput } from "@formbricks/types/displays";
+import { TDisplay, ZDisplayLegacyCreateInput } from "@formbricks/types/displays";
+import { InvalidInputError } from "@formbricks/types/errors";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise {
@@ -13,8 +13,11 @@ export async function OPTIONS(): Promise {
}
export async function POST(request: Request): Promise {
- const jsonInput: unknown = await request.json();
- const inputValidation = ZDisplayCreateInput.safeParse(jsonInput);
+ const jsonInput = await request.json();
+ if (jsonInput.personId === "legacy") {
+ delete jsonInput.personId;
+ }
+ const inputValidation = ZDisplayLegacyCreateInput.safeParse(jsonInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -24,12 +27,14 @@ export async function POST(request: Request): Promise {
);
}
- const displayInput = inputValidation.data;
+ const { surveyId, responseId } = inputValidation.data;
+ let { personId } = inputValidation.data;
+
// find environmentId from surveyId
let survey;
try {
- survey = await getSurvey(displayInput.surveyId);
+ survey = await getSurvey(surveyId);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
@@ -45,7 +50,11 @@ export async function POST(request: Request): Promise {
// create display
let display: TDisplay;
try {
- display = await createDisplay(displayInput);
+ display = await createDisplayLegacy({
+ surveyId,
+ personId,
+ responseId,
+ });
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
@@ -57,7 +66,7 @@ export async function POST(request: Request): Promise {
if (teamDetails?.teamOwnerId) {
await capturePosthogEvent(teamDetails.teamOwnerId, "display created", teamDetails.teamId, {
- surveyId: displayInput.surveyId,
+ surveyId,
});
} else {
console.warn("Posthog capture not possible. No team owner found");
diff --git a/apps/web/app/api/v1/client/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/(legacy)/responses/[responseId]/route.ts
similarity index 100%
rename from apps/web/app/api/v1/client/responses/[responseId]/route.ts
rename to apps/web/app/api/v1/client/(legacy)/responses/[responseId]/route.ts
diff --git a/apps/web/app/api/v1/client/(legacy)/responses/route.ts b/apps/web/app/api/v1/client/(legacy)/responses/route.ts
new file mode 100644
index 0000000000..69cc39c300
--- /dev/null
+++ b/apps/web/app/api/v1/client/(legacy)/responses/route.ts
@@ -0,0 +1,109 @@
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { sendToPipeline } from "@/app/lib/pipelines";
+import { InvalidInputError } from "@formbricks/types/errors";
+import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
+import { getSurvey } from "@formbricks/lib/survey/service";
+import { createResponse } from "@formbricks/lib/response/service";
+import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
+import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
+import { NextResponse } from "next/server";
+import { UAParser } from "ua-parser-js";
+import { TSurvey } from "@formbricks/types/surveys";
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function POST(request: Request): Promise {
+ const responseInput: TResponseInput = await request.json();
+ if (responseInput.personId === "legacy") {
+ responseInput.personId = null;
+ }
+ const agent = UAParser(request.headers.get("user-agent"));
+ const inputValidation = ZResponseInput.safeParse(responseInput);
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ let survey: TSurvey | null;
+
+ try {
+ survey = await getSurvey(responseInput.surveyId);
+ if (!survey) {
+ return responses.notFoundResponse("Survey", responseInput.surveyId);
+ }
+ } catch (error) {
+ if (error instanceof InvalidInputError) {
+ return responses.badRequestResponse(error.message);
+ } else {
+ console.error(error);
+ return responses.internalServerErrorResponse(error.message);
+ }
+ }
+
+ const teamDetails = await getTeamDetails(survey.environmentId);
+
+ let response: TResponse;
+ try {
+ const meta = {
+ source: responseInput?.meta?.source,
+ url: responseInput?.meta?.url,
+ userAgent: {
+ browser: agent?.browser.name,
+ device: agent?.device.type,
+ os: agent?.os.name,
+ },
+ };
+
+ // check if personId is anonymous
+ if (responseInput.personId === "anonymous") {
+ // remove this from the request
+ responseInput.personId = null;
+ }
+
+ response = await createResponse({
+ ...responseInput,
+ meta,
+ });
+ } catch (error) {
+ if (error instanceof InvalidInputError) {
+ return responses.badRequestResponse(error.message);
+ } else {
+ console.error(error);
+ return responses.internalServerErrorResponse(error.message);
+ }
+ }
+
+ sendToPipeline({
+ event: "responseCreated",
+ environmentId: survey.environmentId,
+ surveyId: response.surveyId,
+ response: response,
+ });
+
+ if (responseInput.finished) {
+ sendToPipeline({
+ event: "responseFinished",
+ environmentId: survey.environmentId,
+ surveyId: response.surveyId,
+ response: response,
+ });
+ }
+
+ if (teamDetails?.teamOwnerId) {
+ await capturePosthogEvent(teamDetails.teamOwnerId, "response created", teamDetails.teamId, {
+ surveyId: response.surveyId,
+ surveyType: survey.type,
+ });
+ } else {
+ console.warn("Posthog capture not possible. No team owner found");
+ }
+
+ return responses.successResponse(response, true);
+}
diff --git a/apps/web/app/api/v1/js/actions/route.ts b/apps/web/app/api/v1/client/[environmentId]/actions/route.ts
similarity index 64%
rename from apps/web/app/api/v1/js/actions/route.ts
rename to apps/web/app/api/v1/client/[environmentId]/actions/route.ts
index 0e0fbf7391..35883bc7a4 100644
--- a/apps/web/app/api/v1/js/actions/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/actions/route.ts
@@ -4,16 +4,25 @@ import { createAction } from "@formbricks/lib/action/service";
import { ZActionInput } from "@formbricks/types/actions";
import { NextResponse } from "next/server";
+interface Context {
+ params: {
+ environmentId: string;
+ };
+}
+
export async function OPTIONS(): Promise {
return responses.successResponse({}, true);
}
-export async function POST(req: Request): Promise {
+export async function POST(req: Request, context: Context): Promise {
try {
const jsonInput = await req.json();
// validate using zod
- const inputValidation = ZActionInput.safeParse(jsonInput);
+ const inputValidation = ZActionInput.safeParse({
+ ...jsonInput,
+ environmentId: context.params.environmentId,
+ });
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -23,19 +32,7 @@ export async function POST(req: Request): Promise {
);
}
- const { environmentId, sessionId, name, properties } = inputValidation.data;
-
- // hotfix: don't create action for "Exit Intent (Desktop)", 50% Scroll events
- if (["Exit Intent (Desktop)", "50% Scroll"].includes(name)) {
- return responses.successResponse({}, true);
- }
-
- createAction({
- environmentId,
- sessionId,
- name,
- properties,
- });
+ await createAction(inputValidation.data);
return responses.successResponse({}, true);
} catch (error) {
diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/[displayId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/displays/[displayId]/route.ts
new file mode 100644
index 0000000000..bed7f341ae
--- /dev/null
+++ b/apps/web/app/api/v1/client/[environmentId]/displays/[displayId]/route.ts
@@ -0,0 +1,40 @@
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { updateDisplay } from "@formbricks/lib/display/service";
+import { ZDisplayUpdateInput } from "@formbricks/types/displays";
+import { NextResponse } from "next/server";
+
+interface Context {
+ params: {
+ displayId: string;
+ environmentId: string;
+ };
+}
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function PUT(request: Request, context: Context): Promise {
+ const { displayId } = context.params;
+ const jsonInput = await request.json();
+ const inputValidation = ZDisplayUpdateInput.safeParse({
+ ...jsonInput,
+ });
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ try {
+ const display = await updateDisplay(displayId, inputValidation.data);
+ return responses.successResponse(display, true);
+ } catch (error) {
+ console.error(error);
+ return responses.internalServerErrorResponse(error.message, true);
+ }
+}
diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts
new file mode 100644
index 0000000000..15927e015e
--- /dev/null
+++ b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts
@@ -0,0 +1,65 @@
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { createDisplay } from "@formbricks/lib/display/service";
+import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
+import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
+import { TDisplay, ZDisplayCreateInput } from "@formbricks/types/displays";
+import { InvalidInputError } from "@formbricks/types/errors";
+import { NextResponse } from "next/server";
+
+interface Context {
+ params: {
+ environmentId: string;
+ };
+}
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function POST(request: Request, context: Context): Promise {
+ const jsonInput = await request.json();
+ const inputValidation = ZDisplayCreateInput.safeParse({
+ ...jsonInput,
+ environmentId: context.params.environmentId,
+ });
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ // find teamId & teamOwnerId from environmentId
+ const teamDetails = await getTeamDetails(inputValidation.data.environmentId);
+
+ // create display
+ let display: TDisplay;
+ try {
+ display = await createDisplay(inputValidation.data);
+ } catch (error) {
+ if (error instanceof InvalidInputError) {
+ return responses.badRequestResponse(error.message);
+ } else {
+ console.error(error);
+ return responses.internalServerErrorResponse(error.message);
+ }
+ }
+
+ if (teamDetails?.teamOwnerId) {
+ await capturePosthogEvent(teamDetails.teamOwnerId, "display created", teamDetails.teamId);
+ } else {
+ console.warn("Posthog capture not possible. No team owner found");
+ }
+
+ return responses.successResponse(
+ {
+ ...display,
+ createdAt: display.createdAt.toISOString(),
+ updatedAt: display.updatedAt.toISOString(),
+ },
+ true
+ );
+}
diff --git a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts
new file mode 100644
index 0000000000..5cbfe6a72a
--- /dev/null
+++ b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts
@@ -0,0 +1,113 @@
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { getActionClasses } from "@formbricks/lib/actionClass/service";
+import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
+import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
+import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
+import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
+import { getSyncSurveysCached } from "@formbricks/lib/survey/service";
+import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
+import { TEnvironment } from "@formbricks/types/environment";
+import { TJsState, ZJsPeopleUserIdInput } from "@formbricks/types/js";
+import { NextResponse } from "next/server";
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function GET(
+ _: Request,
+ {
+ params,
+ }: {
+ params: {
+ environmentId: string;
+ userId: string;
+ };
+ }
+): Promise {
+ try {
+ // validate using zod
+ const inputValidation = ZJsPeopleUserIdInput.safeParse({
+ environmentId: params.environmentId,
+ userId: params.userId,
+ });
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ const { environmentId, userId } = inputValidation.data;
+
+ // check if person exists
+ const person = await getOrCreatePersonByUserId(userId, environmentId);
+
+ if (!person) {
+ return responses.badRequestResponse(`Person with userId ${userId} not found`);
+ }
+
+ let environment: TEnvironment | null;
+
+ // check if environment exists
+ environment = await getEnvironment(environmentId);
+
+ if (!environment) {
+ throw new Error("Environment does not exist");
+ }
+
+ if (!environment?.widgetSetupCompleted) {
+ await updateEnvironment(environment.id, { widgetSetupCompleted: true });
+ }
+
+ // check team subscriptons
+ const team = await getTeamByEnvironmentId(environmentId);
+
+ if (!team) {
+ throw new Error("Team does not exist");
+ }
+
+ // check if Monthly Active Users limit is reached
+ if (IS_FORMBRICKS_CLOUD) {
+ const hasUserTargetingSubscription =
+ team?.billing?.features.userTargeting.status &&
+ team?.billing?.features.userTargeting.status in ["active", "canceled"];
+ const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
+ const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
+
+ // TODO: Problem is that if isMauLimitReached, all sync request will fail
+ // But what we essentially want, is to fail only for new people syncing for the first time
+
+ if (isMauLimitReached) {
+ const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
+ throw new Error(errorMessage);
+ }
+ }
+
+ const [surveys, noCodeActionClasses, product] = await Promise.all([
+ getSyncSurveysCached(environmentId, person),
+ getActionClasses(environmentId),
+ getProductByEnvironmentId(environmentId),
+ ]);
+
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
+ // return state
+ const state: TJsState = {
+ person,
+ surveys,
+ noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
+ product,
+ };
+
+ return responses.successResponse({ ...state }, true);
+ } catch (error) {
+ console.error(error);
+ return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
+ }
+}
diff --git a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts
new file mode 100644
index 0000000000..078107b79b
--- /dev/null
+++ b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/route.ts
@@ -0,0 +1,66 @@
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { getActionClasses } from "@formbricks/lib/actionClass/service";
+import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
+import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
+import { getSurveys } from "@formbricks/lib/survey/service";
+import { TJsState, ZJsPublicSyncInput } from "@formbricks/types/js";
+import { NextRequest, NextResponse } from "next/server";
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function GET(
+ _: NextRequest,
+ { params }: { params: { environmentId: string } }
+): Promise {
+ try {
+ // validate using zod
+ const environmentIdValidation = ZJsPublicSyncInput.safeParse({
+ environmentId: params.environmentId,
+ });
+
+ if (!environmentIdValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(environmentIdValidation.error),
+ true
+ );
+ }
+
+ const { environmentId } = environmentIdValidation.data;
+
+ const environment = await getEnvironment(environmentId);
+
+ if (!environment) {
+ throw new Error("Environment does not exist");
+ }
+
+ if (!environment?.widgetSetupCompleted) {
+ await updateEnvironment(environment.id, { widgetSetupCompleted: true });
+ }
+
+ const [surveys, noCodeActionClasses, product] = await Promise.all([
+ getSurveys(environmentId),
+ getActionClasses(environmentId),
+ getProductByEnvironmentId(environmentId),
+ ]);
+
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
+ const state: TJsState = {
+ surveys: surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web"),
+ noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
+ product,
+ person: null,
+ };
+
+ return responses.successResponse({ ...state }, true);
+ } catch (error) {
+ console.error(error);
+ return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true);
+ }
+}
diff --git a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts b/apps/web/app/api/v1/client/[environmentId]/people/[personId]/set-attribute/route.ts
similarity index 82%
rename from apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts
rename to apps/web/app/api/v1/client/[environmentId]/people/[personId]/set-attribute/route.ts
index 2aef228c59..418f1d68ce 100644
--- a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/people/[personId]/set-attribute/route.ts
@@ -1,4 +1,4 @@
-import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
+import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
@@ -8,13 +8,20 @@ import { surveyCache } from "@formbricks/lib/survey/cache";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
+interface Context {
+ params: {
+ personId: string;
+ environmentId: string;
+ };
+}
+
export async function OPTIONS(): Promise {
return responses.successResponse({}, true);
}
-export async function POST(req: Request, { params }): Promise {
+export async function POST(req: Request, context: Context): Promise {
try {
- const { personId } = params;
+ const { personId, environmentId } = context.params;
const jsonInput = await req.json();
// validate using zod
@@ -28,7 +35,7 @@ export async function POST(req: Request, { params }): Promise {
);
}
- const { environmentId, sessionId, key, value } = inputValidation.data;
+ const { key, value } = inputValidation.data;
const existingPerson = await getPerson(personId);
@@ -59,7 +66,7 @@ export async function POST(req: Request, { params }): Promise {
environmentId,
});
- const state = await getUpdatedState(environmentId, personId, sessionId);
+ const state = await getUpdatedState(environmentId, personId);
return responses.successResponse({ ...state }, true);
} catch (error) {
diff --git a/apps/web/app/api/v1/client/[environmentId]/people/route.ts b/apps/web/app/api/v1/client/[environmentId]/people/route.ts
new file mode 100644
index 0000000000..6bfcdc993a
--- /dev/null
+++ b/apps/web/app/api/v1/client/[environmentId]/people/route.ts
@@ -0,0 +1,32 @@
+import { responses } from "@/app/lib/api/response";
+import { createPerson } from "@formbricks/lib/person/service";
+import { NextRequest } from "next/server";
+
+export async function OPTIONS() {
+ // cors headers
+
+ return responses.successResponse({}, true);
+}
+
+export async function POST(req: NextRequest) {
+ // we need to create a new person
+ // call the createPerson service from here
+
+ const { environmentId, userId } = await req.json();
+
+ if (!environmentId) {
+ return responses.badRequestResponse("environmentId is required", { environmentId }, true);
+ }
+
+ if (!userId) {
+ return responses.badRequestResponse("userId is required", { environmentId }, true);
+ }
+
+ try {
+ const person = await createPerson(environmentId, userId);
+
+ return responses.successResponse({ status: "success", person }, true);
+ } catch (err) {
+ return responses.internalServerErrorResponse("Something went wrong", true);
+ }
+}
diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts
new file mode 100644
index 0000000000..e59bf5c9cc
--- /dev/null
+++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts
@@ -0,0 +1,87 @@
+import { responses } from "@/app/lib/api/response";
+import { transformErrorToDetails } from "@/app/lib/api/validator";
+import { sendToPipeline } from "@/app/lib/pipelines";
+import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { getSurvey } from "@formbricks/lib/survey/service";
+import { updateResponse } from "@formbricks/lib/response/service";
+import { ZResponseUpdateInput } from "@formbricks/types/responses";
+import { NextResponse } from "next/server";
+
+export async function OPTIONS(): Promise {
+ return responses.successResponse({}, true);
+}
+
+export async function PUT(
+ request: Request,
+ { params }: { params: { responseId: string } }
+): Promise {
+ const { responseId } = params;
+
+ if (!responseId) {
+ return responses.badRequestResponse("Response ID is missing", undefined, true);
+ }
+
+ const responseUpdate = await request.json();
+
+ const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
+
+ if (!inputValidation.success) {
+ return responses.badRequestResponse(
+ "Fields are missing or incorrectly formatted",
+ transformErrorToDetails(inputValidation.error),
+ true
+ );
+ }
+
+ // update response
+ let response;
+ try {
+ response = await updateResponse(responseId, inputValidation.data);
+ } catch (error) {
+ if (error instanceof ResourceNotFoundError) {
+ return responses.notFoundResponse("Response", responseId, true);
+ }
+ if (error instanceof InvalidInputError) {
+ return responses.badRequestResponse(error.message);
+ }
+ if (error instanceof DatabaseError) {
+ console.error(error);
+ return responses.internalServerErrorResponse(error.message);
+ }
+ }
+
+ // get survey to get environmentId
+ let survey;
+ try {
+ survey = await getSurvey(response.surveyId);
+ } catch (error) {
+ if (error instanceof InvalidInputError) {
+ return responses.badRequestResponse(error.message);
+ }
+ if (error instanceof DatabaseError) {
+ console.error(error);
+ return responses.internalServerErrorResponse(error.message);
+ }
+ }
+
+ // send response update to pipeline
+ // don't await to not block the response
+ sendToPipeline({
+ event: "responseUpdated",
+ environmentId: survey.environmentId,
+ surveyId: survey.id,
+ response,
+ });
+
+ if (response.finished) {
+ // send response to pipeline
+ // don't await to not block the response
+ sendToPipeline({
+ event: "responseFinished",
+ environmentId: survey.environmentId,
+ surveyId: survey.id,
+ response: response,
+ });
+ }
+ return responses.successResponse(response, true);
+}
diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts
similarity index 94%
rename from apps/web/app/api/v1/client/responses/route.ts
rename to apps/web/app/api/v1/client/[environmentId]/responses/route.ts
index 3fa79addbe..c8425c2b39 100644
--- a/apps/web/app/api/v1/client/responses/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts
@@ -9,7 +9,6 @@ import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
import { NextResponse } from "next/server";
import { UAParser } from "ua-parser-js";
-import { TSurvey } from "@formbricks/types/surveys";
export async function OPTIONS(): Promise {
return responses.successResponse({}, true);
@@ -28,13 +27,10 @@ export async function POST(request: Request): Promise {
);
}
- let survey: TSurvey | null;
+ let survey;
try {
survey = await getSurvey(responseInput.surveyId);
- if (!survey) {
- return responses.notFoundResponse("Survey", responseInput.surveyId);
- }
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
@@ -58,6 +54,12 @@ export async function POST(request: Request): Promise {
},
};
+ // check if personId is anonymous
+ if (responseInput.personId === "anonymous") {
+ // remove this from the request
+ responseInput.personId = null;
+ }
+
response = await createResponse({
...responseInput,
meta,
diff --git a/apps/web/app/api/v1/client/storage/lib/uploadPrivateFile.ts b/apps/web/app/api/v1/client/[environmentId]/storage/lib/uploadPrivateFile.ts
similarity index 100%
rename from apps/web/app/api/v1/client/storage/lib/uploadPrivateFile.ts
rename to apps/web/app/api/v1/client/[environmentId]/storage/lib/uploadPrivateFile.ts
diff --git a/apps/web/app/api/v1/client/storage/local/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts
similarity index 90%
rename from apps/web/app/api/v1/client/storage/local/route.ts
rename to apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts
index 45c757f391..1e60f7c520 100644
--- a/apps/web/app/api/v1/client/storage/local/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts
@@ -11,7 +11,15 @@ import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { validateLocalSignedUrl } from "@formbricks/lib/crypto";
-export async function POST(req: NextRequest): Promise {
+interface Context {
+ params: {
+ environmentId: string;
+ };
+}
+
+export async function POST(req: NextRequest, context: Context): Promise {
+ const environmentId = context.params.environmentId;
+
const accessType = "private"; // private files are accessible only by authorized users
const headersList = headers();
@@ -47,16 +55,12 @@ export async function POST(req: NextRequest): Promise {
return responses.unauthorizedResponse();
}
- const survey = await getSurvey(surveyId);
+ const [survey, team] = await Promise.all([getSurvey(surveyId), getTeamByEnvironmentId(environmentId)]);
if (!survey) {
return responses.notFoundResponse("Survey", surveyId);
}
- const { environmentId } = survey;
-
- const team = await getTeamByEnvironmentId(environmentId);
-
if (!team) {
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
}
diff --git a/apps/web/app/api/v1/client/storage/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/route.ts
similarity index 81%
rename from apps/web/app/api/v1/client/storage/route.ts
rename to apps/web/app/api/v1/client/[environmentId]/storage/route.ts
index 2691616011..3f71ecfc82 100644
--- a/apps/web/app/api/v1/client/storage/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/storage/route.ts
@@ -4,13 +4,21 @@ import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { NextRequest, NextResponse } from "next/server";
import uploadPrivateFile from "./lib/uploadPrivateFile";
+interface Context {
+ params: {
+ environmentId: string;
+ };
+}
+
// api endpoint for uploading private files
// uploaded files will be private, only the user who has access to the environment can access the file
// uploading private files requires no authentication
// use this to let users upload files to a survey for example
// this api endpoint will return a signed url for uploading the file to s3 and another url for uploading file to the local storage
-export async function POST(req: NextRequest): Promise {
+export async function POST(req: NextRequest, context: Context): Promise {
+ const environmentId = context.params.environmentId;
+
const { fileName, fileType, surveyId } = await req.json();
if (!surveyId) {
@@ -25,16 +33,12 @@ export async function POST(req: NextRequest): Promise {
return responses.badRequestResponse("contentType is required");
}
- const survey = await getSurvey(surveyId);
+ const [survey, team] = await Promise.all([getSurvey(surveyId), getTeamByEnvironmentId(environmentId)]);
if (!survey) {
return responses.notFoundResponse("Survey", surveyId);
}
- const { environmentId } = survey;
-
- const team = await getTeamByEnvironmentId(environmentId);
-
if (!team) {
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
}
diff --git a/apps/web/app/api/v1/client/environments/[environmentId]/people/route.ts b/apps/web/app/api/v1/client/environments/[environmentId]/people/route.ts
deleted file mode 100644
index 79149d8ede..0000000000
--- a/apps/web/app/api/v1/client/environments/[environmentId]/people/route.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { getSettings } from "@/app/lib/api/clientSettings";
-import { responses } from "@/app/lib/api/response";
-import { createPerson } from "@formbricks/lib/person/service";
-import { createSession } from "@formbricks/lib/session/service";
-import { NextResponse } from "next/server";
-
-export async function OPTIONS(): Promise {
- return responses.successResponse({}, true);
-}
-
-export async function POST(
- _: Request,
- { params }: { params: { environmentId: string } }
-): Promise {
- const { environmentId } = params;
-
- if (!environmentId) {
- return responses.badRequestResponse(
- "Missing environmentId",
- {
- missing_field: "environmentId",
- },
- true
- );
- }
-
- try {
- const person = await createPerson(environmentId);
- const session = await createSession(person.id);
- const settings = await getSettings(environmentId, person.id);
-
- return responses.successResponse(
- {
- person,
- session,
- settings,
- },
- true
- );
- } catch (error) {
- return responses.internalServerErrorResponse(error.message, true);
- }
-}
diff --git a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts
deleted file mode 100644
index f4514c4a09..0000000000
--- a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
-import { responses } from "@/app/lib/api/response";
-import { transformErrorToDetails } from "@/app/lib/api/validator";
-import { prisma } from "@formbricks/database";
-import { getDisplaysByPersonId, updateDisplay } from "@formbricks/lib/display/service";
-import { personCache } from "@formbricks/lib/person/cache";
-import { deletePerson, selectPerson, transformPrismaPerson } from "@formbricks/lib/person/service";
-import { surveyCache } from "@formbricks/lib/survey/cache";
-import { ZJsPeopleUserIdInput } from "@formbricks/types/js";
-import { NextResponse } from "next/server";
-
-export async function OPTIONS(): Promise {
- return responses.successResponse({}, true);
-}
-
-export async function POST(req: Request, { params }): Promise {
- try {
- const { personId } = params;
- const jsonInput = await req.json();
-
- // validate using zod
- const inputValidation = ZJsPeopleUserIdInput.safeParse(jsonInput);
-
- if (!inputValidation.success) {
- return responses.badRequestResponse(
- "Fields are missing or incorrectly formatted",
- transformErrorToDetails(inputValidation.error),
- true
- );
- }
-
- const { environmentId, userId, sessionId } = inputValidation.data;
-
- let returnedPerson;
- // check if person with this userId exists
- const person = await prisma.person.findFirst({
- where: {
- environmentId,
- attributes: {
- some: {
- attributeClass: {
- name: "userId",
- },
- value: userId,
- },
- },
- },
- select: selectPerson,
- });
- // if person exists, reconnect displays, session and delete old user
- if (person) {
- const displays = await getDisplaysByPersonId(personId);
-
- await Promise.all(displays.map((display) => updateDisplay(display.id, { personId: person.id })));
-
- // reconnect session to new person
- await prisma.session.update({
- where: {
- id: sessionId,
- },
- data: {
- person: {
- connect: {
- id: person.id,
- },
- },
- },
- });
-
- // delete old person
- await deletePerson(personId);
-
- returnedPerson = person;
- } else {
- // update person with userId
- returnedPerson = await prisma.person.update({
- where: {
- id: personId,
- },
- data: {
- attributes: {
- create: {
- value: userId,
- attributeClass: {
- connect: {
- name_environmentId: {
- name: "userId",
- environmentId,
- },
- },
- },
- },
- },
- },
- select: selectPerson,
- });
-
- personCache.revalidate({
- id: returnedPerson.id,
- environmentId: returnedPerson.environmentId,
- });
- }
-
- const transformedPerson = transformPrismaPerson(returnedPerson);
-
- personCache.revalidate({
- id: transformedPerson.id,
- environmentId: environmentId,
- });
-
- surveyCache.revalidate({
- environmentId,
- });
-
- const state = await getUpdatedState(environmentId, transformedPerson.id, sessionId);
-
- return responses.successResponse({ ...state }, true);
- } catch (error) {
- console.error(error);
- return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
- }
-}
diff --git a/apps/web/app/lib/api/clientPerson.ts b/apps/web/app/lib/api/clientPerson.ts
deleted file mode 100644
index b846e8b752..0000000000
--- a/apps/web/app/lib/api/clientPerson.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { prisma } from "@formbricks/database";
-import { TPerson } from "@formbricks/types/people";
-import { transformPrismaPerson } from "@formbricks/lib/person/service";
-import { personCache } from "@formbricks/lib/person/cache";
-
-const select = {
- id: true,
- environmentId: true,
- createdAt: true,
- updatedAt: true,
- attributes: {
- select: {
- id: true,
- value: true,
- attributeClass: {
- select: {
- id: true,
- name: true,
- },
- },
- },
- },
-};
-
-export const createPerson = async (environmentId: string): Promise => {
- const prismaPerson = await prisma.person.create({
- data: {
- environment: {
- connect: {
- id: environmentId,
- },
- },
- },
- select,
- });
-
- const person = transformPrismaPerson(prismaPerson);
-
- personCache.revalidate({
- id: person.id,
- environmentId: person.environmentId,
- });
- return person;
-};
diff --git a/apps/web/app/lib/api/clientSession.ts b/apps/web/app/lib/api/clientSession.ts
deleted file mode 100644
index 6e6f173edc..0000000000
--- a/apps/web/app/lib/api/clientSession.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { prisma } from "@formbricks/database";
-import { TSession } from "@formbricks/types/sessions";
-
-export const createSession = async (personId: string): Promise> => {
- return prisma.session.create({
- data: {
- person: {
- connect: {
- id: personId,
- },
- },
- },
- select: {
- id: true,
- },
- });
-};
diff --git a/apps/web/app/lib/api/clientSettings.ts b/apps/web/app/lib/api/clientSettings.ts
index a27990ca9e..ffd84cd9de 100644
--- a/apps/web/app/lib/api/clientSettings.ts
+++ b/apps/web/app/lib/api/clientSettings.ts
@@ -72,7 +72,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
triggers: {
select: {
id: true,
- eventClass: {
+ actionClass: {
select: {
id: true,
name: true,
@@ -181,7 +181,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
return {
id: survey.id,
questions: JSON.parse(JSON.stringify(survey.questions)),
- triggers: survey.triggers.map((trigger) => trigger.eventClass.name),
+ triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
thankYouCard: JSON.parse(JSON.stringify(survey.thankYouCard)),
welcomeCard: JSON.parse(JSON.stringify(survey.welcomeCard)),
autoClose: survey.autoClose,
@@ -189,7 +189,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
};
});
- const noCodeEvents = await prisma.eventClass.findMany({
+ const noCodeEvents = await prisma.actionClass.findMany({
where: {
environmentId,
type: "noCode",
diff --git a/apps/web/app/lib/formbricks.ts b/apps/web/app/lib/formbricks.ts
index 24a26cad37..e56580f8a8 100644
--- a/apps/web/app/lib/formbricks.ts
+++ b/apps/web/app/lib/formbricks.ts
@@ -11,9 +11,10 @@ export const createResponse = async (
): Promise => {
const api = formbricks.getApi();
const personId = formbricks.getPerson()?.id;
+
return await api.client.response.create({
surveyId,
- personId,
+ personId: personId ?? "",
finished,
data,
});
diff --git a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx
index 0c6b37bbd3..92b906cbd2 100644
--- a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx
+++ b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx
@@ -58,6 +58,7 @@ export default function LinkSurvey({
new ResponseQueue(
{
apiHost: webAppUrl,
+ environmentId: survey.environmentId,
retryAttempts: 2,
onResponseSendingFailed: (response) => {
alert(`Failed to send response: ${JSON.stringify(response, null, 2)}`);
diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/events/index.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/events/index.ts
index 50b77b2d17..9e97d576cc 100644
--- a/apps/web/pages/api/v1/client/environments/[environmentId]/events/index.ts
+++ b/apps/web/pages/api/v1/client/environments/[environmentId]/events/index.ts
@@ -15,10 +15,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
}
// POST
else if (req.method === "POST") {
- const { sessionId, eventName, properties } = req.body;
+ const { personId, eventName, properties } = req.body;
- if (!sessionId) {
- return res.status(400).json({ message: "Missing sessionId" });
+ if (!personId) {
+ return res.status(400).json({ message: "Missing personId" });
}
if (!eventName) {
return res.status(400).json({ message: "Missing eventName" });
@@ -29,15 +29,15 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
eventType = "automatic";
}
- const eventData = await prisma.event.create({
+ const eventData = await prisma.action.create({
data: {
properties,
- session: {
+ person: {
connect: {
- id: sessionId,
+ id: personId,
},
},
- eventClass: {
+ actionClass: {
connectOrCreate: {
where: {
name_environmentId: {
diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts
deleted file mode 100644
index 7932b0007b..0000000000
--- a/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { getSettings } from "@/app/lib/api/clientSettings";
-import { prisma } from "@formbricks/database";
-import { personCache } from "@formbricks/lib/person/cache";
-import { deletePerson } from "@formbricks/lib/person/service";
-import type { NextApiRequest, NextApiResponse } from "next";
-
-export default async function handle(req: NextApiRequest, res: NextApiResponse) {
- const environmentId = req.query.environmentId?.toString();
-
- if (!environmentId) {
- return res.status(400).json({ message: "Missing environmentId" });
- }
-
- const personId = req.query.personId?.toString();
-
- if (!personId) {
- return res.status(400).json({ message: "Missing personId" });
- }
-
- // CORS
- if (req.method === "OPTIONS") {
- res.status(200).end();
- }
- // POST
- else if (req.method === "POST") {
- const { userId, sessionId } = req.body;
- if (!userId) {
- return res.status(400).json({ message: "Missing userId" });
- }
- if (!sessionId) {
- return res.status(400).json({ message: "Missing sessionId" });
- }
- let person;
- // check if person exists
- const existingPerson = await prisma.person.findFirst({
- where: {
- environmentId,
- attributes: {
- some: {
- attributeClass: {
- name: "userId",
- },
- value: userId,
- },
- },
- },
- select: {
- id: true,
- environmentId: true,
- attributes: {
- select: {
- id: true,
- value: true,
- attributeClass: {
- select: {
- id: true,
- name: true,
- },
- },
- },
- },
- },
- });
- // if person exists, reconnect session and delete old user
- if (existingPerson) {
- // reconnect session to new person
- await prisma.session.update({
- where: {
- id: sessionId,
- },
- data: {
- person: {
- connect: {
- id: existingPerson.id,
- },
- },
- },
- });
-
- // delete old person
- await deletePerson(personId);
- person = existingPerson;
- } else {
- // update person
- person = await prisma.person.update({
- where: {
- id: personId,
- },
- data: {
- attributes: {
- create: {
- value: userId,
- attributeClass: {
- connect: {
- name_environmentId: {
- name: "userId",
- environmentId,
- },
- },
- },
- },
- },
- },
- select: {
- id: true,
- environmentId: true,
- attributes: {
- select: {
- id: true,
- value: true,
- attributeClass: {
- select: {
- id: true,
- name: true,
- },
- },
- },
- },
- },
- });
-
- personCache.revalidate({
- id: person.id,
- environmentId: person.environmentId,
- });
- }
-
- personCache.revalidate({
- id: person.id,
- environmentId: person.environmentId,
- });
-
- const settings = await getSettings(environmentId, person.id);
-
- // return updated person and settings
- return res.json({ person, settings });
- }
-
- // Unknown HTTP Method
- else {
- throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
- }
-}
diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts
index 1602dde092..b05690ca5f 100644
--- a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts
+++ b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts
@@ -1,8 +1,8 @@
import { sendToPipeline } from "@/app/lib/pipelines";
import { prisma } from "@formbricks/database";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
+import { transformPrismaPerson } from "@formbricks/lib/person/service";
import { responseCache } from "@formbricks/lib/response/cache";
-import { TPerson } from "@formbricks/types/people";
import { TPipelineInput } from "@formbricks/types/pipelines";
import { TResponse } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
@@ -69,6 +69,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
person: {
select: {
id: true,
+ userId: true,
+ environmentId: true,
createdAt: true,
updatedAt: true,
attributes: {
@@ -122,21 +124,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
environmentId,
});
- const transformPrismaPerson = (person): TPerson => {
- const attributes = person.attributes.reduce((acc, attr) => {
- acc[attr.attributeClass.name] = attr.value;
- return acc;
- }, {} as Record);
-
- return {
- id: person.id,
- attributes: attributes,
- createdAt: person.createdAt,
- updatedAt: person.updatedAt,
- environmentId: environmentId,
- };
- };
-
const responseData: TResponse = {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts
index f61f69d08b..a0602937fa 100644
--- a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts
+++ b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts
@@ -1,8 +1,8 @@
import { sendToPipeline } from "@/app/lib/pipelines";
import { prisma } from "@formbricks/database";
+import { transformPrismaPerson } from "@formbricks/lib/person/service";
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
import { captureTelemetry } from "@formbricks/lib/telemetry";
-import { TPerson } from "@formbricks/types/people";
import { TResponse } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import type { NextApiRequest, NextApiResponse } from "next";
@@ -113,6 +113,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
person: {
select: {
id: true,
+ userId: true,
+ environmentId: true,
createdAt: true,
updatedAt: true,
attributes: {
@@ -159,21 +161,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
},
});
- const transformPrismaPerson = (person): TPerson => {
- const attributes = person.attributes.reduce((acc, attr) => {
- acc[attr.attributeClass.name] = attr.value;
- return acc;
- }, {} as Record);
-
- return {
- id: person.id,
- attributes: attributes,
- createdAt: person.createdAt,
- updatedAt: person.updatedAt,
- environmentId: environmentId,
- };
- };
-
const responseData: TResponse = {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/sessions/index.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/sessions/index.ts
deleted file mode 100644
index e6e14053b5..0000000000
--- a/apps/web/pages/api/v1/client/environments/[environmentId]/sessions/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { createSession } from "@/app/lib/api/clientSession";
-import { getSettings } from "@/app/lib/api/clientSettings";
-import type { NextApiRequest, NextApiResponse } from "next";
-
-export default async function handle(req: NextApiRequest, res: NextApiResponse) {
- const environmentId = req.query.environmentId?.toString();
-
- if (!environmentId) {
- return res.status(400).json({ message: "Missing environmentId" });
- }
-
- // CORS
- if (req.method === "OPTIONS") {
- res.status(200).end();
- }
- // GET
- else if (req.method === "POST") {
- const { personId } = req.body;
-
- if (!personId) {
- return res.status(400).json({ message: "Missing personId" });
- }
-
- try {
- const session = await createSession(personId);
- const settings = await getSettings(environmentId, personId);
-
- return res.json({ session, settings });
- } catch (error) {
- res.status(500).json({ message: error.message });
- }
- }
-
- // Unknown HTTP Method
- else {
- throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
- }
-}
diff --git a/packages/api/src/api/client/display.ts b/packages/api/src/api/client/display.ts
index 8e16c4ba65..5d7510e25a 100644
--- a/packages/api/src/api/client/display.ts
+++ b/packages/api/src/api/client/display.ts
@@ -5,19 +5,28 @@ import { TDisplay, TDisplayCreateInput, TDisplayUpdateInput } from "@formbricks/
export class DisplayAPI {
private apiHost: string;
+ private environmentId: string;
- constructor(baseUrl: string) {
+ constructor(baseUrl: string, environmentId: string) {
this.apiHost = baseUrl;
+ this.environmentId = environmentId;
}
- async create(displayInput: TDisplayCreateInput): Promise> {
- return makeRequest(this.apiHost, "/api/v1/client/displays", "POST", displayInput);
+ async create(
+ displayInput: Omit
+ ): Promise> {
+ return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput);
}
async update(
displayId: string,
displayInput: TDisplayUpdateInput
): Promise> {
- return makeRequest(this.apiHost, `/api/v1/client/displays/${displayId}`, "PUT", displayInput);
+ return makeRequest(
+ this.apiHost,
+ `/api/v1/client/${this.environmentId}/displays/${displayId}`,
+ "PUT",
+ displayInput
+ );
}
}
diff --git a/packages/api/src/api/client/index.ts b/packages/api/src/api/client/index.ts
index 9df02a5e48..f51842f98d 100644
--- a/packages/api/src/api/client/index.ts
+++ b/packages/api/src/api/client/index.ts
@@ -7,9 +7,9 @@ export class Client {
display: DisplayAPI;
constructor(options: ApiConfig) {
- const { apiHost } = options;
+ const { apiHost, environmentId } = options;
- this.response = new ResponseAPI(apiHost);
- this.display = new DisplayAPI(apiHost);
+ this.response = new ResponseAPI(apiHost, environmentId);
+ this.display = new DisplayAPI(apiHost, environmentId);
}
}
diff --git a/packages/api/src/api/client/response.ts b/packages/api/src/api/client/response.ts
index a6ca16fc43..c3a810c3f3 100644
--- a/packages/api/src/api/client/response.ts
+++ b/packages/api/src/api/client/response.ts
@@ -7,13 +7,15 @@ type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: s
export class ResponseAPI {
private apiHost: string;
+ private environmentId: string;
- constructor(apiHost: string) {
+ constructor(apiHost: string, environmentId: string) {
this.apiHost = apiHost;
+ this.environmentId = environmentId;
}
async create(responseInput: TResponseInput): Promise> {
- return makeRequest(this.apiHost, "/api/v1/client/responses", "POST", responseInput);
+ return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
}
async update({
@@ -21,7 +23,7 @@ export class ResponseAPI {
finished,
data,
}: TResponseUpdateInputWithResponseId): Promise> {
- return makeRequest(this.apiHost, `/api/v1/client/responses/${responseId}`, "PUT", {
+ return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
finished,
data,
});
diff --git a/packages/database/jsonTypes.ts b/packages/database/jsonTypes.ts
index 5e72b359a1..4f5e0cbe65 100644
--- a/packages/database/jsonTypes.ts
+++ b/packages/database/jsonTypes.ts
@@ -17,8 +17,8 @@ import { TUserNotificationSettings } from "@formbricks/types/users";
declare global {
namespace PrismaJson {
- export type EventProperties = { [key: string]: string };
- export type EventClassNoCodeConfig = TActionClassNoCodeConfig;
+ export type ActionProperties = { [key: string]: string };
+ export type ActionClassNoCodeConfig = TActionClassNoCodeConfig;
export type IntegrationConfig = TIntegrationConfig;
export type ResponseData = TResponseData;
export type ResponseMeta = TResponseMeta;
diff --git a/packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql b/packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql
new file mode 100644
index 0000000000..ec71ceb81a
--- /dev/null
+++ b/packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql
@@ -0,0 +1,82 @@
+/*
+ Warnings:
+
+ - You are about to drop the `Event` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "Event" DROP CONSTRAINT "Event_eventClassId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "Event" DROP CONSTRAINT "Event_sessionId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "Session" DROP CONSTRAINT "Session_personId_fkey";
+
+-- DropTable
+DROP TABLE "Event";
+
+-- DropTable
+DROP TABLE "Session";
+
+ALTER TABLE "EventClass" RENAME TO "ActionClass";
+
+-- AlterTable
+ALTER TABLE "ActionClass" RENAME CONSTRAINT "EventClass_pkey" TO "ActionClass_pkey";
+
+-- RenameForeignKey
+ALTER TABLE "ActionClass" RENAME CONSTRAINT "EventClass_environmentId_fkey" TO "ActionClass_environmentId_fkey";
+
+-- RenameIndex
+ALTER INDEX "EventClass_name_environmentId_key" RENAME TO "ActionClass_name_environmentId_key";
+
+-- CreateTable
+CREATE TABLE "Action" (
+ "id" TEXT NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "actionClassId" TEXT NOT NULL,
+ "personId" TEXT NOT NULL,
+ "properties" JSONB NOT NULL DEFAULT '{}',
+
+ CONSTRAINT "Action_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "Action" ADD CONSTRAINT "Action_actionClassId_fkey" FOREIGN KEY ("actionClassId") REFERENCES "ActionClass"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Action" ADD CONSTRAINT "Action_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE "SurveyTrigger" RENAME COLUMN "eventClassId" TO "actionClassId";
+
+-- RenameForeignKey
+ALTER TABLE "SurveyTrigger" RENAME CONSTRAINT "SurveyTrigger_eventClassId_fkey" TO "SurveyTrigger_actionClassId_fkey";
+
+-- RenameIndex
+ALTER INDEX "SurveyTrigger_surveyId_eventClassId_key" RENAME TO "SurveyTrigger_surveyId_actionClassId_key";
+
+ALTER TYPE "EventType" RENAME TO "ActionType";
+
+-- CreateIndex
+CREATE INDEX "Action_personId_idx" ON "Action"("personId");
+
+-- CreateIndex
+CREATE INDEX "Action_actionClassId_idx" ON "Action"("actionClassId");
+
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[environmentId,userId]` on the table `Person` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "Person" ADD COLUMN "userId" SERIAL NOT NULL;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Person_environmentId_userId_key" ON "Person"("environmentId", "userId");
+
+-- AlterTable
+ALTER TABLE "Person" ALTER COLUMN "userId" DROP DEFAULT,
+ALTER COLUMN "userId" SET DATA TYPE TEXT;
+DROP SEQUENCE "Person_userId_seq";
diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma
index 699dd33c66..02613b7e2c 100644
--- a/packages/database/schema.prisma
+++ b/packages/database/schema.prisma
@@ -89,15 +89,17 @@ model AttributeClass {
model Person {
id String @id @default(cuid())
+ userId String
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
responses Response[]
- sessions Session[]
attributes Attribute[]
displays Display[]
+ actions Action[]
+ @@unique([environmentId, userId])
@@index([environmentId])
}
@@ -195,15 +197,15 @@ model Display {
}
model SurveyTrigger {
- id String @id @default(cuid())
- createdAt DateTime @default(now()) @map(name: "created_at")
- updatedAt DateTime @updatedAt @map(name: "updated_at")
- survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
- surveyId String
- eventClass EventClass @relation(fields: [eventClassId], references: [id], onDelete: Cascade)
- eventClassId String
+ id String @id @default(cuid())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
+ surveyId String
+ actionClass ActionClass @relation(fields: [actionClassId], references: [id], onDelete: Cascade)
+ actionClassId String
- @@unique([surveyId, eventClassId])
+ @@unique([surveyId, actionClassId])
@@index([surveyId])
}
@@ -296,51 +298,44 @@ model Survey {
@@index([environmentId])
}
-model Event {
- id String @id @default(cuid())
- createdAt DateTime @default(now()) @map(name: "created_at")
- eventClass EventClass? @relation(fields: [eventClassId], references: [id])
- eventClassId String?
- session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
- sessionId String
- /// @zod.custom(imports.ZEventProperties)
- /// @zod.custom(imports.ZEventProperties)
- /// [EventProperties]
- properties Json @default("{}")
-}
-
-enum EventType {
+enum ActionType {
code
noCode
automatic
}
-model EventClass {
+model ActionClass {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
description String?
- type EventType
- events Event[]
+ type ActionType
/// @zod.custom(imports.ZActionClassNoCodeConfig)
- /// [EventClassNoCodeConfig]
+ /// [ActionClassNoCodeConfig]
noCodeConfig Json?
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
surveys SurveyTrigger[]
+ actions Action[]
@@unique([name, environmentId])
}
-model Session {
- id String @id @default(cuid())
- createdAt DateTime @default(now()) @map(name: "created_at")
- updatedAt DateTime @updatedAt @map(name: "updated_at")
- expiresAt DateTime @default(now())
- person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
- personId String
- events Event[]
+model Action {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ actionClass ActionClass @relation(fields: [actionClassId], references: [id], onDelete: Cascade)
+ actionClassId String
+ person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
+ personId String
+ /// @zod.custom(imports.ZActionProperties)
+ /// @zod.custom(imports.ZActionProperties)
+ /// [ActionProperties]
+ properties Json @default("{}")
+
+ @@index([personId])
+ @@index([actionClassId])
}
enum EnvironmentType {
@@ -376,7 +371,7 @@ model Environment {
widgetSetupCompleted Boolean @default(false)
surveys Survey[]
people Person[]
- eventClasses EventClass[]
+ actionClasses ActionClass[]
attributeClasses AttributeClass[]
apiKeys ApiKey[]
webhooks Webhook[]
diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts
index 473d0c9a76..3050bdc8bd 100644
--- a/packages/database/zod-utils.ts
+++ b/packages/database/zod-utils.ts
@@ -1,6 +1,6 @@
import z from "zod";
-export const ZEventProperties = z.record(z.string());
+export const ZActionProperties = z.record(z.string());
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
export { ZIntegrationConfig } from "@formbricks/types/integration";
diff --git a/packages/js/index.html b/packages/js/index.html
index 7838db2db8..3de01e695f 100644
--- a/packages/js/index.html
+++ b/packages/js/index.html
@@ -4,7 +4,7 @@
var t = document.createElement("script");
(t.type = "text/javascript"),
(t.async = !0),
- (t.src = "https://unpkg.com/@formbricks/js@^1.1.4/dist/index.umd.js");
+ (t.src = "https://unpkg.com/@formbricks/js@^1.2.0/dist/index.umd.js");
var e = document.getElementsByTagName("script")[0];
e.parentNode.insertBefore(t, e),
setTimeout(function () {
diff --git a/packages/js/package.json b/packages/js/package.json
index d93907a575..4db7ddb6de 100644
--- a/packages/js/package.json
+++ b/packages/js/package.json
@@ -1,7 +1,7 @@
{
"name": "@formbricks/js",
"license": "MIT",
- "version": "1.1.5",
+ "version": "1.2.0",
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
"keywords": [
"Formbricks",
diff --git a/packages/js/src/index.ts b/packages/js/src/index.ts
index a6a68709e7..89dc6ef803 100644
--- a/packages/js/src/index.ts
+++ b/packages/js/src/index.ts
@@ -19,8 +19,8 @@ const init = async (initConfig: TJsConfigInput) => {
await queue.wait();
};
-const setUserId = async (userId: string | number): Promise => {
- queue.add(true, setPersonUserId, userId);
+const setUserId = async (): Promise => {
+ queue.add(true, setPersonUserId);
await queue.wait();
};
diff --git a/packages/js/src/lib/actions.ts b/packages/js/src/lib/actions.ts
index 85597c7443..22b972ac11 100644
--- a/packages/js/src/lib/actions.ts
+++ b/packages/js/src/lib/actions.ts
@@ -14,13 +14,15 @@ export const trackAction = async (
): Promise> => {
const input: TJsActionInput = {
environmentId: config.get().environmentId,
- sessionId: config.get().state?.session?.id ?? "",
+ userId: config.get().state?.person?.userId,
name,
properties: properties || {},
};
- if (!intentsToNotCreateOnApp.includes(name)) {
- const res = await fetch(`${config.get().apiHost}/api/v1/js/actions`, {
+ // don't send actions to the backend if the person is not identified
+ if (config.get().state?.person?.userId && !intentsToNotCreateOnApp.includes(name)) {
+ logger.debug(`Sending action "${name}" to backend`);
+ const res = await fetch(`${config.get().apiHost}/api/v1/client/${config.get().environmentId}/actions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -34,7 +36,7 @@ export const trackAction = async (
return err({
code: "network_error",
- message: `Error tracking event: ${JSON.stringify(error)}`,
+ message: `Error tracking action: ${JSON.stringify(error)}`,
status: res.status,
url: res.url,
responseMessage: error.message,
@@ -42,7 +44,7 @@ export const trackAction = async (
}
}
- logger.debug(`Formbricks: Event "${name}" tracked`);
+ logger.debug(`Formbricks: Action "${name}" tracked`);
// get a list of surveys that are collecting insights
const activeSurveys = config.get().state?.surveys;
diff --git a/packages/js/src/lib/config.ts b/packages/js/src/lib/config.ts
index a08ca95757..d59bd53266 100644
--- a/packages/js/src/lib/config.ts
+++ b/packages/js/src/lib/config.ts
@@ -1,4 +1,4 @@
-import { TJsConfig } from "@formbricks/types/js";
+import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js";
import { Result, err, ok, wrapThrows } from "./errors";
export const LOCAL_STORAGE_KEY = "formbricks-js";
@@ -14,11 +14,14 @@ export class Config {
return Config.instance;
}
- public update(newConfig: TJsConfig): void {
+ public update(newConfig: TJsConfigUpdateInput): void {
if (newConfig) {
+ const expiresAt = new Date(new Date().getTime() + 15 * 60000); // 15 minutes in the future
+
this.config = {
...this.config,
...newConfig,
+ expiresAt,
};
this.saveToLocalStorage();
@@ -39,6 +42,13 @@ export class Config {
// TODO: validate config
// This is a hack to get around the fact that we don't have a proper
// way to validate the config yet.
+ const parsedConfig = JSON.parse(savedConfig) as TJsConfig;
+
+ // check if the config has expired
+ if (parsedConfig.expiresAt && new Date(parsedConfig.expiresAt) <= new Date()) {
+ return err(new Error("Config in local storage has expired"));
+ }
+
return ok(JSON.parse(savedConfig) as TJsConfig);
}
}
diff --git a/packages/js/src/lib/eventListeners.ts b/packages/js/src/lib/eventListeners.ts
index 88979f0da8..22f8631b4e 100644
--- a/packages/js/src/lib/eventListeners.ts
+++ b/packages/js/src/lib/eventListeners.ts
@@ -10,12 +10,12 @@ import {
removeClickEventListener,
removePageUrlEventListeners,
} from "./noCodeActions";
-import { addSyncEventListener, removeSyncEventListener } from "./sync";
+import { addExpiryCheckListener, removeExpiryCheckListener } from "./sync";
let areRemoveEventListenersAdded = false;
-export const addEventListeners = (debug: boolean = false): void => {
- addSyncEventListener(debug);
+export const addEventListeners = (): void => {
+ addExpiryCheckListener();
addPageUrlEventListeners();
addClickEventListener();
addExitIntentListener();
@@ -25,7 +25,7 @@ export const addEventListeners = (debug: boolean = false): void => {
export const addCleanupEventListeners = (): void => {
if (areRemoveEventListenersAdded) return;
window.addEventListener("beforeunload", () => {
- removeSyncEventListener();
+ removeExpiryCheckListener();
removePageUrlEventListeners();
removeClickEventListener();
removeExitIntentListener();
@@ -37,7 +37,7 @@ export const addCleanupEventListeners = (): void => {
export const removeCleanupEventListeners = (): void => {
if (!areRemoveEventListenersAdded) return;
window.removeEventListener("beforeunload", () => {
- removeSyncEventListener();
+ removeExpiryCheckListener();
removePageUrlEventListeners();
removeClickEventListener();
removeExitIntentListener();
@@ -47,7 +47,7 @@ export const removeCleanupEventListeners = (): void => {
};
export const removeAllEventListeners = (): void => {
- removeSyncEventListener();
+ removeExpiryCheckListener();
removePageUrlEventListeners();
removeClickEventListener();
removeExitIntentListener();
diff --git a/packages/js/src/lib/initialize.ts b/packages/js/src/lib/initialize.ts
index 39da52325a..fc57399ee9 100644
--- a/packages/js/src/lib/initialize.ts
+++ b/packages/js/src/lib/initialize.ts
@@ -13,10 +13,9 @@ import {
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
import { Logger } from "./logger";
import { checkPageUrl } from "./noCodeActions";
-import { resetPerson } from "./person";
-import { isExpired } from "./session";
import { sync } from "./sync";
import { addWidgetContainer } from "./widget";
+import { trackAction } from "./actions";
const config = Config.getInstance();
const logger = Logger.getInstance();
@@ -70,46 +69,40 @@ export const initialize = async (
localConfigResult.ok &&
localConfigResult.value.state &&
localConfigResult.value.environmentId === c.environmentId &&
- localConfigResult.value.apiHost === c.apiHost
+ localConfigResult.value.apiHost === c.apiHost &&
+ localConfigResult.value.state?.person?.userId === c.userId &&
+ localConfigResult.value.expiresAt // only accept config when they follow new config version with expiresAt
) {
- const { state, apiHost, environmentId } = localConfigResult.value;
-
- logger.debug("Found existing configuration. Checking session.");
- const existingSession = state.session;
-
- config.update(localConfigResult.value);
-
- if (isExpired(existingSession)) {
- logger.debug("Session expired. Resyncing.");
-
- try {
- await sync({
- apiHost,
- environmentId,
- personId: state.person.id,
- sessionId: existingSession.id,
- });
- } catch (e) {
- logger.debug("Sync failed. Clearing config and starting from scratch.");
- await resetPerson();
- return await initialize(c);
- }
+ logger.debug("Found existing configuration.");
+ if (localConfigResult.value.expiresAt < new Date()) {
+ logger.debug("Configuration expired.");
+ await sync({
+ apiHost: c.apiHost,
+ environmentId: c.environmentId,
+ userId: c.userId,
+ });
} else {
- logger.debug("Session valid. Continuing.");
- // continue for now - next sync will check complete state
+ logger.debug("Configuration not expired. Extending expiration.");
+ config.update(localConfigResult.value);
}
} else {
- logger.debug("No valid configuration found. Creating new config.");
-
+ logger.debug("No valid configuration found or it has been expired. Creating new config.");
logger.debug("Syncing.");
+
+ // when the local storage is expired / empty, we sync to get the latest config
+
await sync({
apiHost: c.apiHost,
environmentId: c.environmentId,
+ userId: c.userId,
});
+
+ // and track the new session event
+ trackAction("New Session");
}
logger.debug("Adding event listeners");
- addEventListeners(c.debug);
+ addEventListeners();
addCleanupEventListeners();
isInitialized = true;
diff --git a/packages/js/src/lib/person.ts b/packages/js/src/lib/person.ts
index fa80289f51..e2dd443a56 100644
--- a/packages/js/src/lib/person.ts
+++ b/packages/js/src/lib/person.ts
@@ -1,4 +1,4 @@
-import { TJsPeopleAttributeInput, TJsPeopleUserIdInput, TJsState } from "@formbricks/types/js";
+import { TJsPeopleAttributeInput, TJsState } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { Config } from "./config";
import {
@@ -16,51 +16,11 @@ import { Logger } from "./logger";
const config = Config.getInstance();
const logger = Logger.getInstance();
-export const updatePersonUserId = async (
- userId: string
-): Promise> => {
- if (!config.get().state.person || !config.get().state.person.id)
- return err({
- code: "missing_person",
- message: "Unable to update userId. No person set.",
- });
-
- const url = `${config.get().apiHost}/api/v1/js/people/${config.get().state.person.id}/set-user-id`;
-
- const input: TJsPeopleUserIdInput = {
- environmentId: config.get().environmentId,
- userId,
- sessionId: config.get().state.session.id,
- };
-
- const res = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(input),
- });
-
- const jsonRes = await res.json();
-
- if (!res.ok) {
- return err({
- code: "network_error",
- message: "Error updating person",
- status: res.status,
- url,
- responseMessage: jsonRes.message,
- });
- }
-
- return ok(jsonRes.data as TJsState);
-};
-
export const updatePersonAttribute = async (
key: string,
value: string
): Promise> => {
- if (!config.get().state.person || !config.get().state.person.id) {
+ if (!config.get().state.person || !config.get().state.person?.id) {
return err({
code: "missing_person",
message: "Unable to update attribute. No person set.",
@@ -68,14 +28,14 @@ export const updatePersonAttribute = async (
}
const input: TJsPeopleAttributeInput = {
- environmentId: config.get().environmentId,
- sessionId: config.get().state.session.id,
key,
value,
};
const res = await fetch(
- `${config.get().apiHost}/api/v1/js/people/${config.get().state.person.id}/set-attribute`,
+ `${config.get().apiHost}/api/v1/client/${config.get().environmentId}/people/${
+ config.get().state.person?.id
+ }/set-attribute`,
{
method: "POST",
headers: {
@@ -114,33 +74,10 @@ export const hasAttributeKey = (key: string): boolean => {
return false;
};
-export const setPersonUserId = async (
- userId: string | number
-): Promise> => {
- logger.debug("setting userId: " + userId);
- // check if attribute already exists with this value
- if (hasAttributeValue("userId", userId.toString())) {
- logger.debug("userId already set to this value. Skipping update.");
- return okVoid();
- }
- if (hasAttributeKey("userId")) {
- return err({
- code: "attribute_already_exists",
- message: "userId cannot be changed after it has been set. You need to reset first",
- });
- }
- const result = await updatePersonUserId(userId.toString());
-
- if (result.ok !== true) return err(result.error);
-
- const state = result.value;
-
- config.update({
- apiHost: config.get().apiHost,
- environmentId: config.get().environmentId,
- state,
- });
-
+export const setPersonUserId = async (): Promise<
+ Result
+> => {
+ logger.error("'setUserId' is no longer supported. Please set the userId in the init call instead.");
return okVoid();
};
@@ -181,6 +118,7 @@ export const resetPerson = async (): Promise> => {
const syncParams = {
environmentId: config.get().environmentId,
apiHost: config.get().apiHost,
+ userId: config.get().state?.person?.userId,
};
await logoutPerson();
try {
@@ -191,6 +129,6 @@ export const resetPerson = async (): Promise> => {
}
};
-export const getPerson = (): TPerson => {
+export const getPerson = (): TPerson | null => {
return config.get().state.person;
};
diff --git a/packages/js/src/lib/session.ts b/packages/js/src/lib/session.ts
deleted file mode 100644
index 669c02da75..0000000000
--- a/packages/js/src/lib/session.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { TSession } from "@formbricks/types/sessions";
-
-export const isExpired = (session: TSession): boolean => {
- if (!session) return true;
- return session.expiresAt < new Date();
-};
diff --git a/packages/js/src/lib/sync.ts b/packages/js/src/lib/sync.ts
index 94e78ad028..deb23d85f7 100644
--- a/packages/js/src/lib/sync.ts
+++ b/packages/js/src/lib/sync.ts
@@ -1,35 +1,51 @@
import { TJsState, TJsSyncParams } from "@formbricks/types/js";
-import { trackAction } from "./actions";
import { Config } from "./config";
import { NetworkError, Result, err, ok } from "./errors";
import { Logger } from "./logger";
-import packageJson from "../../package.json";
const config = Config.getInstance();
const logger = Logger.getInstance();
let syncIntervalId: number | null = null;
+const diffInDays = (date1: Date, date2: Date) => {
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
+ return Math.floor(diffTime / (1000 * 60 * 60 * 24));
+};
+
const syncWithBackend = async ({
apiHost,
environmentId,
- personId,
- sessionId,
+ userId,
}: TJsSyncParams): Promise> => {
- const url = `${apiHost}/api/v1/js/sync`;
+ const url = `${apiHost}/api/v1/client/${environmentId}/in-app/sync/${userId}`;
+ const publicUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
+
+ // if user id is available
+
+ if (!userId) {
+ // public survey
+ const response = await fetch(publicUrl);
+
+ if (!response.ok) {
+ const jsonRes = await response.json();
+
+ return err({
+ code: "network_error",
+ status: response.status,
+ message: "Error syncing with backend",
+ url,
+ responseMessage: jsonRes.message,
+ });
+ }
+
+ return ok((await response.json()).data as TJsState);
+ }
+
+ // userId is available, call the api with the `userId` param
+
+ const response = await fetch(url);
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- environmentId,
- personId,
- sessionId,
- jsVersion: packageJson.version,
- }),
- });
if (!response.ok) {
const jsonRes = await response.json();
@@ -42,7 +58,10 @@ const syncWithBackend = async ({
});
}
- return ok((await response.json()).data as TJsState);
+ const data = await response.json();
+ const { data: state } = data;
+
+ return ok(state as TJsState);
};
export const sync = async (params: TJsSyncParams): Promise => {
@@ -50,7 +69,7 @@ export const sync = async (params: TJsSyncParams): Promise => {
const syncResult = await syncWithBackend(params);
if (syncResult?.ok !== true) {
logger.error(`Sync failed: ${JSON.stringify(syncResult.error)}`);
- return;
+ throw syncResult.error;
}
const state = syncResult.value;
@@ -60,42 +79,112 @@ export const sync = async (params: TJsSyncParams): Promise => {
} catch (e) {
// ignore error
}
+
config.update({
apiHost: params.apiHost,
environmentId: params.environmentId,
state,
});
- const surveyNames = state.surveys.map((s) => s.name);
- logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
- // if session is new, track action
- if (!oldState?.session || oldState.session.id !== state.session.id) {
- const trackActionResult = await trackAction("New Session");
- if (trackActionResult.ok !== true) {
- logger.error(`Action tracking failed: ${trackActionResult.error}`);
- }
+ // before finding the surveys, check for public use
+
+ if (!state.person?.id) {
+ // unidentified user
+ // set the displays and filter out surveys
+ const publicState = {
+ ...state,
+ displays: oldState?.displays || [],
+ };
+
+ const filteredState = filterPublicSurveys(publicState);
+
+ // update config
+ config.update({
+ apiHost: params.apiHost,
+ environmentId: params.environmentId,
+ state: filteredState,
+ });
+
+ const surveyNames = filteredState.surveys.map((s) => s.name);
+ logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
+ } else {
+ const surveyNames = state.surveys.map((s) => s.name);
+ logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
}
} catch (error) {
logger.error(`Error during sync: ${error}`);
+ throw error;
}
};
-export const addSyncEventListener = (debug: boolean = false): void => {
- const updateInterval = debug ? 1000 * 60 : 1000 * 60 * 5; // 5 minutes in production, 1 minute in debug mode
+export const filterPublicSurveys = (state: TJsState): TJsState => {
+ const { displays, product } = state;
+
+ let { surveys } = state;
+
+ if (!displays) {
+ return state;
+ }
+
+ // filter surveys that meet the displayOption criteria
+ let filteredSurveys = surveys.filter((survey) => {
+ if (survey.displayOption === "respondMultiple") {
+ return true;
+ } else if (survey.displayOption === "displayOnce") {
+ return displays.filter((display) => display.surveyId === survey.id).length === 0;
+ } else if (survey.displayOption === "displayMultiple") {
+ return displays.filter((display) => display.surveyId === survey.id && display.responded).length === 0;
+ } else {
+ throw Error("Invalid displayOption");
+ }
+ });
+
+ const latestDisplay = displays.length > 0 ? displays[displays.length - 1] : undefined;
+
+ // filter surveys that meet the recontactDays criteria
+ filteredSurveys = filteredSurveys.filter((survey) => {
+ if (!latestDisplay) {
+ return true;
+ } else if (survey.recontactDays !== null) {
+ const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
+ if (!lastDisplaySurvey) {
+ return true;
+ }
+ return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
+ } else if (product.recontactDays !== null) {
+ return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
+ } else {
+ return true;
+ }
+ });
+
+ return {
+ ...state,
+ surveys: filteredSurveys,
+ };
+};
+
+export const addExpiryCheckListener = (): void => {
+ const updateInterval = 1000 * 60; // every minute
// add event listener to check sync with backend on regular interval
if (typeof window !== "undefined" && syncIntervalId === null) {
syncIntervalId = window.setInterval(async () => {
+ // check if the config has not expired yet
+ if (config.get().expiresAt && new Date(config.get().expiresAt) >= new Date()) {
+ return;
+ }
+ logger.debug("Config has expired. Starting sync.");
await sync({
apiHost: config.get().apiHost,
environmentId: config.get().environmentId,
- personId: config.get().state?.person?.id,
- sessionId: config.get().state?.session?.id,
+ userId: config.get().state?.person?.userId,
+ // personId: config.get().state?.person?.id,
});
}, updateInterval);
}
};
-export const removeSyncEventListener = (): void => {
+export const removeExpiryCheckListener = (): void => {
if (typeof window !== "undefined" && syncIntervalId !== null) {
window.clearInterval(syncIntervalId);
diff --git a/packages/js/src/lib/widget.ts b/packages/js/src/lib/widget.ts
index 0a8291b09e..8540943cba 100644
--- a/packages/js/src/lib/widget.ts
+++ b/packages/js/src/lib/widget.ts
@@ -1,12 +1,12 @@
import { ResponseQueue } from "@formbricks/lib/responseQueue";
import SurveyState from "@formbricks/lib/surveyState";
import { renderSurveyModal } from "@formbricks/surveys";
-import { TSurveyWithTriggers } from "@formbricks/types/js";
+import { TJSStateDisplay, TSurveyWithTriggers } from "@formbricks/types/js";
import { TResponseUpdate } from "@formbricks/types/responses";
import { Config } from "./config";
import { ErrorHandler } from "./errors";
import { Logger } from "./logger";
-import { sync } from "./sync";
+import { filterPublicSurveys, sync } from "./sync";
import { FormbricksAPI } from "@formbricks/api";
const containerId = "formbricks-web-container";
@@ -33,6 +33,7 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
const responseQueue = new ResponseQueue(
{
apiHost: config.get().apiHost,
+ environmentId: config.get().environmentId,
retryAttempts: 2,
onResponseSendingFailed: (response) => {
alert(`Failed to send response: ${JSON.stringify(response, null, 2)}`);
@@ -58,13 +59,33 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
highlightBorderColor,
placement,
onDisplay: async () => {
+ // if config does not have a person, we store the displays in local storage
+ if (!config.get().state.person || !config.get().state.person?.userId) {
+ const localDisplay: TJSStateDisplay = {
+ createdAt: new Date(),
+ surveyId: survey.id,
+ responded: false,
+ };
+
+ const existingDisplays = config.get().state.displays;
+ const displays = existingDisplays ? [...existingDisplays, localDisplay] : [localDisplay];
+ const previousConfig = config.get();
+ config.update({
+ ...previousConfig,
+ state: {
+ ...previousConfig.state,
+ displays,
+ },
+ });
+ }
+
const api = new FormbricksAPI({
apiHost: config.get().apiHost,
environmentId: config.get().environmentId,
});
const res = await api.client.display.create({
surveyId: survey.id,
- personId: config.get().state.person.id,
+ userId: config.get().state.person?.userId,
});
if (!res.ok) {
throw new Error("Could not create display");
@@ -75,7 +96,29 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
responseQueue.updateSurveyState(surveyState);
},
onResponse: (responseUpdate: TResponseUpdate) => {
- surveyState.updatePersonId(config.get().state.person.id);
+ // if user is unidentified, update the display in local storage if not already updated
+ if (!config.get().state.person || !config.get().state.person?.userId) {
+ const displays = config.get().state.displays;
+ const lastDisplay = displays && displays[displays.length - 1];
+ if (!lastDisplay) {
+ throw new Error("No lastDisplay found");
+ }
+ if (!lastDisplay.responded) {
+ lastDisplay.responded = true;
+ const previousConfig = config.get();
+ config.update({
+ ...previousConfig,
+ state: {
+ ...previousConfig.state,
+ displays,
+ },
+ });
+ }
+ }
+
+ if (config.get().state.person && config.get().state.person?.id) {
+ surveyState.updatePersonId(config.get().state.person?.id!);
+ }
responseQueue.updateSurveyState(surveyState);
responseQueue.add({
data: responseUpdate.data,
@@ -92,12 +135,24 @@ export const closeSurvey = async (): Promise => {
document.getElementById(containerId)?.remove();
addWidgetContainer();
+ // if unidentified user, refilter the surveys
+ if (!config.get().state.person || !config.get().state.person?.userId) {
+ const state = config.get().state;
+ const updatedState = filterPublicSurveys(state);
+ config.update({
+ ...config.get(),
+ state: updatedState,
+ });
+ surveyRunning = false;
+ return;
+ }
+
+ // for identified users we sync to get the latest surveys
try {
await sync({
apiHost: config.get().apiHost,
environmentId: config.get().environmentId,
- personId: config.get().state.person?.id,
- sessionId: config.get().state.session?.id,
+ userId: config.get().state?.person?.userId,
});
surveyRunning = false;
} catch (e) {
diff --git a/packages/js/tests/__mocks__/apiMock.ts b/packages/js/tests/__mocks__/apiMock.ts
index a65a4e2133..ddbd1ab833 100644
--- a/packages/js/tests/__mocks__/apiMock.ts
+++ b/packages/js/tests/__mocks__/apiMock.ts
@@ -32,12 +32,6 @@ export const mockInitResponse = () => {
updatedAt: "2021-03-09T15:00:00.000Z",
attributes: {},
},
- session: {
- id: sessionId,
- createdAt: "2021-03-09T15:00:00.000Z",
- updatedAt: "2021-03-09T15:00:00.000Z",
- expiresAt: expiryTime,
- },
surveys: [
{
id: surveyId,
@@ -171,12 +165,6 @@ export const mockUpdateEmailResponse = () => {
data: {
surveys: [],
noCodeActionClasses: [],
- session: {
- id: sessionId,
- createdAt: "2021-03-09T15:00:00.000Z",
- updatedAt: "2021-03-09T15:00:00.000Z",
- expiresAt: expiryTime,
- },
person: {
id: initialPersonUid,
environmentId,
@@ -218,10 +206,7 @@ export const mockResetResponse = () => {
fetchMock.mockResponseOnce(
JSON.stringify({
data: {
- settings: {
- surveys: [],
- noCodeEvents: [],
- },
+ surveys: [],
person: {
id: newPersonUid,
environmentId,
diff --git a/packages/js/tests/index.test.ts b/packages/js/tests/index.test.ts
index c9ff9db3fa..6b032880ed 100644
--- a/packages/js/tests/index.test.ts
+++ b/packages/js/tests/index.test.ts
@@ -41,6 +41,7 @@ test("Formbricks should Initialise", async () => {
await formbricks.init({
environmentId,
apiHost,
+ userId: initialUserId,
});
const configFromBrowser = localStorage.getItem("formbricks-js");
@@ -60,20 +61,6 @@ test("Formbricks should get the current person with no attributes", () => {
expect(Object.keys(currentStatePersonAttributes)).toHaveLength(0);
});
-test("Formbricks should set userId", async () => {
- mockSetUserIdResponse();
- await formbricks.setUserId(initialUserId);
-
- const currentStatePerson = formbricks.getPerson();
-
- const currentStatePersonAttributes = currentStatePerson.attributes;
- const numberOfUserAttributes = Object.keys(currentStatePersonAttributes).length;
- expect(numberOfUserAttributes).toStrictEqual(1);
-
- const userId = currentStatePersonAttributes.userId;
- expect(userId).toStrictEqual(initialUserId);
-});
-
test("Formbricks should set email", async () => {
mockSetEmailIdResponse();
await formbricks.setEmail(initialUserEmail);
diff --git a/packages/lib/action/cache.ts b/packages/lib/action/cache.ts
index 7d3ad66628..64859901c0 100644
--- a/packages/lib/action/cache.ts
+++ b/packages/lib/action/cache.ts
@@ -14,9 +14,12 @@ export const actionCache = {
return `environments-${personId}-actions`;
},
},
- revalidate({ environmentId }: RevalidateProps): void {
+ revalidate({ environmentId, personId }: RevalidateProps): void {
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
+ if (personId) {
+ revalidateTag(this.tag.byPersonId(personId));
+ }
},
};
diff --git a/packages/lib/action/service.ts b/packages/lib/action/service.ts
index de83bb8565..95fd0b21cf 100644
--- a/packages/lib/action/service.ts
+++ b/packages/lib/action/service.ts
@@ -5,15 +5,15 @@ import { TActionClassType } from "@formbricks/types/actionClasses";
import { TAction, TActionInput, ZActionInput } from "@formbricks/types/actions";
import { ZOptionalNumber } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
-import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { DatabaseError } from "@formbricks/types/errors";
import { Prisma } from "@prisma/client";
-import { revalidateTag, unstable_cache } from "next/cache";
+import { unstable_cache } from "next/cache";
import { actionClassCache } from "../actionClass/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
-import { getSession } from "../session/service";
import { createActionClass, getActionClassByEnvironmentIdAndName } from "../actionClass/service";
import { validateInputs } from "../utils/validate";
import { actionCache } from "./cache";
+import { getPersonByUserId } from "../person/service";
export const getLatestActionByEnvironmentId = async (environmentId: string): Promise => {
const action = await unstable_cache(
@@ -21,9 +21,9 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
validateInputs([environmentId, ZId]);
try {
- const actionPrisma = await prisma.event.findFirst({
+ const actionPrisma = await prisma.action.findFirst({
where: {
- eventClass: {
+ actionClass: {
environmentId: environmentId,
},
},
@@ -31,7 +31,7 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
createdAt: "desc",
},
include: {
- eventClass: true,
+ actionClass: true,
},
});
if (!actionPrisma) {
@@ -40,9 +40,10 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
const action: TAction = {
id: actionPrisma.id,
createdAt: actionPrisma.createdAt,
- sessionId: actionPrisma.sessionId,
+ // sessionId: actionPrisma.sessionId,
+ personId: actionPrisma.personId,
properties: actionPrisma.properties,
- actionClass: actionPrisma.eventClass,
+ actionClass: actionPrisma.actionClass,
};
return action;
} catch (error) {
@@ -75,10 +76,10 @@ export const getActionsByPersonId = async (personId: string, page?: number): Pro
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
- const actionsPrisma = await prisma.event.findMany({
+ const actionsPrisma = await prisma.action.findMany({
where: {
- session: {
- personId: personId,
+ person: {
+ id: personId,
},
},
orderBy: {
@@ -87,7 +88,7 @@ export const getActionsByPersonId = async (personId: string, page?: number): Pro
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
- eventClass: true,
+ actionClass: true,
},
});
@@ -97,9 +98,10 @@ export const getActionsByPersonId = async (personId: string, page?: number): Pro
actions.push({
id: action.id,
createdAt: action.createdAt,
- sessionId: action.sessionId,
+ personId: action.personId,
+ // sessionId: action.sessionId,
properties: action.properties,
- actionClass: action.eventClass,
+ actionClass: action.actionClass,
});
});
return actions;
@@ -124,9 +126,9 @@ export const getActionsByEnvironmentId = async (environmentId: string, page?: nu
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
- const actionsPrisma = await prisma.event.findMany({
+ const actionsPrisma = await prisma.action.findMany({
where: {
- eventClass: {
+ actionClass: {
environmentId: environmentId,
},
},
@@ -136,7 +138,7 @@ export const getActionsByEnvironmentId = async (environmentId: string, page?: nu
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
- eventClass: true,
+ actionClass: true,
},
});
const actions: TAction[] = [];
@@ -145,9 +147,10 @@ export const getActionsByEnvironmentId = async (environmentId: string, page?: nu
actions.push({
id: action.id,
createdAt: action.createdAt,
- sessionId: action.sessionId,
+ // sessionId: action.sessionId,
+ personId: action.personId,
properties: action.properties,
- actionClass: action.eventClass,
+ actionClass: action.actionClass,
});
});
return actions;
@@ -177,17 +180,17 @@ export const getActionsByEnvironmentId = async (environmentId: string, page?: nu
export const createAction = async (data: TActionInput): Promise => {
validateInputs([data, ZActionInput]);
- const { environmentId, name, properties, sessionId } = data;
+ const { environmentId, name, properties, userId } = data;
- let eventType: TActionClassType = "code";
+ let actionType: TActionClassType = "code";
if (name === "Exit Intent (Desktop)" || name === "50% Scroll") {
- eventType = "automatic";
+ actionType = "automatic";
}
- const session = await getSession(sessionId);
+ const person = await getPersonByUserId(userId, environmentId);
- if (!session) {
- throw new ResourceNotFoundError("Session", sessionId);
+ if (!person) {
+ throw new Error("Person not found");
}
let actionClass = await getActionClassByEnvironmentIdAndName(environmentId, name);
@@ -195,20 +198,20 @@ export const createAction = async (data: TActionInput): Promise => {
if (!actionClass) {
actionClass = await createActionClass(environmentId, {
name,
- type: eventType,
+ type: actionType,
environmentId,
});
}
- const action = await prisma.event.create({
+ const action = await prisma.action.create({
data: {
properties,
- session: {
+ person: {
connect: {
- id: sessionId,
+ id: person.id,
},
},
- eventClass: {
+ actionClass: {
connect: {
id: actionClass.id,
},
@@ -216,15 +219,15 @@ export const createAction = async (data: TActionInput): Promise => {
},
});
- revalidateTag(sessionId);
actionCache.revalidate({
environmentId,
+ personId: person.id,
});
return {
id: action.id,
createdAt: action.createdAt,
- sessionId: action.sessionId,
+ personId: action.personId,
properties: action.properties,
actionClass,
};
@@ -236,9 +239,9 @@ export const getActionCountInLastHour = async (actionClassId: string): Promise => {
validateInputs([displayInput, ZDisplayCreateInput]);
+ try {
+ let person;
+ if (displayInput.userId) {
+ person = await getPersonByUserId(displayInput.userId, displayInput.environmentId);
+ }
+ const display = await prisma.display.create({
+ data: {
+ survey: {
+ connect: {
+ id: displayInput.surveyId,
+ },
+ },
+
+ ...(person && {
+ person: {
+ connect: {
+ id: person.id,
+ },
+ },
+ }),
+ },
+ select: selectDisplay,
+ });
+
+ displayCache.revalidate({
+ id: display.id,
+ personId: display.personId,
+ surveyId: display.surveyId,
+ });
+
+ return display;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+
+ throw error;
+ }
+};
+
+export const createDisplayLegacy = async (displayInput: TDisplayLegacyCreateInput): Promise => {
+ validateInputs([displayInput, ZDisplayLegacyCreateInput]);
try {
const display = await prisma.display.create({
data: {
diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts
index 2ac44691fc..ea3b313dee 100644
--- a/packages/lib/environment/service.ts
+++ b/packages/lib/environment/service.ts
@@ -184,7 +184,7 @@ export const createEnvironment = async (
type: environmentInput.type || "development",
product: { connect: { id: productId } },
widgetSetupCompleted: environmentInput.widgetSetupCompleted || false,
- eventClasses: {
+ actionClasses: {
create: [
{
name: "New Session",
diff --git a/packages/lib/person/service.ts b/packages/lib/person/service.ts
index 0d6f4f8742..a1c4e49042 100644
--- a/packages/lib/person/service.ts
+++ b/packages/lib/person/service.ts
@@ -1,19 +1,19 @@
import "server-only";
import { prisma } from "@formbricks/database";
+import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
-import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
+import { DatabaseError } from "@formbricks/types/errors";
import { TPerson, TPersonUpdateInput, ZPersonUpdateInput } from "@formbricks/types/people";
import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";
+import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { validateInputs } from "../utils/validate";
-import { getAttributeClassByName } from "../attributeClass/service";
-import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
-import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { personCache } from "./cache";
export const selectPerson = {
id: true,
+ userId: true,
createdAt: true,
updatedAt: true,
environmentId: true,
@@ -28,6 +28,7 @@ export const selectPerson = {
attributeClass: {
select: {
name: true,
+ id: true,
},
},
},
@@ -36,6 +37,7 @@ export const selectPerson = {
type TransformPersonInput = {
id: string;
+ userId: string;
environmentId: string;
attributes: {
value: string;
@@ -55,6 +57,7 @@ export const transformPrismaPerson = (person: TransformPersonInput): TPerson =>
return {
id: person.id,
+ userId: person.userId,
attributes: attributes,
environmentId: person.environmentId,
createdAt: new Date(person.createdAt),
@@ -157,7 +160,7 @@ export const getPeopleCount = async (environmentId: string): Promise =>
}
)();
-export const createPerson = async (environmentId: string): Promise => {
+export const createPerson = async (environmentId: string, userId: string): Promise => {
validateInputs([environmentId, ZId]);
try {
@@ -168,6 +171,7 @@ export const createPerson = async (environmentId: string): Promise => {
id: environmentId,
},
},
+ userId,
},
select: selectPerson,
});
@@ -243,13 +247,26 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
}
};
-export const getOrCreatePersonByUserId = async (userId: string, environmentId: string): Promise => {
+export const getPersonByUserId = async (userId: string, environmentId: string): Promise => {
const personPrisma = await unstable_cache(
async () => {
validateInputs([userId, ZString], [environmentId, ZId]);
+ // check if userId exists as a column
+ const personWithUserId = await prisma.person.findFirst({
+ where: {
+ environmentId,
+ userId,
+ },
+ select: selectPerson,
+ });
+
+ if (personWithUserId) {
+ return personWithUserId;
+ }
+
// Check if a person with the userId attribute exists
- const personPrisma = await prisma.person.findFirst({
+ let personWithUserIdAttribute = await prisma.person.findFirst({
where: {
environmentId,
attributes: {
@@ -264,50 +281,78 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
select: selectPerson,
});
- if (personPrisma) {
- return personPrisma;
- } else {
- // Create a new person with the userId attribute
- const userIdAttributeClass = await getAttributeClassByName(environmentId, "userId");
+ const userIdAttributeClassId = personWithUserIdAttribute?.attributes.find(
+ (attr) => attr.attributeClass.name === "userId" && attr.value === userId
+ )?.attributeClass.id;
- if (!userIdAttributeClass) {
- throw new ResourceNotFoundError(
- "Attribute class not found for the given environment",
- environmentId
- );
- }
+ if (!personWithUserIdAttribute) {
+ return null;
+ }
- const person = await prisma.person.create({
- data: {
- environment: {
- connect: {
- id: environmentId,
- },
- },
- attributes: {
- create: [
- {
- attributeClass: {
- connect: {
- id: userIdAttributeClass.id,
- },
- },
- value: userId,
- },
- ],
- },
- },
- select: selectPerson,
- });
-
- personCache.revalidate({
- id: person.id,
- environmentId: person.environmentId,
+ personWithUserIdAttribute = await prisma.person.update({
+ where: {
+ id: personWithUserIdAttribute.id,
+ },
+ data: {
userId,
- });
+ attributes: {
+ deleteMany: { attributeClassId: userIdAttributeClassId },
+ },
+ },
+ select: selectPerson,
+ });
+ personCache.revalidate({
+ id: personWithUserIdAttribute.id,
+ environmentId: personWithUserIdAttribute.environmentId,
+ userId,
+ });
+
+ return personWithUserIdAttribute;
+ },
+ [`getPersonByUserId-${userId}-${environmentId}`],
+ {
+ tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
+ revalidate: SERVICES_REVALIDATION_INTERVAL,
+ }
+ )();
+ if (!personPrisma) {
+ return null;
+ }
+ return transformPrismaPerson(personPrisma);
+};
+
+export const getOrCreatePersonByUserId = async (userId: string, environmentId: string): Promise =>
+ await unstable_cache(
+ async () => {
+ validateInputs([userId, ZString], [environmentId, ZId]);
+
+ let person = await getPersonByUserId(userId, environmentId);
+
+ if (person) {
return person;
}
+
+ // create a new person
+ const personPrisma = await prisma.person.create({
+ data: {
+ environment: {
+ connect: {
+ id: environmentId,
+ },
+ },
+ userId,
+ },
+ select: selectPerson,
+ });
+
+ personCache.revalidate({
+ id: personPrisma.id,
+ environmentId: personPrisma.environmentId,
+ userId,
+ });
+
+ return transformPrismaPerson(personPrisma);
},
[`getOrCreatePersonByUserId-${userId}-${environmentId}`],
{
@@ -316,9 +361,6 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
}
)();
- return transformPrismaPerson(personPrisma);
-};
-
export const updatePersonAttribute = async (
personId: string,
attributeClassId: string,
diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts
index db01daef63..8a147d3572 100644
--- a/packages/lib/response/service.ts
+++ b/packages/lib/response/service.ts
@@ -38,6 +38,7 @@ const responseSelection = {
person: {
select: {
id: true,
+ userId: true,
createdAt: true,
updatedAt: true,
environmentId: true,
diff --git a/packages/lib/responseQueue.ts b/packages/lib/responseQueue.ts
index 54b791d6de..bd38b49565 100644
--- a/packages/lib/responseQueue.ts
+++ b/packages/lib/responseQueue.ts
@@ -4,6 +4,7 @@ import SurveyState from "./surveyState";
interface QueueConfig {
apiHost: string;
+ environmentId: string;
retryAttempts: number;
onResponseSendingFailed?: (responseUpdate: TResponseUpdate) => void;
setSurveyState?: (state: SurveyState) => void;
@@ -21,7 +22,7 @@ export class ResponseQueue {
this.surveyState = surveyState;
this.api = new FormbricksAPI({
apiHost: config.apiHost,
- environmentId: "",
+ environmentId: config.environmentId,
});
}
diff --git a/packages/lib/session/cache.ts b/packages/lib/session/cache.ts
deleted file mode 100644
index ef9438be1e..0000000000
--- a/packages/lib/session/cache.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { revalidateTag } from "next/cache";
-
-interface RevalidateProps {
- id?: string;
- personId?: string;
-}
-
-export const sessionCache = {
- tag: {
- byId(id: string) {
- return `sessions-${id}`;
- },
- byPersonId(personId: string) {
- return `people-${personId}-sessions`;
- },
- },
- revalidate({ id, personId }: RevalidateProps): void {
- if (id) {
- revalidateTag(this.tag.byId(id));
- }
-
- if (personId) {
- revalidateTag(this.tag.byPersonId(personId));
- }
- },
-};
diff --git a/packages/lib/session/service.ts b/packages/lib/session/service.ts
deleted file mode 100644
index 4b49cfd1d1..0000000000
--- a/packages/lib/session/service.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-"use server";
-import "server-only";
-
-import { prisma } from "@formbricks/database";
-import { ZId } from "@formbricks/types/environment";
-import { DatabaseError } from "@formbricks/types/errors";
-import { TSession } from "@formbricks/types/sessions";
-import { Prisma } from "@prisma/client";
-import { unstable_cache } from "next/cache";
-import { validateInputs } from "../utils/validate";
-import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
-import { sessionCache } from "./cache";
-import { formatSessionDateFields } from "./util";
-
-const select = {
- id: true,
- createdAt: true,
- updatedAt: true,
- expiresAt: true,
- personId: true,
-};
-
-const oneHour = 1000 * 60 * 60;
-
-export const getSession = async (sessionId: string): Promise => {
- const session = await unstable_cache(
- async () => {
- validateInputs([sessionId, ZId]);
-
- try {
- const session = await prisma.session.findUnique({
- where: {
- id: sessionId,
- },
- select,
- });
-
- return session;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError(error.message);
- }
-
- throw error;
- }
- },
- [`getSession-${sessionId}`],
- {
- tags: [sessionCache.tag.byId(sessionId)],
- revalidate: SERVICES_REVALIDATION_INTERVAL,
- }
- )();
-
- if (!session) return null;
-
- return formatSessionDateFields(session);
-};
-
-export const getSessionCount = async (personId: string): Promise =>
- unstable_cache(
- async () => {
- validateInputs([personId, ZId]);
-
- try {
- const sessionCount = await prisma.session.count({
- where: {
- personId,
- },
- });
- return sessionCount;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError(error.message);
- }
- throw error;
- }
- },
- [`getSessionCount-${personId}`],
- {
- tags: [sessionCache.tag.byPersonId(personId)],
- revalidate: SERVICES_REVALIDATION_INTERVAL,
- }
- )();
-
-export const createSession = async (personId: string): Promise => {
- validateInputs([personId, ZId]);
- try {
- const session = await prisma.session.create({
- data: {
- person: {
- connect: {
- id: personId,
- },
- },
- expiresAt: new Date(Date.now() + oneHour),
- },
- select,
- });
-
- if (session) {
- sessionCache.revalidate({
- id: session.id,
- personId,
- });
- }
-
- return session;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError(error.message);
- }
-
- throw error;
- }
-};
-
-export const extendSession = async (sessionId: string): Promise => {
- validateInputs([sessionId, ZId]);
-
- try {
- const session = await prisma.session.update({
- where: {
- id: sessionId,
- },
- data: {
- expiresAt: new Date(Date.now() + oneHour),
- },
- select,
- });
-
- // revalidate session cache
- sessionCache.revalidate({
- id: sessionId,
- personId: session.personId,
- });
-
- return session;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError(error.message);
- }
-
- throw error;
- }
-};
diff --git a/packages/lib/session/util.ts b/packages/lib/session/util.ts
deleted file mode 100644
index 6ba2b18659..0000000000
--- a/packages/lib/session/util.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import "server-only";
-
-import { TSession } from "@formbricks/types/sessions";
-
-export const formatSessionDateFields = (session: TSession): TSession => {
- if (typeof session.createdAt === "string") {
- session.createdAt = new Date(session.createdAt);
- }
- if (typeof session.updatedAt === "string") {
- session.updatedAt = new Date(session.updatedAt);
- }
- if (typeof session.expiresAt === "string") {
- session.expiresAt = new Date(session.expiresAt);
- }
-
- return session;
-};
diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts
index e80bc468e9..2432a24856 100644
--- a/packages/lib/survey/service.ts
+++ b/packages/lib/survey/service.ts
@@ -15,6 +15,14 @@ import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { formatSurveyDateFields } from "./util";
import { surveyCache } from "./cache";
+import { displayCache } from "../display/cache";
+import { productCache } from "../product/cache";
+import { TPerson } from "@formbricks/types/people";
+import { TSurveyWithTriggers } from "@formbricks/types/js";
+import { getAttributeClasses } from "../attributeClass/service";
+import { getProductByEnvironmentId } from "../product/service";
+import { getDisplaysByPersonId } from "../display/service";
+import { diffInDays } from "../utils/datetime";
export const selectSurvey = {
id: true,
@@ -43,7 +51,7 @@ export const selectSurvey = {
pin: true,
triggers: {
select: {
- eventClass: {
+ actionClass: {
select: {
id: true,
createdAt: true,
@@ -116,7 +124,7 @@ export const getSurvey = async (surveyId: string): Promise => {
const transformedSurvey = {
...surveyPrisma,
- triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
+ triggers: surveyPrisma.triggers.map((trigger) => trigger.actionClass.name),
};
return transformedSurvey;
@@ -166,7 +174,7 @@ export const getSurveysByAttributeClassId = async (
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
- triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
+ triggers: surveyPrisma.triggers.map((trigger) => trigger.actionClass.name),
};
surveys.push(transformedSurvey);
}
@@ -195,7 +203,7 @@ export const getSurveysByActionClassId = async (actionClassId: string, page?: nu
where: {
triggers: {
some: {
- eventClass: {
+ actionClass: {
id: actionClassId,
},
},
@@ -211,7 +219,7 @@ export const getSurveysByActionClassId = async (actionClassId: string, page?: nu
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
- triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
+ triggers: surveyPrisma.triggers.map((trigger) => trigger.actionClass.name),
};
surveys.push(transformedSurvey);
}
@@ -259,7 +267,7 @@ export const getSurveys = async (environmentId: string, page?: number): Promise<
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
- triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
+ triggers: surveyPrisma.triggers.map((trigger) => trigger.actionClass.name),
};
surveys.push(transformedSurvey);
}
@@ -324,7 +332,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise =>
data.triggers = {
...(data.triggers || []),
create: newTriggers.map((trigger) => ({
- eventClassId: getActionClassIdFromName(actionClasses, trigger),
+ actionClassId: getActionClassIdFromName(actionClasses, trigger),
})),
};
}
@@ -333,7 +341,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise =>
data.triggers = {
...(data.triggers || []),
deleteMany: {
- eventClassId: {
+ actionClassId: {
in: removedTriggers.map((trigger) => getActionClassIdFromName(actionClasses, trigger)),
},
},
@@ -474,7 +482,7 @@ export async function deleteSurvey(surveyId: string) {
// Revalidate triggers by actionClassId
deletedSurvey.triggers.forEach((trigger) => {
surveyCache.revalidate({
- actionClassId: trigger.eventClass.id,
+ actionClassId: trigger.actionClass.id,
});
});
// Revalidate surveys by attributeClassId
@@ -520,7 +528,7 @@ export const createSurvey = async (environmentId: string, surveyBody: TSurveyInp
const transformedSurvey = {
...survey,
- triggers: survey.triggers.map((trigger) => trigger.eventClass.name),
+ triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
};
captureTelemetry("survey created");
@@ -559,7 +567,7 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: existingSurvey.triggers.map((trigger) => ({
- eventClassId: getActionClassIdFromName(actionClasses, trigger),
+ actionClassId: getActionClassIdFromName(actionClasses, trigger),
})),
},
attributeFilters: {
@@ -601,3 +609,102 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
return newSurvey;
};
+
+export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
+ unstable_cache(
+ async () => {
+ return await getSyncSurveys(environmentId, person);
+ },
+ [`getSyncSurveysCached-${environmentId}`],
+ {
+ tags: [
+ displayCache.tag.byPersonId(person.id),
+ surveyCache.tag.byEnvironmentId(environmentId),
+ productCache.tag.byEnvironmentId(environmentId),
+ ],
+ revalidate: SERVICES_REVALIDATION_INTERVAL,
+ }
+ )();
+
+export const getSyncSurveys = async (
+ environmentId: string,
+ person: TPerson
+): Promise => {
+ // get recontactDays from product
+ const product = await getProductByEnvironmentId(environmentId);
+
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
+ let surveys = await getSurveys(environmentId);
+
+ // filtered surveys for running and web
+ surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
+
+ const displays = await getDisplaysByPersonId(person.id);
+
+ // filter surveys that meet the displayOption criteria
+ surveys = surveys.filter((survey) => {
+ if (survey.displayOption === "respondMultiple") {
+ return true;
+ } else if (survey.displayOption === "displayOnce") {
+ return displays.filter((display) => display.surveyId === survey.id).length === 0;
+ } else if (survey.displayOption === "displayMultiple") {
+ return (
+ displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
+ 0
+ );
+ } else {
+ throw Error("Invalid displayOption");
+ }
+ });
+
+ const attributeClasses = await getAttributeClasses(environmentId);
+
+ // filter surveys that meet the attributeFilters criteria
+ const potentialSurveysWithAttributes = surveys.filter((survey) => {
+ const attributeFilters = survey.attributeFilters;
+ if (attributeFilters.length === 0) {
+ return true;
+ }
+ // check if meets all attribute filters criterias
+ return attributeFilters.every((attributeFilter) => {
+ const attributeClassName = attributeClasses.find(
+ (attributeClass) => attributeClass.id === attributeFilter.attributeClassId
+ )?.name;
+ if (!attributeClassName) {
+ throw Error("Invalid attribute filter class");
+ }
+ const personAttributeValue = person.attributes[attributeClassName];
+ if (attributeFilter.condition === "equals") {
+ return personAttributeValue === attributeFilter.value;
+ } else if (attributeFilter.condition === "notEquals") {
+ return personAttributeValue !== attributeFilter.value;
+ } else {
+ throw Error("Invalid attribute filter condition");
+ }
+ });
+ });
+
+ const latestDisplay = displays[0];
+
+ // filter surveys that meet the recontactDays criteria
+ surveys = potentialSurveysWithAttributes.filter((survey) => {
+ if (!latestDisplay) {
+ return true;
+ } else if (survey.recontactDays !== null) {
+ const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
+ if (!lastDisplaySurvey) {
+ return true;
+ }
+ return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
+ } else if (product.recontactDays !== null) {
+ return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
+ } else {
+ return true;
+ }
+ });
+
+ return surveys;
+};
diff --git a/packages/lib/team/service.ts b/packages/lib/team/service.ts
index c1e88c2703..5591181950 100644
--- a/packages/lib/team/service.ts
+++ b/packages/lib/team/service.ts
@@ -306,7 +306,7 @@ export const getMonthlyActiveTeamPeopleCount = async (teamId: string): Promise {
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
+ return Math.floor(diffTime / (1000 * 60 * 60 * 24));
+};
diff --git a/packages/types/actions.ts b/packages/types/actions.ts
index 40c48532cb..3ff6b6849b 100644
--- a/packages/types/actions.ts
+++ b/packages/types/actions.ts
@@ -4,7 +4,7 @@ import { ZActionClass } from "./actionClasses";
export const ZAction = z.object({
id: z.string(),
createdAt: z.date(),
- sessionId: z.string(),
+ personId: z.string(),
properties: z.record(z.string()),
actionClass: ZActionClass.nullable(),
});
@@ -12,10 +12,20 @@ export const ZAction = z.object({
export type TAction = z.infer;
export const ZActionInput = z.object({
- environmentId: z.string().cuid2(),
- sessionId: z.string().cuid2(),
+ environmentId: z.string().cuid(),
+ userId: z.string(),
name: z.string(),
properties: z.record(z.string()),
});
export type TActionInput = z.infer;
+
+export const ZActionLegacyInput = z.object({
+ environmentId: z.string().cuid2(),
+ personId: z.string().optional(),
+ sessionId: z.string().optional(),
+ name: z.string(),
+ properties: z.record(z.string()),
+});
+
+export type TActionLegacyInput = z.infer;
diff --git a/packages/types/displays.ts b/packages/types/displays.ts
index af179fd9b4..8d7cf2296c 100644
--- a/packages/types/displays.ts
+++ b/packages/types/displays.ts
@@ -1,28 +1,37 @@
import { z } from "zod";
export const ZDisplay = z.object({
- id: z.string().cuid2(),
+ id: z.string().cuid(),
createdAt: z.date(),
updatedAt: z.date(),
- personId: z.string().cuid2().nullable(),
- surveyId: z.string().cuid2(),
- responseId: z.string().cuid2().nullable(),
+ personId: z.string().cuid().nullable(),
+ surveyId: z.string().cuid(),
+ responseId: z.string().cuid().nullable(),
status: z.enum(["seen", "responded"]).optional(),
});
export type TDisplay = z.infer;
export const ZDisplayCreateInput = z.object({
- surveyId: z.string().cuid2(),
- personId: z.string().cuid2().optional(),
- responseId: z.string().cuid2().optional(),
+ environmentId: z.string().cuid(),
+ surveyId: z.string().cuid(),
+ userId: z.string().optional(),
+ responseId: z.string().cuid().optional(),
});
export type TDisplayCreateInput = z.infer;
+export const ZDisplayLegacyCreateInput = z.object({
+ surveyId: z.string().cuid(),
+ personId: z.string().cuid().optional(),
+ responseId: z.string().cuid().optional(),
+});
+
+export type TDisplayLegacyCreateInput = z.infer;
+
export const ZDisplayUpdateInput = z.object({
- personId: z.string().cuid2().optional(),
- responseId: z.string().cuid2().optional(),
+ personId: z.string().cuid().optional(),
+ responseId: z.string().cuid().optional(),
});
export type TDisplayUpdateInput = z.infer;
diff --git a/packages/types/js.ts b/packages/types/js.ts
index 9dd69c8942..cbcb581f3d 100644
--- a/packages/types/js.ts
+++ b/packages/types/js.ts
@@ -1,6 +1,5 @@
import z from "zod";
import { ZPerson } from "./people";
-import { ZSession } from "./sessions";
import { ZSurvey } from "./surveys";
import { ZActionClass } from "./actionClasses";
import { ZProduct } from "./product";
@@ -11,62 +10,110 @@ const ZSurveyWithTriggers = ZSurvey.extend({
export type TSurveyWithTriggers = z.infer;
+export const ZJSStateDisplay = z.object({
+ createdAt: z.date(),
+ surveyId: z.string().cuid(),
+ responded: z.boolean(),
+});
+
+export type TJSStateDisplay = z.infer;
+
export const ZJsState = z.object({
- person: ZPerson,
- session: ZSession,
+ person: ZPerson.nullable(),
surveys: z.array(ZSurveyWithTriggers),
noCodeActionClasses: z.array(ZActionClass),
product: ZProduct,
+ displays: z.array(ZJSStateDisplay).optional(),
});
export type TJsState = z.infer;
+export const ZJsLegacyState = z.object({
+ person: ZPerson.nullable().or(z.object({})),
+ session: z.object({}),
+ surveys: z.array(ZSurveyWithTriggers),
+ noCodeActionClasses: z.array(ZActionClass),
+ product: ZProduct,
+ displays: z.array(ZJSStateDisplay).optional(),
+});
+
+export type TJsLegacyState = z.infer;
+
+export const ZJsPublicSyncInput = z.object({
+ environmentId: z.string().cuid(),
+});
+
+export type TJsPublicSyncInput = z.infer;
+
export const ZJsSyncInput = z.object({
- environmentId: z.string().cuid2(),
- personId: z.string().cuid2().optional(),
- sessionId: z.string().cuid2().optional(),
+ environmentId: z.string().cuid(),
+ userId: z.string().optional().optional(),
jsVersion: z.string().optional(),
});
export type TJsSyncInput = z.infer;
+export const ZJsSyncLegacyInput = z.object({
+ environmentId: z.string().cuid(),
+ personId: z.string().cuid().optional(),
+ sessionId: z.string().cuid().optional(),
+ jsVersion: z.string().optional(),
+});
+
+export type TJsSyncLegacyInput = z.infer;
+
export const ZJsConfig = z.object({
- environmentId: z.string().cuid2(),
+ environmentId: z.string().cuid(),
apiHost: z.string(),
state: ZJsState,
+ expiresAt: z.date(),
});
export type TJsConfig = z.infer;
+export const ZJsConfigUpdateInput = z.object({
+ environmentId: z.string().cuid(),
+ apiHost: z.string(),
+ state: ZJsState,
+});
+
+export type TJsConfigUpdateInput = z.infer;
+
export const ZJsConfigInput = z.object({
- environmentId: z.string().cuid2(),
+ environmentId: z.string().cuid(),
apiHost: z.string(),
debug: z.boolean().optional(),
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
+ userId: z.string().optional(),
});
export type TJsConfigInput = z.infer;
export const ZJsPeopleUserIdInput = z.object({
- environmentId: z.string().cuid2(),
+ environmentId: z.string().cuid(),
userId: z.string().min(1).max(255),
- sessionId: z.string().cuid2(),
});
export type TJsPeopleUserIdInput = z.infer;
export const ZJsPeopleAttributeInput = z.object({
- environmentId: z.string().cuid2(),
- sessionId: z.string().cuid2(),
key: z.string(),
value: z.string(),
});
export type TJsPeopleAttributeInput = z.infer;
+export const ZJsPeopleLegacyAttributeInput = z.object({
+ environmentId: z.string().cuid(),
+ key: z.string(),
+ value: z.string(),
+});
+
+export type TJsPeopleLegacyAttributeInput = z.infer;
+
export const ZJsActionInput = z.object({
- environmentId: z.string().cuid2(),
- sessionId: z.string().cuid2(),
+ environmentId: z.string().cuid(),
+ userId: z.string().optional(),
name: z.string(),
properties: z.record(z.string()),
});
@@ -74,10 +121,9 @@ export const ZJsActionInput = z.object({
export type TJsActionInput = z.infer;
export const ZJsSyncParams = z.object({
- environmentId: z.string().cuid2(),
+ environmentId: z.string().cuid(),
apiHost: z.string(),
- personId: z.string().cuid2().optional(),
- sessionId: z.string().cuid2().optional(),
+ userId: z.string().optional(),
});
export type TJsSyncParams = z.infer;
diff --git a/packages/types/people.ts b/packages/types/people.ts
index 9c78b70fd4..4a00c8c7b1 100644
--- a/packages/types/people.ts
+++ b/packages/types/people.ts
@@ -5,6 +5,7 @@ export type TPersonAttributes = z.infer;
export const ZPerson = z.object({
id: z.string().cuid2(),
+ userId: z.string(),
attributes: ZPersonAttributes,
createdAt: z.date(),
updatedAt: z.date(),
diff --git a/packages/types/sessions.ts b/packages/types/sessions.ts
deleted file mode 100644
index 0a51406a3a..0000000000
--- a/packages/types/sessions.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import z from "zod";
-
-export const ZSession = z.object({
- id: z.string().cuid2(),
- createdAt: z.date(),
- updatedAt: z.date(),
- expiresAt: z.date(),
- personId: z.string().cuid2(),
-});
-
-export type TSession = z.infer;
-
-export const ZSessionWithActions = z.object({
- id: z.string().cuid2(),
- events: z.array(
- z.object({
- id: z.string().cuid2(),
- createdAt: z.date(),
- eventClass: z
- .object({
- name: z.string(),
- description: z.union([z.string(), z.null()]),
- type: z.enum(["code", "noCode", "automatic"]),
- })
- .nullable(),
- })
- ),
-});
-
-export type TSessionWithActions = z.infer;
diff --git a/packages/ui/Alert/index.tsx b/packages/ui/Alert/index.tsx
index 2a4d4fd1c5..545d62c725 100644
--- a/packages/ui/Alert/index.tsx
+++ b/packages/ui/Alert/index.tsx
@@ -11,6 +11,7 @@ const alertVariants = cva(
default: "bg-background text-foreground",
destructive:
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
+ info: "text-slate-800 bg-brand/5",
},
},
defaultVariants: {