11import { Injectable } from '@nestjs/common' ;
2- import { Request } from 'express' ;
32import geoip from 'geoip-lite' ;
43import { UrlAnalytics } from './analytics.entity' ;
54import { InjectRepository } from '@nestjs/typeorm' ;
65import { Repository } from 'typeorm' ;
76import useragent from 'useragent' ;
87import { ParsedUserAgent } from './types' ;
8+ import { OnEvent } from '@nestjs/event-emitter' ;
9+ import { UrlRedirectedEvent } from 'src/event/Url-redirected.events' ;
10+ import { FilterAnalyticsRequestData } from './dto/filter-analytics-request-data' ;
911@Injectable ( )
1012export class AnalyticsService {
1113 constructor (
1214 @InjectRepository ( UrlAnalytics )
1315 private readonly analyticsRepo : Repository < UrlAnalytics > ,
1416 ) { }
1517
16- async recordClick ( urlId : string , req : Request ) : Promise < void > {
17- const ip =
18- ( req . headers [ 'x-forwarded-for' ] as string ) ?. split ( ',' ) [ 0 ] ?. trim ( ) ||
19- req . socket ?. remoteAddress ?. replace ( '::ffff:' , '' ) ||
20- '0.0.0.0' ;
18+ @OnEvent ( 'url.redirected' )
19+ async recordClick ( event : UrlRedirectedEvent ) : Promise < void > {
20+ const urlId = event . urlId ;
21+ const req = event . req ;
22+ const ipAddress = ( req . headers [ 'x-forwarded-for' ] as string )
23+ ?. split ( ',' ) [ 0 ]
24+ ?. trim ( ) ;
2125
2226 const userAgent = req . headers [ 'user-agent' ] || '' ;
2327 const parsed = (
@@ -26,19 +30,31 @@ export class AnalyticsService {
2630 }
2731 ) . parse ( userAgent ) ;
2832
33+ // Match the first section inside parentheses of the User-Agent string (up to the first semicolon).
34+ // This typically represents the device or platform, e.g. "Windows NT 10.0" or "iPhone".
2935 const deviceMatch = parsed . source . match ( / \( ( [ ^ ; ] + ) ; / ) ;
3036 const device = deviceMatch ? deviceMatch [ 1 ] : 'Unknown Device' ;
3137
38+ // Look for known browser names followed by a version number,
39+ // e.g. "Chrome/120.0", "Firefox/118.0".
3240 const browserMatch = parsed . source . match (
3341 / ( C h r o m e | F i r e f o x | S a f a r i | E d g e | O p e r a ) \/ [ \d . ] + / ,
3442 ) ;
3543 const browser = browserMatch ? browserMatch [ 0 ] : 'Unknown Browser' ;
3644
37- const geo = geoip . lookup ( ip ) ;
45+ // Match the substring inside parentheses that follows the first semicolon.
46+ // For example, from "(Windows NT 10.0; Win64; x64)" → captures "Win64; x64".
47+ const osMatch = parsed . source . match ( / \( (?: [ ^ ; ] + ) ; \s * ( [ ^ ) ] + ) \) / ) ;
48+
49+ const os = osMatch ? osMatch [ 1 ] : 'Unknown OS' ;
50+
51+ const geo = geoip . lookup ( ipAddress ) ;
3852 const country = geo ?. country || 'Unknown' ;
53+
3954 const analytics = this . analyticsRepo . create ( {
4055 urlId,
41- ip,
56+ os,
57+ ipAddress,
4258 browser : browser ,
4359 userAgent,
4460 device : device ,
@@ -47,4 +63,57 @@ export class AnalyticsService {
4763
4864 await this . analyticsRepo . save ( analytics ) ;
4965 }
66+
67+ async getAnalytics ( requestData : FilterAnalyticsRequestData , userId : string ) {
68+ const qb = this . analyticsRepo
69+ . createQueryBuilder ( 'a' )
70+ . innerJoin ( 'a.url' , 'url' )
71+ . take ( 10 )
72+ . skip ( 10 ) ;
73+
74+ qb . andWhere ( 'url.userId=:userId' , { userId } ) ;
75+
76+ const start = requestData . startDate || new Date ( 0 ) ;
77+ const end = requestData . endDate || new Date ( ) ;
78+
79+ qb . andWhere ( 'a.redirectedAt BETWEEN :start AND :end' , {
80+ start,
81+ end,
82+ } ) ;
83+
84+ if ( requestData . browser ) {
85+ qb . andWhere ( 'a.browser = :browser' , { browser : requestData . browser } ) ;
86+ }
87+ if ( requestData . country ) {
88+ qb . andWhere ( 'a.country = :country' , { country : requestData . country } ) ;
89+ }
90+
91+ if ( requestData . device ) {
92+ qb . andWhere ( 'a.device = :device' , { device : requestData . device } ) ;
93+ }
94+
95+ if ( requestData . os ) {
96+ qb . andWhere ( 'a.os = :os' , { os : requestData . os } ) ;
97+ }
98+
99+ const groupColumns : string [ ] = [ ] ;
100+
101+ if ( requestData . groupByUrl ) groupColumns . push ( 'a.url_id' ) ;
102+ if ( requestData . groupByDevice ) groupColumns . push ( 'a.device' ) ;
103+ if ( requestData . groupByOs ) groupColumns . push ( 'a.os' ) ;
104+ if ( requestData . groupByBrowser ) groupColumns . push ( 'a.browser' ) ;
105+ if ( requestData . groupByCountry ) groupColumns . push ( 'a.country' ) ;
106+ if ( requestData . groupByIpAddress ) groupColumns . push ( 'a.ip_address' ) ;
107+
108+ if ( groupColumns . length > 0 ) {
109+ qb . select ( groupColumns . join ( ', ' ) )
110+ . addSelect ( 'COUNT(*)' , 'hits' )
111+ . groupBy ( groupColumns . join ( ', ' ) )
112+ . orderBy ( 'hits' , 'DESC' ) ;
113+
114+ return qb . getRawMany ( ) ;
115+ }
116+
117+ return await qb . getMany ( ) ;
118+ }
50119}
0 commit comments