mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-31 00:50:34 -06:00
chore(cloud): move from customer-io to brevo (#4681)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
This commit is contained in:
@@ -167,9 +167,9 @@ ENTERPRISE_LICENSE_KEY=
|
||||
# DEFAULT_ORGANIZATION_ID=
|
||||
# DEFAULT_ORGANIZATION_ROLE=owner
|
||||
|
||||
# Send new users to customer.io
|
||||
# CUSTOMER_IO_API_KEY=
|
||||
# CUSTOMER_IO_SITE_ID=
|
||||
# Send new users to Brevo
|
||||
# BREVO_API_KEY=
|
||||
# BREVO_LIST_ID=
|
||||
|
||||
# Ignore Rate Limiting across the Formbricks app
|
||||
# RATE_LIMITING_DISABLED=1
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto";
|
||||
import { verifyToken } from "@formbricks/lib/jwt";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { createBrevoCustomer } from "./brevo";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
@@ -162,6 +163,9 @@ export const authOptions: NextAuthOptions = {
|
||||
|
||||
user = await updateUser(user.id, { emailVerified: new Date() });
|
||||
|
||||
// send new user to brevo after email verification
|
||||
createBrevoCustomer({ id: user.id, email: user.email });
|
||||
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
|
||||
58
apps/web/modules/auth/lib/brevo.test.ts
Normal file
58
apps/web/modules/auth/lib/brevo.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Response } from "node-fetch";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { createBrevoCustomer } from "./brevo";
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
BREVO_API_KEY: "mock_api_key",
|
||||
BREVO_LIST_ID: "123",
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/utils/validate", () => ({
|
||||
validateInputs: vi.fn(),
|
||||
}));
|
||||
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("createBrevoCustomer", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return early if BREVO_API_KEY is not defined", async () => {
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
BREVO_API_KEY: undefined,
|
||||
BREVO_LIST_ID: "123",
|
||||
}));
|
||||
|
||||
const { createBrevoCustomer } = await import("./brevo");
|
||||
|
||||
const result = await createBrevoCustomer({ id: "123", email: "test@example.com" });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
expect(validateInputs).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log an error if fetch fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error");
|
||||
|
||||
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Fetch failed"));
|
||||
|
||||
await createBrevoCustomer({ id: "123", email: "test@example.com" });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Error sending user to Brevo:", expect.any(Error));
|
||||
});
|
||||
|
||||
it("should log the error response if fetch status is not 200", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error");
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce(
|
||||
new Response("Bad Request", { status: 400, statusText: "Bad Request" })
|
||||
);
|
||||
|
||||
await createBrevoCustomer({ id: "123", email: "test@example.com" });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Error sending user to Brevo:", "Bad Request");
|
||||
});
|
||||
});
|
||||
42
apps/web/modules/auth/lib/brevo.ts
Normal file
42
apps/web/modules/auth/lib/brevo.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { BREVO_API_KEY, BREVO_LIST_ID } from "@formbricks/lib/constants";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { TUserEmail, ZUserEmail } from "@formbricks/types/user";
|
||||
|
||||
export const createBrevoCustomer = async ({ id, email }: { id: string; email: TUserEmail }) => {
|
||||
if (!BREVO_API_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateInputs([id, ZId], [email, ZUserEmail]);
|
||||
|
||||
try {
|
||||
const requestBody: any = {
|
||||
email,
|
||||
ext_id: id,
|
||||
updateEnabled: false,
|
||||
};
|
||||
|
||||
// Add `listIds` only if `BREVO_LIST_ID` is defined
|
||||
const listId = BREVO_LIST_ID ? parseInt(BREVO_LIST_ID, 10) : null;
|
||||
if (listId && !Number.isNaN(listId)) {
|
||||
requestBody.listIds = [listId];
|
||||
}
|
||||
|
||||
const res = await fetch("https://api.brevo.com/v3/contacts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"api-key": BREVO_API_KEY,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error("Error sending user to Brevo:", await res.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending user to Brevo:", error);
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { createCustomerIoCustomer } from "@formbricks/lib/customerio";
|
||||
import { userCache } from "@formbricks/lib/user/cache";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { mockUser } from "./mock-data";
|
||||
@@ -27,10 +26,6 @@ vi.mock("@formbricks/database", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/customerio", () => ({
|
||||
createCustomerIoCustomer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/user/cache", () => ({
|
||||
userCache: {
|
||||
revalidate: vi.fn(),
|
||||
@@ -57,10 +52,6 @@ describe("User Management", () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockPrismaUser);
|
||||
expect(createCustomerIoCustomer).toHaveBeenCalledWith({
|
||||
id: mockPrismaUser.id,
|
||||
email: mockPrismaUser.email,
|
||||
});
|
||||
expect(userCache.revalidate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { createCustomerIoCustomer } from "@formbricks/lib/customerio";
|
||||
import { userCache } from "@formbricks/lib/user/cache";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
@@ -128,8 +127,6 @@ export const createUser = async (data: TUserCreateInput) => {
|
||||
count: true,
|
||||
});
|
||||
|
||||
// send new user customer.io to customer.io
|
||||
createCustomerIoCustomer({ id: user.id, email: user.email });
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { getUserByEmail, updateUser } from "@/modules/auth/lib/user";
|
||||
import { createUser } from "@/modules/auth/lib/user";
|
||||
import type { IdentityProvider } from "@prisma/client";
|
||||
@@ -78,6 +79,9 @@ export const handleSSOCallback = async ({ user, account }: { user: TUser; accoun
|
||||
locale: await findMatchingLocale(),
|
||||
});
|
||||
|
||||
// send new user to brevo
|
||||
createBrevoCustomer({ id: user.id, email: user.email });
|
||||
|
||||
// Default organization assignment if env variable is set
|
||||
if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) {
|
||||
// check if organization exists
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"next-auth": "4.24.11",
|
||||
"next-intl": "3.26.1",
|
||||
"next-safe-action": "7.10.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.16",
|
||||
"optional": "0.1.4",
|
||||
"otplib": "12.0.1",
|
||||
|
||||
@@ -188,8 +188,9 @@ export const REDIS_URL = env.REDIS_URL;
|
||||
export const REDIS_HTTP_URL = env.REDIS_HTTP_URL;
|
||||
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
|
||||
|
||||
export const CUSTOMER_IO_SITE_ID = env.CUSTOMER_IO_SITE_ID;
|
||||
export const CUSTOMER_IO_API_KEY = env.CUSTOMER_IO_API_KEY;
|
||||
export const BREVO_API_KEY = env.BREVO_API_KEY;
|
||||
export const BREVO_LIST_ID = env.BREVO_LIST_ID;
|
||||
|
||||
export const UNSPLASH_ACCESS_KEY = env.UNSPLASH_ACCESS_KEY;
|
||||
export const UNSPLASH_ALLOWED_DOMAINS = ["api.unsplash.com"];
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { TUserEmail, ZUserEmail } from "@formbricks/types/user";
|
||||
import { CUSTOMER_IO_API_KEY, CUSTOMER_IO_SITE_ID } from "./constants";
|
||||
import { validateInputs } from "./utils/validate";
|
||||
|
||||
export const createCustomerIoCustomer = async ({ id, email }: { id: string; email: TUserEmail }) => {
|
||||
if (!CUSTOMER_IO_SITE_ID || !CUSTOMER_IO_API_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateInputs([id, ZId], [email, ZUserEmail]);
|
||||
|
||||
try {
|
||||
const auth = Buffer.from(`${CUSTOMER_IO_SITE_ID}:${CUSTOMER_IO_API_KEY}`).toString("base64");
|
||||
const res = await fetch(`https://track-eu.customer.io/api/v1/customers/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: id,
|
||||
email: email,
|
||||
}),
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
console.log("Error sending user to CustomerIO:", await res.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error sending user to CustomerIO:", error);
|
||||
}
|
||||
};
|
||||
@@ -18,8 +18,8 @@ export const env = createEnv({
|
||||
AZUREAD_CLIENT_SECRET: z.string().optional(),
|
||||
AZUREAD_TENANT_ID: z.string().optional(),
|
||||
CRON_SECRET: z.string().min(10),
|
||||
CUSTOMER_IO_API_KEY: z.string().optional(),
|
||||
CUSTOMER_IO_SITE_ID: z.string().optional(),
|
||||
BREVO_API_KEY: z.string().optional(),
|
||||
BREVO_LIST_ID: z.string().optional(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
DEBUG: z.enum(["1", "0"]).optional(),
|
||||
DEFAULT_ORGANIZATION_ID: z.string().optional(),
|
||||
@@ -141,9 +141,9 @@ export const env = createEnv({
|
||||
AZUREAD_CLIENT_ID: process.env.AZUREAD_CLIENT_ID,
|
||||
AZUREAD_CLIENT_SECRET: process.env.AZUREAD_CLIENT_SECRET,
|
||||
AZUREAD_TENANT_ID: process.env.AZUREAD_TENANT_ID,
|
||||
BREVO_API_KEY: process.env.BREVO_API_KEY,
|
||||
BREVO_LIST_ID: process.env.BREVO_LIST_ID,
|
||||
CRON_SECRET: process.env.CRON_SECRET,
|
||||
CUSTOMER_IO_API_KEY: process.env.CUSTOMER_IO_API_KEY,
|
||||
CUSTOMER_IO_SITE_ID: process.env.CUSTOMER_IO_SITE_ID,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
DEBUG: process.env.DEBUG,
|
||||
DEFAULT_ORGANIZATION_ID: process.env.DEFAULT_ORGANIZATION_ID,
|
||||
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -154,7 +154,7 @@ importers:
|
||||
version: 8.14.0
|
||||
autoprefixer:
|
||||
specifier: 10.4.20
|
||||
version: 10.4.20(postcss@8.4.49)
|
||||
version: 10.4.20(postcss@8.5.1)
|
||||
clsx:
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
@@ -513,7 +513,7 @@ importers:
|
||||
version: 4.0.18(react@19.0.0)(zod@3.24.1)
|
||||
autoprefixer:
|
||||
specifier: 10.4.20
|
||||
version: 10.4.20(postcss@8.5.1)
|
||||
version: 10.4.20(postcss@8.4.49)
|
||||
bcryptjs:
|
||||
specifier: 2.4.3
|
||||
version: 2.4.3
|
||||
@@ -586,6 +586,9 @@ importers:
|
||||
next-safe-action:
|
||||
specifier: 7.10.2
|
||||
version: 7.10.2(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1)
|
||||
node-fetch:
|
||||
specifier: 3.3.2
|
||||
version: 3.3.2
|
||||
nodemailer:
|
||||
specifier: 6.9.16
|
||||
version: 6.9.16
|
||||
|
||||
@@ -84,12 +84,12 @@
|
||||
"AZUREAD_CLIENT_ID",
|
||||
"AZUREAD_CLIENT_SECRET",
|
||||
"AZUREAD_TENANT_ID",
|
||||
"BREVO_API_KEY",
|
||||
"BREVO_LIST_ID",
|
||||
"DEFAULT_ORGANIZATION_ID",
|
||||
"DEFAULT_ORGANIZATION_ROLE",
|
||||
"CRON_SECRET",
|
||||
"CUSTOM_CACHE_DISABLED",
|
||||
"CUSTOMER_IO_API_KEY",
|
||||
"CUSTOMER_IO_SITE_ID",
|
||||
"DATABASE_URL",
|
||||
"DEBUG",
|
||||
"E2E_TESTING",
|
||||
|
||||
Reference in New Issue
Block a user