📈(frontend) abstract analytics classes

Add abstract classes for analytics services.
We will be able to add easily any analytic
services.
Our first analytic service usecase is Posthog.
This commit is contained in:
Anthony LC
2025-02-20 17:14:27 +01:00
committed by Anthony LC
parent a026435eb7
commit 2e13dfb9bc
6 changed files with 132 additions and 6 deletions

View File

@@ -3,8 +3,9 @@ import { PropsWithChildren, useEffect } from 'react';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useLanguageSynchronizer } from '@/features/language/hooks/useLanguageSynchronizer';
import { CrispProvider, PostHogProvider } from '@/services';
import { useLanguageSynchronizer } from '@/features/language/';
import { useAnalytics } from '@/libs';
import { CrispProvider, PostHogAnalytic } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';
import { useConfig } from './api/useConfig';
@@ -13,6 +14,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
const { data: conf } = useConfig();
const { setSentry } = useSentryStore();
const { setTheme } = useCunninghamTheme();
const { AnalyticsProvider } = useAnalytics();
const { synchronizeLanguage } = useLanguageSynchronizer();
useEffect(() => {
@@ -35,6 +37,14 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
void synchronizeLanguage();
}, [synchronizeLanguage]);
useEffect(() => {
if (!conf?.POSTHOG_KEY) {
return;
}
new PostHogAnalytic(conf.POSTHOG_KEY);
}, [conf?.POSTHOG_KEY]);
if (!conf) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
@@ -44,10 +54,10 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
}
return (
<PostHogProvider conf={conf.POSTHOG_KEY}>
<AnalyticsProvider>
<CrispProvider websiteId={conf?.CRISP_WEBSITE_ID}>
{children}
</CrispProvider>
</PostHogProvider>
</AnalyticsProvider>
);
};

View File

@@ -1 +1,2 @@
export * from './hooks/useLanguageSynchronizer';
export * from './LanguagePicker';

View File

@@ -0,0 +1,85 @@
import { JSX, PropsWithChildren, ReactNode } from 'react';
type AnalyticEventClick = {
eventName: 'click';
};
type AnalyticEventUser = {
eventName: 'user';
id: string;
email: string;
};
export type AnalyticEvent = AnalyticEventClick | AnalyticEventUser;
export abstract class AbstractAnalytic {
public constructor() {
Analytics.registerAnalytic(this);
}
public abstract Provider(children?: ReactNode): JSX.Element;
public abstract trackEvent(evt: AnalyticEvent): void;
public abstract isFeatureFlagActivated(flagName: string): boolean;
}
export class Analytics {
private static instance: Analytics;
private static analytics: AbstractAnalytic[] = [];
private constructor() {}
public static getInstance(): Analytics {
if (!Analytics.instance) {
Analytics.instance = new Analytics();
}
return Analytics.instance;
}
public static clearAnalytics(): void {
Analytics.analytics = [];
}
public static registerAnalytic(analytic: AbstractAnalytic): void {
Analytics.analytics.push(analytic);
}
public static trackEvent(evt: AnalyticEvent): void {
Analytics.analytics.forEach((analytic) => analytic.trackEvent(evt));
}
public static providers(children: ReactNode) {
return Analytics.analytics.reduceRight(
(acc, analytic) => analytic.Provider(acc),
children,
);
}
/**
* Check if a feature flag is activated
*
* Feature flags are activated if at least one analytic is activated
* because we don't want to hide feature if the user does not
* use analytics (AB testing, etc)
*/
public static isFeatureFlagActivated(flagName: string): boolean {
if (!Analytics.analytics.length) {
return true;
}
return Analytics.analytics.some((analytic) =>
analytic.isFeatureFlagActivated(flagName),
);
}
}
export const useAnalytics = () => {
return {
AnalyticsProvider: ({ children }: PropsWithChildren) =>
Analytics.providers(children),
isFeatureFlagActivated: (flagName: string) =>
Analytics.isFeatureFlagActivated(flagName),
trackEvent: (evt: AnalyticEvent) => Analytics.trackEvent(evt),
};
};

View File

@@ -0,0 +1 @@
export * from './Analytics';

View File

@@ -1,7 +1,36 @@
import { Router } from 'next/router';
import posthog from 'posthog-js';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { PropsWithChildren, useEffect } from 'react';
import { JSX, PropsWithChildren, ReactNode, useEffect } from 'react';
import { AbstractAnalytic } from '@/libs/';
export class PostHogAnalytic extends AbstractAnalytic {
private conf?: PostHogConf = undefined;
public constructor(conf?: PostHogConf) {
super();
this.conf = conf;
}
public Provider(children?: ReactNode): JSX.Element {
return <PostHogProvider conf={this.conf}>{children}</PostHogProvider>;
}
public trackEvent(): void {}
public isFeatureFlagActivated(flagName: string): boolean {
if (
posthog.featureFlags.getFlags().includes(flagName) &&
posthog.isFeatureEnabled(flagName) === false
) {
return false;
}
return true;
}
}
export interface PostHogConf {
id: string;

View File

@@ -1,2 +1,2 @@
export * from './Crisp';
export * from './Posthog';
export * from './PosthogAnalytic';