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' ;
@@ -8,6 +7,7 @@ import useragent from 'useragent';
87import { ParsedUserAgent } from './types' ;
98import { OnEvent } from '@nestjs/event-emitter' ;
109import { UrlRedirectedEvent } from 'src/event/Url-redirected.events' ;
10+ import { FilterAnalyticsRequestData } from './dto/filter-analytics-request-data' ;
1111@Injectable ( )
1212export class AnalyticsService {
1313 constructor (
@@ -19,10 +19,9 @@ export class AnalyticsService {
1919 async recordClick ( event : UrlRedirectedEvent ) : Promise < void > {
2020 const urlId = event . urlId ;
2121 const req = event . req ;
22- const ip =
23- ( req . headers [ 'x-forwarded-for' ] as string ) ?. split ( ',' ) [ 0 ] ?. trim ( ) ||
24- req . socket ?. remoteAddress ?. replace ( '::ffff:' , '' ) ||
25- '0.0.0.0' ;
22+ const ipAddress = ( req . headers [ 'x-forwarded-for' ] as string )
23+ ?. split ( ',' ) [ 0 ]
24+ ?. trim ( ) ;
2625
2726 const userAgent = req . headers [ 'user-agent' ] || '' ;
2827 const parsed = (
@@ -31,20 +30,85 @@ export class AnalyticsService {
3130 }
3231 ) . parse ( userAgent ) ;
3332
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".
3435 const deviceMatch = parsed . source . match ( / \( ( [ ^ ; ] + ) ; / ) ;
3536 const device = deviceMatch ? deviceMatch [ 1 ] : 'Unknown Device' ;
36- const geo = geoip . lookup ( ip ) ;
37+
38+ // Look for known browser names followed by a version number,
39+ // e.g. "Chrome/120.0", "Firefox/118.0".
40+ const browserMatch = parsed . source . match (
41+ / ( 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 . ] + / ,
42+ ) ;
43+ const browser = browserMatch ? browserMatch [ 0 ] : 'Unknown Browser' ;
44+
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 ) ;
3752 const country = geo ?. country || 'Unknown' ;
3853
3954 const analytics = this . analyticsRepo . create ( {
4055 urlId,
41- ip,
42- browser : `${ parsed . family } ${ parsed . major } .${ parsed . minor } .${ parsed . patch } ` ,
56+ os,
57+ ipAddress,
58+ browser : browser ,
4359 userAgent,
4460 device : device ,
4561 country,
4662 } ) ;
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+
72+ qb . andWhere ( 'url.userId=:userId' , { userId } ) ;
73+
74+ if ( requestData . startDate && requestData . endDate ) {
75+ qb . andWhere ( 'a.redirectedAt BETWEEN :start AND :end' , {
76+ start : requestData . startDate ,
77+ end : requestData . endDate ,
78+ } ) ;
79+ }
80+
81+ if ( requestData . startDate ) {
82+ qb . andWhere ( 'a.redirectedAt >= :start' , { start : requestData . startDate } ) ;
83+ }
84+
85+ if ( requestData . endDate ) {
86+ qb . andWhere ( 'a.redirectedAt <= :end' , { end : requestData . endDate } ) ;
87+ }
88+
89+ if ( requestData . browser ) {
90+ qb . andWhere ( 'a.browser = :browser' , { browser : requestData . browser } ) ;
91+ }
92+ if ( requestData . country ) {
93+ qb . andWhere ( 'a.country = :country' , { country : requestData . country } ) ;
94+ }
95+
96+ if ( requestData . device ) {
97+ qb . andWhere ( 'a.device = :device' , { device : requestData . device } ) ;
98+ }
99+
100+ if ( requestData . os ) {
101+ qb . andWhere ( 'a.os = :os' , { os : requestData . os } ) ;
102+ }
103+
104+ if ( requestData . groupByUrl ) {
105+ qb . select ( 'a.url' , 'url' )
106+ . addSelect ( 'COUNT(*)' , 'hits' )
107+ . groupBy ( 'a.url' )
108+ . orderBy ( 'hits' , 'DESC' ) ;
109+ return qb . getRawMany ( ) ;
110+ }
111+
112+ return qb . getMany ( ) ;
113+ }
50114}
0 commit comments