Merge dev → main: surveys, P/OEM page, trial upsell, catalog-issue report, pcat timings #127

Merged
root merged 36 commits from merge-dev-to-main into main 2026-06-11 16:28:16 +03:00
8 changed files with 124 additions and 63 deletions
Showing only changes of commit 597391bbdf - Show all commits

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View 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(() => {});
}

View File

@@ -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);
}
/**

View File

@@ -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: {

View File

@@ -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
View File

@@ -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