agmission/Development/client/src/app/app.component.ts

483 lines
16 KiB
TypeScript

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() {
}
}