483 lines
16 KiB
TypeScript
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() {
|
|
}
|
|
}
|