fix: added cache no-store when formbricksDebug is enabled (#5197)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Dhruwang Jariwala
2025-04-02 10:49:27 +05:30
committed by GitHub
parent e347f2179a
commit bf39b0fbfb
14 changed files with 48 additions and 251 deletions

View File

@@ -1,31 +0,0 @@
import { type TAttributeUpdateInput } from "@formbricks/types/attributes";
import { type Result } from "@formbricks/types/error-handlers";
import { type ApiErrorResponse } from "@formbricks/types/errors";
import { makeRequest } from "../../utils/make-request";
export class AttributeAPI {
private appUrl: string;
private environmentId: string;
constructor(appUrl: string, environmentId: string) {
this.appUrl = appUrl;
this.environmentId = environmentId;
}
async update(
attributeUpdateInput: Omit<TAttributeUpdateInput, "environmentId">
): Promise<Result<{ changed: boolean; message: string; messages?: string[] }, ApiErrorResponse>> {
// transform all attributes to string if attributes are present into a new attributes copy
const attributes: Record<string, string> = {};
for (const key in attributeUpdateInput.attributes) {
attributes[key] = String(attributeUpdateInput.attributes[key]);
}
return makeRequest(
this.appUrl,
`/api/v1/client/${this.environmentId}/contacts/${attributeUpdateInput.userId}/attributes`,
"PUT",
{ attributes }
);
}
}

View File

@@ -1,20 +0,0 @@
import { type TDisplayCreateInput } from "@formbricks/types/displays";
import { type Result } from "@formbricks/types/error-handlers";
import { type ApiErrorResponse } from "@formbricks/types/errors";
import { makeRequest } from "../../utils/make-request";
export class DisplayAPI {
private appUrl: string;
private environmentId: string;
constructor(appUrl: string, environmentId: string) {
this.appUrl = appUrl;
this.environmentId = environmentId;
}
async create(
displayInput: Omit<TDisplayCreateInput, "environmentId">
): Promise<Result<{ id: string }, ApiErrorResponse>> {
return makeRequest(this.appUrl, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput);
}
}

View File

@@ -6,13 +6,21 @@ import { makeRequest } from "../../utils/make-request";
export class EnvironmentAPI {
private appUrl: string;
private environmentId: string;
private isDebug: boolean;
constructor(appUrl: string, environmentId: string) {
constructor(appUrl: string, environmentId: string, isDebug: boolean) {
this.appUrl = appUrl;
this.environmentId = environmentId;
this.isDebug = isDebug;
}
async getState(): Promise<Result<TJsEnvironmentState, ApiErrorResponse>> {
return makeRequest(this.appUrl, `/api/v1/client/${this.environmentId}/environment`, "GET");
return makeRequest(
this.appUrl,
`/api/v1/client/${this.environmentId}/environment`,
"GET",
undefined,
this.isDebug
);
}
}

View File

@@ -1,27 +1,16 @@
import { type ApiConfig } from "../../types";
import { AttributeAPI } from "./attribute";
import { DisplayAPI } from "./display";
import { EnvironmentAPI } from "./environment";
import { ResponseAPI } from "./response";
import { StorageAPI } from "./storage";
import { UserAPI } from "./user";
export class Client {
response: ResponseAPI;
display: DisplayAPI;
storage: StorageAPI;
attribute: AttributeAPI;
user: UserAPI;
environment: EnvironmentAPI;
constructor(options: ApiConfig) {
const { appUrl, environmentId } = options;
const { appUrl, environmentId, isDebug } = options;
const isDebugMode = isDebug ?? false;
this.response = new ResponseAPI(appUrl, environmentId);
this.display = new DisplayAPI(appUrl, environmentId);
this.attribute = new AttributeAPI(appUrl, environmentId);
this.storage = new StorageAPI(appUrl, environmentId);
this.user = new UserAPI(appUrl, environmentId);
this.environment = new EnvironmentAPI(appUrl, environmentId);
this.user = new UserAPI(appUrl, environmentId, isDebugMode);
this.environment = new EnvironmentAPI(appUrl, environmentId, isDebugMode);
}
}

View File

@@ -1,41 +0,0 @@
import { type Result } from "@formbricks/types/error-handlers";
import { type ApiErrorResponse } from "@formbricks/types/errors";
import { type TResponseInput, type TResponseUpdateInput } from "@formbricks/types/responses";
import { makeRequest } from "../../utils/make-request";
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
export class ResponseAPI {
private appUrl: string;
private environmentId: string;
constructor(appUrl: string, environmentId: string) {
this.appUrl = appUrl;
this.environmentId = environmentId;
}
async create(
responseInput: Omit<TResponseInput, "environmentId">
): Promise<Result<{ id: string }, ApiErrorResponse>> {
return makeRequest(this.appUrl, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
}
async update({
responseId,
finished,
endingId,
data,
ttc,
variables,
language,
}: TResponseUpdateInputWithResponseId): Promise<Result<object, ApiErrorResponse>> {
return makeRequest(this.appUrl, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
finished,
endingId,
data,
ttc,
variables,
language,
});
}
}

View File

@@ -1,127 +0,0 @@
/* eslint-disable no-console -- used for error logging */
import type { TUploadFileConfig, TUploadFileResponse } from "@formbricks/types/storage";
export class StorageAPI {
private appUrl: string;
private environmentId: string;
constructor(appUrl: string, environmentId: string) {
this.appUrl = appUrl;
this.environmentId = environmentId;
}
async uploadFile(
file: {
type: string;
name: string;
base64: string;
},
{ allowedFileExtensions, surveyId }: TUploadFileConfig | undefined = {}
): Promise<string> {
if (!file.name || !file.type || !file.base64) {
throw new Error(`Invalid file object`);
}
const payload = {
fileName: file.name,
fileType: file.type,
allowedFileExtensions,
surveyId,
};
const response = await fetch(`${this.appUrl}/api/v1/client/${this.environmentId}/storage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Upload failed with status: ${String(response.status)}`);
}
const json = (await response.json()) as TUploadFileResponse;
const { data } = json;
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
let localUploadDetails: Record<string, string> = {};
if (signingData) {
const { signature, timestamp, uuid } = signingData;
localUploadDetails = {
fileType: file.type,
fileName: encodeURIComponent(updatedFileName),
surveyId: surveyId ?? "",
signature,
timestamp: String(timestamp),
uuid,
};
}
const formData: Record<string, string> = {};
const formDataForS3 = new FormData();
if (presignedFields) {
Object.entries(presignedFields).forEach(([key, value]) => {
formDataForS3.append(key, value);
});
try {
const binaryString = atob(file.base64.split(",")[1]);
const uint8Array = Uint8Array.from([...binaryString].map((char) => char.charCodeAt(0)));
const blob = new Blob([uint8Array], { type: file.type });
formDataForS3.append("file", blob);
} catch (err) {
console.error(err);
throw new Error("Error uploading file");
}
}
formData.fileBase64String = file.base64;
let uploadResponse: Response = {} as Response;
const signedUrlCopy = signedUrl.replace("http://localhost:3000", this.appUrl);
try {
uploadResponse = await fetch(signedUrlCopy, {
method: "POST",
body: presignedFields
? formDataForS3
: JSON.stringify({
...formData,
...localUploadDetails,
}),
});
} catch (err) {
console.error("Error uploading file", err);
}
if (!uploadResponse.ok) {
// if local storage is used, we'll use the json response:
if (signingData) {
const uploadJson = (await uploadResponse.json()) as { message: string };
const error = new Error(uploadJson.message);
error.name = "FileTooLargeError";
throw error;
}
// if s3 is used, we'll use the text response:
const errorText = await uploadResponse.text();
if (presignedFields && errorText.includes("EntityTooLarge")) {
const error = new Error("File size exceeds the size limit for your plan");
error.name = "FileTooLargeError";
throw error;
}
throw new Error(`Upload failed with status: ${String(uploadResponse.status)}`);
}
return fileUrl;
}
}

View File

@@ -5,10 +5,12 @@ import { makeRequest } from "../../utils/make-request";
export class UserAPI {
private appUrl: string;
private environmentId: string;
private isDebug: boolean;
constructor(appUrl: string, environmentId: string) {
constructor(appUrl: string, environmentId: string, isDebug: boolean) {
this.appUrl = appUrl;
this.environmentId = environmentId;
this.isDebug = isDebug;
}
async createOrUpdate(userUpdateInput: { userId: string; attributes?: Record<string, string> }): Promise<
@@ -37,9 +39,15 @@ export class UserAPI {
attributes[key] = String(userUpdateInput.attributes[key]);
}
return makeRequest(this.appUrl, `/api/v2/client/${this.environmentId}/user`, "POST", {
userId: userUpdateInput.userId,
attributes,
});
return makeRequest(
this.appUrl,
`/api/v2/client/${this.environmentId}/user`,
"POST",
{
userId: userUpdateInput.userId,
attributes,
},
this.isDebug
);
}
}

View File

@@ -3,6 +3,7 @@ import { type ApiErrorResponse } from "@formbricks/types/errors";
export interface ApiConfig {
environmentId: string;
appUrl: string;
isDebug?: boolean;
}
export type ApiResponse = ApiSuccessResponse | ApiErrorResponse;

View File

@@ -6,15 +6,16 @@ export const makeRequest = async <T>(
appUrl: string,
endpoint: string,
method: "GET" | "POST" | "PUT" | "DELETE",
data?: unknown
data?: unknown,
isDebug?: boolean
): Promise<Result<T, ApiErrorResponse>> => {
const url = new URL(appUrl + endpoint);
const body = data ? JSON.stringify(data) : undefined;
const res = await wrapThrowsAsync(fetch)(url.toString(), {
method,
headers: {
"Content-Type": "application/json",
...(isDebug && { "Cache-Control": "no-cache" }),
},
body,
});

View File

@@ -179,7 +179,11 @@ export const setup = async (
let environmentState: TEnvironmentState = existingConfig.environment;
let userState: TUserState = existingConfig.user;
if (isEnvironmentStateExpired) {
if (isEnvironmentStateExpired || isDebug) {
if (isDebug) {
logger.debug("Debug mode is active, refetching environment state");
}
const environmentStateResponse = await fetchEnvironmentState({
appUrl: configInput.appUrl,
environmentId: configInput.environmentId,
@@ -201,10 +205,14 @@ export const setup = async (
}
}
if (isUserStateExpired) {
if (isUserStateExpired || isDebug) {
// If the existing person state (expired) has a userId, we need to fetch the person state
// If the existing person state (expired) has no userId, we need to set the person state to the default
if (isDebug) {
logger.debug("Debug mode is active, refetching user state");
}
if (userState.data.userId) {
const updatesResponse = await sendUpdatesToBackend({
appUrl: configInput.appUrl,
@@ -230,7 +238,7 @@ export const setup = async (
responseMessage: "Unknown error",
});
}
} else {
} else if (!isDebug) {
userState = DEFAULT_USER_STATE_NO_USER_ID;
}
}
@@ -271,7 +279,6 @@ export const setup = async (
throw environmentStateResponse.error;
}
// const personState = DEFAULT_USER_STATE_NO_USER_ID;
let userState: TUserState = DEFAULT_USER_STATE_NO_USER_ID;
if ("userId" in configInput && configInput.userId) {

View File

@@ -2,7 +2,7 @@
import { FormbricksAPI } from "@formbricks/api";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys } from "@/lib/common/utils";
import { filterSurveys, getIsDebug } from "@/lib/common/utils";
import type { TConfigInput, TEnvironmentState } from "@/types/config";
import { type ApiErrorResponse, type Result, err, ok } from "@/types/error";
@@ -20,7 +20,7 @@ export const fetchEnvironmentState = async ({
environmentId,
}: TConfigInput): Promise<Result<TEnvironmentState, ApiErrorResponse>> => {
const url = `${appUrl}/api/v1/client/${environmentId}/environment`;
const api = new FormbricksAPI({ appUrl, environmentId });
const api = new FormbricksAPI({ appUrl, environmentId, isDebug: getIsDebug() });
try {
const response = await api.client.environment.getState();

View File

@@ -37,6 +37,7 @@ vi.mock("@/lib/common/logger", () => ({
// Mock filterSurveys
vi.mock("@/lib/common/utils", () => ({
filterSurveys: vi.fn(),
getIsDebug: vi.fn(),
}));
// Mock Config

View File

@@ -30,6 +30,7 @@ vi.mock("@/lib/common/logger", () => ({
vi.mock("@/lib/common/utils", () => ({
filterSurveys: vi.fn(),
getIsDebug: vi.fn(),
}));
vi.mock("@formbricks/api", () => ({

View File

@@ -2,7 +2,7 @@
import { FormbricksAPI } from "@formbricks/api";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys } from "@/lib/common/utils";
import { filterSurveys, getIsDebug } from "@/lib/common/utils";
import { type TUpdates, type TUserState } from "@/types/config";
import { type ApiErrorResponse, type Result, type ResultError, err, ok, okVoid } from "@/types/error";
@@ -26,7 +26,7 @@ export const sendUpdatesToBackend = async ({
const url = `${appUrl}/api/v1/client/${environmentId}/user`;
try {
const api = new FormbricksAPI({ appUrl, environmentId });
const api = new FormbricksAPI({ appUrl, environmentId, isDebug: getIsDebug() });
const response = await api.client.user.createOrUpdate({
userId: updates.userId,