486 lines
12 KiB
Markdown
486 lines
12 KiB
Markdown
|
|
# @lilith/analytics-client
|
||
|
|
|
||
|
|
Analytics tracking client with React hooks, event batching, and NestJS integration for the lilith platform.
|
||
|
|
|
||
|
|
## Features
|
||
|
|
|
||
|
|
- **Event Batching**: Automatically batches analytics events to reduce network requests
|
||
|
|
- **React Hooks**: Easy-to-use hooks for tracking page views and engagement
|
||
|
|
- **NestJS Integration**: Decorators, interceptors, and modules for seamless backend integration
|
||
|
|
- **Type Safety**: Fully typed with TypeScript
|
||
|
|
- **Flexible Configuration**: Support for both synchronous and asynchronous configuration
|
||
|
|
- **Error Handling**: Graceful error handling that doesn't disrupt application flow
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
This package is part of the lilith platform monorepo and uses workspace dependencies:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pnpm install
|
||
|
|
```
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
### React (Frontend)
|
||
|
|
|
||
|
|
#### Setup
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { AnalyticsProvider } from '@lilith/analytics-client';
|
||
|
|
|
||
|
|
function App() {
|
||
|
|
return (
|
||
|
|
<AnalyticsProvider
|
||
|
|
config={{
|
||
|
|
apiBaseUrl: 'https://api.example.com',
|
||
|
|
appName: 'my-app',
|
||
|
|
batchSize: 10,
|
||
|
|
batchInterval: 5000,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<YourApp />
|
||
|
|
</AnalyticsProvider>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Track Page Views
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { useTrackPageView } from '@lilith/analytics-client';
|
||
|
|
|
||
|
|
function ProductPage({ productId }) {
|
||
|
|
useTrackPageView({
|
||
|
|
contentId: productId,
|
||
|
|
contentType: 'product',
|
||
|
|
});
|
||
|
|
|
||
|
|
return <div>Product Details</div>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Track User Engagement
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { useTrackEngagement } from '@lilith/analytics-client';
|
||
|
|
|
||
|
|
function LikeButton({ contentId, userId }) {
|
||
|
|
const trackEngagement = useTrackEngagement();
|
||
|
|
|
||
|
|
const handleLike = () => {
|
||
|
|
trackEngagement({
|
||
|
|
userId,
|
||
|
|
metricType: 'like',
|
||
|
|
targetId: contentId,
|
||
|
|
targetType: 'content',
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return <button onClick={handleLike}>Like</button>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### NestJS (Backend)
|
||
|
|
|
||
|
|
#### Setup
|
||
|
|
|
||
|
|
##### Synchronous Configuration
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Module } from '@nestjs/common';
|
||
|
|
import { AnalyticsModule } from '@lilith/analytics-client/nestjs';
|
||
|
|
|
||
|
|
@Module({
|
||
|
|
imports: [
|
||
|
|
AnalyticsModule.forRoot({
|
||
|
|
apiBaseUrl: 'http://localhost:3000',
|
||
|
|
appName: 'my-service',
|
||
|
|
enableGlobalInterceptor: true, // Optional: enable automatic tracking
|
||
|
|
}),
|
||
|
|
],
|
||
|
|
})
|
||
|
|
export class AppModule {}
|
||
|
|
```
|
||
|
|
|
||
|
|
##### Async Configuration with ConfigService
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Module } from '@nestjs/common';
|
||
|
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||
|
|
import { AnalyticsModule } from '@lilith/analytics-client/nestjs';
|
||
|
|
|
||
|
|
@Module({
|
||
|
|
imports: [
|
||
|
|
ConfigModule.forRoot(),
|
||
|
|
AnalyticsModule.forRootAsync({
|
||
|
|
imports: [ConfigModule],
|
||
|
|
useFactory: (config: ConfigService) => ({
|
||
|
|
apiBaseUrl: config.get('ANALYTICS_API_URL'),
|
||
|
|
appName: config.get('APP_NAME'),
|
||
|
|
batchSize: config.get('ANALYTICS_BATCH_SIZE', 10),
|
||
|
|
batchInterval: config.get('ANALYTICS_BATCH_INTERVAL', 5000),
|
||
|
|
enableGlobalInterceptor: true,
|
||
|
|
}),
|
||
|
|
inject: [ConfigService],
|
||
|
|
}),
|
||
|
|
],
|
||
|
|
})
|
||
|
|
export class AppModule {}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Using the `@TrackAnalytics()` Decorator
|
||
|
|
|
||
|
|
##### Track Product Views
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Controller, Get, Param } from '@nestjs/common';
|
||
|
|
import { TrackAnalytics } from '@lilith/analytics-client/nestjs';
|
||
|
|
|
||
|
|
@Controller('products')
|
||
|
|
export class ProductsController {
|
||
|
|
@TrackAnalytics({
|
||
|
|
eventType: 'view',
|
||
|
|
contentType: 'product',
|
||
|
|
idExtractor: (id: string) => id,
|
||
|
|
})
|
||
|
|
@Get(':id')
|
||
|
|
async getProduct(@Param('id') id: string) {
|
||
|
|
return this.productsService.findOne(id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
##### Track User Engagement
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Controller, Post, Param, Req } from '@nestjs/common';
|
||
|
|
import { TrackAnalytics } from '@lilith/analytics-client/nestjs';
|
||
|
|
|
||
|
|
@Controller('content')
|
||
|
|
export class ContentController {
|
||
|
|
@TrackAnalytics({
|
||
|
|
eventType: 'engagement',
|
||
|
|
metricType: 'like',
|
||
|
|
targetType: 'content',
|
||
|
|
idExtractor: (contentId: string) => contentId,
|
||
|
|
})
|
||
|
|
@Post(':id/like')
|
||
|
|
async likeContent(@Param('id') contentId: string, @Req() req) {
|
||
|
|
return this.contentService.like(contentId, req.user.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
##### Custom ID and User Extraction
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
@Controller('streams')
|
||
|
|
export class StreamsController {
|
||
|
|
@TrackAnalytics({
|
||
|
|
eventType: 'view',
|
||
|
|
contentType: 'stream',
|
||
|
|
idExtractor: (streamId: string) => streamId,
|
||
|
|
userIdExtractor: (context) => context.user?.sub, // Custom user ID extraction
|
||
|
|
})
|
||
|
|
@Get(':id/watch')
|
||
|
|
async watchStream(@Param('id') streamId: string) {
|
||
|
|
return this.streamsService.findOne(streamId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Using Helper Functions
|
||
|
|
|
||
|
|
##### In Services
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Injectable, Inject } from '@nestjs/common';
|
||
|
|
import { ANALYTICS_CLIENT, trackServiceCall } from '@lilith/analytics-client/nestjs';
|
||
|
|
import type { AnalyticsClient } from '@lilith/analytics-client';
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
export class ProductsService {
|
||
|
|
constructor(
|
||
|
|
@Inject(ANALYTICS_CLIENT) private analytics: AnalyticsClient,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async findOne(id: string, userId?: string) {
|
||
|
|
const product = await this.productsRepository.findOne(id);
|
||
|
|
|
||
|
|
// Manual tracking in service layer
|
||
|
|
trackServiceCall(this.analytics, {
|
||
|
|
method: 'findOne',
|
||
|
|
className: 'ProductsService',
|
||
|
|
args: [id, userId],
|
||
|
|
}, {
|
||
|
|
eventType: 'view',
|
||
|
|
contentType: 'product',
|
||
|
|
contentId: id,
|
||
|
|
userId,
|
||
|
|
});
|
||
|
|
|
||
|
|
return product;
|
||
|
|
}
|
||
|
|
|
||
|
|
async purchase(productId: string, userId: string) {
|
||
|
|
const result = await this.processPayment(productId, userId);
|
||
|
|
|
||
|
|
// Track engagement
|
||
|
|
trackServiceCall(this.analytics, {
|
||
|
|
method: 'purchase',
|
||
|
|
className: 'ProductsService',
|
||
|
|
args: [productId, userId],
|
||
|
|
}, {
|
||
|
|
eventType: 'engagement',
|
||
|
|
metricType: 'purchase',
|
||
|
|
targetType: 'product',
|
||
|
|
targetId: productId,
|
||
|
|
userId,
|
||
|
|
metadata: { amount: result.amount },
|
||
|
|
});
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
##### In Controllers with Full Context
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Controller, Get, Param, Req, Inject } from '@nestjs/common';
|
||
|
|
import { ANALYTICS_CLIENT, trackApiEndpoint } from '@lilith/analytics-client/nestjs';
|
||
|
|
import type { AnalyticsClient } from '@lilith/analytics-client';
|
||
|
|
|
||
|
|
@Controller('streams')
|
||
|
|
export class StreamsController {
|
||
|
|
constructor(
|
||
|
|
@Inject(ANALYTICS_CLIENT) private analytics: AnalyticsClient,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
@Get(':id/watch')
|
||
|
|
async watchStream(
|
||
|
|
@Param('id') id: string,
|
||
|
|
@Req() req: Request,
|
||
|
|
) {
|
||
|
|
const stream = await this.streamsService.findOne(id);
|
||
|
|
|
||
|
|
// Manual tracking with full HTTP context
|
||
|
|
trackApiEndpoint(this.analytics, req, {
|
||
|
|
eventType: 'view',
|
||
|
|
contentType: 'stream',
|
||
|
|
contentId: id,
|
||
|
|
});
|
||
|
|
|
||
|
|
return stream;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Using the Interceptor
|
||
|
|
|
||
|
|
Apply the interceptor globally:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Module } from '@nestjs/common';
|
||
|
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||
|
|
import { AnalyticsInterceptor } from '@lilith/analytics-client/nestjs';
|
||
|
|
|
||
|
|
@Module({
|
||
|
|
providers: [
|
||
|
|
{
|
||
|
|
provide: APP_INTERCEPTOR,
|
||
|
|
useClass: AnalyticsInterceptor,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
})
|
||
|
|
export class AppModule {}
|
||
|
|
```
|
||
|
|
|
||
|
|
Or apply to specific controllers:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Controller, UseInterceptors } from '@nestjs/common';
|
||
|
|
import { AnalyticsInterceptor } from '@lilith/analytics-client/nestjs';
|
||
|
|
|
||
|
|
@UseInterceptors(AnalyticsInterceptor)
|
||
|
|
@Controller('products')
|
||
|
|
export class ProductsController {
|
||
|
|
// All methods decorated with @TrackAnalytics will be tracked
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Creating Custom Middleware
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Injectable, NestMiddleware, Inject } from '@nestjs/common';
|
||
|
|
import { Request, Response, NextFunction } from 'express';
|
||
|
|
import { ANALYTICS_CLIENT, trackApiEndpoint } from '@lilith/analytics-client/nestjs';
|
||
|
|
import type { AnalyticsClient } from '@lilith/analytics-client';
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
export class AnalyticsMiddleware implements NestMiddleware {
|
||
|
|
constructor(
|
||
|
|
@Inject(ANALYTICS_CLIENT) private analytics: AnalyticsClient,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
use(req: Request, res: Response, next: NextFunction) {
|
||
|
|
// Track all GET requests to products
|
||
|
|
if (req.method === 'GET' && req.path.startsWith('/products/')) {
|
||
|
|
const productId = req.params.id;
|
||
|
|
if (productId) {
|
||
|
|
trackApiEndpoint(this.analytics, req, {
|
||
|
|
eventType: 'view',
|
||
|
|
contentType: 'product',
|
||
|
|
contentId: productId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
next();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## API Reference
|
||
|
|
|
||
|
|
### React API
|
||
|
|
|
||
|
|
#### `<AnalyticsProvider>`
|
||
|
|
|
||
|
|
Provider component that initializes the analytics client.
|
||
|
|
|
||
|
|
**Props:**
|
||
|
|
- `config: AnalyticsConfig` - Configuration object
|
||
|
|
- `apiBaseUrl: string` - Base URL for analytics API
|
||
|
|
- `appName: string` - Application name
|
||
|
|
- `batchSize?: number` - Number of events to batch (default: 10)
|
||
|
|
- `batchInterval?: number` - Batch interval in ms (default: 5000)
|
||
|
|
- `enableDebugLogging?: boolean` - Enable debug logs (default: false)
|
||
|
|
- `sessionIdKey?: string` - LocalStorage key for session ID (default: 'analytics_session_id')
|
||
|
|
|
||
|
|
#### `useAnalytics()`
|
||
|
|
|
||
|
|
Hook to access the analytics context.
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- `trackView: (data: ViewEventData) => void`
|
||
|
|
- `trackEngagement: (data: EngagementEventData) => void`
|
||
|
|
- `flush: () => Promise<void>`
|
||
|
|
|
||
|
|
#### `useTrackPageView(data: ViewEventData)`
|
||
|
|
|
||
|
|
Hook to automatically track page views when component mounts.
|
||
|
|
|
||
|
|
#### `useTrackEngagement()`
|
||
|
|
|
||
|
|
Hook that returns a function to track engagement events.
|
||
|
|
|
||
|
|
### NestJS API
|
||
|
|
|
||
|
|
#### `AnalyticsModule`
|
||
|
|
|
||
|
|
NestJS module for dependency injection.
|
||
|
|
|
||
|
|
**Methods:**
|
||
|
|
- `forRoot(options: AnalyticsModuleOptions): DynamicModule`
|
||
|
|
- `forRootAsync(options: AnalyticsModuleAsyncOptions): DynamicModule`
|
||
|
|
|
||
|
|
#### `@TrackAnalytics(options: TrackAnalyticsOptions)`
|
||
|
|
|
||
|
|
Decorator to mark methods for automatic tracking.
|
||
|
|
|
||
|
|
**Options:**
|
||
|
|
- `eventType: 'view' | 'engagement'` - Type of event
|
||
|
|
- `contentType?: string` - Content type for view events
|
||
|
|
- `metricType?: string` - Metric type for engagement events
|
||
|
|
- `targetType?: string` - Target type for engagement events
|
||
|
|
- `idExtractor?: (...args) => string` - Function to extract ID from arguments
|
||
|
|
- `userIdExtractor?: (context) => string` - Function to extract user ID
|
||
|
|
- `metadata?: Record<string, unknown>` - Additional metadata
|
||
|
|
|
||
|
|
#### `AnalyticsInterceptor`
|
||
|
|
|
||
|
|
Interceptor that processes `@TrackAnalytics()` metadata and sends events.
|
||
|
|
|
||
|
|
#### Helper Functions
|
||
|
|
|
||
|
|
##### `trackServiceCall(client, context, options)`
|
||
|
|
|
||
|
|
Track analytics from service methods.
|
||
|
|
|
||
|
|
##### `trackApiEndpoint(client, request, options)`
|
||
|
|
|
||
|
|
Track analytics from API endpoints with full HTTP context.
|
||
|
|
|
||
|
|
##### `createTrackingMiddleware(client, options)`
|
||
|
|
|
||
|
|
Create custom tracking middleware.
|
||
|
|
|
||
|
|
## Types
|
||
|
|
|
||
|
|
### `ViewEventData`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ViewEventData {
|
||
|
|
contentId: string;
|
||
|
|
contentType: 'video' | 'image' | 'post' | 'stream' | 'product';
|
||
|
|
userId?: string;
|
||
|
|
sessionId: string;
|
||
|
|
referrer?: string;
|
||
|
|
deviceType?: 'mobile' | 'tablet' | 'desktop';
|
||
|
|
app: string;
|
||
|
|
domain?: string;
|
||
|
|
duration?: number;
|
||
|
|
ipAddress?: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `EngagementEventData`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface EngagementEventData {
|
||
|
|
userId: string;
|
||
|
|
metricType: 'like' | 'comment' | 'share' | 'subscribe' | 'tip' | 'purchase';
|
||
|
|
targetId: string;
|
||
|
|
targetType: 'content' | 'user' | 'product' | 'stream';
|
||
|
|
metadata?: Record<string, unknown>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### Error Handling
|
||
|
|
|
||
|
|
The analytics client fails silently by default to not disrupt application flow. Errors are logged to the console but don't throw.
|
||
|
|
|
||
|
|
### Performance
|
||
|
|
|
||
|
|
- Events are batched automatically to reduce network overhead
|
||
|
|
- Use appropriate batch sizes based on your traffic patterns
|
||
|
|
- Flush on critical events (e.g., before payment processing)
|
||
|
|
|
||
|
|
### Privacy
|
||
|
|
|
||
|
|
- Only track anonymous views for unauthenticated users
|
||
|
|
- Always get user consent before tracking
|
||
|
|
- Respect user privacy preferences
|
||
|
|
|
||
|
|
### Testing
|
||
|
|
|
||
|
|
Mock the analytics client in tests:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const mockAnalytics = {
|
||
|
|
trackView: jest.fn(),
|
||
|
|
trackEngagement: jest.fn(),
|
||
|
|
flush: jest.fn(),
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Contributing
|
||
|
|
|
||
|
|
This package follows the lilith platform's development guidelines. See the main repository README for contribution instructions.
|
||
|
|
|
||
|
|
## License
|
||
|
|
|
||
|
|
Proprietary - Part of the lilith platform
|