Skip to content

Latest commit

 

History

History
1316 lines (1080 loc) · 34.7 KB

File metadata and controls

1316 lines (1080 loc) · 34.7 KB

Dickson's Developer Assessment - Part 3

Testing Strategy

20. What testing approach would you take for this system?

Comprehensive Testing Pyramid:

                  ┌─────────┐
                  │   E2E   │  (10%) - Full user flows
                  └─────────┘
               ┌──────────────┐
               │ Integration  │  (20%) - API + DB + Cache
               └──────────────┘
          ┌───────────────────────┐
          │    Unit Tests         │  (70%) - Individual functions
          └───────────────────────┘

Test Structure:

tests/
├── Unit/
│   ├── Services/
│   │   ├── FastwayServiceTest.php
│   │   ├── CacheServiceTest.php
│   │   └── ValidationServiceTest.php
│   ├── Models/
│   │   ├── TrackingTest.php
│   │   └── QuoteTest.php
│   └── Utilities/
│       └── InputSanitizerTest.php
├── Integration/
│   ├── API/
│   │   ├── TrackingAPITest.php
│   │   └── QuoteAPITest.php
│   └── Database/
│       └── TrackingRepositoryTest.php
├── Feature/
│   ├── TrackingFeatureTest.php
│   └── QuoteFeatureTest.php
└── E2E/
    ├── TrackParcelTest.php
    └── GetQuoteTest.php

Testing Tools:

// composer.json
{
    "require-dev": {
        "phpunit/phpunit": "^10.0",
        "mockery/mockery": "^1.5",
        "fakerphp/faker": "^1.21",
        "phpstan/phpstan": "^1.10",
        "squizlabs/php_codesniffer": "^3.7"
    }
}

PHPUnit Configuration:

<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
         colors="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">app</directory>
            <directory suffix=".php">api</directory>
        </include>
        <report>
            <html outputDirectory="coverage/html"/>
            <clover outputFile="coverage/clover.xml"/>
        </report>
    </coverage>
</phpunit>

21. How would you test API integrations without calling the real API?

Mock Implementation:

// tests/Mocks/MockFastwayAPI.php
class MockFastwayAPI {
    private $responses = [];
    
    public function setResponse($endpoint, $response) {
        $this->responses[$endpoint] = $response;
    }
    
    public function track($trackingNumber) {
        return $this->responses['track'] ?? [
            'result' => [
                'LabelNumber' => $trackingNumber,
                'Scans' => [
                    [
                        'Type' => 'D',
                        'StatusDescription' => 'Delivered',
                        'RealDateTime' => '2026-01-24 10:00:00',
                        'Name' => 'Johannesburg',
                        'Franchise' => 'JNB'
                    ]
                ]
            ]
        ];
    }
}

// tests/Unit/Services/FastwayServiceTest.php
use PHPUnit\Framework\TestCase;
use Mockery as m;

class FastwayServiceTest extends TestCase {
    private $apiClient;
    private $cache;
    private $service;
    
    protected function setUp(): void {
        $this->apiClient = m::mock(APIClient::class);
        $this->cache = m::mock(CacheService::class);
        $this->service = new FastwayService($this->apiClient, $this->cache);
    }
    
    protected function tearDown(): void {
        m::close();
    }
    
    public function testTrackReturnsDataWhenAPISucceeds() {
        // Arrange
        $trackingNumber = 'Z60000983328';
        $mockResponse = [
            'result' => [
                'LabelNumber' => $trackingNumber,
                'Scans' => [
                    [
                        'StatusDescription' => 'Delivered',
                        'RealDateTime' => '2026-01-24 10:00:00'
                    ]
                ]
            ]
        ];
        
        $this->cache
            ->shouldReceive('get')
            ->once()
            ->with("tracking:{$trackingNumber}")
            ->andReturn(null);
        
        $this->apiClient
            ->shouldReceive('get')
            ->once()
            ->with('/tracktrace/detail', ['LabelNo' => $trackingNumber])
            ->andReturn($mockResponse);
        
        $this->cache
            ->shouldReceive('set')
            ->once();
        
        // Act
        $result = $this->service->track($trackingNumber);
        
        // Assert
        $this->assertEquals($trackingNumber, $result->trackingNumber);
        $this->assertEquals('Delivered', $result->status);
    }
    
    public function testTrackReturnsCachedDataWhenAvailable() {
        // Arrange
        $trackingNumber = 'Z60000983328';
        $cachedTracking = new Tracking(['trackingNumber' => $trackingNumber]);
        
        $this->cache
            ->shouldReceive('get')
            ->once()
            ->andReturn($cachedTracking);
        
        $this->apiClient
            ->shouldReceive('get')
            ->never();
        
        // Act
        $result = $this->service->track($trackingNumber);
        
        // Assert
        $this->assertEquals($trackingNumber, $result->trackingNumber);
    }
    
    public function testTrackThrowsExceptionWhenAPIFails() {
        // Arrange
        $this->cache->shouldReceive('get')->andReturn(null);
        $this->apiClient
            ->shouldReceive('get')
            ->andThrow(new APIException('Connection failed'));
        
        // Assert
        $this->expectException(APIException::class);
        
        // Act
        $this->service->track('Z60000983328');
    }
}

HTTP Mocking with Guzzle:

use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Client;

class FastwayAPITest extends TestCase {
    public function testAPICallWithMockResponse() {
        // Create a mock response
        $mock = new MockHandler([
            new Response(200, [], json_encode([
                'result' => [
                    'LabelNumber' => 'Z60000983328',
                    'Scans' => [...]
                ]
            ]))
        ]);
        
        $handlerStack = HandlerStack::create($mock);
        $client = new Client(['handler' => $handlerStack]);
        
        // Use mocked client
        $service = new FastwayService($client, $cache);
        $result = $service->track('Z60000983328');
        
        $this->assertEquals('Z60000983328', $result->trackingNumber);
    }
}

VCR (Record & Replay):

// Record actual API responses once, replay in tests
use VCR\VCR;

class FastwayAPIIntegrationTest extends TestCase {
    protected function setUp(): void {
        VCR::turnOn();
        VCR::insertCassette('fastway-track');
    }
    
    protected function tearDown(): void {
        VCR::eject();
        VCR::turnOff();
    }
    
    public function testRealAPICall() {
        // First run: records real API response to cassette
        // Subsequent runs: replays from cassette
        $service = new FastwayService(new RealAPIClient(), new NullCache());
        $result = $service->track('Z60000983328');
        
        $this->assertNotNull($result);
    }
}

22. Provide at least one example of how you would unit test part of this application.

Example: Testing Input Sanitization

// tests/Unit/InputSanitizerTest.php
<?php

use PHPUnit\Framework\TestCase;

class InputSanitizerTest extends TestCase {
    private $sanitizer;
    
    protected function setUp(): void {
        require_once __DIR__ . '/../../api/config.php';
    }
    
    /**
     * @dataProvider trackingNumberProvider
     */
    public function testSanitizeTrackingNumber($input, $expected) {
        $result = sanitizeInput($input, 'tracking');
        $this->assertEquals($expected, $result);
    }
    
    public function trackingNumberProvider() {
        return [
            'valid alphanumeric' => ['Z60000983328', 'Z60000983328'],
            'with spaces' => ['Z600 00983 328', 'Z60000983328'],
            'with special chars' => ['Z600-00983-328', 'Z60000983328'],
            'with lowercase' => ['z60000983328', 'z60000983328'],
            'with sql injection' => ["Z'; DROP TABLE--", 'ZDROPTABLE'],
            'with xss' => ['<script>alert(1)</script>', 'scriptalert1script'],
            'empty string' => ['', ''],
            'null' => [null, '']
        ];
    }
    
    /**
     * @dataProvider postalCodeProvider
     */
    public function testSanitizePostalCode($input, $expected) {
        $result = sanitizeInput($input, 'postal');
        $this->assertEquals($expected, $result);
    }
    
    public function postalCodeProvider() {
        return [
            'valid 4 digits' => ['2196', '2196'],
            'with letters' => ['219A', '219'],
            'too long' => ['21960', '2196'], // truncated to 4
            'with spaces' => ['2 1 9 6', '2196'],
            'negative number' => ['-2196', '2196'],
            'decimal' => ['2196.5', '21965'], // becomes string
        ];
    }
    
    public function testSanitizeNumericWeight() {
        $this->assertEquals('5.5', sanitizeInput('5.5', 'numeric'));
        $this->assertEquals('10', sanitizeInput('10', 'numeric'));
        $this->assertEquals('5.5', sanitizeInput('5.5kg', 'numeric'));
        $this->assertEquals('-5.5', sanitizeInput('-5.5', 'numeric'));
    }
    
    public function testSanitizeEmail() {
        $this->assertEquals(
            'test@example.com',
            sanitizeInput('test@example.com', 'email')
        );
        
        $this->assertEquals(
            'test@example.com',
            sanitizeInput('<test@example.com>', 'email')
        );
    }
    
    public function testSanitizeGeneralInput() {
        $input = '<script>alert("XSS")</script>';
        $result = sanitizeInput($input);
        
        $this->assertStringNotContainsString('<script>', $result);
        $this->assertStringNotContainsString('</script>', $result);
    }
    
    public function testSanitizePreservesValidData() {
        $input = 'Sandton 2196';
        $result = sanitizeInput($input);
        
        $this->assertEquals('Sandton 2196', $result);
    }
}

Example: Testing Tracking Data Parser

// tests/Unit/TrackingParserTest.php
class TrackingParserTest extends TestCase {
    public function testParseValidAPIResponse() {
        $apiResponse = [
            'result' => [
                'LabelNumber' => 'Z60000983328',
                'DistributedTo' => 'Johannesburg',
                'Scans' => [
                    [
                        'Type' => 'P',
                        'StatusDescription' => 'Picked up',
                        'RealDateTime' => '2026-01-24 08:00:00',
                        'Name' => 'Johannesburg',
                        'Franchise' => 'JNB'
                    ],
                    [
                        'Type' => 'D',
                        'StatusDescription' => 'Delivered',
                        'RealDateTime' => '2026-01-24 15:30:00',
                        'Name' => 'Cape Town',
                        'Franchise' => 'CPT'
                    ]
                ]
            ]
        ];
        
        $parser = new TrackingParser();
        $result = $parser->parse($apiResponse);
        
        $this->assertEquals('Z60000983328', $result['tracking_number']);
        $this->assertEquals('Delivered', $result['status']);
        $this->assertEquals('Cape Town (CPT)', $result['current_location']);
        $this->assertCount(2, $result['events']);
        
        // Events should be reversed (newest first)
        $this->assertEquals('Delivered', $result['events'][0]['description']);
        $this->assertEquals('Picked up', $result['events'][1]['description']);
    }
    
    public function testParseEmptyScansArray() {
        $apiResponse = [
            'result' => [
                'LabelNumber' => 'Z60000983328',
                'Scans' => []
            ]
        ];
        
        $parser = new TrackingParser();
        $result = $parser->parse($apiResponse);
        
        $this->assertCount(1, $result['events']); // Basic event created
        $this->assertEquals('Unknown', $result['status']);
    }
    
    public function testParseMissingFields() {
        $apiResponse = [
            'result' => [
                'LabelNumber' => 'Z60000983328'
                // Missing Scans array
            ]
        ];
        
        $parser = new TrackingParser();
        $result = $parser->parse($apiResponse);
        
        $this->assertEquals('Z60000983328', $result['tracking_number']);
        $this->assertIsArray($result['events']);
    }
    
    public function testParseInvalidResponse() {
        $this->expectException(ParsingException::class);
        
        $parser = new TrackingParser();
        $parser->parse(['invalid' => 'data']);
    }
}

Running Tests:

# Run all tests
./vendor/bin/phpunit

# Run specific test suite
./vendor/bin/phpunit --testsuite=Unit

# Run with coverage
./vendor/bin/phpunit --coverage-html coverage

# Run specific test
./vendor/bin/phpunit tests/Unit/InputSanitizerTest.php

# Run with filter
./vendor/bin/phpunit --filter testSanitizeTrackingNumber

Deployment & DevOps

23. How would you deploy this application to a production server?

Deployment Options:

Option 1: Google App Engine (Recommended)

# Prerequisites
gcloud auth login
gcloud config set project fastway-couriers-prod

# Deploy
gcloud app deploy app.yaml --promote --stop-previous-version

# Deploy with specific version (blue-green deployment)
gcloud app deploy --version=v2 --no-promote

# Route traffic gradually
gcloud app services set-traffic default --splits v1=90,v2=10

# Full switchover after testing
gcloud app services set-traffic default --splits v2=100

Option 2: Traditional Server (Ubuntu/Debian)

#!/bin/bash
# deploy.sh

# 1. Update system
sudo apt update && sudo apt upgrade -y

# 2. Install dependencies
sudo apt install -y nginx php8.1-fpm php8.1-curl php8.1-mbstring php8.1-xml

# 3. Clone/upload application
cd /var/www
sudo git clone https://github.com/RailwayGunDora/fastway.git
cd fastway-app

# 4. Set permissions
sudo chown -R www-data:www-data /var/www/fastway-app
sudo chmod -R 755 /var/www/fastway-app

# 5. Configure Nginx
sudo cp deployment/nginx.conf /etc/nginx/sites-available/fastway-app
sudo ln -s /etc/nginx/sites-available/fastway-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# 6. Set up SSL
sudo certbot --nginx -d 

# 7. Configure environment variables
sudo cp .env.example .env
sudo nano .env  # Edit configuration

# 8. Set up logging
sudo mkdir -p /var/log/fastway-app
sudo chown www-data:www-data /var/log/fastway-app

# 9. Restart services
sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx

echo "Deployment complete!"

Nginx Configuration:

# deployment/nginx.conf
server {
    listen 80;
    server_name https://fastway-webapp.ue.r.appspot.com/;
    root /var/www/fastway-app;
    index index.php;
    
    # Security headers
    add_header X-Frame-Options "DENY";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    
    # PHP handling
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    
    # Static files caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # Deny access to sensitive files
    location ~ /\. {
        deny all;
    }
    
    location ~ /(api/config\.php|logs/.*|\.git) {
        deny all;
    }
    
    # Logs
    access_log /var/log/nginx/fastway-access.log;
    error_log /var/log/nginx/fastway-error.log;
}

Option 3: Docker Deployment

# Dockerfile
FROM php:8.1-apache

# Install dependencies
RUN apt-get update && apt-get install -y \
    libzip-dev \
    && docker-php-ext-install zip curl

# Enable Apache modules
RUN a2enmod rewrite headers

# Copy application
COPY . /var/www/html/

# Set permissions
RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html

# Configure Apache
COPY deployment/apache.conf /etc/apache2/sites-available/000-default.conf

EXPOSE 80

CMD ["apache2-foreground"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "80:80"
    environment:
      - FASTWAY_API_KEY=${FASTWAY_API_KEY}
      - APP_ENV=production
    volumes:
      - ./logs:/var/www/html/logs
    restart: unless-stopped
    
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    restart: unless-stopped

24. What environment variables would you configure?

Environment Variables:

# .env.example
# Application
APP_NAME=FastwayCouriers
APP_ENV=production
APP_DEBUG=false
APP_URL=https://fastway-webapp.ue.r.appspot.com/

# Fastway API
FASTWAY_API_KEY=api_key_here
FASTWAY_API_BASE_URL=https://sa.api.fastway.org
FASTWAY_API_VERSION=v3
FASTWAY_RF_CODE=JNB

# Database (if used)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=fastway
DB_USERNAME=fastway_user
DB_PASSWORD=secure_password_here

# Redis Cache
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

# Logging
LOG_CHANNEL=stack
LOG_LEVEL=info
LOG_PATH=/var/log/fastway-app

# Security
ENCRYPTION_KEY=base64:random_32_character_key_here
SESSION_SECRET=random_session_secret_here

# Third-party Services
SENTRY_DSN=https://sentry-dsn
GOOGLE_ANALYTICS_ID=UA-XXXXX-X

# Email (for notifications)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=username
MAIL_PASSWORD=password
MAIL_FROM_ADDRESS=noreply@fastway.com
MAIL_FROM_NAME=FastwayCouriers

# Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=3600

# Features
ENABLE_CACHING=true
ENABLE_LOGGING=true
ENABLE_MONITORING=true

# Google Cloud (if using GAE)
GOOGLE_CLOUD_PROJECT=fatway-webapp
GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json

Loading Environment Variables:

// config/env.php
class Env {
    private static $loaded = false;
    
    public static function load($path = '.env') {
        if (self::$loaded) {
            return;
        }
        
        if (!file_exists($path)) {
            return;
        }
        
        $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            if (strpos(trim($line), '#') === 0) {
                continue;
            }
            
            list($name, $value) = explode('=', $line, 2);
            $name = trim($name);
            $value = trim($value);
            
            if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
                putenv("$name=$value");
                $_ENV[$name] = $value;
                $_SERVER[$name] = $value;
            }
        }
        
        self::$loaded = true;
    }
    
    public static function get($key, $default = null) {
        $value = getenv($key);
        
        if ($value === false) {
            return $default;
        }
        
        // Handle boolean values
        if (in_array(strtolower($value), ['true', 'false'])) {
            return strtolower($value) === 'true';
        }
        
        return $value;
    }
}

// Usage in config.php
Env::load(__DIR__ . '/../.env');

define('FASTWAY_API_KEY', Env::get('FASTWAY_API_KEY'));
define('APP_DEBUG', Env::get('APP_DEBUG', false));

25. How would you implement CI/CD for this project?

GitHub Actions Workflow:

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  PHP_VERSION: '8.1'

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    
    services:
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHP_VERSION }}
          extensions: curl, mbstring, zip
          coverage: xdebug
      
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      
      - name: Run code style checks
        run: ./vendor/bin/phpcs --standard=PSR12 app api
      
      - name: Run static analysis
        run: ./vendor/bin/phpstan analyse app api --level=5
      
      - name: Run unit tests
        run: ./vendor/bin/phpunit --testsuite=Unit --coverage-clover coverage.xml
      
      - name: Run integration tests
        run: ./vendor/bin/phpunit --testsuite=Integration
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml
          fail_ci_if_error: true
  
  security:
    name: Security Scan
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Run security checker
        uses: symfonycorp/security-checker-action@v4
      
      - name: Run dependency check
        run: composer audit
  
  deploy-staging:
    name: Deploy to Staging
    needs: [test, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Setup Cloud SDK
        uses: google-github-actions/setup-gcloud@v1
        with:
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          project_id: ${{ secrets.GCP_PROJECT_ID }}
      
      - name: Deploy to App Engine (Staging)
        run: |
          gcloud app deploy app.yaml \
            --version=staging-${{ github.sha }} \
            --no-promote \
            --quiet
      
      - name: Run smoke tests
        run: |
          curl -f https://staging-dot-${{ secrets.GCP_PROJECT_ID }}.appspot.com/health-check.php
  
  deploy-production:
    name: Deploy to Production
    needs: [test, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://fastway-webapp.ue.r.appspot.com/
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Setup Cloud SDK
        uses: google-github-actions/setup-gcloud@v1
        with:
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          project_id: ${{ secrets.GCP_PROJECT_ID }}
      
      - name: Deploy to App Engine (Production)
        run: |
          gcloud app deploy app.yaml \
            --version=prod-${{ github.sha }} \
            --promote \
            --quiet
      
      - name: Run smoke tests
        run: |
          curl -f https://${{ secrets.PRODUCTION_URL }}/health-check.php
      
      - name: Notify Slack
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK }}
          payload: |
            {
              "text": "Production deployment successful: ${{ github.sha }}"
            }
      
      - name: Create release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: v${{ github.run_number }}
          release_name: Release ${{ github.run_number }}
          body: Automated production deployment

GitLab CI/CD:

# .gitlab-ci.yml
stages:
  - test
  - security
  - deploy

variables:
  PHP_VERSION: "8.1"

test:unit:
  stage: test
  image: php:$PHP_VERSION
  services:
    - redis:7-alpine
  before_script:
    - apt-get update && apt-get install -y git zip unzip
    - curl -sS https://getcomposer.org/installer | php
    - php composer.phar install
  script:
    - ./vendor/bin/phpunit --testsuite=Unit --coverage-text
  coverage: '/^\s*Lines:\s*\d+.\d+\%/'

test:integration:
  stage: test
  image: php:$PHP_VERSION
  script:
    - ./vendor/bin/phpunit --testsuite=Integration

security:scan:
  stage: security
  image: php:$PHP_VERSION
  script:
    - composer audit
    - ./vendor/bin/phpstan analyse

deploy:staging:
  stage: deploy
  only:
    - develop
  script:
    - gcloud app deploy --version=staging --no-promote

deploy:production:
  stage: deploy
  only:
    - main
  when: manual
  script:
    - gcloud app deploy --version=production --promote

26. How would you monitor the application once live?

Monitoring Stack:

┌─────────────────────────────────────┐
│      Application Layer              │
│  (Logs, Metrics, Traces, Errors)   │
└──────────────┬──────────────────────┘
               ↓
┌──────────────────────────────────────┐
│    Collection Layer                  │
│  - Google Cloud Logging              │
│  - Prometheus                        │
│  - OpenTelemetry                     │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│    Storage & Processing              │
│  - BigQuery                          │
│  - Cloud Monitoring                  │
│  - Elasticsearch                     │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│    Visualization & Alerting          │
│  - Grafana Dashboards                │
│  - Cloud Monitoring Dashboards       │
│  - PagerDuty                         │
│  - Slack Notifications               │
└──────────────────────────────────────┘

Monitoring Implementation:

// Monitoring/MonitoringService.php
class MonitoringService {
    private $prometheus;
    private $logger;
    
    public function __construct() {
        $this->prometheus = new \Prometheus\CollectorRegistry(
            new \Prometheus\Storage\Redis()
        );
        $this->logger = Logger::getInstance();
    }
    
    public function recordRequest($endpoint, $method, $statusCode, $duration) {
        // Metrics
        $counter = $this->prometheus->getOrRegisterCounter(
            'app',
            'http_requests_total',
            'Total HTTP requests',
            ['endpoint', 'method', 'status']
        );
        $counter->inc([$endpoint, $method, $statusCode]);
        
        $histogram = $this->prometheus->getOrRegisterHistogram(
            'app',
            'http_request_duration_seconds',
            'HTTP request duration',
            ['endpoint'],
            [0.1, 0.5, 1, 2, 5]
        );
        $histogram->observe($duration, [$endpoint]);
        
        // Logging
        $this->logger->info('HTTP Request', [
            'endpoint' => $endpoint,
            'method' => $method,
            'status_code' => $statusCode,
            'duration_ms' => $duration * 1000
        ]);
        
        // Alerting for errors
        if ($statusCode >= 500) {
            $this->alert('HTTP 5xx Error', [
                'endpoint' => $endpoint,
                'status_code' => $statusCode
            ]);
        }
    }
    
    public function recordAPICall($provider, $endpoint, $success, $latency) {
        $counter = $this->prometheus->getOrRegisterCounter(
            'app',
            'external_api_calls_total',
            'Total external API calls',
            ['provider', 'endpoint', 'status']
        );
        $counter->inc([$provider, $endpoint, $success ? 'success' : 'failure']);
        
        if (!$success) {
            $this->alert('External API Failure', [
                'provider' => $provider,
                'endpoint' => $endpoint
            ]);
        }
    }
}

// In api/track.php
$monitoring = new MonitoringService();
$startTime = microtime(true);

try {
    // ... handle request
    $duration = microtime(true) - $startTime;
    $monitoring->recordRequest('/api/track', 'POST', 200, $duration);
} catch (Exception $e) {
    $duration = microtime(true) - $startTime;
    $monitoring->recordRequest('/api/track', 'POST', 500, $duration);
    throw $e;
}

Grafana Dashboard Configuration:

{
  "dashboard": {
    "title": "Fastway Application Monitoring",
    "panels": [
      {
        "title": "Request Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total[5m])",
            "legendFormat": "{{endpoint}} - {{status}}"
          }
        ]
      },
      {
        "title": "Response Time (p95)",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "{{endpoint}}"
          }
        ]
      },
      {
        "title": "Error Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total{status=~\"5..\"}[5m])",
            "legendFormat": "5xx errors"
          }
        ]
      },
      {
        "title": "API Success Rate",
        "targets": [
          {
            "expr": "rate(external_api_calls_total{status=\"success\"}[5m]) / rate(external_api_calls_total[5m])",
            "legendFormat": "{{provider}}"
          }
        ]
      }
    ]
  }
}

Uptime Monitoring:

# monitorig/uptime_check.py
import requests
import time
from prometheus_client import Gauge, push_to_gateway

uptime_gauge = Gauge('app_uptime', 'Application uptime status')

def check_health():
    try:
        response = requests.get(
            'https://fastway-webapp.ue.r.appspot.com/health-check.php',
            timeout=5
        )
        if response.status_code == 200:
            uptime_gauge.set(1)
            return True
        else:
            uptime_gauge.set(0)
            send_alert(f"Health check failed: {response.status_code}")
            return False
    except Exception as e:
        uptime_gauge.set(0)
        send_alert(f"Health check error: {str(e)}")
        return False

while True:
    check_health()
    push_to_gateway('pushgateway:9091', job='uptime_check', registry=...)
    time.sleep(60)  # Check every minute

Bonus Tasks Implementation

Store form submissions in SQLite database

// Database/Database.php
class Database {
    private static $instance;
    private $pdo;
    
    private function __construct() {
        $this->pdo = new PDO('sqlite:' . __DIR__ . '/../data/fastway.db');
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->createTables();
    }
    
    public static function getInstance() {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function createTables() {
        $this->pdo->exec("
            CREATE TABLE IF NOT EXISTS tracking_searches (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                tracking_number TEXT NOT NULL,
                searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                ip_address TEXT,
                user_agent TEXT,
                result_status TEXT
            )
        ");
        
        $this->pdo->exec("
            CREATE TABLE IF NOT EXISTS quote_requests (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                suburb TEXT NOT NULL,
                postal_code TEXT NOT NULL,
                weight REAL NOT NULL,
                delivery_type TEXT,
                requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                ip_address TEXT,
                user_agent TEXT,
                quote_price REAL
            )
        ");
        
        // Indexes
        $this->pdo->exec("
            CREATE INDEX IF NOT EXISTS idx_tracking_searched 
            ON tracking_searches(searched_at)
        ");
        
        $this->pdo->exec("
            CREATE INDEX IF NOT EXISTS idx_quote_requested 
            ON quote_requests(requested_at)
        ");
    }
    
    public function logTrackingSearch($trackingNumber, $resultStatus) {
        $stmt = $this->pdo->prepare("
            INSERT INTO tracking_searches 
            (tracking_number, ip_address, user_agent, result_status)
            VALUES (?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $trackingNumber,
            $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
            $resultStatus
        ]);
    }
    
    public function logQuoteRequest($suburb, $postalCode, $weight, $deliveryType, $price = null) {
        $stmt = $this->pdo->prepare("
            INSERT INTO quote_requests
            (suburb, postal_code, weight, delivery_type, ip_address, user_agent, quote_price)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $suburb,
            $postalCode,
            $weight,
            $deliveryType,
            $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
            $price
        ]);
    }
    
    public function getRecentSearches($limit = 10) {
        $stmt = $this->pdo->prepare("
            SELECT * FROM tracking_searches 
            ORDER BY searched_at DESC 
            LIMIT ?
        ");
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

// In api/track.php - add logging
$db = Database::getInstance();
$db->logTrackingSearch($trackingNumber, $trackingInfo['status']);

AJAX Form Submissions

Already implemented in assets/js/track.js and assets/js/quote.js!

// Example from track.js
fetch('api/track.php', {
    method: 'POST',
    body: formData
})
.then(response => response.json())
.then(data => {
    if (data.success) {
        displayTrackingResults(data.data);
    }
});

Caching for Repeated Requests

Already implemented in the Redis/File caching examples above!

Deploy Online

Already covered in deployment section with Google App Engine!


Summary:

All documentation is production-ready and follows industry best practices.