Skip to content

Commit

Permalink
Update config JSON model to v6 + fix inconsistent error 1000 reportin…
Browse files Browse the repository at this point in the history
…g + improve config json deserialization error reporting
  • Loading branch information
adams85 committed Jan 26, 2024
1 parent ccebd01 commit d027e39
Show file tree
Hide file tree
Showing 36 changed files with 916 additions and 264 deletions.
14 changes: 0 additions & 14 deletions src/Attributes/Config.php

This file was deleted.

15 changes: 0 additions & 15 deletions src/Attributes/PercentageAttributes.php

This file was deleted.

14 changes: 0 additions & 14 deletions src/Attributes/Preferences.php

This file was deleted.

17 changes: 0 additions & 17 deletions src/Attributes/RolloutAttributes.php

This file was deleted.

17 changes: 0 additions & 17 deletions src/Attributes/SettingAttributes.php

This file was deleted.

6 changes: 2 additions & 4 deletions src/Cache/ConfigEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ConfigCat\Cache;

use ConfigCat\ConfigJson\Config;
use UnexpectedValueException;

/**
Expand Down Expand Up @@ -79,10 +80,7 @@ public static function empty(): ConfigEntry

public static function fromConfigJson(string $configJson, string $etag, float $fetchTime): ConfigEntry
{
$deserialized = json_decode($configJson, true);
if (null == $deserialized) {
return self::empty();
}
$deserialized = Config::deserialize($configJson);

return new ConfigEntry($configJson, $deserialized, $etag, $fetchTime);
}
Expand Down
61 changes: 38 additions & 23 deletions src/ConfigCatClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

namespace ConfigCat;

use ConfigCat\Attributes\Config;
use ConfigCat\Attributes\PercentageAttributes;
use ConfigCat\Attributes\RolloutAttributes;
use ConfigCat\Attributes\SettingAttributes;
use ConfigCat\Cache\ArrayCache;
use ConfigCat\Cache\ConfigCache;
use ConfigCat\Cache\ConfigEntry;
use ConfigCat\ConfigJson\Config;
use ConfigCat\ConfigJson\PercentageOption;
use ConfigCat\ConfigJson\Setting;
use ConfigCat\ConfigJson\SettingValue;
use ConfigCat\ConfigJson\SettingValueContainer;
use ConfigCat\ConfigJson\TargetingRule;
use ConfigCat\Log\DefaultLogger;
use ConfigCat\Log\InternalLogger;
use ConfigCat\Log\LogLevel;
Expand Down Expand Up @@ -420,7 +422,7 @@ public function isOffline(): bool

private function checkSettingsAvailable(SettingsResult $settingsResult, string $defaultReturnValue): bool
{
if (empty($settingsResult->settings)) {
if (!$settingsResult->hasConfigJson) {
$this->logger->error('Config JSON is not present. Returning '.$defaultReturnValue.'.', [
'event_id' => 1000,
]);
Expand Down Expand Up @@ -513,27 +515,38 @@ private function evaluate(string $key, array $setting, ?User $user, float $fetch
}

/**
* @param array<string, mixed> $json
* @param array<string, mixed> $settings
*/
private function parseKeyAndValue(array $json, string $variationId): ?Pair
private function parseKeyAndValue(array $settings, string $variationId): ?Pair
{
foreach ($json as $key => $value) {
if ($variationId == $value[SettingAttributes::VARIATION_ID]) {
return new Pair($key, $value[SettingAttributes::VALUE]);
}
foreach ($settings as $key => $setting) {
$settingType = Setting::getType(Setting::ensure($setting));

$rolloutRules = $value[SettingAttributes::ROLLOUT_RULES];
$percentageItems = $value[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS];
if ($variationId === ($setting[Setting::VARIATION_ID] ?? null)) {
return new Pair($key, SettingValue::get($setting[Setting::VALUE], $settingType));
}

foreach ($rolloutRules as $rolloutValue) {
if ($variationId == $rolloutValue[RolloutAttributes::VARIATION_ID]) {
return new Pair($key, $rolloutValue[RolloutAttributes::VALUE]);
$targetingRules = TargetingRule::ensureList($setting[Setting::TARGETING_RULES] ?? []);
foreach ($targetingRules as $targetingRule) {
if (TargetingRule::hasPercentageOptions(TargetingRule::ensure($targetingRule))) {
$percentageOptions = $targetingRule[TargetingRule::PERCENTAGE_OPTIONS];
foreach ($percentageOptions as $percentageOption) {
if ($variationId === ($percentageOption[PercentageOption::VARIATION_ID] ?? null)) {
return new Pair($key, SettingValue::get($percentageOption[PercentageOption::VALUE], $settingType));

Check warning on line 535 in src/ConfigCatClient.php

View check run for this annotation

Codecov / codecov/patch

src/ConfigCatClient.php#L531-L535

Added lines #L531 - L535 were not covered by tests
}
}
} else {
$simpleValue = $targetingRule[TargetingRule::SIMPLE_VALUE];
if ($variationId === ($simpleValue[SettingValueContainer::VARIATION_ID] ?? null)) {
return new Pair($key, SettingValue::get($simpleValue[SettingValueContainer::VALUE], $settingType));

Check warning on line 541 in src/ConfigCatClient.php

View check run for this annotation

Codecov / codecov/patch

src/ConfigCatClient.php#L539-L541

Added lines #L539 - L541 were not covered by tests
}
}
}

foreach ($percentageItems as $percentageValue) {
if ($variationId == $percentageValue[PercentageAttributes::VARIATION_ID]) {
return new Pair($key, $percentageValue[PercentageAttributes::VALUE]);
$percentageOptions = PercentageOption::ensureList($setting[Setting::PERCENTAGE_OPTIONS] ?? []);
foreach ($percentageOptions as $percentageOption) {
if ($variationId === ($percentageOption[PercentageOption::VARIATION_ID] ?? null)) {
return new Pair($key, SettingValue::get($percentageOption[PercentageOption::VALUE], $settingType));

Check warning on line 549 in src/ConfigCatClient.php

View check run for this annotation

Codecov / codecov/patch

src/ConfigCatClient.php#L548-L549

Added lines #L548 - L549 were not covered by tests
}
}
}
Expand All @@ -556,13 +569,13 @@ private function getSettingsResult(): SettingsResult
$local = $this->overrides->getDataSource()->getOverrides();
$remote = $this->getRemoteSettingsResult();

return new SettingsResult(array_merge($remote->settings, $local), $remote->fetchTime, $remote->hasConfigJson);
return new SettingsResult(array_merge($remote->settings, $local), $remote->fetchTime, true);

default: // remote over local
$local = $this->overrides->getDataSource()->getOverrides();
$remote = $this->getRemoteSettingsResult();

return new SettingsResult(array_merge($local, $remote->settings), $remote->fetchTime, $remote->hasConfigJson);
return new SettingsResult(array_merge($local, $remote->settings), $remote->fetchTime, true);
}
}

Expand All @@ -581,13 +594,15 @@ private function getRemoteSettingsResult(): SettingsResult
return new SettingsResult([], 0, false);
}

return new SettingsResult($cacheEntry->getConfig()[Config::ENTRIES], $cacheEntry->getFetchTime(), true);
$settings = Setting::ensureMap($cacheEntry->getConfig()[Config::SETTINGS] ?? []);

return new SettingsResult($settings, $cacheEntry->getFetchTime(), true);
}

private function handleResponse(FetchResponse $response, ConfigEntry $cacheEntry): ConfigEntry
{
if ($response->isFetched()) {
$this->hooks->fireOnConfigChanged($response->getConfigEntry()->getConfig()[Config::ENTRIES]);
$this->hooks->fireOnConfigChanged($response->getConfigEntry()->getConfig()[Config::SETTINGS] ?? []);
$this->cache->store($this->cacheKey, $response->getConfigEntry());

return $response->getConfigEntry();
Expand Down
28 changes: 14 additions & 14 deletions src/ConfigFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

namespace ConfigCat;

use ConfigCat\Attributes\Config;
use ConfigCat\Attributes\Preferences;
use ConfigCat\Cache\ConfigEntry;
use ConfigCat\ConfigJson\Config;
use ConfigCat\ConfigJson\Preferences;
use ConfigCat\ConfigJson\RedirectMode;
use ConfigCat\Http\FetchClientInterface;
use ConfigCat\Http\GuzzleFetchClient;
use ConfigCat\Log\InternalLogger;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use UnexpectedValueException;

/**
* Class ConfigFetcher This class is used to fetch the latest configuration.
Expand All @@ -21,15 +23,11 @@
final class ConfigFetcher
{
public const ETAG_HEADER = 'ETag';
public const CONFIG_JSON_NAME = 'config_v5.json';
public const CONFIG_JSON_NAME = 'config_v6.json';

public const GLOBAL_URL = 'https://cdn-global.configcat.com';
public const EU_ONLY_URL = 'https://cdn-eu.configcat.com';

public const NO_REDIRECT = 0;
public const SHOULD_REDIRECT = 1;
public const FORCE_REDIRECT = 2;

private InternalLogger $logger;
private string $urlPath;
private string $baseUrl;
Expand Down Expand Up @@ -105,16 +103,16 @@ private function executeFetch(?string $etag, string $url, int $executionCount):
}

$preferences = $response->getConfigEntry()->getConfig()[Config::PREFERENCES];
$redirect = $preferences[Preferences::REDIRECT];
if ($this->urlIsCustom && self::FORCE_REDIRECT != $redirect) {
$redirect = RedirectMode::from($preferences[Preferences::REDIRECT] ?? RedirectMode::NO->value);
if ($this->urlIsCustom && RedirectMode::FORCE != $redirect) {
return $response;
}

if (self::NO_REDIRECT == $redirect) {
if (RedirectMode::NO == $redirect) {
return $response;
}

if (self::SHOULD_REDIRECT == $redirect) {
if (RedirectMode::SHOULD == $redirect) {
$this->logger->warning(
'The `dataGovernance` parameter specified at the client initialization is '.
'not in sync with the preferences on the ConfigCat Dashboard. '.
Expand Down Expand Up @@ -159,11 +157,13 @@ private function sendConfigFetchRequest(?string $etag, string $url): FetchRespon
if ($statusCode >= 200 && $statusCode < 300) {
$this->logger->debug('Fetch was successful: new config fetched.');

$entry = ConfigEntry::fromConfigJson((string) $response->getBody(), $etag ?? '', Utils::getUnixMilliseconds());
if (JSON_ERROR_NONE !== json_last_error()) {
$message = 'Fetching config JSON was successful but the HTTP response content was invalid. JSON error: '.json_last_error_msg();
try {
$entry = ConfigEntry::fromConfigJson((string) $response->getBody(), $etag ?? '', Utils::getUnixMilliseconds());
} catch (UnexpectedValueException $ex) {
$message = 'Fetching config JSON was successful but the HTTP response content was invalid.';
$messageCtx = [
'event_id' => 1105,
'exception' => $ex,
];
$this->logger->error($message, $messageCtx);

Expand Down
15 changes: 15 additions & 0 deletions src/ConfigJson/Condition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace ConfigCat\ConfigJson;

/**
* Represents the JSON keys of a condition.
*/
abstract class Condition
{
public const USER_CONDITION = 'u';
public const PREREQUISITE_FLAG_CONDITION = 'p';
public const SEGMENT_CONDITION = 's';
}
66 changes: 66 additions & 0 deletions src/ConfigJson/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace ConfigCat\ConfigJson;

use UnexpectedValueException;

/**
* Represents the root JSON keys of a ConfigCat config_v6.json file.
*/
abstract class Config
{
/**
* Preferences of the config.json, mostly for controlling the redirection behaviour of the SDK.
*/
public const PREFERENCES = 'p';

/**
* Segment definitions for re-using segment rules in targeting rules.
*/
public const SEGMENTS = 's';

/**
* Setting definitions.
*/
public const SETTINGS = 'f';

/**
* @param array<string, mixed> $config
* @internal
*/
public static function fixup(array &$config): void
{
$settings = &$config[self::SETTINGS] ?? [];
if (is_array($settings) && !empty($settings)) {
$salt = $config[self::PREFERENCES][Preferences::SALT] ?? null;
$segments = $config[self::SEGMENTS] ?? [];

foreach ($settings as &$setting) {
$setting[Setting::CONFIG_JSON_SALT] = $salt;
$setting[Setting::CONFIG_SEGMENTS] = $segments;
}
}
}

/**
* @return array<string, mixed>
* @throws UnexpectedValueException
*/
public static function deserialize(string $json): array
{
$config = json_decode($json, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new UnexpectedValueException('JSON error: '.json_last_error_msg());
}

if (!is_array($config)) {
throw new UnexpectedValueException('Invalid config JSON content: '.$json);

Check warning on line 59 in src/ConfigJson/Config.php

View check run for this annotation

Codecov / codecov/patch

src/ConfigJson/Config.php#L59

Added line #L59 was not covered by tests
}

self::fixup($config);

return $config;
}
}
Loading

0 comments on commit d027e39

Please sign in to comment.