Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ trim_trailing_whitespace = true
[*.php]
insert_final_newline = true
indent_size = 4

[Makefile]
indent_style = tab
indent_size = 4
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ cache/
.phpunit*

example-1

monorepo
specs
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.PHONY: install test setup-monorepo update-monorepo test-example-project

install:
composer install

test:
composer test

setup-monorepo:
mkdir -p monorepo
if [ ! -d "monorepo/.git" ]; then \
git clone git@github.com:featurevisor/featurevisor.git monorepo; \
else \
(cd monorepo && git fetch origin main && git checkout main && git pull origin main); \
fi
(cd monorepo && make install && make build)

update-monorepo:
(cd monorepo && git pull origin main)

test-example-project:
./featurevisor test --projectDirectoryPath="./monorepo/examples/example-1"
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
- [Set sticky afterwards](#set-sticky-afterwards)
- [Setting datafile](#setting-datafile)
- [Updating datafile](#updating-datafile)
- [Interval-based update](#interval-based-update)
- [Logging](#logging)
- [Levels](#levels)
- [Customizing levels](#customizing-levels)
- [Handler](#handler)
- [Events](#events)
Expand Down Expand Up @@ -347,18 +347,21 @@ The triggers for setting the datafile again can be:
- a specific event in your application (like a user action), or
- an event served via websocket or server-sent events (SSE)

### Interval-based update

Here's an example of using interval-based update:

@TODO

## Logging

By default, Featurevisor SDKs will print out logs to the console for `info` level and above.
Featurevisor PHP-SDK by default uses [PSR-3 standard](https://www.php-fig.org/psr/psr-3/) simple implementation.
You can also choose from many mature implementations like e.g. [Monolog](https://github.com/Seldaek/monolog)

### Levels

These are all the available log levels:

- `error`
- `warning`
- `info`
- `debug`

### Customizing levels

If you choose `debug` level to make the logs more verbose, you can set it at the time of SDK initialization.
Expand Down Expand Up @@ -416,8 +419,6 @@ Featurevisor SDK implements a simple event emitter that allows you to listen to

You can listen to these events that can occur at various stages in your application:

@TODO: verify these events

### `datafile_set`

```php
Expand Down Expand Up @@ -515,8 +516,8 @@ $myCustomHook = [
// rest of the properties below are all optional per hook

// before evaluation
'before' => function (options) {
$type = $options['type']; // `feature` | `variation` | `variable`
'before' => function ($options) {
$type = $options['type']; // `flag` | `variation` | `variable`
$featureKey = $options['featureKey'];
$variableKey = $options['variableKey']; // if type is `variable`
$context = $options['context'];
Expand Down Expand Up @@ -655,9 +656,13 @@ $ vendor/bin/featurevisor test \
--quiet|verbose \
--onlyFailures \
--keyPattern="myFeatureKey" \
--assertionPattern="#1"
--assertionPattern="#1" \
--with-tags \
--with-scopes
```

If your assertions include `scope`, run tests with `--with-scopes` to evaluate against scoped datafiles generated on the fly via `npx featurevisor build --scope=<scopeName> --environment=<env> --json`.

### Benchmark

Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking).
Expand Down
161 changes: 118 additions & 43 deletions featurevisor
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ $cliOptions = [
'variation' => parseCliOption($argv, 'variation'),
'verbose' => parseCliOption($argv, 'verbose'),
'inflate' => parseCliOption($argv, 'inflate'),
'withScopes' => parseCliOption($argv, 'withScopes'),
'withTags' => parseCliOption($argv, 'withTags'),
'rootDirectoryPath' => $cwd,
'populateUuid' => array_reduce($argv, function($acc, $arg) {
if (strpos($arg, '--populateUuid=') === 0) {
Expand All @@ -70,13 +72,13 @@ function executeCommand(string $command): string {

function getConfig(string $featurevisorProjectPath): array {
echo "Getting config..." . PHP_EOL;
$configOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor config --json)");
$configOutput = executeCommand("(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor config --json)");
return json_decode($configOutput, true);
}

function getSegments(string $featurevisorProjectPath): array {
echo "Getting segments..." . PHP_EOL;
$segmentsOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor list --segments --json)");
$segmentsOutput = executeCommand("(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor list --segments --json)");
$segments = json_decode($segmentsOutput, true);
$segmentsByKey = [];
foreach ($segments as $segment) {
Expand All @@ -85,14 +87,74 @@ function getSegments(string $featurevisorProjectPath): array {
return $segmentsByKey;
}

function buildDatafiles(string $featurevisorProjectPath, array $environments): array {
$datafilesByEnvironment = [];
function buildSingleDatafile(
string $featurevisorProjectPath,
string $environment,
?string $tag = null,
?string $scope = null
): array {
$command = "(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor build --json";
$command .= " --environment=" . escapeshellarg($environment);

if ($tag) {
$command .= " --tag=" . escapeshellarg($tag);
}

if ($scope) {
$command .= " --scope=" . escapeshellarg($scope);
}

$command .= ")";
$output = executeCommand($command);

$decoded = json_decode($output, true);

return is_array($decoded) ? $decoded : [];
}

function buildDatafiles(string $featurevisorProjectPath, array $config, array $cliOptions): array {
$datafilesByKey = [];

$environments = $config['environments'] ?? [];
$scopes = $config['scopes'] ?? [];
$tags = $config['tags'] ?? [];

foreach ($environments as $environment) {
echo "Building datafile for environment: $environment..." . PHP_EOL;
$datafileOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor build --environment=$environment --json)");
$datafilesByEnvironment[$environment] = json_decode($datafileOutput, true);
$datafilesByKey[$environment] = buildSingleDatafile($featurevisorProjectPath, $environment);

if ($cliOptions['withScopes'] === true) {
foreach ($scopes as $scope) {
if (!isset($scope['name']) || !is_string($scope['name'])) {
continue;
}
$scopeName = $scope['name'];
echo "Building scoped datafile for environment: $environment, scope: $scopeName..." . PHP_EOL;
$datafilesByKey[$environment . '-scope-' . $scopeName] = buildSingleDatafile(
$featurevisorProjectPath,
$environment,
null,
$scopeName
);
}
}

if ($cliOptions['withTags'] === true) {
foreach ($tags as $tag) {
if (!is_string($tag)) {
continue;
}
echo "Building tagged datafile for environment: $environment, tag: $tag..." . PHP_EOL;
$datafilesByKey[$environment . '-tag-' . $tag] = buildSingleDatafile(
$featurevisorProjectPath,
$environment,
$tag
);
}
}
}
return $datafilesByEnvironment;

return $datafilesByKey;
}

function getLoggerLevel(array $cliOptions): string {
Expand Down Expand Up @@ -121,7 +183,7 @@ function getTests(string $featurevisorProjectPath, array $cliOptions): array {
$testsSuffix .= " --assertionPattern=" . $cliOptions['assertionPattern'];
}

$testsOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor list --tests --applyMatrix --json" . $testsSuffix . ")");
$testsOutput = executeCommand("(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor list --tests --applyMatrix --json" . $testsSuffix . ")");
return json_decode($testsOutput, true);
}

Expand All @@ -130,8 +192,8 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
$sticky = isset($assertion["sticky"]) ? $assertion["sticky"] : [];

// Update the SDK instance context and sticky values for this assertion
$f->setContext($context);
$f->setSticky($sticky);
$f->setContext($context, true);
$f->setSticky($sticky, true);

$hasError = false;
$errors = "";
Expand Down Expand Up @@ -187,7 +249,7 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
foreach ($expectedEvaluations["flag"] as $key => $expectedValue) {
if ($actualEvaluation[$key] !== $expectedValue) {
$hasError = true;
$errors .= " ✘ expectedEvaluations.flag.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($evaluation[$key]) . PHP_EOL;
$errors .= " ✘ expectedEvaluations.flag.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualEvaluation[$key] ?? null) . PHP_EOL;
}
}
}
Expand All @@ -199,7 +261,7 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
foreach ($expectedEvaluations["variation"] as $key => $expectedValue) {
if ($actualEvaluation[$key] !== $expectedValue) {
$hasError = true;
$errors .= " ✘ expectedEvaluations.variation.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($evaluation[$key]) . PHP_EOL;
$errors .= " ✘ expectedEvaluations.variation.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualEvaluation[$key] ?? null) . PHP_EOL;
}
}
}
Expand All @@ -214,7 +276,7 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
foreach ($expectedEvaluation as $key => $expectedValue) {
if ($actualEvaluation[$key] !== $expectedValue) {
$hasError = true;
$errors .= " ✘ expectedEvaluations.variables.$variableKey.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualValue[$key]) . PHP_EOL;
$errors .= " ✘ expectedEvaluations.variables.$variableKey.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualEvaluation[$key] ?? null) . PHP_EOL;
}
}
}
Expand Down Expand Up @@ -288,7 +350,7 @@ function test(array $cliOptions) {
$config = getConfig($featurevisorProjectPath);
$environments = $config['environments'];
$segmentsByKey = getSegments($featurevisorProjectPath);
$datafilesByEnvironment = buildDatafiles($featurevisorProjectPath, $environments);
$datafilesByKey = buildDatafiles($featurevisorProjectPath, $config, $cliOptions);

echo PHP_EOL;

Expand All @@ -300,27 +362,6 @@ function test(array $cliOptions) {
return;
}

// Create SDK instances for each environment
$sdkInstancesByEnvironment = [];
foreach ($environments as $environment) {
$datafile = $datafilesByEnvironment[$environment];
$sdkInstancesByEnvironment[$environment] = Featurevisor::createInstance([
'datafile' => $datafile,
'logger' => Logger::create([
'level' => $level,
]),
'hooks' => [
[
'name' => 'tester-hook',
'bucketValue' => function ($options) {
// This will be overridden per assertion if needed
return $options["bucketValue"];
}
]
]
]);
}

$passedTestsCount = 0;
$failedTestsCount = 0;
$passedAssertionsCount = 0;
Expand All @@ -337,29 +378,63 @@ function test(array $cliOptions) {
$testResult = [];

if (isset($test["feature"])) {
$environment = $assertion["environment"];
$f = $sdkInstancesByEnvironment[$environment];
$environment = $assertion["environment"] ?? null;
if (!$environment || !isset($datafilesByKey[$environment])) {
$testResult = [
'hasError' => true,
'errors' => " ✘ missing datafile for environment: " . json_encode($environment) . PHP_EOL,
'duration' => 0
];
} else {
$datafile = $datafilesByKey[$environment];

if (isset($assertion["scope"])) {
$scopeDatafileKey = $environment . '-scope-' . $assertion["scope"];
if ($cliOptions['withScopes'] === true && isset($datafilesByKey[$scopeDatafileKey])) {
$datafile = $datafilesByKey[$scopeDatafileKey];
} elseif ($cliOptions['withScopes'] !== true) {
$scope = null;
foreach ($config['scopes'] ?? [] as $scopeCandidate) {
if (($scopeCandidate['name'] ?? null) === $assertion["scope"]) {
$scope = $scopeCandidate;
break;
}
}

if ($scope && isset($scope['context']) && is_array($scope['context'])) {
$assertion['context'] = array_merge($scope['context'], $assertion['context'] ?? []);
}
}
}

if (isset($assertion["tag"])) {
$tagDatafileKey = $environment . '-tag-' . $assertion["tag"];
if ($cliOptions['withTags'] === true && isset($datafilesByKey[$tagDatafileKey])) {
$datafile = $datafilesByKey[$tagDatafileKey];
}
}

// If "at" parameter is provided, create a new SDK instance with the specific hook
if (isset($assertion["at"])) {
$datafile = $datafilesByEnvironment[$environment];
$f = Featurevisor::createInstance([
'datafile' => $datafile,
'logger' => Logger::create([
'level' => $level,
]),
'sticky' => $assertion['sticky'] ?? [],
'hooks' => [
[
'name' => 'tester-hook',
'bucketValue' => function ($options) use ($assertion) {
return $assertion["at"] * 1000;
if (isset($assertion["at"])) {
return $assertion["at"] * 1000;
}
return $options["bucketValue"];
}
]
]
]);
}

$testResult = testFeature($assertion, $test["feature"], $f, $level);
$testResult = testFeature($assertion, $test["feature"], $f, $level);
}
} else if (isset($test["segment"])) {
$testResult = testSegment($assertion, $segmentsByKey[$test["segment"]], $level);
}
Expand Down
Loading
Loading