mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-03 04:11:55 -05:00
Add user agent as meta data to responses (#398)
* chore: add ua parser * feat: add user agent to meta * feat: parse ua * feat: add tooltip * fix: type * fix: empty tooltip * fix: typo * add simple formatting to tooltip text --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -16,6 +17,7 @@ export async function OPTIONS(): Promise<NextResponse> {
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
const responseInput: TResponseInput = await request.json();
|
||||
const agent = UAParser(request.headers.get("user-agent"));
|
||||
const inputValidation = ZResponseInput.safeParse(responseInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -72,8 +74,20 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
const teamOwnerId = memberships[0]?.userId;
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await createResponse(responseInput);
|
||||
const meta = {
|
||||
userAgent: {
|
||||
browser: agent?.browser.name,
|
||||
device: agent?.device.type,
|
||||
os: agent?.os.name,
|
||||
},
|
||||
};
|
||||
|
||||
response = await createResponse({
|
||||
...responseInput,
|
||||
meta,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
|
||||
+68
-20
@@ -12,7 +12,7 @@ import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { RatingResponse } from "../RatingResponse";
|
||||
import ResponseNote from "./ResponseNote";
|
||||
@@ -30,6 +30,13 @@ export interface OpenTextSummaryProps {
|
||||
scale?: "number" | "star" | "smiley";
|
||||
range?: number;
|
||||
}[];
|
||||
meta?: {
|
||||
userAgent?: {
|
||||
browser?: string;
|
||||
os?: string;
|
||||
device?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +45,28 @@ function findEmail(person) {
|
||||
return emailAttribute ? emailAttribute.value : null;
|
||||
}
|
||||
|
||||
interface TooltipRendererProps {
|
||||
shouldRender: boolean;
|
||||
tooltipContent: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function TooltipRenderer(props: TooltipRendererProps) {
|
||||
const { children, shouldRender, tooltipContent } = props;
|
||||
if (shouldRender) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function SingleResponse({ data, environmentId, surveyId }: OpenTextSummaryProps) {
|
||||
const router = useRouter();
|
||||
const email = data.person && findEmail(data.person);
|
||||
@@ -55,16 +84,38 @@ export default function SingleResponse({ data, environmentId, surveyId }: OpenTe
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
const tooltipContent = data.personAttributes && Object.keys(data.personAttributes).length > 0 && (
|
||||
<TooltipContent>
|
||||
{Object.keys(data.personAttributes).map((key) => {
|
||||
return (
|
||||
<p>
|
||||
{key}: <span className="font-bold">{data.personAttributes && data.personAttributes[key]}</span>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</TooltipContent>
|
||||
const renderTooltip = Boolean(
|
||||
(data.personAttributes && Object.keys(data.personAttributes).length > 0) ||
|
||||
(data.meta?.userAgent && Object.keys(data.meta.userAgent).length > 0)
|
||||
);
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
{data.personAttributes && Object.keys(data.personAttributes).length > 0 && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">Person attributes:</p>
|
||||
{Object.keys(data.personAttributes).map((key) => (
|
||||
<p key={key}>
|
||||
{key}: <span className="font-bold">{data.personAttributes && data.personAttributes[key]}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.meta?.userAgent && Object.keys(data.meta.userAgent).length > 0 && (
|
||||
<div className="text-slate-600">
|
||||
{data.personAttributes && Object.keys(data.personAttributes).length > 0 && (
|
||||
<hr className="my-2 border-slate-200" />
|
||||
)}
|
||||
<p className="py-1 font-bold text-slate-700">Device info:</p>
|
||||
{data.meta?.userAgent?.browser && <p>Browser: {data.meta.userAgent.browser}</p>}
|
||||
{data.meta?.userAgent?.os && <p>OS: {data.meta.userAgent.os}</p>}
|
||||
{data.meta?.userAgent && (
|
||||
<p>Device: {data.meta.userAgent.device ? data.meta.userAgent.device : "PC / Generic device"}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -80,21 +131,18 @@ export default function SingleResponse({ data, environmentId, surveyId }: OpenTe
|
||||
<Link
|
||||
className="group flex items-center"
|
||||
href={`/environments/${environmentId}/people/${data.person.id}`}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<PersonAvatar personId={data.person.id} />
|
||||
</TooltipTrigger>
|
||||
{tooltipContent}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<PersonAvatar personId={data.person.id} />
|
||||
</TooltipRenderer>
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</TooltipRenderer>
|
||||
<h3 className="ml-4 pb-1 font-semibold text-slate-600">Anonymous</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"stripe": "^12.6.0",
|
||||
"swr": "^2.1.5",
|
||||
"typescript": "5.0.4",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -12,6 +12,7 @@ const responseSelection = {
|
||||
surveyId: true,
|
||||
finished: true,
|
||||
data: true,
|
||||
meta: true,
|
||||
personAttributes: true,
|
||||
person: {
|
||||
select: {
|
||||
@@ -82,6 +83,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
||||
},
|
||||
personAttributes: person?.attributes,
|
||||
}),
|
||||
...(responseInput.meta && ({ meta: responseInput?.meta } as Prisma.JsonObject)),
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
@@ -52,6 +52,17 @@ export const ZResponseInput = z.object({
|
||||
personId: z.string().cuid2().nullable(),
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
meta: z
|
||||
.object({
|
||||
userAgent: z
|
||||
.object({
|
||||
browser: z.string().optional(),
|
||||
device: z.string().optional(),
|
||||
os: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TResponseInput = z.infer<typeof ZResponseInput>;
|
||||
|
||||
Generated
+106
-17
@@ -1,9 +1,5 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -319,6 +315,9 @@ importers:
|
||||
typescript:
|
||||
specifier: 5.0.4
|
||||
version: 5.0.4
|
||||
ua-parser-js:
|
||||
specifier: ^1.0.35
|
||||
version: 1.0.35
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
version: 3.21.4
|
||||
@@ -392,7 +391,7 @@ importers:
|
||||
version: 5.0.1
|
||||
tsup:
|
||||
specifier: ^6.7.0
|
||||
version: 6.7.0(typescript@5.0.4)
|
||||
version: 6.7.0
|
||||
|
||||
packages/database:
|
||||
dependencies:
|
||||
@@ -484,7 +483,7 @@ importers:
|
||||
version: 5.0.1
|
||||
tsup:
|
||||
specifier: ^6.7.0
|
||||
version: 6.7.0(typescript@5.0.4)
|
||||
version: 6.7.0
|
||||
|
||||
packages/eslint-config-formbricks:
|
||||
dependencies:
|
||||
@@ -10404,7 +10403,7 @@ packages:
|
||||
eslint: 8.41.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.41.0)
|
||||
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-typescript@3.5.2)(eslint@8.41.0)
|
||||
eslint-plugin-import: 2.26.0(eslint@8.41.0)
|
||||
eslint-plugin-jsx-a11y: 6.6.1(eslint@8.41.0)
|
||||
eslint-plugin-react: 7.32.2(eslint@8.41.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.41.0)
|
||||
@@ -10429,7 +10428,7 @@ packages:
|
||||
eslint: 8.41.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.41.0)
|
||||
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-typescript@3.5.2)(eslint@8.41.0)
|
||||
eslint-plugin-import: 2.26.0(eslint@8.41.0)
|
||||
eslint-plugin-jsx-a11y: 6.6.1(eslint@8.41.0)
|
||||
eslint-plugin-react: 7.32.2(eslint@8.41.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.41.0)
|
||||
@@ -10498,7 +10497,7 @@ packages:
|
||||
debug: 4.3.4
|
||||
enhanced-resolve: 5.12.0
|
||||
eslint: 8.41.0
|
||||
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-typescript@3.5.2)(eslint@8.41.0)
|
||||
eslint-plugin-import: 2.26.0(eslint@8.41.0)
|
||||
get-tsconfig: 4.4.0
|
||||
globby: 13.1.2
|
||||
is-core-module: 2.11.0
|
||||
@@ -10508,7 +10507,7 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@3.5.2)(eslint@8.41.0):
|
||||
/eslint-module-utils@2.7.4(eslint-import-resolver-node@0.3.6)(eslint@8.41.0):
|
||||
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -10529,11 +10528,9 @@ packages:
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.59.8(eslint@8.41.0)(typescript@5.1.3)
|
||||
debug: 3.2.7
|
||||
eslint: 8.41.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.41.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
@@ -10554,7 +10551,7 @@ packages:
|
||||
semver: 7.3.8
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-typescript@3.5.2)(eslint@8.41.0):
|
||||
/eslint-plugin-import@2.26.0(eslint@8.41.0):
|
||||
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -10564,14 +10561,13 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.59.8(eslint@8.41.0)(typescript@5.1.3)
|
||||
array-includes: 3.1.6
|
||||
array.prototype.flat: 1.3.1
|
||||
debug: 2.6.9
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.41.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@3.5.2)(eslint@8.41.0)
|
||||
eslint-module-utils: 2.7.4(eslint-import-resolver-node@0.3.6)(eslint@8.41.0)
|
||||
has: 1.0.3
|
||||
is-core-module: 2.11.0
|
||||
is-glob: 4.0.3
|
||||
@@ -13154,7 +13150,7 @@ packages:
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.10
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.5.0(@types/node@20.2.3)
|
||||
jest-config: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
prompts: 2.4.2
|
||||
@@ -13165,6 +13161,44 @@ packages:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/jest-config@29.5.0:
|
||||
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
ts-node: '>=9.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.20.12
|
||||
'@jest/test-sequencer': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
babel-jest: 29.5.0(@babel/core@7.20.12)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.7.0
|
||||
deepmerge: 4.2.2
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.10
|
||||
jest-circus: 29.5.0
|
||||
jest-environment-node: 29.5.0
|
||||
jest-get-type: 29.4.3
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.5.0
|
||||
jest-runner: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
micromatch: 4.0.5
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.5.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/jest-config@29.5.0(@types/node@20.2.3):
|
||||
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -16596,6 +16630,22 @@ packages:
|
||||
postcss: 8.4.23
|
||||
dev: true
|
||||
|
||||
/postcss-load-config@3.1.4:
|
||||
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
||||
engines: {node: '>= 10'}
|
||||
peerDependencies:
|
||||
postcss: '>=8.0.9'
|
||||
ts-node: '>=9.0.0'
|
||||
peerDependenciesMeta:
|
||||
postcss:
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
dependencies:
|
||||
lilconfig: 2.1.0
|
||||
yaml: 1.10.2
|
||||
dev: true
|
||||
|
||||
/postcss-load-config@3.1.4(postcss@8.4.21):
|
||||
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -20392,6 +20442,41 @@ packages:
|
||||
/tslib@2.5.2:
|
||||
resolution: {integrity: sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==}
|
||||
|
||||
/tsup@6.7.0:
|
||||
resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==}
|
||||
engines: {node: '>=14.18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@swc/core': ^1
|
||||
postcss: ^8.4.12
|
||||
typescript: '>=4.1.0'
|
||||
peerDependenciesMeta:
|
||||
'@swc/core':
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
bundle-require: 4.0.1(esbuild@0.17.11)
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
debug: 4.3.4
|
||||
esbuild: 0.17.11
|
||||
execa: 5.1.1
|
||||
globby: 11.1.0
|
||||
joycon: 3.1.1
|
||||
postcss-load-config: 3.1.4
|
||||
resolve-from: 5.0.0
|
||||
rollup: 3.5.1
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.29.0
|
||||
tree-kill: 1.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/tsup@6.7.0(postcss@8.4.24)(typescript@5.0.4):
|
||||
resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==}
|
||||
engines: {node: '>=14.18'}
|
||||
@@ -20453,7 +20538,7 @@ packages:
|
||||
execa: 5.1.1
|
||||
globby: 11.1.0
|
||||
joycon: 3.1.1
|
||||
postcss-load-config: 3.1.4(postcss@8.4.24)
|
||||
postcss-load-config: 3.1.4
|
||||
resolve-from: 5.0.0
|
||||
rollup: 3.5.1
|
||||
source-map: 0.8.0-beta.0
|
||||
@@ -20688,6 +20773,10 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
/ua-parser-js@1.0.35:
|
||||
resolution: {integrity: sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==}
|
||||
dev: false
|
||||
|
||||
/uc.micro@1.0.6:
|
||||
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
|
||||
dev: false
|
||||
|
||||
Reference in New Issue
Block a user