chore: overhaul infrastructure and integrate @mintel packages
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
- Restructure to pnpm monorepo (site moved to apps/web) - Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config - Implement Docker service architecture (Varnish, Directus, Gatekeeper) - Setup environment-aware Gitea Actions deployment
This commit is contained in:
107
apps/web/src/utils/analytics/index.ts
Normal file
107
apps/web/src/utils/analytics/index.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Analytics Service - Main entry point with DI
|
||||
* Clean constructor-based dependency injection
|
||||
*/
|
||||
|
||||
import type { AnalyticsAdapter, AnalyticsEvent, AnalyticsConfig } from './interfaces';
|
||||
import { PlausibleAdapter } from './plausible-adapter';
|
||||
import { UmamiAdapter, type UmamiConfig } from './umami-adapter';
|
||||
|
||||
export class AnalyticsService {
|
||||
private adapter: AnalyticsAdapter;
|
||||
|
||||
/**
|
||||
* Create analytics service with dependency injection
|
||||
* @param adapter - Analytics adapter implementation
|
||||
*/
|
||||
constructor(adapter: AnalyticsAdapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
getAdapter(): AnalyticsAdapter {
|
||||
return this.adapter;
|
||||
}
|
||||
|
||||
async track(event: AnalyticsEvent): Promise<void> {
|
||||
return this.adapter.track(event);
|
||||
}
|
||||
|
||||
async page(path: string, props?: Record<string, any>): Promise<void> {
|
||||
if (this.adapter.page) {
|
||||
return this.adapter.page(path, props);
|
||||
}
|
||||
return this.track({ name: 'Pageview', props: { path, ...props } });
|
||||
}
|
||||
|
||||
async identify(userId: string, traits?: Record<string, any>): Promise<void> {
|
||||
if (this.adapter.identify) {
|
||||
return this.adapter.identify(userId, traits);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
async trackEvent(name: string, props?: Record<string, any>): Promise<void> {
|
||||
return this.track({ name, props });
|
||||
}
|
||||
|
||||
async trackOutboundLink(url: string, text: string): Promise<void> {
|
||||
return this.track({
|
||||
name: 'Outbound Link',
|
||||
props: { url, text }
|
||||
});
|
||||
}
|
||||
|
||||
async trackSearch(query: string, path: string): Promise<void> {
|
||||
return this.track({
|
||||
name: 'Search',
|
||||
props: { query, path }
|
||||
});
|
||||
}
|
||||
|
||||
async trackPageLoad(loadTime: number, path: string, userAgent: string): Promise<void> {
|
||||
return this.track({
|
||||
name: 'Page Load',
|
||||
props: { loadTime: Math.round(loadTime), path, userAgent }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
export function createPlausibleAnalytics(config: AnalyticsConfig): AnalyticsService {
|
||||
return new AnalyticsService(new PlausibleAdapter(config));
|
||||
}
|
||||
|
||||
export function createUmamiAnalytics(config: UmamiConfig): AnalyticsService {
|
||||
return new AnalyticsService(new UmamiAdapter(config));
|
||||
}
|
||||
|
||||
// Default singleton
|
||||
let defaultAnalytics: AnalyticsService | null = null;
|
||||
|
||||
export function getDefaultAnalytics(): AnalyticsService {
|
||||
if (!defaultAnalytics) {
|
||||
const provider = process.env.NEXT_PUBLIC_ANALYTICS_PROVIDER || 'plausible';
|
||||
|
||||
if (provider === 'umami') {
|
||||
defaultAnalytics = createUmamiAnalytics({
|
||||
websiteId: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || '',
|
||||
hostUrl: process.env.NEXT_PUBLIC_UMAMI_HOST_URL,
|
||||
});
|
||||
} else {
|
||||
defaultAnalytics = createPlausibleAnalytics({
|
||||
domain: process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || 'mintel.me',
|
||||
scriptUrl: process.env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL || 'https://plausible.yourdomain.com/js/script.js'
|
||||
});
|
||||
}
|
||||
}
|
||||
return defaultAnalytics;
|
||||
}
|
||||
|
||||
// Convenience function
|
||||
export async function track(name: string, props?: Record<string, any>): Promise<void> {
|
||||
return getDefaultAnalytics().trackEvent(name, props);
|
||||
}
|
||||
|
||||
// Re-export for advanced usage
|
||||
export type { AnalyticsAdapter, AnalyticsEvent, AnalyticsConfig };
|
||||
export { PlausibleAdapter };
|
||||
20
apps/web/src/utils/analytics/interfaces.ts
Normal file
20
apps/web/src/utils/analytics/interfaces.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Analytics interfaces - decoupled contracts
|
||||
*/
|
||||
|
||||
export interface AnalyticsEvent {
|
||||
name: string;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AnalyticsAdapter {
|
||||
track(event: AnalyticsEvent): Promise<void>;
|
||||
identify?(userId: string, traits?: Record<string, any>): Promise<void>;
|
||||
page?(path: string, props?: Record<string, any>): Promise<void>;
|
||||
getScriptTag?(): string;
|
||||
}
|
||||
|
||||
export interface AnalyticsConfig {
|
||||
domain?: string;
|
||||
scriptUrl?: string;
|
||||
}
|
||||
38
apps/web/src/utils/analytics/plausible-adapter.ts
Normal file
38
apps/web/src/utils/analytics/plausible-adapter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Plausible Analytics Adapter
|
||||
* Decoupled implementation
|
||||
*/
|
||||
|
||||
import type { AnalyticsAdapter, AnalyticsEvent, AnalyticsConfig } from './interfaces';
|
||||
|
||||
export class PlausibleAdapter implements AnalyticsAdapter {
|
||||
private domain: string;
|
||||
private scriptUrl: string;
|
||||
|
||||
constructor(config: AnalyticsConfig) {
|
||||
this.domain = config.domain || 'mintel.me';
|
||||
this.scriptUrl = config.scriptUrl || 'https://plausible.yourdomain.com/js/script.js';
|
||||
}
|
||||
|
||||
async track(event: AnalyticsEvent): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const w = window as any;
|
||||
if (w.plausible) {
|
||||
w.plausible(event.name, {
|
||||
props: event.props
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async page(path: string, props?: Record<string, any>): Promise<void> {
|
||||
await this.track({
|
||||
name: 'Pageview',
|
||||
props: { path, ...props }
|
||||
});
|
||||
}
|
||||
|
||||
getScriptTag(): string {
|
||||
return `<script defer data-domain="${this.domain}" src="${this.scriptUrl}"></script>`;
|
||||
}
|
||||
}
|
||||
54
apps/web/src/utils/analytics/umami-adapter.ts
Normal file
54
apps/web/src/utils/analytics/umami-adapter.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Umami Analytics Adapter
|
||||
* Decoupled implementation
|
||||
*/
|
||||
|
||||
import type { AnalyticsAdapter, AnalyticsEvent, AnalyticsConfig } from './interfaces';
|
||||
|
||||
export interface UmamiConfig extends AnalyticsConfig {
|
||||
websiteId: string;
|
||||
hostUrl?: string;
|
||||
}
|
||||
|
||||
export class UmamiAdapter implements AnalyticsAdapter {
|
||||
private websiteId: string;
|
||||
private hostUrl: string;
|
||||
|
||||
constructor(config: UmamiConfig) {
|
||||
this.websiteId = config.websiteId;
|
||||
this.hostUrl = config.hostUrl || 'https://cloud.umami.is';
|
||||
}
|
||||
|
||||
async track(event: AnalyticsEvent): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const w = window as any;
|
||||
if (w.umami) {
|
||||
w.umami.track(event.name, event.props);
|
||||
}
|
||||
}
|
||||
|
||||
async page(path: string, props?: Record<string, any>): Promise<void> {
|
||||
// Umami tracks pageviews automatically by default,
|
||||
// but we can manually trigger it if needed.
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const w = window as any;
|
||||
if (w.umami) {
|
||||
w.umami.track(props?.name || 'pageview', { url: path, ...props });
|
||||
}
|
||||
}
|
||||
|
||||
async identify(userId: string, traits?: Record<string, any>): Promise<void> {
|
||||
// Umami doesn't have a direct 'identify' like Segment,
|
||||
// but we can track it as an event or session property if supported by the instance.
|
||||
await this.track({
|
||||
name: 'identify',
|
||||
props: { userId, ...traits }
|
||||
});
|
||||
}
|
||||
|
||||
getScriptTag(): string {
|
||||
return `<script async src="${this.hostUrl}/script.js" data-website-id="${this.websiteId}"></script>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user