From 9500c3e9139105036b132499c6bc612daae8a553 Mon Sep 17 00:00:00 2001 From: pandutibil Date: Wed, 13 Sep 2023 03:24:44 +0530 Subject: [PATCH] Telemetry implemented for page event, click event, and filter change event --- package.json | 1 + src/app/app.component.ts | 33 +++++++++++-------- .../browser-detection.service.spec.ts | 16 +++++++++ .../services/browser-detection.service.ts | 27 +++++++++++++++ .../services/device-detection.service.spec.ts | 16 +++++++++ .../core/services/device-detection.service.ts | 21 ++++++++++++ .../dom-event-tracker.service.spec.ts | 16 +++++++++ .../services/dom-event-tracker.service.ts | 29 ++++++++++++++++ .../services/page-tracker.service.spec.ts | 16 +++++++++ src/app/core/services/page-tracker.service.ts | 30 +++++++++++++++++ .../core/services/telemetry.service.spec.ts | 16 +++++++++ src/app/core/services/telemetry.service.ts | 29 ++++++++++++++++ .../download-button.component.html | 4 +-- .../filter-panel/filter-panel.component.html | 2 +- .../directives/change.directive.spec.ts | 8 +++++ src/app/shared/directives/change.directive.ts | 20 +++++++++++ .../shared/directives/click.directive.spec.ts | 8 +++++ src/app/shared/directives/click.directive.ts | 20 +++++++++++ src/app/shared/shared.module.ts | 8 +++-- src/app/utilities/DateFormatter.ts | 13 ++++++++ src/environments/environment.ts | 8 +++-- 21 files changed, 320 insertions(+), 21 deletions(-) create mode 100644 src/app/core/services/browser-detection.service.spec.ts create mode 100644 src/app/core/services/browser-detection.service.ts create mode 100644 src/app/core/services/device-detection.service.spec.ts create mode 100644 src/app/core/services/device-detection.service.ts create mode 100644 src/app/core/services/dom-event-tracker.service.spec.ts create mode 100644 src/app/core/services/dom-event-tracker.service.ts create mode 100644 src/app/core/services/page-tracker.service.spec.ts create mode 100644 src/app/core/services/page-tracker.service.ts create mode 100644 src/app/core/services/telemetry.service.spec.ts create mode 100644 src/app/core/services/telemetry.service.ts create mode 100644 src/app/shared/directives/change.directive.spec.ts create mode 100644 src/app/shared/directives/change.directive.ts create mode 100644 src/app/shared/directives/click.directive.spec.ts create mode 100644 src/app/shared/directives/click.directive.ts create mode 100644 src/app/utilities/DateFormatter.ts diff --git a/package.json b/package.json index 241c3bfe..baae6f2b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "ng-circle-progress": "^1.6.0", "ngx-bootstrap": "^9.0.0", "ngx-daterangepicker-material": "^6.0.4", + "ngx-device-detector": "^4.0.1", "ngx-spinner": "^14.0.0", "rxjs": "~7.5.0", "tslib": "^2.3.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 589511a3..dc36b5b1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,11 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, HostListener } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { environment } from 'src/environments/environment'; import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError, Event, ActivatedRoute } from '@angular/router'; import { filter, map } from 'rxjs/operators'; import { Title } from '@angular/platform-browser'; import { AppConfig } from './app.config'; import { HttpClient } from '@angular/common/http'; +import { PageTrackerService } from './core/services/page-tracker.service'; declare const gtag: Function; // <------------Important: the declartion for gtag is required! declare var dataLayer: Array; @@ -19,7 +19,7 @@ export class AppComponent { title = 'cQube National'; loadingDataImg: boolean = false; constructor(private translate: TranslateService, private titleService: Title, - private router: Router, private activatedRoute: ActivatedRoute, public config: AppConfig, private http: HttpClient) { + private router: Router, private activatedRoute: ActivatedRoute, public config: AppConfig, private http: HttpClient, private pageTrackerService: PageTrackerService) { translate.setDefaultLang('en'); translate.use('en'); /** START : Code to Track Page View using gtag.js */ @@ -41,18 +41,20 @@ export class AppComponent { // }) // }) - // //Add dynamic title for selected pages - Start - // router.events.subscribe(event => { - // if (event instanceof NavigationEnd) { - // var title = this.getTitle(router.routerState, router.routerState.root).join(' > '); - // titleService.setTitle(title); - // } - // }); - + //Add dynamic title for selected pages - Start + router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + if (event.url !== '/login') { + this.pageTrackerService.onPageChange(event); + } + } + }); } + ngOnInit() { window.scrollTo(0, 0); } + // collect that title data properties from all child routes getTitle(state, parent) { var data = []; @@ -88,7 +90,7 @@ export class AppComponent { } } - async grabTheTrackIds(trackIds) { + async grabTheTrackIds(trackIds) { for (const [key, value] of Object.entries(trackIds)) { const gaTrackId = value; let customGtagScriptEle = document.createElement('script'); @@ -107,5 +109,10 @@ export class AppComponent { } } - + @HostListener('window:beforeunload', ['$event']) + handleUnload(event: Event): void { + if (this.router.url !== '/login') { + this.pageTrackerService.onPageChange(event); + } + } } diff --git a/src/app/core/services/browser-detection.service.spec.ts b/src/app/core/services/browser-detection.service.spec.ts new file mode 100644 index 00000000..12ce04d5 --- /dev/null +++ b/src/app/core/services/browser-detection.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BrowserDetectionService } from './browser-detection.service'; + +describe('BrowserDetectionService', () => { + let service: BrowserDetectionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BrowserDetectionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/browser-detection.service.ts b/src/app/core/services/browser-detection.service.ts new file mode 100644 index 00000000..4adbed19 --- /dev/null +++ b/src/app/core/services/browser-detection.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class BrowserDetectionService { + + constructor() { } + + getBrowserType(): string { + const userAgent = window.navigator.userAgent; + + if (userAgent.indexOf('Chrome') !== -1) { + return 'Chrome'; + } else if (userAgent.indexOf('Firefox') !== -1) { + return 'Firefox'; + } else if (userAgent.indexOf('Safari') !== -1) { + return 'Safari'; + } else if (userAgent.indexOf('Edge') !== -1) { + return 'Edge'; + } else if (userAgent.indexOf('IE') !== -1 || userAgent.indexOf('Trident/') !== -1) { + return 'Internet Explorer'; + } else { + return 'Unknown'; + } + } +} diff --git a/src/app/core/services/device-detection.service.spec.ts b/src/app/core/services/device-detection.service.spec.ts new file mode 100644 index 00000000..1934f1f2 --- /dev/null +++ b/src/app/core/services/device-detection.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DeviceDetectionService } from './device-detection.service'; + +describe('DeviceDetectionService', () => { + let service: DeviceDetectionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DeviceDetectionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/device-detection.service.ts b/src/app/core/services/device-detection.service.ts new file mode 100644 index 00000000..54374e63 --- /dev/null +++ b/src/app/core/services/device-detection.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { DeviceDetectorService } from 'ngx-device-detector'; + +@Injectable({ + providedIn: 'root' +}) +export class DeviceDetectionService { + constructor(private deviceService: DeviceDetectorService) {} + + getDeviceType(): string { + if (this.deviceService.isDesktop()) { + return 'Desktop'; + } else if (this.deviceService.isTablet()) { + return 'Tablet'; + } else if (this.deviceService.isMobile()) { + return 'Mobile'; + } else { + return 'Unknown'; + } + } +} diff --git a/src/app/core/services/dom-event-tracker.service.spec.ts b/src/app/core/services/dom-event-tracker.service.spec.ts new file mode 100644 index 00000000..98014b5d --- /dev/null +++ b/src/app/core/services/dom-event-tracker.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DomEventTrackerService } from './dom-event-tracker.service'; + +describe('DomEventTrackerService', () => { + let service: DomEventTrackerService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DomEventTrackerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/dom-event-tracker.service.ts b/src/app/core/services/dom-event-tracker.service.ts new file mode 100644 index 00000000..93ccbf13 --- /dev/null +++ b/src/app/core/services/dom-event-tracker.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { TelemetryService } from './telemetry.service'; +import { Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class DomEventTrackerService { + + constructor(private readonly _router: Router, private readonly _telemetryService: TelemetryService) { } + + onClick(event): void { + const { pageName, eventName } = event; + this._telemetryService.saveTelemetry({ + pageName: pageName ? pageName : this._router.url?.slice(1), + pageEvent: "click", + pageEventName: eventName ? eventName : "filter" + }).subscribe(res => console.log('telemetry saved!!!'));; + } + + onChange(event): void { + const { pageName, eventName } = event; + this._telemetryService.saveTelemetry({ + pageName: pageName ? pageName : this._router.url?.slice(1), + pageEvent: "change", + pageEventName: eventName ? eventName : "filter" + }).subscribe(res => console.log('telemetry saved!!!'));; + } +} diff --git a/src/app/core/services/page-tracker.service.spec.ts b/src/app/core/services/page-tracker.service.spec.ts new file mode 100644 index 00000000..d6301471 --- /dev/null +++ b/src/app/core/services/page-tracker.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PageTrackerService } from './page-tracker.service'; + +describe('PageTrackerService', () => { + let service: PageTrackerService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PageTrackerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/page-tracker.service.ts b/src/app/core/services/page-tracker.service.ts new file mode 100644 index 00000000..0be58355 --- /dev/null +++ b/src/app/core/services/page-tracker.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { TelemetryService } from './telemetry.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PageTrackerService { + + previousRoute: string; + startTime: Date; + + constructor(private readonly _telemetryService: TelemetryService) { } + + onPageChange(event): void { + if (this.previousRoute && event.url !== this.previousRoute) { + const now = new Date(); + this._telemetryService.saveTelemetry({ + pageName: this.previousRoute.slice(1), + pageEvent: "onLoad", + pageEventName: "onLoad", + timeSpent: (now.getTime() - this.startTime.getTime()), + timeIn: this.startTime.getTime(), + timeOut: now.getTime() + }).subscribe(res => console.log('telemetry saved!!!')); + } + + this.startTime = new Date(); + this.previousRoute = event.url; + } +} diff --git a/src/app/core/services/telemetry.service.spec.ts b/src/app/core/services/telemetry.service.spec.ts new file mode 100644 index 00000000..317654cd --- /dev/null +++ b/src/app/core/services/telemetry.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { TelemetryService } from './telemetry.service'; + +describe('TelemetryService', () => { + let service: TelemetryService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TelemetryService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/telemetry.service.ts b/src/app/core/services/telemetry.service.ts new file mode 100644 index 00000000..96342c6a --- /dev/null +++ b/src/app/core/services/telemetry.service.ts @@ -0,0 +1,29 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { BrowserDetectionService } from './browser-detection.service'; +import { DeviceDetectionService } from './device-detection.service'; +import { formatDateToDDMMYY } from 'src/app/utilities/DateFormatter'; + +@Injectable({ + providedIn: 'root' +}) +export class TelemetryService { + + constructor(private _http: HttpClient, private _browserDetectionService: BrowserDetectionService, private _deviceDetectionService: DeviceDetectionService) { } + + saveTelemetry(eventData: any): Observable { + let data = { + date: formatDateToDDMMYY(new Date()), + timestamp: new Date().getTime(), + userId: localStorage.getItem('user_id'), + deviceType: this._deviceDetectionService.getDeviceType(), + userLocation: "", + browserType: this._browserDetectionService.getBrowserType(), + ...eventData + }; + + return this._http.post(`${environment.apiURL}/captureTelemetry`, data); + } +} diff --git a/src/app/shared/components/buttons/download-button/download-button.component.html b/src/app/shared/components/buttons/download-button/download-button.component.html index 3b40a08d..a6704bf1 100644 --- a/src/app/shared/components/buttons/download-button/download-button.component.html +++ b/src/app/shared/components/buttons/download-button/download-button.component.html @@ -5,14 +5,14 @@ -->
-
- diff --git a/src/app/shared/components/filter-panel/filter-panel.component.html b/src/app/shared/components/filter-panel/filter-panel.component.html index 0f853b46..cff55717 100644 --- a/src/app/shared/components/filter-panel/filter-panel.component.html +++ b/src/app/shared/components/filter-panel/filter-panel.component.html @@ -7,7 +7,7 @@ + (ngModelChange)="onSelectOption($event, i, selectRef)" appChange> {{ option.label }} diff --git a/src/app/shared/directives/change.directive.spec.ts b/src/app/shared/directives/change.directive.spec.ts new file mode 100644 index 00000000..a1d845c4 --- /dev/null +++ b/src/app/shared/directives/change.directive.spec.ts @@ -0,0 +1,8 @@ +import { ChangeDirective } from './change.directive'; + +describe('ChangeDirective', () => { + it('should create an instance', () => { + const directive = new ChangeDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/shared/directives/change.directive.ts b/src/app/shared/directives/change.directive.ts new file mode 100644 index 00000000..4cde8233 --- /dev/null +++ b/src/app/shared/directives/change.directive.ts @@ -0,0 +1,20 @@ +import { Directive, ElementRef, HostListener } from '@angular/core'; +import { DomEventTrackerService } from 'src/app/core/services/dom-event-tracker.service'; + +@Directive({ + selector: '[appChange]' +}) +export class ChangeDirective { + + constructor(private el: ElementRef, private readonly _domEventTrackerService: DomEventTrackerService) { } + + @HostListener('change') onChange() { + const eventName = this.el.nativeElement.getAttribute("data-event-name"); + const pageName = this.el.nativeElement.getAttribute("data-page-name"); + this._domEventTrackerService.onChange({ + eventName, + pageName + }); + } + +} diff --git a/src/app/shared/directives/click.directive.spec.ts b/src/app/shared/directives/click.directive.spec.ts new file mode 100644 index 00000000..f1c56e72 --- /dev/null +++ b/src/app/shared/directives/click.directive.spec.ts @@ -0,0 +1,8 @@ +import { ClickDirective } from './click.directive'; + +describe('ClickDirective', () => { + it('should create an instance', () => { + const directive = new ClickDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/shared/directives/click.directive.ts b/src/app/shared/directives/click.directive.ts new file mode 100644 index 00000000..04ac4966 --- /dev/null +++ b/src/app/shared/directives/click.directive.ts @@ -0,0 +1,20 @@ +import { Directive, ElementRef, HostListener } from '@angular/core'; +import { DomEventTrackerService } from 'src/app/core/services/dom-event-tracker.service'; + +@Directive({ + selector: '[appClick]' +}) +export class ClickDirective { + + constructor(private el: ElementRef, private readonly _domEventTrackerService: DomEventTrackerService) { } + + @HostListener('click') onClick() { + const eventName = this.el.nativeElement.getAttribute("data-event-name"); + const pageName = this.el.nativeElement.getAttribute("data-page-name"); + this._domEventTrackerService.onClick({ + eventName, + pageName + }); + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index f7b57990..0b93b97a 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -56,6 +56,8 @@ import { MatIconModule } from '@angular/material/icon'; import { CriteriaComponent } from './components/criteria/criteria.component'; import { SearchFilterPipe } from './pipes/searchFilter/search-filter.pipe'; import { SearchBarComponent } from './components/search-bar/search-bar.component'; +import { ClickDirective } from './directives/click.directive'; +import { ChangeDirective } from './directives/change.directive'; const IMPORTS: any[] = [ ReactiveFormsModule, @@ -119,13 +121,15 @@ const DECLARATIONS = [ CriteriaComponent, MaterialHeatChartDrilldownTableComponent, SearchFilterPipe, - SearchBarComponent + SearchBarComponent, + ClickDirective, + ChangeDirective ]; @NgModule({ declarations: [ DECLARATIONS, - BreadcrumbComponentComponent, + BreadcrumbComponentComponent ], imports: [ CommonModule, diff --git a/src/app/utilities/DateFormatter.ts b/src/app/utilities/DateFormatter.ts new file mode 100644 index 00000000..87321b26 --- /dev/null +++ b/src/app/utilities/DateFormatter.ts @@ -0,0 +1,13 @@ +export function formatDateToDDMMYY(inputDate) { + const date = new Date(inputDate); + + // Get day, month, and year components + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Note: Month is zero-based, so we add 1 + const year = String(date.getFullYear()).slice(-2); // Get the last two digits of the year + + // Create the formatted date string in "dd/mm/yy" format + const formattedDate = `${day}/${month}/${year}`; + + return formattedDate; +} diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 5970117a..202104b5 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -2,11 +2,11 @@ // `ng build` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. -export const environment = { +export const environment = { production: false, - google_analytics_tracking_id: "UA-260554959-3", // apiURL: 'https://qr.staging.cqube.samagra.io', apiURL: 'http://localhost:3008', + // apiURL: 'http://192.168.1.159:3008', stateCode: "NA", numberFormat: { reports: { @@ -15,7 +15,9 @@ export const environment = { } }, config: "NVSK", - + loginNeeded: true, + guestUsername: "guest-user", + guestPassword: "guest" }; /*