From 1bb4c10911bc0ad9fd6d9aede8e9c1a9a85b2fb5 Mon Sep 17 00:00:00 2001 From: Jamie Byrne Date: Wed, 28 May 2025 12:57:54 +0100 Subject: [PATCH 1/3] fix(GAT-6927): handle error gracefully for features --- app/Providers/FeatureServiceProvider.php | 31 ++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/app/Providers/FeatureServiceProvider.php b/app/Providers/FeatureServiceProvider.php index 41ebcce0e..1784c894d 100644 --- a/app/Providers/FeatureServiceProvider.php +++ b/app/Providers/FeatureServiceProvider.php @@ -6,6 +6,7 @@ use Illuminate\Support\ServiceProvider; use App\Services\FeatureFlagManager; use Illuminate\Support\Facades\Cache; +use Illuminate\Http\Client\ConnectionException; class FeatureServiceProvider extends ServiceProvider { @@ -21,19 +22,35 @@ public function boot() $featureFlags = Cache::remember('feature_flags', now()->addMinutes(10), function () use ($url) { logger()->info('Calling that Bucket'); - $res = Http::retry(3, 5000, function ($exception, $requestNumber) use ($url) { - logger()->warning('Retrying feature flag fetch', [ + + try { + $res = Http::timeout(60) + ->retry(3, 5000, function ($exception, $requestNumber) use ($url) { + logger()->warning('Retrying feature flag fetch', [ + 'url' => $url, + 'attempt' => $requestNumber, + 'error' => $exception->getMessage(), + ]); + }) + ->get($url); + } catch (ConnectionException $e) { + logger()->error('ConnectionException when fetching feature flags', [ 'url' => $url, - 'attempt' => $requestNumber, - 'error' => $exception->getMessage(), + 'error' => $e->getMessage(), ]); - })->get($url); + return []; + } if (!$res->successful()) { - logger()->error('Failed to fetch feature flags', ['url' => $url, 'status' => $res->status()]); + logger()->error('Failed to fetch feature flags', [ + 'url' => $url, + 'status' => $res->status(), + 'body' => $res->body(), + ]); + return []; } - return $res->successful() ? $res->json() : []; + return $res->json(); }); if (is_array($featureFlags) && !empty($featureFlags)) { From c3dfe69cca873dd56d5a980692c264c0a4647dd9 Mon Sep 17 00:00:00 2001 From: Jamie Byrne Date: Wed, 28 May 2025 13:01:02 +0100 Subject: [PATCH 2/3] 2 second between retries --- app/Providers/FeatureServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Providers/FeatureServiceProvider.php b/app/Providers/FeatureServiceProvider.php index 1784c894d..64602dbb9 100644 --- a/app/Providers/FeatureServiceProvider.php +++ b/app/Providers/FeatureServiceProvider.php @@ -25,7 +25,7 @@ public function boot() try { $res = Http::timeout(60) - ->retry(3, 5000, function ($exception, $requestNumber) use ($url) { + ->retry(3, 2000, function ($exception, $requestNumber) use ($url) { logger()->warning('Retrying feature flag fetch', [ 'url' => $url, 'attempt' => $requestNumber, From e54e65666caf07827f42266e3274ae9f3357efb4 Mon Sep 17 00:00:00 2001 From: Jamie Byrne Date: Fri, 30 May 2025 08:57:43 +0100 Subject: [PATCH 3/3] feat(GAT-6927): feature flagging GCS call --- .env.example | 2 +- app/FeatureFlags/FeatureFlagManager.php | 103 ++++++++++++++++++ .../Api/V1/FeatureFlagController.php | 42 ++----- app/Providers/FeatureServiceProvider.php | 63 ++--------- app/Services/FeatureFlagManager.php | 59 ---------- config/filesystems.php | 11 ++ 6 files changed, 134 insertions(+), 146 deletions(-) create mode 100644 app/FeatureFlags/FeatureFlagManager.php delete mode 100644 app/Services/FeatureFlagManager.php diff --git a/.env.example b/.env.example index 7c9bad30d..379cc91ba 100644 --- a/.env.example +++ b/.env.example @@ -230,4 +230,4 @@ PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- -----END PUBLIC KEY-----" FEATURE_FLAG_API_TOKEN=alovelytoken -FEATURE_FLAGGING_CONFIG_URL=https://raw.githubusercontent.com/HDRUK/hdruk-feature-configurations/refs/heads/main/dev/features.json +FEATURE_FLAGGING_CONFIG_URL=https://storage.googleapis.com/hdr-gw-feature-configurations-storage-bucket-prod/dev/features.json \ No newline at end of file diff --git a/app/FeatureFlags/FeatureFlagManager.php b/app/FeatureFlags/FeatureFlagManager.php new file mode 100644 index 000000000..c567e8074 --- /dev/null +++ b/app/FeatureFlags/FeatureFlagManager.php @@ -0,0 +1,103 @@ +storage = app('filesystem')->disk('gcs.feature_flags'); + $this->loadFeatures(); + } + + protected function loadFeatures(): void + { + Log::info('Loading feature flags...'); + + $this->features = cache()->remember('feature_flags.json', now()->addMinutes(30), function () { + if (app()->environment('local')) { + Log::info('Local environment detected. Using HTTP Call, no cache.'); + return [ + 'SDEConciergeServiceEnquiry' => ['enabled' => true], + 'Aliases' => ['enabled' => true], + ]; + // $url = env('FEATURE_FLAGGING_CONFIG_URL'); + // $res = Http::get($url); + // if ($res->successful()) { + // return $res->json(); + // } + + // logger()->error('Failed to fetch feature flags from URL', ['url' => $url]); + // return []; + } + + try { + try { + $json = $this->storage->get('features.json'); + Log::info('Successfully fetched features.json from GCS.'); + } catch (\Throwable $e) { + Log::error('Error accessing GCS bucket for features.json: ' . $e->getMessage()); + return []; + } + + $features = json_decode($json, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Log::error('Failed to decode features.json: ' . json_last_error_msg()); + return []; + } + + Log::info('Feature flags loaded and parsed successfully.', ['features' => $features]); + + return $features; + + } catch (\Throwable $e) { + Log::error('Unexpected error loading features.json: ' . $e->getMessage()); + return []; + } + }); + } + + + + public function reload(): void + { + Log::info('Reloading feature flags: Clearing cache and reloading...'); + cache()->forget('feature_flags.json'); + $this->loadFeatures(); + } + + public function getEnabledFeatures(): array + { + $this->loadFeatures(); + + $enabledFeatures = array_filter($this->features, function ($feature) { + return is_array($feature) && ($feature['enabled'] ?? false) === true; + }); + + Log::info('Fetching all enabled feature flags.', ['enabled_features' => array_keys($enabledFeatures)]); + + return $enabledFeatures; + } + + public function resolve(string $feature, mixed $scope = null): mixed + { + $this->loadFeatures(); + + $value = $this->features[$feature] ?? false; + Log::info('Resolving feature flag.', [ + 'feature' => $feature, + 'resolved_value' => $value, + 'scope' => $scope, + ]); + + return $value; + } +} diff --git a/app/Http/Controllers/Api/V1/FeatureFlagController.php b/app/Http/Controllers/Api/V1/FeatureFlagController.php index 9c83e57c0..887c9f486 100644 --- a/app/Http/Controllers/Api/V1/FeatureFlagController.php +++ b/app/Http/Controllers/Api/V1/FeatureFlagController.php @@ -4,12 +4,10 @@ use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Cache; use App\Http\Controllers\Controller; use Laravel\Pennant\Feature; -use App\Services\FeatureFlagManager; +use App\FeatureFlags\FeatureFlagManager; class FeatureFlagController extends Controller { @@ -51,7 +49,7 @@ class FeatureFlagController extends Controller * ) * ) */ - public function index(Request $request, FeatureFlagManager $flagManager): JsonResponse + public function reload(FeatureFlagManager $resolver): JsonResponse { $featureFlagToken = env('FEATURE_FLAG_API_TOKEN'); $authHeader = $request->header('Authorization'); @@ -67,37 +65,13 @@ public function index(Request $request, FeatureFlagManager $flagManager): JsonRe Log::warning('Invalid API token', ['provided' => $providedToken]); return response()->json(['message' => 'Unauthorized: Invalid token.'], 401); } - Cache::forget('getAllFlags'); - Cache::forget('feature_flags'); + $resolver->reload(); - $url = env('FEATURE_FLAGGING_CONFIG_URL'); + return response()->json([ + 'message' => 'Feature flags reloaded from GCS.' + ]); - if (app()->environment('testing') || !$url) { - return response()->json(['message' => 'Feature flagging disabled in this environment.'], 200); - } - - $res = Http::get($url); - - if (!$res->successful()) { - Log::error('Failed to fetch feature flags from GitHub', ['url' => $url]); - return response()->json(['message' => 'Failed to fetch feature flags.'], 500); - } - - $featureFlags = $res->json(); - - - - - if (!is_array($featureFlags)) { - return response()->json(['message' => 'Invalid feature flag format.'], 422); - } - - Log::info("Using feature flags from Bucket: " . print_r($featureFlags, true)); - - $flagManager->define($featureFlags); - - return response()->json(['message' => 'Feature flags defined successfully.'], 200); } /** * @OA\Get( @@ -115,9 +89,9 @@ public function index(Request $request, FeatureFlagManager $flagManager): JsonRe * ) * ) */ - public function getEnabledFeatures(Request $request, FeatureFlagManager $flagManager): JsonResponse + public function getEnabledFeatures(FeatureFlagManager $resolver): JsonResponse { - $allFlags = $flagManager->getAllFlags(); + $allFlags = $resolver->getEnabledFeatures(); return response()->json($allFlags, 200); } diff --git a/app/Providers/FeatureServiceProvider.php b/app/Providers/FeatureServiceProvider.php index 64602dbb9..c1adde694 100644 --- a/app/Providers/FeatureServiceProvider.php +++ b/app/Providers/FeatureServiceProvider.php @@ -2,64 +2,23 @@ namespace App\Providers; -use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; -use App\Services\FeatureFlagManager; -use Illuminate\Support\Facades\Cache; -use Illuminate\Http\Client\ConnectionException; +use Laravel\Pennant\Feature; +use App\FeatureFlags\FeatureFlagManager; class FeatureServiceProvider extends ServiceProvider { - public function boot() + public function register(): void { - $this->app->booted(function () { - logger()->info('Starting features'); - $url = env('FEATURE_FLAGGING_CONFIG_URL'); - - if (app()->environment('testing') || !$url) { - return; - } - - $featureFlags = Cache::remember('feature_flags', now()->addMinutes(10), function () use ($url) { - logger()->info('Calling that Bucket'); - - try { - $res = Http::timeout(60) - ->retry(3, 2000, function ($exception, $requestNumber) use ($url) { - logger()->warning('Retrying feature flag fetch', [ - 'url' => $url, - 'attempt' => $requestNumber, - 'error' => $exception->getMessage(), - ]); - }) - ->get($url); - } catch (ConnectionException $e) { - logger()->error('ConnectionException when fetching feature flags', [ - 'url' => $url, - 'error' => $e->getMessage(), - ]); - return []; - } - - if (!$res->successful()) { - logger()->error('Failed to fetch feature flags', [ - 'url' => $url, - 'status' => $res->status(), - 'body' => $res->body(), - ]); - return []; - } - - return $res->json(); - }); - - if (is_array($featureFlags) && !empty($featureFlags)) { - app(FeatureFlagManager::class)->define($featureFlags); - } else { - logger()->warning('No feature flags were defined - empty or failed response.', ['url' => $url]); - } + $this->app->singleton(FeatureFlagManager::class, function ($app) { + return new FeatureFlagManager(); }); } - + public function boot(): void + { + Feature::resolveScopeUsing( + $this->app->make(FeatureFlagManager::class) + ); + } } diff --git a/app/Services/FeatureFlagManager.php b/app/Services/FeatureFlagManager.php deleted file mode 100644 index c91b73978..000000000 --- a/app/Services/FeatureFlagManager.php +++ /dev/null @@ -1,59 +0,0 @@ - $value) { - $fullKey = $prefix ? "{$prefix}.{$key}" : $key; - - if (is_array($value)) { - if (array_key_exists('enabled', $value) && is_bool($value['enabled'])) { - Feature::define($fullKey, $value['enabled']); - - - Log::info("Feature flag defined: {$fullKey} = " . ($value['enabled'] ? 'ENABLED' : 'DISABLED')); - } - - - if (isset($value['features']) && is_array($value['features'])) { - $this->define($value['features'], $fullKey); - } - - - foreach ($value as $subKey => $subVal) { - if (is_array($subVal) && $subKey !== 'features' && $subKey !== 'enabled') { - $this->define([$subKey => $subVal], $fullKey); - } - } - } - } - } - - - public function getAllFlags(): array - { - $url = env('FEATURE_FLAGGING_CONFIG_URL'); - $featureFlags = Cache::remember('getAllFlags', now()->addMinutes(60), function () use ($url) { - $res = Http::get($url); - if ($res->successful()) { - return $res->json(); - } - - logger()->error('Failed to fetch feature flags from URL', ['url' => $url]); - return []; - }); - - - - - return $featureFlags; - } -} diff --git a/config/filesystems.php b/config/filesystems.php index 2cd9c93e9..8b9c6e04d 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -88,6 +88,17 @@ ], 'gcs' => [ + 'feature_flags' => [ + 'driver' => 'gcs', + 'key_file_path' => env('GOOGLE_APPLICATION_CREDENTIALS'), + 'project_id' => env('GOOGLE_CLOUD_PROJECT_ID'), + 'bucket' => env('GOOGLE_CLOUD_FEATURE_FLAGS_BUCKET'), + 'path_prefix' => env('GOOGLE_CLOUD_FEATURE_FLAGS_PATH_PREFIX', ''), + 'storage_api_uri' => env('GOOGLE_CLOUD_STORAGE_API_URI', null), + 'apiEndpoint' => env('GOOGLE_CLOUD_STORAGE_API_ENDPOINT', null), + 'visibility' => 'public', + 'throw' => false, + ], 'unscanned' => [ 'driver' => 'gcs', 'key_file_path' => env('GOOGLE_APPLICATION_CREDENTIALS'),