From 2e13dfb9bc2f2e3473889a8a488ff1db0d97dc72 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 20 Feb 2025 17:14:27 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=88(frontend)=20abstract=20analytics?= =?UTF-8?q?=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add abstract classes for analytics services. We will be able to add easily any analytic services. Our first analytic service usecase is Posthog. --- .../src/core/config/ConfigProvider.tsx | 18 +++- .../impress/src/features/language/index.ts | 1 + .../apps/impress/src/libs/Analytics.tsx | 85 +++++++++++++++++++ src/frontend/apps/impress/src/libs/index.ts | 1 + .../{Posthog.tsx => PosthogAnalytic.tsx} | 31 ++++++- .../apps/impress/src/services/index.ts | 2 +- 6 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 src/frontend/apps/impress/src/libs/Analytics.tsx create mode 100644 src/frontend/apps/impress/src/libs/index.ts rename src/frontend/apps/impress/src/services/{Posthog.tsx => PosthogAnalytic.tsx} (60%) diff --git a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx index e9bf019b..2e1868f5 100644 --- a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx +++ b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx @@ -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 ( @@ -44,10 +54,10 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => { } return ( - + {children} - + ); }; diff --git a/src/frontend/apps/impress/src/features/language/index.ts b/src/frontend/apps/impress/src/features/language/index.ts index b5818aa7..4b60c8bd 100644 --- a/src/frontend/apps/impress/src/features/language/index.ts +++ b/src/frontend/apps/impress/src/features/language/index.ts @@ -1 +1,2 @@ +export * from './hooks/useLanguageSynchronizer'; export * from './LanguagePicker'; diff --git a/src/frontend/apps/impress/src/libs/Analytics.tsx b/src/frontend/apps/impress/src/libs/Analytics.tsx new file mode 100644 index 00000000..8e9464c3 --- /dev/null +++ b/src/frontend/apps/impress/src/libs/Analytics.tsx @@ -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), + }; +}; diff --git a/src/frontend/apps/impress/src/libs/index.ts b/src/frontend/apps/impress/src/libs/index.ts new file mode 100644 index 00000000..dca4ebe3 --- /dev/null +++ b/src/frontend/apps/impress/src/libs/index.ts @@ -0,0 +1 @@ +export * from './Analytics'; diff --git a/src/frontend/apps/impress/src/services/Posthog.tsx b/src/frontend/apps/impress/src/services/PosthogAnalytic.tsx similarity index 60% rename from src/frontend/apps/impress/src/services/Posthog.tsx rename to src/frontend/apps/impress/src/services/PosthogAnalytic.tsx index a8b83719..deaf0b71 100644 --- a/src/frontend/apps/impress/src/services/Posthog.tsx +++ b/src/frontend/apps/impress/src/services/PosthogAnalytic.tsx @@ -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 {children}; + } + + 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; diff --git a/src/frontend/apps/impress/src/services/index.ts b/src/frontend/apps/impress/src/services/index.ts index 967ebd48..92ed6c8e 100644 --- a/src/frontend/apps/impress/src/services/index.ts +++ b/src/frontend/apps/impress/src/services/index.ts @@ -1,2 +1,2 @@ export * from './Crisp'; -export * from './Posthog'; +export * from './PosthogAnalytic';