mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 14:29:20 -06:00
fix: adds vercel style guide to @formbricks/js-core (#4520)
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
// method -> PUT (to be the same as the signedUrl method)
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { getBiggerUploadFileSizePermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { headers } from "next/headers";
|
||||
import { NextRequest } from "next/server";
|
||||
import { ENCRYPTION_KEY, UPLOADS_DIR } from "@formbricks/lib/constants";
|
||||
import { validateLocalSignedUrl } from "@formbricks/lib/crypto";
|
||||
@@ -24,8 +23,7 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers":
|
||||
"Content-Type, Authorization, X-File-Name, X-File-Type, X-Survey-ID, X-Signature, X-Timestamp, X-UUID",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -36,15 +34,14 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
|
||||
const environmentId = params.environmentId;
|
||||
|
||||
const accessType = "private"; // private files are accessible only by authorized users
|
||||
const headersList = await headers();
|
||||
|
||||
const fileType = headersList.get("X-File-Type");
|
||||
const encodedFileName = headersList.get("X-File-Name");
|
||||
const surveyId = headersList.get("X-Survey-ID");
|
||||
|
||||
const signedSignature = headersList.get("X-Signature");
|
||||
const signedUuid = headersList.get("X-UUID");
|
||||
const signedTimestamp = headersList.get("X-Timestamp");
|
||||
const formData = await req.json();
|
||||
const fileType = formData.fileType as string;
|
||||
const encodedFileName = formData.fileName as string;
|
||||
const surveyId = formData.surveyId as string;
|
||||
const signedSignature = formData.signature as string;
|
||||
const signedUuid = formData.uuid as string;
|
||||
const signedTimestamp = formData.timestamp as string;
|
||||
|
||||
if (!fileType) {
|
||||
return responses.badRequestResponse("contentType is required");
|
||||
@@ -101,7 +98,6 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const formData = await req.json();
|
||||
const base64String = formData.fileBase64String as string;
|
||||
|
||||
const buffer = Buffer.from(base64String.split(",")[1], "base64");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type TAttributeUpdateInput } from "@formbricks/types/attributes";
|
||||
import { type Result } from "@formbricks/types/error-handlers";
|
||||
import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { makeRequest } from "../../utils/make-request";
|
||||
|
||||
export class AttributeAPI {
|
||||
@@ -15,10 +15,7 @@ export class AttributeAPI {
|
||||
async update(
|
||||
attributeUpdateInput: Omit<TAttributeUpdateInput, "environmentId">
|
||||
): Promise<
|
||||
Result<
|
||||
{ changed: boolean; message: string; details?: Record<string, string> },
|
||||
NetworkError | Error | ForbiddenError
|
||||
>
|
||||
Result<{ changed: boolean; message: string; details?: Record<string, string> }, ApiErrorResponse>
|
||||
> {
|
||||
// transform all attributes to string if attributes are present into a new attributes copy
|
||||
const attributes: Record<string, string> = {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type TDisplayCreateInput } from "@formbricks/types/displays";
|
||||
import { type Result } from "@formbricks/types/error-handlers";
|
||||
import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { makeRequest } from "../../utils/make-request";
|
||||
|
||||
export class DisplayAPI {
|
||||
@@ -14,7 +14,7 @@ export class DisplayAPI {
|
||||
|
||||
async create(
|
||||
displayInput: Omit<TDisplayCreateInput, "environmentId">
|
||||
): Promise<Result<{ id: string }, ForbiddenError | NetworkError | Error>> {
|
||||
): Promise<Result<{ id: string }, ApiErrorResponse>> {
|
||||
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Result } from "@formbricks/types/error-handlers";
|
||||
import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { type TResponseInput, type TResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { makeRequest } from "../../utils/make-request";
|
||||
|
||||
@@ -16,7 +16,7 @@ export class ResponseAPI {
|
||||
|
||||
async create(
|
||||
responseInput: Omit<TResponseInput, "environmentId">
|
||||
): Promise<Result<{ id: string }, ForbiddenError | NetworkError | Error>> {
|
||||
): Promise<Result<{ id: string }, ApiErrorResponse>> {
|
||||
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ResponseAPI {
|
||||
ttc,
|
||||
variables,
|
||||
language,
|
||||
}: TResponseUpdateInputWithResponseId): Promise<Result<object, NetworkError | Error | ForbiddenError>> {
|
||||
}: TResponseUpdateInputWithResponseId): Promise<Result<object, ApiErrorResponse>> {
|
||||
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
|
||||
finished,
|
||||
endingId,
|
||||
|
||||
@@ -47,18 +47,18 @@ export class StorageAPI {
|
||||
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
let localUploadDetails: Record<string, string> = {};
|
||||
|
||||
if (signingData) {
|
||||
const { signature, timestamp, uuid } = signingData;
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-Survey-ID": surveyId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
"X-UUID": uuid,
|
||||
localUploadDetails = {
|
||||
fileType: file.type,
|
||||
fileName: encodeURIComponent(updatedFileName),
|
||||
surveyId: surveyId ?? "",
|
||||
signature,
|
||||
timestamp: String(timestamp),
|
||||
uuid,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,14 +91,12 @@ export class StorageAPI {
|
||||
try {
|
||||
uploadResponse = await fetch(signedUrlCopy, {
|
||||
method: "POST",
|
||||
...(signingData
|
||||
? {
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
body: presignedFields ? formDataForS3 : JSON.stringify(formData),
|
||||
body: presignedFields
|
||||
? formDataForS3
|
||||
: JSON.stringify({
|
||||
...formData,
|
||||
...localUploadDetails,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error uploading file", err);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
|
||||
export interface ApiConfig {
|
||||
environmentId: string;
|
||||
apiHost: string;
|
||||
@@ -8,17 +10,3 @@ export type ApiResponse = ApiSuccessResponse | ApiErrorResponse;
|
||||
export interface ApiSuccessResponse<T = Record<string, unknown>> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
code:
|
||||
| "not_found"
|
||||
| "gone"
|
||||
| "bad_request"
|
||||
| "internal_server_error"
|
||||
| "unauthorized"
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden";
|
||||
message: string;
|
||||
details: Record<string, string | string[] | number | number[] | boolean | boolean[]>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import { type ForbiddenError, type NetworkError } from "@formbricks/types/errors";
|
||||
import type { ApiErrorResponse, ApiResponse, ApiSuccessResponse } from "../types";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { type ApiResponse, type ApiSuccessResponse } from "../types";
|
||||
|
||||
export const makeRequest = async <T>(
|
||||
apiHost: string,
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
data?: unknown
|
||||
): Promise<Result<T, NetworkError | Error | ForbiddenError>> => {
|
||||
): Promise<Result<T, ApiErrorResponse>> => {
|
||||
const url = new URL(apiHost + endpoint);
|
||||
const body = data ? JSON.stringify(data) : undefined;
|
||||
|
||||
@@ -19,7 +19,8 @@ export const makeRequest = async <T>(
|
||||
body,
|
||||
});
|
||||
|
||||
if (!res.ok) return err(res.error);
|
||||
// TODO: Only return api error response relevant keys
|
||||
if (!res.ok) return err(res.error as unknown as ApiErrorResponse);
|
||||
|
||||
const response = res.data;
|
||||
const json = (await response.json()) as ApiResponse;
|
||||
@@ -31,7 +32,7 @@ export const makeRequest = async <T>(
|
||||
status: response.status,
|
||||
message: errorResponse.message || "Something went wrong",
|
||||
url,
|
||||
...(Object.keys(errorResponse.details).length > 0 && { details: errorResponse.details }),
|
||||
...(Object.keys(errorResponse.details ?? {}).length > 0 && { details: errorResponse.details }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/legacy-library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: ["@formbricks/eslint-config/library.js"],
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { TJsConfigInput, TJsTrackProperties } from "@formbricks/types/js";
|
||||
/* eslint-disable import/no-default-export -- We need default exports for the js sdk */
|
||||
import { type TJsConfigInput, type TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { trackCodeAction } from "./lib/actions";
|
||||
import { getApi } from "./lib/api";
|
||||
import { setAttributeInApp } from "./lib/attributes";
|
||||
import { CommandQueue } from "./lib/commandQueue";
|
||||
import { CommandQueue } from "./lib/command-queue";
|
||||
import { ErrorHandler } from "./lib/errors";
|
||||
import { initialize } from "./lib/initialize";
|
||||
import { Logger } from "./lib/logger";
|
||||
import { checkPageUrl } from "./lib/noCodeActions";
|
||||
import { checkPageUrl } from "./lib/no-code-actions";
|
||||
import { logoutPerson, resetPerson } from "./lib/person";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
@@ -14,18 +15,18 @@ const logger = Logger.getInstance();
|
||||
logger.debug("Create command queue");
|
||||
const queue = new CommandQueue();
|
||||
|
||||
const init = async (initConfig: TJsConfigInput) => {
|
||||
const init = async (initConfig: TJsConfigInput): Promise<void> => {
|
||||
ErrorHandler.init(initConfig.errorHandler);
|
||||
queue.add(false, initialize, initConfig);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const setEmail = async (email: string): Promise<void> => {
|
||||
setAttribute("email", email);
|
||||
await setAttribute("email", email);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const setAttribute = async (key: string, value: any): Promise<void> => {
|
||||
const setAttribute = async (key: string, value: string): Promise<void> => {
|
||||
queue.add(true, setAttributeInApp, key, value);
|
||||
await queue.wait();
|
||||
};
|
||||
@@ -40,8 +41,8 @@ const reset = async (): Promise<void> => {
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
const track = async (name: string, properties?: TJsTrackProperties): Promise<void> => {
|
||||
queue.add<any>(true, trackCodeAction, name, properties);
|
||||
const track = async (code: string, properties?: TJsTrackProperties): Promise<void> => {
|
||||
queue.add(true, trackCodeAction, code, properties);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
@@ -61,5 +62,5 @@ const formbricks = {
|
||||
getApi,
|
||||
};
|
||||
|
||||
export type TFormbricksApp = typeof formbricks;
|
||||
export default formbricks as TFormbricksApp;
|
||||
export type TFormbricks = typeof formbricks;
|
||||
export default formbricks;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { type TJsTrackProperties } from "@formbricks/types/js";
|
||||
import { Config } from "./config";
|
||||
import { InvalidCodeError, NetworkError, Result, err, okVoid } from "./errors";
|
||||
import { type InvalidCodeError, type NetworkError, type Result, err, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { triggerSurvey } from "./widget";
|
||||
|
||||
@@ -12,14 +12,14 @@ export const trackAction = async (
|
||||
alias?: string,
|
||||
properties?: TJsTrackProperties
|
||||
): Promise<Result<void, NetworkError>> => {
|
||||
const aliasName = alias || name;
|
||||
const aliasName = alias ?? name;
|
||||
|
||||
logger.debug(`Formbricks: Action "${aliasName}" tracked`);
|
||||
|
||||
// get a list of surveys that are collecting insights
|
||||
const activeSurveys = config.get().filteredSurveys;
|
||||
|
||||
if (!!activeSurveys && activeSurveys.length > 0) {
|
||||
if (Boolean(activeSurveys) && activeSurveys.length > 0) {
|
||||
for (const survey of activeSurveys) {
|
||||
for (const trigger of survey.triggers) {
|
||||
if (trigger.actionClass.name === name) {
|
||||
@@ -41,7 +41,7 @@ export const trackCodeAction = (
|
||||
const actionClasses = config.get().environmentState.data.actionClasses;
|
||||
|
||||
const codeActionClasses = actionClasses.filter((action) => action.type === "code");
|
||||
const action = codeActionClasses.find((action) => action.key === code);
|
||||
const action = codeActionClasses.find((codeActionClass) => codeActionClass.key === code);
|
||||
|
||||
if (!action) {
|
||||
return err({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { ForbiddenError } from "@formbricks/types/errors";
|
||||
import { type TAttributes } from "@formbricks/types/attributes";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { Config } from "./config";
|
||||
import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "./errors";
|
||||
import { type Result, err, ok, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { fetchPersonState } from "./personState";
|
||||
import { fetchPersonState } from "./person-state";
|
||||
import { filterSurveys } from "./utils";
|
||||
|
||||
const config = Config.getInstance();
|
||||
@@ -20,7 +20,7 @@ export const updateAttribute = async (
|
||||
message: string;
|
||||
details?: Record<string, string>;
|
||||
},
|
||||
NetworkError | ForbiddenError
|
||||
ApiErrorResponse
|
||||
>
|
||||
> => {
|
||||
const { apiHost, environmentId } = config.get();
|
||||
@@ -31,7 +31,7 @@ export const updateAttribute = async (
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
message: "Missing userId",
|
||||
url: `${apiHost}/api/v1/client/${environmentId}/contacts/${userId}/attributes`,
|
||||
url: new URL(`${apiHost}/api/v1/client/${environmentId}/contacts/${userId ?? ""}/attributes`),
|
||||
responseMessage: "Missing userId",
|
||||
});
|
||||
}
|
||||
@@ -44,9 +44,9 @@ export const updateAttribute = async (
|
||||
const res = await api.client.attribute.update({ userId, attributes: { [key]: value } });
|
||||
|
||||
if (!res.ok) {
|
||||
// @ts-expect-error
|
||||
if (res.error.details?.ignore) {
|
||||
logger.error(res.error.message ?? `Error updating person with userId ${userId}`);
|
||||
const responseError = res.error;
|
||||
if (responseError.details?.ignore) {
|
||||
logger.error(responseError.message);
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
@@ -57,8 +57,8 @@ export const updateAttribute = async (
|
||||
}
|
||||
|
||||
return err({
|
||||
code: (res.error as ForbiddenError).code ?? "network_error",
|
||||
status: (res.error as NetworkError | ForbiddenError).status ?? 500,
|
||||
code: res.error.code,
|
||||
status: res.error.status,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: new URL(`${apiHost}/api/v1/client/${environmentId}/contacts/${userId}/attributes`),
|
||||
responseMessage: res.error.message,
|
||||
@@ -66,8 +66,8 @@ export const updateAttribute = async (
|
||||
}
|
||||
|
||||
if (res.data.details) {
|
||||
Object.entries(res.data.details).forEach(([key, value]) => {
|
||||
logger.error(`${key}: ${value}`);
|
||||
Object.entries(res.data.details).forEach(([detailsKey, detailsValue]) => {
|
||||
logger.error(`${detailsKey}: ${detailsValue}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export const updateAttributes = async (
|
||||
environmentId: string,
|
||||
userId: string,
|
||||
attributes: TAttributes
|
||||
): Promise<Result<TAttributes, NetworkError | ForbiddenError>> => {
|
||||
): Promise<Result<TAttributes, ApiErrorResponse>> => {
|
||||
// clean attributes and remove existing attributes if config already exists
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
@@ -113,7 +113,7 @@ export const updateAttributes = async (
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes));
|
||||
logger.debug(`Updating attributes: ${JSON.stringify(updatedAttributes)}`);
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost,
|
||||
@@ -130,27 +130,28 @@ export const updateAttributes = async (
|
||||
}
|
||||
|
||||
return ok(updatedAttributes);
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
if (res.error.details?.ignore) {
|
||||
logger.error(res.error.message ?? `Error updating person with userId ${userId}`);
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
return err({
|
||||
code: (res.error as ForbiddenError).code ?? "network_error",
|
||||
status: (res.error as NetworkError | ForbiddenError).status ?? 500,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`),
|
||||
responseMessage: res.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const responseError = res.error;
|
||||
|
||||
if (responseError.details?.ignore) {
|
||||
logger.error(responseError.message);
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
return err({
|
||||
code: responseError.code,
|
||||
status: responseError.status,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`),
|
||||
responseMessage: responseError.responseMessage,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAttributeInApp = async (
|
||||
key: string,
|
||||
value: any
|
||||
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
||||
value: string
|
||||
): Promise<Result<void, ApiErrorResponse>> => {
|
||||
if (key === "userId") {
|
||||
logger.error("Setting userId is no longer supported. Please set the userId in the init call instead.");
|
||||
return okVoid();
|
||||
@@ -158,7 +159,7 @@ export const setAttributeInApp = async (
|
||||
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
logger.debug("Setting attribute: " + key + " to value: " + value);
|
||||
logger.debug(`Setting attribute: ${key} to value: ${value}`);
|
||||
|
||||
if (!userId) {
|
||||
logger.error(
|
||||
@@ -194,12 +195,11 @@ export const setAttributeInApp = async (
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
} else {
|
||||
const error = result.error;
|
||||
if (error && error.code === "forbidden") {
|
||||
logger.error(`Authorization error: ${error.responseMessage}`);
|
||||
}
|
||||
}
|
||||
const error = result.error;
|
||||
if (error.code === "forbidden") {
|
||||
logger.error(`Authorization error: ${error.responseMessage ?? ""}`);
|
||||
}
|
||||
|
||||
return err(result.error as NetworkError);
|
||||
return err(result.error);
|
||||
};
|
||||
|
||||
91
packages/js-core/src/lib/command-queue.ts
Normal file
91
packages/js-core/src/lib/command-queue.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import { ErrorHandler, type Result } from "./errors";
|
||||
import { checkInitialized } from "./initialize";
|
||||
|
||||
// Define a base type for acceptable return types
|
||||
type CommandReturnType = Promise<Result<void, unknown>> | Result<void, unknown> | Promise<void>;
|
||||
|
||||
// Define a type for functions that return our accepted return types
|
||||
type CommandFunction<Args extends unknown[] = unknown[]> = (...args: Args) => CommandReturnType;
|
||||
|
||||
type TArgs = unknown[];
|
||||
|
||||
// Define a queue item type that's generic over the function and its arguments
|
||||
interface QueueItem<F extends CommandFunction> {
|
||||
command: F;
|
||||
checkInitialized: boolean;
|
||||
commandArgs: Parameters<F>;
|
||||
}
|
||||
|
||||
export class CommandQueue {
|
||||
private queue: QueueItem<CommandFunction>[] = [];
|
||||
private running = false;
|
||||
private resolvePromise: (() => void) | null = null;
|
||||
private commandPromise: Promise<void> | null = null;
|
||||
|
||||
// Make add generic over the function type and its parameters
|
||||
public add<Args extends TArgs>(
|
||||
checkInitializedArg: boolean,
|
||||
command: CommandFunction<Args>,
|
||||
...args: Args
|
||||
): void {
|
||||
this.queue.push({
|
||||
command: command as CommandFunction,
|
||||
checkInitialized: checkInitializedArg,
|
||||
commandArgs: args,
|
||||
});
|
||||
|
||||
if (!this.running) {
|
||||
this.commandPromise = new Promise((resolve) => {
|
||||
this.resolvePromise = resolve;
|
||||
void this.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async wait(): Promise<void> {
|
||||
if (this.running) {
|
||||
await this.commandPromise;
|
||||
}
|
||||
}
|
||||
|
||||
private async run(): Promise<void> {
|
||||
this.running = true;
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
const currentItem = this.queue.shift();
|
||||
if (!currentItem) continue;
|
||||
|
||||
if (currentItem.checkInitialized) {
|
||||
const initResult = checkInitialized();
|
||||
if (!initResult.ok) {
|
||||
errorHandler.handle(initResult.error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const executeCommand = async (): Promise<Result<void, unknown>> => {
|
||||
return (await currentItem.command.apply(null, currentItem.commandArgs)) as Result<void, unknown>;
|
||||
};
|
||||
|
||||
const result = await wrapThrowsAsync(executeCommand)();
|
||||
|
||||
if (result.ok) {
|
||||
if (!result.data.ok) {
|
||||
errorHandler.handle(result.data.error);
|
||||
}
|
||||
}
|
||||
if (!result.ok) {
|
||||
errorHandler.handle(result.error);
|
||||
}
|
||||
}
|
||||
|
||||
this.running = false;
|
||||
if (this.resolvePromise) {
|
||||
this.resolvePromise();
|
||||
this.resolvePromise = null;
|
||||
this.commandPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import { ErrorHandler, Result } from "./errors";
|
||||
import { checkInitialized } from "./initialize";
|
||||
|
||||
export class CommandQueue {
|
||||
private queue: {
|
||||
command: (args: any) => Promise<Result<void, any>> | Result<void, any> | Promise<void>;
|
||||
checkInitialized: boolean;
|
||||
commandArgs: any[any];
|
||||
}[] = [];
|
||||
private running: boolean = false;
|
||||
private resolvePromise: (() => void) | null = null;
|
||||
private commandPromise: Promise<void> | null = null;
|
||||
|
||||
public add<A>(
|
||||
checkInitialized: boolean = true,
|
||||
command: (...args: A[]) => Promise<Result<void, any>> | Result<void, any> | Promise<void>,
|
||||
...args: A[]
|
||||
) {
|
||||
this.queue.push({ command, checkInitialized, commandArgs: args });
|
||||
|
||||
if (!this.running) {
|
||||
this.commandPromise = new Promise((resolve) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async wait() {
|
||||
if (this.running) {
|
||||
await this.commandPromise;
|
||||
}
|
||||
}
|
||||
|
||||
private async run() {
|
||||
this.running = true;
|
||||
while (this.queue.length > 0) {
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
const currentItem = this.queue.shift();
|
||||
|
||||
if (!currentItem) continue;
|
||||
|
||||
// make sure formbricks is initialized
|
||||
if (currentItem.checkInitialized) {
|
||||
// call different function based on package type
|
||||
const initResult = checkInitialized();
|
||||
|
||||
if (initResult && initResult.ok !== true) {
|
||||
errorHandler.handle(initResult.error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const executeCommand = async () => {
|
||||
return (await currentItem?.command.apply(null, currentItem?.commandArgs)) as Result<void, any>;
|
||||
};
|
||||
|
||||
const result = await wrapThrowsAsync(executeCommand)();
|
||||
|
||||
if (!result) continue;
|
||||
|
||||
if (result.ok) {
|
||||
if (result.data && !result.data.ok) {
|
||||
errorHandler.handle(result.data.error);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.ok !== true) {
|
||||
errorHandler.handle(result.error);
|
||||
}
|
||||
}
|
||||
this.running = false;
|
||||
if (this.resolvePromise) {
|
||||
this.resolvePromise();
|
||||
this.resolvePromise = null;
|
||||
this.commandPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js";
|
||||
import { type TJsConfig, type TJsConfigUpdateInput } from "@formbricks/types/js";
|
||||
import { JS_LOCAL_STORAGE_KEY } from "./constants";
|
||||
import { Result, err, ok, wrapThrows } from "./errors";
|
||||
import { type Result, err, ok, wrapThrows } from "./errors";
|
||||
|
||||
export class Config {
|
||||
private static instance: Config | undefined;
|
||||
@@ -22,18 +22,16 @@ export class Config {
|
||||
}
|
||||
|
||||
public update(newConfig: TJsConfigUpdateInput): void {
|
||||
if (newConfig) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig,
|
||||
status: {
|
||||
value: newConfig.status?.value || "success",
|
||||
expiresAt: newConfig.status?.expiresAt || null,
|
||||
},
|
||||
};
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig,
|
||||
status: {
|
||||
value: newConfig.status?.value ?? "success",
|
||||
expiresAt: newConfig.status?.expiresAt ?? null,
|
||||
},
|
||||
};
|
||||
|
||||
this.saveToStorage();
|
||||
}
|
||||
void this.saveToStorage();
|
||||
}
|
||||
|
||||
public get(): TJsConfig {
|
||||
@@ -43,7 +41,7 @@ export class Config {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public loadFromLocalStorage(): Result<TJsConfig, Error> {
|
||||
public loadFromLocalStorage(): Result<TJsConfig> {
|
||||
if (typeof window !== "undefined") {
|
||||
const savedConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY);
|
||||
if (savedConfig) {
|
||||
@@ -54,8 +52,8 @@ export class Config {
|
||||
|
||||
// check if the config has expired
|
||||
if (
|
||||
parsedConfig.environmentState?.expiresAt &&
|
||||
new Date(parsedConfig.environmentState.expiresAt) <= new Date()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- In case of an error, we don't have environmentState
|
||||
new Date(parsedConfig.environmentState?.expiresAt) <= new Date()
|
||||
) {
|
||||
return err(new Error("Config in local storage has expired"));
|
||||
}
|
||||
@@ -67,18 +65,18 @@ export class Config {
|
||||
return err(new Error("No or invalid config in local storage"));
|
||||
}
|
||||
|
||||
private async saveToStorage(): Promise<Result<Promise<void>, Error>> {
|
||||
return wrapThrows(async () => {
|
||||
await localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(this.config));
|
||||
private saveToStorage(): Result<void> {
|
||||
return wrapThrows(() => {
|
||||
localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(this.config));
|
||||
})();
|
||||
}
|
||||
|
||||
// reset the config
|
||||
|
||||
public async resetConfig(): Promise<Result<Promise<void>, Error>> {
|
||||
public resetConfig(): Result<void> {
|
||||
this.config = null;
|
||||
|
||||
return wrapThrows(async () => {
|
||||
return wrapThrows(() => {
|
||||
localStorage.removeItem(JS_LOCAL_STORAGE_KEY);
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// shared functions for environment and person state(s)
|
||||
import { TJsEnvironmentState, TJsEnvironmentSyncParams } from "@formbricks/types/js";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { type TJsEnvironmentState, type TJsEnvironmentSyncParams } from "@formbricks/types/js";
|
||||
import { Config } from "./config";
|
||||
import { err } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
@@ -19,9 +20,9 @@ let environmentStateSyncIntervalId: number | null = null;
|
||||
*/
|
||||
export const fetchEnvironmentState = async (
|
||||
{ apiHost, environmentId }: TJsEnvironmentSyncParams,
|
||||
noCache: boolean = false
|
||||
noCache = false
|
||||
): Promise<TJsEnvironmentState> => {
|
||||
let fetchOptions: RequestInit = {};
|
||||
const fetchOptions: RequestInit = {};
|
||||
|
||||
if (noCache || getIsDebug()) {
|
||||
fetchOptions.cache = "no-cache";
|
||||
@@ -33,9 +34,9 @@ export const fetchEnvironmentState = async (
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const jsonRes = await response.json();
|
||||
const jsonRes = (await response.json()) as { message: string };
|
||||
|
||||
const error = err({
|
||||
const error = err<ApiErrorResponse>({
|
||||
code: "network_error",
|
||||
status: response.status,
|
||||
message: "Error syncing with backend",
|
||||
@@ -43,14 +44,15 @@ export const fetchEnvironmentState = async (
|
||||
responseMessage: jsonRes.message,
|
||||
});
|
||||
|
||||
throw error;
|
||||
// eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error
|
||||
throw error.error;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = (await response.json()) as { data: TJsEnvironmentState["data"] };
|
||||
const { data: state } = data;
|
||||
|
||||
return {
|
||||
data: { ...(state as TJsEnvironmentState["data"]) },
|
||||
data: { ...state },
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes
|
||||
};
|
||||
};
|
||||
@@ -59,14 +61,14 @@ export const fetchEnvironmentState = async (
|
||||
* Add a listener to check if the environment state has expired with a certain interval
|
||||
*/
|
||||
export const addEnvironmentStateExpiryCheckListener = (): void => {
|
||||
let updateInterval = 1000 * 60; // every minute
|
||||
const updateInterval = 1000 * 60; // every minute
|
||||
if (typeof window !== "undefined" && environmentStateSyncIntervalId === null) {
|
||||
environmentStateSyncIntervalId = window.setInterval(async () => {
|
||||
const intervalHandler = async (): Promise<void> => {
|
||||
const expiresAt = config.get().environmentState.expiresAt;
|
||||
|
||||
try {
|
||||
// check if the environmentState has not expired yet
|
||||
if (expiresAt && new Date(expiresAt) >= new Date()) {
|
||||
if (new Date(expiresAt) >= new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,13 +90,15 @@ export const addEnvironmentStateExpiryCheckListener = (): void => {
|
||||
environmentState,
|
||||
filteredSurveys,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Error during expiry check: ${e}`);
|
||||
} catch (e: unknown) {
|
||||
logger.error(`Error during expiry check: ${e as string}`);
|
||||
logger.debug("Extending config and try again later.");
|
||||
const existingConfig = config.get();
|
||||
config.update(existingConfig);
|
||||
}
|
||||
}, updateInterval);
|
||||
};
|
||||
|
||||
environmentStateSyncIntervalId = window.setInterval(() => void intervalHandler(), updateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
/* eslint-disable no-console -- Required for logging a warning here */
|
||||
import { Logger } from "./logger";
|
||||
|
||||
export type { ZErrorHandler } from "@formbricks/types/errors";
|
||||
|
||||
export type ResultError<T> = { ok: false; error: T };
|
||||
export interface ResultError<T> {
|
||||
ok: false;
|
||||
error: T;
|
||||
}
|
||||
|
||||
export type ResultOk<T> = { ok: true; value: T };
|
||||
export interface ResultOk<T> {
|
||||
ok: true;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export type Result<T, E = Error> = ResultOk<T> | ResultError<E>;
|
||||
|
||||
@@ -20,14 +27,14 @@ export const err = <E = Error>(error: E): ResultError<E> => ({
|
||||
export const wrap =
|
||||
<T, R>(fn: (value: T) => R) =>
|
||||
(result: Result<T>): Result<R> =>
|
||||
result.ok === true ? { ok: true, value: fn(result.value) } : result;
|
||||
result.ok ? { ok: true, value: fn(result.value) } : result;
|
||||
|
||||
export function match<TSuccess, TError, TReturn>(
|
||||
result: Result<TSuccess, TError>,
|
||||
onSuccess: (value: TSuccess) => TReturn,
|
||||
onError: (error: TError) => TReturn
|
||||
) {
|
||||
if (result.ok === true) {
|
||||
): TReturn {
|
||||
if (result.ok) {
|
||||
return onSuccess(result.value);
|
||||
}
|
||||
|
||||
@@ -48,7 +55,7 @@ if (result.ok === true) {
|
||||
}
|
||||
*/
|
||||
export const wrapThrows =
|
||||
<T, A extends any[]>(fn: (...args: A) => T) =>
|
||||
<T, A extends unknown[]>(fn: (...args: A) => T) =>
|
||||
(...args: A): Result<T> => {
|
||||
try {
|
||||
return {
|
||||
@@ -63,58 +70,60 @@ export const wrapThrows =
|
||||
}
|
||||
};
|
||||
|
||||
export type NetworkError = {
|
||||
export interface NetworkError {
|
||||
code: "network_error";
|
||||
status: number;
|
||||
message: string;
|
||||
url: string;
|
||||
responseMessage: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type MissingFieldError = {
|
||||
export interface MissingFieldError {
|
||||
code: "missing_field";
|
||||
field: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type InvalidMatchTypeError = {
|
||||
export interface InvalidMatchTypeError {
|
||||
code: "invalid_match_type";
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type MissingPersonError = {
|
||||
export interface MissingPersonError {
|
||||
code: "missing_person";
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type NotInitializedError = {
|
||||
export interface NotInitializedError {
|
||||
code: "not_initialized";
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AttributeAlreadyExistsError = {
|
||||
export interface AttributeAlreadyExistsError {
|
||||
code: "attribute_already_exists";
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type InvalidCodeError = {
|
||||
export interface InvalidCodeError {
|
||||
code: "invalid_code";
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export class ErrorHandler {
|
||||
private static instance: ErrorHandler | null;
|
||||
private handleError: (error: any) => void;
|
||||
public customized: boolean = false;
|
||||
private handleError: (error: unknown) => void;
|
||||
public customized = false;
|
||||
public static initialized = false;
|
||||
|
||||
private constructor(errorHandler?: (error: any) => void) {
|
||||
private constructor(errorHandler?: (error: unknown) => void) {
|
||||
if (errorHandler) {
|
||||
this.handleError = errorHandler;
|
||||
this.customized = true;
|
||||
} else {
|
||||
this.handleError = (err) => Logger.getInstance().error(JSON.stringify(err));
|
||||
this.handleError = (error) => {
|
||||
Logger.getInstance().error(JSON.stringify(error));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +135,7 @@ export class ErrorHandler {
|
||||
return ErrorHandler.instance;
|
||||
}
|
||||
|
||||
static init(errorHandler?: (error: any) => void): void {
|
||||
static init(errorHandler?: (error: unknown) => void): void {
|
||||
this.initialized = true;
|
||||
|
||||
ErrorHandler.instance = new ErrorHandler(errorHandler);
|
||||
@@ -136,7 +145,7 @@ export class ErrorHandler {
|
||||
logger.debug(`Custom error handler: ${this.customized ? "yes" : "no"}`);
|
||||
}
|
||||
|
||||
public handle(error: any): void {
|
||||
public handle(error: unknown): void {
|
||||
console.warn("🧱 Formbricks - Global error: ", error);
|
||||
this.handleError(error);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
addEnvironmentStateExpiryCheckListener,
|
||||
clearEnvironmentStateExpiryCheckListener,
|
||||
} from "./environmentState";
|
||||
} from "./environment-state";
|
||||
import {
|
||||
addClickEventListener,
|
||||
addExitIntentListener,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
removeExitIntentListener,
|
||||
removePageUrlEventListeners,
|
||||
removeScrollDepthListener,
|
||||
} from "./noCodeActions";
|
||||
import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./personState";
|
||||
} from "./no-code-actions";
|
||||
import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./person-state";
|
||||
|
||||
let areRemoveEventListenersAdded = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { type ForbiddenError } from "@formbricks/types/errors";
|
||||
/* eslint-disable no-console -- required for logging */
|
||||
import { type TAttributes } from "@formbricks/types/attributes";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
import { updateAttributes } from "./attributes";
|
||||
@@ -9,22 +10,20 @@ import {
|
||||
LEGACY_JS_APP_LOCAL_STORAGE_KEY,
|
||||
LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY,
|
||||
} from "./constants";
|
||||
import { fetchEnvironmentState } from "./environmentState";
|
||||
import { fetchEnvironmentState } from "./environment-state";
|
||||
import {
|
||||
ErrorHandler,
|
||||
MissingFieldError,
|
||||
MissingPersonError,
|
||||
NetworkError,
|
||||
NotInitializedError,
|
||||
Result,
|
||||
type MissingFieldError,
|
||||
type NotInitializedError,
|
||||
type Result,
|
||||
err,
|
||||
okVoid,
|
||||
wrapThrows,
|
||||
} from "./errors";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./event-listeners";
|
||||
import { Logger } from "./logger";
|
||||
import { checkPageUrl } from "./noCodeActions";
|
||||
import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./personState";
|
||||
import { checkPageUrl } from "./no-code-actions";
|
||||
import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./person-state";
|
||||
import { filterSurveys, getIsDebug } from "./utils";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget";
|
||||
|
||||
@@ -32,7 +31,7 @@ const logger = Logger.getInstance();
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
export const setIsInitialized = (value: boolean) => {
|
||||
export const setIsInitialized = (value: boolean): void => {
|
||||
isInitialized = value;
|
||||
};
|
||||
|
||||
@@ -42,7 +41,13 @@ const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => {
|
||||
|
||||
if (oldWebsiteConfig) {
|
||||
localStorage.removeItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY);
|
||||
const parsedOldConfig = JSON.parse(oldWebsiteConfig) as TJsConfig;
|
||||
const parsedOldConfig = JSON.parse(oldWebsiteConfig) as Partial<{
|
||||
environmentId: string;
|
||||
apiHost: string;
|
||||
environmentState: TJsConfig["environmentState"];
|
||||
personState: TJsConfig["personState"];
|
||||
filteredSurveys: TJsConfig["filteredSurveys"];
|
||||
}>;
|
||||
|
||||
if (
|
||||
parsedOldConfig.environmentId &&
|
||||
@@ -55,14 +60,20 @@ const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => {
|
||||
|
||||
return {
|
||||
changed: true,
|
||||
newState: newLocalStorageConfig,
|
||||
newState: newLocalStorageConfig as TJsConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (oldAppConfig) {
|
||||
localStorage.removeItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY);
|
||||
const parsedOldConfig = JSON.parse(oldAppConfig) as TJsConfig;
|
||||
const parsedOldConfig = JSON.parse(oldAppConfig) as Partial<{
|
||||
environmentId: string;
|
||||
apiHost: string;
|
||||
environmentState: TJsConfig["environmentState"];
|
||||
personState: TJsConfig["personState"];
|
||||
filteredSurveys: TJsConfig["filteredSurveys"];
|
||||
}>;
|
||||
|
||||
if (
|
||||
parsedOldConfig.environmentId &&
|
||||
@@ -86,20 +97,24 @@ const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } =
|
||||
const existingConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY);
|
||||
|
||||
if (existingConfig) {
|
||||
const parsedConfig = JSON.parse(existingConfig);
|
||||
const parsedConfig = JSON.parse(existingConfig) as TJsConfig;
|
||||
|
||||
if (parsedConfig.environmentState.data.product) {
|
||||
const { environmentState, filteredSurveys, ...restConfig } = parsedConfig;
|
||||
// @ts-expect-error - product is not in the type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- environmentState could be undefined in an error state
|
||||
if (parsedConfig.environmentState?.data?.product) {
|
||||
const { environmentState: _, filteredSurveys, ...restConfig } = parsedConfig;
|
||||
|
||||
// @ts-expect-error
|
||||
const fixedFilteredSurveys = filteredSurveys.map((survey) => {
|
||||
// @ts-expect-error - productOverwrites is not in the type
|
||||
const { productOverwrites, ...rest } = survey;
|
||||
return {
|
||||
...rest,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- productOverwrites is not in the type
|
||||
projectOverwrites: productOverwrites,
|
||||
};
|
||||
});
|
||||
|
||||
// @ts-expect-error - product is not in the type
|
||||
const { product, ...rest } = parsedConfig.environmentState.data;
|
||||
|
||||
const newLocalStorageConfig = {
|
||||
@@ -108,6 +123,7 @@ const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } =
|
||||
...parsedConfig.environmentState,
|
||||
data: {
|
||||
...rest,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- product is not in the type
|
||||
project: product,
|
||||
},
|
||||
},
|
||||
@@ -126,11 +142,12 @@ const migrateProductToProject = (): { changed: boolean; newState?: TJsConfig } =
|
||||
|
||||
export const initialize = async (
|
||||
configInput: TJsConfigInput
|
||||
): Promise<Result<void, MissingFieldError | NetworkError | MissingPersonError | ForbiddenError>> => {
|
||||
): Promise<Result<void, MissingFieldError | ApiErrorResponse>> => {
|
||||
const isDebug = getIsDebug();
|
||||
if (isDebug) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
}
|
||||
|
||||
let config = Config.getInstance();
|
||||
|
||||
const { changed, newState } = migrateLocalStorage();
|
||||
@@ -166,12 +183,12 @@ export const initialize = async (
|
||||
try {
|
||||
existingConfig = config.get();
|
||||
logger.debug("Found existing configuration.");
|
||||
} catch (e) {
|
||||
} catch {
|
||||
logger.debug("No existing configuration found.");
|
||||
}
|
||||
|
||||
// formbricks is in error state, skip initialization
|
||||
if (existingConfig?.status?.value === "error") {
|
||||
if (existingConfig?.status.value === "error") {
|
||||
if (isDebug) {
|
||||
logger.debug(
|
||||
"Formbricks is in error state, but debug mode is active. Resetting config and continuing."
|
||||
@@ -180,16 +197,15 @@ export const initialize = async (
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
logger.debug("Formbricks was set to an error state.");
|
||||
console.error("🧱 Formbricks - Formbricks was set to an error state.");
|
||||
|
||||
const expiresAt = existingConfig?.status?.expiresAt;
|
||||
const expiresAt = existingConfig.status.expiresAt;
|
||||
|
||||
if (expiresAt && new Date(expiresAt) > new Date()) {
|
||||
logger.debug("Error state is not expired, skipping initialization");
|
||||
console.error("🧱 Formbricks - Error state is not expired, skipping initialization");
|
||||
return okVoid();
|
||||
} else {
|
||||
logger.debug("Error state is expired. Continue with initialization.");
|
||||
}
|
||||
console.error("🧱 Formbricks - Error state is expired. Continuing with initialization.");
|
||||
}
|
||||
|
||||
ErrorHandler.getInstance().printStatus();
|
||||
@@ -217,8 +233,7 @@ export const initialize = async (
|
||||
addWidgetContainer();
|
||||
|
||||
if (
|
||||
existingConfig &&
|
||||
existingConfig.environmentState &&
|
||||
existingConfig?.environmentState &&
|
||||
existingConfig.environmentId === configInput.environmentId &&
|
||||
existingConfig.apiHost === configInput.apiHost
|
||||
) {
|
||||
@@ -233,6 +248,7 @@ export const initialize = async (
|
||||
|
||||
if (
|
||||
configInput.userId &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- personState could be null
|
||||
(existingConfig.personState === null ||
|
||||
(existingConfig.personState.expiresAt && new Date(existingConfig.personState.expiresAt) < new Date()))
|
||||
) {
|
||||
@@ -278,8 +294,8 @@ export const initialize = async (
|
||||
});
|
||||
|
||||
const surveyNames = filteredSurveys.map((s) => s.name);
|
||||
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
||||
} catch (e) {
|
||||
logger.debug(`Fetched ${surveyNames.length.toString()} surveys during sync: ${surveyNames.join(", ")}`);
|
||||
} catch {
|
||||
putFormbricksInErrorState(config);
|
||||
}
|
||||
} else {
|
||||
@@ -319,9 +335,9 @@ export const initialize = async (
|
||||
configInput.attributes
|
||||
);
|
||||
|
||||
if (res.ok !== true) {
|
||||
if (!res.ok) {
|
||||
if (res.error.code === "forbidden") {
|
||||
logger.error(`Authorization error: ${res.error.responseMessage}`);
|
||||
logger.error(`Authorization error: ${res.error.responseMessage ?? ""}`);
|
||||
}
|
||||
return err(res.error);
|
||||
}
|
||||
@@ -341,7 +357,7 @@ export const initialize = async (
|
||||
attributes: updatedAttributes ?? {},
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrorOnFirstInit(e as Error);
|
||||
handleErrorOnFirstInit(e);
|
||||
}
|
||||
|
||||
// and track the new session event
|
||||
@@ -357,13 +373,14 @@ export const initialize = async (
|
||||
|
||||
// check page url if initialized after page load
|
||||
|
||||
checkPageUrl();
|
||||
void checkPageUrl();
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const handleErrorOnFirstInit = (e: any) => {
|
||||
if (e.error.code === "forbidden") {
|
||||
logger.error(`Authorization error: ${e.error.responseMessage}`);
|
||||
export const handleErrorOnFirstInit = (e: unknown): void => {
|
||||
const error = e as ApiErrorResponse;
|
||||
if (error.code === "forbidden") {
|
||||
logger.error(`Authorization error: ${error.responseMessage ?? ""}`);
|
||||
}
|
||||
|
||||
if (getIsDebug()) {
|
||||
@@ -380,7 +397,9 @@ export const handleErrorOnFirstInit = (e: any) => {
|
||||
};
|
||||
|
||||
// can't use config.update here because the config is not yet initialized
|
||||
wrapThrows(() => localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))();
|
||||
wrapThrows(() => {
|
||||
localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig));
|
||||
})();
|
||||
throw new Error("Could not initialize formbricks");
|
||||
};
|
||||
|
||||
@@ -404,7 +423,7 @@ export const deinitalize = (): void => {
|
||||
setIsInitialized(false);
|
||||
};
|
||||
|
||||
export const putFormbricksInErrorState = (config: Config): void => {
|
||||
export const putFormbricksInErrorState = (formbricksConfig: Config): void => {
|
||||
if (getIsDebug()) {
|
||||
logger.debug("Not putting formbricks in error state because debug mode is active (no error state)");
|
||||
return;
|
||||
@@ -412,8 +431,8 @@ export const putFormbricksInErrorState = (config: Config): void => {
|
||||
|
||||
logger.debug("Putting formbricks in error state");
|
||||
// change formbricks status to error
|
||||
config.update({
|
||||
...config.get(),
|
||||
formbricksConfig.update({
|
||||
...formbricksConfig.get(),
|
||||
status: {
|
||||
value: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console -- Required for logging */
|
||||
type LogLevel = "debug" | "error";
|
||||
|
||||
interface LoggerConfig {
|
||||
@@ -8,8 +9,6 @@ export class Logger {
|
||||
private static instance: Logger | undefined;
|
||||
private logLevel: LogLevel = "error";
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
@@ -18,7 +17,7 @@ export class Logger {
|
||||
}
|
||||
|
||||
configure(config: LoggerConfig): void {
|
||||
if (config && config.logLevel !== undefined) {
|
||||
if (config.logLevel !== undefined) {
|
||||
this.logLevel = config.logLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TJsEnvironmentStateActionClass } from "@formbricks/types/js";
|
||||
import { type TJsEnvironmentStateActionClass } from "@formbricks/types/js";
|
||||
import { trackNoCodeAction } from "./actions";
|
||||
import { Config } from "./config";
|
||||
import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "./errors";
|
||||
import { ErrorHandler, type NetworkError, type Result, type ResultError, err, match, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { TimeoutStack } from "./timeout-stack";
|
||||
import { evaluateNoCodeConfigClick, handleUrlFilters } from "./utils";
|
||||
@@ -18,6 +18,9 @@ const events = ["hashchange", "popstate", "pushstate", "replacestate", "load"];
|
||||
// Page URL Event Handlers
|
||||
let arePageUrlEventListenersAdded = false;
|
||||
let isHistoryPatched = false;
|
||||
export const setIsHistoryPatched = (value: boolean): void => {
|
||||
isHistoryPatched = value;
|
||||
};
|
||||
|
||||
export const checkPageUrl = async (): Promise<Result<void, NetworkError>> => {
|
||||
logger.debug(`Checking page url: ${window.location.href}`);
|
||||
@@ -34,7 +37,7 @@ export const checkPageUrl = async (): Promise<Result<void, NetworkError>> => {
|
||||
if (isValidUrl) {
|
||||
const trackResult = await trackNoCodeAction(event.name);
|
||||
|
||||
if (trackResult.ok !== true) {
|
||||
if (!trackResult.ok) {
|
||||
return err(trackResult.error);
|
||||
}
|
||||
} else {
|
||||
@@ -52,43 +55,45 @@ export const checkPageUrl = async (): Promise<Result<void, NetworkError>> => {
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
const checkPageUrlWrapper = () => {
|
||||
checkPageUrl();
|
||||
};
|
||||
const checkPageUrlWrapper = (): ReturnType<typeof checkPageUrl> => checkPageUrl();
|
||||
|
||||
export const addPageUrlEventListeners = (): void => {
|
||||
if (typeof window === "undefined" || arePageUrlEventListenersAdded) return;
|
||||
|
||||
// Monkey patch history methods if not already done
|
||||
if (!isHistoryPatched) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- We need to access the original method
|
||||
const originalPushState = history.pushState;
|
||||
|
||||
// eslint-disable-next-line func-names -- We need an anonymous function here
|
||||
history.pushState = function (...args) {
|
||||
const returnValue = originalPushState.apply(this, args);
|
||||
originalPushState.apply(this, args);
|
||||
const event = new Event("pushstate");
|
||||
window.dispatchEvent(event);
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
isHistoryPatched = true;
|
||||
}
|
||||
|
||||
events.forEach((event) => window.addEventListener(event, checkPageUrlWrapper));
|
||||
events.forEach((event) => {
|
||||
window.addEventListener(event, checkPageUrlWrapper as EventListener);
|
||||
});
|
||||
arePageUrlEventListenersAdded = true;
|
||||
};
|
||||
|
||||
export const removePageUrlEventListeners = (): void => {
|
||||
if (typeof window === "undefined" || !arePageUrlEventListenersAdded) return;
|
||||
events.forEach((event) => window.removeEventListener(event, checkPageUrlWrapper));
|
||||
events.forEach((event) => {
|
||||
window.removeEventListener(event, checkPageUrlWrapper as EventListener);
|
||||
});
|
||||
arePageUrlEventListenersAdded = false;
|
||||
};
|
||||
|
||||
// Click Event Handlers
|
||||
let isClickEventListenerAdded = false;
|
||||
|
||||
const checkClickMatch = (event: MouseEvent) => {
|
||||
const checkClickMatch = (event: MouseEvent): void => {
|
||||
const { environmentState } = appConfig.get();
|
||||
if (!environmentState) return;
|
||||
|
||||
const { actionClasses = [] } = environmentState.data;
|
||||
|
||||
@@ -100,18 +105,26 @@ const checkClickMatch = (event: MouseEvent) => {
|
||||
|
||||
noCodeClickActionClasses.forEach((action: TJsEnvironmentStateActionClass) => {
|
||||
if (evaluateNoCodeConfigClick(targetElement, action)) {
|
||||
trackNoCodeAction(action.name).then((res) => {
|
||||
match(
|
||||
res,
|
||||
(_value: unknown) => {},
|
||||
(err: any) => errorHandler.handle(err)
|
||||
);
|
||||
});
|
||||
trackNoCodeAction(action.name)
|
||||
.then((res) => {
|
||||
match(
|
||||
res,
|
||||
(_value: unknown) => undefined,
|
||||
(actionError: unknown) => {
|
||||
errorHandler.handle(actionError);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
errorHandler.handle(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const checkClickMatchWrapper = (e: MouseEvent) => checkClickMatch(e);
|
||||
const checkClickMatchWrapper = (e: MouseEvent): void => {
|
||||
checkClickMatch(e);
|
||||
};
|
||||
|
||||
export const addClickEventListener = (): void => {
|
||||
if (typeof window === "undefined" || isClickEventListenerAdded) return;
|
||||
@@ -128,9 +141,9 @@ export const removeClickEventListener = (): void => {
|
||||
// Exit Intent Handlers
|
||||
let isExitIntentListenerAdded = false;
|
||||
|
||||
const checkExitIntent = async (e: MouseEvent) => {
|
||||
const checkExitIntent = async (e: MouseEvent): Promise<ResultError<NetworkError> | undefined> => {
|
||||
const { environmentState } = appConfig.get();
|
||||
const { actionClasses = [] } = environmentState.data ?? {};
|
||||
const { actionClasses = [] } = environmentState.data;
|
||||
|
||||
const noCodeExitIntentActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "exitIntent"
|
||||
@@ -144,23 +157,25 @@ const checkExitIntent = async (e: MouseEvent) => {
|
||||
if (!isValidUrl) continue;
|
||||
|
||||
const trackResult = await trackNoCodeAction(event.name);
|
||||
if (trackResult.ok !== true) return err(trackResult.error);
|
||||
if (!trackResult.ok) return err(trackResult.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkExitIntentWrapper = (e: MouseEvent) => checkExitIntent(e);
|
||||
const checkExitIntentWrapper = (e: MouseEvent): ReturnType<typeof checkExitIntent> => checkExitIntent(e);
|
||||
|
||||
export const addExitIntentListener = (): void => {
|
||||
if (typeof document !== "undefined" && !isExitIntentListenerAdded) {
|
||||
document.querySelector("body")!.addEventListener("mouseleave", checkExitIntentWrapper);
|
||||
document
|
||||
.querySelector("body")
|
||||
?.addEventListener("mouseleave", checkExitIntentWrapper as unknown as EventListener);
|
||||
isExitIntentListenerAdded = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeExitIntentListener = (): void => {
|
||||
if (isExitIntentListenerAdded) {
|
||||
document.removeEventListener("mouseleave", checkExitIntentWrapper);
|
||||
document.removeEventListener("mouseleave", checkExitIntentWrapper as unknown as EventListener);
|
||||
isExitIntentListenerAdded = false;
|
||||
}
|
||||
};
|
||||
@@ -169,7 +184,7 @@ export const removeExitIntentListener = (): void => {
|
||||
let scrollDepthListenerAdded = false;
|
||||
let scrollDepthTriggered = false;
|
||||
|
||||
const checkScrollDepth = async () => {
|
||||
const checkScrollDepth = async (): Promise<Result<void, unknown>> => {
|
||||
const scrollPosition = window.scrollY;
|
||||
const windowSize = window.innerHeight;
|
||||
const bodyHeight = document.documentElement.scrollHeight;
|
||||
@@ -182,7 +197,7 @@ const checkScrollDepth = async () => {
|
||||
scrollDepthTriggered = true;
|
||||
|
||||
const { environmentState } = appConfig.get();
|
||||
const { actionClasses = [] } = environmentState.data ?? {};
|
||||
const { actionClasses = [] } = environmentState.data;
|
||||
|
||||
const noCodefiftyPercentScrollActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "fiftyPercentScroll"
|
||||
@@ -195,22 +210,22 @@ const checkScrollDepth = async () => {
|
||||
if (!isValidUrl) continue;
|
||||
|
||||
const trackResult = await trackNoCodeAction(event.name);
|
||||
if (trackResult.ok !== true) return err(trackResult.error);
|
||||
if (!trackResult.ok) return err(trackResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
const checkScrollDepthWrapper = () => checkScrollDepth();
|
||||
const checkScrollDepthWrapper = (): ReturnType<typeof checkScrollDepth> => checkScrollDepth();
|
||||
|
||||
export const addScrollDepthListener = (): void => {
|
||||
if (typeof window !== "undefined" && !scrollDepthListenerAdded) {
|
||||
if (document.readyState === "complete") {
|
||||
window.addEventListener("scroll", checkScrollDepthWrapper);
|
||||
window.addEventListener("scroll", checkScrollDepthWrapper as EventListener);
|
||||
} else {
|
||||
window.addEventListener("load", () => {
|
||||
window.addEventListener("scroll", checkScrollDepthWrapper);
|
||||
window.addEventListener("scroll", checkScrollDepthWrapper as EventListener);
|
||||
});
|
||||
}
|
||||
scrollDepthListenerAdded = true;
|
||||
@@ -219,7 +234,7 @@ export const addScrollDepthListener = (): void => {
|
||||
|
||||
export const removeScrollDepthListener = (): void => {
|
||||
if (scrollDepthListenerAdded) {
|
||||
window.removeEventListener("scroll", checkScrollDepthWrapper);
|
||||
window.removeEventListener("scroll", checkScrollDepthWrapper as EventListener);
|
||||
scrollDepthListenerAdded = false;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TJsPersonState, TJsPersonSyncParams } from "@formbricks/types/js";
|
||||
import { type TJsPersonState, type TJsPersonSyncParams } from "@formbricks/types/js";
|
||||
import { Config } from "./config";
|
||||
import { err } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
@@ -30,9 +30,9 @@ export const DEFAULT_PERSON_STATE_NO_USER_ID: TJsPersonState = {
|
||||
*/
|
||||
export const fetchPersonState = async (
|
||||
{ apiHost, environmentId, userId }: TJsPersonSyncParams,
|
||||
noCache: boolean = false
|
||||
noCache = false
|
||||
): Promise<TJsPersonState> => {
|
||||
let fetchOptions: RequestInit = {};
|
||||
const fetchOptions: RequestInit = {};
|
||||
|
||||
if (noCache || getIsDebug()) {
|
||||
fetchOptions.cache = "no-cache";
|
||||
@@ -44,7 +44,7 @@ export const fetchPersonState = async (
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const jsonRes = await response.json();
|
||||
const jsonRes = (await response.json()) as { code: string; message: string };
|
||||
|
||||
const error = err({
|
||||
code: jsonRes.code === "forbidden" ? "forbidden" : "network_error",
|
||||
@@ -54,10 +54,10 @@ export const fetchPersonState = async (
|
||||
responseMessage: jsonRes.message,
|
||||
});
|
||||
|
||||
throw error;
|
||||
throw new Error(error.error.message);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = (await response.json()) as { data: TJsPersonState["data"] };
|
||||
const { data: state } = data;
|
||||
|
||||
const defaultPersonState: TJsPersonState = {
|
||||
@@ -76,7 +76,7 @@ export const fetchPersonState = async (
|
||||
}
|
||||
|
||||
return {
|
||||
data: { ...(state as TJsPersonState["data"]) },
|
||||
data: { ...state },
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes
|
||||
};
|
||||
};
|
||||
@@ -88,7 +88,7 @@ export const addPersonStateExpiryCheckListener = (): void => {
|
||||
const updateInterval = 1000 * 60; // every 60 seconds
|
||||
|
||||
if (typeof window !== "undefined" && personStateSyncIntervalId === null) {
|
||||
personStateSyncIntervalId = window.setInterval(async () => {
|
||||
const intervalHandler = (): void => {
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
if (!userId) {
|
||||
@@ -103,7 +103,9 @@ export const addPersonStateExpiryCheckListener = (): void => {
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes
|
||||
},
|
||||
});
|
||||
}, updateInterval);
|
||||
};
|
||||
|
||||
personStateSyncIntervalId = window.setInterval(intervalHandler, updateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Config } from "./config";
|
||||
import { NetworkError, Result, err, okVoid } from "./errors";
|
||||
import { type NetworkError, type Result, err, okVoid } from "./errors";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { setIsHistoryPatched } from "./no-code-actions";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await -- There are no promises but our proxy makes the functions async
|
||||
export const logoutPerson = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
config.resetConfig();
|
||||
@@ -24,7 +26,10 @@ export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
...(userId && { userId }),
|
||||
attributes: config.get().attributes,
|
||||
};
|
||||
|
||||
await logoutPerson();
|
||||
setIsHistoryPatched(false);
|
||||
|
||||
try {
|
||||
await initialize(syncParams);
|
||||
return okVoid();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
export class TimeoutStack {
|
||||
private static instance: TimeoutStack;
|
||||
// private timeouts: number[] = [];
|
||||
private static instance: TimeoutStack | null = null;
|
||||
private timeouts: { event: string; timeoutId: number }[] = [];
|
||||
|
||||
// Private constructor to prevent direct instantiation
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function -- Empty constructor is intentional
|
||||
private constructor() {}
|
||||
|
||||
// Retrieve the singleton instance of TimeoutStack
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { diffInDays } from "@formbricks/lib/utils/datetime";
|
||||
import { TActionClassNoCodeConfig, TActionClassPageUrlRule } from "@formbricks/types/action-classes";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import {
|
||||
TJsEnvironmentState,
|
||||
TJsEnvironmentStateActionClass,
|
||||
TJsEnvironmentStateSurvey,
|
||||
TJsPersonState,
|
||||
TJsTrackProperties,
|
||||
type TActionClassNoCodeConfig,
|
||||
type TActionClassPageUrlRule,
|
||||
} from "@formbricks/types/action-classes";
|
||||
import { type TAttributes } from "@formbricks/types/attributes";
|
||||
import {
|
||||
type TJsEnvironmentState,
|
||||
type TJsEnvironmentStateActionClass,
|
||||
type TJsEnvironmentStateSurvey,
|
||||
type TJsPersonState,
|
||||
type TJsTrackProperties,
|
||||
} from "@formbricks/types/js";
|
||||
import { TResponseHiddenFieldValue } from "@formbricks/types/responses";
|
||||
import { type TResponseHiddenFieldValue } from "@formbricks/types/responses";
|
||||
import { Logger } from "./logger";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
@@ -37,7 +40,7 @@ export const checkUrlMatch = (
|
||||
};
|
||||
|
||||
export const handleUrlFilters = (urlFilters: TActionClassNoCodeConfig["urlFilters"]): boolean => {
|
||||
if (!urlFilters || urlFilters.length === 0) {
|
||||
if (urlFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ export const evaluateNoCodeConfigClick = (
|
||||
if (cssSelector) {
|
||||
// Split selectors that start with a . or # including the . or #
|
||||
const individualSelectors = cssSelector.split(/\s*(?=[.#])/);
|
||||
for (let selector of individualSelectors) {
|
||||
for (const selector of individualSelectors) {
|
||||
if (!targetElement.matches(selector)) {
|
||||
return false;
|
||||
}
|
||||
@@ -86,7 +89,7 @@ export const handleHiddenFields = (
|
||||
hiddenFieldsConfig: TJsEnvironmentStateSurvey["hiddenFields"],
|
||||
hiddenFields: TJsTrackProperties["hiddenFields"]
|
||||
): TResponseHiddenFieldValue => {
|
||||
const { enabled: enabledHiddenFields, fieldIds: hiddenFieldIds } = hiddenFieldsConfig || {};
|
||||
const { enabled: enabledHiddenFields, fieldIds: hiddenFieldIds } = hiddenFieldsConfig;
|
||||
|
||||
let hiddenFieldsObject: TResponseHiddenFieldValue = {};
|
||||
|
||||
@@ -94,14 +97,14 @@ export const handleHiddenFields = (
|
||||
logger.error("Hidden fields are not enabled for this survey");
|
||||
} else if (hiddenFieldIds && hiddenFields) {
|
||||
const unknownHiddenFields: string[] = [];
|
||||
hiddenFieldsObject = Object.keys(hiddenFields).reduce((acc, key) => {
|
||||
if (hiddenFieldIds?.includes(key)) {
|
||||
acc[key] = hiddenFields?.[key];
|
||||
hiddenFieldsObject = Object.keys(hiddenFields).reduce<TResponseHiddenFieldValue>((acc, key) => {
|
||||
if (hiddenFieldIds.includes(key)) {
|
||||
acc[key] = hiddenFields[key];
|
||||
} else {
|
||||
unknownHiddenFields.push(key);
|
||||
}
|
||||
return acc;
|
||||
}, {} as TResponseHiddenFieldValue);
|
||||
}, {});
|
||||
|
||||
if (unknownHiddenFields.length > 0) {
|
||||
logger.error(
|
||||
@@ -113,7 +116,7 @@ export const handleHiddenFields = (
|
||||
return hiddenFieldsObject;
|
||||
};
|
||||
|
||||
export const shouldDisplayBasedOnPercentage = (displayPercentage: number) => {
|
||||
export const shouldDisplayBasedOnPercentage = (displayPercentage: number): boolean => {
|
||||
const randomNum = Math.floor(Math.random() * 10000) / 100;
|
||||
return randomNum <= displayPercentage;
|
||||
};
|
||||
@@ -125,40 +128,39 @@ export const getLanguageCode = (
|
||||
const language = attributes.language;
|
||||
const availableLanguageCodes = survey.languages.map((surveyLanguage) => surveyLanguage.language.code);
|
||||
if (!language) return "default";
|
||||
else {
|
||||
const selectedLanguage = survey.languages.find((surveyLanguage) => {
|
||||
return (
|
||||
surveyLanguage.language.code === language.toLowerCase() ||
|
||||
surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase()
|
||||
);
|
||||
});
|
||||
if (selectedLanguage?.default) {
|
||||
return "default";
|
||||
}
|
||||
if (
|
||||
!selectedLanguage ||
|
||||
!selectedLanguage?.enabled ||
|
||||
!availableLanguageCodes.includes(selectedLanguage.language.code)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return selectedLanguage.language.code;
|
||||
|
||||
const selectedLanguage = survey.languages.find((surveyLanguage) => {
|
||||
return (
|
||||
surveyLanguage.language.code === language.toLowerCase() ||
|
||||
surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase()
|
||||
);
|
||||
});
|
||||
if (selectedLanguage?.default) {
|
||||
return "default";
|
||||
}
|
||||
if (
|
||||
!selectedLanguage ||
|
||||
!selectedLanguage.enabled ||
|
||||
!availableLanguageCodes.includes(selectedLanguage.language.code)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return selectedLanguage.language.code;
|
||||
};
|
||||
|
||||
export const getDefaultLanguageCode = (survey: TJsEnvironmentStateSurvey) => {
|
||||
const defaultSurveyLanguage = survey.languages?.find((surveyLanguage) => {
|
||||
return surveyLanguage.default === true;
|
||||
export const getDefaultLanguageCode = (survey: TJsEnvironmentStateSurvey): string | undefined => {
|
||||
const defaultSurveyLanguage = survey.languages.find((surveyLanguage) => {
|
||||
return surveyLanguage.default;
|
||||
});
|
||||
if (defaultSurveyLanguage) return defaultSurveyLanguage.language.code;
|
||||
};
|
||||
|
||||
export const getIsDebug = () => window.location.search.includes("formbricksDebug=true");
|
||||
export const getIsDebug = (): boolean => window.location.search.includes("formbricksDebug=true");
|
||||
|
||||
/**
|
||||
* Filters surveys based on the displayOption, recontactDays, and segments
|
||||
* @param environmentSate The environment state
|
||||
* @param personState The person state
|
||||
* @param environmentSate - The environment state
|
||||
* @param personState - The person state
|
||||
* @returns The filtered surveys
|
||||
*/
|
||||
|
||||
@@ -170,10 +172,6 @@ export const filterSurveys = (
|
||||
const { project, surveys } = environmentState.data;
|
||||
const { displays, responses, lastDisplayAt, segments, userId } = personState.data;
|
||||
|
||||
if (!displays) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Function to filter surveys based on displayOption criteria
|
||||
let filteredSurveys = surveys.filter((survey: TJsEnvironmentStateSurvey) => {
|
||||
switch (survey.displayOption) {
|
||||
@@ -211,19 +209,19 @@ export const filterSurveys = (
|
||||
// if survey has recontactDays, check if the last display was more than recontactDays ago
|
||||
else if (survey.recontactDays !== null) {
|
||||
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- lastDisplaySurvey could be falsy
|
||||
if (!lastDisplaySurvey) {
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
}
|
||||
// use recontactDays of the project if survey does not have recontactDays
|
||||
else if (project.recontactDays !== null) {
|
||||
else if (project.recontactDays) {
|
||||
return diffInDays(new Date(), new Date(lastDisplayAt)) >= project.recontactDays;
|
||||
}
|
||||
// if no recontactDays is set, show the survey
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!userId) {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/* eslint-disable no-console -- Required for error logging */
|
||||
/* eslint-disable @typescript-eslint/no-empty-function -- There are some empty functions here that we need */
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||
import { SurveyState } from "@formbricks/lib/surveyState";
|
||||
import { getStyling } from "@formbricks/lib/utils/styling";
|
||||
import {
|
||||
TJsEnvironmentStateSurvey,
|
||||
TJsFileUploadParams,
|
||||
TJsPersonState,
|
||||
TJsTrackProperties,
|
||||
type TJsEnvironmentStateSurvey,
|
||||
type TJsFileUploadParams,
|
||||
type TJsPersonState,
|
||||
type TJsTrackProperties,
|
||||
} from "@formbricks/types/js";
|
||||
import { TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { type TResponseHiddenFieldValue, type TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { Config } from "./config";
|
||||
import { CONTAINER_ID } from "./constants";
|
||||
import { Logger } from "./logger";
|
||||
@@ -27,10 +29,10 @@ const logger = Logger.getInstance();
|
||||
const timeoutStack = TimeoutStack.getInstance();
|
||||
|
||||
let isSurveyRunning = false;
|
||||
let setIsError = (_: boolean) => {};
|
||||
let setIsResponseSendingFinished = (_: boolean) => {};
|
||||
let setIsError = (_: boolean): void => {};
|
||||
let setIsResponseSendingFinished = (_: boolean): void => {};
|
||||
|
||||
export const setIsSurveyRunning = (value: boolean) => {
|
||||
export const setIsSurveyRunning = (value: boolean): void => {
|
||||
isSurveyRunning = value;
|
||||
};
|
||||
|
||||
@@ -60,7 +62,7 @@ const renderWidget = async (
|
||||
survey: TJsEnvironmentStateSurvey,
|
||||
action?: string,
|
||||
hiddenFields: TResponseHiddenFieldValue = {}
|
||||
) => {
|
||||
): Promise<void> => {
|
||||
if (isSurveyRunning) {
|
||||
logger.debug("A survey is already running. Skipping.");
|
||||
return;
|
||||
@@ -69,11 +71,11 @@ const renderWidget = async (
|
||||
setIsSurveyRunning(true);
|
||||
|
||||
if (survey.delay) {
|
||||
logger.debug(`Delaying survey "${survey.name}" by ${survey.delay} seconds.`);
|
||||
logger.debug(`Delaying survey "${survey.name}" by ${survey.delay.toString()} seconds.`);
|
||||
}
|
||||
|
||||
const { project } = config.get().environmentState.data ?? {};
|
||||
const { attributes } = config.get() ?? {};
|
||||
const { project } = config.get().environmentState.data;
|
||||
const { attributes } = config.get();
|
||||
|
||||
const isMultiLanguageSurvey = survey.languages.length > 1;
|
||||
let languageCode = "default";
|
||||
@@ -151,7 +153,7 @@ const renderWidget = async (
|
||||
|
||||
const existingDisplays = config.get().personState.data.displays;
|
||||
const newDisplay = { surveyId: survey.id, createdAt: new Date() };
|
||||
const displays = existingDisplays ? [...existingDisplays, newDisplay] : [newDisplay];
|
||||
const displays = existingDisplays.length ? [...existingDisplays, newDisplay] : [newDisplay];
|
||||
const previousConfig = config.get();
|
||||
|
||||
const updatedPersonState: TJsPersonState = {
|
||||
@@ -203,7 +205,7 @@ const renderWidget = async (
|
||||
...config.get().personState,
|
||||
data: {
|
||||
...config.get().personState.data,
|
||||
responses: [...responses, surveyState.surveyId],
|
||||
responses: responses.length ? [...responses, surveyState.surveyId] : [surveyState.surveyId],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -235,7 +237,7 @@ const renderWidget = async (
|
||||
},
|
||||
onRetry: () => {
|
||||
setIsError(false);
|
||||
responseQueue.processQueue();
|
||||
void responseQueue.processQueue();
|
||||
},
|
||||
hiddenFieldsRecord: hiddenFields,
|
||||
});
|
||||
@@ -246,7 +248,7 @@ const renderWidget = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const closeSurvey = async (): Promise<void> => {
|
||||
export const closeSurvey = (): void => {
|
||||
// remove container element from DOM
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
@@ -276,16 +278,19 @@ export const removeWidgetContainer = (): void => {
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurveys> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- We need to check if the formbricksSurveys object exists
|
||||
if (window.formbricksSurveys) {
|
||||
resolve(window.formbricksSurveys);
|
||||
} else {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${config.get().apiHost}/js/surveys.umd.cjs`;
|
||||
script.async = true;
|
||||
script.onload = () => resolve(window.formbricksSurveys);
|
||||
script.onload = () => {
|
||||
resolve(window.formbricksSurveys);
|
||||
};
|
||||
script.onerror = (error) => {
|
||||
console.error("Failed to load Formbricks Surveys library:", error);
|
||||
reject(error);
|
||||
reject(new Error(`Failed to load Formbricks Surveys library: ${error as string}`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/js",
|
||||
"license": "MIT",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "Formbricks-js allows you to connect your index to Formbricks, display surveys and trigger events.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type FormbricksApp from "@formbricks/js-core";
|
||||
import type Formbricks from "@formbricks/js-core";
|
||||
import { loadFormbricksToProxy } from "./lib/load-formbricks";
|
||||
|
||||
type TFormbricksApp = typeof FormbricksApp;
|
||||
type TFormbricks = typeof Formbricks;
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricks: TFormbricksApp | undefined;
|
||||
formbricks: TFormbricks | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const formbricksProxyHandler: ProxyHandler<TFormbricksApp> = {
|
||||
const formbricksProxyHandler: ProxyHandler<TFormbricks> = {
|
||||
get(_target, prop, _receiver) {
|
||||
return (...args: unknown[]) => loadFormbricksToProxy(prop as string, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
const formbricksApp: TFormbricksApp = new Proxy({} as TFormbricksApp, formbricksProxyHandler);
|
||||
const formbricks: TFormbricks = new Proxy({} as TFormbricks, formbricksProxyHandler);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Required for UMD
|
||||
export default formbricksApp;
|
||||
export default formbricks;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/react-native",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"license": "MIT",
|
||||
"description": "Formbricks React Native SDK allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||
"homepage": "https://formbricks.com",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- could be undefined */
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import type { TAttributes } from "@formbricks/types/attributes";
|
||||
import type { ForbiddenError, NetworkError } from "@formbricks/types/errors";
|
||||
import type { ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { type Result, err, ok } from "../../../js-core/src/lib/errors";
|
||||
import { Logger } from "../../../js-core/src/lib/logger";
|
||||
|
||||
@@ -12,7 +11,7 @@ export const updateAttributes = async (
|
||||
environmentId: string,
|
||||
userId: string,
|
||||
attributes: TAttributes
|
||||
): Promise<Result<TAttributes, NetworkError | ForbiddenError>> => {
|
||||
): Promise<Result<TAttributes, ApiErrorResponse>> => {
|
||||
// clean attributes and remove existing attributes if config already exists
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
@@ -22,7 +21,7 @@ export const updateAttributes = async (
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes));
|
||||
logger.debug(`Updating attributes: ${JSON.stringify(updatedAttributes)}`);
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost,
|
||||
@@ -41,18 +40,18 @@ export const updateAttributes = async (
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
// @ts-expect-error -- details could be defined and present
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- details could be defined and present
|
||||
if (res.error.details?.ignore) {
|
||||
logger.error(res.error.message ?? `Error updating person with userId ${userId}`);
|
||||
const responseError = res.error;
|
||||
|
||||
if (responseError.details?.ignore) {
|
||||
logger.error(responseError.message);
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
return err({
|
||||
code: (res.error as ForbiddenError).code ?? "network_error",
|
||||
status: (res.error as NetworkError | ForbiddenError).status ?? 500,
|
||||
code: responseError.code,
|
||||
status: responseError.status,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`),
|
||||
responseMessage: res.error.message,
|
||||
responseMessage: responseError.responseMessage,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -47,18 +47,18 @@ export class StorageAPI {
|
||||
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
let localUploadDetails: Record<string, string> = {};
|
||||
|
||||
if (signingData) {
|
||||
const { signature, timestamp, uuid } = signingData;
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-Survey-ID": surveyId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
"X-UUID": uuid,
|
||||
localUploadDetails = {
|
||||
fileType: file.type,
|
||||
fileName: encodeURIComponent(updatedFileName),
|
||||
surveyId: surveyId ?? "",
|
||||
signature,
|
||||
timestamp: String(timestamp),
|
||||
uuid,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,14 +91,12 @@ export class StorageAPI {
|
||||
try {
|
||||
uploadResponse = await fetch(signedUrlCopy, {
|
||||
method: "POST",
|
||||
...(signingData
|
||||
? {
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
body: presignedFields ? formDataForS3 : JSON.stringify(formData),
|
||||
body: presignedFields
|
||||
? formDataForS3
|
||||
: JSON.stringify({
|
||||
...formData,
|
||||
...localUploadDetails,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error uploading file", err);
|
||||
|
||||
@@ -90,14 +90,17 @@ interface NetworkError {
|
||||
message: string;
|
||||
status: number;
|
||||
url: URL;
|
||||
responseMessage?: string;
|
||||
details?: Record<string, string | string[] | number | number[] | boolean | boolean[]>;
|
||||
}
|
||||
|
||||
interface ForbiddenError {
|
||||
code: "forbidden";
|
||||
message: string;
|
||||
responseMessage?: string;
|
||||
status: number;
|
||||
url: URL;
|
||||
responseMessage?: string;
|
||||
details?: Record<string, string | string[] | number | number[] | boolean | boolean[]>;
|
||||
}
|
||||
|
||||
export const ZErrorHandler = z.function().args(z.any()).returns(z.void());
|
||||
@@ -115,3 +118,21 @@ export {
|
||||
AuthorizationError,
|
||||
};
|
||||
export type { NetworkError, ForbiddenError };
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
code:
|
||||
| "not_found"
|
||||
| "gone"
|
||||
| "bad_request"
|
||||
| "internal_server_error"
|
||||
| "unauthorized"
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "network_error";
|
||||
message: string;
|
||||
status: number;
|
||||
url: URL;
|
||||
details?: Record<string, string | string[] | number | number[] | boolean | boolean[]>;
|
||||
responseMessage?: string;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"@formbricks/js#lint": {
|
||||
"dependsOn": ["@formbricks/js-core#build"]
|
||||
},
|
||||
"@formbricks/js-core#lint": {
|
||||
"dependsOn": ["@formbricks/api#build"]
|
||||
},
|
||||
"@formbricks/react-native#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
|
||||
Reference in New Issue
Block a user