Skip to content

Commit

Permalink
Release v1.12.0
Browse files Browse the repository at this point in the history
  • Loading branch information
imjoehaines authored May 20, 2022
2 parents 113a067 + c7e1aaf commit 0cb1831
Show file tree
Hide file tree
Showing 27 changed files with 730 additions and 5 deletions.
2 changes: 1 addition & 1 deletion BugsnagBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ class BugsnagBundle extends Bundle
*
* @return string
*/
const VERSION = '1.11.2';
const VERSION = '1.12.0';
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

## 1.12.0 (2022-05-20)

### Enhancements

* New APIs to support feature flag and experiment functionality. For more information, please see https://docs.bugsnag.com/product/features-experiments.
[#153](https://github.com/bugsnag/bugsnag-symfony/pull/153)

## 1.11.2 (2022-02-02)

### Bug Fixes
Expand Down
28 changes: 27 additions & 1 deletion DependencyInjection/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Bugsnag\Callbacks\CustomUser;
use Bugsnag\Client;
use Bugsnag\Configuration as Config;
use Bugsnag\FeatureFlag;
use Bugsnag\Shutdown\ShutdownStrategyInterface;
use GuzzleHttp;
use Symfony\Component\HttpKernel\Kernel;
Expand Down Expand Up @@ -210,6 +211,16 @@ class ClientFactory
*/
private $redactedKeys;

/**
* An array of feature flags with a "name" and optional "variant".
*
* For example:
* [['name' => 'example', 'variant' => 'test'], ['name' => 'another name']]
*
* @var array[]
*/
private $featureFlags;

/**
* @param SymfonyResolver $resolver
* @param TokenStorageInterface|null $tokens
Expand Down Expand Up @@ -237,6 +248,7 @@ class ClientFactory
* @param int|null|false $memoryLimitIncrease
* @param array $discardClasses
* @param string[] $redactedKeys
* @param array[] $featureFlags
*
* @return void
*/
Expand Down Expand Up @@ -266,7 +278,8 @@ public function __construct(
GuzzleHttp\ClientInterface $guzzle = null,
$memoryLimitIncrease = false,
array $discardClasses = [],
array $redactedKeys = []
array $redactedKeys = [],
array $featureFlags = []
) {
$this->resolver = $resolver;
$this->tokens = $tokens;
Expand Down Expand Up @@ -296,6 +309,7 @@ public function __construct(
$this->memoryLimitIncrease = $memoryLimitIncrease;
$this->discardClasses = $discardClasses;
$this->redactedKeys = $redactedKeys;
$this->featureFlags = $featureFlags;
}

/**
Expand Down Expand Up @@ -365,6 +379,18 @@ public function make()
$client->setRedactedKeys($this->redactedKeys);
}

if ($this->featureFlags) {
$featureFlags = array_map(function (array $flag) {
if (array_key_exists('variant', $flag)) {
return new FeatureFlag($flag['name'], $flag['variant']);
}

return new FeatureFlag($flag['name']);
}, $this->featureFlags);

$client->addFeatureFlags($featureFlags);
}

return $client;
}

Expand Down
18 changes: 18 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ public function getConfigTreeBuilder()
->treatNullLike([])
->defaultValue([])
->end()
->arrayNode('feature_flags')
->prototype('array')
->children()
->scalarNode('name')
->isRequired()
->validate()
->ifTrue(function ($value) {
return !is_string($value);
})
->thenInvalid('Feature flag name should be a string, got %s')
->end()
->end()
->scalarNode('variant')->end()
->end()
->end()
->treatNullLike([])
->defaultValue([])
->end()
->end();

return $treeBuilder;
Expand Down
1 change: 1 addition & 0 deletions Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ services:
- '%bugsnag.memory_limit_increase%'
- '%bugsnag.discard_classes%'
- '%bugsnag.redacted_keys%'
- '%bugsnag.feature_flags%'

bugsnag:
class: '%bugsnag.client%'
Expand Down
26 changes: 26 additions & 0 deletions Tests/DependencyInjection/ClientFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,32 @@ public function testRedactedKeysIsSetCorrectly()
$this->assertSame($redactedKeys, $actual);
}

public function testFeatureFlagsAreSetCorrectly()
{
$client = $this->createClient([
'featureFlags' => [
['name' => 'flag1'],
['name' => 'flag2', 'variant' => '1'],
['name' => 'flag3', 'variant' => 2],
['name' => 'flag4', 'variant' => null],
],
]);

$this->assertInstanceOf(Client::class, $client);

$expected = [
['featureFlag' => 'flag1'],
['featureFlag' => 'flag2', 'variant' => '1'],
['featureFlag' => 'flag3', 'variant' => '2'],
['featureFlag' => 'flag4'],
];

/** @var Client $client */
$actual = $client->getConfig()->getFeatureFlagsCopy()->toArray();

$this->assertSame($expected, $actual);
}

/**
* Get the value of the given property on the given object.
*
Expand Down
179 changes: 179 additions & 0 deletions Tests/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

namespace Bugsnag\BugsnagBundle\Tests\DependencyInjection;

use Bugsnag\BugsnagBundle\DependencyInjection\ClientFactory;
use Bugsnag\BugsnagBundle\DependencyInjection\Configuration;
use Bugsnag\BugsnagBundle\EventListener\BugsnagListener;
use Bugsnag\BugsnagBundle\EventListener\BugsnagShutdown;
use Bugsnag\BugsnagBundle\Request\SymfonyResolver;
use Bugsnag\Client;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;

final class ConfigurationTest extends TestCase
{
private $defaultConfiguration = [
'api_key' => null,
'endpoint' => null,
'callbacks' => true,
'user' => true,
'app_type' => null,
'app_version' => null,
'batch_sending' => true,
'hostname' => null,
'send_code' => true,
'release_stage' => null,
'strip_path' => null,
'project_root' => null,
'auto_notify' => true,
'resolver' => SymfonyResolver::class,
'factory' => ClientFactory::class,
'client' => Client::class,
'listener' => BugsnagListener::class,
'notify_release_stages' => [],
'filters' => [],
'shutdown' => BugsnagShutdown::class,
'strip_path_regex' => null,
'project_root_regex' => null,
'guzzle' => null,
'memory_limit_increase' => false,
'discard_classes' => [],
'redacted_keys' => [],
'feature_flags' => [],
];

/**
* @dataProvider configProvider
*
* @param array $input
* @param array $expected
*
* @return void
*/
public function testConfiguration(array $input, array $expected)
{
$actual = $this->processConfiguration($input);

$this->assertSame($expected, $actual);
}

public function configProvider()
{
return [
'no provided config' => [
[],
$this->buildExpectedConfiguration(),
],
'some provided config' => [
$someConfig = [
'api_key' => 'key123',
'callbacks' => false,
'send_code' => false,
'notify_release_stages' => ['one', 'two', 'three'],
'strip_path_regex' => '/abc/',
'memory_limit_increase' => 1234,
],
$this->buildExpectedConfiguration($someConfig),
],
'all provided config' => [
$fullConfig = [
'api_key' => 'my api key',
'endpoint' => 'https://example.com',
'callbacks' => false,
'user' => false,
'app_type' => 'good',
'app_version' => '1.2.3',
'batch_sending' => false,
'hostname' => 'example.com',
'send_code' => false,
'release_stage' => 'staging',
'strip_path' => '/a/b/c',
'project_root' => '/x/y/z',
'auto_notify' => false,
'resolver' => 'MyResolver',
'factory' => 'MyFactory',
'client' => 'MyClient',
'listener' => 'MyListener',
'notify_release_stages' => ['not staging'],
'filters' => ['a', 'b', 'c'],
'shutdown' => 'MyShutdown',
'strip_path_regex' => '/abc/',
'project_root_regex' => '/xyz/',
'guzzle' => 'MyGuzzle',
'memory_limit_increase' => 1234,
'discard_classes' => ['SomeClass', 'AnotherClass'],
'redacted_keys' => ['one', 'two'],
'feature_flags' => [
['name' => 'flag1'],
['name' => 'flag2', 'variant' => 'var1'],
],
],
$this->buildExpectedConfiguration($fullConfig),
],
];
}

public function testFeatureFlagsRequireAName()
{
if (method_exists(TestCase::class, 'expectException')) {
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Unrecognized option "not name" under "bugsnag.feature_flags.1"');
} else {
$this->setExpectedException(
InvalidConfigurationException::class,
'Unrecognized option "not name" under "bugsnag.feature_flags.1"'
);
}

$this->processConfiguration([
'feature_flags' => [
['name' => 'flag1'],
['not name' => 'flag2'],
],
]);
}

public function testFeatureFlagNameMustBeAString()
{
if (method_exists(TestCase::class, 'expectException')) {
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Invalid configuration for path "bugsnag.feature_flags.2.name": Feature flag name should be a string, got 3');
} else {
$this->setExpectedException(
InvalidConfigurationException::class,
'Invalid configuration for path "bugsnag.feature_flags.2.name": Feature flag name should be a string, got 3'
);
}

$this->processConfiguration([
'feature_flags' => [
['name' => 'flag1'],
['name' => 'flag2'],
['name' => 3],
],
]);
}

private function processConfiguration(array $input)
{
$configuration = new Configuration();
$node = $configuration->getConfigTreeBuilder()->buildTree();

return $node->finalize($node->normalize($input));
}

private function buildExpectedConfiguration(array $overrides = [])
{
if ($overrides === []) {
return $this->defaultConfiguration;
}

// find values that have not changed from the default configuration
$defaults = array_diff_key($this->defaultConfiguration, $overrides);

// append the defaults to the overrides - there are no shared keys
// because $defaults only contains keys not present in $overrides
return array_merge($overrides, $defaults);
}
}
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
],
"require": {
"php": ">=5.5",
"bugsnag/bugsnag": "^3.26.0",
"bugsnag/bugsnag": "^3.28.0",
"symfony/config": "^2.7|^3|^4|^5|^6",
"symfony/console": "^2.7|^3|^4|^5|^6",
"symfony/dependency-injection": "^2.7|^3|^4|^5|^6",
Expand Down
Loading

0 comments on commit 0cb1831

Please sign in to comment.