feat: attributes on initialising formbricks (#1736)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2023-12-06 18:09:46 +05:30
committed by GitHub
parent b9def78d2e
commit e15b00a9f7
8 changed files with 179 additions and 18 deletions

View File

@@ -22,11 +22,13 @@ export default function AppPage({}) {
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const isUserId = window.location.href.includes("userId=true");
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
formbricks.init({
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
userId,
debug: true,
attributes,
});
window.formbricks = formbricks;
}

View File

@@ -10,9 +10,31 @@ export const metadata = {
One way to send attributes to Formbricks is in your code. In Formbricks, there are two special attributes for [user identification](/docs/attributes/identify-users)(user ID & email) and custom attributes. An example:
## Setting Custom User Attributes
## Setting during Initialization
It's recommended to set custom user attributes directly during the initialization of Formbricks for better user identification.
<Col>
<CodeGroup title="Set custom attributes during initialization">
```javascript
formbricks.init({
environmentId: "<environment-id>",
apiHost: "<api-host>",
userId: "<user_id>",
attributes: {
plan: "free",
},
});
```
</CodeGroup>
</Col>
## Setting independently
You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.) anywhere in the user journey. Formbricks maintains a state of the current user inside the browser and makes sure attributes aren't sent to the backend twice.
You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.):
<Col>
<CodeGroup title="Setting Plan to Pro">

View File

@@ -29,6 +29,29 @@ formbricks.init({
</CodeGroup>
</Col>
## Enhanced Initialization with User Attributes
In addition to setting the `userId`, Formbricks allows you to set user attributes right at the initialization. This ensures that your user data is seamlessly integrated from the start. Here's how you can include user attributes in the `init()` function:
<Col>
<CodeGroup title="Enhanced Initialization with User Attributes">
```javascript
formbricks.init({
environmentId: "<environment-id>",
apiHost: "<api-host>",
userId: "<user_id>",
attributes: {
// your custom attributes
Plan: "premium",
},
});
```
</CodeGroup>
</Col>
## Setting User Email
The `userId` is the main identifier used in Formbricks and user identification is only enabled when it is set. In addition to the userId you can also set attributes that describes the user better. The email address can be set using the setEmail function:

View File

@@ -1,7 +1,7 @@
{
"name": "@formbricks/js",
"license": "MIT",
"version": "1.2.6",
"version": "1.2.7",
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
"keywords": [
"Formbricks",

View File

@@ -7,6 +7,13 @@ export class Config {
private static instance: Config | undefined;
private config: TJsConfig | null = null;
private constructor() {
const localConfig = this.loadFromLocalStorage();
if (localConfig.ok) {
this.config = localConfig.value;
}
}
static getInstance(): Config {
if (!Config.instance) {
Config.instance = new Config();

View File

@@ -1,4 +1,4 @@
import type { TJsConfigInput } from "@formbricks/types/js";
import type { TJsConfig, TJsConfigInput } from "@formbricks/types/js";
import { Config } from "./config";
import {
ErrorHandler,
@@ -16,6 +16,8 @@ import { checkPageUrl } from "./noCodeActions";
import { sync } from "./sync";
import { addWidgetContainer, closeSurvey } from "./widget";
import { trackAction } from "./actions";
import { updatePersonAttributes } from "./person";
import { TPersonAttributes } from "@formbricks/types/people";
const config = Config.getInstance();
const logger = Logger.getInstance();
@@ -63,19 +65,44 @@ export const initialize = async (
logger.debug("Adding widget container to DOM");
addWidgetContainer();
const localConfigResult = config.loadFromLocalStorage();
if (!c.userId && c.attributes) {
logger.error("No userId provided but attributes. Cannot update attributes without userId.");
return err({
code: "missing_field",
field: "userId",
});
}
// if userId and attributes are available, set them in backend
let updatedAttributes: TPersonAttributes | null = null;
if (c.userId && c.attributes) {
const res = await updatePersonAttributes(c.apiHost, c.environmentId, c.userId, c.attributes);
if (res.ok !== true) {
return err(res.error);
}
updatedAttributes = res.value;
}
let existingConfig: TJsConfig | undefined;
try {
existingConfig = config.get();
} catch (e) {
logger.debug("No existing configuration found.");
}
if (
localConfigResult.ok &&
localConfigResult.value.state &&
localConfigResult.value.environmentId === c.environmentId &&
localConfigResult.value.apiHost === c.apiHost &&
localConfigResult.value.userId === c.userId &&
localConfigResult.value.expiresAt // only accept config when they follow new config version with expiresAt
existingConfig &&
existingConfig.state &&
existingConfig.environmentId === c.environmentId &&
existingConfig.apiHost === c.apiHost &&
existingConfig.userId === c.userId &&
existingConfig.expiresAt // only accept config when they follow new config version with expiresAt
) {
logger.debug("Found existing configuration.");
if (localConfigResult.value.expiresAt < new Date()) {
if (existingConfig.expiresAt < new Date()) {
logger.debug("Configuration expired.");
await sync({
apiHost: c.apiHost,
environmentId: c.environmentId,
@@ -83,14 +110,12 @@ export const initialize = async (
});
} else {
logger.debug("Configuration not expired. Extending expiration.");
config.update(localConfigResult.value);
config.update(existingConfig);
}
} else {
logger.debug("No valid configuration found or it has been expired. Creating new config.");
logger.debug("Syncing.");
// when the local storage is expired / empty, we sync to get the latest config
await sync({
apiHost: c.apiHost,
environmentId: c.environmentId,
@@ -98,7 +123,20 @@ export const initialize = async (
});
// and track the new session event
trackAction("New Session");
await trackAction("New Session");
}
// update attributes in config
if (updatedAttributes && Object.keys(updatedAttributes).length > 0) {
config.update({
environmentId: config.get().environmentId,
apiHost: config.get().apiHost,
userId: config.get().userId,
state: {
...config.get().state,
attributes: { ...config.get().state.attributes, ...c.attributes },
},
});
}
logger.debug("Adding event listeners");

View File

@@ -1,7 +1,15 @@
import { FormbricksAPI } from "@formbricks/api";
import { TPersonUpdateInput } from "@formbricks/types/people";
import { TPersonAttributes, TPersonUpdateInput } from "@formbricks/types/people";
import { Config } from "./config";
import { AttributeAlreadyExistsError, MissingPersonError, NetworkError, Result, err, okVoid } from "./errors";
import {
AttributeAlreadyExistsError,
MissingPersonError,
NetworkError,
Result,
err,
ok,
okVoid,
} from "./errors";
import { deinitalize, initialize } from "./initialize";
import { Logger } from "./logger";
import { sync } from "./sync";
@@ -55,6 +63,66 @@ export const updatePersonAttribute = async (
return okVoid();
};
export const updatePersonAttributes = async (
apiHost: string,
environmentId: string,
userId: string,
attributes: TPersonAttributes
): Promise<Result<TPersonAttributes, NetworkError | MissingPersonError>> => {
if (!userId) {
return err({
code: "missing_person",
message: "Unable to update attribute. User identification deactivated. No userId set.",
});
}
// clean attributes and remove existing attributes if config already exists
const updatedAttributes = { ...attributes };
try {
const existingAttributes = config.get()?.state?.attributes;
if (existingAttributes) {
for (const [key, value] of Object.entries(existingAttributes)) {
if (updatedAttributes[key] === value) {
delete updatedAttributes[key];
}
}
}
} catch (e) {
logger.debug("config not set; sending all attributes to backend");
}
// send to backend if updatedAttributes is not empty
if (Object.keys(updatedAttributes).length === 0) {
logger.debug("No attributes to update. Skipping update.");
return ok(updatedAttributes);
}
logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes));
const input: TPersonUpdateInput = {
attributes: updatedAttributes,
};
const api = new FormbricksAPI({
apiHost,
environmentId,
});
const res = await api.client.people.update(userId, input);
if (res.ok) {
return ok(updatedAttributes);
}
return err({
code: "network_error",
status: 500,
message: `Error updating person with userId ${userId}`,
url: `${apiHost}/api/v1/client/${environmentId}/people/${userId}`,
responseMessage: res.error.message,
});
};
export const isExistingAttribute = (key: string, value: string): boolean => {
if (config.get().state.attributes[key] === value) {
return true;

View File

@@ -96,6 +96,7 @@ export const ZJsConfigInput = z.object({
debug: z.boolean().optional(),
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
userId: z.string().optional(),
attributes: ZPersonAttributes.optional(),
});
export type TJsConfigInput = z.infer<typeof ZJsConfigInput>;