Add events handling and enhance analytics tracking (#47)
* Add events handling and enhance analytics tracking Added a new events system to track user actions throughout the application. Specific significant events such as user signup, sign-in, and checkout have dedicated handlers. Updated the analytics system to handle these event triggers and improved analytics reporting. An analytics provider has been implemented to manage event subscriptions and analytics event mappings. * Remove unused dependencies from package.json files Unused packages "@tanstack/react-table" and "next" have been removed from the packages/shared and tooling directories respectively. These changes help ensure that only needed packages are included in the project, reducing potential security risks and unnecessary processing overhead. * Update dependencies Multiple package versions were updated including "@tanstack/react-query" and "lucide-react"
This commit is contained in:
committed by
GitHub
parent
868f907c81
commit
5eefa7ff16
@@ -1,5 +1,5 @@
|
||||
import { NullAnalyticsService } from './null-analytics-service';
|
||||
import {
|
||||
import type {
|
||||
AnalyticsManager,
|
||||
AnalyticsService,
|
||||
CreateAnalyticsManagerOptions,
|
||||
@@ -18,7 +18,7 @@ export function createAnalyticsManager<T extends string, Config extends object>(
|
||||
|
||||
const getActiveService = (): AnalyticsService => {
|
||||
if (activeService === NullAnalyticsService) {
|
||||
console.warn(
|
||||
console.debug(
|
||||
'Analytics service not initialized. Using NullAnalyticsService.',
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function createAnalyticsManager<T extends string, Config extends object>(
|
||||
const factory = options.providers[provider];
|
||||
|
||||
if (!factory) {
|
||||
console.error(
|
||||
console.warn(
|
||||
`Analytics provider '${provider}' not registered. Using NullAnalyticsService.`,
|
||||
);
|
||||
|
||||
@@ -57,6 +57,7 @@ export function createAnalyticsManager<T extends string, Config extends object>(
|
||||
trackPageView: (url: string) => {
|
||||
return getActiveService().trackPageView(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Track an event with the given name and properties.
|
||||
* @param eventName
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { AnalyticsService } from './types';
|
||||
|
||||
const noop = () => {
|
||||
const noop = (event: string) => {
|
||||
// do nothing - this is to prevent errors when the analytics service is not initialized
|
||||
|
||||
return (...args: unknown[]) => {
|
||||
console.debug(
|
||||
`Noop analytics service called with event: ${event}`,
|
||||
...args.filter(Boolean),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -9,8 +16,8 @@ const noop = () => {
|
||||
* the user is calling analytics methods before the analytics service is initialized.
|
||||
*/
|
||||
export const NullAnalyticsService: AnalyticsService = {
|
||||
initialize: noop,
|
||||
trackPageView: noop,
|
||||
trackEvent: noop,
|
||||
identify: noop,
|
||||
initialize: noop('initialize'),
|
||||
trackPageView: noop('trackPageView'),
|
||||
trackEvent: noop('trackEvent'),
|
||||
identify: noop('identify'),
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@types/react": "^18.3.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
"react-hook-form": "^7.52.1",
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
"@kit/ui": "workspace:^",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"next": "14.2.5",
|
||||
"next-themes": "0.3.0",
|
||||
"react": "18.3.1",
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"@makerkit/data-loader-supabase-core": "^0.0.8",
|
||||
"@makerkit/data-loader-supabase-nextjs": "^1.2.3",
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"@types/react": "^18.3.3",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
"@marsidev/react-turnstile": "^0.7.2",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@types/react": "^18.3.3",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"next": "14.2.5",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^15.0.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
import { useSignInWithOtp } from '@kit/supabase/hooks/use-sign-in-with-otp';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -44,6 +45,7 @@ export function MagicLinkAuthContainer({
|
||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
||||
const { t } = useTranslation();
|
||||
const signInWithOtpMutation = useSignInWithOtp();
|
||||
const appEvents = useAppEvents();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
@@ -65,8 +67,8 @@ export function MagicLinkAuthContainer({
|
||||
|
||||
const emailRedirectTo = url.href;
|
||||
|
||||
const promise = () =>
|
||||
signInWithOtpMutation.mutateAsync({
|
||||
const promise = async () => {
|
||||
await signInWithOtpMutation.mutateAsync({
|
||||
email,
|
||||
options: {
|
||||
emailRedirectTo,
|
||||
@@ -75,6 +77,14 @@ export function MagicLinkAuthContainer({
|
||||
},
|
||||
});
|
||||
|
||||
appEvents.emit({
|
||||
type: 'user.signedUp',
|
||||
payload: {
|
||||
method: 'magiclink',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t('auth:sendingEmailLink'),
|
||||
success: t(`auth:sendLinkSuccessToast`),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { CheckCircledIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
import { useSignUpWithEmailAndPassword } from '@kit/supabase/hooks/use-sign-up-with-email-password';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -33,8 +34,10 @@ export function EmailPasswordSignUpContainer({
|
||||
|
||||
const signUpMutation = useSignUpWithEmailAndPassword();
|
||||
const redirecting = useRef(false);
|
||||
const loading = signUpMutation.isPending || redirecting.current;
|
||||
const [showVerifyEmailAlert, setShowVerifyEmailAlert] = useState(false);
|
||||
const appEvents = useAppEvents();
|
||||
|
||||
const loading = signUpMutation.isPending || redirecting.current;
|
||||
|
||||
const onSignupRequested = useCallback(
|
||||
async (credentials: { email: string; password: string }) => {
|
||||
@@ -49,6 +52,13 @@ export function EmailPasswordSignUpContainer({
|
||||
captchaToken,
|
||||
});
|
||||
|
||||
appEvents.emit({
|
||||
type: 'user.signedUp',
|
||||
payload: {
|
||||
method: 'password',
|
||||
},
|
||||
});
|
||||
|
||||
setShowVerifyEmailAlert(true);
|
||||
|
||||
if (onSignUp) {
|
||||
@@ -61,6 +71,7 @@ export function EmailPasswordSignUpContainer({
|
||||
}
|
||||
},
|
||||
[
|
||||
appEvents,
|
||||
captchaToken,
|
||||
emailRedirectTo,
|
||||
loading,
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@types/react": "^18.3.3",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^15.0.0"
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:^",
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@kit/shared": "workspace:^",
|
||||
"@kit/tailwind-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"react-i18next": "^15.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -12,14 +12,15 @@
|
||||
"exports": {
|
||||
"./logger": "./src/logger/index.ts",
|
||||
"./utils": "./src/utils.ts",
|
||||
"./hooks": "./src/hooks/index.ts"
|
||||
"./hooks": "./src/hooks/index.ts",
|
||||
"./events": "./src/events/index.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tailwind-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@tanstack/react-table": "^8.19.3"
|
||||
"@types/react": "^18.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "^9.3.1"
|
||||
|
||||
118
packages/shared/src/events/index.tsx
Normal file
118
packages/shared/src/events/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useRef } from 'react';
|
||||
|
||||
type EmptyPayload = NonNullable<unknown>;
|
||||
|
||||
// Base event types
|
||||
export interface BaseAppEventTypes {
|
||||
'user.signedIn': { userId: string };
|
||||
'user.signedUp': { method: `magiclink` | `password` };
|
||||
'user.updated': EmptyPayload;
|
||||
'checkout.started': { planId: string; account?: string };
|
||||
|
||||
// Add more base event types here
|
||||
}
|
||||
|
||||
export type ConsumerProvidedEventTypes = EmptyPayload;
|
||||
|
||||
// Helper type for extending event types
|
||||
export type ExtendedAppEventTypes<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
> = BaseAppEventTypes & T;
|
||||
|
||||
// Generic type for the entire module
|
||||
export type AppEventType<T extends ConsumerProvidedEventTypes> =
|
||||
keyof ExtendedAppEventTypes<T>;
|
||||
|
||||
export type AppEvent<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
> = {
|
||||
type: K;
|
||||
payload: ExtendedAppEventTypes<T>[K];
|
||||
};
|
||||
|
||||
export type EventCallback<
|
||||
T extends ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
> = (event: AppEvent<T, K>) => void;
|
||||
|
||||
interface InternalAppEventsContextType<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
> {
|
||||
emit: (event: AppEvent<never, never>) => void;
|
||||
on: (eventType: K, callback: EventCallback<T, K>) => void;
|
||||
off: (eventType: K, callback: EventCallback<T, K>) => void;
|
||||
}
|
||||
|
||||
interface AppEventsContextType<T extends ConsumerProvidedEventTypes> {
|
||||
emit: <K extends AppEventType<T>>(event: AppEvent<T, K>) => void;
|
||||
|
||||
on: <K extends AppEventType<T>>(
|
||||
eventType: K,
|
||||
callback: EventCallback<T, K>,
|
||||
) => void;
|
||||
|
||||
off: <K extends AppEventType<T>>(
|
||||
eventType: K,
|
||||
callback: EventCallback<T, K>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const AppEventsContext = createContext<InternalAppEventsContextType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function AppEventsProvider<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
>({ children }: React.PropsWithChildren) {
|
||||
const listeners = useRef<Record<K, EventCallback<T, K>[]>>(
|
||||
{} as Record<K, EventCallback<T, K>[]>,
|
||||
);
|
||||
|
||||
const emit = useCallback(
|
||||
(event: AppEvent<T, K>) => {
|
||||
const eventListeners = listeners.current[event.type] ?? [];
|
||||
|
||||
eventListeners.forEach((callback) => callback(event));
|
||||
},
|
||||
[listeners],
|
||||
);
|
||||
|
||||
const on = useCallback((eventType: K, callback: EventCallback<T, K>) => {
|
||||
listeners.current = {
|
||||
...listeners.current,
|
||||
[eventType]: [...(listeners.current[eventType] ?? []), callback],
|
||||
};
|
||||
}, []) as AppEventsContextType<T>['on'];
|
||||
|
||||
const off = useCallback((eventType: K, callback: EventCallback<T, K>) => {
|
||||
listeners.current = {
|
||||
...listeners.current,
|
||||
[eventType]: (listeners.current[eventType] ?? []).filter(
|
||||
(cb) => cb !== callback,
|
||||
),
|
||||
};
|
||||
}, []) as AppEventsContextType<T>['off'];
|
||||
|
||||
return (
|
||||
<AppEventsContext.Provider value={{ emit, on, off }}>
|
||||
{children}
|
||||
</AppEventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAppEvents<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
>(): AppEventsContextType<T> {
|
||||
const context = useContext(AppEventsContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAppEvents must be used within an AppEventsProvider');
|
||||
}
|
||||
|
||||
return context as AppEventsContextType<T>;
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@supabase/gotrue-js": "2.64.4",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.44.4",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@types/react": "^18.3.3",
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useEffect } from 'react';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { AuthChangeEvent, Session } from '@supabase/supabase-js';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
/**
|
||||
@@ -14,15 +16,18 @@ const PRIVATE_PATH_PREFIXES = ['/home', '/admin', '/join', '/update-password'];
|
||||
|
||||
/**
|
||||
* @name useAuthChangeListener
|
||||
* @param privatePathPrefixes
|
||||
* @param appHomePath
|
||||
* @param privatePathPrefixes - A list of private path prefixes
|
||||
* @param appHomePath - The path to redirect to when the user is signed out
|
||||
* @param onEvent - Callback function to be called when an auth event occurs
|
||||
*/
|
||||
export function useAuthChangeListener({
|
||||
privatePathPrefixes = PRIVATE_PATH_PREFIXES,
|
||||
appHomePath,
|
||||
onEvent,
|
||||
}: {
|
||||
appHomePath: string;
|
||||
privatePathPrefixes?: string[];
|
||||
onEvent?: (event: AuthChangeEvent, user: Session | null) => void;
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const pathName = usePathname();
|
||||
@@ -30,6 +35,10 @@ export function useAuthChangeListener({
|
||||
useEffect(() => {
|
||||
// keep this running for the whole session unless the component was unmounted
|
||||
const listener = client.auth.onAuthStateChange((event, user) => {
|
||||
if (onEvent) {
|
||||
onEvent(event, user);
|
||||
}
|
||||
|
||||
// log user out if user is falsy
|
||||
// and if the current path is a private route
|
||||
const shouldRedirectUser =
|
||||
@@ -50,7 +59,7 @@ export function useAuthChangeListener({
|
||||
|
||||
// destroy listener on un-mounts
|
||||
return () => listener.data.subscription.unsubscribe();
|
||||
}, [client.auth, pathName, appHomePath, privatePathPrefixes]);
|
||||
}, [client.auth, pathName, appHomePath, privatePathPrefixes, onEvent]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"input-otp": "1.2.4",
|
||||
"lucide-react": "^0.411.0",
|
||||
"lucide-react": "^0.412.0",
|
||||
"react-top-loading-bar": "2.3.1",
|
||||
"tailwind-merge": "^2.4.0"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
"@kit/tailwind-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@tanstack/react-query": "5.51.9",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
||||
Reference in New Issue
Block a user