Full Trust European Hosting

BLOG about Full Trust Hosting and Its Technology - Dedicated to European Windows Hosting Customer

AngularJS Hosting Europe - HostForLIFE :: Product Administration (Angular)

clock January 22, 2026 09:16 by author Peter

The goal of this module is to develop full product administration capability, which includes smooth connection between the frontend interface and backend APIs as well as full CRUD activities. It guarantees that users may effectively create, update, view, and remove products while preserving a responsive and seamless system experience.

This system will allow your administrators to:

  • Manage products

Communicate with the backend APIs you've already built

This lecture covers everything in one place:

  • Auth Guard
  • Full CRUD page for Product
  • Product Service
  • Product Route
  • Update App Routes
  • Update Menu

1. - Auth Guard
Path : "src/app/services/authguard/auth-guard.service.ts"

auth-guard.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthApiService } from '../api/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuardService {
  constructor(
    private authService: AuthApiService,
    private router: Router
  ) {}

  canActivate(): boolean {
    const token = this.authService.getToken();
    const refreshToken = this.authService.getRefreshToken();

    // If no token and no refresh token, redirect to login
    if (!token && !refreshToken) {
      this.router.navigate(['/auth/login']);
      return false;
    }

    // If no token but refresh token exists, try to refresh
    if (!token && refreshToken) {
      this.attemptTokenRefresh(refreshToken);
      return false; // Wait for refresh attempt
    }

    // If token exists, user is authenticated
    if (token) {
      return true;
    }

    return false;
  }

  private attemptTokenRefresh(refreshToken: string): void {
    this.authService.refresh({ token: '', refreshToken }).subscribe({
      next: () => {
        // Token refreshed successfully, reload current route
        this.router.navigate([this.router.url]);
      },
      error: () => {
        // Refresh failed, redirect to login
        this.authService.logout();
        this.router.navigate(['/auth/login']);
      }
    });
  }
}


2. - UI Pages using PrimeNG
Create 3 Files for Product
Path : "src/app/features/products/product/product.html"
"src/app/features/products/product/product.scss" (blank file)
"src/app/features/products/product/product.ts"


product.html

<p-toolbar styleClass="mb-6">
    <ng-template #start>
        <p-button label="New" icon="pi pi-plus" severity="secondary" class="mr-2" (onClick)="openNew()" />
        <p-button severity="secondary" label="Delete" icon="pi pi-trash" outlined (onClick)="deleteSelectedProducts()" [disabled]="!selectedProducts || !selectedProducts.length" />
    </ng-template>

    <ng-template #end>
        <p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" />
    </ng-template>
</p-toolbar>

<p-table
    #dt
    [value]="products()"
    [rows]="10"
    [columns]="cols"
    [paginator]="true"
    [globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
    [tableStyle]="{ 'min-width': '75rem' }"
    [(selection)]="selectedProducts"
    [rowHover]="true"
    dataKey="id"
    currentPageReportTemplate="Showing {first} to {last} of {totalRecords} products"
    [showCurrentPageReport]="true"
    [rowsPerPageOptions]="[10, 20, 30]"
>
    <ng-template #caption>
        <div class="flex items-center justify-between">
            <h5 class="m-0">Manage Products</h5>
            <p-iconfield>
                <p-inputicon styleClass="pi pi-search" />
                <input pInputText type="text" (input)="onGlobalFilter(dt, $event)" placeholder="Search..." />
            </p-iconfield>
        </div>
    </ng-template>
    <ng-template #header>
        <tr>
            <th style="width: 3rem">
                <p-tableHeaderCheckbox />
            </th>
            <th style="min-width: 8rem">ID</th>
            <th pSortableColumn="name" style="min-width:16rem">
                Name
                <p-sortIcon field="name" />
            </th>
            <th pSortableColumn="description" style="min-width:16rem">
                Description
                <p-sortIcon field="description" />
            </th>
            <th pSortableColumn="price" style="min-width: 8rem">
                Price
                <p-sortIcon field="price" />
            </th>
            <th pSortableColumn="stock" style="min-width: 8rem">
                Stock
                <p-sortIcon field="stock" />
            </th>
            <th style="min-width: 12rem"></th>
        </tr>
    </ng-template>
    <ng-template #body let-product>
        <tr>
            <td style="width: 3rem">
                <p-tableCheckbox [value]="product" />
            </td>
            <td style="min-width: 8rem">{{ product.id }}</td>
            <td style="min-width: 16rem">{{ product.name }}</td>
            <td style="min-width: 16rem">{{ product.description }}</td>
            <td>{{ product.price | currency: 'USD' }}</td>
            <td>{{ product.stock }}</td>
            <td>
                <p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editProduct(product)" />
                <p-button icon="pi pi-trash" severity="danger" [rounded]="true" [outlined]="true" (click)="deleteProduct(product)" />
            </td>
        </tr>
    </ng-template>
</p-table>

<p-dialog [(visible)]="productDialog" [style]="{ width: '450px' }" header="Product Details" [modal]="true">
    <ng-template #content>
        <div class="flex flex-col gap-6">
            <div>
                <label for="name" class="block font-bold mb-3">Name</label>
                <input type="text" pInputText id="name" [(ngModel)]="product.name" required autofocus fluid />
                <small class="text-red-500" *ngIf="submitted && !product.name">Name is required.</small>
            </div>
            <div>
                <label for="description" class="block font-bold mb-3">Description</label>
                <textarea id="description" pTextarea [(ngModel)]="product.description" required rows="3" cols="20" fluid></textarea>
            </div>
            <div class="grid grid-cols-12 gap-4">
                <div class="col-span-6">
                    <label for="price" class="block font-bold mb-3">Price</label>
                    <p-inputnumber id="price" [(ngModel)]="product.price" mode="currency" currency="USD" locale="en-US" fluid />
                </div>
                <div class="col-span-6">
                    <label for="stock" class="block font-bold mb-3">Stock</label>
                    <p-inputnumber id="stock" [(ngModel)]="product.stock" fluid />
                </div>
            </div>
        </div>
    </ng-template>

    <ng-template #footer>
        <p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
        <p-button label="Save" icon="pi pi-check" (click)="saveProduct()" />
    </ng-template>
</p-dialog>

<p-confirmdialog [style]="{ width: '450px' }" />

product.scss
product.ts

import { Component, OnInit, signal, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { Table, TableModule } from 'primeng/table';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { RippleModule } from 'primeng/ripple';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { RatingModule } from 'primeng/rating';
import { InputTextModule } from 'primeng/inputtext';
import { TextareaModule } from 'primeng/textarea';
import { SelectModule } from 'primeng/select';
import { RadioButtonModule } from 'primeng/radiobutton';
import { InputNumberModule } from 'primeng/inputnumber';
import { DialogModule } from 'primeng/dialog';
import { TagModule } from 'primeng/tag';
import { InputIconModule } from 'primeng/inputicon';
import { IconFieldModule } from 'primeng/iconfield';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ProductDto, ProductApiService } from '../../../services/api/product.service';

interface Column {
    field: string;
    header: string;
    customExportHeader?: string;
}

interface ExportColumn {
    title: string;
    dataKey: string;
}

@Component({
  selector: 'app-product',
  standalone: true,
  imports: [
    CommonModule,
    TableModule,
    FormsModule,
    ButtonModule,
    RippleModule,
    ToastModule,
    ToolbarModule,
    RatingModule,
    InputTextModule,
    TextareaModule,
    SelectModule,
    RadioButtonModule,
    InputNumberModule,
    DialogModule,
    TagModule,
    InputIconModule,
    IconFieldModule,
    ConfirmDialogModule
  ],
  templateUrl: './product.html',
  styleUrl: './product.scss',
    providers: [MessageService, ConfirmationService]
})
export class Product implements OnInit {
    productDialog: boolean = false;
    products = signal<ProductDto[]>([]);
    product!: ProductDto;
    selectedProducts!: ProductDto[] | null;
    submitted: boolean = false;
    @ViewChild('dt') dt!: Table;
    exportColumns!: ExportColumn[];
    cols!: Column[];

    constructor(
        private productApi: ProductApiService,
        private messageService: MessageService,
        private confirmationService: ConfirmationService
    ) {}

    exportCSV() {
        this.dt.exportCSV();
    }

    ngOnInit() {
        this.loadProducts();
        this.cols = [
            { field: 'id', header: 'ID' },
            { field: 'name', header: 'Name' },
            { field: 'description', header: 'Description' },
            { field: 'price', header: 'Price' },
            { field: 'stock', header: 'Stock' }
        ];
        this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
    }

    loadProducts() {
        this.productApi.list().subscribe((data) => {
            this.products.set(data);
        });
    }

    onGlobalFilter(table: Table, event: Event) {
        table.filterGlobal((event.target as HTMLInputElement).value, 'contains');
    }

    openNew() {
        // Do not set `id` (omit it) so backend can accept/create without GUID parsing errors
        this.product = { name: '', description: '', price: 0, stock: 0 } as ProductDto;
        this.submitted = false;
        this.productDialog = true;
    }

    editProduct(product: ProductDto) {
        this.product = { ...product };
        this.productDialog = true;
    }

    deleteSelectedProducts() {
        this.confirmationService.confirm({
            message: 'Are you sure you want to delete the selected products?',
            header: 'Confirm',
            icon: 'pi pi-exclamation-triangle',
            accept: () => {
                if (this.selectedProducts) {
                    this.selectedProducts.forEach(product => {
                        if (product.id) {
                            this.productApi.delete(product.id).subscribe(() => {
                                this.loadProducts();
                            });
                        }
                    });
                    this.selectedProducts = null;
                    this.messageService.add({
                        severity: 'success',
                        summary: 'Successful',
                        detail: 'Products Deleted',
                        life: 3000
                    });
                }
            }
        });
    }

    hideDialog() {
        this.productDialog = false;
        this.submitted = false;
    }

    deleteProduct(product: ProductDto) {
        this.confirmationService.confirm({
            message: 'Are you sure you want to delete ' + product.name + '?',
            header: 'Confirm',
            icon: 'pi pi-exclamation-triangle',
            accept: () => {
                if (product.id) {
                    this.productApi.delete(product.id).subscribe(() => {
                        this.loadProducts();
                        this.messageService.add({
                            severity: 'success',
                            summary: 'Successful',
                            detail: 'Product Deleted',
                            life: 3000
                        });
                    });
                }
            }
        });
    }

    findIndexById(id: string): number {
        let index = -1;
        for (let i = 0; i < this.products().length; i++) {
            if (this.products()[i].id === id) {
                index = i;
                break;
            }
        }

        return index;
    }

    createId(): string {
        let id = '';
        var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        for (var i = 0; i < 5; i++) {
            id += chars.charAt(Math.floor(Math.random() * chars.length));
        }
        return id;
    }

    getSeverity(status: string) {
        switch (status) {
            case 'INSTOCK':
                return 'success';
            case 'LOWSTOCK':
                return 'warn';
            case 'OUTOFSTOCK':
                return 'danger';
            default:
                return 'info';
        }
    }

    saveProduct() {
        this.submitted = true;
        if (this.product.name?.trim()) {
            if (this.product.id) {
                this.productApi.update(this.product.id, this.product).subscribe(() => {
                    this.loadProducts();
                    this.messageService.add({
                        severity: 'success',
                        summary: 'Successful',
                        detail: 'Product Updated',
                        life: 3000
                    });
                });
            } else {
                this.productApi.create(this.product).subscribe((created) => {
                    this.loadProducts();
                    this.messageService.add({
                        severity: 'success',
                        summary: 'Successful',
                        detail: 'Product Created',
                        life: 3000
                    });
                });
            }
            this.productDialog = false;
        }
    }
}

3. Product Services
Create Product Service
Path : "/src/app/services/api/product.service.ts"

product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// Try to read API base url from environment; fallback to '/api' when not set
import { environment } from '../../../environments/environment';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// Match backend ProductDto
export interface ProductDto {
  id?: string;
  name: string;
  description: string;
  price: number;
  stock: number;
}

interface ApiListResponse<T> {
  fromCache: boolean;
  data: T;
}

@Injectable({
  providedIn: 'root'
})
export class ProductApiService {
  private baseUrl = environment.baseUrl + '/products';

  constructor(private http: HttpClient) {}

  // Returns product[] (unwrapped from { fromCache, data })
  list(): Observable<ProductDto[]> {
    return this.http
      .get<ApiListResponse<ProductDto[]>>(this.baseUrl)
      .pipe(map((res) => res.data));
  }

  // Returns single product (unwrapped)
  get(id: string): Observable<ProductDto> {
    return this.http
      .get<ApiListResponse<ProductDto>>(`${this.baseUrl}/${id}`)
      .pipe(map((res) => res.data));
  }

  // Create returns created product (controller returns dto in body)
  create(product: ProductDto): Observable<ProductDto> {
    return this.http.post<ProductDto>(this.baseUrl, product);
  }

  // Update returns no content; use void
  update(id: string, product: ProductDto): Observable<void> {
    return this.http.put<void>(`${this.baseUrl}/${id}`, product);
  }

  delete(id: string): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`);
  }
}

4. Product Route
Create New File for Product Ruting
Path : "src/app/features/products/products.routes.ts"
import { Routes } from '@angular/router';
import { Product } from './product/product';

export default [
    { path: '', component: Product }
] as Routes;

4. Update App Routes
Path : "src/app/app.routes.ts"
import { Routes } from '@angular/router';
import { AppLayout } from './app/layout/component/app.layout';
import { Dashboard } from './app/pages/dashboard/dashboard';
import { Documentation } from './app/pages/documentation/documentation';
import { Landing } from './app/pages/landing/landing';
import { Notfound } from './app/pages/notfound/notfound';
import { AuthGuardService } from './app/services/authguard/auth-guard.service';

export const appRoutes: Routes = [
    {
        path: '',
        redirectTo: () => {
            const token = localStorage.getItem('auth_token');
            return token ? '/dashboard' : '/auth/login';
        },
        pathMatch: 'full'
    },
    {
        path: 'dashboard',
        component: AppLayout,
        canActivate: [AuthGuardService],
        children: [
            { path: '', component: Dashboard }
        ]
    },
    {
        path: 'products',
        component: AppLayout,
        canActivate: [AuthGuardService],
        children: [
            { path: '', loadChildren: () => import('./app/features/products/products.routes') }
        ]
    },
    {
        path: 'uikit',
        component: AppLayout,
        canActivate: [AuthGuardService],
        children: [
            { path: '', loadChildren: () => import('./app/pages/uikit/uikit.routes') }
        ]
    },
    {
        path: 'pages',
        component: AppLayout,
        canActivate: [AuthGuardService],
        children: [
            { path: '', loadChildren: () => import('./app/pages/pages.routes') }
        ]
    },
    {
        path: 'documentation',
        component: AppLayout,
        canActivate: [AuthGuardService],
        children: [
            { path: '', component: Documentation }
        ]
    },
    { path: 'landing', component: Landing },
    { path: 'notfound', component: Notfound },
    { path: 'auth', loadChildren: () => import('./app/features/auth/auth.routes') },
    { path: '**', component: Notfound }
];


5. Update App Menu
Path : "src/app/layout/component/app.menu.ts"
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { AppMenuitem } from './app.menuitem';

@Component({
    selector: 'app-menu',
    standalone: true,
    imports: [CommonModule, AppMenuitem, RouterModule],
    template: `<ul class="layout-menu">
        <ng-container *ngFor="let item of model; let i = index">
            <li app-menuitem *ngIf="!item.separator" [item]="item" [index]="i" [root]="true"></li>
            <li *ngIf="item.separator" class="menu-separator"></li>
        </ng-container>
    </ul> `
})
export class AppMenu {
    model: MenuItem[] = [];

    ngOnInit() {
        this.model = [
            {
                label: 'Home',
                items: [{ label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: ['/dashboard'] }]
            },
            {
                label: 'Products',
                items: [{ label: 'Products', icon: 'pi pi-fw pi-home', routerLink: ['/products'] }]
            },
            {
                label: 'UI Components',
                items: [
                    { label: 'Form Layout', icon: 'pi pi-fw pi-id-card', routerLink: ['/uikit/formlayout'] },
                    { label: 'Input', icon: 'pi pi-fw pi-check-square', routerLink: ['/uikit/input'] },
                    { label: 'Button', icon: 'pi pi-fw pi-mobile', class: 'rotated-icon', routerLink: ['/uikit/button'] },
                    { label: 'Table', icon: 'pi pi-fw pi-table', routerLink: ['/uikit/table'] },
                    { label: 'List', icon: 'pi pi-fw pi-list', routerLink: ['/uikit/list'] },
                    { label: 'Tree', icon: 'pi pi-fw pi-share-alt', routerLink: ['/uikit/tree'] },
                    { label: 'Panel', icon: 'pi pi-fw pi-tablet', routerLink: ['/uikit/panel'] },
                    { label: 'Overlay', icon: 'pi pi-fw pi-clone', routerLink: ['/uikit/overlay'] },
                    { label: 'Media', icon: 'pi pi-fw pi-image', routerLink: ['/uikit/media'] },
                    { label: 'Menu', icon: 'pi pi-fw pi-bars', routerLink: ['/uikit/menu'] },
                    { label: 'Message', icon: 'pi pi-fw pi-comment', routerLink: ['/uikit/message'] },
                    { label: 'File', icon: 'pi pi-fw pi-file', routerLink: ['/uikit/file'] },
                    { label: 'Chart', icon: 'pi pi-fw pi-chart-bar', routerLink: ['/uikit/charts'] },
                    { label: 'Timeline', icon: 'pi pi-fw pi-calendar', routerLink: ['/uikit/timeline'] },
                    { label: 'Misc', icon: 'pi pi-fw pi-circle', routerLink: ['/uikit/misc'] }
                ]
            },
            {
                label: 'Pages',
                icon: 'pi pi-fw pi-briefcase',
                routerLink: ['/pages'],
                items: [
                    {
                        label: 'Landing',
                        icon: 'pi pi-fw pi-globe',
                        routerLink: ['/landing']
                    },
                    {
                        label: 'Auth',
                        icon: 'pi pi-fw pi-user',
                        items: [
                            {
                                label: 'Login',
                                icon: 'pi pi-fw pi-sign-in',
                                routerLink: ['/auth/login']
                            },
                            {
                                label: 'Error',
                                icon: 'pi pi-fw pi-times-circle',
                                routerLink: ['/auth/error']
                            },
                            {
                                label: 'Access Denied',
                                icon: 'pi pi-fw pi-lock',
                                routerLink: ['/auth/access']
                            }
                        ]
                    },
                    {
                        label: 'Crud',
                        icon: 'pi pi-fw pi-pencil',
                        routerLink: ['/pages/crud']
                    },
                    {
                        label: 'Not Found',
                        icon: 'pi pi-fw pi-exclamation-circle',
                        routerLink: ['/pages/notfound']
                    },
                    {
                        label: 'Empty',
                        icon: 'pi pi-fw pi-circle-off',
                        routerLink: ['/pages/empty']
                    }
                ]
            },
            {
                label: 'Hierarchy',
                items: [
                    {
                        label: 'Submenu 1',
                        icon: 'pi pi-fw pi-bookmark',
                        items: [
                            {
                                label: 'Submenu 1.1',
                                icon: 'pi pi-fw pi-bookmark',
                                items: [
                                    { label: 'Submenu 1.1.1', icon: 'pi pi-fw pi-bookmark' },
                                    { label: 'Submenu 1.1.2', icon: 'pi pi-fw pi-bookmark' },
                                    { label: 'Submenu 1.1.3', icon: 'pi pi-fw pi-bookmark' }
                                ]
                            },
                            {
                                label: 'Submenu 1.2',
                                icon: 'pi pi-fw pi-bookmark',
                                items: [{ label: 'Submenu 1.2.1', icon: 'pi pi-fw pi-bookmark' }]
                            }
                        ]
                    },
                    {
                        label: 'Submenu 2',
                        icon: 'pi pi-fw pi-bookmark',
                        items: [
                            {
                                label: 'Submenu 2.1',
                                icon: 'pi pi-fw pi-bookmark',
                                items: [
                                    { label: 'Submenu 2.1.1', icon: 'pi pi-fw pi-bookmark' },
                                    { label: 'Submenu 2.1.2', icon: 'pi pi-fw pi-bookmark' }
                                ]
                            },
                            {
                                label: 'Submenu 2.2',
                                icon: 'pi pi-fw pi-bookmark',
                                items: [{ label: 'Submenu 2.2.1', icon: 'pi pi-fw pi-bookmark' }]
                            }
                        ]
                    }
                ]
            },
            {
                label: 'Get Started',
                items: [
                    {
                        label: 'Documentation',
                        icon: 'pi pi-fw pi-book',
                        routerLink: ['/documentation']
                    },
                    {
                        label: 'View Source',
                        icon: 'pi pi-fw pi-github',
                        url: 'https://github.com/primefaces/sakai-ng',
                        target: '_blank'
                    }
                ]
            }
        ];
    }
}

 



European Visual Studio 2022 Hosting - HostForLIFE.eu :: Visual Studio Tips That Save Hours

clock January 19, 2026 07:04 by author Peter

1. Ctrl + T (Go To Anything)
Forget Solution Explorer.

  • Instantly jump to files, classes, methods, and symbols
  • Works even with partial names

This shortcut alone can save hours every week.

2. Code Cleanup (Auto-Fix Your Code)
Instead of manually fixing warnings:
Right-click → Code Cleanup

Or configure rules using EditorConfig

You get:

  • Sorted using statements
  • Fixed formatting
  • Removed unused code

Pro move: bind Code Cleanup to a keyboard shortcut.

3. Multi-Cursor Editing (Alt + Click)
Edit multiple lines at once:

  • Rename similar variables
  • Add logging across multiple methods
  • Modify repetitive code quickly

This is much faster than regex-based search and replace.

4. Live Templates and Snippets
Type shortcuts such as:
prop → auto property
ctor → constructor
for, foreach, try, cw

You can also create custom snippets for boilerplate code you repeat frequently.

5. IntelliCode (AI-Powered Suggestions)

Visual Studio learns from:
Your code
Open-source projects

It then:
Ranks the most likely methods first

Reduces scrolling through IntelliSense lists

Ensure IntelliCode is enabled in settings.

6. Debug Faster with Conditional Breakpoints
Right-click a breakpoint and add conditions.
Examples:

  • userId == 0
  • items.Count > 100

This helps avoid stepping through unnecessary iterations, especially in loops.

7. Immediate Window
While debugging:
Press Ctrl + Alt + I

Run code live:

  • myObject.Property
  • DateTime.Now

No recompiling, no logging, just instant answers.

8. Hot Reload
Change code while the application is running.

  • Works with ASP.NET, Blazor, and minimal APIs
  • Ideal for UI tweaks and logic fixes

Especially useful during front-end tuning.

9. Track Changes with CodeLens
CodeLens shows:

  • Who changed the code
  • How many references exist
  • Linked work items

This is especially helpful in large teams or legacy projects.

10. Peek Definition (Alt + F12)
View method definitions inline without leaving your current file.

This keeps your mental context intact and avoids opening unnecessary tabs.

11. Solution Explorer Search
Type directly inside Solution Explorer:

  • Instantly filters files
  • Supports partial matches

Combine this with Ctrl + T for advanced navigation.

12. Task List for TODOs
Add comments such as:
// TODO: Refactor this logic
// HACK: Temporary fix

View them under View → Task List.

This is useful for tracking technical debt.

13. Built-In Performance Analysis

Use the Performance Profiler to analyze:

  • CPU usage
  • Memory leaks
  • Asynchronous bottlenecks

External tools are not always required.

14. Prefer Keyboard Shortcuts Over Mouse

Useful shortcuts to memorize:

  • Ctrl + . → Quick fixes
  • Ctrl + K + D → Format document
  • Ctrl + Shift + B → Build solution
  • F12 → Go to definition

Small habits like these deliver significant productivity gains.



AngularJS Hosting Europe - HostForLIFE :: Connect Angular to New Relic for Error Tracking, API Tracing, and SPA Monitoring

clock January 8, 2026 07:44 by author Peter

For Angular Single Page Applications (SPAs), New Relic Browser Monitoring offers complete observability. Using the @newrelic/browser-agent NPM package, which offers a more manageable method than conventional script-based implementations, this article shows how to incorporate New Relic into an Angular application.

Before starting, ensure you have:

  • Angular Application (Angular 15+ recommended)
  • New Relic Account with Browser Monitoring enabled
  • New Relic Credentials:
  • Account ID
  • License Key (Browser monitoring license key)
  • Application ID
  • Agent ID
  • Trust Key

You can find these credentials in your New Relic account:
Go to Account Settings → API keys → Browser monitoring

Why Use NPM Package Instead of Script Tag?
Advantages:

  • TypeScript support with full type safety
  • Environment-based configuration management
  • Better integration with Angular's build system
  • Custom service layer for easier usage
  • Framework-specific features (SPA route tracking, HTTP interception)
  • Version control through package. json

Installation
Install the New Relic Browser Agent Package
npm install @newrelic/browse

Configuration
Add New Relic Configuration to Environment Files
export const environment = {
  newRelic: {
    enabled: true,
    accountID: 'YOUR_ACCOUNT_ID',
    trustKey: 'YOUR_TRUST_KEY',
    agentID: 'YOUR_AGENT_ID',
    licenseKey: 'YOUR_LICENSE_KEY',
    applicationID: 'YOUR_APPLICATION_ID'
  }
};


Create separate configurations for different environments (development, staging, production) with appropriate credentials.
Implementation

Step 1: Initialize New Relic in main.ts

The New Relic agent should be initialized after Angular bootstraps.
import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
import {NewRelicHttpInterceptor} from './app/global/services/newrelic-handler/newrelic-http.interceptor';

// ... other imports and providers

if (environment.production) {
  enableProdMode();
}

// Bootstrap Angular first
bootstrapApplication(AppComponent, {
  providers : [
    // ... your other providers
    provideHttpClient(withInterceptorsFromDi()),

    // NEW RELIC: Register HTTP Interceptor
    {
      provide : HTTP_INTERCEPTORS,
      useClass : NewRelicHttpInterceptor,
      multi : true
    }
  ]
})
    .then(() => {
      if (environment.newRelic?.enabled) {
        setTimeout(() => {
          try {
            // Capture native console functions before agent patches
            const nativeWarn = console.warn.bind(console);
            const nativeError = console.error.bind(console);

            // Initialize New Relic Browser Agent
            new BrowserAgent( {
              init : {
                distributed_tracing : { enabled : true },
                privacy : { cookies_enabled : true },
                ajax : { deny_list : [], enabled : true },
                session_trace : { enabled : true },
                session_replay : {
                  enabled : true,
                  sampling_rate : 10,
                  error_sampling_rate : 100
                },
                jserrors : {
                  enabled : true,
                  harvestConsoleErrors : false  // Don't capture console.error
                } as any,
                logging : {
                  enabled : true,
                  harvestConsoleErrors : false,
                  harvestConsoleWarns : false,
                  harvestConsoleInfo : false
                } as any,
                metrics : { enabled : true },
                page_action : { enabled : true }
              },
              info : {
                beacon : 'bam.nr-data.net',
                errorBeacon : 'bam.nr-data.net',
                licenseKey : environment.newRelic.licenseKey,
                applicationID : environment.newRelic.applicationID,
                sa : 1
              },
              loader_config : {
                accountID : environment.newRelic.accountID,
                trustKey : environment.newRelic.trustKey,
                agentID : environment.newRelic.agentID,
                licenseKey : environment.newRelic.licenseKey,
                applicationID : environment.newRelic.applicationID
              }
            });

            // Restore native console functions to prevent console logs from
            // being sent to New Relic
            console.warn = nativeWarn;
            console.error = nativeError;

            // Optional: Customize New Relic logging behavior
            const nr : any = (window as any).newrelic;
            if (nr?.log) {
              const originalLog = nr.log.bind(nr);
              nr.log = function(message: string, attributes ?: any) {
                const enhancedAttributes = { ... attributes };
                return originalLog(message, enhancedAttributes);
              };
            }
          } catch (error) {
            console.error('New Relic initialization failed:', error);
          }
        }, 100);
      }
    })
    .catch(err => console.log(err));

  • Initialize after Angular bootstrap to ensure proper timing
  • Capture and restore console functions to prevent console logs from being sent to New Relic
  • Wrap initialization in try-catch for error handling

Step 2: Create New Relic Service Wrapper
Create a service to wrap New Relic functionality: src/app/global/services/newrelic-handler/newrelic.service.ts
import { Injectable } from '@angular/core';
@Injectable({providedIn: 'root'})
export class NewRelicService {
  private isInitialized = false;

  constructor() {
    // Check if New Relic is already initialized (from main.ts)
    if ((window as any).newrelic) {
      this.isInitialized = true;
    }
  }

  /**
   * Report custom error to New Relic
   * @param error - Error object
   * @param customAttributes - Additional attributes to track
   */
  noticeError(error: Error, customAttributes?: Record<string, any>): void {
    if (!this.isReady()) return;

    try {
      const attributes = {
        timestamp: new Date().toISOString(),
        errorName: error.name,
        errorMessage: error.message,
        errorStack: (error as any).originalStack || error.stack,
        userAgent: navigator.userAgent,
        url: window.location.href,
        ...customAttributes
      };
      const nr = (window as any).newrelic;

      if (nr.log) {
        Object.entries(attributes).forEach(([key, value]) => {
          if (value !== undefined && value !== null && typeof value !== 'object') {
            try {
              nr.setCustomAttribute(key, value);
            } catch {}
          }
        });
        nr.log(error.message || 'Error occurred', {
          level: 'ERROR',
          ...attributes
        });
      }
    } catch (e) {
      console.error('New Relic error reporting failed:', e);
    }
  }

  /**
   * Track custom user action/event
   * @param name - Name of the action
   * @param attributes - Custom attributes for the action
   */
  addPageAction(name: string, attributes?: Record<string, any>): void {
    if (!this.isReady()) return;

    try {
      (window as any).newrelic.addPageAction(name, {
        ...attributes,
        timestamp: new Date().toISOString(),
        url: window.location.href,
        userAgent: navigator.userAgent
      });
    } catch (e) {
      console.error('New Relic page action failed:', e);
    }
  }

  /**
   * Set custom attribute for the current session
   * @param name - Attribute name
   * @param value - Attribute value
   */
  setCustomAttribute(name: string, value: string | number | boolean): void {
    if (!this.isReady()) return;

    try {
      (window as any).newrelic.setCustomAttribute(name, value);
    } catch (e) {
      console.error('New Relic custom attribute failed:', e);
    }
  }

  /**
   * Set user ID for tracking
   * @param userId - Unique user identifier
   */
  setUserId(userId: string): void {
    this.setCustomAttribute('userId', userId);
    this.setCustomAttribute('enduser.id', userId);
  }

  /**
   * Set user information
   * @param userInfo - User information object
   */
  setUserInfo(userInfo: { userId: string; email?: string; name?: string; role?: string }): void {
    if (userInfo.userId) this.setUserId(userInfo.userId);
    if (userInfo.email) this.setCustomAttribute('userEmail', userInfo.email);
    if (userInfo.name) this.setCustomAttribute('userName', userInfo.name);
    if (userInfo.role) this.setCustomAttribute('userRole', userInfo.role);
  }

  /**
   * Track page view manually (useful for SPA)
   * @param pageName - Name of the page/route
   */
  setPageViewName(pageName: string): void {
    if (!this.isReady()) return;

    try {
      (window as any).newrelic.setPageViewName(pageName);
    } catch (e) {
      console.error('New Relic page view name failed:', e);
    }
  }

  /**
   * Add release version for tracking
   * @param version - Application version
   */
  setApplicationVersion(version: string): void {
    this.setCustomAttribute('applicationVersion', version);
    this.setCustomAttribute('release', version);
  }

  /**
   * Check if New Relic is initialized and ready with full API
   */
  isReady(): boolean {
    const nr = (window as any).newrelic;
    const isReady =
      !!nr &&
      typeof nr.addPageAction === 'function' &&
      typeof nr.noticeError === 'function';

    if (isReady && !this.isInitialized) {
      this.isInitialized = true;
    }
    return isReady;
  }

  /**
   * Track custom metric
   * @param metricName - Name of the metric
   * @param value - Metric value
   * @param unit - Unit of measurement (default: 'ms')
   */
  trackMetric(metricName: string, value: number, unit: string = 'ms'): void {
    this.addPageAction('CustomMetric', { metricName, value, unit });
  }
}

  • Provides a clean TypeScript interface to New Relic
  • Includes readiness checks before making API calls
  • Handles errors gracefully
  • Adds useful metadata automatically

Step 3: Create Router Tracking Service
For SPAs, tracking route changes is crucial. So, create: src/app/global/services/newrelic-handler/newrelic-router-tracker.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationError, NavigationCancel, Event } from '@angular/router';
import { NewRelicService } from './newrelic.service';
import { filter } from 'rxjs/operators';

@Injectable({  providedIn: 'root'})
export class NewRelicRouterTrackerService {
  private navigationStartTime: number = 0;
  private currentUrl: string = '';
  private previousUrl: string = '';

  constructor(
    private router: Router,
    private newRelicService: NewRelicService
  ) {}

  /**
   * Start tracking Angular router navigation events
   */
  startTracking(): void {
    if (this.newRelicService.isReady()) {
      this.initializeTracking();
    } else {
      // Use polling with exponential backoff to wait for New Relic
      this.waitForNewRelic();
    }
  }

  /**
   * Wait for New Relic to be ready before initializing tracking
   * Uses polling with exponential backoff
   */
  private waitForNewRelic(attempt: number = 1, maxAttempts: number = 10): void {
    // Start with 100ms, increase with each attempt (100, 200, 400, 800, etc.)
    const delay = Math.min(100 * Math.pow(2, attempt - 1), 3000);

    console.log(`Waiting for New Relic to be ready (attempt ${attempt}/${maxAttempts})...`);

    setTimeout(() => {
      if (this.newRelicService.isReady()) {
        console.log('✓ New Relic is now ready, starting router tracking');
        this.initializeTracking();
      } else if (attempt < maxAttempts) {
        this.waitForNewRelic(attempt + 1, maxAttempts);
      } else {
        console.warn(`New Relic not ready after ${maxAttempts} attempts, router tracking disabled`);
      }
    }, delay);
  }

  /**
   * Initialize the actual tracking once New Relic is ready
   */
  private initializeTracking(): void {
    // Track navigation start
    this.router.events.pipe(
      filter((event: Event): event is NavigationStart => event instanceof NavigationStart)
    ).subscribe((event: NavigationStart) => {
      this.navigationStartTime = performance.now();
      this.previousUrl = this.currentUrl;
      this.currentUrl = event.url;

      this.newRelicService.addPageAction('RouteChangeStart', {
        url: event.url,
        previousUrl: this.previousUrl,
        navigationTrigger: event.navigationTrigger,
        restoredState: event.restoredState ? 'yes' : 'no'
      });
    });

    // Track navigation end (success)
    this.router.events.pipe(
      filter((event: Event): event is NavigationEnd => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      const duration = performance.now() - this.navigationStartTime;

      this.newRelicService.addPageAction('RouteChangeComplete', {
        url: event.urlAfterRedirects,
        previousUrl: this.previousUrl,
        duration: Math.round(duration),
        status: 'success'
      });

      // Set page view name for better tracking in New Relic
      const pageName = this.extractPageName(event.urlAfterRedirects);
      this.newRelicService.setPageViewName(pageName);

      // Track as successful route change metric
      this.newRelicService.trackMetric('RouteChangeDuration', duration, 'ms');
    });

    // Track navigation errors
    this.router.events.pipe(
      filter((event: Event): event is NavigationError => event instanceof NavigationError)
    ).subscribe((event: NavigationError) => {
      const duration = performance.now() - this.navigationStartTime;

      this.newRelicService.addPageAction('RouteChangeError', {
        url: event.url,
        previousUrl: this.previousUrl,
        duration: Math.round(duration),
        status: 'error',
        errorMessage: event.error?.message || 'Unknown navigation error'
      });

      // Report as error to New Relic
      const error = new Error(`Navigation Error: ${event.error?.message || 'Unknown error'}`);
      this.newRelicService.noticeError(error, {
        errorType: 'NavigationError',
        url: event.url,
        previousUrl: this.previousUrl
      });
    });

    // Track navigation cancel
    this.router.events.pipe(
      filter((event: Event): event is NavigationCancel => event instanceof NavigationCancel)
    ).subscribe((event: NavigationCancel) => {
      const duration = performance.now() - this.navigationStartTime;

      this.newRelicService.addPageAction('RouteChangeCancel', {
        url: event.url,
        previousUrl: this.previousUrl,
        duration: Math.round(duration),
        status: 'cancelled',
        reason: event.reason
      });
    });

    console.log('✓ New Relic Router Tracking started');
  }

  /**
   * Extract a clean page name from URL
   * @param url - Full URL path
   */
  private extractPageName(url: string): string {
    // Remove query parameters and fragments
    let cleanUrl = url.split('?')[0].split('#')[0];

    // Remove leading slash
    if (cleanUrl.startsWith('/')) {
      cleanUrl = cleanUrl.substring(1);
    }

    // If empty, it's the home page
    if (!cleanUrl) {
      return 'Home';
    }

    // Replace slashes with dots and capitalize
    const pageName = cleanUrl
      .split('/')
      .map(part => part.charAt(0).toUpperCase() + part.slice(1))
      .join('.');

    return pageName;
  }

  /**
   * Track specific route manually
   * @param routeName - Name of the route
   * @param metadata - Additional metadata
   */
  trackRouteManually(routeName: string, metadata?: Record<string, any>): void {
    this.newRelicService.addPageAction('ManualRouteTrack', {
      routeName,
      url: window.location.href,
      ...metadata
    });
  }
}

  • Handles timing issues with exponential backoff polling
  • Tracks all router events (start, end, error, cancel)
  • Measures navigation duration
  • Extracts clean page names for better dashboard organization

Step 4: Create HTTP Interceptor
Track API calls automatically: src/app/global/services/newrelic-handler/newrelic-http.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { NewRelicService } from './newrelic.service';

@Injectable()
export class NewRelicHttpInterceptor implements HttpInterceptor {
  constructor(private newRelicService: NewRelicService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!this.newRelicService.isReady()) {
      return next.handle(req);
    }

    const startTime = performance.now();
    const requestDetails = {
      url: req.url,
      method: req.method,
      urlWithParams: req.urlWithParams
    };

    return next.handle(req).pipe(
      tap(
        event => {
          if (event instanceof HttpResponse) {
            const duration = Math.round(performance.now() - startTime);

            this.newRelicService.addPageAction('APICallSuccess', {
              ...requestDetails,
              statusCode: event.status,
              statusText: event.statusText,
              duration,
              responseType: event.type,
              contentType: event.headers.get('content-type') || 'unknown'
            });

            this.newRelicService.trackMetric(`API_${req.method}_Duration`, duration);
          }
        },
        error => {
          if (error instanceof HttpErrorResponse) {
            const duration = Math.round(performance.now() - startTime);

            this.newRelicService.addPageAction('APICallFailure', {
              ...requestDetails,
              statusCode: error.status,
              statusText: error.statusText,
              errorMessage: error.message,
              errorName: error.name,
              duration
            });

            const errorObj = new Error(`API Error`);

            this.newRelicService.noticeError(errorObj, {
              errorType: 'HttpError',
              httpMethod: req.method,
              apiUrl: req.url,
              apiEndpoint: new URL(req.url).pathname,
              statusCode: error.status,
              statusText: error.statusText,
              errorMessage: error.message,
              serverErrorMessage: error.error?.error || error.error?.message || error.error,
              duration,
              pageUrl: window.location.href,
              pagePath: window.location.pathname,
              timestamp: new Date().toISOString()
            });
          }
        }
      )
    );
  }
}

  • Automatically tracks all HTTP requests
  • Measures API call duration
  • Captures success and failure scenarios
  • Provides detailed error information

Step 5: Initialize Router Tracking in AppComponent
Start router tracking in your root component: src/app/app.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { NewRelicRouterTrackerService } from './global/services/newrelic-handler/newrelic-router-tracker.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private newRelicRouterTracker: NewRelicRouterTrackerService
  ) {}

  ngOnInit() {
    // ... other initialization code

    // Start New Relic router tracking
    this.startNewRelicRouterTracking();
  }

  /**
   * Start New Relic Router Tracking
   */
  private startNewRelicRouterTracking(): void {
    try {
      // Start router tracking for SPA navigation
      this.newRelicRouterTracker.startTracking();
    } catch (error) {
      // Silently fail if New Relic is not available
      console.debug('New Relic router tracking not started:', error);
    }
  }
}

Advanced Features
User Identification
Track user sessions with user information:
import { NewRelicService } from './global/services/newrelic-handler/newrelic.service';

constructor(private newRelicService: NewRelicService) {}

onUserLogin(user: any) {
  this.newRelicService.setUserInfo({
    userId: user.id,
    email: user.email,
    name: user.name,
    role: user.role
  });
}

Custom Events
Track custom business events:
// Track a button click
this.newRelicService.addPageAction('ButtonClick', {
  buttonName: 'SubmitForm',
  formType: 'Contact',
  timestamp: new Date().toISOString()
});

// Track a purchase
this.newRelicService.addPageAction('Purchase', {
  productId: '123',
  amount: 99.99,
  currency: 'USD'
});

Application Version Tracking
Track application versions for release management:
this.newRelicService.setApplicationVersion('1.2.3');

Performance Considerations

  • New Relic data is batched and sent asynchronously
  • The agent has minimal performance impact
  • Use custom attributes sparingly to avoid payload size issues


About HostForLIFE

HostForLIFE is European Windows Hosting Provider which focuses on Windows Platform only. We deliver on-demand hosting solutions including Shared hosting, Reseller Hosting, Cloud Hosting, Dedicated Servers, and IT as a Service for companies of all sizes.

We have offered the latest Windows 2019 Hosting, ASP.NET 5 Hosting, ASP.NET MVC 6 Hosting and SQL 2019 Hosting.


Tag cloud

Sign in