Skip to content

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

Additional Resources