20. Angular Deployment and Production¶
Learning Objectives¶
By the end of this module, you will be able to:
- Prepare Angular applications for production deployment
- Optimize build configurations for different environments
- Implement security best practices for production applications
- Deploy Angular applications to various hosting platforms
- Set up continuous integration and deployment pipelines
- Monitor and maintain production applications
- Troubleshoot common production issues
- Implement progressive web app features for production
Production Build Optimization¶
Build Configuration¶
// angular.json - Production configuration
{
"projects": {
"your-app": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/your-app",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"optimization": true,
"aot": true,
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
"staging": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"outputHashing": "all",
"sourceMap": true,
"namedChunks": true,
"extractLicenses": true,
"vendorChunk": true,
"buildOptimizer": true,
"optimization": true,
"aot": true
}
}
}
}
}
}
}
Custom Webpack Configuration¶
// webpack.config.js - Custom webpack configuration
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = (config, options) => {
// Add custom plugins
config.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
);
// Add bundle analyzer for production analysis
if (process.env.ANALYZE === 'true') {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
);
}
// Optimize chunks
config.optimization = {
...config.optimization,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5
}
}
}
};
// Add source map configuration for production debugging
if (options.configuration === 'production') {
config.devtool = 'source-map';
}
return config;
};
// Build scripts in package.json
{
"scripts": {
"build": "ng build",
"build:prod": "ng build --configuration=production",
"build:staging": "ng build --configuration=staging",
"build:analyze": "ANALYZE=true ng build --configuration=production",
"build:stats": "ng build --configuration=production --stats-json",
"analyze:bundle": "npx webpack-bundle-analyzer dist/stats.json"
}
}
Tree Shaking and Dead Code Elimination¶
// Tree shaking configuration
// tsconfig.json
{
"compilerOptions": {
"module": "es2020",
"moduleResolution": "node",
"target": "es2015",
"lib": ["es2018", "dom"],
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"importHelpers": true,
"noImplicitAny": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"strictPropertyInitialization": false,
"typeRoots": ["node_modules/@types"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
// Optimize imports
// Bad - imports entire library
import * as _ from 'lodash';
// Good - imports only what's needed
import { debounce, throttle } from 'lodash-es';
// Better - use individual packages
import debounce from 'lodash.debounce';
// Utility service for commonly used functions
@Injectable({
providedIn: 'root'
})
export class OptimizedUtilsService {
// Use native APIs when possible
debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = window.setTimeout(() => func.apply(this, args), wait);
};
}
// Optimized array operations
groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
return array.reduce((groups, item) => {
const groupKey = String(item[key]);
groups[groupKey] = groups[groupKey] || [];
groups[groupKey].push(item);
return groups;
}, {} as Record<string, T[]>);
}
// Memory-efficient sorting
sortBy<T>(array: T[], key: keyof T): T[] {
return [...array].sort((a, b) => {
const aVal = a[key];
const bVal = b[key];
if (aVal < bVal) return -1;
if (aVal > bVal) return 1;
return 0;
});
}
}
Environment Configuration¶
Environment Files Setup¶
// src/environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
wsUrl: 'ws://localhost:3000',
enableDevTools: true,
logLevel: 'debug',
features: {
enableAnalytics: false,
enablePushNotifications: false,
enableOfflineMode: false
},
auth: {
clientId: 'dev-client-id',
domain: 'dev.auth0.com'
},
monitoring: {
sentryDsn: '',
enableErrorReporting: false
}
};
// src/environments/environment.staging.ts
export const environment = {
production: false,
apiUrl: 'https://staging-api.yourapp.com/api',
wsUrl: 'wss://staging-api.yourapp.com',
enableDevTools: false,
logLevel: 'info',
features: {
enableAnalytics: true,
enablePushNotifications: true,
enableOfflineMode: true
},
auth: {
clientId: 'staging-client-id',
domain: 'staging.auth0.com'
},
monitoring: {
sentryDsn: 'https://staging-sentry-dsn',
enableErrorReporting: true
}
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.yourapp.com/api',
wsUrl: 'wss://api.yourapp.com',
enableDevTools: false,
logLevel: 'error',
features: {
enableAnalytics: true,
enablePushNotifications: true,
enableOfflineMode: true
},
auth: {
clientId: 'prod-client-id',
domain: 'yourapp.auth0.com'
},
monitoring: {
sentryDsn: 'https://production-sentry-dsn',
enableErrorReporting: true
}
};
// Environment configuration service
@Injectable({
providedIn: 'root'
})
export class ConfigService {
private config = environment;
get apiUrl(): string {
return this.config.apiUrl;
}
get isProduction(): boolean {
return this.config.production;
}
get logLevel(): string {
return this.config.logLevel;
}
isFeatureEnabled(feature: keyof typeof environment.features): boolean {
return this.config.features[feature];
}
getAuthConfig() {
return this.config.auth;
}
getMonitoringConfig() {
return this.config.monitoring;
}
}
Runtime Configuration¶
// Runtime configuration service
@Injectable({
providedIn: 'root'
})
export class RuntimeConfigService {
private config: RuntimeConfig | null = null;
async loadConfig(): Promise<RuntimeConfig> {
if (this.config) {
return this.config;
}
try {
const response = await fetch('/assets/config/config.json');
this.config = await response.json();
return this.config;
} catch (error) {
console.error('Failed to load runtime configuration:', error);
// Fallback to environment config
this.config = {
apiUrl: environment.apiUrl,
features: environment.features
};
return this.config;
}
}
getConfig(): RuntimeConfig | null {
return this.config;
}
}
// App initializer
export function configInitializer(configService: RuntimeConfigService) {
return () => configService.loadConfig();
}
// app.module.ts
@NgModule({
// ...
providers: [
{
provide: APP_INITIALIZER,
useFactory: configInitializer,
deps: [RuntimeConfigService],
multi: true
}
]
})
export class AppModule {}
// assets/config/config.json (for different environments)
{
"apiUrl": "https://api.yourapp.com/api",
"features": {
"enableAnalytics": true,
"enablePushNotifications": true,
"enableOfflineMode": true
},
"maintenance": {
"enabled": false,
"message": "System maintenance in progress"
}
}
interface RuntimeConfig {
apiUrl: string;
features: {
enableAnalytics: boolean;
enablePushNotifications: boolean;
enableOfflineMode: boolean;
};
maintenance?: {
enabled: boolean;
message: string;
};
}
Security Best Practices¶
Content Security Policy (CSP)¶
<!-- index.html - CSP Configuration -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Your App</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Content Security Policy -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.trusted-domain.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.yourapp.com wss://api.yourapp.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
">
<!-- Security headers -->
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
</head>
<body>
<app-root></app-root>
</body>
</html>
Security Service Implementation¶
@Injectable({
providedIn: 'root'
})
export class SecurityService {
constructor(private sanitizer: DomSanitizer) {}
// Sanitize HTML content
sanitizeHtml(html: string): SafeHtml {
return this.sanitizer.sanitize(SecurityContext.HTML, html) || '';
}
// Sanitize URLs
sanitizeUrl(url: string): SafeUrl {
return this.sanitizer.sanitize(SecurityContext.URL, url) || '';
}
// Validate and sanitize user input
sanitizeInput(input: string): string {
if (!input) return '';
// Remove potentially dangerous characters
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+=/gi, '');
}
// Generate secure random tokens
generateSecureToken(length: number = 32): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// Validate file uploads
validateFileUpload(file: File, allowedTypes: string[], maxSize: number): boolean {
// Check file type
if (!allowedTypes.includes(file.type)) {
throw new Error(`File type ${file.type} is not allowed`);
}
// Check file size
if (file.size > maxSize) {
throw new Error(`File size exceeds maximum allowed size of ${maxSize} bytes`);
}
// Additional security checks
if (file.name.includes('..') || file.name.includes('/')) {
throw new Error('Invalid file name');
}
return true;
}
// Secure local storage operations
setSecureItem(key: string, value: any): void {
try {
const encrypted = this.encrypt(JSON.stringify(value));
localStorage.setItem(key, encrypted);
} catch (error) {
console.error('Failed to set secure item:', error);
}
}
getSecureItem<T>(key: string): T | null {
try {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
const decrypted = this.decrypt(encrypted);
return JSON.parse(decrypted);
} catch (error) {
console.error('Failed to get secure item:', error);
return null;
}
}
private encrypt(text: string): string {
// Implement encryption logic (use a proper encryption library)
return btoa(text); // Simple base64 encoding (not secure for production)
}
private decrypt(encryptedText: string): string {
// Implement decryption logic
return atob(encryptedText); // Simple base64 decoding
}
}
// Security interceptor
@Injectable()
export class SecurityInterceptor implements HttpInterceptor {
constructor(private securityService: SecurityService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add security headers
const secureReq = req.clone({
setHeaders: {
'X-Requested-With': 'XMLHttpRequest',
'X-Request-ID': this.securityService.generateSecureToken(16)
}
});
// Validate request data
if (req.body && typeof req.body === 'object') {
this.validateRequestData(req.body);
}
return next.handle(secureReq).pipe(
catchError((error: HttpErrorResponse) => {
// Log security-related errors
if (error.status === 403 || error.status === 401) {
console.warn('Security error detected:', error);
}
return throwError(error);
})
);
}
private validateRequestData(data: any): void {
if (typeof data === 'string') {
if (data.includes('<script>') || data.includes('javascript:')) {
throw new Error('Potential XSS attack detected');
}
}
}
}
Deployment Strategies¶
Static File Hosting (Netlify, Vercel)¶
# Netlify deployment configuration
# netlify.toml
[build]
publish = "dist/your-app"
command = "npm run build:prod"
[build.environment]
NODE_VERSION = "16"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*.js"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/*.css"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/index.html"
[headers.values]
Cache-Control = "public, max-age=0, must-revalidate"
# Vercel deployment configuration
# vercel.json
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"buildCommand": "npm run build:prod",
"outputDirectory": "dist/your-app"
}
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/index.html"
}
],
"headers": [
{
"source": "/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
Docker Deployment¶
# Dockerfile
# Multi-stage build for optimized production image
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build:prod
# Production stage
FROM nginx:alpine
# Copy built application
COPY --from=builder /app/dist/your-app /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Add security headers
COPY nginx-security.conf /etc/nginx/conf.d/security.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
include /etc/nginx/conf.d/security.conf;
# Handle Angular routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy (if needed)
location /api {
proxy_pass http://api-server:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
# nginx-security.conf
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: unless-stopped
# Optional: Redis for caching
redis:
image: redis:alpine
restart: unless-stopped
# Optional: Database
db:
image: postgres:13
environment:
POSTGRES_DB: yourapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
Kubernetes Deployment¶
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: angular-app
labels:
app: angular-app
spec:
replicas: 3
selector:
matchLabels:
app: angular-app
template:
metadata:
labels:
app: angular-app
spec:
containers:
- name: angular-app
image: your-registry/angular-app:latest
ports:
- containerPort: 80
env:
- name: NODE_ENV
value: "production"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: angular-app-service
spec:
selector:
app: angular-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: angular-app-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- yourapp.com
secretName: angular-app-tls
rules:
- host: yourapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: angular-app-service
port:
number: 80
CI/CD Pipeline Setup¶
GitHub Actions¶
# .github/workflows/deploy.yml
name: Deploy Angular App
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '16'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm run test:ci
- name: Run e2e tests
run: npm run e2e:ci
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
build:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
environment: [staging, production]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build:${{ matrix.environment }}
- name: Run bundle analysis
run: npm run analyze:bundle
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.environment }}
path: dist/
retention-days: 30
security-scan:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Run OWASP ZAP security scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'http://localhost:4200'
deploy-staging:
needs: [build, security-scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment:
name: staging
url: https://staging.yourapp.com
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-staging
path: dist/
- name: Deploy to staging
run: |
# Deploy to staging environment
echo "Deploying to staging..."
# Add your deployment commands here
- name: Run staging tests
run: |
# Run smoke tests against staging
npm run test:staging
deploy-production:
needs: [build, security-scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://yourapp.com
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-production
path: dist/
- name: Deploy to production
run: |
# Deploy to production environment
echo "Deploying to production..."
# Add your deployment commands here
- name: Run production smoke tests
run: |
# Run critical smoke tests
npm run test:production
- name: Notify deployment success
uses: 8398a7/action-slack@v3
with:
status: success
text: 'Production deployment successful! 🚀'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
docker-build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Azure DevOps Pipeline¶
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
variables:
- group: angular-app-variables
- name: nodeVersion
value: '16.x'
- name: buildConfiguration
value: 'production'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildAndTest
displayName: 'Build and Test Job'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: $(nodeVersion)
- task: Npm@1
displayName: 'npm install'
inputs:
command: 'install'
- task: Npm@1
displayName: 'npm run lint'
inputs:
command: 'custom'
customCommand: 'run lint'
- task: Npm@1
displayName: 'npm run test'
inputs:
command: 'custom'
customCommand: 'run test:ci'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results.xml'
searchFolder: '$(System.DefaultWorkingDirectory)'
mergeTestResults: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml'
reportDirectory: '$(System.DefaultWorkingDirectory)/coverage'
- task: Npm@1
displayName: 'npm run build'
inputs:
command: 'custom'
customCommand: 'run build:$(buildConfiguration)'
- task: PublishBuildArtifacts@1
displayName: 'Publish build artifacts'
inputs:
pathToPublish: 'dist/'
artifactName: 'angular-app'
- stage: Deploy
displayName: 'Deploy to Azure'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToAzure
displayName: 'Deploy to Azure Static Web Apps'
environment: 'production'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: DownloadBuildArtifacts@0
displayName: 'Download build artifacts'
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'angular-app'
downloadPath: '$(System.ArtifactsDirectory)'
- task: AzureStaticWebApp@0
displayName: 'Deploy to Azure Static Web Apps'
inputs:
app_location: '$(System.ArtifactsDirectory)/angular-app'
azure_static_web_apps_api_token: '$(AZURE_STATIC_WEB_APPS_API_TOKEN)'
Monitoring and Analytics¶
Application Monitoring Setup¶
// Monitoring service
@Injectable({
providedIn: 'root'
})
export class MonitoringService {
private isInitialized = false;
constructor(@Inject(DOCUMENT) private document: Document) {}
async initialize(): Promise<void> {
if (this.isInitialized || !environment.monitoring.enableErrorReporting) {
return;
}
// Initialize Sentry for error tracking
if (environment.monitoring.sentryDsn) {
const Sentry = await import('@sentry/angular');
Sentry.init({
dsn: environment.monitoring.sentryDsn,
environment: environment.production ? 'production' : 'development',
tracesSampleRate: environment.production ? 0.1 : 1.0,
beforeSend: (event) => {
// Filter out certain errors or add context
if (event.exception) {
const error = event.exception.values?.[0];
if (error?.value?.includes('Non-Error promise rejection')) {
return null; // Don't send this type of error
}
}
return event;
}
});
}
// Initialize Google Analytics
if (environment.features.enableAnalytics) {
this.initializeGoogleAnalytics();
}
// Initialize performance monitoring
this.initializePerformanceMonitoring();
this.isInitialized = true;
}
private initializeGoogleAnalytics(): void {
// Load Google Analytics
const gaScript = this.document.createElement('script');
gaScript.async = true;
gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID';
this.document.head.appendChild(gaScript);
// Initialize gtag
(window as any).dataLayer = (window as any).dataLayer || [];
function gtag(...args: any[]) {
(window as any).dataLayer.push(args);
}
(window as any).gtag = gtag;
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID', {
page_title: this.document.title,
page_location: window.location.href
});
}
private initializePerformanceMonitoring(): void {
// Monitor Core Web Vitals
if ('PerformanceObserver' in window) {
// Largest Contentful Paint
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.trackMetric('LCP', entry.startTime);
}
}).observe({ entryTypes: ['largest-contentful-paint'] });
// First Input Delay
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.trackMetric('FID', (entry as any).processingStart - entry.startTime);
}
}).observe({ entryTypes: ['first-input'] });
// Cumulative Layout Shift
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as any).hadRecentInput) {
clsValue += (entry as any).value;
}
}
this.trackMetric('CLS', clsValue);
}).observe({ entryTypes: ['layout-shift'] });
}
}
trackPageView(url: string, title: string): void {
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_title: title,
page_location: url
});
}
}
trackEvent(action: string, category: string, label?: string, value?: number): void {
if (typeof gtag !== 'undefined') {
gtag('event', action, {
event_category: category,
event_label: label,
value: value
});
}
}
trackError(error: Error, context?: any): void {
console.error('Application error:', error, context);
// Send to monitoring service
if (this.isInitialized && environment.monitoring.enableErrorReporting) {
// Error is automatically captured by Sentry
// You can add additional context here
}
}
trackMetric(name: string, value: number, unit: string = 'ms'): void {
if (typeof gtag !== 'undefined') {
gtag('event', 'timing_complete', {
name: name,
value: Math.round(value)
});
}
// Also log to console in development
if (!environment.production) {
console.log(`Performance metric - ${name}: ${value}${unit}`);
}
}
trackUserAction(action: string, element?: string): void {
this.trackEvent(action, 'user_interaction', element);
}
}
// Global error handler
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(private monitoringService: MonitoringService) {}
handleError(error: any): void {
this.monitoringService.trackError(error);
console.error('Global error handler:', error);
}
}
// app.module.ts
@NgModule({
providers: [
{
provide: ErrorHandler,
useClass: GlobalErrorHandler
},
{
provide: APP_INITIALIZER,
useFactory: (monitoringService: MonitoringService) => () => monitoringService.initialize(),
deps: [MonitoringService],
multi: true
}
]
})
export class AppModule {}
Performance Monitoring¶
// Performance monitoring service
@Injectable({
providedIn: 'root'
})
export class PerformanceMonitoringService {
private metrics = new Map<string, PerformanceMetric>();
private observers: PerformanceObserver[] = [];
constructor(private monitoringService: MonitoringService) {
this.initializeObservers();
}
private initializeObservers(): void {
if (!('PerformanceObserver' in window)) {
return;
}
// Monitor navigation timing
const navObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const navEntry = entry as PerformanceNavigationTiming;
this.trackNavigationMetrics(navEntry);
}
});
navObserver.observe({ entryTypes: ['navigation'] });
this.observers.push(navObserver);
// Monitor resource loading
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const resourceEntry = entry as PerformanceResourceTiming;
this.trackResourceMetrics(resourceEntry);
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
this.observers.push(resourceObserver);
// Monitor long tasks
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.trackLongTask(entry);
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
this.observers.push(longTaskObserver);
}
private trackNavigationMetrics(entry: PerformanceNavigationTiming): void {
const metrics = {
'DNS Lookup': entry.domainLookupEnd - entry.domainLookupStart,
'TCP Connection': entry.connectEnd - entry.connectStart,
'Request': entry.responseStart - entry.requestStart,
'Response': entry.responseEnd - entry.responseStart,
'DOM Processing': entry.domComplete - entry.domLoading,
'Total Load Time': entry.loadEventEnd - entry.navigationStart
};
Object.entries(metrics).forEach(([name, value]) => {
this.recordMetric(name, value);
this.monitoringService.trackMetric(name, value);
});
}
private trackResourceMetrics(entry: PerformanceResourceTiming): void {
// Track slow resources
const loadTime = entry.responseEnd - entry.startTime;
if (loadTime > 1000) { // Resources taking more than 1 second
this.monitoringService.trackEvent('slow_resource', 'performance', entry.name, loadTime);
}
// Track resource types
const resourceType = this.getResourceType(entry.name);
this.recordMetric(`${resourceType}_load_time`, loadTime);
}
private trackLongTask(entry: PerformanceEntry): void {
const duration = entry.duration;
this.monitoringService.trackEvent('long_task', 'performance', entry.name, duration);
this.recordMetric('long_task_duration', duration);
}
private getResourceType(url: string): string {
if (url.includes('.js')) return 'javascript';
if (url.includes('.css')) return 'stylesheet';
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return 'image';
if (url.includes('.woff') || url.includes('.ttf')) return 'font';
return 'other';
}
private recordMetric(name: string, value: number): void {
const existing = this.metrics.get(name);
if (existing) {
existing.count++;
existing.sum += value;
existing.average = existing.sum / existing.count;
existing.max = Math.max(existing.max, value);
existing.min = Math.min(existing.min, value);
} else {
this.metrics.set(name, {
name,
count: 1,
sum: value,
average: value,
max: value,
min: value
});
}
}
getMetrics(): PerformanceMetric[] {
return Array.from(this.metrics.values());
}
generatePerformanceReport(): PerformanceReport {
const metrics = this.getMetrics();
const navigationMetrics = metrics.filter(m =>
['DNS Lookup', 'TCP Connection', 'Request', 'Response', 'DOM Processing', 'Total Load Time'].includes(m.name)
);
const resourceMetrics = metrics.filter(m =>
m.name.includes('_load_time') && !navigationMetrics.some(n => n.name === m.name)
);
const performanceIssues = metrics.filter(m =>
(m.name === 'long_task_duration' && m.average > 50) ||
(m.name.includes('_load_time') && m.average > 2000)
);
return {
timestamp: new Date(),
navigationMetrics,
resourceMetrics,
performanceIssues,
overallScore: this.calculatePerformanceScore(metrics)
};
}
private calculatePerformanceScore(metrics: PerformanceMetric[]): number {
let score = 100;
// Penalize slow loading
const totalLoadTime = metrics.find(m => m.name === 'Total Load Time');
if (totalLoadTime) {
if (totalLoadTime.average > 3000) score -= 30;
else if (totalLoadTime.average > 2000) score -= 20;
else if (totalLoadTime.average > 1000) score -= 10;
}
// Penalize long tasks
const longTasks = metrics.find(m => m.name === 'long_task_duration');
if (longTasks && longTasks.count > 0) {
score -= Math.min(longTasks.count * 5, 25);
}
return Math.max(0, score);
}
cleanup(): void {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
this.metrics.clear();
}
}
interface PerformanceMetric {
name: string;
count: number;
sum: number;
average: number;
max: number;
min: number;
}
interface PerformanceReport {
timestamp: Date;
navigationMetrics: PerformanceMetric[];
resourceMetrics: PerformanceMetric[];
performanceIssues: PerformanceMetric[];
overallScore: number;
}
Summary¶
This comprehensive deployment and production module covered all essential aspects of taking Angular applications to production:
- Build Optimization: Production configurations, tree shaking, and bundle analysis
- Environment Management: Multi-environment setup and runtime configuration
- Security: CSP implementation, input sanitization, and security headers
- Deployment: Multiple hosting options from static sites to Kubernetes
- CI/CD: Automated pipelines with testing, security scanning, and deployment
- Monitoring: Error tracking, analytics, and performance monitoring
- Maintenance: Health checks, logging, and troubleshooting strategies
Next Steps¶
- Explore advanced monitoring tools like New Relic or DataDog
- Learn about A/B testing and feature flags
- Study infrastructure as code with Terraform
- Practice disaster recovery and backup strategies
- Investigate micro-frontend architectures