Skip to content

Observability & infrastructure

This page documents the observability and infrastructure integrations wired into the platform, verified against the source in backend/ and frontend/. Every claim below cites the file it came from. Where an integration is not actually wired, that is called out rather than glossed over.

Sentry is the one integration wired on both sides of the stack.

Backend. The Laravel API uses sentry/sentry-laravel (^3.1, per backend/composer.json), configured in backend/config/sentry.php. Key behaviour:

  • DSN resolves from SENTRY_LARAVEL_DSN, falling back to SENTRY_DSN (backend/config/sentry.php:5).
  • Release and environment come from SENTRY_RELEASE and SENTRY_ENVIRONMENT (backend/config/sentry.php:9,12); when the environment is blank Laravel’s own environment is used.
  • Breadcrumbs capture Laravel logs, SQL queries and SQL bindings, queue-job info, and command info (backend/config/sentry.php:14-29).
  • Tracing captures SQL queries, views, and sync-driver queue jobs as spans; queue-job transactions are gated behind SENTRY_TRACE_QUEUE_ENABLED (backend/config/sentry.php:31-52).
  • send_default_pii defaults to false (backend/config/sentry.php:55), and traces_sample_rate is null unless SENTRY_TRACES_SAMPLE_RATE is set (backend/config/sentry.php:58).

Frontend. Each of the four React apps initialises Sentry independently in its src/main.tsx, using @sentry/react (^7.112.2, per frontend/package.json). Every app reads a shared process.env['NX_SENTRY_ENV'] plus its own per-app DSN, and installs Sentry.reactRouterV6BrowserTracingIntegration(...) + Sentry.replayIntegration() with tracesSampleRate: 0.1. Sentry is disabled when the env is development (enabled: sentryEnv !== 'development').

Per-app DSN env var and init file:

App DSN env var Init file
Broker NX_SENTRY_BROKER_DSN frontend/apps/orascom-broker-app/src/main.tsx:23
Shopper NX_SENTRY_SHOPPER_DSN frontend/apps/orascom-shopper-app/src/main.tsx:21
Sales-man NX_SENTRY_SALESMAN_DSN frontend/apps/orascom-sales-man-app/src/main.tsx:24
Shopper-analytics NX_SENTRY_ANALYTICS_DSN frontend/apps/orascom-shopper-analytics-app/src/main.tsx:21

The shopper app additionally sets replaysSessionSampleRate: 0.1 and replaysOnErrorSampleRate: 1.0 (frontend/apps/orascom-shopper-app/src/main.tsx:30-33); the other three rely on the replay integration’s defaults.

Source-map upload at build time is handled by a wrapper around @sentry/vite-plugin at frontend/tools/orascom-sentry-vite-plugin.ts (see frontend/CLAUDE.md §10). This is a build-time concern, not a runtime DSN.

Not present. There is no OpenTelemetry instrumentation anywhere in the platform:

  • No open-telemetry/* package in backend/composer.json and no @opentelemetry/* package in frontend/package.json.
  • A repo-wide search for opentelemetry / otel returns only false positives — the substring otel inside words like “hotel”, plus passing mentions in planning docs (backend/PROJECT-OVERVIEW.md, backend/KT-QUESTIONS.md). No exporter, collector, SDK init, or instrumentation code exists.

Distributed-tracing needs are met instead by Sentry tracing (backend spans for SQL/views/queue jobs, and reactRouterV6BrowserTracingIntegration on the frontend — see the Sentry section). If OpenTelemetry is ever required, it would be a green-field addition.

GA4 is wired on the frontend only, via Google Tag Manager plus a GA4 gtag.js tag loaded directly in each app’s index.html.

  • Each app’s index.html loads the GTM container GTM-TXQLKZQ8 and a GA4 gtag.js tag, then calls gtag('js', new Date()) and gtag('config', id) (e.g. frontend/apps/orascom-broker-app/index.html:57-74).
  • GA4 measurement IDs are hard-coded in index.html (they are public client-side IDs, not secrets):
App GA4 measurement ID(s) GTM container
Broker G-8W9ER405MZ GTM-TXQLKZQ8
Shopper G-EV22VQLJN6 GTM-TXQLKZQ8
Sales-man G-9V8KP8Z8X2 and G-EV22VQLJN6 GTM-TXQLKZQ8
Shopper-analytics G-EV22VQLJN6 GTM-TXQLKZQ8
  • Custom events are emitted through thin wrappers in each app’s src/utils/analytics-events.ts, each calling gtag('event', ...) inside a try/catch so an analytics failure never breaks the UI (e.g. frontend/apps/orascom-broker-app/src/utils/analytics-events.ts). Shared event-parameter shapes come from CommonEventParameters in @orascom/utils.
  • gtag itself is a global provided by the index.html script; TypeScript sees it via an ambient declaration declare function gtag(...args: any[]): void; at frontend/apps/<app>/src/definitions/declarations/gtag.d.ts.

There is no GA4 / server-side analytics on the Laravel backend.

Object storage is Laravel’s filesystem abstraction, configured in backend/config/filesystems.php, backed by league/flysystem-aws-s3-v3 (3.28.0, per backend/composer.json).

  • The default disk is FILESYSTEM_DISK (defaults to local) (backend/config/filesystems.php:16).
  • Local disks: uploads (→ public_path('uploads')), local (→ storage_path('app')), and public (→ storage_path('app/public'), symlinked via storage:link) (backend/config/filesystems.php:53-88,101-103).
  • The s3 disk is standard S3, driven by AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_URL, AWS_ENDPOINT, and AWS_USE_PATH_STYLE_ENDPOINT (backend/config/filesystems.php:76-86).

MinIO is the local/self-hosted S3-compatible target: backend/.env.example sets AWS_ENDPOINT=http://localhost:9000 (MinIO’s default port). Pointing the same s3 disk at a MinIO endpoint with AWS_USE_PATH_STYLE_ENDPOINT=true is the standard way to run S3 locally without AWS.

Two purpose-specific disks are selected by env so dev/staging can stay local while prod uses S3:

  • sales_offer_disk — where generated sales-offer PDFs land; SALES_OFFER_DISK (with SALES_OFFER_EXPIRY_DAYS, default 14) (backend/config/filesystems.php:27-28).
  • broker_pre_registration_documents_disk — broker pre-registration uploads; BROKER_PRE_REGISTRATION_DOCUMENTS_DISK (default local; local in dev/staging, s3 in UAT/prod) (backend/config/filesystems.php:38).

The same AWS credentials also back Amazon SES for mail (ses block in backend/config/services.php:28-32) and would back DynamoDB cache if enabled (backend/config/cache.php:82-89) — both reuse AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION.

Redis is provisioned and available, but the committed default env does not route everyday traffic through it — that is a per-environment switch.

What’s wired:

  • The PHP client is predis/predis (^3.1, per backend/composer.json); REDIS_CLIENT defaults to predis (backend/config/database.php:129).
  • Two Redis connections are defined: default (DB REDIS_DB, default 0) and cache (DB REDIS_CACHE_DB, default 1), both keyed off REDIS_HOST / REDIS_PORT / REDIS_PASSWORD / REDIS_USERNAME (backend/config/database.php:127-154).
  • A redis cache store exists that uses the cache connection for data and the default connection for locks (backend/config/cache.php:76-80).
  • The broadcasting config also defines a redis connection on the default Redis connection (backend/config/broadcasting.php:58-61).

The nuance — default env uses non-Redis drivers. In backend/.env.example: CACHE_DRIVER=file, QUEUE_CONNECTION=sync, SESSION_DRIVER=file, BROADCAST_DRIVER=log, with REDIS_HOST=127.0.0.1. So out of the box the app caches to file, runs queues synchronously, and stores sessions on disk; production is expected to flip CACHE_DRIVER/QUEUE_CONNECTION/SESSION_DRIVER to redis. The default cache store selector is CACHE_DRIVER (default file) at backend/config/cache.php:18, and the default queue is QUEUE_CONNECTION (default sync).

Config scaffold only — not wired to any feature. The Pusher connection block exists, but nothing broadcasts through it:

  • backend/config/broadcasting.php:33-51 defines a pusher connection reading PUSHER_APP_KEY / PUSHER_APP_SECRET / PUSHER_APP_ID, with host derived from PUSHER_HOST / PUSHER_APP_CLUSTER (default api-<cluster>.pusher.com), PUSHER_PORT (443), and PUSHER_SCHEME.
  • But there is no Pusher SDK in backend/composer.json (no pusher/pusher-php-server), so this driver could not actually send events as-is.
  • The default broadcaster is null (backend/config/broadcasting.php:18), and backend/.env.example sets BROADCAST_DRIVER=log.
  • No application event implements ShouldBroadcast and nothing calls broadcast(...); backend/routes/channels.php contains only the default Laravel App.Models.User.{id} channel, and backend/app/Providers/BroadcastServiceProvider.php is the default scaffold.
  • The frontend has no pusher-js or laravel-echo dependency in frontend/package.json and no client subscription code.

In other words, real-time broadcasting is a stock Laravel skeleton left in place; if a live feature is ever built, the Pusher SDK (or another driver) must be added and events wired first.

Currency-Layer (apilayer) is the FX-rate feed, wired server-side in the Laravel API.

  • Config lives in backend/config/currency.php: api_url from CURRENY_LAYER_API_URL, api_key from CURRENY_LAYER_API_KEY, and cache_ttl_seconds from CURRENCY_RATES_CACHE_TTL_SECONDS (default 3600).
  • backend/app/Services/CurrencyRatesService.php does the work: it calls Http::get($url, ['access_key' => ..., 'source' => $source, 'currencies' => ...]) against the live endpoint, requesting the currencies enumerated by CurrencyEnums, and wraps the result in Cache::remember('currency_rates:<SOURCE>', ...) for the configured TTL. On a non-success response it throws AppCustomException with the API’s error info (backend/app/Services/CurrencyRatesService.php:33-61).
  • backend/.env.example points CURRENY_LAYER_API_URL at https://apilayer.net/api/live (per the frontend .env.example; the backend key is left blank).
  • Public routes: GET currencies (CurrencyController) and GET currency-rates (CurrencyRatesController) in backend/routes/api.php:40-41.

Keys referenced by the integrations above, by name only (never populate these in a committed .env — real values live in AWS Secrets Manager / per-env config). Backend keys unless noted.

Sentry

  • Backend: SENTRY_LARAVEL_DSN (or SENTRY_DSN), SENTRY_RELEASE, SENTRY_ENVIRONMENT, SENTRY_TRACES_SAMPLE_RATE, SENTRY_TRACE_QUEUE_ENABLED, SENTRY_SEND_DEFAULT_PII.
  • Frontend: NX_SENTRY_ENV, NX_SENTRY_BROKER_DSN, NX_SENTRY_SHOPPER_DSN, NX_SENTRY_SALESMAN_DSN, NX_SENTRY_ANALYTICS_DSN.

GA4 — no env vars; measurement IDs and the GTM container ID are hard-coded in each app’s index.html.

Object storage (S3 / MinIO)FILESYSTEM_DISK, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_URL, AWS_ENDPOINT, AWS_USE_PATH_STYLE_ENDPOINT, SALES_OFFER_DISK, SALES_OFFER_EXPIRY_DAYS, BROKER_PRE_REGISTRATION_DOCUMENTS_DISK.

Cache / queue / RedisCACHE_DRIVER, QUEUE_CONNECTION, SESSION_DRIVER, REDIS_CLIENT, REDIS_CLUSTER, REDIS_PREFIX, REDIS_URL, REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB, REDIS_CACHE_DB, CACHE_PREFIX.

Realtime (Pusher, scaffold only)BROADCAST_DRIVER, PUSHER_APP_ID, PUSHER_APP_KEY, PUSHER_APP_SECRET, PUSHER_HOST, PUSHER_PORT, PUSHER_SCHEME, PUSHER_APP_CLUSTER.

Currency-LayerCURRENY_LAYER_API_URL, CURRENY_LAYER_API_KEY, CURRENCY_RATES_CACHE_TTL_SECONDS (note the intentional CURRENY typo). Frontend mirrors (unused): NX_CURRENY_LAYER_API_URL, NX_CURRENY_LAYER_API_KEY.