Merge dev → main: surveys, P/OEM page, trial upsell, catalog-issue report, pcat timings #127
@@ -30,6 +30,8 @@ ARG VITE_POSTHOG_KEY=
|
||||
ARG VITE_META_PIXEL_ID=
|
||||
ARG VITE_CHATWOOT_BASE_URL=
|
||||
ARG VITE_CHATWOOT_WEBSITE_TOKEN=
|
||||
ARG VITE_FORMBRICKS_APP_URL=
|
||||
ARG VITE_FORMBRICKS_ENV_ID=
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ async function bootstrap() {
|
||||
"https://storage.sase.tr",
|
||||
"https://www.facebook.com",
|
||||
"https://destek.sase.tr",
|
||||
"https://anket.sase.tr",
|
||||
],
|
||||
fontSrc: ["'self'", "https:", "data:"],
|
||||
mediaSrc: ["'self'", "data:", "https://destek.sase.tr"],
|
||||
@@ -51,6 +52,7 @@ async function bootstrap() {
|
||||
"https://challenges.cloudflare.com",
|
||||
"https://destek.sase.tr",
|
||||
"wss://destek.sase.tr",
|
||||
"https://anket.sase.tr",
|
||||
// Sentry browser SDK envelope POSTs (otolog org, de region).
|
||||
// Without this CSP silently blocks every error/replay upload.
|
||||
"https://*.ingest.de.sentry.io",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@cardog-icons/react": "^1.1.1",
|
||||
"@formbricks/js": "4.4.0",
|
||||
"@grafana/faro-web-sdk": "^2.2.4",
|
||||
"@grafana/faro-web-tracing": "^2.2.4",
|
||||
"@remotion/player": "^4.0.422",
|
||||
|
||||
79
apps/web/src/lib/formbricks.ts
Normal file
79
apps/web/src/lib/formbricks.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// Formbricks in-app surveys (self-hosted at anket.sase.tr) — lazily loaded so
|
||||
// it stays out of the initial bundle. All exported functions are fire-and-forget;
|
||||
// when the env vars are unset (e.g. local dev) every call is a no-op.
|
||||
//
|
||||
// PostHog stays the single instrumentation source: lib/posthog.ts forwards
|
||||
// allowlisted capture() events here (FORMBRICKS_TRIGGER_EVENTS) so surveys
|
||||
// trigger on the same event names that exist in PostHog.
|
||||
|
||||
type Formbricks = typeof import("@formbricks/js")["default"];
|
||||
|
||||
const APP_URL = import.meta.env.VITE_FORMBRICKS_APP_URL;
|
||||
const ENV_ID = import.meta.env.VITE_FORMBRICKS_ENV_ID;
|
||||
|
||||
let _setupPromise: Promise<Formbricks | null> | null = null;
|
||||
|
||||
function load(): Promise<Formbricks | null> {
|
||||
if (!APP_URL || !ENV_ID) return Promise.resolve(null);
|
||||
if (_setupPromise) return _setupPromise;
|
||||
_setupPromise = import("@formbricks/js")
|
||||
.then(async (m) => {
|
||||
await m.default.setup({ environmentId: ENV_ID, appUrl: APP_URL });
|
||||
return m.default;
|
||||
})
|
||||
.catch(() => null);
|
||||
return _setupPromise;
|
||||
}
|
||||
|
||||
export function initFormbricks(): void {
|
||||
void load();
|
||||
}
|
||||
|
||||
/** Forwarded from posthog.ts capture() for allowlisted events — shows any
|
||||
* survey whose trigger action matches the event code. */
|
||||
export function trackFormbricks(event: string): void {
|
||||
load()
|
||||
.then((fb) => fb?.track(event))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
export function identifyFormbricksUser(user: {
|
||||
id: string;
|
||||
email: string;
|
||||
role?: string;
|
||||
}): void {
|
||||
load()
|
||||
.then(async (fb) => {
|
||||
if (!fb) return;
|
||||
await fb.setUserId(user.id);
|
||||
await fb.setEmail(user.email);
|
||||
if (user.role) await fb.setAttribute("role", user.role);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** Person properties → Formbricks attributes (string-only API), so surveys can
|
||||
* target e.g. subscription_status=trial the same way PostHog cohorts do. */
|
||||
export function setFormbricksAttributes(properties: Record<string, unknown>): void {
|
||||
const attrs: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (value !== null && value !== undefined) attrs[key] = String(value);
|
||||
}
|
||||
if (Object.keys(attrs).length === 0) return;
|
||||
load()
|
||||
.then((fb) => fb?.setAttributes(attrs))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
export function resetFormbricksUser(): void {
|
||||
load()
|
||||
.then((fb) => fb?.logout())
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** SPA navigation hook — lets URL-based (no-code) survey triggers fire. */
|
||||
export function formbricksRouteChange(): void {
|
||||
load()
|
||||
.then((fb) => fb?.registerRouteChange())
|
||||
.catch(() => {});
|
||||
}
|
||||
@@ -1,8 +1,26 @@
|
||||
// PostHog — lazily loaded so it doesn't land in the initial bundle.
|
||||
// All exported functions are fire-and-forget; analytics loss on failure is acceptable.
|
||||
|
||||
import {
|
||||
formbricksRouteChange,
|
||||
identifyFormbricksUser,
|
||||
resetFormbricksUser,
|
||||
setFormbricksAttributes,
|
||||
trackFormbricks,
|
||||
} from "./formbricks";
|
||||
|
||||
type PostHog = import("posthog-js").PostHog;
|
||||
|
||||
// Events that double as Formbricks survey triggers (mirrored as code actions
|
||||
// at anket.sase.tr). Keep in sync when adding a survey with an event trigger.
|
||||
const FORMBRICKS_TRIGGER_EVENTS = new Set([
|
||||
"trial_urgency_banner_viewed",
|
||||
"subscription_cancelled",
|
||||
"onboarding_completed",
|
||||
"vin_decode_error",
|
||||
"empty_catalog_cta_clicked",
|
||||
]);
|
||||
|
||||
let _ph: PostHog | null = null;
|
||||
let _loadPromise: Promise<PostHog> | null = null;
|
||||
let _initialized = false;
|
||||
@@ -60,14 +78,17 @@ export function identifyUser(user: {
|
||||
role: user.role,
|
||||
});
|
||||
});
|
||||
identifyFormbricksUser({ id: user.id, email: user.email, role: user.role });
|
||||
}
|
||||
|
||||
export function resetUser(): void {
|
||||
load().then((ph) => ph.reset());
|
||||
resetFormbricksUser();
|
||||
}
|
||||
|
||||
export function capture(event: string, properties?: Record<string, unknown>): void {
|
||||
load().then((ph) => ph.capture(event, properties));
|
||||
if (FORMBRICKS_TRIGGER_EVENTS.has(event)) trackFormbricks(event);
|
||||
}
|
||||
|
||||
export function capturePageView(path: string): void {
|
||||
@@ -79,10 +100,12 @@ export function capturePageView(path: string): void {
|
||||
load().then((ph) =>
|
||||
ph.capture("$pageview", { $current_url: window.location.href, $pathname: path }),
|
||||
);
|
||||
formbricksRouteChange();
|
||||
}
|
||||
|
||||
export function setPeopleProperties(properties: Record<string, unknown>): void {
|
||||
load().then((ph) => ph.people?.set(properties));
|
||||
setFormbricksAttributes(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { initChatwoot } from "./lib/chatwoot";
|
||||
import { initFaro } from "./lib/faro";
|
||||
import { initFormbricks } from "./lib/formbricks";
|
||||
import { initLocale } from "./lib/i18n";
|
||||
import { initMetaPixel } from "./lib/meta-pixel";
|
||||
import { initPostHog } from "./lib/posthog";
|
||||
@@ -30,6 +31,9 @@ initMetaPixel();
|
||||
// Initialize Chatwoot live-chat widget
|
||||
initChatwoot();
|
||||
|
||||
// Initialize Formbricks in-app surveys
|
||||
initFormbricks();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -10,6 +10,8 @@ services:
|
||||
- VITE_META_PIXEL_ID=${VITE_META_PIXEL_ID:-}
|
||||
- VITE_CHATWOOT_BASE_URL=${VITE_CHATWOOT_BASE_URL:-}
|
||||
- VITE_CHATWOOT_WEBSITE_TOKEN=${VITE_CHATWOOT_WEBSITE_TOKEN:-}
|
||||
- VITE_FORMBRICKS_APP_URL=${VITE_FORMBRICKS_APP_URL:-}
|
||||
- VITE_FORMBRICKS_ENV_ID=${VITE_FORMBRICKS_ENV_ID:-}
|
||||
# Sentry browser SDK — gated on DSN presence (unset = skip init).
|
||||
# VITE_* args must be build-time; runtime env won't reach the bundle.
|
||||
- VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-}
|
||||
|
||||
74
pnpm-lock.yaml
generated
74
pnpm-lock.yaml
generated
@@ -199,6 +199,9 @@ importers:
|
||||
'@cardog-icons/react':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(react@19.2.4)
|
||||
'@formbricks/js':
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
'@grafana/faro-web-sdk':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
@@ -225,7 +228,7 @@ importers:
|
||||
version: 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
better-auth:
|
||||
specifier: ^1.2.0
|
||||
version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8))(mysql2@3.22.4(@types/node@22.19.11))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8))(mysql2@3.22.4(@types/node@22.19.11))(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
canvas-confetti:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
@@ -1328,6 +1331,9 @@ packages:
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@formbricks/js@4.4.0':
|
||||
resolution: {integrity: sha512-97nu0lOhsDXmymE2bwg+PJqCXD7gIl5YbUaXWnvEGdRwyKmPZ3hFPEmw05G3y5xqjjd2XkmfuIYNP8x6WVYwMw==}
|
||||
|
||||
'@grafana/faro-core@2.2.4':
|
||||
resolution: {integrity: sha512-VUmvJgHUzgB7zGROpQfOHEW6xumwvcHJNwTSFLkAVfl7A5aNq+fXnMawn+W7qMJuUFkmUG6lMZ2N+WCvd0GPTA==}
|
||||
|
||||
@@ -7471,6 +7477,8 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@formbricks/js@4.4.0': {}
|
||||
|
||||
'@grafana/faro-core@2.2.4':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -10176,30 +10184,7 @@ snapshots:
|
||||
drizzle-kit: 0.31.9
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8)
|
||||
mysql2: 3.22.4(@types/node@22.19.11)
|
||||
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
vitest: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8))(mysql2@3.22.4(@types/node@22.19.11))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
||||
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
|
||||
'@better-auth/utils': 0.3.0
|
||||
'@better-fetch/fetch': 1.1.21
|
||||
'@noble/ciphers': 2.1.1
|
||||
'@noble/hashes': 2.0.1
|
||||
better-call: 1.1.8(zod@4.3.6)
|
||||
defu: 6.1.4
|
||||
jose: 6.1.3
|
||||
kysely: 0.28.11
|
||||
nanostores: 1.1.0
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
drizzle-kit: 0.31.9
|
||||
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8)
|
||||
mysql2: 3.22.4(@types/node@22.19.11)
|
||||
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
vitest: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
@@ -10632,16 +10617,6 @@ snapshots:
|
||||
mysql2: 3.22.4(@types/node@22.19.11)
|
||||
postgres: 3.4.8
|
||||
|
||||
drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8):
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@types/pg': 8.15.6
|
||||
gel: 2.2.0
|
||||
kysely: 0.28.11
|
||||
mysql2: 3.22.4(@types/node@22.19.11)
|
||||
postgres: 3.4.8
|
||||
optional: true
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -11893,34 +11868,7 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 16.1.6
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.9.19
|
||||
caniuse-lite: 1.0.30001769
|
||||
postcss: 8.4.31
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.1.6
|
||||
'@next/swc-darwin-x64': 16.1.6
|
||||
'@next/swc-linux-arm64-gnu': 16.1.6
|
||||
'@next/swc-linux-arm64-musl': 16.1.6
|
||||
'@next/swc-linux-x64-gnu': 16.1.6
|
||||
'@next/swc-linux-x64-musl': 16.1.6
|
||||
'@next/swc-win32-arm64-msvc': 16.1.6
|
||||
'@next/swc-win32-x64-msvc': 16.1.6
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@playwright/test': 1.58.2
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
optional: true
|
||||
|
||||
next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 16.1.6
|
||||
'@swc/helpers': 0.5.15
|
||||
|
||||
Reference in New Issue
Block a user