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.
Error tracking (Sentry)
Section titled “Error tracking (Sentry)”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 toSENTRY_DSN(backend/config/sentry.php:5). - Release and environment come from
SENTRY_RELEASEandSENTRY_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_piidefaults tofalse(backend/config/sentry.php:55), andtraces_sample_rateis null unlessSENTRY_TRACES_SAMPLE_RATEis 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.
Tracing (OpenTelemetry)
Section titled “Tracing (OpenTelemetry)”Not present. There is no OpenTelemetry instrumentation anywhere in the platform:
- No
open-telemetry/*package inbackend/composer.jsonand no@opentelemetry/*package infrontend/package.json. - A repo-wide search for
opentelemetry/otelreturns only false positives — the substringotelinside 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.
Product analytics (GA4)
Section titled “Product analytics (GA4)”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.htmlloads the GTM containerGTM-TXQLKZQ8and a GA4gtag.jstag, then callsgtag('js', new Date())andgtag('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 callinggtag('event', ...)inside atry/catchso an analytics failure never breaks the UI (e.g.frontend/apps/orascom-broker-app/src/utils/analytics-events.ts). Shared event-parameter shapes come fromCommonEventParametersin@orascom/utils. gtagitself is a global provided by theindex.htmlscript; TypeScript sees it via an ambient declarationdeclare function gtag(...args: any[]): void;atfrontend/apps/<app>/src/definitions/declarations/gtag.d.ts.
There is no GA4 / server-side analytics on the Laravel backend.
Object storage (AWS S3 / MinIO)
Section titled “Object storage (AWS S3 / MinIO)”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 tolocal) (backend/config/filesystems.php:16). - Local disks:
uploads(→public_path('uploads')),local(→storage_path('app')), andpublic(→storage_path('app/public'), symlinked viastorage:link) (backend/config/filesystems.php:53-88,101-103). - The
s3disk is standard S3, driven byAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_DEFAULT_REGION,AWS_BUCKET,AWS_URL,AWS_ENDPOINT, andAWS_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(withSALES_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(defaultlocal; 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.
Cache & queue (Redis)
Section titled “Cache & queue (Redis)”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, perbackend/composer.json);REDIS_CLIENTdefaults topredis(backend/config/database.php:129). - Two Redis connections are defined:
default(DBREDIS_DB, default0) andcache(DBREDIS_CACHE_DB, default1), both keyed offREDIS_HOST/REDIS_PORT/REDIS_PASSWORD/REDIS_USERNAME(backend/config/database.php:127-154). - A
rediscache store exists that uses thecacheconnection for data and thedefaultconnection for locks (backend/config/cache.php:76-80). - The broadcasting config also defines a
redisconnection on thedefaultRedis 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).
Realtime (Pusher)
Section titled “Realtime (Pusher)”Config scaffold only — not wired to any feature. The Pusher connection block exists, but nothing broadcasts through it:
backend/config/broadcasting.php:33-51defines apusherconnection readingPUSHER_APP_KEY/PUSHER_APP_SECRET/PUSHER_APP_ID, with host derived fromPUSHER_HOST/PUSHER_APP_CLUSTER(defaultapi-<cluster>.pusher.com),PUSHER_PORT(443), andPUSHER_SCHEME.- But there is no Pusher SDK in
backend/composer.json(nopusher/pusher-php-server), so this driver could not actually send events as-is. - The default broadcaster is
null(backend/config/broadcasting.php:18), andbackend/.env.examplesetsBROADCAST_DRIVER=log. - No application event implements
ShouldBroadcastand nothing callsbroadcast(...);backend/routes/channels.phpcontains only the default LaravelApp.Models.User.{id}channel, andbackend/app/Providers/BroadcastServiceProvider.phpis the default scaffold. - The frontend has no
pusher-jsorlaravel-echodependency infrontend/package.jsonand 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.
FX rates (Currency-Layer)
Section titled “FX rates (Currency-Layer)”Currency-Layer (apilayer) is the FX-rate feed, wired server-side in the Laravel API.
- Config lives in
backend/config/currency.php:api_urlfromCURRENY_LAYER_API_URL,api_keyfromCURRENY_LAYER_API_KEY, andcache_ttl_secondsfromCURRENCY_RATES_CACHE_TTL_SECONDS(default3600). backend/app/Services/CurrencyRatesService.phpdoes the work: it callsHttp::get($url, ['access_key' => ..., 'source' => $source, 'currencies' => ...])against the live endpoint, requesting the currencies enumerated byCurrencyEnums, and wraps the result inCache::remember('currency_rates:<SOURCE>', ...)for the configured TTL. On a non-successresponse it throwsAppCustomExceptionwith the API’s error info (backend/app/Services/CurrencyRatesService.php:33-61).backend/.env.examplepointsCURRENY_LAYER_API_URLathttps://apilayer.net/api/live(per the frontend.env.example; the backend key is left blank).- Public routes:
GET currencies(CurrencyController) andGET currency-rates(CurrencyRatesController) inbackend/routes/api.php:40-41.
Config / env
Section titled “Config / env”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(orSENTRY_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 / Redis — CACHE_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-Layer — CURRENY_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.
