feat(client): added posthog analytics (#151)

This commit is contained in:
Corentin Thomasset
2025-03-14 23:18:52 +01:00
committed by GitHub
parent 2fd681b8a4
commit 3e7b4ea2db
10 changed files with 181 additions and 60 deletions

View File

@@ -48,6 +48,7 @@
"lodash-es": "^4.17.21",
"ofetch": "^1.4.1",
"plausible-tracker": "^0.3.9",
"posthog-js": "^1.231.0",
"radix3": "^1.1.2",
"solid-js": "^1.8.11",
"solid-sonner": "^0.2.8",

View File

@@ -10,9 +10,10 @@ import { CommandPaletteProvider } from './modules/command-palette/command-palett
import { ConfigProvider } from './modules/config/config.provider';
import { DemoIndicator } from './modules/demo/demo.provider';
import { I18nProvider } from './modules/i18n/i18n.provider';
import { PlausibleTracker } from './modules/plausible/components/plausible-tracker.component';
import { ConfirmModalProvider } from './modules/shared/confirm';
import { queryClient } from './modules/shared/query/query-client';
import { IdentifyUser } from './modules/tracking/components/identify-user.component';
import { PageViewTracker } from './modules/tracking/components/pageview-tracker.component';
import { Toaster } from './modules/ui/components/sonner';
import { routes } from './routes';
import '@unocss/reset/tailwind.css';
@@ -30,7 +31,9 @@ render(
children={routes}
root={props => (
<QueryClientProvider client={queryClient}>
<PlausibleTracker />
<PageViewTracker />
<IdentifyUser />
<Suspense>
<I18nProvider>
<ConfirmModalProvider>

View File

@@ -2,13 +2,25 @@ import { organizationClient as organizationClientPlugin } from 'better-auth/clie
import { createAuthClient as createBetterAuthClient } from 'better-auth/solid';
import { buildTimeConfig } from '../config/config';
import { trackingServices } from '../tracking/tracking.services';
import { createDemoAuthClient } from './auth.demo.services';
export function createAuthClient() {
return createBetterAuthClient({
const client = createBetterAuthClient({
baseURL: buildTimeConfig.baseApiUrl,
plugins: [organizationClientPlugin()],
});
return {
...client,
signOut: async () => {
trackingServices.capture({ event: 'User logged out' });
const result = await client.signOut();
trackingServices.reset();
return result;
},
};
}
export const {

View File

@@ -1,34 +1,36 @@
export const isDev = import.meta.env.MODE === 'development';
const asBoolean = (value: string | undefined, defaultValue: boolean) => value === undefined ? defaultValue : value.trim().toLowerCase() === 'true';
const asString = <T extends string | undefined>(value: string | undefined, defaultValue?: T): T extends undefined ? string | undefined : string => (value ?? defaultValue) as T extends undefined ? string | undefined : string;
const asNumber = <T extends number | undefined>(value: string | undefined, defaultValue?: T): T extends undefined ? number | undefined : number => (value === undefined ? defaultValue : Number(value)) as T extends undefined ? number | undefined : number;
export const buildTimeConfig = {
papraVersion: import.meta.env.VITE_PAPRA_VERSION,
baseUrl: (import.meta.env.VITE_BASE_URL ?? window.location.origin) as string,
baseApiUrl: (import.meta.env.VITE_BASE_API_URL ?? window.location.origin) as string,
vitrineBaseUrl: (import.meta.env.VITE_VITRINE_BASE_URL ?? 'http://localhost:3000/') as string,
isDemoMode: import.meta.env.VITE_IS_DEMO_MODE === 'true',
papraVersion: asString(import.meta.env.VITE_PAPRA_VERSION),
baseUrl: asString(import.meta.env.VITE_BASE_URL, window.location.origin),
baseApiUrl: asString(import.meta.env.VITE_BASE_API_URL, window.location.origin),
vitrineBaseUrl: asString(import.meta.env.VITE_VITRINE_BASE_URL, 'http://localhost:3000/'),
isDemoMode: asBoolean(import.meta.env.VITE_IS_DEMO_MODE, false),
auth: {
isRegistrationEnabled: import.meta.env.VITE_AUTH_IS_REGISTRATION_ENABLED !== 'false',
isPasswordResetEnabled: import.meta.env.VITE_AUTH_IS_PASSWORD_RESET_ENABLED !== 'false',
isEmailVerificationRequired: import.meta.env.VITE_AUTH_IS_EMAIL_VERIFICATION_REQUIRED !== 'false',
showLegalLinksOnAuthPage: import.meta.env.VITE_AUTH_SHOW_LEGAL_LINKS_ON_AUTH_PAGE === 'true',
isRegistrationEnabled: asBoolean(import.meta.env.VITE_AUTH_IS_REGISTRATION_ENABLED, true),
isPasswordResetEnabled: asBoolean(import.meta.env.VITE_AUTH_IS_PASSWORD_RESET_ENABLED, true),
isEmailVerificationRequired: asBoolean(import.meta.env.VITE_AUTH_IS_EMAIL_VERIFICATION_REQUIRED, true),
showLegalLinksOnAuthPage: asBoolean(import.meta.env.VITE_AUTH_SHOW_LEGAL_LINKS_ON_AUTH_PAGE, false),
providers: {
github: {
isEnabled: import.meta.env.VITE_AUTH_PROVIDERS_GITHUB_IS_ENABLED === 'true',
},
google: {
isEnabled: import.meta.env.VITE_AUTH_PROVIDERS_GOOGLE_IS_ENABLED === 'true',
},
github: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GITHUB_IS_ENABLED, false) },
google: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GOOGLE_IS_ENABLED, false) },
},
},
documents: {
deletedDocumentsRetentionDays: Number(import.meta.env.VITE_DOCUMENTS_DELETED_DOCUMENTS_RETENTION_DAYS ?? 30),
deletedDocumentsRetentionDays: asNumber(import.meta.env.VITE_DOCUMENTS_DELETED_DOCUMENTS_RETENTION_DAYS, 30),
},
plausible: {
isEnabled: import.meta.env.VITE_PLAUSIBLE_IS_ENABLED === 'true',
domain: import.meta.env.VITE_PLAUSIBLE_DOMAIN,
apiHost: import.meta.env.VITE_PLAUSIBLE_API_HOST,
posthog: {
apiKey: asString(import.meta.env.VITE_POSTHOG_API_KEY),
host: asString(import.meta.env.VITE_POSTHOG_HOST),
isEnabled: asBoolean(import.meta.env.VITE_POSTHOG_API_KEY, false),
},
intakeEmails: {
isEnabled: import.meta.env.VITE_INTAKE_EMAILS_IS_ENABLED === 'true',
emailGenerationDomain: import.meta.env.VITE_INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN,
isEnabled: asBoolean(import.meta.env.VITE_INTAKE_EMAILS_IS_ENABLED, false),
emailGenerationDomain: asString(import.meta.env.VITE_INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN),
},
} as const;

View File

@@ -3,6 +3,7 @@ import type { Organization } from '../organizations/organizations.types';
import type { Tag } from '../tags/tags.types';
import { createStorage, prefixStorage } from 'unstorage';
import localStorageDriver from 'unstorage/drivers/localstorage';
import { trackingServices } from '../tracking/tracking.services';
const storage = createStorage<any>({
driver: localStorageDriver({ base: 'demo:' }),
@@ -16,4 +17,5 @@ export const tagDocumentStorage = prefixStorage<{ documentId: string; tagId: str
export async function clearDemoStorage() {
await storage.clear();
trackingServices.capture({ event: 'Demo storage cleared' });
}

View File

@@ -1,35 +0,0 @@
import { buildTimeConfig } from '@/modules/config/config';
import { buildUrl } from '@corentinth/chisels';
import { useCurrentMatches } from '@solidjs/router';
import Plausible from 'plausible-tracker';
import { type Component, createEffect } from 'solid-js';
export const PlausibleTracker: Component = () => {
const { isEnabled, apiHost, domain } = buildTimeConfig.plausible;
if (!isEnabled) {
return null;
}
const plausible = Plausible({
domain,
apiHost,
trackLocalhost: false,
});
const matches = useCurrentMatches();
createEffect(() => {
const basePattern = matches().at(-1)?.route.pattern ?? '/';
const pattern = basePattern === '*404' ? window.location.pathname : basePattern;
const url = buildUrl({
path: pattern,
baseUrl: window.location.origin,
});
plausible.trackPageview({ url });
});
return null;
};

View File

@@ -0,0 +1,20 @@
import { useSession } from '@/modules/auth/auth.services';
import { type Component, createEffect } from 'solid-js';
import { trackingServices } from '../tracking.services';
export const IdentifyUser: Component = () => {
const session = useSession();
createEffect(() => {
const user = session()?.data?.user;
if (user) {
trackingServices.identify({
userId: user.id,
email: user.email,
});
}
});
return null;
};

View File

@@ -0,0 +1,13 @@
import { useCurrentMatches } from '@solidjs/router';
import { type Component, createEffect, on } from 'solid-js';
import { trackingServices } from '../tracking.services';
export const PageViewTracker: Component = () => {
const matches = useCurrentMatches();
createEffect(on(matches, () => {
trackingServices.capture({ event: '$pageview' });
}));
return null;
};

View File

@@ -0,0 +1,62 @@
import posthog from 'posthog-js';
import { buildTimeConfig, isDev } from '../config/config';
type TrackingServices = {
capture: (args: {
event: string;
properties?: Record<string, unknown>;
}) => void;
reset: () => void;
identify: (args: {
userId: string;
email: string;
}) => void;
};
const dummyTrackingServices: TrackingServices = {
capture: ({ event, ...args }) => {
if (isDev) {
// eslint-disable-next-line no-console
console.log(`[dev] captured event ${event}`, args);
}
},
reset: () => {},
identify: () => {},
};
function createTrackingServices(): TrackingServices {
const { isEnabled, apiKey, host } = buildTimeConfig.posthog;
if (!isEnabled) {
return dummyTrackingServices;
}
if (!apiKey) {
console.warn('PostHog API key is not set');
return dummyTrackingServices;
}
posthog.init(
apiKey,
{
api_host: host,
capture_pageview: false,
},
);
return {
capture: ({ event, properties }) => {
posthog.capture(event, properties);
},
reset: () => {
posthog.reset();
},
identify: ({ userId, email }) => {
posthog.identify(userId, { email });
},
};
}
export const trackingServices = createTrackingServices();

41
pnpm-lock.yaml generated
View File

@@ -138,6 +138,9 @@ importers:
plausible-tracker:
specifier: ^0.3.9
version: 0.3.9
posthog-js:
specifier: ^1.231.0
version: 1.231.0
radix3:
specifier: ^1.1.2
version: 1.1.2
@@ -3479,6 +3482,9 @@ packages:
core-js-compat@3.39.0:
resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==}
core-js@3.41.0:
resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -4148,6 +4154,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
figue@2.2.0:
resolution: {integrity: sha512-KOXaFezLrSuymCOo3c62iWcCF9SbDBxQv9WAX4bRTFAoKHQrTiEUZTdzbg07/AdqS8DZFEgIlCtc2myiY4CVmQ==}
peerDependencies:
@@ -5391,6 +5400,20 @@ packages:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.231.0:
resolution: {integrity: sha512-8v3zRytQBg3KyKUPLy/9S5fw7ATeiKz3n3pLFxl1fQsV/a2mIt/MAwkIREZXTzi7mamsvtfXhSdggG7UYK/Ojw==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
preact@10.26.4:
resolution: {integrity: sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@@ -6419,6 +6442,9 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -10511,6 +10537,8 @@ snapshots:
dependencies:
browserslist: 4.24.3
core-js@3.41.0: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -11546,6 +11574,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.4.8: {}
figue@2.2.0(zod@3.24.2):
dependencies:
zod: 3.24.2
@@ -13282,6 +13312,15 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.231.0:
dependencies:
core-js: 3.41.0
fflate: 0.4.8
preact: 10.26.4
web-vitals: 4.2.4
preact@10.26.4: {}
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.0.3
@@ -14494,6 +14533,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
web-vitals@4.2.4: {}
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}