import { Component, OnInit, OnDestroy, HostBinding } from '@angular/core'; import * as L from 'leaflet'; import { globals, Roles, RoleIds, ProdTypes, ProdType, vehTypes, VehType, MatType, matTypes } from './shared/global'; import { filter } from 'rxjs/operators'; import { NavigationEnd, NavigationError, NavigationCancel, NavigationStart } from '@angular/router'; import { environment } from '@environments/environment'; import { BaseComp } from './shared/base/base.component'; // Declare ga as a function to set and sent the events // declare let ga: Function; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent extends BaseComp implements OnInit, OnDestroy { @HostBinding('@.disabled') public animationsDisabled = L.Browser.mobile; // Disable Web Animation as it is not turn on as default in IOS private navigationStartTime: number = 0; private previousUrl: string = ''; private sessionPageCount: number = 0; private pageStartTime: number = 0; get showFooter() { return location.href.indexOf('/login') != -1; } constructor() { super(); this["name"] = "AppComp"; } ngOnInit() { // Initialize GA4 when Angular app is ready this.gaSvc.initialize(); if (!environment.production) { !environment.production && console.log('GA4 Service initialized:', this.gaSvc.isInitialized()); } // Track session start this.trackSessionStart(); // Subscribe to router events for comprehensive navigation tracking this.router.events.subscribe(event => { if (event instanceof NavigationStart) { this.handleNavigationStart(event); } else if (event instanceof NavigationEnd) { this.handleNavigationEnd(event); } else if (event instanceof NavigationError) { this.handleNavigationError(event); } else if (event instanceof NavigationCancel) { this.handleNavigationCancel(event); } }); // Track initial page load this.pageStartTime = Date.now(); } /** * Extract page title from URL path for analytics * @param url - The URL path * @returns Human-readable page title */ private getPageTitle(url: string): string { // Remove query parameters and fragments const cleanUrl = url.split('?')[0].split('#')[0]; // Extract main route segments const segments = cleanUrl.split('/').filter(segment => segment.length > 0); if (segments.length === 0) { return 'Dashboard'; } // Map common routes to readable titles const routeTitleMap: { [key: string]: string } = { 'login': 'Login', 'dashboard': 'Dashboard', 'jobs': 'Jobs', 'job': 'Job Details', 'clients': 'Clients', 'client': 'Client Details', 'accounts': 'Accounts', 'billing': 'Billing', 'profile': 'Profile', 'tools': 'Tools', 'areas': 'Areas Management', 'upload': 'File Upload', 'track': 'Tracking', 'admin': 'Administration' }; const mainRoute = segments[0]; return routeTitleMap[mainRoute] || this.capitalizeRoute(mainRoute); } /** * Capitalize route name for display * @param route - Route string * @returns Capitalized route name */ private capitalizeRoute(route: string): string { return route.charAt(0).toUpperCase() + route.slice(1).replace(/-/g, ' '); } /** * Handle navigation start event * @param event - NavigationStart event */ private handleNavigationStart(event: NavigationStart): void { this.navigationStartTime = Date.now(); // Track navigation start this.gaSvc.trackEvent('navigation_started', { navigation_type: 'route_change', source_url: this.previousUrl, destination_url: event.url, navigation_method: event.navigationTrigger === 'imperative' ? 'programmatic' : 'router_link', navigation_timing_ms: 0, is_authenticated: !!(this.authSvc.user && this.authSvc.byPUserId), session_page_count: this.sessionPageCount, time_on_previous_page_ms: this.pageStartTime ? Date.now() - this.pageStartTime : 0, user_id: this.authSvc.byPUserId, user_role: this.getUserRole(), referrer: document.referrer, user_agent: navigator.userAgent, viewport_width: window.innerWidth, viewport_height: window.innerHeight, screen_resolution: `${screen.width}x${screen.height}` }); } /** * Handle successful navigation end * @param event - NavigationEnd event */ private handleNavigationEnd(event: NavigationEnd): void { const navigationTime = this.navigationStartTime ? Date.now() - this.navigationStartTime : 0; this.sessionPageCount++; if (!environment.production) { console.log('Page navigation:', event.urlAfterRedirects); } // Track navigation completion this.gaSvc.trackEvent('navigation_completed', { navigation_type: 'route_change', source_url: this.previousUrl, destination_url: event.urlAfterRedirects, navigation_method: 'router_link', navigation_timing_ms: navigationTime, page_title: this.getPageTitle(event.urlAfterRedirects), previous_page_title: this.previousUrl ? this.getPageTitle(this.previousUrl) : '', is_authenticated: !!(this.authSvc.user && this.authSvc.byPUserId), session_page_count: this.sessionPageCount, time_on_previous_page_ms: this.pageStartTime ? Date.now() - this.pageStartTime : 0, user_id: this.authSvc.byPUserId, user_role: this.getUserRole(), referrer: document.referrer, user_agent: navigator.userAgent, viewport_width: window.innerWidth, viewport_height: window.innerHeight, screen_resolution: `${screen.width}x${screen.height}`, bounce_candidate: this.sessionPageCount === 1 }); // Track traditional page view for backward compatibility this.gaSvc.trackPageView( this.getPageTitle(event.urlAfterRedirects), event.urlAfterRedirects ); // Set user ID if user is authenticated if (this.authSvc.user && this.authSvc.byPUserId) { this.gaSvc.setUserId(this.authSvc.byPUserId); // Set user properties for better segmentation this.gaSvc.setUserProperties({ user_type: 'authenticated', client_name: this.authSvc.user.name || 'unknown' }); } // Update tracking variables this.previousUrl = event.urlAfterRedirects; this.pageStartTime = Date.now(); // Track slow page loads (threshold: 3 seconds) if (navigationTime > 3000) { this.gaSvc.trackEvent('slow_page_load', { page_title: this.getPageTitle(event.urlAfterRedirects), load_time_ms: navigationTime, connection_type: this.getConnectionType(), device_type: this.getDeviceType(), platform: 'web' }); } } /** * Handle navigation error * @param event - NavigationError event */ private handleNavigationError(event: NavigationError): void { const navigationTime = this.navigationStartTime ? Date.now() - this.navigationStartTime : 0; if (!environment.production) { console.error('Navigation error:', event.error, 'URL:', event.url); } // Determine error type based on error message let errorType: 'route_not_found' | 'navigation_cancelled' | 'guard_rejected' | 'resolver_error' | 'timeout' | 'network_error' | 'permission_denied' = 'navigation_cancelled'; if (event.error?.message?.includes('Cannot match any routes')) { errorType = 'route_not_found'; } else if (event.error?.message?.includes('guard')) { errorType = 'guard_rejected'; } else if (event.error?.message?.includes('resolver')) { errorType = 'resolver_error'; } else if (event.error?.message?.includes('timeout')) { errorType = 'timeout'; } else if (event.error?.message?.includes('network')) { errorType = 'network_error'; } else if (event.error?.message?.includes('permission')) { errorType = 'permission_denied'; } // Track navigation error this.gaSvc.trackEvent('navigation_error', { error_type: errorType, error_message: event.error?.message || 'Unknown navigation error', error_code: event.error?.name || 'NavigationError', error_stack: event.error?.stack || '', attempted_url: event.url, source_url: this.previousUrl, navigation_method: 'router_link', error_timestamp: new Date().toISOString(), navigation_timing_ms: navigationTime, is_authenticated: !!(this.authSvc.user && this.authSvc.byPUserId), user_permissions: this.getUserPermissions(), session_duration_ms: this.pageStartTime ? Date.now() - this.pageStartTime : 0, previous_successful_navigation: this.previousUrl, user_id: this.authSvc.byPUserId, user_role: this.getUserRole(), browser_info: navigator.userAgent, device_type: this.getDeviceType(), route_depth: event.url.split('/').length - 1, resolution_action: this.getResolutionAction(errorType), resolution_successful: false, resolution_time_ms: 0 }); // Attempt to resolve the error this.resolveNavigationError(event, errorType); } /** * Handle navigation cancel * @param event - NavigationCancel event */ private handleNavigationCancel(event: NavigationCancel): void { const navigationTime = this.navigationStartTime ? Date.now() - this.navigationStartTime : 0; if (!environment.production) { console.log('Navigation cancelled:', event.reason, 'URL:', event.url); } // Track navigation cancellation this.gaSvc.trackEvent('navigation_cancelled', { error_type: 'navigation_cancelled', error_message: event.reason || 'Navigation was cancelled', error_code: 'NavigationCancel', attempted_url: event.url, source_url: this.previousUrl, navigation_method: 'router_link', error_timestamp: new Date().toISOString(), navigation_timing_ms: navigationTime, is_authenticated: !!(this.authSvc.user && this.authSvc.byPUserId), user_permissions: this.getUserPermissions(), session_duration_ms: this.pageStartTime ? Date.now() - this.pageStartTime : 0, previous_successful_navigation: this.previousUrl, user_id: this.authSvc.byPUserId, user_role: this.getUserRole(), browser_info: navigator.userAgent, device_type: this.getDeviceType(), route_depth: event.url.split('/').length - 1, resolution_action: 'none', resolution_successful: false, resolution_time_ms: 0 }); } /** * Get user role from user model using shared analytics helpers */ private getUserRole(): string { if (!this.authSvc.user?.roles) { return 'anonymous'; } // Use shared analytics helper through base component convenience method return this.getAnalyticsUserRole(); } /** * Get user permissions from user model */ private getUserPermissions(): string[] { if (!this.authSvc.user?.roles) { return []; } const permissions: string[] = []; const roles = this.authSvc.user.roles; // Map roles to permissions if (roles.admin) permissions.push('admin', 'full_access'); if (roles.officer) permissions.push('officer', 'job_management', 'financial_access'); if (roles.pilot) permissions.push('pilot', 'job_execution', 'tracking_access'); if (roles.applicator) permissions.push('applicator', 'job_execution', 'tracking_access'); if (roles.client) permissions.push('client', 'job_creation', 'report_access'); if (roles.inspector) permissions.push('inspector', 'report_access'); if (roles.aircraft) permissions.push('aircraft', 'data_upload'); return permissions; } /** * Determine device type based on screen size and user agent */ private getDeviceType(): 'desktop' | 'mobile' | 'tablet' { const userAgent = navigator.userAgent; if (/tablet|ipad|playbook|silk/i.test(userAgent)) { return 'tablet'; } if (/mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(userAgent)) { return 'mobile'; } return 'desktop'; } /** * Determine connection type based on Network Information API */ private getConnectionType(): 'wifi' | 'cellular' | 'ethernet' | 'unknown' { // Check if Network Information API is available if ('connection' in navigator) { const connection = (navigator as any).connection; const effectiveType = connection?.effectiveType; // Map effective connection types to our categories if (effectiveType === 'slow-2g' || effectiveType === '2g' || effectiveType === '3g') { return 'cellular'; } if (effectiveType === '4g') { return 'cellular'; } // Check connection type if available const type = connection?.type; if (type === 'wifi') return 'wifi'; if (type === 'ethernet') return 'ethernet'; if (type === 'cellular') return 'cellular'; } return 'unknown'; } /** * Determine resolution action based on error type */ private getResolutionAction(errorType: string): 'redirect_to_home' | 'redirect_to_login' | 'show_error_page' | 'retry_navigation' | 'none' { switch (errorType) { case 'route_not_found': return 'redirect_to_home'; case 'guard_rejected': case 'permission_denied': return 'redirect_to_login'; case 'resolver_error': case 'timeout': case 'network_error': return 'retry_navigation'; default: return 'show_error_page'; } } /** * Attempt to resolve navigation errors */ private resolveNavigationError(event: NavigationError, errorType: string): void { const resolutionStartTime = Date.now(); const action = this.getResolutionAction(errorType); switch (action) { case 'redirect_to_home': this.router.navigate(['/']).then(success => { this.trackResolutionResult(event, action, success, resolutionStartTime); }); break; case 'redirect_to_login': this.router.navigate(['/login']).then(success => { this.trackResolutionResult(event, action, success, resolutionStartTime); }); break; case 'retry_navigation': // Retry the original navigation after a brief delay setTimeout(() => { this.router.navigate([event.url]).then(success => { this.trackResolutionResult(event, action, success, resolutionStartTime); }); }, 1000); break; default: this.trackResolutionResult(event, action, false, resolutionStartTime); break; } } /** * Track the result of navigation error resolution */ private trackResolutionResult(event: NavigationError, action: string, success: boolean, startTime: number): void { const resolutionTime = Date.now() - startTime; // Update the original navigation error event with resolution results this.gaSvc.trackEvent('navigation_error', { error_type: 'navigation_cancelled', error_message: event.error?.message || 'Navigation error resolved', error_code: event.error?.name || 'NavigationError', attempted_url: event.url, source_url: this.previousUrl, navigation_method: 'router_link', error_timestamp: new Date().toISOString(), is_authenticated: !!(this.authSvc.user && this.authSvc.byPUserId), user_id: this.authSvc.byPUserId, user_role: this.getUserRole(), resolution_action: action as any, resolution_successful: success, resolution_time_ms: resolutionTime }); } /** * Track session start event */ private trackSessionStart(): void { // Get current route for entry page const entryPage = this.router.url || '/'; // Track session start with required parameters this.gaSvc.trackEvent('session_start', { platform: 'web', user_role: this.getUserRole(), entry_page: entryPage, referrer: document.referrer || undefined, session_id: this.generateSessionId(), user_id: this.authSvc.byPUserId }); } /** * Generate a unique session ID */ private generateSessionId(): string { return 'sess_' + Date.now().toString(36) + Math.random().toString(36).substr(2); } ngOnDestroy() { } }