mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-28 11:30:15 -06:00
chore: prepare 2.4.0 release (#2959)
This commit is contained in:
@@ -8,6 +8,108 @@ export const metadata = {
|
||||
|
||||
# Migration Guide
|
||||
|
||||
## v2.4
|
||||
|
||||
Formbricks v2.4 allows you to create multiple endings for your surveys and decide which ending the user should see based on logic jumps. This release also includes many bug fixes and performance improvements.
|
||||
|
||||
<Note>
|
||||
This release will drop support for advanced targeting (enterprise targeting for app surveys) with actions
|
||||
(e.g. only target users that triggered action x 3 times in the last month). This means that actions can
|
||||
still be used as triggers, but will no longer be stored on the server in order to improve the overall
|
||||
performance of the Formbricks system.
|
||||
</Note>
|
||||
|
||||
### Steps to Migrate
|
||||
|
||||
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
|
||||
|
||||
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
|
||||
|
||||
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.4_$(date +%Y%m%d_%H%M%S).dump
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
<Note>
|
||||
If you run into “No such container”, use `docker ps` to find your container name, e.g.
|
||||
`formbricks_postgres_1`.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
If you prefer storing the backup as an `*.sql` file remove the `-Fc` (custom format) option. In case of a
|
||||
restore scenario you will need to use `psql` then with an empty `formbricks` database.
|
||||
</Note>
|
||||
|
||||
2. Pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Restarting the containers with the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.4" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
|
||||
|
||||
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
|
||||
### Additional Updates
|
||||
|
||||
- The `CRON_SECRET` environment variable is now required to improve the security of the internal cron APIs. Please make sure that the variable is set in your environment / docker-compose.yml. You can use `openssl rand -hex 32` to generate a secure secret.
|
||||
|
||||
## v2.3
|
||||
|
||||
Formbricks v2.3 includes new color options for rating questions, improved multi-language functionality for Chinese (Simplified & Traditional), and various bug fixes and performance improvements.
|
||||
|
||||
@@ -7,30 +7,14 @@ import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { SHORT_URL_BASE, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { createShortUrl } from "@formbricks/lib/shortUrl/service";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError, AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export const createShortUrlAction = async (url: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthenticationError("Not authenticated");
|
||||
|
||||
const regexPattern = new RegExp("^" + WEBAPP_URL);
|
||||
const isValidUrl = regexPattern.test(url);
|
||||
|
||||
if (!isValidUrl) throw new Error("Only Formbricks survey URLs are allowed");
|
||||
|
||||
const shortUrl = await createShortUrl(url);
|
||||
const fullShortUrl = SHORT_URL_BASE + "/" + shortUrl.id;
|
||||
return fullShortUrl;
|
||||
};
|
||||
|
||||
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled)
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { createShortUrlAction } from "../actions";
|
||||
|
||||
type UrlShortenerFormDataProps = {
|
||||
url: string;
|
||||
};
|
||||
type UrlValidationState = "default" | "valid" | "invalid";
|
||||
|
||||
export const UrlShortenerForm = ({ webAppUrl }: { webAppUrl: string }) => {
|
||||
const [urlValidationState, setUrlValidationState] = useState<UrlValidationState>("default");
|
||||
const [shortUrl, setShortUrl] = useState("");
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<UrlShortenerFormDataProps>({
|
||||
mode: "onSubmit",
|
||||
defaultValues: {
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleUrlValidation = () => {
|
||||
const value = watch("url").trim();
|
||||
if (!value) {
|
||||
setUrlValidationState("default");
|
||||
return;
|
||||
}
|
||||
|
||||
const regexPattern = new RegExp("^" + webAppUrl);
|
||||
const isValid = regexPattern.test(value);
|
||||
if (!isValid) {
|
||||
setUrlValidationState("invalid");
|
||||
toast.error("Only formbricks survey links allowed.");
|
||||
} else {
|
||||
setUrlValidationState("valid");
|
||||
}
|
||||
};
|
||||
|
||||
const shortenUrl = async (data: UrlShortenerFormDataProps) => {
|
||||
if (urlValidationState !== "valid") return;
|
||||
|
||||
const shortUrl = await createShortUrlAction(data.url.trim());
|
||||
setShortUrl(shortUrl);
|
||||
};
|
||||
|
||||
const copyShortUrlToClipboard = () => {
|
||||
navigator.clipboard.writeText(shortUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 p-4">
|
||||
<form onSubmit={handleSubmit(shortenUrl)}>
|
||||
<div className="w-full space-y-2 rounded-lg">
|
||||
<Label>Paste Survey Link</Label>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={`${webAppUrl}...`}
|
||||
className={clsx(
|
||||
"",
|
||||
urlValidationState === "valid"
|
||||
? "border-green-500 bg-green-50"
|
||||
: urlValidationState === "invalid"
|
||||
? "border-red-200 bg-red-50"
|
||||
: "border-slate-200"
|
||||
)}
|
||||
{...register("url", {
|
||||
required: true,
|
||||
})}
|
||||
onBlur={handleUrlValidation}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={watch("url") === ""}
|
||||
size="sm"
|
||||
type="submit"
|
||||
loading={isSubmitting}>
|
||||
Shorten
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{shortUrl && (
|
||||
<div className="w-full space-y-2 rounded-lg">
|
||||
<Label>Short Link</Label>
|
||||
<div className="flex gap-3">
|
||||
<span
|
||||
className="h-10 w-full cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
|
||||
onClick={() => {
|
||||
if (shortUrl) {
|
||||
copyShortUrlToClipboard();
|
||||
}
|
||||
}}>
|
||||
{shortUrl}
|
||||
</span>
|
||||
<Button
|
||||
disabled={shortUrl === ""}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => copyShortUrlToClipboard()}>
|
||||
<span>Copy</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import { UrlShortenerForm } from "./UrlShortenerForm";
|
||||
|
||||
type UrlShortenerModalProps = {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
webAppUrl: string;
|
||||
};
|
||||
|
||||
export const UrlShortenerModal = ({ open, setOpen, webAppUrl }: UrlShortenerModalProps) => {
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} noPadding closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg pb-4">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-10 w-10 text-slate-500">
|
||||
<LinkIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">URL shortener</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
Create a short URL to make URL params less obvious.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UrlShortenerForm webAppUrl={webAppUrl} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { UrlShortenerForm } from "@/app/(app)/environments/[environmentId]/components/UrlShortenerForm";
|
||||
import Link from "next/link";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ShareSurveyLink } from "@formbricks/ui/ShareSurveyLink";
|
||||
@@ -60,12 +59,6 @@ export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl }: LinkTabP
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 pt-2 font-semibold text-slate-700">Survey link got too long? Shorten it!</p>
|
||||
<div className="rounded-md border border-slate-200 bg-white">
|
||||
<UrlShortenerForm webAppUrl={webAppUrl} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/api",
|
||||
"license": "MIT",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "Formbricks-api is an api wrapper for the Formbricks client API",
|
||||
"keywords": [
|
||||
"Formbricks",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"data-migration:v2.3": "pnpm data-migration:zh-to-zh-Hans",
|
||||
"data-migration:segments-cleanup": "ts-node ./data-migrations/20240712123456_segments_cleanup/data-migration.ts",
|
||||
"data-migration:multiple-endings": "ts-node ./data-migrations/20240801120500_thankYouCard_to_endings/data-migration.ts",
|
||||
"data-migration:simplified-email-verification": "ts-node ./data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts"
|
||||
"data-migration:simplified-email-verification": "ts-node ./data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts",
|
||||
"data-migration:v2.4": "pnpm data-migration:segments-cleanup && pnpm data-migration:multiple-endings && pnpm data-migration:simplified-email-verification"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.17.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// DEPRECATED
|
||||
// The ShortUrl feature is deprecated and only available for backward compatibility.
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { cache as reactCache } from "react";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
@@ -9,43 +10,6 @@ import { cache } from "../cache";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { shortUrlCache } from "./cache";
|
||||
|
||||
// Create the short url and return it
|
||||
export const createShortUrl = async (url: string): Promise<TShortUrl> => {
|
||||
validateInputs([url, z.string().url()]);
|
||||
|
||||
try {
|
||||
// Check if an entry with the provided fullUrl already exists.
|
||||
const existingShortUrl = await getShortUrlByUrl(url);
|
||||
|
||||
if (existingShortUrl) {
|
||||
return existingShortUrl;
|
||||
}
|
||||
|
||||
// If an entry with the provided fullUrl does not exist, create a new one.
|
||||
const id = customAlphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 10)();
|
||||
|
||||
const shortUrl = await prisma.shortUrl.create({
|
||||
data: {
|
||||
id,
|
||||
url,
|
||||
},
|
||||
});
|
||||
|
||||
shortUrlCache.revalidate({
|
||||
id: shortUrl.id,
|
||||
url: shortUrl.url,
|
||||
});
|
||||
|
||||
return shortUrl;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the full url from short url and return it
|
||||
export const getShortUrl = reactCache(
|
||||
(id: string): Promise<TShortUrl | null> =>
|
||||
|
||||
Reference in New Issue
Block a user