chore: prepare 2.4.0 release (#2959)

This commit is contained in:
Matti Nannt
2024-08-02 20:32:21 +02:00
committed by GitHub
parent 3416c26bdc
commit 0988f2145c
9 changed files with 109 additions and 217 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "2.3.2",
"version": "2.4.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",

View File

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

View File

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

View File

@@ -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> =>