Refactor analytics services to support multiple providers

The analytics manager has been significantly refactored to allow the use of multiple providers simultaneously. The API has been adjusted, replacing the single active service with a new Map called activeServices, to hold the active analytics services. Additionally, several methods have been modified to return promises rather than void. The 'defaultProvider' property has been removed, as it no longer fits into the system architecture.
This commit is contained in:
gbuomprisco
2024-07-23 10:49:04 +02:00
parent 24c2ea2d27
commit b9e9b8af48
4 changed files with 58 additions and 49 deletions

View File

@@ -5,69 +5,69 @@ import type {
CreateAnalyticsManagerOptions,
} from './types';
/**
* Creates an analytics manager that can be used to track page views and events. The manager is initialized with a
* default provider and can be switched to a different provider at any time. The manager will use a NullAnalyticsService
* if the provider is not registered.
* @param options
*/
export function createAnalyticsManager<T extends string, Config extends object>(
options: CreateAnalyticsManagerOptions<T, Config>,
): AnalyticsManager {
let activeService: AnalyticsService = NullAnalyticsService;
const activeServices = new Map<T, AnalyticsService>();
const getActiveService = (): AnalyticsService => {
if (activeService === NullAnalyticsService) {
const getActiveServices = (): AnalyticsService[] => {
if (activeServices.size === 0) {
console.debug(
'Analytics service not initialized. Using NullAnalyticsService.',
);
}
return activeService;
};
const initialize = (provider: T, config: Config) => {
const factory = options.providers[provider];
if (!factory) {
console.warn(
`Analytics provider '${provider}' not registered. Using NullAnalyticsService.`,
'No active analytics services. Using NullAnalyticsService.',
);
activeService = NullAnalyticsService;
return;
return [NullAnalyticsService];
}
activeService = factory(config);
activeService.initialize();
return Array.from(activeServices.values());
};
// Initialize with the default provider
initialize(options.defaultProvider, {} as Config);
return {
addProvider: (
provider: T,
config: Config,
) => {
const factory = options.providers[provider];
if (!factory) {
console.warn(
`Analytics provider '${provider}' not registered. Skipping initialization.`,
);
return Promise.resolve();
}
const service = factory(config);
activeServices.set(provider, service);
return service.initialize();
},
removeProvider: (provider: T) => {
activeServices.delete(provider);
},
identify: (userId: string, traits?: Record<string, string>) => {
return getActiveService().identify(userId, traits);
return Promise.all(
getActiveServices().map((service) => service.identify(userId, traits)),
);
},
/**
* Track a page view with the given URL.
* @param url
*/
trackPageView: (url: string) => {
return getActiveService().trackPageView(url);
return Promise.all(
getActiveServices().map((service) => service.trackPageView(url)),
);
},
/**
* Track an event with the given name and properties.
* @param eventName
* @param eventProperties
*/
trackEvent: (
eventName: string,
eventProperties?: Record<string, string | string[]>,
) => {
return getActiveService().trackEvent(eventName, eventProperties);
return Promise.all(
getActiveServices().map((service) =>
service.trackEvent(eventName, eventProperties),
),
);
},
};
}

View File

@@ -3,7 +3,6 @@ import { NullAnalyticsService } from './null-analytics-service';
import type { AnalyticsManager } from './types';
export const analytics: AnalyticsManager = createAnalyticsManager({
defaultProvider: 'null',
providers: {
null: () => NullAnalyticsService,
},

View File

@@ -3,7 +3,8 @@ import { AnalyticsService } from './types';
const noop = (event: string) => {
// do nothing - this is to prevent errors when the analytics service is not initialized
return (...args: unknown[]) => {
// eslint-disable-next-line @typescript-eslint/require-await
return async (...args: unknown[]) => {
console.debug(
`Noop analytics service called with event: ${event}`,
...args.filter(Boolean),

View File

@@ -2,19 +2,25 @@ interface TrackEvent {
trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>,
): void;
): Promise<unknown>;
}
interface TrackPageView {
trackPageView(url: string): void;
trackPageView(url: string): Promise<unknown>;
}
interface Identify {
identify(userId: string, traits?: Record<string, string>): void;
identify(userId: string, traits?: Record<string, string>): Promise<unknown>;
}
interface ProviderManager {
addProvider(provider: string, config: object): Promise<unknown>;
removeProvider(provider: string): void;
}
export interface AnalyticsService extends TrackPageView, TrackEvent, Identify {
initialize(): void;
initialize(): Promise<unknown>;
}
export type AnalyticsProviderFactory<Config extends object> = (
@@ -25,8 +31,11 @@ export interface CreateAnalyticsManagerOptions<
T extends string,
Config extends object,
> {
defaultProvider: T;
providers: Record<T, AnalyticsProviderFactory<Config>>;
}
export interface AnalyticsManager extends TrackPageView, TrackEvent, Identify {}
export interface AnalyticsManager
extends TrackPageView,
TrackEvent,
Identify,
ProviderManager {}