Full Code of ekino/EkinoNewRelicBundle for AI

master aa29fd8e1f20 cached
55 files
168.9 KB
41.0k tokens
312 symbols
1 requests
Download .txt
Repository: ekino/EkinoNewRelicBundle
Branch: master
Commit: aa29fd8e1f20
Files: 55
Total size: 168.9 KB

Directory structure:
gitextract_cqkhw3dv/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── Command/
│   └── NotifyDeploymentCommand.php
├── DependencyInjection/
│   ├── Compiler/
│   │   └── MonologHandlerPass.php
│   ├── Configuration.php
│   └── EkinoNewRelicExtension.php
├── EkinoNewRelicBundle.php
├── Exception/
│   └── DeprecationException.php
├── LICENSE
├── Listener/
│   ├── CommandListener.php
│   ├── DeprecationListener.php
│   ├── ExceptionListener.php
│   ├── RequestListener.php
│   └── ResponseListener.php
├── Logging/
│   └── AdaptiveHandler.php
├── NewRelic/
│   ├── AdaptiveInteractor.php
│   ├── BlackholeInteractor.php
│   ├── Config.php
│   ├── LoggingInteractorDecorator.php
│   ├── NewRelicInteractor.php
│   └── NewRelicInteractorInterface.php
├── README.md
├── Resources/
│   ├── config/
│   │   ├── command_listener.xml
│   │   ├── deprecation_listener.xml
│   │   ├── exception_listener.xml
│   │   ├── http_listener.xml
│   │   ├── monolog.xml
│   │   ├── services.xml
│   │   └── twig.xml
│   └── recipes/
│       └── newrelic.rb
├── Tests/
│   ├── AppKernel.php
│   ├── BundleInitializationTest.php
│   ├── DependencyInjection/
│   │   ├── Compiler/
│   │   │   └── MonologHandlerPassTest.php
│   │   ├── ConfigurationTest.php
│   │   └── EkinoNewRelicExtensionTest.php
│   ├── Listener/
│   │   ├── CommandListenerTest.php
│   │   ├── DeprecationListenerTest.php
│   │   ├── ExceptionListenerTest.php
│   │   ├── RequestListenerTest.php
│   │   └── ResponseListenerTest.php
│   ├── NewRelic/
│   │   ├── ConfigTest.php
│   │   └── LoggingInteractorDecoratorTest.php
│   ├── TransactionNamingStrategy/
│   │   └── ControllerNamingStrategyTest.php
│   └── Twig/
│       └── NewRelicExtensionTest.php
├── TransactionNamingStrategy/
│   ├── ControllerNamingStrategy.php
│   ├── RouteNamingStrategy.php
│   └── TransactionNamingStrategyInterface.php
├── Twig/
│   └── NewRelicExtension.php
├── UPGRADE-2.0.md
├── composer.json
└── phpunit.xml.dist

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .gitattributes
================================================
.editorconfig     export-ignore
.gitattributes    export-ignore
.gitignore        export-ignore
/.php_cs          export-ignore
/.scrutinizer.yml export-ignore
/.styleci.yml     export-ignore
/.travis.yml      export-ignore
/phpunit.xml.dist export-ignore
/Tests/           export-ignore


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  pull_request: ~
  push:
    branches:
      - master

jobs:
  php-cs-fixer:
    name: PHP-CS-Fixer
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: PHP-CS-Fixer
      uses: docker://oskarstark/php-cs-fixer-ga

  build:
    name: Build
    runs-on: Ubuntu-20.04
    strategy:
      fail-fast: false
      matrix:
        php: ['7.1', '7.2', '7.3', '7.4', '8.0']
        composer: ['']
        phpunit: ['']
        deprecation: ['']
        symfony: ['']
        stability: ['']
        include:
          # Minimum supported dependencies with the latest and oldest PHP version
          - php: 8.0
            composer: --prefer-stable --prefer-lowest
            deprecation: max[direct]=0
          - php: 7.1
            composer: --prefer-stable --prefer-lowest
            deprecation: max[direct]=0

          # symfony version
          - php: 8.0
            symfony: '^3.0'
          - php: 8.0
            symfony: '^4.0'
          - php: 8.0
            symfony: '^5.0'

          # dev
          - php: 8.0
            stability: 'dev'

    steps:
      - name: Set up PHP
        uses: shivammathur/setup-php@2.7.0
        with:
          php-version: ${{ matrix.php }}
          coverage: none
          tools: flex

      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup stability
        if: matrix.stability != ''
        run: composer config minimum-stability "${{ matrix.stability }}"

      - name: Setup deprecation
        if: matrix.deprecation != ''
        run: echo 'SYMFONY_DEPRECATIONS_HELPER=${{ matrix.deprecation }}' >> $GITHUB_ENV

      - name: Setup symfony
        if: matrix.symfony != ''
        run: |
          echo 'SYMFONY_REQUIRE=${{ matrix.symfony }}' >> $GITHUB_ENV

      - name: Download dependencies
        run: |
          composer update ${{ matrix.composer}} --prefer-dist --no-interaction
          ./vendor/bin/simple-phpunit install

      - name: Validate
        run: |
            composer validate --strict --no-check-lock

      - name: Run tests
        env:
          SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: 1
        run: |
          ${{ matrix.phpunit }} ./vendor/bin/simple-phpunit


================================================
FILE: .gitignore
================================================
.php_cs.cache
build
phpunit.xml
coverage
composer.lock
vendor


================================================
FILE: .php-cs-fixer.php
================================================
<?php

$header = <<<'EOF'
This file is part of Ekino New Relic bundle.

(c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>

For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;

return (new PhpCsFixer\Config())
    ->setRiskyAllowed(true)
    ->setRules([
        '@Symfony' => true,
        '@Symfony:risky' => true,
        'array_syntax' => ['syntax' => 'short'],
        'ordered_imports' => true,
        'header_comment' => ['header' => $header],
        'linebreak_after_opening_tag' => true,
        'modernize_types_casting' => true,
        'no_superfluous_elseif' => true,
        'no_useless_else' => true,
        'phpdoc_order' => true,
        'psr_autoloading' => true,
        'simplified_null_return' => true,
        'php_unit_strict' => true,
        'no_useless_return' => true,
        'strict_param' => true,
        'strict_comparison' => true,
        'yoda_style' => true,
        'declare_strict_types' => true,
        'native_function_invocation' => true,
    ])
    ->setFinder(
        PhpCsFixer\Finder::create()
            ->in(__DIR__)
            ->exclude('vendor')
            ->name('*.php')
    )
    ->setCacheFile(__DIR__.'/.php_cs.cache')
    ->setUsingCache(true)
;



================================================
FILE: CHANGELOG.md
================================================
# Changelog

## v2.4.0

### Added

- Support for distributed tracing functions provided by the NewRelic PHP extension are now supported in the `NewRelicInteractorInterface`

## v2.3.0

### Added

- Make recordDatastoreSegment execute the callback

## v2.2.3

### Added

- Allow Symfony 6.0
- Fix Symfony 5.3 deprecations

## v2.2.2

### Added

- Test against PHP 8.0

## v2.2.1

### Fixed

- Fixed class loaded twice.

## v2.2.0

### Added

- Added `api_host` configuration property used by `NotifyDeploymentCommand`

## v2.1.3

### Added

- Test against PHP 7.4
- Use typehinted alias in EventListener

### Fixed

- Wrong event handled in RequestListener

## v2.1.2

### Fixed

- Fixed compatibility issues with Symfony 5.0
- Handle new ResponseEvent, RequestEvent and ExceptionEvent in EventListeners

## v2.1.1

### Added

- Allow Symfony 5.0

## v2.1.0

### Added

- More detail/context when PSR-3 Logging the Newrelic transactions

### Fixed

- Even when handling a streamed response should call 'endTransaction' on onKernelResponse even
- Warnings in PHP 7.4
- Stop using Twig deprecated classes

## v2.0.2

### Changed

- Remove deprecations triggered by Symfony 4.0.
- Excluded tests from classmap.

### Fixed

- Fixed call to non-allowed method `setContent` on a `StreamedResponse`.
- Fixed multiple decoration of error handler when the bundle is often started and stopped like in test suite.
- Fixed issue in monolog's service configuration that does not allows application's services or aliases.

## v2.0.1

### Fixed

- Fixed type error when configuration's property `deployment_names` is not a string

## v2.0.0

### Changed

- Update the return type annotation of `NewRelicInteractorInterface::disableAutoRUM` to `?bool`
  to match the latest changes in the NewRelic API.

## v2.0.0-beta5

### Fixed

- Memory leak in the `ResponseListener` that may cause issues on large HTML responses.
- Fixed type error when no Content-Type header was returned.
- Make sure `NewRelicInteractor::disableAutoRUM` always returns true.

## v2.0.0-beta4

### Changed

- Changed the configuration for monolog's channel to a configuration similar to MonologBundle.

## v2.0.0-beta3

### Changed

- Moved "instrument" to the root level
- The `AdaptiveInteractor` is now the default interactor.

### Fixed

- Bug where logging deprecations did not work.

## v2.0.0-beta2

### Changed

- Add default "deployment_names"
- Updated interface variable names to match the NewRelic extension.


## v2.0.0-beta1

### Added

- All functions provided by the NewRelic PHP extension are now supported in the `NewRelicInteractorInterface`.
- Added a new `deprecations` parameter to logs `E_USER_DEPRECATED`.
- Added a new `monolog` parameter to send logs to new relic.

### Changed

- Command Configuration explicit
- The configuration syntax
- The bundle uses class-named service ids. See `UPGRADE-2.0.md` for the exhaustive list of changes

### Removed

- Support for Silex
- Support for PHP < 7.1
- Support for Symfony < 3.4


================================================
FILE: Command/NotifyDeploymentCommand.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Command;

use Ekino\NewRelicBundle\NewRelic\Config;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class NotifyDeploymentCommand extends Command
{
    public const EXIT_NO_APP_NAMES = 1;
    public const EXIT_UNAUTHORIZED = 2;
    public const EXIT_HTTP_ERROR = 3;

    protected static $defaultName = 'newrelic:notify-deployment';

    private $newrelic;

    public function __construct(Config $newrelic)
    {
        $this->newrelic = $newrelic;

        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->setDefinition([
                new InputOption(
                    'user', null, InputOption::VALUE_OPTIONAL,
                    'The name of the user/process that triggered this deployment', null
                ),
                new InputOption(
                    'revision', null, InputOption::VALUE_OPTIONAL,
                    'A revision number (e.g., git commit SHA)', null
                ),
                new InputOption(
                    'changelog', null, InputOption::VALUE_OPTIONAL,
                    'A list of changes for this deployment', null
                ),
                new InputOption(
                    'description', null, InputOption::VALUE_OPTIONAL,
                    'Text annotation for the deployment — notes for you', null
                ),
            ])
            ->setDescription('Notifies New Relic that a new deployment has been made')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $appNames = $this->newrelic->getDeploymentNames();

        if (!$appNames) {
            $output->writeLn('<error>No deployment application configured.</error>');

            return self::EXIT_NO_APP_NAMES;
        }

        $exitCode = 0;

        foreach ($appNames as $appName) {
            $response = $this->performRequest($this->newrelic->getApiKey(), $this->createPayload($appName, $input), $this->newrelic->getApiHost());

            switch ($response['status']) {
                case 200:
                case 201:
                    $output->writeLn(sprintf("Recorded deployment to '%s' (%s)", $appName, ($input->getOption('description') ?: date('r'))));
                    break;
                case 403:
                    $output->writeLn(sprintf("<error>Deployment not recorded to '%s': API key invalid</error>", $appName));
                    $exitCode = self::EXIT_UNAUTHORIZED;
                    break;
                case null:
                    $output->writeLn(sprintf("<error>Deployment not recorded to '%s': Did not understand response</error>", $appName));
                    $exitCode = self::EXIT_HTTP_ERROR;
                    break;
                default:
                    $output->writeLn(sprintf("<error>Deployment not recorded to '%s': Received HTTP status %d</error>", $appName, $response['status']));
                    $exitCode = self::EXIT_HTTP_ERROR;
                    break;
            }
        }

        return $exitCode;
    }

    public function performRequest(string $api_key, string $payload, ?string $api_host = null): array
    {
        $headers = [
            sprintf('x-api-key: %s', $api_key),
            'Content-type: application/x-www-form-urlencoded',
        ];

        $context = [
            'http' => [
                'method' => 'POST',
                'header' => implode("\r\n", $headers),
                'content' => $payload,
                'ignore_errors' => true,
            ],
        ];

        $level = error_reporting(0);
        $content = file_get_contents(sprintf('https://%s/deployments.xml', $api_host ?? 'api.newrelic.com'), false, stream_context_create($context));
        error_reporting($level);
        if (false === $content) {
            $error = error_get_last();
            throw new \RuntimeException($error['message']);
        }

        $response = [
            'status' => null,
            'error' => null,
        ];

        if (isset($http_response_header[0])) {
            preg_match('/^HTTP\/1.\d (\d+)/', $http_response_header[0], $matches);

            if (isset($matches[1])) {
                $status = $matches[1];

                $response['status'] = $status;

                preg_match('/<error>(.*?)<\/error>/', $content, $matches);

                if (isset($matches[1])) {
                    $response['error'] = $matches[1];
                }
            }
        }

        return $response;
    }

    private function createPayload(string $appName, InputInterface $input): string
    {
        $content_array = [
            'deployment[app_name]' => $appName,
        ];

        if (($user = $input->getOption('user'))) {
            $content_array['deployment[user]'] = $user;
        }

        if (($revision = $input->getOption('revision'))) {
            $content_array['deployment[revision]'] = $revision;
        }

        if (($changelog = $input->getOption('changelog'))) {
            $content_array['deployment[changelog]'] = $changelog;
        }

        if (($description = $input->getOption('description'))) {
            $content_array['deployment[description]'] = $description;
        }

        return http_build_query($content_array);
    }
}


================================================
FILE: DependencyInjection/Compiler/MonologHandlerPass.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

class MonologHandlerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if (!$container->hasParameter('ekino.new_relic.monolog') || !$container->hasDefinition('monolog.logger')) {
            return;
        }

        $configuration = $container->getParameter('ekino.new_relic.monolog');
        if ($container->hasDefinition('ekino.new_relic.logs_handler') && $container->hasParameter('ekino.new_relic.application_name')) {
            $container->findDefinition('ekino.new_relic.logs_handler')
                ->setArgument('$level', \is_int($configuration['level']) ? $configuration['level'] : \constant('Monolog\Logger::'.strtoupper($configuration['level'])))
                ->setArgument('$bubble', true)
                ->setArgument('$appName', $container->getParameter('ekino.new_relic.application_name'));
        }

        if (!isset($configuration['channels'])) {
            $channels = $this->getChannels($container);
        } elseif ('inclusive' === $configuration['channels']['type']) {
            $channels = $configuration['channels']['elements'] ?: $this->getChannels($container);
        } else {
            $channels = array_diff($this->getChannels($container), $configuration['channels']['elements']);
        }

        foreach ($channels as $channel) {
            try {
                $def = $container->getDefinition('app' === $channel ? 'monolog.logger' : 'monolog.logger.'.$channel);
            } catch (InvalidArgumentException $e) {
                $msg = 'NewRelicBundle configuration error: The logging channel "'.$channel.'" does not exist.';
                throw new \InvalidArgumentException($msg, 0, $e);
            }
            $def->addMethodCall('pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
        }
    }

    private function getChannels(ContainerBuilder $container)
    {
        $channels = [];
        foreach ($container->getDefinitions() as $id => $definition) {
            if ('monolog.logger' === $id) {
                $channels[] = 'app';
                continue;
            }
            if (0 === strpos($id, 'monolog.logger.')) {
                $channels[] = substr($id, 15);
            }
        }

        return $channels;
    }
}


================================================
FILE: DependencyInjection/Configuration.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\DependencyInjection;

use Psr\Log\LogLevel;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Twig\Environment;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('ekino_new_relic');
        if (method_exists(TreeBuilder::class, 'getRootNode')) {
            $rootNode = $treeBuilder->getRootNode();
        } else {
            $rootNode = $treeBuilder->root('ekino_new_relic');
        }

        $rootNode
            ->fixXmlConfig('deployment_name')
            ->children()
                ->booleanNode('enabled')->defaultTrue()->end()
                ->scalarNode('interactor')->end()
                ->booleanNode('twig')->defaultValue(class_exists(Environment::class))->end()
                ->scalarNode('api_key')->defaultValue(null)->end()
                ->scalarNode('api_host')->defaultValue(null)->end()
                ->scalarNode('license_key')->defaultValue(null)->end()
                ->scalarNode('application_name')->defaultValue(null)->end()
                ->arrayNode('deployment_names')
                    ->prototype('scalar')
                    ->end()
                    ->beforeNormalization()
                        ->ifTrue(function ($v) { return !\is_array($v); })
                        ->then(function ($v) { return array_values(array_filter(explode(';', (string) $v))); })
                    ->end()
                ->end()
                ->scalarNode('xmit')->defaultValue(false)->end()
                ->booleanNode('logging')
                    ->info('Write logs to a PSR3 logger whenever we send data to NewRelic.')
                    ->defaultFalse()
                ->end()
                ->arrayNode('exceptions')
                    ->canBeDisabled()
                ->end()
                ->arrayNode('commands')
                    ->canBeDisabled()
                    ->children()
                        ->arrayNode('ignored_commands')
                            ->prototype('scalar')
                            ->end()
                            ->beforeNormalization()
                                ->ifTrue(function ($v) { return !\is_array($v); })
                                ->then(function ($v) { return (array) $v; })
                            ->end()
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('deprecations')
                    ->canBeDisabled()
                ->end()
                ->arrayNode('http')
                    ->canBeDisabled()
                    ->children()
                        ->scalarNode('transaction_naming')
                            ->defaultValue('route')
                            ->validate()
                                ->ifNotInArray(['route', 'controller', 'service'])
                                ->thenInvalid('Invalid transaction naming scheme "%s", must be "route", "controller" or "service".')
                            ->end()
                        ->end()
                        ->scalarNode('transaction_naming_service')->defaultNull()->end()
                        ->arrayNode('ignored_routes')
                            ->prototype('scalar')
                            ->end()
                            ->beforeNormalization()
                                ->ifTrue(function ($v) { return !\is_array($v); })
                                ->then(function ($v) { return (array) $v; })
                            ->end()
                        ->end()
                        ->arrayNode('ignored_paths')
                            ->prototype('scalar')
                            ->end()
                            ->beforeNormalization()
                                ->ifTrue(function ($v) { return !\is_array($v); })
                                ->then(function ($v) { return (array) $v; })
                            ->end()
                        ->end()
                        ->scalarNode('using_symfony_cache')->defaultFalse()->end()
                    ->end()
                ->end()
                ->booleanNode('instrument')
                    ->defaultFalse()
                ->end()
                ->arrayNode('monolog')
                    ->canBeEnabled()
                    ->children()
                        ->arrayNode('channels')
                            ->fixXmlConfig('channel', 'elements')
                            ->canBeUnset()
                            ->beforeNormalization()
                                ->ifString()
                                ->then(function ($v) { return ['elements' => [$v]]; })
                            ->end()
                            ->beforeNormalization()
                                ->ifTrue(function ($v) { return \is_array($v) && is_numeric(key($v)); })
                                ->then(function ($v) { return ['elements' => $v]; })
                            ->end()
                            ->validate()
                                ->ifTrue(function ($v) { return empty($v); })
                                ->thenUnset()
                            ->end()
                            ->validate()
                                ->always(function ($v) {
                                    $isExclusive = null;
                                    if (isset($v['type'])) {
                                        $isExclusive = 'exclusive' === $v['type'];
                                    }

                                    $elements = [];
                                    foreach ($v['elements'] as $element) {
                                        if (0 === strpos($element, '!')) {
                                            if (false === $isExclusive) {
                                                throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list.');
                                            }
                                            $elements[] = substr($element, 1);
                                            $isExclusive = true;
                                        } else {
                                            if (true === $isExclusive) {
                                                throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list');
                                            }
                                            $elements[] = $element;
                                            $isExclusive = false;
                                        }
                                    }

                                    if (!\count($elements)) {
                                        return;
                                    }

                                    return ['type' => $isExclusive ? 'exclusive' : 'inclusive', 'elements' => $elements];
                                })
                            ->end()
                                ->children()
                                ->scalarNode('type')
                                    ->validate()
                                        ->ifNotInArray(['inclusive', 'exclusive'])
                                        ->thenInvalid('The type of channels has to be inclusive or exclusive')
                                    ->end()
                                ->end()
                                ->arrayNode('elements')
                                    ->prototype('scalar')->end()
                                ->end()
                            ->end()
                        ->end()
                        ->scalarNode('level')->defaultValue(LogLevel::ERROR)->end()
                        ->scalarNode('service')->defaultValue('ekino.new_relic.monolog_handler')->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}


================================================
FILE: DependencyInjection/EkinoNewRelicExtension.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\DependencyInjection;

use Ekino\NewRelicBundle\Listener\CommandListener;
use Ekino\NewRelicBundle\Listener\RequestListener;
use Ekino\NewRelicBundle\Listener\ResponseListener;
use Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor;
use Ekino\NewRelicBundle\NewRelic\BlackholeInteractor;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\LoggingInteractorDecorator;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\TransactionNamingStrategy\ControllerNamingStrategy;
use Ekino\NewRelicBundle\TransactionNamingStrategy\RouteNamingStrategy;
use Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

/**
 * This is the class that loads and manages your bundle configuration.
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class EkinoNewRelicExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container): void
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');

        $container->setAlias(NewRelicInteractorInterface::class, $this->getInteractorServiceId($config))->setPublic(false);
        $container->setAlias(TransactionNamingStrategyInterface::class, $this->getTransactionNamingServiceId($config))->setPublic(false);

        if ($config['logging']) {
            $container->register(LoggingInteractorDecorator::class)
                ->setDecoratedService(NewRelicInteractorInterface::class)
                ->setArguments(
                    [
                        '$interactor' => new Reference(LoggingInteractorDecorator::class.'.inner'),
                        '$logger' => new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE),
                    ]
                )
                ->setPublic(false)
            ;
        }

        if (empty($config['deployment_names'])) {
            $config['deployment_names'] = array_values(array_filter(explode(';', $config['application_name'] ?? '')));
        }

        $container->getDefinition(Config::class)
            ->setArguments(
                [
                    '$name' => $config['application_name'],
                    '$apiKey' => $config['api_key'],
                    '$licenseKey' => $config['license_key'],
                    '$xmit' => $config['xmit'],
                    '$deploymentNames' => $config['deployment_names'],
                    '$apiHost' => $config['api_host'],
                ]
            );

        if ($config['http']['enabled']) {
            $loader->load('http_listener.xml');
            $container->getDefinition(RequestListener::class)
                ->setArguments(
                    [
                        '$ignoreRoutes' => $config['http']['ignored_routes'],
                        '$ignoredPaths' => $config['http']['ignored_paths'],
                        '$symfonyCache' => $config['http']['using_symfony_cache'],
                    ]
                );

            $container->getDefinition(ResponseListener::class)
                ->setArguments(
                    [
                        '$instrument' => $config['instrument'],
                        '$symfonyCache' => $config['http']['using_symfony_cache'],
                    ]
                );
        }

        if ($config['commands']['enabled']) {
            $loader->load('command_listener.xml');
            $container->getDefinition(CommandListener::class)
                ->setArguments(
                    [
                        '$ignoredCommands' => $config['commands']['ignored_commands'],
                    ]
                );
        }

        if ($config['exceptions']['enabled']) {
            $loader->load('exception_listener.xml');
        }

        if ($config['deprecations']['enabled']) {
            $loader->load('deprecation_listener.xml');
        }

        if ($config['twig']) {
            $loader->load('twig.xml');
        }

        if ($config['enabled'] && $config['monolog']['enabled']) {
            if (!class_exists(\Monolog\Handler\NewRelicHandler::class)) {
                throw new \LogicException('The "symfony/monolog-bundle" package must be installed in order to use "monolog" option.');
            }
            $loader->load('monolog.xml');
            $container->setParameter('ekino.new_relic.monolog', $config['monolog'] ?? []);
            $container->setParameter('ekino.new_relic.application_name', $config['application_name']);
            $container->setAlias('ekino.new_relic.logs_handler', $config['monolog']['service'])->setPublic(false);
        }
    }

    private function getInteractorServiceId(array $config): string
    {
        if (!$config['enabled']) {
            return BlackholeInteractor::class;
        }

        if (!isset($config['interactor'])) {
            // Fallback on AdaptiveInteractor.
            return AdaptiveInteractor::class;
        }

        if ('auto' === $config['interactor']) {
            // Check if the extension is loaded or not
            return \extension_loaded('newrelic') ? NewRelicInteractor::class : BlackholeInteractor::class;
        }

        return $config['interactor'];
    }

    private function getTransactionNamingServiceId(array $config): string
    {
        switch ($config['http']['transaction_naming']) {
            case 'controller':
                return ControllerNamingStrategy::class;
            case 'route':
                return RouteNamingStrategy::class;
            case 'service':
                if (!isset($config['http']['transaction_naming_service'])) {
                    throw new \LogicException('When using the "service", transaction naming scheme, the "transaction_naming_service" config parameter must be set.');
                }

                return $config['http']['transaction_naming_service'];
            default:
                throw new \InvalidArgumentException(sprintf('Invalid transaction naming scheme "%s", must be "route", "controller" or "service".', $config['http']['transaction_naming']));
        }
    }
}


================================================
FILE: EkinoNewRelicBundle.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle;

use Ekino\NewRelicBundle\DependencyInjection\Compiler\MonologHandlerPass;
use Ekino\NewRelicBundle\Listener\DeprecationListener;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class EkinoNewRelicBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new MonologHandlerPass());
    }

    public function boot()
    {
        parent::boot();

        if ($this->container->has(DeprecationListener::class)) {
            $this->container->get(DeprecationListener::class)->register();
        }
    }

    public function shutdown()
    {
        if ($this->container->has(DeprecationListener::class)) {
            $this->container->get(DeprecationListener::class)->unregister();
        }

        parent::shutdown();
    }
}


================================================
FILE: Exception/DeprecationException.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Exception;

/**
 * Exception dedicated to report Deprecation.
 */
class DeprecationException extends \ErrorException
{
}


================================================
FILE: LICENSE
================================================
The MIT License

Copyright (c) 2012 Ekino - thomas.rabaix@ekino.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: Listener/CommandListener.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Listener;

use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CommandListener implements EventSubscriberInterface
{
    private $interactor;
    private $config;
    private $ignoredCommands;

    public function __construct(Config $config, NewRelicInteractorInterface $interactor, array $ignoredCommands)
    {
        $this->config = $config;
        $this->interactor = $interactor;
        $this->ignoredCommands = $ignoredCommands;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            ConsoleEvents::COMMAND => ['onConsoleCommand', 0],
            ConsoleEvents::ERROR => ['onConsoleError', 0],
        ];
    }

    public function onConsoleCommand(ConsoleCommandEvent $event): void
    {
        $command = $event->getCommand();
        $input = $event->getInput();

        if ($this->config->getName()) {
            $this->interactor->setApplicationName($this->config->getName(), $this->config->getLicenseKey(), $this->config->getXmit());
        }
        $this->interactor->setTransactionName($command->getName());

        // Due to newrelic's extension implementation, the method `ignoreTransaction` must be called after `setApplicationName`
        // see https://discuss.newrelic.com/t/newrelic-ignore-transaction-not-being-honored/5450/5
        if (\in_array($command->getName(), $this->ignoredCommands, true)) {
            $this->interactor->ignoreTransaction();
        }

        $this->interactor->enableBackgroundJob();

        // send parameters to New Relic
        foreach ($input->getOptions() as $key => $value) {
            $key = '--'.$key;
            if (\is_array($value)) {
                foreach ($value as $k => $v) {
                    $this->interactor->addCustomParameter($key.'['.$k.']', $v);
                }
            } else {
                $this->interactor->addCustomParameter($key, $value);
            }
        }

        foreach ($input->getArguments() as $key => $value) {
            if (\is_array($value)) {
                foreach ($value as $k => $v) {
                    $this->interactor->addCustomParameter($key.'['.$k.']', $v);
                }
            } else {
                $this->interactor->addCustomParameter($key, $value);
            }
        }
    }

    public function onConsoleError(ConsoleErrorEvent $event): void
    {
        $this->interactor->noticeThrowable($event->getError());
    }
}


================================================
FILE: Listener/DeprecationListener.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Listener;

use Ekino\NewRelicBundle\Exception\DeprecationException;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;

class DeprecationListener
{
    private $isRegistered = false;
    private $interactor;

    public function __construct(NewRelicInteractorInterface $interactor)
    {
        $this->interactor = $interactor;
    }

    public function register(): void
    {
        if ($this->isRegistered) {
            return;
        }
        $this->isRegistered = true;

        $prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler) {
            if (\E_USER_DEPRECATED === $type) {
                $this->interactor->noticeThrowable(new DeprecationException($msg, 0, $type, $file, $line));
            }

            return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
        });
    }

    public function unregister(): void
    {
        if (!$this->isRegistered) {
            return;
        }
        $this->isRegistered = false;
        restore_error_handler();
    }
}


================================================
FILE: Listener/ExceptionListener.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Listener;

use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Listen to exceptions dispatched by Symfony to log them to NewRelic.
 */
class ExceptionListener implements EventSubscriberInterface
{
    private $interactor;

    public function __construct(NewRelicInteractorInterface $interactor)
    {
        $this->interactor = $interactor;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::EXCEPTION => ['onKernelException', 0],
        ];
    }

    /**
     * @param GetResponseForExceptionEvent|ExceptionEvent $event
     */
    public function onKernelException(KernelExceptionEvent $event): void
    {
        $exception = method_exists($event, 'getThrowable') ? $event->getThrowable() : $event->getException();
        if (!$exception instanceof HttpExceptionInterface) {
            $this->interactor->noticeThrowable($exception);
        }
    }
}

if (!class_exists(KernelExceptionEvent::class)) {
    if (class_exists(ExceptionEvent::class)) {
        class_alias(ExceptionEvent::class, KernelExceptionEvent::class);
    } else {
        class_alias(GetResponseForExceptionEvent::class, KernelExceptionEvent::class);
    }
}


================================================
FILE: Listener/RequestListener.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Listener;

use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;

class RequestListener implements EventSubscriberInterface
{
    private $ignoredRoutes;
    private $ignoredPaths;
    private $config;
    private $interactor;
    private $transactionNamingStrategy;
    private $symfonyCache;

    public function __construct(
        Config $config,
        NewRelicInteractorInterface $interactor,
        array $ignoreRoutes,
        array $ignoredPaths,
        TransactionNamingStrategyInterface $transactionNamingStrategy,
        bool $symfonyCache = false
    ) {
        $this->config = $config;
        $this->interactor = $interactor;
        $this->ignoredRoutes = $ignoreRoutes;
        $this->ignoredPaths = $ignoredPaths;
        $this->transactionNamingStrategy = $transactionNamingStrategy;
        $this->symfonyCache = $symfonyCache;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => [
                 ['setApplicationName', 255],
                 ['setIgnoreTransaction', 31],
                 ['setTransactionName', -10],
            ],
        ];
    }

    public function setApplicationName(KernelRequestEvent $event): void
    {
        if (!$this->isEventValid($event)) {
            return;
        }

        $appName = $this->config->getName();

        if (!$appName) {
            return;
        }

        if ($this->symfonyCache) {
            $this->interactor->startTransaction($appName);
        }

        // Set application name if different from ini configuration
        if ($appName !== ini_get('newrelic.appname')) {
            $this->interactor->setApplicationName($appName, $this->config->getLicenseKey(), $this->config->getXmit());
        }
    }

    public function setTransactionName(KernelRequestEvent $event): void
    {
        if (!$this->isEventValid($event)) {
            return;
        }

        $transactionName = $this->transactionNamingStrategy->getTransactionName($event->getRequest());

        $this->interactor->setTransactionName($transactionName);
    }

    public function setIgnoreTransaction(KernelRequestEvent $event): void
    {
        if (!$this->isEventValid($event)) {
            return;
        }

        $request = $event->getRequest();
        if (\in_array($request->get('_route'), $this->ignoredRoutes, true)) {
            $this->interactor->ignoreTransaction();
        }

        if (\in_array($request->getPathInfo(), $this->ignoredPaths, true)) {
            $this->interactor->ignoreTransaction();
        }
    }

    /**
     * Make sure we should consider this event. Example: make sure it is a master request.
     */
    private function isEventValid(KernelRequestEvent $event): bool
    {
        return HttpKernelInterface::MASTER_REQUEST === $event->getRequestType();
    }
}

if (!class_exists(KernelRequestEvent::class)) {
    if (class_exists(RequestEvent::class)) {
        class_alias(RequestEvent::class, KernelRequestEvent::class);
    } else {
        class_alias(GetResponseEvent::class, KernelRequestEvent::class);
    }
}


================================================
FILE: Listener/ResponseListener.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Listener;

use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class ResponseListener implements EventSubscriberInterface
{
    private $newRelic;
    private $interactor;
    private $instrument;
    private $symfonyCache;
    private $newRelicTwigExtension;

    public function __construct(
        Config $newRelic,
        NewRelicInteractorInterface $interactor,
        bool $instrument = false,
        bool $symfonyCache = false,
        NewRelicExtension $newRelicTwigExtension = null
    ) {
        $this->newRelic = $newRelic;
        $this->interactor = $interactor;
        $this->instrument = $instrument;
        $this->symfonyCache = $symfonyCache;
        $this->newRelicTwigExtension = $newRelicTwigExtension;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => [
                ['onKernelResponse', -255],
            ],
        ];
    }

    public function onKernelResponse(KernelResponseEvent $event): void
    {
        $isMainRequest = method_exists($event, 'isMainRequest') ? $event->isMainRequest() : $event->isMasterRequest();

        if (!$isMainRequest) {
            return;
        }

        if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isUsed()) {
            foreach ($this->newRelic->getCustomMetrics() as $name => $value) {
                $this->interactor->addCustomMetric((string) $name, (float) $value);
            }

            foreach ($this->newRelic->getCustomParameters() as $name => $value) {
                $this->interactor->addCustomParameter((string) $name, $value);
            }
        }

        foreach ($this->newRelic->getCustomEvents() as $name => $events) {
            foreach ($events as $attributes) {
                $this->interactor->addCustomEvent((string) $name, $attributes);
            }
        }

        if ($this->instrument) {
            if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isUsed()) {
                $this->interactor->disableAutoRUM();
            }

            // Some requests might not want to get instrumented
            if ($event->getRequest()->attributes->get('_instrument', true)) {
                $response = $event->getResponse();

                // We can only instrument HTML responses
                if (!$response instanceof StreamedResponse
                    && 'text/html' === substr($response->headers->get('Content-Type', ''), 0, 9)
                ) {
                    $responseContent = $response->getContent();
                    $response->setContent(''); // free the memory

                    if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isHeaderCalled()) {
                        $responseContent = preg_replace('|<head>|i', '$0'.$this->interactor->getBrowserTimingHeader(), $responseContent);
                    }

                    if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isFooterCalled()) {
                        $responseContent = preg_replace('|</body>|i', $this->interactor->getBrowserTimingFooter().'$0', $responseContent);
                    }

                    $response->setContent($responseContent);
                }
            }
        }

        if ($this->symfonyCache) {
            $this->interactor->endTransaction();
        }
    }
}

if (!class_exists(KernelResponseEvent::class)) {
    if (class_exists(ResponseEvent::class)) {
        class_alias(ResponseEvent::class, KernelResponseEvent::class);
    } else {
        class_alias(FilterResponseEvent::class, KernelResponseEvent::class);
    }
}


================================================
FILE: Logging/AdaptiveHandler.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Logging;

use Monolog\Handler\NewRelicHandler;
use Psr\Log\LogLevel;

class AdaptiveHandler extends NewRelicHandler
{
    public function __construct(
        string $level = LogLevel::ERROR,
        bool $bubble = true,
        string $appName = null,
        bool $explodeArrays = false,
        string $transactionName = null
    ) {
        parent::__construct($level, $bubble, $appName, $explodeArrays, $transactionName);
    }

    protected function write(array $record): void
    {
        if (!$this->isNewRelicEnabled()) {
            return;
        }

        parent::write($record);
    }
}


================================================
FILE: NewRelic/AdaptiveInteractor.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\NewRelic;

/**
 * This interactor does never assume that the NewRelic extension is installed. It will check
 * for the existence of the NewRelic extension every time this is class is instantiated. This
 * is a good interactor to use when you want to enable and disable the NewRelic extension
 * without rebuilding your container.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class AdaptiveInteractor implements NewRelicInteractorInterface
{
    private $interactor;

    public function __construct(NewRelicInteractorInterface $real, NewRelicInteractorInterface $fake)
    {
        $this->interactor = \extension_loaded('newrelic') ? $real : $fake;
    }

    public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
    {
        return $this->interactor->setApplicationName($name, $license, $xmit);
    }

    public function setTransactionName(string $name): bool
    {
        return $this->interactor->setTransactionName($name);
    }

    public function ignoreTransaction(): void
    {
        $this->interactor->ignoreTransaction();
    }

    public function addCustomEvent(string $name, array $attributes): void
    {
        $this->interactor->addCustomEvent($name, $attributes);
    }

    public function addCustomMetric(string $name, float $value): bool
    {
        return $this->interactor->addCustomMetric($name, $value);
    }

    public function addCustomParameter(string $name, $value): bool
    {
        return $this->interactor->addCustomParameter($name, $value);
    }

    public function getBrowserTimingHeader(bool $includeTags = true): string
    {
        return $this->interactor->getBrowserTimingHeader($includeTags);
    }

    public function getBrowserTimingFooter(bool $includeTags = true): string
    {
        return $this->interactor->getBrowserTimingFooter($includeTags);
    }

    public function disableAutoRUM(): ?bool
    {
        return $this->interactor->disableAutoRUM();
    }

    public function noticeThrowable(\Throwable $e, string $message = null): void
    {
        $this->interactor->noticeThrowable($e, $message);
    }

    public function noticeError(
        int $errno,
        string $errstr,
        string $errfile = null,
        int $errline = null,
        string $errcontext = null
    ): void {
        $this->interactor->noticeError($errno, $errstr, $errfile, $errline, $errcontext);
    }

    public function enableBackgroundJob(): void
    {
        $this->interactor->enableBackgroundJob();
    }

    public function disableBackgroundJob(): void
    {
        $this->interactor->disableBackgroundJob();
    }

    public function startTransaction(string $name = null, string $license = null): bool
    {
        return $this->interactor->startTransaction($name, $license);
    }

    public function endTransaction(bool $ignore = false): bool
    {
        return $this->interactor->endTransaction($ignore);
    }

    public function excludeFromApdex(): void
    {
        $this->interactor->excludeFromApdex();
    }

    public function addCustomTracer(string $name): bool
    {
        return $this->interactor->addCustomTracer($name);
    }

    public function setCaptureParams(bool $enabled): void
    {
        $this->interactor->setCaptureParams($enabled);
    }

    public function stopTransactionTiming(): void
    {
        $this->interactor->stopTransactionTiming();
    }

    public function recordDatastoreSegment(callable $func, array $parameters)
    {
        return $this->interactor->recordDatastoreSegment($func, $parameters);
    }

    public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
    {
        return $this->interactor->setUserAttributes($userValue, $accountValue, $productValue);
    }

    public function getTraceMetadata(): array
    {
        if (!method_exists($this->interactor, 'getTraceMetadata')) {
            throw new \BadMethodCallException('The decorated interaction does not implement this method');
        }

        return $this->interactor->getTraceMetadata();
    }

    public function getLinkingMetadata(): array
    {
        if (!method_exists($this->interactor, 'getLinkingMetadata')) {
            throw new \BadMethodCallException('The decorated interaction does not implement this method');
        }

        return $this->interactor->getLinkingMetadata();
    }

    public function isSampled(): bool
    {
        if (!method_exists($this->interactor, 'isSampled')) {
            throw new \BadMethodCallException('The decorated interaction does not implement this method');
        }

        return $this->interactor->isSampled();
    }

    public function insertDistributedTracingHeaders(array &$headers): void
    {
        if (!method_exists($this->interactor, 'insertDistributedTracingHeaders')) {
            throw new \BadMethodCallException('The decorated interaction does not implement this method');
        }

        $this->interactor->insertDistributedTracingHeaders($headers);
    }

    public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
    {
        if (!method_exists($this->interactor, 'acceptDistributedTraceHeaders')) {
            throw new \BadMethodCallException('The decorated interaction does not implement this method');
        }

        $this->interactor->acceptDistributedTraceHeaders($headers, $transportType);
    }
}


================================================
FILE: NewRelic/BlackholeInteractor.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\NewRelic;

/**
 * This interactor throw away any call.
 *
 * It can be used to avoid conditional log calls.
 */
class BlackholeInteractor implements NewRelicInteractorInterface
{
    public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
    {
        return true;
    }

    public function setTransactionName(string $name): bool
    {
        return true;
    }

    public function ignoreTransaction(): void
    {
    }

    public function addCustomEvent(string $name, array $attributes): void
    {
    }

    public function addCustomMetric(string $name, float $value): bool
    {
        return true;
    }

    public function addCustomParameter(string $name, $value): bool
    {
        return true;
    }

    public function getBrowserTimingHeader(bool $includeTags = true): string
    {
        return '';
    }

    public function getBrowserTimingFooter(bool $includeTags = true): string
    {
        return '';
    }

    public function disableAutoRUM(): ?bool
    {
        return true;
    }

    public function noticeThrowable(\Throwable $e, string $message = null): void
    {
    }

    public function noticeError(
        int $errno,
        string $errstr,
        string $errfile = null,
        int $errline = null,
        string $errcontext = null
    ): void {
    }

    public function enableBackgroundJob(): void
    {
    }

    public function disableBackgroundJob(): void
    {
    }

    public function startTransaction(string $name = null, string $license = null): bool
    {
        return true;
    }

    public function endTransaction(bool $ignore = false): bool
    {
        return true;
    }

    public function excludeFromApdex(): void
    {
    }

    public function addCustomTracer(string $name): bool
    {
        return true;
    }

    public function setCaptureParams(bool $enabled): void
    {
    }

    public function stopTransactionTiming(): void
    {
    }

    public function recordDatastoreSegment(callable $func, array $parameters)
    {
        return $func();
    }

    public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
    {
        return true;
    }

    public function getTraceMetadata(): array
    {
        return [];
    }

    public function getLinkingMetadata(): array
    {
        return [];
    }

    public function isSampled(): bool
    {
        return true;
    }

    public function insertDistributedTracingHeaders(array &$headers): void
    {
    }

    public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
    {
    }
}


================================================
FILE: NewRelic/Config.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\NewRelic;

/**
 * This value object contains data and configuration that should be passed to the interactors.
 */
class Config
{
    private $name;
    private $apiKey;
    private $apiHost = null;
    private $licenseKey;
    private $xmit;
    private $customEvents;
    private $customMetrics;
    private $customParameters;
    private $deploymentNames;

    public function __construct(?string $name, string $apiKey = null, string $licenseKey = null, bool $xmit = false, array $deploymentNames = [], ?string $apiHost = null)
    {
        $this->name = (!empty($name) ? $name : ini_get('newrelic.appname')) ?: '';
        $this->apiKey = $apiKey;
        $this->apiHost = $apiHost;
        $this->licenseKey = (!empty($licenseKey) ? $licenseKey : ini_get('newrelic.license')) ?: '';
        $this->xmit = $xmit;
        $this->deploymentNames = $deploymentNames;
        $this->customEvents = [];
        $this->customMetrics = [];
        $this->customParameters = [];
    }

    public function setCustomEvents(array $customEvents): void
    {
        $this->customEvents = $customEvents;
    }

    public function getCustomEvents(): array
    {
        return $this->customEvents;
    }

    public function addCustomEvent(string $name, array $attributes): void
    {
        $this->customEvents[$name][] = $attributes;
    }

    public function setCustomMetrics(array $customMetrics): void
    {
        $this->customMetrics = $customMetrics;
    }

    public function getCustomMetrics(): array
    {
        return $this->customMetrics;
    }

    public function setCustomParameters(array $customParameters): void
    {
        $this->customParameters = $customParameters;
    }

    /**
     * @param string|int|float $value or any scalar value
     */
    public function addCustomParameter(string $name, $value): void
    {
        $this->customParameters[$name] = $value;
    }

    public function addCustomMetric(string $name, float $value): void
    {
        $this->customMetrics[$name] = $value;
    }

    public function getCustomParameters(): array
    {
        return $this->customParameters;
    }

    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string[]
     */
    public function getDeploymentNames(): array
    {
        return $this->deploymentNames;
    }

    public function getApiKey(): ?string
    {
        return $this->apiKey;
    }

    public function getApiHost(): ?string
    {
        return $this->apiHost;
    }

    public function getLicenseKey(): ?string
    {
        return $this->licenseKey;
    }

    public function getXmit(): bool
    {
        return $this->xmit;
    }
}


================================================
FILE: NewRelic/LoggingInteractorDecorator.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\NewRelic;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class LoggingInteractorDecorator implements NewRelicInteractorInterface
{
    private $interactor;
    private $logger;

    public function __construct(NewRelicInteractorInterface $interactor, LoggerInterface $logger = null)
    {
        $this->interactor = $interactor;
        $this->logger = $logger ?? new NullLogger();
    }

    public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
    {
        $this->logger->debug('Setting New Relic Application name to {name}', ['name' => $name]);

        return $this->interactor->setApplicationName($name, $license, $xmit);
    }

    public function setTransactionName(string $name): bool
    {
        $this->logger->debug('Setting New Relic Transaction name to {name}', ['name' => $name]);

        return $this->interactor->setTransactionName($name);
    }

    public function ignoreTransaction(): void
    {
        $this->logger->debug('Ignoring transaction');
        $this->interactor->ignoreTransaction();
    }

    public function addCustomEvent(string $name, array $attributes): void
    {
        $this->logger->debug('Adding custom New Relic event {name}', ['name' => $name, 'attributes' => $attributes]);
        $this->interactor->addCustomEvent($name, $attributes);
    }

    public function addCustomMetric(string $name, float $value): bool
    {
        $this->logger->debug('Adding custom New Relic metric {name}: {value}', ['name' => $name, 'value' => $value]);

        return $this->interactor->addCustomMetric($name, $value);
    }

    public function addCustomParameter(string $name, $value): bool
    {
        $this->logger->debug('Adding custom New Relic parameters {name}: {value}', ['name' => $name, 'value' => $value]);

        return $this->interactor->addCustomParameter($name, $value);
    }

    public function getBrowserTimingHeader(bool $includeTags = true): string
    {
        $this->logger->debug('Getting New Relic RUM timing header');

        return $this->interactor->getBrowserTimingHeader($includeTags);
    }

    public function getBrowserTimingFooter(bool $includeTags = true): string
    {
        $this->logger->debug('Getting New Relic RUM timing footer');

        return $this->interactor->getBrowserTimingFooter($includeTags);
    }

    public function disableAutoRUM(): ?bool
    {
        $this->logger->debug('Disabling New Relic Auto-RUM');

        return $this->interactor->disableAutoRUM();
    }

    public function noticeError(
        int $errno,
        string $errstr,
        string $errfile = null,
        int $errline = null,
        string $errcontext = null
    ): void {
        $this->logger->debug('Sending notice error to New Relic', [
            'error_code' => $errno,
            'message' => $errstr,
            'file' => $errfile,
            'line' => $errline,
            'context_error' => $errcontext,
        ]);
        $this->interactor->noticeError($errno, $errstr, $errfile, $errline, $errcontext);
    }

    public function noticeThrowable(\Throwable $e, string $message = null): void
    {
        $this->logger->debug('Sending exception to New Relic', [
            'message' => $message,
            'exception' => $e,
        ]);
        $this->interactor->noticeThrowable($e, $message);
    }

    public function enableBackgroundJob(): void
    {
        $this->logger->debug('Enabling New Relic background job');
        $this->interactor->enableBackgroundJob();
    }

    public function disableBackgroundJob(): void
    {
        $this->logger->debug('Disabling New Relic background job');
        $this->interactor->disableBackgroundJob();
    }

    public function endTransaction(bool $ignore = false): bool
    {
        $this->logger->debug('Ending a New Relic transaction');

        return $this->interactor->endTransaction($ignore);
    }

    public function startTransaction(string $name = null, string $license = null): bool
    {
        $this->logger->debug('Starting a new New Relic transaction for app {name}', ['name' => $name]);

        return $this->interactor->startTransaction($name, $license);
    }

    public function excludeFromApdex(): void
    {
        $this->logger->debug('Excluding current transaction from New Relic Apdex score');
        $this->interactor->excludeFromApdex();
    }

    public function addCustomTracer(string $name): bool
    {
        $this->logger->debug('Adding custom New Relic tracer', ['name' => $name]);

        return $this->interactor->addCustomTracer($name);
    }

    public function setCaptureParams(bool $enabled): void
    {
        $this->logger->debug('Toggle New Relic capture params to {enabled}', ['enabled' => $enabled]);
        $this->interactor->setCaptureParams($enabled);
    }

    public function stopTransactionTiming(): void
    {
        $this->logger->debug('Stopping New Relic timing');
        $this->interactor->stopTransactionTiming();
    }

    public function recordDatastoreSegment(callable $func, array $parameters)
    {
        $this->logger->debug('Adding custom New Relic datastore segment', [
            'parameters' => $parameters,
        ]);

        return $this->interactor->recordDatastoreSegment($func, $parameters);
    }

    public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
    {
        $this->logger->debug('Setting New Relic user attributes', [
            'user_value' => $userValue,
            'account_value' => $accountValue,
            'product_value' => $productValue,
        ]);

        return $this->interactor->setUserAttributes($userValue, $accountValue, $productValue);
    }

    public function getTraceMetadata(): array
    {
        $traceMetadata = $this->interactor->getTraceMetadata();

        $this->logger->debug('Getting New Relic trace metadata', $traceMetadata);

        return $traceMetadata;
    }

    public function getLinkingMetadata(): array
    {
        $linkingMetadata = $this->interactor->getLinkingMetadata();

        $this->logger->debug('Getting New Relic linking metadata', $linkingMetadata);

        return $linkingMetadata;
    }

    public function isSampled(): bool
    {
        $isSampled = $this->interactor->isSampled();

        $this->logger->debug('Getting New Relic sampled status', ['sampled' => $isSampled]);

        return $isSampled;
    }

    public function insertDistributedTracingHeaders(array &$headers): void
    {
        $this->logger->debug('Setting New Relic distributed tracing headers', ['headers' => $headers]);

        $this->interactor->insertDistributedTracingHeaders($headers);
    }

    public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
    {
        $this->logger->debug('Accepting New Relic distributed tracing headers', [
            'headers' => $headers,
            'transport_type' => $transportType,
        ]);

        $this->interactor->acceptDistributedTraceHeaders($headers, $transportType);
    }
}


================================================
FILE: NewRelic/NewRelicInteractor.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\NewRelic;

class NewRelicInteractor implements NewRelicInteractorInterface
{
    public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
    {
        return newrelic_set_appname($name, $license, $xmit);
    }

    public function setTransactionName(string $name): bool
    {
        return newrelic_name_transaction($name);
    }

    public function ignoreTransaction(): void
    {
        newrelic_ignore_transaction();
    }

    public function addCustomEvent(string $name, array $attributes): void
    {
        newrelic_record_custom_event((string) $name, $attributes);
    }

    public function addCustomMetric(string $name, float $value): bool
    {
        return newrelic_custom_metric($name, $value);
    }

    public function addCustomParameter(string $name, $value): bool
    {
        return newrelic_add_custom_parameter((string) $name, $value);
    }

    public function getBrowserTimingHeader(bool $includeTags = true): string
    {
        return newrelic_get_browser_timing_header($includeTags);
    }

    public function getBrowserTimingFooter(bool $includeTags = true): string
    {
        return newrelic_get_browser_timing_footer($includeTags);
    }

    public function disableAutoRUM(): ?bool
    {
        return newrelic_disable_autorum();
    }

    public function noticeError(int $errno, string $errstr, string $errfile = null, int $errline = null, string $errcontext = null): void
    {
        newrelic_notice_error($errno, $errstr, $errfile, $errline, $errcontext);
    }

    public function noticeThrowable(\Throwable $e, string $message = null): void
    {
        newrelic_notice_error($message ?: $e->getMessage(), $e);
    }

    public function enableBackgroundJob(): void
    {
        newrelic_background_job(true);
    }

    public function disableBackgroundJob(): void
    {
        newrelic_background_job(false);
    }

    public function endTransaction(bool $ignore = false): bool
    {
        return newrelic_end_transaction($ignore);
    }

    public function startTransaction(string $name = null, string $license = null): bool
    {
        if (null === $name) {
            $name = ini_get('newrelic.appname');
        }

        if (null === $license) {
            return newrelic_start_transaction($name);
        }

        return newrelic_start_transaction($name, $license);
    }

    public function excludeFromApdex(): void
    {
        newrelic_ignore_apdex();
    }

    public function addCustomTracer(string $name): bool
    {
        return newrelic_add_custom_tracer($name);
    }

    public function setCaptureParams(bool $enabled): void
    {
        newrelic_capture_params($enabled);
    }

    public function stopTransactionTiming(): void
    {
        newrelic_end_of_transaction();
    }

    public function recordDatastoreSegment(callable $func, array $parameters)
    {
        return newrelic_record_datastore_segment($func, $parameters);
    }

    public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
    {
        return newrelic_set_user_attributes($userValue, $accountValue, $productValue);
    }

    public function getTraceMetadata(): array
    {
        if (!function_exists('newrelic_get_trace_metadata')) {
            throw new \BadMethodCallException('You need the "newrelic" extension version 9.3 or higher to use this method');
        }

        return newrelic_get_trace_metadata();
    }

    public function getLinkingMetadata(): array
    {
        if (!function_exists('newrelic_get_linking_metadata')) {
            throw new \BadMethodCallException('You need the "newrelic" extension version 9.3 or higher to use this method');
        }

        return newrelic_get_linking_metadata();
    }

    public function isSampled(): bool
    {
        if (!function_exists('newrelic_is_sampled')) {
            throw new \BadMethodCallException('You need the "newrelic" extension version 9.3 or higher to use this method');
        }

        return newrelic_is_sampled();
    }

    public function insertDistributedTracingHeaders(array &$headers): void
    {
        if (!function_exists('newrelic_insert_distributed_trace_headers')) {
            throw new \BadMethodCallException('You need the "newrelic" extension version 9.8 or higher to use this method');
        }

        newrelic_insert_distributed_trace_headers($headers);
    }

    public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
    {
        if (!function_exists('newrelic_accept_distributed_trace_headers')) {
            throw new \BadMethodCallException('You need the "newrelic" extension version 9.8 or higher to use this method');
        }

        newrelic_accept_distributed_trace_headers($headers, $transportType);
    }
}


================================================
FILE: NewRelic/NewRelicInteractorInterface.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\NewRelic;

/**
 * This is the service that talks to NewRelic.
 *
 * @method array getTraceMetadata()
 * @method array getLinkingMetadata()
 * @method bool isSampled()
 * @method void insertDistributedTracingHeaders(array &$headers)
 * @method void acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP')
 */
interface NewRelicInteractorInterface
{
    /**
     * Sets the New Relic app name, which controls data rollup.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_set_appname}
     */
    public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool;

    /**
     * Set custom name for current transaction.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_name_transaction}
     */
    public function setTransactionName(string $name): bool;

    /**
     * Do not instrument the current transaction.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_ignore_transaction}
     */
    public function ignoreTransaction(): void;

    /**
     * Record a custom event with the given name and attributes.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_record_custom_event}
     */
    public function addCustomEvent(string $name, array $attributes): void;

    /**
     * Add a custom metric (in milliseconds) to time a component of your app not captured by default.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newreliccustommetric-php-agent-api}
     */
    public function addCustomMetric(string $name, float $value): bool;

    /**
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_add_custom_parameter}.
     *
     * @param string|int|float $value should be a scalar
     */
    public function addCustomParameter(string $name, $value): bool;

    /**
     * Returns a New Relic Browser snippet to inject in the head of your HTML output.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_get_browser_timing_header}
     */
    public function getBrowserTimingHeader(bool $includeTags = true): string;

    /**
     * Returns a New Relic Browser snippet to inject at the end of the HTML output.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_get_browser_timing_footer}
     */
    public function getBrowserTimingFooter(bool $includeTags = true): string;

    /**
     * Disable automatic injection of the New Relic Browser snippet on particular pages.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_disable_autorum}
     */
    public function disableAutoRUM(): ?bool;

    /**
     * Use these calls to collect errors that the PHP agent does not collect automatically and to set the callback for
     * your own error and exception handler.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_notice_error}
     */
    public function noticeThrowable(\Throwable $e, string $message = null): void;

    /**
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_notice_error}.
     */
    public function noticeError(int $errno, string $errstr, string $errfile = null, int $errline = null, string $errcontext = null): void;

    /**
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_background_job}.
     */
    public function enableBackgroundJob(): void;

    /**
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_background_job}.
     */
    public function disableBackgroundJob(): void;

    /**
     * If you previously ended a transaction you many want to start a new one.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_start_transaction}
     */
    public function startTransaction(string $name = null, string $license = null): bool;

    /**
     * Stop instrumenting the current transaction immediately.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_end_transaction}
     */
    public function endTransaction(bool $ignore = false): bool;

    /**
     * Ignore the current transaction when calculating Apdex.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_ignore_apdex}
     */
    public function excludeFromApdex(): void;

    /**
     * Specify functions or methods for the agent to target for custom instrumentation. This is the API equivalent of
     * the newrelic.transaction_tracer.custom setting.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_add_custom_tracer}
     */
    public function addCustomTracer(string $name): bool;

    /**
     * Enable or disable the capture of URL parameters.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_capture_params}
     */
    public function setCaptureParams(bool $enabled): void;

    /**
     * Stop timing the current transaction, but continue instrumenting it.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_end_of_transaction}
     */
    public function stopTransactionTiming(): void;

    /**
     * Records a datastore segment.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_record_datastore_segment}
     *
     * @return bool|mixed The return value of $func is returned. If an error occurs, false is returned.
     */
    public function recordDatastoreSegment(callable $func, array $parameters);

    /**
     * Create user-related custom attributes. newrelic_add_custom_parameter is more flexible.
     *
     * {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_set_user_attributes}
     */
    public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool;
}


================================================
FILE: README.md
================================================
Ekino NewRelic Bundle
=====================

[![Build Status](https://secure.travis-ci.org/ekino/EkinoNewRelicBundle.png?branch=master)](http://travis-ci.org/ekino/EkinoNewRelicBundle)
[![Latest Version](https://img.shields.io/github/release/ekino/EkinoNewRelicBundle.svg?style=flat-square)](https://github.com/ekino/EkinoNewRelicBundle/releases)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/ekino/EkinoNewRelicBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/ekino/EkinoNewRelicBundle)
[![Quality Score](https://img.shields.io/scrutinizer/g/ekino/EkinoNewRelicBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/ekino/EkinoNewRelicBundle)
[![Total Downloads](https://img.shields.io/packagist/dt/ekino/newrelic-bundle.svg?style=flat-square)](https://packagist.org/packages/ekino/newrelic-bundle)

This bundle integrates the NewRelic PHP API into Symfony. For more information about NewRelic, please visit http://newrelic.com. The built-in New Relic agent doesn't add as much Symfony integration as it claims. This bundle adds a lot more essentials. Here's a quick list:

1. **Better transaction naming strategy**: Your transaction traces can be named accurately by route names, the controller name or you can decide on a custom naming strategy via a seamless interface that uses any naming convention you deem fit. While running console commands, it also sets the transaction name as the command name.

2. **Console Commands Enhancements**: While running console commands, its sets the options and arguments passed via the CLI as custom parameters to the transaction trace for easier debugging.

3. **Exception Listening**: It also captures all Symfony exceptions in web requests and console commands and sends them to New Relic (something new relic doesn't do too well itself as symfony aggressively catches all exceptions/errors). It also ensures all HTTP Exceptions (4xx codes) are logged as notices in New Relic and not exceptions to reduce the noise in New Relic.

4. **Interactor Service**: It provides you the New Relic PHP Agent API via a Service class `NewRelicInteractorInterface::class` so in my code, I can inject it into any class, controller, service and do stuff like -

    ```php
    // Bundle
    $this->newRelic->addCustomParameter('name', 'john');

    // Extension
    if (extension_loaded('newrelic')) {
        \newrelic_add_custom_parameter('name', 'john');
    }
    ```

5. **Logging Support**: In development, you are unlikely to have New Relic setup. There's a configuration to enable logging which outputs all New Relic actions to your Symfony log, hence emulating what it would actually do in production.

6. **Ignored Routes, Paths, Commands**: You can configure a list of route name, url paths and console commands to be ignored from New Relic traces.

    ![image](https://cloud.githubusercontent.com/assets/670655/5153003/5c956c1e-7235-11e4-9eb2-d203fa42420b.png)

7. **Misc**: There are other useful configuration like your New Relic API Key, explicitly defining your app name instead of php.ini, notifying New Relic about new deployments via capifony, etc.


![Ekino NewRelicBundle](https://dl.dropbox.com/s/bufb6f8o0end5xo/ekino_newrelic_bundle.png "Ekino NewRelicBundle")



## Installation

### Step 0 : Install NewRelic

review http://newrelic.com ...

### Step 1: add dependency

```bash
$ composer require ekino/newrelic-bundle
```

### Step 2 : Register the bundle

Then register the bundle with your kernel:

```php
<?php

// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new Ekino\NewRelicBundle\EkinoNewRelicBundle(),
    // ...
);
```

### Step 3 : Configure the bundle

In New Relic's web interface, make sure to get a valid (REST) API Key, not to be confused with your License key : New Relic Dashboard > Account settings > Integration > API Keys

```yaml
# app/config/config.yml

ekino_new_relic:
    enabled: true                         # Defaults to true
    application_name: Awesome Application # default value in newrelic is "PHP Application", or whatever is set
                                          # as php ini-value
    deployment_names: ~                   # default value is 'application_name', supports string array or semi-colon separated string
    api_key:                              # New Relic API
    api_host: ~                           # New Relic API Host (default value is api.newrelic.com, for EU should be set to api.eu.newrelic.com )
    license_key:                          # New Relic license key (optional, default value is read from php.ini)
    xmit: false                           # if you want to record the metric data up to the point newrelic_set_appname is called, set this to true (default: false)
    logging: false                        # If true, logs all New Relic interactions to the Symfony log (default: false)
    interactor: ~                         # The interactor service that is used. Setting enabled=false will override this value 
    twig: true                            # Allows you to disable twig integration (falls back to class_exists(\Twig_Environment::class))
    exceptions: true                      # If true, sends exceptions to New Relic (default: true)
    deprecations: true                    # If true, reports deprecations to New Relic (default: true)
    instrument: false                     # If true, uses enhanced New Relic RUM instrumentation (see below) (default: false)
    http:
        enabled: true
        using_symfony_cache: false        # Symfony HTTP cache (see below) (default: false)
        transaction_naming: route         # route, controller or service (see below)
        transaction_naming_service: ~     # Transaction naming service (see below)
        ignored_routes: []                # No transaction recorded for this routes
        ignored_paths: []                 # No transaction recorded for this paths
    monolog: 
        enabled: false                    # When enabled, send application's logs to New Relic (default: disabled)
        channels: [app]                   # Channels to listen (default: null). [See Symfony's documentation](http://symfony.com/doc/current/logging/channels_handlers.html#yaml-specification)
        level: error                      # Report only logs higher than this level (see \Psr\Log\LogLevel) (default: error)
        service: app.my_custom_handler    # Define a custom log handler (default: ekino.new_relic.monolog_handler)
    commands: 
        enabled: true                     # If true, logs CLI commands to New Relic as Background jobs (>2.3 only) (default: true)
        ignored_commands: []              # No transaction recorded for this commands (background tasks)
```

## Enhanced RUM instrumentation

The bundle comes with an option for enhanced real user monitoring. Ordinarily the New Relic extension (unless disabled by configuration) automatically adds a tracking code for RUM instrumentation to all HTML responses. Using enhanced RUM instrumentation, the bundle allows you to selectively disable instrumentation on certain requests.

This can be useful if, e.g. you're returning HTML verbatim for an HTML editor.

If enhanced RUM instrumentation is enabled, you can *disable* instrumentation for a given request by passing along a `_instrument` request parameter, and setting it to `false`. This can be done e.g. through the routing configuration.

## Transaction naming strategies

The bundle comes with two built-in transaction naming strategies. `route` and `controller`, naming the New Relic transaction after the route or controller respectively. However, the bundle supports custom transaction naming strategies through the `service` configuration option. If you have selected the `service` configuration option, you must pass the name of your own transaction naming service as the `transaction_naming_service` configuration option.

The transaction naming service class must implement the `Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface` interface. For more information on creating your own services, see the Symfony documentation on [Creating/Configuring Services in the Container](http://symfony.com/doc/current/book/service_container.html#creating-configuring-services-in-the-container).

## Symfony HTTP Cache

When you are using Symfony's HTTP cache your `app/AppCache.php` will build up a response with your Edge Side Includes (ESI). This will look like one transaction in New Relic. When you set `using_symfony_cache: true` will these ESI request be separate transaction which improves the statistics. If you are using some other reverse proxy cache or no cache at all, leave this to false.

If true is required to set the `application_name`.


## Deployment notification

You can use the `newrelic:notify-deployment` command to send deployment notifications to New Relic. This requires the `api_key` configuration to be set.

The command has a bunch of options, as displayed in the help data.

```
$ app/console newrelic:notify-deployment --help
Usage:
 newrelic:notify-deployment [--user[="..."]] [--revision[="..."]] [--changelog[="..."]] [--description[="..."]]

Options:
 --user         The name of the user/process that triggered this deployment
 --revision     A revision number (e.g., git commit SHA)
 --changelog    A list of changes for this deployment
 --description  Text annotation for the deployment — notes for you
```

The bundle provide a [Capifony](http://capifony.org) recipe to automate the deployment notifications (see `Resources/recipes/newrelic.rb`).

It makes one request per `app_name`, due roll-up names are not supported by Data REST API.

## Interactor services

The config key`ekino_new_relic.interactor` will accept a service ID to a service implementing `NewRelicInteractorInterface`. 
This bundle comes with a few services that may be suitable for you. 

| Configuration value | Description |
| ------------------- | ----------- |
| `Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor` | This is the default interactor. It will check once per request if the NewRelic PHP extension is installed or not. It is a decorator for the `NewRelicInteractor` | 
| `Ekino\NewRelicBundle\NewRelic\NewRelicInteractor` | This interactor communicates with NewRelic. It is the one decorator that actually does some work. | 
| `Ekino\NewRelicBundle\NewRelic\BlackholeInteractor` | This interactor does nothing. | 
| `auto` | This value will check if the NewRelic PHP extension is installed when you build your container. | 

Note that if you set `ekino_new_relic.enabled: false` you will always use the `BlackholeInteractor` no matter what value 
used for `ekino_new_relic.interactor`.
 

## Flow of the Request

1. A request comes in and the first thing we do is to `setApplicationName` so that we use the correct license key and name.
2. The `RouterListener` might throw a 404 or add routing values to the request.
3. If no 404 was thrown we `setIgnoreTransaction` which means that we call `NewRelicInteractorInterface::ignoreTransaction()` if we have configured to ignore the route.
4. The Firewall is the next interesting thing that will happen. It could change the controller or throw a 403.
5. The developer might have configured many more request listeners that will now execute and possibly add stuff to the request.
6. We will execute `setTransactionName` to use our `TransactionNamingStrategyInterface` to set a nice name.

All 6 steps will be executed for a normal request. Exceptions to this is 404 and 403 responses that will be created in
step 2 and step 4 respectively. If an exception to these step occurs (I'm not talking about `\Exception`) you will have
the transaction logged with the correct license key but you do not have the proper transaction name. The `setTransactionName` may
have dependencies on data set by other listeners that is why it has such low priority.


================================================
FILE: Resources/config/command_listener.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />

        <service id="Ekino\NewRelicBundle\Listener\CommandListener"/>
    </services>
</container>


================================================
FILE: Resources/config/deprecation_listener.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />

        <service id="Ekino\NewRelicBundle\Listener\DeprecationListener" public="true" />
    </services>
</container>


================================================
FILE: Resources/config/exception_listener.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />

        <service id="Ekino\NewRelicBundle\Listener\ExceptionListener"/>
    </services>
</container>


================================================
FILE: Resources/config/http_listener.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />

        <service id="Ekino\NewRelicBundle\Listener\RequestListener"/>
        <service id="Ekino\NewRelicBundle\Listener\ResponseListener"/>
    </services>
</container>


================================================
FILE: Resources/config/monolog.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />
        <prototype namespace="Ekino\NewRelicBundle\Logging\" resource="../../Logging/*" />

        <!-- id must be defined to not override custom configuration of `Monolog\Handler\NewRelicHandler` -->
        <service id="ekino.new_relic.monolog_handler" class="Monolog\Handler\NewRelicHandler" />
    </services>
</container>


================================================
FILE: Resources/config/services.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />
        <prototype namespace="Ekino\NewRelicBundle\Command\" resource="../../Command/*" />
        <prototype namespace="Ekino\NewRelicBundle\NewRelic\" resource="../../NewRelic/*" />
        <prototype namespace="Ekino\NewRelicBundle\TransactionNamingStrategy\" resource="../../TransactionNamingStrategy/*" />

        <service id="Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor">
            <argument type="service" id="Ekino\NewRelicBundle\NewRelic\NewRelicInteractor" />
            <argument type="service" id="Ekino\NewRelicBundle\NewRelic\BlackholeInteractor" />
        </service>
    </services>
</container>


================================================
FILE: Resources/config/twig.xml
================================================
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />
        <prototype namespace="Ekino\NewRelicBundle\Twig\" resource="../../Twig/*" />

    </services>
</container>


================================================
FILE: Resources/recipes/newrelic.rb
================================================
namespace :newrelic do

  # on all deployments, notify New Relic
  desc "Record a deployment in New Relic (newrelic.com)"
  task :notice_deployment, :roles => :app, :only => { :primary => true }, :except => { :no_release => true } do
    begin
      # allow overrides to be defined for revision, description, changelog
      rev = fetch(:newrelic_revision) if exists?(:newrelic_revision)
      description = fetch(:newrelic_desc) if exists?(:newrelic_desc)
      changelog = fetch(:newrelic_changelog) if exists?(:newrelic_changelog)
      user = fetch(:newrelic_user) if exists?(:newrelic_user)

      if !changelog
        logger.debug "Getting log of changes for New Relic Deployment details"
        from_revision = source.local.next_revision(current_revision)
        if scm == :git
          log_command = "git log --no-color --pretty=format:'  * [%ai] %an: %s' --abbrev-commit --no-merges #{previous_revision}..#{real_revision}"
        else
          log_command = "#{source.local.log(from_revision)}"
        end
        changelog = `#{log_command}`
      end
      if rev.nil?
        rev = source.local.query_revision(source.local.head()) do |cmd|
          logger.debug "executing locally: '#{cmd}'"
          `#{cmd}`
        end
        rev = rev[0..6] if scm == :git
      end
      new_revision = rev
      logger.debug "Uploading deployment to New Relic"
      capifony_pretty_print "--> Notifying New Relic of deployment"

      run "cd #{latest_release} && #{php_bin} #{symfony_console} newrelic:notify-deployment #{console_options} --revision=#{rev.shellescape} --changelog=#{changelog.to_s.shellescape} --description=#{description.to_s.shellescape} --user=#{user.to_s.shellescape}"
      capifony_puts_ok
    rescue Capistrano::CommandError
      logger.info "Unable to notify New Relic of the deployment... skipping"
    end
  end
end


================================================
FILE: Tests/AppKernel.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests;

use Ekino\NewRelicBundle\EkinoNewRelicBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\RouteCollection;

class AppKernel extends Kernel
{
    /**
     * @var string
     */
    private $cachePrefix = '';

    /**
     * @var string|null;
     */
    private $fakedProjectDir;

    /**
     * @param string $cachePrefix
     */
    public function __construct($cachePrefix)
    {
        parent::__construct($cachePrefix, true);
        $this->cachePrefix = $cachePrefix;
    }

    public function getCacheDir(): string
    {
        return sys_get_temp_dir().'/ekino/'.$this->cachePrefix;
    }

    public function getLogDir(): string
    {
        return sys_get_temp_dir().'/ekino/log';
    }

    public function getProjectDir(): string
    {
        if (null === $this->fakedProjectDir) {
            return realpath(__DIR__.'/../../../../');
        }

        return $this->fakedProjectDir;
    }

    /**
     * @param string|null $rootDir
     */
    public function setRootDir($rootDir)
    {
        $this->rootDir = $rootDir;
    }

    /**
     * @param string|null $projectDir
     */
    public function setProjectDir($projectDir)
    {
        $this->fakedProjectDir = $projectDir;
    }

    public function registerBundles(): iterable
    {
        return [
            new FrameworkBundle(),
            new EkinoNewRelicBundle(),
        ];
    }

    /**
     * (From MicroKernelTrait)
     * {@inheritdoc}
     */
    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(function (ContainerBuilder $container) {
            $container->loadFromExtension('framework', [
                'secret' => 'test',
                'router' => [
                    'resource' => 'kernel:loadRoutes',
                    'type' => 'service',
                ],
            ]);

            // Not setting the router to utf8 is deprecated in symfony 5.1
            if (Kernel::VERSION_ID >= 50100) {
                $container->loadFromExtension('framework', [
                    'router' => ['utf8' => true],
                ]);
            }

            // Not setting the "framework.session.storage_factory_id" configuration option is deprecated in symfony 5.3
            if (Kernel::VERSION_ID >= 50300) {
                $container->loadFromExtension('framework', [
                    'session' => ['storage_factory_id' => 'session.storage.factory.mock_file'],
                ]);
            } else {
                $container->loadFromExtension('framework', [
                    'session' => ['storage_id' => 'session.storage.mock_file'],
                ]);
            }

            $container->addObjectResource($this);
        });
    }

    /**
     * (From MicroKernelTrait).
     *
     * @internal
     */
    public function loadRoutes(LoaderInterface $loader)
    {
        return new RouteCollection();
    }

    /**
     * {@inheritdoc}
     */
    protected function buildContainer(): ContainerBuilder
    {
        $container = parent::buildContainer();

        $container->addCompilerPass(new class() implements CompilerPassInterface {
            public function process(ContainerBuilder $container)
            {
                foreach ($container->getDefinitions() as $id => $definition) {
                    if (preg_match('|Ekino.*|i', $id)) {
                        $definition->setPublic(true);
                    }
                }

                foreach ($container->getAliases() as $id => $alias) {
                    if (preg_match('|Ekino.*|i', $id)) {
                        $alias->setPublic(true);
                    }
                }
            }
        });

        return $container;
    }
}


================================================
FILE: Tests/BundleInitializationTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests;

use Ekino\NewRelicBundle\EkinoNewRelicBundle;
use Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor;
use Ekino\NewRelicBundle\NewRelic\BlackholeInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;

/**
 * Smoke test to see if the bundle can run.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class BundleInitializationTest extends TestCase
{
    protected function getBundleClass()
    {
        return EkinoNewRelicBundle::class;
    }

    public function testInitBundle()
    {
        $kernel = new AppKernel(uniqid('cache'));
        $kernel->boot();

        // Get the container
        $container = $kernel->getContainer();

        $services = [
            NewRelicInteractorInterface::class => AdaptiveInteractor::class,
            BlackholeInteractor::class,
            NewRelicInteractor::class,
        ];

        // Test if you services exists
        foreach ($services as $id => $class) {
            if (\is_int($id)) {
                $id = $class;
            }
            $this->assertTrue($container->has($id));
            $service = $container->get($id);
            $this->assertInstanceOf($class, $service);
        }
    }
}


================================================
FILE: Tests/DependencyInjection/Compiler/MonologHandlerPassTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\DependencyInjection\Compiler;

use Ekino\NewRelicBundle\DependencyInjection\Compiler\MonologHandlerPass;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class MonologHandlerPassTest extends AbstractCompilerPassTestCase
{
    protected function registerCompilerPass(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new MonologHandlerPass());
    }

    public function testProcessChannel()
    {
        $this->container->setParameter('ekino.new_relic.monolog', ['level' => 100, 'channels' => ['type' => 'inclusive', 'elements' => ['app', 'foo']]]);
        $this->container->setParameter('ekino.new_relic.application_name', 'app');
        $this->registerService('ekino.new_relic.monolog_handler', \Monolog\Handler\NewRelicHandler::class);
        $this->container->setAlias('ekino.new_relic.logs_handler', 'ekino.new_relic.monolog_handler')->setPublic(false);
        $this->registerService('monolog.logger', \Monolog\Logger::class)->setArgument(0, 'app');
        $this->registerService('monolog.logger.foo', \Monolog\Logger::class)->setArgument(0, 'foo');

        $this->compile();

        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger.foo', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
    }

    public function testProcessChannelAllChannels()
    {
        $this->container->setParameter('ekino.new_relic.monolog', ['level' => 100, 'channels' => null]);
        $this->container->setParameter('ekino.new_relic.application_name', 'app');
        $this->registerService('ekino.new_relic.monolog_handler', \Monolog\Handler\NewRelicHandler::class);
        $this->container->setAlias('ekino.new_relic.logs_handler', 'ekino.new_relic.monolog_handler')->setPublic(false);
        $this->registerService('monolog.logger', \Monolog\Logger::class)->setArgument(0, 'app');
        $this->registerService('monolog.logger.foo', \Monolog\Logger::class)->setArgument(0, 'foo');

        $this->compile();

        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger.foo', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
    }

    public function testProcessChannelExcludeChannels()
    {
        $this->container->setParameter('ekino.new_relic.monolog', ['level' => 100, 'channels' => ['type' => 'exclusive', 'elements' => ['foo']]]);
        $this->container->setParameter('ekino.new_relic.application_name', 'app');
        $this->registerService('ekino.new_relic.monolog_handler', \Monolog\Handler\NewRelicHandler::class);
        $this->container->setAlias('ekino.new_relic.logs_handler', 'ekino.new_relic.monolog_handler')->setPublic(false);
        $this->registerService('monolog.logger', \Monolog\Logger::class)->setArgument(0, 'app');
        $this->registerService('monolog.logger.foo', \Monolog\Logger::class)->setArgument(0, 'foo');

        $this->compile();

        $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
    }
}


================================================
FILE: Tests/DependencyInjection/ConfigurationTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\DependencyInjection;

use Ekino\NewRelicBundle\DependencyInjection\Configuration;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\Definition\PrototypedArrayNode;

class ConfigurationTest extends TestCase
{
    public function testIgnoredRoutes()
    {
        $configuration = new Configuration();
        $rootNode = $configuration->getConfigTreeBuilder()
            ->buildTree();
        $children = $rootNode->getChildren();

        /** @var PrototypedArrayNode $ignoredRoutesNode */
        $ignoredRoutesNode = $children['http']->getChildren()['ignored_routes'];

        $this->assertInstanceOf('\Symfony\Component\Config\Definition\PrototypedArrayNode', $ignoredRoutesNode);
        $this->assertFalse($ignoredRoutesNode->isRequired());
        $this->assertEmpty($ignoredRoutesNode->getDefaultValue());

        $this->assertSame(['ignored_route1', 'ignored_route2'], $ignoredRoutesNode->normalize(['ignored_route1', 'ignored_route2']));
        $this->assertSame(['ignored_route'], $ignoredRoutesNode->normalize('ignored_route'));
        $this->assertSame(['ignored_route1', 'ignored_route2'], $ignoredRoutesNode->merge(['ignored_route1'], ['ignored_route2']));
    }

    public function testIgnoredPaths()
    {
        $configuration = new Configuration();
        $rootNode = $configuration->getConfigTreeBuilder()
            ->buildTree();
        $children = $rootNode->getChildren();

        /** @var PrototypedArrayNode $ignoredPathsNode */
        $ignoredPathsNode = $children['http']->getChildren()['ignored_paths'];

        $this->assertInstanceOf('\Symfony\Component\Config\Definition\PrototypedArrayNode', $ignoredPathsNode);
        $this->assertFalse($ignoredPathsNode->isRequired());
        $this->assertEmpty($ignoredPathsNode->getDefaultValue());

        $this->assertSame(['/ignored/path1', '/ignored/path2'], $ignoredPathsNode->normalize(['/ignored/path1', '/ignored/path2']));
        $this->assertSame(['/ignored/path'], $ignoredPathsNode->normalize('/ignored/path'));
        $this->assertSame(['/ignored/path1', '/ignored/path2'], $ignoredPathsNode->merge(['/ignored/path1'], ['/ignored/path2']));
    }

    public function testIgnoredCommands()
    {
        $configuration = new Configuration();
        $rootNode = $configuration->getConfigTreeBuilder()
            ->buildTree();
        $children = $rootNode->getChildren();

        /** @var PrototypedArrayNode $ignoredCommandsNode */
        $ignoredCommandsNode = $children['commands']->getChildren()['ignored_commands'];

        $this->assertInstanceOf('\Symfony\Component\Config\Definition\PrototypedArrayNode', $ignoredCommandsNode);
        $this->assertFalse($ignoredCommandsNode->isRequired());
        $this->assertEmpty($ignoredCommandsNode->getDefaultValue());

        $this->assertSame(['test:ignored-command1', 'test:ignored-command2'], $ignoredCommandsNode->normalize(['test:ignored-command1', 'test:ignored-command2']));
        $this->assertSame(['test:ignored-command'], $ignoredCommandsNode->normalize('test:ignored-command'));
        $this->assertSame(['test:ignored-command1', 'test:ignored-command2'], $ignoredCommandsNode->merge(['test:ignored-command1'], ['test:ignored-command2']));
    }

    public function testDefaults()
    {
        $processor = new Processor();

        $config = $processor->processConfiguration(new Configuration(), []);

        $this->assertEmpty($config['http']['ignored_routes']);
        $this->assertIsArray($config['http']['ignored_routes']);
        $this->assertEmpty($config['http']['ignored_paths']);
        $this->assertIsArray($config['http']['ignored_paths']);
        $this->assertEmpty($config['commands']['ignored_commands']);
        $this->assertIsArray($config['commands']['ignored_commands']);
        $this->assertEmpty($config['deployment_names']);
        $this->assertIsArray($config['deployment_names']);
    }

    public static function ignoredRoutesProvider()
    {
        return [
            ['single_ignored_route', ['single_ignored_route']],
            [['single_ignored_route'], ['single_ignored_route']],
            [['ignored_route1', 'ignored_route2'], ['ignored_route1', 'ignored_route2']],
        ];
    }

    public static function ignoredPathsProvider()
    {
        return [
            ['/single/ignored/path', ['/single/ignored/path']],
            [['/single/ignored/path'], ['/single/ignored/path']],
            [['/ignored/path1', '/ignored/path2'], ['/ignored/path1', '/ignored/path2']],
        ];
    }

    public static function ignoredCommandsProvider()
    {
        return [
            ['single:ignored:command', ['single:ignored:command']],
            [['single:ignored:command'], ['single:ignored:command']],
            [['ignored:command1', 'ignored:command2'], ['ignored:command1', 'ignored:command2']],
        ];
    }

    public static function deploymentNamesProvider()
    {
        return [
            ['App1', ['App1']],
            [['App1'], ['App1']],
            [['App1', 'App2'], ['App1', 'App2']],
        ];
    }

    /**
     * @dataProvider deploymentNamesProvider
     */
    public function testDeploymentNames($deploymentNameConfig, $expected)
    {
        $processor = new Processor();

        $config1 = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['deployment_name' => $deploymentNameConfig]]);
        $config2 = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['deployment_names' => $deploymentNameConfig]]);

        $this->assertSame($expected, $config1['deployment_names']);
        $this->assertSame($expected, $config2['deployment_names']);
    }

    /**
     * @dataProvider ignoredRoutesProvider
     */
    public function testIgnoreRoutes($ignoredRoutesConfig, $expected)
    {
        $processor = new Processor();

        $config = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['http' => ['ignored_routes' => $ignoredRoutesConfig]]]);

        $this->assertSame($expected, $config['http']['ignored_routes']);
    }

    /**
     * @dataProvider ignoredPathsProvider
     */
    public function testIgnorePaths($ignoredPathsConfig, $expected)
    {
        $processor = new Processor();

        $config = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['http' => ['ignored_paths' => $ignoredPathsConfig]]]);

        $this->assertSame($expected, $config['http']['ignored_paths']);
    }

    /**
     * @dataProvider ignoredCommandsProvider
     */
    public function testIgnoreCommands($ignoredCommandsConfig, $expected)
    {
        $processor = new Processor();

        $config = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['commands' => ['ignored_commands' => $ignoredCommandsConfig]]]);

        $this->assertSame($expected, $config['commands']['ignored_commands']);
    }
}


================================================
FILE: Tests/DependencyInjection/EkinoNewRelicExtensionTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\DependencyInjection;

use Ekino\NewRelicBundle\DependencyInjection\EkinoNewRelicExtension;
use Ekino\NewRelicBundle\Listener\CommandListener;
use Ekino\NewRelicBundle\Listener\DeprecationListener;
use Ekino\NewRelicBundle\Listener\ExceptionListener;
use Ekino\NewRelicBundle\NewRelic\BlackholeInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerHasParameterConstraint;
use PHPUnit\Framework\Constraint\LogicalNot;

class EkinoNewRelicExtensionTest extends AbstractExtensionTestCase
{
    protected function getContainerExtensions(): array
    {
        return [new EkinoNewRelicExtension()];
    }

    protected function setUp(): void
    {
        parent::setUp();

        $this->setParameter('kernel.bundles', []);
    }

    public function testDefaultConfiguration()
    {
        $this->load();

        $this->assertContainerBuilderHasService(NewRelicExtension::class);
        $this->assertContainerBuilderHasService(CommandListener::class);
        $this->assertContainerBuilderHasService(ExceptionListener::class);
    }

    public function testAlternativeConfiguration()
    {
        $this->load([
            'exceptions' => false,
            'commands' => false,
            'twig' => false,
        ]);

        $this->assertContainerBuilderNotHasService(NewRelicExtension::class);
        $this->assertContainerBuilderNotHasService(CommandListener::class);
        $this->assertContainerBuilderNotHasService(ExceptionListener::class);
    }

    public function testDeprecation()
    {
        $this->load();

        $this->assertContainerBuilderHasService(DeprecationListener::class);
    }

    public function testMonolog()
    {
        $this->load(['monolog' => true]);

        $this->assertContainerBuilderHasParameter('ekino.new_relic.monolog');
        $this->assertContainerBuilderHasParameter('ekino.new_relic.application_name');
        $this->assertContainerBuilderHasService('ekino.new_relic.logs_handler');
    }

    public function testMonologDisabled()
    {
        $this->load(['monolog' => false]);

        self::assertThat(
            $this->container,
            new LogicalNot(new ContainerHasParameterConstraint('ekino.new_relic.monolog', null, false))
        );
    }

    public function testConfigDisabled()
    {
        $this->load([
            'enabled' => false,
        ]);

        $this->assertContainerBuilderHasAlias(NewRelicInteractorInterface::class, BlackholeInteractor::class);
    }

    public function testConfigDisabledWithInteractor()
    {
        $this->load([
            'enabled' => false,
            'interactor' => 'ekino.new_relic.interactor.adaptive',
        ]);

        $this->assertContainerBuilderHasAlias(NewRelicInteractorInterface::class, BlackholeInteractor::class);
    }

    public function testConfigEnabledWithInteractor()
    {
        $this->load([
            'enabled' => true,
            'interactor' => 'ekino.new_relic.interactor.adaptive',
        ]);

        $this->assertContainerBuilderHasAlias(NewRelicInteractorInterface::class, 'ekino.new_relic.interactor.adaptive');
    }
}


================================================
FILE: Tests/Listener/CommandListenerTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\Listener;

use Ekino\NewRelicBundle\Listener\CommandListener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class CommandListenerTest extends TestCase
{
    public function testCommandMarkedAsBackgroundJob()
    {
        if (!class_exists('Symfony\Component\Console\Event\ConsoleCommandEvent')) {
            $this->markTestSkipped('Console Events is only available from Symfony 2.3');
        }

        $parameters = [
            '--foo' => true,
            '--foobar' => ['baz', 'baz_2'],
            'name' => 'bar',
        ];

        $definition = new InputDefinition([
            new InputOption('foo'),
            new InputOption('foobar', 'fb', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY),
            new InputArgument('name', InputArgument::REQUIRED),
         ]);

        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('setTransactionName')->with($this->equalTo('test:newrelic'));
        $interactor->expects($this->once())->method('enableBackgroundJob');

        $interactor->expects($this->exactly(4))->method('addCustomParameter')->withConsecutive(
            ['--foo', true],
            ['--foobar[0]', 'baz'],
            ['--foobar[1]', 'baz_2'],
            ['name', 'bar']
        );

        $command = new Command('test:newrelic');
        $input = new ArrayInput($parameters, $definition);

        $output = $this->getMockBuilder(OutputInterface::class)->getMock();

        $event = new ConsoleCommandEvent($command, $input, $output);

        $listener = new CommandListener(new Config('App name', 'Token'), $interactor, []);
        $listener->onConsoleCommand($event);
    }

    public function testIgnoreBackgroundJob()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->never())->method('startTransaction');

        $command = new Command('test:ignored-commnand');
        $input = new ArrayInput([], new InputDefinition([]));

        $output = $this->getMockBuilder(OutputInterface::class)->getMock();

        $event = new ConsoleCommandEvent($command, $input, $output);

        $listener = new CommandListener(new Config('App name', 'Token'), $interactor, ['test:ignored-command']);
        $listener->onConsoleCommand($event);
    }

    public function testConsoleError()
    {
        $exception = new \Exception('', 1);

        $newrelic = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock();
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('noticeThrowable')->with($exception);

        $command = new Command('test:exception');

        $input = new ArrayInput([], new InputDefinition([]));
        $output = $this->getMockBuilder(OutputInterface::class)->getMock();

        $event = new ConsoleErrorEvent($input, $output, $exception, $command);

        $listener = new CommandListener($newrelic, $interactor, ['test:exception']);
        $listener->onConsoleError($event);
    }

    public function testConsoleErrorsWithThrowable()
    {
        $exception = new \Error();

        $newrelic = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock();
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('noticeThrowable')->with($exception);
        $command = new Command('test:exception');

        $input = new ArrayInput([], new InputDefinition([]));
        $output = $this->getMockBuilder(OutputInterface::class)->getMock();

        $event = new ConsoleErrorEvent($input, $output, $exception, $command);

        $listener = new CommandListener($newrelic, $interactor, ['test:exception']);
        $listener->onConsoleError($event);
    }
}


================================================
FILE: Tests/Listener/DeprecationListenerTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\Listener;

use Ekino\NewRelicBundle\Exception\DeprecationException;
use Ekino\NewRelicBundle\Listener\DeprecationListener;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;

class DeprecationListenerTest extends TestCase
{
    public function testDeprecationIsReported()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('noticeThrowable')->with(
            $this->isInstanceOf(DeprecationException::class)
        );

        $listener = new DeprecationListener($interactor);

        set_error_handler(function () { return false; });
        try {
            $listener->register();
            @trigger_error('This is a deprecation', \E_USER_DEPRECATED);
        } finally {
            $listener->unregister();
            restore_error_handler();
        }
    }

    public function testDeprecationIsReportedRegardlessErrorReporting()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('noticeThrowable');

        $listener = new DeprecationListener($interactor);

        set_error_handler(function () { return false; });
        $e = error_reporting(0);
        try {
            $listener->register();
            @trigger_error('This is a deprecation', \E_USER_DEPRECATED);
        } finally {
            $listener->unregister();
            error_reporting($e);
            restore_error_handler();
        }
    }

    public function testOtherErrorAreIgnored()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->never())->method('noticeThrowable');

        $listener = new DeprecationListener($interactor);

        set_error_handler(function () { return false; });
        try {
            $listener->register();
            @trigger_error('This is a notice', \E_USER_NOTICE);
        } finally {
            $listener->unregister();
            restore_error_handler();
        }
    }

    public function testInitialHandlerIsCalled()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('noticeThrowable');

        $handler = $this->createPartialMock(DummyHandler::class, ['__invoke']);
        $handler->expects($this->once())->method('__invoke');

        $listener = new DeprecationListener($interactor);

        set_error_handler($handler);
        try {
            $listener->register();
            @trigger_error('This is a deprecation', \E_USER_DEPRECATED);
        } finally {
            $listener->unregister();
            restore_error_handler();
        }
    }

    public function testUnregisterRemovesHandler()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->never())->method('noticeThrowable');

        $listener = new DeprecationListener($interactor);

        set_error_handler(function () { return false; });
        try {
            $listener->register();
            $listener->unregister();
            @trigger_error('This is a deprecation', \E_USER_DEPRECATED);
        } finally {
            restore_error_handler();
        }
    }

    public function testUnregisterRestorePreviousHandler()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();

        $handler = $this->createPartialMock(DummyHandler::class, ['__invoke']);
        $handler->expects($this->once())->method('__invoke');

        $listener = new DeprecationListener($interactor);

        set_error_handler($handler);
        try {
            $listener->register();
            $listener->unregister();
            @trigger_error('This is a deprecation', \E_USER_DEPRECATED);
        } finally {
            restore_error_handler();
        }
    }
}

class DummyHandler
{
    public function __invoke()
    {
    }
}


================================================
FILE: Tests/Listener/ExceptionListenerTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\Listener;

use Ekino\NewRelicBundle\Listener\ExceptionListener;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class ExceptionListenerTest extends TestCase
{
    public function testOnKernelException()
    {
        $exception = new \Exception('Boom');

        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('noticeThrowable')->with($exception);

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
        $request = new Request();

        $eventClass = class_exists(ExceptionEvent::class) ? ExceptionEvent::class : GetResponseForExceptionEvent::class;
        $event = new $eventClass($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);

        $listener = new ExceptionListener($interactor);
        $listener->onKernelException($event);
    }

    public function testOnKernelExceptionWithHttp()
    {
        $exception = new BadRequestHttpException('Boom');

        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->never())->method('noticeThrowable');

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
        $request = new Request();

        $eventClass = class_exists(ExceptionEvent::class) ? ExceptionEvent::class : GetResponseForExceptionEvent::class;
        $event = new $eventClass($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);

        $listener = new ExceptionListener($interactor);
        $listener->onKernelException($event);
    }
}


================================================
FILE: Tests/Listener/RequestListenerTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\Listener;

use Ekino\NewRelicBundle\Listener\RequestListener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class RequestListenerTest extends TestCase
{
    public function testSubRequest()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->never())->method('setTransactionName');

        $namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();

        $eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
        $event = new $eventClass($kernel, new Request(), HttpKernelInterface::SUB_REQUEST, new Response());

        $listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy);
        $listener->setApplicationName($event);
    }

    public function testMasterRequest()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('setTransactionName');

        $namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)
            ->setMethods(['getTransactionName'])
            ->getMock();
        $namingStrategy->expects($this->once())->method('getTransactionName')->willReturn('foobar');

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();

        $eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
        $event = new $eventClass($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response());

        $listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy);
        $listener->setTransactionName($event);
    }

    public function testPathIsIgnored()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('ignoreTransaction');

        $namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
        $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/ignored_path']);

        $eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
        $event = new $eventClass($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response());

        $listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], ['/ignored_path'], $namingStrategy);
        $listener->setIgnoreTransaction($event);
    }

    public function testRouteIsIgnored()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('ignoreTransaction');

        $namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
        $request = new Request([], [], ['_route' => 'ignored_route']);

        $eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
        $event = new $eventClass($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response());

        $listener = new RequestListener(new Config('App name', 'Token'), $interactor, ['ignored_route'], [], $namingStrategy);
        $listener->setIgnoreTransaction($event);
    }

    public function testSymfonyCacheEnabled()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->once())->method('startTransaction');

        $namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();

        $eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
        $event = new $eventClass($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response());

        $listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy, true);
        $listener->setApplicationName($event);
    }

    public function testSymfonyCacheDisabled()
    {
        $interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $interactor->expects($this->never())->method('startTransaction');

        $namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();

        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();

        $eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
        $event = new $eventClass($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response());

        $listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy, false);
        $listener->setApplicationName($event);
    }
}


================================================
FILE: Tests/Listener/ResponseListenerTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\Listener;

use Ekino\NewRelicBundle\Listener\ResponseListener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class ResponseListenerTest extends TestCase
{
    protected function setUp(): void
    {
        $this->interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
        $this->newRelic = $this->getMockBuilder(Config::class)
            ->setMethods(['getCustomEvents', 'getCustomMetrics', 'getCustomParameters'])
            ->disableOriginalConstructor()
            ->getMock();
        $this->extension = $this->getMockBuilder(NewRelicExtension::class)
            ->setMethods(['isHeaderCalled', 'isFooterCalled', 'isUsed'])
            ->disableOriginalConstructor()
            ->getMock();
    }

    public function testOnKernelResponseOnlyMasterRequestsAreProcessed()
    {
        $event = $this->createFilterResponseEventDummy(null, null, HttpKernelInterface::SUB_REQUEST);

        $object = new ResponseListener($this->newRelic, $this->interactor);
        $object->onKernelResponse($event);

        $this->newRelic->expects($this->never())->method('getCustomMetrics');
    }

    public function testOnKernelResponseWithOnlyCustomMetricsAndParameters()
    {
        $events = [
            'WidgetSale' => [
                [
                    'color' => 'red',
                    'weight' => 12.5,
                ],
                [
                    'color' => 'blue',
                    'weight' => 12.5,
                ],
            ],
        ];

        $metrics = [
            'foo_a' => 4.7,
            'foo_b' => 11,
        ];

        $parameters = [
            'foo_1' => 'bar_1',
            'foo_2' => 'bar_2',
        ];

        $this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn($events);
        $this->newRelic->expects($this->once())->method('getCustomMetrics')->willReturn($metrics);
        $this->newRelic->expects($this->once())->method('getCustomParameters')->willReturn($parameters);

        $this->interactor->expects($this->exactly(2))->method('addCustomMetric')->withConsecutive(
            ['foo_a', 4.7],
            ['foo_b', 11]
        );
        $this->interactor->expects($this->exactly(2))->method('addCustomParameter')->withConsecutive(
            ['foo_1', 'bar_1'],
            ['foo_2', 'bar_2']
        );
        $this->interactor->expects($this->exactly(2))->method('addCustomEvent')->withConsecutive(
            ['WidgetSale', [
                'color' => 'red',
                'weight' => 12.5,
            ]],
            ['WidgetSale', [
                'color' => 'blue',
                'weight' => 12.5,
            ]]
        );

        $event = $this->createFilterResponseEventDummy();

        $object = new ResponseListener($this->newRelic, $this->interactor, false);
        $object->onKernelResponse($event);
    }

    public function testOnKernelResponseInstrumentDisabledInRequest()
    {
        $this->setupNoCustomMetricsOrParameters();

        $this->interactor->expects($this->once())->method('disableAutoRUM');

        $event = $this->createFilterResponseEventDummy();

        $object = new ResponseListener($this->newRelic, $this->interactor, true);
        $object->onKernelResponse($event);
    }

    public function testSymfonyCacheEnabled()
    {
        $this->setupNoCustomMetricsOrParameters();

        $this->interactor->expects($this->once())->method('endTransaction');

        $event = $this->createFilterResponseEventDummy();

        $object = new ResponseListener($this->newRelic, $this->interactor, false, true);
        $object->onKernelResponse($event);
    }

    public function testSymfonyCacheDisabled()
    {
        $this->setupNoCustomMetricsOrParameters();

        $this->interactor->expects($this->never())->method('endTransaction');

        $event = $this->createFilterResponseEventDummy();

        $object = new ResponseListener($this->newRelic, $this->interactor, false, false);
        $object->onKernelResponse($event);
    }

    /**
     * @dataProvider providerOnKernelResponseOnlyInstrumentHTMLResponses
     */
    public function testOnKernelResponseOnlyInstrumentHTMLResponses($content, $expectsSetContent, $contentType)
    {
        $this->setupNoCustomMetricsOrParameters();

        $this->interactor->expects($this->once())->method('disableAutoRUM');
        $this->interactor->expects($this->any())->method('getBrowserTimingHeader')->willReturn('__Timing_Header__');
        $this->interactor->expects($this->any())->method('getBrowserTimingFooter')->willReturn('__Timing_Feader__');

        $response = $this->createResponseMock($content, $expectsSetContent, $contentType);
        $event = $this->createFilterResponseEventDummy(null, $response);

        $object = new ResponseListener($this->newRelic, $this->interactor, true);
        $object->onKernelResponse($event);
    }

    public function providerOnKernelResponseOnlyInstrumentHTMLResponses()
    {
        return [
            // unsupported content types
            [null, null, 'text/xml'],
            [null, null, 'text/plain'],
            [null, null, 'application/json'],

            ['content', 'content', 'text/html'],
            ['<div class="head">head</div>', '<div class="head">head</div>', 'text/html'],
            ['<header>content</header>', '<header>content</header>', 'text/html'],

            // head, body tags
            ['<head><title /></head>', '<head>__Timing_Header__<title /></head>', 'text/html'],
            ['<body><div /></body>', '<body><div />__Timing_Feader__</body>', 'text/html'],
            ['<head><title /></head><body><div /></body>', '<head>__Timing_Header__<title /></head><body><div />__Timing_Feader__</body>', 'text/html'],

            // with charset
            ['<head><title /></head><body><div /></body>', '<head>__Timing_Header__<title /></head><body><div />__Timing_Feader__</body>', 'text/html; charset=UTF-8'],
        ];
    }

    public function testInteractionWithTwigExtensionHeader()
    {
        $this->newRelic->expects($this->never())->method('getCustomMetrics');
        $this->newRelic->expects($this->never())->method('getCustomParameters');
        $this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);

        $this->interactor->expects($this->never())->method('disableAutoRUM');
        $this->interactor->expects($this->never())->method('getBrowserTimingHeader');
        $this->interactor->expects($this->once())->method('getBrowserTimingFooter')->willReturn('__Timing_Feader__');

        $this->extension->expects($this->exactly(2))->method('isUsed')->willReturn(true);
        $this->extension->expects($this->once())->method('isHeaderCalled')->willReturn(true);
        $this->extension->expects($this->once())->method('isFooterCalled')->willReturn(false);

        $request = $this->createRequestMock(true);
        $response = $this->createResponseMock('content', 'content', 'text/html');
        $event = $this->createFilterResponseEventDummy($request, $response);

        $object = new ResponseListener($this->newRelic, $this->interactor, true, false, $this->extension);
        $object->onKernelResponse($event);
    }

    public function testInteractionWithTwigExtensionFooter()
    {
        $this->newRelic->expects($this->never())->method('getCustomMetrics');
        $this->newRelic->expects($this->never())->method('getCustomParameters');
        $this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);

        $this->interactor->expects($this->never())->method('disableAutoRUM');
        $this->interactor->expects($this->once())->method('getBrowserTimingHeader')->willReturn('__Timing_Feader__');
        $this->interactor->expects($this->never())->method('getBrowserTimingFooter');

        $this->extension->expects($this->exactly(2))->method('isUsed')->willReturn(true);
        $this->extension->expects($this->once())->method('isHeaderCalled')->willReturn(false);
        $this->extension->expects($this->once())->method('isFooterCalled')->willReturn(true);

        $request = $this->createRequestMock(true);
        $response = $this->createResponseMock('content', 'content', 'text/html');
        $event = $this->createFilterResponseEventDummy($request, $response);

        $object = new ResponseListener($this->newRelic, $this->interactor, true, false, $this->extension);
        $object->onKernelResponse($event);
    }

    public function testInteractionWithTwigExtensionHeaderFooter()
    {
        $this->newRelic->expects($this->never())->method('getCustomMetrics');
        $this->newRelic->expects($this->never())->method('getCustomParameters');
        $this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);

        $this->interactor->expects($this->never())->method('disableAutoRUM');
        $this->interactor->expects($this->never())->method('getBrowserTimingHeader');
        $this->interactor->expects($this->never())->method('getBrowserTimingFooter');

        $this->extension->expects($this->exactly(2))->method('isUsed')->willReturn(true);
        $this->extension->expects($this->once())->method('isHeaderCalled')->willReturn(true);
        $this->extension->expects($this->once())->method('isFooterCalled')->willReturn(true);

        $request = $this->createRequestMock(true);
        $response = $this->createResponseMock('content', 'content', 'text/html');
        $event = $this->createFilterResponseEventDummy($request, $response);

        $object = new ResponseListener($this->newRelic, $this->interactor, true, false, $this->extension);
        $object->onKernelResponse($event);
    }

    private function setUpNoCustomMetricsOrParameters()
    {
        $this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);
        $this->newRelic->expects($this->once())->method('getCustomMetrics')->willReturn([]);
        $this->newRelic->expects($this->once())->method('getCustomParameters')->willReturn([]);

        $this->interactor->expects($this->never())->method('addCustomEvent');
        $this->interactor->expects($this->never())->method('addCustomMetric');
        $this->interactor->expects($this->never())->method('addCustomParameter');
    }

    private function createRequestMock($instrumentEnabled = true)
    {
        $mock = $this->getMockBuilder(Request::class)
            ->setMethods(['get'])
            ->getMock();
        $mock->attributes = $mock;

        $mock->expects($this->any())->method('get')->willReturn($instrumentEnabled);

        return $mock;
    }

    private function createResponseMock($content = null, $expectsSetContent = null, $contentType = 'text/html')
    {
        $mock = $this->getMockBuilder(Response::class)
            ->setMethods(['get', 'getContent', 'setContent'])
            ->getMock();
        $mock->headers = $mock;

        $mock->expects($this->any())->method('get')->willReturn($contentType);
        $mock->expects($content ? $this->any() : $this->never())->method('getContent')->willReturn($content ?? false);

        if ($expectsSetContent) {
            $mock->expects($this->exactly(2))->method('setContent')->withConsecutive([''], [$expectsSetContent]);
        } else {
            $mock->expects($this->never())->method('setContent');
        }

        return $mock;
    }

    private function createFilterResponseEventDummy(Request $request = null, Response $response = null, int $requestType = HttpKernelInterface::MASTER_REQUEST)
    {
        $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();

        $eventClass = class_exists(ResponseEvent::class) ? ResponseEvent::class : FilterResponseEvent::class;
        $event = new $eventClass($kernel, $request ?? new Request(), $requestType, $response ?? new Response());

        return $event;
    }
}


================================================
FILE: Tests/NewRelic/ConfigTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\NewRelic;

use Ekino\NewRelicBundle\NewRelic\Config;
use PHPUnit\Framework\TestCase;

class ConfigTest extends TestCase
{
    public function testGeneric()
    {
        $newRelic = new Config('Ekino', 'XXX', null, false, [], 'api.host');

        $this->assertSame('Ekino', $newRelic->getName());
        $this->assertSame('XXX', $newRelic->getApiKey());
        $this->assertSame('api.host', $newRelic->getApiHost());

        $this->assertEmpty($newRelic->getCustomEvents());
        $this->assertEmpty($newRelic->getCustomMetrics());
        $this->assertEmpty($newRelic->getCustomParameters());

        $newRelic->addCustomEvent('WidgetSale', ['color' => 'red', 'weight' => 12.5]);
        $newRelic->addCustomEvent('WidgetSale', ['color' => 'blue', 'weight' => 12.5]);

        $expected = [
            'WidgetSale' => [
                [
                    'color' => 'red',
                    'weight' => 12.5,
                ],
                [
                    'color' => 'blue',
                    'weight' => 12.5,
                ],
            ],
        ];

        $this->assertSame($expected, $newRelic->getCustomEvents());

        $newRelic->addCustomMetric('foo', 4.2);
        $newRelic->addCustomMetric('asd', 1);

        $expected = [
            'foo' => 4.2,
            'asd' => 1.0,
        ];

        $this->assertSame($expected, $newRelic->getCustomMetrics());

        $newRelic->addCustomParameter('param1', 1);

        $expected = [
            'param1' => 1,
        ];

        $this->assertSame($expected, $newRelic->getCustomParameters());
    }

    public function testDefaults()
    {
        $newRelic = new Config('', '');

        $this->assertNotNull($newRelic->getName());
        $this->assertSame(ini_get('newrelic.appname') ?: '', $newRelic->getName());

        $this->assertNotNull($newRelic->getLicenseKey());
        $this->assertSame(ini_get('newrelic.license') ?: '', $newRelic->getLicenseKey());

        $this->assertNull($newRelic->getApiHost());
    }
}


================================================
FILE: Tests/NewRelic/LoggingInteractorDecoratorTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\NewRelic;

use Ekino\NewRelicBundle\NewRelic\LoggingInteractorDecorator;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class LoggingInteractorDecoratorTest extends TestCase
{
    /**
     * @dataProvider provideMethods
     */
    public function testGeneric(string $method, array $arguments, $return)
    {
        $logger = $this->createMock(LoggerInterface::class);
        $decorated = $this->createMock(LoggingInteractorDecorator::class);
        $interactor = new LoggingInteractorDecorator($decorated, $logger);

        $logger->expects($this->once())->method('debug');
        $call = $decorated->expects($this->once())->method($method)
            ->with(...$arguments);
        if (null !== $return) {
            $call->willReturn($return);
        }

        $result = $interactor->$method(...$arguments);

        $this->assertSame($return, $result);
    }

    public function provideMethods()
    {
        $reflection = new \ReflectionClass(NewRelicInteractorInterface::class);
        foreach ($reflection->getMethods() as $method) {
            if (!$method->isPublic()) {
                continue;
            }
            if ($method->isStatic()) {
                continue;
            }

            $arguments = array_map(function (\ReflectionParameter $parameter) {
                return $this->getTypeStub($parameter->getType());
            }, $method->getParameters());

            $return = $method->hasReturnType() ? $this->getTypeStub($method->getReturnType()) : null;

            yield [$method->getName(), $arguments, $return];
        }
    }

    private function getTypeStub(?\ReflectionType $type)
    {
        if (null === $type) {
            return uniqid('', true);
        }

        switch ($type->getName()) {
            case 'string':
                return uniqid('', true);
            case 'bool':
                return (bool) rand(0, 1);
            case 'float':
                return rand(0, 100) / rand(1, 10);
            case 'int':
                return rand(0, 100);
            case 'void':
                return null;
            case 'Throwable':
                return new \Exception();
            case 'callable':
                return function () {};
            case 'array':
                return array_fill(0, 2, uniqid('', true));
            default:
                throw new \UnexpectedValueException('Unknown type. '.$type->getName());
        }
    }
}


================================================
FILE: Tests/TransactionNamingStrategy/ControllerNamingStrategyTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\TransactionNamingStrategy;

use Ekino\NewRelicBundle\TransactionNamingStrategy\ControllerNamingStrategy;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;

class ControllerNamingStrategyTest extends TestCase
{
    public function testControllerAsString()
    {
        $request = new Request();
        $request->attributes->set('_controller', 'SomeBundle:Some:SomeAction');

        $strategy = new ControllerNamingStrategy();
        $this->assertSame('SomeBundle:Some:SomeAction', $strategy->getTransactionName($request));
    }

    public function testControllerAsClosure()
    {
        $request = new Request();
        $request->attributes->set('_controller', function () {
        });

        $strategy = new ControllerNamingStrategy();
        $this->assertSame('Closure controller', $strategy->getTransactionName($request));
    }

    public function testControllerAsCallback()
    {
        $request = new Request();
        $request->attributes->set('_controller', [$this, 'testControllerAsString']);

        $strategy = new ControllerNamingStrategy();
        $this->assertSame('Callback controller: Ekino\NewRelicBundle\Tests\TransactionNamingStrategy\ControllerNamingStrategyTest::testControllerAsString()', $strategy->getTransactionName($request));
    }

    public function testControllerUnknown()
    {
        $request = new Request();

        $strategy = new ControllerNamingStrategy();
        $this->assertSame('Unknown Symfony controller', $strategy->getTransactionName($request));
    }
}


================================================
FILE: Tests/Twig/NewRelicExtensionTest.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Tests\Twig;

use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use PHPUnit\Framework\TestCase;

class NewRelicExtensionTest extends TestCase
{
    /**
     * @var \Ekino\NewRelicBundle\NewRelic\Config
     */
    private $newRelic;

    /**
     * @var \Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface
     */
    private $interactor;

    protected function setUp(): void
    {
        $this->newRelic = $this->getMockBuilder(Config::class)
        ->setMethods(['getCustomMetrics', 'getCustomParameters'])
        ->disableOriginalConstructor()
            ->getMock();
        $this->interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
    }

    /**
     * Tests the initial values returned by state methods.
     */
    public function testInitialSetup()
    {
        $extension = new NewRelicExtension(
            $this->newRelic,
            $this->interactor
        );

        $this->assertFalse($extension->isHeaderCalled());
        $this->assertFalse($extension->isFooterCalled());
        $this->assertFalse($extension->isUsed());
    }

    public function testHeaderException()
    {
        $extension = new NewRelicExtension(
            $this->newRelic,
            $this->interactor
        );

        $this->newRelic->expects($this->once())
            ->method('getCustomMetrics')
            ->willReturn([]);

        $this->newRelic->expects($this->once())
            ->method('getCustomParameters')
            ->willReturn([]);

        $this->expectException(\RuntimeException::class);

        $extension->getNewrelicBrowserTimingHeader();
        $extension->getNewrelicBrowserTimingHeader();
    }

    public function testFooterException()
    {
        $extension = new NewRelicExtension(
            $this->newRelic,
            $this->interactor
        );

        $this->newRelic->expects($this->once())
            ->method('getCustomMetrics')
            ->willReturn([]);

        $this->newRelic->expects($this->once())
            ->method('getCustomParameters')
            ->willReturn([]);

        $this->expectException(\RuntimeException::class);

        $extension->getNewrelicBrowserTimingHeader();
        $extension->getNewrelicBrowserTimingHeader();
    }

    public function testPreparingOfInteractor()
    {
        $headerValue = '__HEADER__TIMING__';
        $footerValue = '__FOOTER__TIMING__';

        $extension = new NewRelicExtension(
            $this->newRelic,
            $this->interactor,
            true
        );

        $this->newRelic->expects($this->once())
            ->method('getCustomMetrics')
            ->willReturn([
                'a' => 'b',
                'c' => 'd',
            ]);

        $this->newRelic->expects($this->once())
            ->method('getCustomParameters')
            ->willReturn([
                'e' => 'f',
                'g' => 'h',
                'i' => 'j',
            ]);

        $this->interactor->expects($this->once())
            ->method('disableAutoRum');

        $this->interactor->expects($this->exactly(2))
            ->method('addCustomMetric');

        $this->interactor->expects($this->exactly(3))
            ->method('addCustomParameter');

        $this->interactor->expects($this->once())
            ->method('getBrowserTimingHeader')
            ->willReturn($headerValue);

        $this->interactor->expects($this->once())
            ->method('getBrowserTimingFooter')
            ->willReturn($footerValue);

        $this->assertSame($headerValue, $extension->getNewrelicBrowserTimingHeader());
        $this->assertTrue($extension->isHeaderCalled());
        $this->assertFalse($extension->isFooterCalled());

        $this->assertSame($footerValue, $extension->getNewrelicBrowserTimingFooter());
        $this->assertTrue($extension->isHeaderCalled());
        $this->assertTrue($extension->isFooterCalled());
    }
}


================================================
FILE: TransactionNamingStrategy/ControllerNamingStrategy.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\TransactionNamingStrategy;

use Symfony\Component\HttpFoundation\Request;

/**
 * @author Magnus Nordlander
 * @author Bart van den Burg <bart@burgov.nl>
 */
class ControllerNamingStrategy implements TransactionNamingStrategyInterface
{
    public function getTransactionName(Request $request): string
    {
        $controller = $request->attributes->get('_controller');
        if (empty($controller)) {
            return 'Unknown Symfony controller';
        }

        if ($controller instanceof \Closure) {
            return 'Closure controller';
        }

        if (\is_object($controller)) {
            if (method_exists($controller, '__invoke')) {
                return 'Callback controller: '.\get_class($controller).'::__invoke()';
            }
        }

        if (\is_callable($controller)) {
            if (\is_array($controller)) {
                if (\is_object($controller[0])) {
                    $controller[0] = \get_class($controller[0]);
                }

                $controller = implode('::', $controller);
            }

            return 'Callback controller: '.$controller.'()';
        }

        return $controller;
    }
}


================================================
FILE: TransactionNamingStrategy/RouteNamingStrategy.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\TransactionNamingStrategy;

use Symfony\Component\HttpFoundation\Request;

/**
 * @author Magnus Nordlander
 */
class RouteNamingStrategy implements TransactionNamingStrategyInterface
{
    public function getTransactionName(Request $request): string
    {
        return $request->get('_route') ?: 'Unknown Symfony route';
    }
}


================================================
FILE: TransactionNamingStrategy/TransactionNamingStrategyInterface.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\TransactionNamingStrategy;

use Symfony\Component\HttpFoundation\Request;

interface TransactionNamingStrategyInterface
{
    public function getTransactionName(Request $request): string;
}


================================================
FILE: Twig/NewRelicExtension.php
================================================
<?php

declare(strict_types=1);

/*
 * This file is part of Ekino New Relic bundle.
 *
 * (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ekino\NewRelicBundle\Twig;

use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

/**
 * Twig extension to manually include BrowserTimingHeader and BrowserTimingFooter into twig templates.
 */
class NewRelicExtension extends AbstractExtension
{
    private $newRelic;
    private $interactor;
    private $instrument;
    private $headerCalled = false;
    private $footerCalled = false;

    public function __construct(
        Config $newRelic,
        NewRelicInteractorInterface $interactor,
        bool $instrument = false
    ) {
        $this->newRelic = $newRelic;
        $this->interactor = $interactor;
        $this->instrument = $instrument;
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('ekino_newrelic_browser_timing_header', [$this, 'getNewrelicBrowserTimingHeader'], ['is_safe' => ['html']]),
            new TwigFunction('ekino_newrelic_browser_timing_footer', [$this, 'getNewrelicBrowserTimingFooter'], ['is_safe' => ['html']]),
        ];
    }

    /**
     * @throws \RuntimeException
     */
    public function getNewrelicBrowserTimingHeader(): string
    {
        if ($this->isHeaderCalled()) {
            throw new \RuntimeException('Function "ekino_newrelic_browser_timing_header" has already been called');
        }

        $this->prepareInteractor();

        $this->headerCalled = true;

        return $this->interactor->getBrowserTimingHeader();
    }

    /**
     * @throws \RuntimeException
     */
    public function getNewrelicBrowserTimingFooter(): string
    {
        if ($this->isFooterCalled()) {
            throw new \RuntimeException('Function "ekino_newrelic_browser_timing_footer" has already been called');
        }

        if (false === $this->isHeaderCalled()) {
            $this->prepareInteractor();
        }

        $this->footerCalled = true;

        return $this->interactor->getBrowserTimingFooter();
    }

    public function isHeaderCalled(): bool
    {
        return $this->headerCalled;
    }

    public function isFooterCalled(): bool
    {
        return $this->footerCalled;
    }

    public function isUsed(): bool
    {
        return $this->isHeaderCalled() || $this->isFooterCalled();
    }

    private function prepareInteractor(): void
    {
        if ($this->instrument) {
            $this->interactor->disableAutoRUM();
        }

        foreach ($this->newRelic->getCustomMetrics() as $name => $value) {
            $this->interactor->addCustomMetric((string) $name, (float) $value);
        }

        foreach ($this->newRelic->getCustomParameters() as $name => $value) {
            $this->interactor->addCustomParameter((string) $name, $value);
        }
    }
}


================================================
FILE: UPGRADE-2.0.md
================================================
UPGRADE FROM 1.x to 2.0
=======================

> Many internal things in the bundle has changed. If you only installed the
> bundle and added some configuration then you just have to check the 2 first
> bullets of this file.

 * The namespace of the bundle has changed to follow:

   Before:

   ```php
   $bundles[] = new Ekino\Bundle\NewRelicBundle\EkinoNewRelicBundle();
   ```

   After:

   ```php
   $bundles[] = new Ekino\NewRelicBundle\EkinoNewRelicBundle();
   ```

* The configuration structure has changed

  ```
  | EkinoNewRelicBundle 1.x      | EkinoNewRelicBundle 2.0
  | ---------------------------- | --------------------------------
  | ekino_new_relic:             | ekino_new_relic:
  |   enabled                    |   enabled
  |   application_name           |   application_name
  |   deployment_names           |   deployment_names
  |   api_key                    |   api_key
  |   license_key                |   license_key
  |   xmit                       |   xmit
  |   logging                    |   logging
  |   instrument                 |   instrument
  |   log_exceptions             |   exceptions
  |                              |   interactor
  |                              |   twig
  |                              |   deprecations
  |                              |   http
  |                              |     enabled
  |   using_symfony_cache        |     using_symfony_cache
  |   transaction_naming         |     transaction_naming
  |   transaction_naming_service |     transaction_naming_service
  |   ignored_routes             |     ignored_routes
  |   ignored_paths              |     ignored_paths
  |                              |   monolog
  |                              |     enabled
  |                              |     channels
  |                              |     level
  |                              |     service
  |                              |   commands
  |   log_commands               |     enabled
  |   ignored_commands           |     ignored_commands
  ```

* The Sonata integration has been removed.

* The Silex integration has been removed.

* Services are private by default. You should either use service injection
  or explicitly define your services as public if you really need to inject
  the container.

* The parameters `ekino.new_relic.interactor.real.class` and `ekino.new_relic.interactor.blackhole.class`
  have been removed. You should decorate the services instead.

* Name of services uses the class FQDN instead of string alias

  | EkinoNewRelicBundle 1.x                                  | EkinoNewRelicBundle 2.0
  | -------------------------------------------------------- | --------------------------------------------------------------------------
  | `ekino.new_relic.command_listener`                       | `Ekino\NewRelicBundle\Listener\CommandListener`
  | `ekino.new_relic.exception_listener`                     | `Ekino\NewRelicBundle\Listener\ExceptionListener`
  | `ekino.new_relic.interactor`                             | `Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface`
  | `ekino.new_relic.interactor.blackhole`                   | `Ekino\NewRelicBundle\NewRelic\BlackholeInteractor`
  | `ekino.new_relic.interactor.logger`                      | `Ekino\NewRelicBundle\NewRelic\LoggingInteractorDecorator`
  | `ekino.new_relic.interactor.real`                        | `Ekino\NewRelicBundle\NewRelic\NewRelicInteractor`
  | `ekino.new_relic.request_listener`                       | `Ekino\NewRelicBundle\Listener\RequestListener`
  | `ekino.new_relic.response_listener`                      | `Ekino\NewRelicBundle\Listener\ResponseListener`
  | `ekino.new_relic.transaction_naming_strategy.controller` | `Ekino\NewRelicBundle\TransactionNamingStrategy\ControllerNamingStrategy`
  | `ekino.new_relic.transaction_naming_strategy.route`      | `Ekino\NewRelicBundle\TransactionNamingStrategy\RouteNamingStrategy`
  | `ekino.new_relic.twig.new_relic_extension`               | `Ekino\NewRelicBundle\Twig\NewRelicExtension`
  | `ekino.new_relic`                                        | `Ekino\NewRelicBundle\NewRelic\Config`


* The `NewRelicInteractorInterface` changed.

  * added scalar type hinting on method declaration
  * added method `addCustomEvent`
  * added method `addCustomTracer`
  * added method `excludeFromApdex`
  * added method `recordDatastoreSegment`
  * added method `setCaptureParams`
  * added method `setUserAttributes`
  * added method `stopTransactionTiming`
  * rename method `noticeException` to `noticeThrowable`
  * added parameter `$ignore` to the method `endTransaction`
  * added parameters `$errno`, `$errstr`, `$errfile`, `$errline`, `$errcontext` to the method `noticeError`
  * added parameter `$license` to the method `startTransaction`

* The `TransactionNamingStrategyInterface` changed.

  * Added scalar type hinting on method declaration

* Allmost all classes changed to add scalar type hinting and protected methods have been removed, you should use composition over inheritance.

* The class `NewRelic\NewRelic` has been renamed to `NewRelic\Config`


================================================
FILE: composer.json
================================================
{
    "name": "ekino/newrelic-bundle",
    "type": "symfony-bundle",
    "description": "Integrate New Relic into Symfony2",
    "keywords": ["metric", "performance", "monitoring"],
    "homepage": "https://github.com/ekino/EkinoNewRelicBundle",
    "license": "MIT",
    "authors": [
        {
            "name": "Thomas Rabaix",
            "email": "thomas.rabaix@ekino.com",
            "homepage": "http://ekino.com"
        }
    ],
    "require": {
        "php": "^7.1 | ^8.0",
        "symfony/config": "^3.4|^4.0|^5.0|^6.0",
        "symfony/console": "^3.4|^4.0|^5.0|^6.0",
        "symfony/dependency-injection": "^3.4|^4.0|^5.0|^6.0",
        "symfony/event-dispatcher": "^3.4|^4.0|^5.0|^6.0",
        "symfony/http-kernel": "^3.4|^4.0|^5.0|^6.0"
    },
    "require-dev": {
        "matthiasnoback/symfony-dependency-injection-test": "^3.1|^4.0",
        "symfony/framework-bundle": "^3.4|^4.0|^5.0|^6.0",
        "symfony/phpunit-bridge": "^5.3",
        "symfony/debug": ">3.4.21",
        "twig/twig": "^1.32|^2.4",
        "symfony/monolog-bundle": "^3.2"
    },
    "conflict": {
        "twig/twig": "<1.32"
    },
    "suggest": {
        "symfony/monolog-bundle": "^3.2"
    },
    "autoload": {
        "psr-4": { "Ekino\\NewRelicBundle\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "autoload-dev": {
        "psr-4": { "Ekino\\NewRelicBundle\\Tests\\": "Tests/" }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "2.0-dev"
        }
    }
}


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>

<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         bootstrap="vendor/autoload.php"
>
    <testsuites>
        <testsuite name="Ekino New Relic Test Suite">
            <directory>./Tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory>./</directory>
            <exclude>
                <directory>./Tests/</directory>
                <directory>./Resources/</directory>
                <directory>./vendor/</directory>
            </exclude>
        </whitelist>
    </filter>

</phpunit>
Download .txt
gitextract_cqkhw3dv/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── Command/
│   └── NotifyDeploymentCommand.php
├── DependencyInjection/
│   ├── Compiler/
│   │   └── MonologHandlerPass.php
│   ├── Configuration.php
│   └── EkinoNewRelicExtension.php
├── EkinoNewRelicBundle.php
├── Exception/
│   └── DeprecationException.php
├── LICENSE
├── Listener/
│   ├── CommandListener.php
│   ├── DeprecationListener.php
│   ├── ExceptionListener.php
│   ├── RequestListener.php
│   └── ResponseListener.php
├── Logging/
│   └── AdaptiveHandler.php
├── NewRelic/
│   ├── AdaptiveInteractor.php
│   ├── BlackholeInteractor.php
│   ├── Config.php
│   ├── LoggingInteractorDecorator.php
│   ├── NewRelicInteractor.php
│   └── NewRelicInteractorInterface.php
├── README.md
├── Resources/
│   ├── config/
│   │   ├── command_listener.xml
│   │   ├── deprecation_listener.xml
│   │   ├── exception_listener.xml
│   │   ├── http_listener.xml
│   │   ├── monolog.xml
│   │   ├── services.xml
│   │   └── twig.xml
│   └── recipes/
│       └── newrelic.rb
├── Tests/
│   ├── AppKernel.php
│   ├── BundleInitializationTest.php
│   ├── DependencyInjection/
│   │   ├── Compiler/
│   │   │   └── MonologHandlerPassTest.php
│   │   ├── ConfigurationTest.php
│   │   └── EkinoNewRelicExtensionTest.php
│   ├── Listener/
│   │   ├── CommandListenerTest.php
│   │   ├── DeprecationListenerTest.php
│   │   ├── ExceptionListenerTest.php
│   │   ├── RequestListenerTest.php
│   │   └── ResponseListenerTest.php
│   ├── NewRelic/
│   │   ├── ConfigTest.php
│   │   └── LoggingInteractorDecoratorTest.php
│   ├── TransactionNamingStrategy/
│   │   └── ControllerNamingStrategyTest.php
│   └── Twig/
│       └── NewRelicExtensionTest.php
├── TransactionNamingStrategy/
│   ├── ControllerNamingStrategy.php
│   ├── RouteNamingStrategy.php
│   └── TransactionNamingStrategyInterface.php
├── Twig/
│   └── NewRelicExtension.php
├── UPGRADE-2.0.md
├── composer.json
└── phpunit.xml.dist
Download .txt
SYMBOL INDEX (312 symbols across 36 files)

FILE: Command/NotifyDeploymentCommand.php
  class NotifyDeploymentCommand (line 22) | class NotifyDeploymentCommand extends Command
    method __construct (line 32) | public function __construct(Config $newrelic)
    method configure (line 39) | protected function configure(): void
    method execute (line 64) | protected function execute(InputInterface $input, OutputInterface $out...
    method performRequest (line 102) | public function performRequest(string $api_key, string $payload, ?stri...
    method createPayload (line 150) | private function createPayload(string $appName, InputInterface $input)...

FILE: DependencyInjection/Compiler/MonologHandlerPass.php
  class MonologHandlerPass (line 21) | class MonologHandlerPass implements CompilerPassInterface
    method process (line 23) | public function process(ContainerBuilder $container): void
    method getChannels (line 56) | private function getChannels(ContainerBuilder $container)

FILE: DependencyInjection/Configuration.php
  class Configuration (line 22) | class Configuration implements ConfigurationInterface
    method getConfigTreeBuilder (line 24) | public function getConfigTreeBuilder(): TreeBuilder

FILE: DependencyInjection/EkinoNewRelicExtension.php
  class EkinoNewRelicExtension (line 40) | class EkinoNewRelicExtension extends Extension
    method load (line 42) | public function load(array $configs, ContainerBuilder $container): void
    method getInteractorServiceId (line 135) | private function getInteractorServiceId(array $config): string
    method getTransactionNamingServiceId (line 154) | private function getTransactionNamingServiceId(array $config): string

FILE: EkinoNewRelicBundle.php
  class EkinoNewRelicBundle (line 21) | class EkinoNewRelicBundle extends Bundle
    method build (line 23) | public function build(ContainerBuilder $container)
    method boot (line 30) | public function boot()
    method shutdown (line 39) | public function shutdown()

FILE: Exception/DeprecationException.php
  class DeprecationException (line 19) | class DeprecationException extends \ErrorException

FILE: Listener/CommandListener.php
  class CommandListener (line 23) | class CommandListener implements EventSubscriberInterface
    method __construct (line 29) | public function __construct(Config $config, NewRelicInteractorInterfac...
    method getSubscribedEvents (line 36) | public static function getSubscribedEvents(): array
    method onConsoleCommand (line 44) | public function onConsoleCommand(ConsoleCommandEvent $event): void
    method onConsoleError (line 85) | public function onConsoleError(ConsoleErrorEvent $event): void

FILE: Listener/DeprecationListener.php
  class DeprecationListener (line 19) | class DeprecationListener
    method __construct (line 24) | public function __construct(NewRelicInteractorInterface $interactor)
    method register (line 29) | public function register(): void
    method unregister (line 45) | public function unregister(): void

FILE: Listener/ExceptionListener.php
  class ExceptionListener (line 26) | class ExceptionListener implements EventSubscriberInterface
    method __construct (line 30) | public function __construct(NewRelicInteractorInterface $interactor)
    method getSubscribedEvents (line 35) | public static function getSubscribedEvents(): array
    method onKernelException (line 45) | public function onKernelException(KernelExceptionEvent $event): void

FILE: Listener/RequestListener.php
  class RequestListener (line 25) | class RequestListener implements EventSubscriberInterface
    method __construct (line 34) | public function __construct(
    method getSubscribedEvents (line 50) | public static function getSubscribedEvents(): array
    method setApplicationName (line 61) | public function setApplicationName(KernelRequestEvent $event): void
    method setTransactionName (line 83) | public function setTransactionName(KernelRequestEvent $event): void
    method setIgnoreTransaction (line 94) | public function setIgnoreTransaction(KernelRequestEvent $event): void
    method isEventValid (line 113) | private function isEventValid(KernelRequestEvent $event): bool

FILE: Listener/ResponseListener.php
  class ResponseListener (line 25) | class ResponseListener implements EventSubscriberInterface
    method __construct (line 33) | public function __construct(
    method getSubscribedEvents (line 47) | public static function getSubscribedEvents(): array
    method onKernelResponse (line 56) | public function onKernelResponse(KernelResponseEvent $event): void

FILE: Logging/AdaptiveHandler.php
  class AdaptiveHandler (line 19) | class AdaptiveHandler extends NewRelicHandler
    method __construct (line 21) | public function __construct(
    method write (line 31) | protected function write(array $record): void

FILE: NewRelic/AdaptiveInteractor.php
  class AdaptiveInteractor (line 24) | class AdaptiveInteractor implements NewRelicInteractorInterface
    method __construct (line 28) | public function __construct(NewRelicInteractorInterface $real, NewReli...
    method setApplicationName (line 33) | public function setApplicationName(string $name, string $license = nul...
    method setTransactionName (line 38) | public function setTransactionName(string $name): bool
    method ignoreTransaction (line 43) | public function ignoreTransaction(): void
    method addCustomEvent (line 48) | public function addCustomEvent(string $name, array $attributes): void
    method addCustomMetric (line 53) | public function addCustomMetric(string $name, float $value): bool
    method addCustomParameter (line 58) | public function addCustomParameter(string $name, $value): bool
    method getBrowserTimingHeader (line 63) | public function getBrowserTimingHeader(bool $includeTags = true): string
    method getBrowserTimingFooter (line 68) | public function getBrowserTimingFooter(bool $includeTags = true): string
    method disableAutoRUM (line 73) | public function disableAutoRUM(): ?bool
    method noticeThrowable (line 78) | public function noticeThrowable(\Throwable $e, string $message = null)...
    method noticeError (line 83) | public function noticeError(
    method enableBackgroundJob (line 93) | public function enableBackgroundJob(): void
    method disableBackgroundJob (line 98) | public function disableBackgroundJob(): void
    method startTransaction (line 103) | public function startTransaction(string $name = null, string $license ...
    method endTransaction (line 108) | public function endTransaction(bool $ignore = false): bool
    method excludeFromApdex (line 113) | public function excludeFromApdex(): void
    method addCustomTracer (line 118) | public function addCustomTracer(string $name): bool
    method setCaptureParams (line 123) | public function setCaptureParams(bool $enabled): void
    method stopTransactionTiming (line 128) | public function stopTransactionTiming(): void
    method recordDatastoreSegment (line 133) | public function recordDatastoreSegment(callable $func, array $parameters)
    method setUserAttributes (line 138) | public function setUserAttributes(string $userValue, string $accountVa...
    method getTraceMetadata (line 143) | public function getTraceMetadata(): array
    method getLinkingMetadata (line 152) | public function getLinkingMetadata(): array
    method isSampled (line 161) | public function isSampled(): bool
    method insertDistributedTracingHeaders (line 170) | public function insertDistributedTracingHeaders(array &$headers): void
    method acceptDistributedTraceHeaders (line 179) | public function acceptDistributedTraceHeaders(array $headers, string $...

FILE: NewRelic/BlackholeInteractor.php
  class BlackholeInteractor (line 21) | class BlackholeInteractor implements NewRelicInteractorInterface
    method setApplicationName (line 23) | public function setApplicationName(string $name, string $license = nul...
    method setTransactionName (line 28) | public function setTransactionName(string $name): bool
    method ignoreTransaction (line 33) | public function ignoreTransaction(): void
    method addCustomEvent (line 37) | public function addCustomEvent(string $name, array $attributes): void
    method addCustomMetric (line 41) | public function addCustomMetric(string $name, float $value): bool
    method addCustomParameter (line 46) | public function addCustomParameter(string $name, $value): bool
    method getBrowserTimingHeader (line 51) | public function getBrowserTimingHeader(bool $includeTags = true): string
    method getBrowserTimingFooter (line 56) | public function getBrowserTimingFooter(bool $includeTags = true): string
    method disableAutoRUM (line 61) | public function disableAutoRUM(): ?bool
    method noticeThrowable (line 66) | public function noticeThrowable(\Throwable $e, string $message = null)...
    method noticeError (line 70) | public function noticeError(
    method enableBackgroundJob (line 79) | public function enableBackgroundJob(): void
    method disableBackgroundJob (line 83) | public function disableBackgroundJob(): void
    method startTransaction (line 87) | public function startTransaction(string $name = null, string $license ...
    method endTransaction (line 92) | public function endTransaction(bool $ignore = false): bool
    method excludeFromApdex (line 97) | public function excludeFromApdex(): void
    method addCustomTracer (line 101) | public function addCustomTracer(string $name): bool
    method setCaptureParams (line 106) | public function setCaptureParams(bool $enabled): void
    method stopTransactionTiming (line 110) | public function stopTransactionTiming(): void
    method recordDatastoreSegment (line 114) | public function recordDatastoreSegment(callable $func, array $parameters)
    method setUserAttributes (line 119) | public function setUserAttributes(string $userValue, string $accountVa...
    method getTraceMetadata (line 124) | public function getTraceMetadata(): array
    method getLinkingMetadata (line 129) | public function getLinkingMetadata(): array
    method isSampled (line 134) | public function isSampled(): bool
    method insertDistributedTracingHeaders (line 139) | public function insertDistributedTracingHeaders(array &$headers): void
    method acceptDistributedTraceHeaders (line 143) | public function acceptDistributedTraceHeaders(array $headers, string $...

FILE: NewRelic/Config.php
  class Config (line 19) | class Config
    method __construct (line 31) | public function __construct(?string $name, string $apiKey = null, stri...
    method setCustomEvents (line 44) | public function setCustomEvents(array $customEvents): void
    method getCustomEvents (line 49) | public function getCustomEvents(): array
    method addCustomEvent (line 54) | public function addCustomEvent(string $name, array $attributes): void
    method setCustomMetrics (line 59) | public function setCustomMetrics(array $customMetrics): void
    method getCustomMetrics (line 64) | public function getCustomMetrics(): array
    method setCustomParameters (line 69) | public function setCustomParameters(array $customParameters): void
    method addCustomParameter (line 77) | public function addCustomParameter(string $name, $value): void
    method addCustomMetric (line 82) | public function addCustomMetric(string $name, float $value): void
    method getCustomParameters (line 87) | public function getCustomParameters(): array
    method getName (line 92) | public function getName(): string
    method getDeploymentNames (line 100) | public function getDeploymentNames(): array
    method getApiKey (line 105) | public function getApiKey(): ?string
    method getApiHost (line 110) | public function getApiHost(): ?string
    method getLicenseKey (line 115) | public function getLicenseKey(): ?string
    method getXmit (line 120) | public function getXmit(): bool

FILE: NewRelic/LoggingInteractorDecorator.php
  class LoggingInteractorDecorator (line 19) | class LoggingInteractorDecorator implements NewRelicInteractorInterface
    method __construct (line 24) | public function __construct(NewRelicInteractorInterface $interactor, L...
    method setApplicationName (line 30) | public function setApplicationName(string $name, string $license = nul...
    method setTransactionName (line 37) | public function setTransactionName(string $name): bool
    method ignoreTransaction (line 44) | public function ignoreTransaction(): void
    method addCustomEvent (line 50) | public function addCustomEvent(string $name, array $attributes): void
    method addCustomMetric (line 56) | public function addCustomMetric(string $name, float $value): bool
    method addCustomParameter (line 63) | public function addCustomParameter(string $name, $value): bool
    method getBrowserTimingHeader (line 70) | public function getBrowserTimingHeader(bool $includeTags = true): string
    method getBrowserTimingFooter (line 77) | public function getBrowserTimingFooter(bool $includeTags = true): string
    method disableAutoRUM (line 84) | public function disableAutoRUM(): ?bool
    method noticeError (line 91) | public function noticeError(
    method noticeThrowable (line 108) | public function noticeThrowable(\Throwable $e, string $message = null)...
    method enableBackgroundJob (line 117) | public function enableBackgroundJob(): void
    method disableBackgroundJob (line 123) | public function disableBackgroundJob(): void
    method endTransaction (line 129) | public function endTransaction(bool $ignore = false): bool
    method startTransaction (line 136) | public function startTransaction(string $name = null, string $license ...
    method excludeFromApdex (line 143) | public function excludeFromApdex(): void
    method addCustomTracer (line 149) | public function addCustomTracer(string $name): bool
    method setCaptureParams (line 156) | public function setCaptureParams(bool $enabled): void
    method stopTransactionTiming (line 162) | public function stopTransactionTiming(): void
    method recordDatastoreSegment (line 168) | public function recordDatastoreSegment(callable $func, array $parameters)
    method setUserAttributes (line 177) | public function setUserAttributes(string $userValue, string $accountVa...
    method getTraceMetadata (line 188) | public function getTraceMetadata(): array
    method getLinkingMetadata (line 197) | public function getLinkingMetadata(): array
    method isSampled (line 206) | public function isSampled(): bool
    method insertDistributedTracingHeaders (line 215) | public function insertDistributedTracingHeaders(array &$headers): void
    method acceptDistributedTraceHeaders (line 222) | public function acceptDistributedTraceHeaders(array $headers, string $...

FILE: NewRelic/NewRelicInteractor.php
  class NewRelicInteractor (line 16) | class NewRelicInteractor implements NewRelicInteractorInterface
    method setApplicationName (line 18) | public function setApplicationName(string $name, string $license = nul...
    method setTransactionName (line 23) | public function setTransactionName(string $name): bool
    method ignoreTransaction (line 28) | public function ignoreTransaction(): void
    method addCustomEvent (line 33) | public function addCustomEvent(string $name, array $attributes): void
    method addCustomMetric (line 38) | public function addCustomMetric(string $name, float $value): bool
    method addCustomParameter (line 43) | public function addCustomParameter(string $name, $value): bool
    method getBrowserTimingHeader (line 48) | public function getBrowserTimingHeader(bool $includeTags = true): string
    method getBrowserTimingFooter (line 53) | public function getBrowserTimingFooter(bool $includeTags = true): string
    method disableAutoRUM (line 58) | public function disableAutoRUM(): ?bool
    method noticeError (line 63) | public function noticeError(int $errno, string $errstr, string $errfil...
    method noticeThrowable (line 68) | public function noticeThrowable(\Throwable $e, string $message = null)...
    method enableBackgroundJob (line 73) | public function enableBackgroundJob(): void
    method disableBackgroundJob (line 78) | public function disableBackgroundJob(): void
    method endTransaction (line 83) | public function endTransaction(bool $ignore = false): bool
    method startTransaction (line 88) | public function startTransaction(string $name = null, string $license ...
    method excludeFromApdex (line 101) | public function excludeFromApdex(): void
    method addCustomTracer (line 106) | public function addCustomTracer(string $name): bool
    method setCaptureParams (line 111) | public function setCaptureParams(bool $enabled): void
    method stopTransactionTiming (line 116) | public function stopTransactionTiming(): void
    method recordDatastoreSegment (line 121) | public function recordDatastoreSegment(callable $func, array $parameters)
    method setUserAttributes (line 126) | public function setUserAttributes(string $userValue, string $accountVa...
    method getTraceMetadata (line 131) | public function getTraceMetadata(): array
    method getLinkingMetadata (line 140) | public function getLinkingMetadata(): array
    method isSampled (line 149) | public function isSampled(): bool
    method insertDistributedTracingHeaders (line 158) | public function insertDistributedTracingHeaders(array &$headers): void
    method acceptDistributedTraceHeaders (line 167) | public function acceptDistributedTraceHeaders(array $headers, string $...

FILE: NewRelic/NewRelicInteractorInterface.php
  type NewRelicInteractorInterface (line 25) | interface NewRelicInteractorInterface
    method setApplicationName (line 32) | public function setApplicationName(string $name, string $license = nul...
    method setTransactionName (line 39) | public function setTransactionName(string $name): bool;
    method ignoreTransaction (line 46) | public function ignoreTransaction(): void;
    method addCustomEvent (line 53) | public function addCustomEvent(string $name, array $attributes): void;
    method addCustomMetric (line 60) | public function addCustomMetric(string $name, float $value): bool;
    method addCustomParameter (line 67) | public function addCustomParameter(string $name, $value): bool;
    method getBrowserTimingHeader (line 74) | public function getBrowserTimingHeader(bool $includeTags = true): string;
    method getBrowserTimingFooter (line 81) | public function getBrowserTimingFooter(bool $includeTags = true): string;
    method disableAutoRUM (line 88) | public function disableAutoRUM(): ?bool;
    method noticeThrowable (line 96) | public function noticeThrowable(\Throwable $e, string $message = null)...
    method noticeError (line 101) | public function noticeError(int $errno, string $errstr, string $errfil...
    method enableBackgroundJob (line 106) | public function enableBackgroundJob(): void;
    method disableBackgroundJob (line 111) | public function disableBackgroundJob(): void;
    method startTransaction (line 118) | public function startTransaction(string $name = null, string $license ...
    method endTransaction (line 125) | public function endTransaction(bool $ignore = false): bool;
    method excludeFromApdex (line 132) | public function excludeFromApdex(): void;
    method addCustomTracer (line 140) | public function addCustomTracer(string $name): bool;
    method setCaptureParams (line 147) | public function setCaptureParams(bool $enabled): void;
    method stopTransactionTiming (line 154) | public function stopTransactionTiming(): void;
    method recordDatastoreSegment (line 163) | public function recordDatastoreSegment(callable $func, array $paramete...
    method setUserAttributes (line 170) | public function setUserAttributes(string $userValue, string $accountVa...

FILE: Tests/AppKernel.php
  class AppKernel (line 24) | class AppKernel extends Kernel
    method __construct (line 39) | public function __construct($cachePrefix)
    method getCacheDir (line 45) | public function getCacheDir(): string
    method getLogDir (line 50) | public function getLogDir(): string
    method getProjectDir (line 55) | public function getProjectDir(): string
    method setRootDir (line 67) | public function setRootDir($rootDir)
    method setProjectDir (line 75) | public function setProjectDir($projectDir)
    method registerBundles (line 80) | public function registerBundles(): iterable
    method registerContainerConfiguration (line 92) | public function registerContainerConfiguration(LoaderInterface $loader)
    method loadRoutes (line 130) | public function loadRoutes(LoaderInterface $loader)
    method buildContainer (line 138) | protected function buildContainer(): ContainerBuilder

FILE: Tests/BundleInitializationTest.php
  class BundleInitializationTest (line 28) | class BundleInitializationTest extends TestCase
    method getBundleClass (line 30) | protected function getBundleClass()
    method testInitBundle (line 35) | public function testInitBundle()

FILE: Tests/DependencyInjection/Compiler/MonologHandlerPassTest.php
  class MonologHandlerPassTest (line 21) | class MonologHandlerPassTest extends AbstractCompilerPassTestCase
    method registerCompilerPass (line 23) | protected function registerCompilerPass(ContainerBuilder $container): ...
    method testProcessChannel (line 28) | public function testProcessChannel()
    method testProcessChannelAllChannels (line 43) | public function testProcessChannelAllChannels()
    method testProcessChannelExcludeChannels (line 58) | public function testProcessChannelExcludeChannels()

FILE: Tests/DependencyInjection/ConfigurationTest.php
  class ConfigurationTest (line 21) | class ConfigurationTest extends TestCase
    method testIgnoredRoutes (line 23) | public function testIgnoredRoutes()
    method testIgnoredPaths (line 42) | public function testIgnoredPaths()
    method testIgnoredCommands (line 61) | public function testIgnoredCommands()
    method testDefaults (line 80) | public function testDefaults()
    method ignoredRoutesProvider (line 96) | public static function ignoredRoutesProvider()
    method ignoredPathsProvider (line 105) | public static function ignoredPathsProvider()
    method ignoredCommandsProvider (line 114) | public static function ignoredCommandsProvider()
    method deploymentNamesProvider (line 123) | public static function deploymentNamesProvider()
    method testDeploymentNames (line 135) | public function testDeploymentNames($deploymentNameConfig, $expected)
    method testIgnoreRoutes (line 149) | public function testIgnoreRoutes($ignoredRoutesConfig, $expected)
    method testIgnorePaths (line 161) | public function testIgnorePaths($ignoredPathsConfig, $expected)
    method testIgnoreCommands (line 173) | public function testIgnoreCommands($ignoredCommandsConfig, $expected)

FILE: Tests/DependencyInjection/EkinoNewRelicExtensionTest.php
  class EkinoNewRelicExtensionTest (line 27) | class EkinoNewRelicExtensionTest extends AbstractExtensionTestCase
    method getContainerExtensions (line 29) | protected function getContainerExtensions(): array
    method setUp (line 34) | protected function setUp(): void
    method testDefaultConfiguration (line 41) | public function testDefaultConfiguration()
    method testAlternativeConfiguration (line 50) | public function testAlternativeConfiguration()
    method testDeprecation (line 63) | public function testDeprecation()
    method testMonolog (line 70) | public function testMonolog()
    method testMonologDisabled (line 79) | public function testMonologDisabled()
    method testConfigDisabled (line 89) | public function testConfigDisabled()
    method testConfigDisabledWithInteractor (line 98) | public function testConfigDisabledWithInteractor()
    method testConfigEnabledWithInteractor (line 108) | public function testConfigEnabledWithInteractor()

FILE: Tests/Listener/CommandListenerTest.php
  class CommandListenerTest (line 29) | class CommandListenerTest extends TestCase
    method testCommandMarkedAsBackgroundJob (line 31) | public function testCommandMarkedAsBackgroundJob()
    method testIgnoreBackgroundJob (line 71) | public function testIgnoreBackgroundJob()
    method testConsoleError (line 87) | public function testConsoleError()
    method testConsoleErrorsWithThrowable (line 106) | public function testConsoleErrorsWithThrowable()

FILE: Tests/Listener/DeprecationListenerTest.php
  class DeprecationListenerTest (line 21) | class DeprecationListenerTest extends TestCase
    method testDeprecationIsReported (line 23) | public function testDeprecationIsReported()
    method testDeprecationIsReportedRegardlessErrorReporting (line 42) | public function testDeprecationIsReportedRegardlessErrorReporting()
    method testOtherErrorAreIgnored (line 61) | public function testOtherErrorAreIgnored()
    method testInitialHandlerIsCalled (line 78) | public function testInitialHandlerIsCalled()
    method testUnregisterRemovesHandler (line 98) | public function testUnregisterRemovesHandler()
    method testUnregisterRestorePreviousHandler (line 115) | public function testUnregisterRestorePreviousHandler()
  class DummyHandler (line 135) | class DummyHandler
    method __invoke (line 137) | public function __invoke()

FILE: Tests/Listener/ExceptionListenerTest.php
  class ExceptionListenerTest (line 25) | class ExceptionListenerTest extends TestCase
    method testOnKernelException (line 27) | public function testOnKernelException()
    method testOnKernelExceptionWithHttp (line 44) | public function testOnKernelExceptionWithHttp()

FILE: Tests/Listener/RequestListenerTest.php
  class RequestListenerTest (line 27) | class RequestListenerTest extends TestCase
    method testSubRequest (line 29) | public function testSubRequest()
    method testMasterRequest (line 45) | public function testMasterRequest()
    method testPathIsIgnored (line 64) | public function testPathIsIgnored()
    method testRouteIsIgnored (line 81) | public function testRouteIsIgnored()
    method testSymfonyCacheEnabled (line 98) | public function testSymfonyCacheEnabled()
    method testSymfonyCacheDisabled (line 114) | public function testSymfonyCacheDisabled()

FILE: Tests/Listener/ResponseListenerTest.php
  class ResponseListenerTest (line 27) | class ResponseListenerTest extends TestCase
    method setUp (line 29) | protected function setUp(): void
    method testOnKernelResponseOnlyMasterRequestsAreProcessed (line 42) | public function testOnKernelResponseOnlyMasterRequestsAreProcessed()
    method testOnKernelResponseWithOnlyCustomMetricsAndParameters (line 52) | public function testOnKernelResponseWithOnlyCustomMetricsAndParameters()
    method testOnKernelResponseInstrumentDisabledInRequest (line 106) | public function testOnKernelResponseInstrumentDisabledInRequest()
    method testSymfonyCacheEnabled (line 118) | public function testSymfonyCacheEnabled()
    method testSymfonyCacheDisabled (line 130) | public function testSymfonyCacheDisabled()
    method testOnKernelResponseOnlyInstrumentHTMLResponses (line 145) | public function testOnKernelResponseOnlyInstrumentHTMLResponses($conte...
    method providerOnKernelResponseOnlyInstrumentHTMLResponses (line 160) | public function providerOnKernelResponseOnlyInstrumentHTMLResponses()
    method testInteractionWithTwigExtensionHeader (line 182) | public function testInteractionWithTwigExtensionHeader()
    method testInteractionWithTwigExtensionFooter (line 204) | public function testInteractionWithTwigExtensionFooter()
    method testInteractionWithTwigExtensionHeaderFooter (line 226) | public function testInteractionWithTwigExtensionHeaderFooter()
    method setUpNoCustomMetricsOrParameters (line 248) | private function setUpNoCustomMetricsOrParameters()
    method createRequestMock (line 259) | private function createRequestMock($instrumentEnabled = true)
    method createResponseMock (line 271) | private function createResponseMock($content = null, $expectsSetConten...
    method createFilterResponseEventDummy (line 290) | private function createFilterResponseEventDummy(Request $request = nul...

FILE: Tests/NewRelic/ConfigTest.php
  class ConfigTest (line 19) | class ConfigTest extends TestCase
    method testGeneric (line 21) | public function testGeneric()
    method testDefaults (line 70) | public function testDefaults()

FILE: Tests/NewRelic/LoggingInteractorDecoratorTest.php
  class LoggingInteractorDecoratorTest (line 21) | class LoggingInteractorDecoratorTest extends TestCase
    method testGeneric (line 26) | public function testGeneric(string $method, array $arguments, $return)
    method provideMethods (line 44) | public function provideMethods()
    method getTypeStub (line 65) | private function getTypeStub(?\ReflectionType $type)

FILE: Tests/TransactionNamingStrategy/ControllerNamingStrategyTest.php
  class ControllerNamingStrategyTest (line 20) | class ControllerNamingStrategyTest extends TestCase
    method testControllerAsString (line 22) | public function testControllerAsString()
    method testControllerAsClosure (line 31) | public function testControllerAsClosure()
    method testControllerAsCallback (line 41) | public function testControllerAsCallback()
    method testControllerUnknown (line 50) | public function testControllerUnknown()

FILE: Tests/Twig/NewRelicExtensionTest.php
  class NewRelicExtensionTest (line 21) | class NewRelicExtensionTest extends TestCase
    method setUp (line 33) | protected function setUp(): void
    method testInitialSetup (line 45) | public function testInitialSetup()
    method testHeaderException (line 57) | public function testHeaderException()
    method testFooterException (line 78) | public function testFooterException()
    method testPreparingOfInteractor (line 99) | public function testPreparingOfInteractor()

FILE: TransactionNamingStrategy/ControllerNamingStrategy.php
  class ControllerNamingStrategy (line 22) | class ControllerNamingStrategy implements TransactionNamingStrategyInter...
    method getTransactionName (line 24) | public function getTransactionName(Request $request): string

FILE: TransactionNamingStrategy/RouteNamingStrategy.php
  class RouteNamingStrategy (line 21) | class RouteNamingStrategy implements TransactionNamingStrategyInterface
    method getTransactionName (line 23) | public function getTransactionName(Request $request): string

FILE: TransactionNamingStrategy/TransactionNamingStrategyInterface.php
  type TransactionNamingStrategyInterface (line 18) | interface TransactionNamingStrategyInterface
    method getTransactionName (line 20) | public function getTransactionName(Request $request): string;

FILE: Twig/NewRelicExtension.php
  class NewRelicExtension (line 24) | class NewRelicExtension extends AbstractExtension
    method __construct (line 32) | public function __construct(
    method getFunctions (line 42) | public function getFunctions(): array
    method getNewrelicBrowserTimingHeader (line 53) | public function getNewrelicBrowserTimingHeader(): string
    method getNewrelicBrowserTimingFooter (line 69) | public function getNewrelicBrowserTimingFooter(): string
    method isHeaderCalled (line 84) | public function isHeaderCalled(): bool
    method isFooterCalled (line 89) | public function isFooterCalled(): bool
    method isUsed (line 94) | public function isUsed(): bool
    method prepareInteractor (line 99) | private function prepareInteractor(): void
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (183K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".gitattributes",
    "chars": 288,
    "preview": ".editorconfig     export-ignore\n.gitattributes    export-ignore\n.gitignore        export-ignore\n/.php_cs          export"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2223,
    "preview": "name: CI\n\non:\n  pull_request: ~\n  push:\n    branches:\n      - master\n\njobs:\n  php-cs-fixer:\n    name: PHP-CS-Fixer\n    r"
  },
  {
    "path": ".gitignore",
    "chars": 62,
    "preview": ".php_cs.cache\nbuild\nphpunit.xml\ncoverage\ncomposer.lock\nvendor\n"
  },
  {
    "path": ".php-cs-fixer.php",
    "chars": 1285,
    "preview": "<?php\n\n$header = <<<'EOF'\nThis file is part of Ekino New Relic bundle.\n\n(c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.c"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3007,
    "preview": "# Changelog\n\n## v2.4.0\n\n### Added\n\n- Support for distributed tracing functions provided by the NewRelic PHP extension ar"
  },
  {
    "path": "Command/NotifyDeploymentCommand.php",
    "chars": 5761,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "DependencyInjection/Compiler/MonologHandlerPass.php",
    "chars": 2893,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "DependencyInjection/Configuration.php",
    "chars": 8479,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "DependencyInjection/EkinoNewRelicExtension.php",
    "chars": 7001,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "EkinoNewRelicBundle.php",
    "chars": 1199,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Exception/DeprecationException.php",
    "chars": 429,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "LICENSE",
    "chars": 1091,
    "preview": "The MIT License\n\nCopyright (c) 2012 Ekino - thomas.rabaix@ekino.com\n\nPermission is hereby granted, free of charge, to an"
  },
  {
    "path": "Listener/CommandListener.php",
    "chars": 3015,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Listener/DeprecationListener.php",
    "chars": 1408,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Listener/ExceptionListener.php",
    "chars": 1829,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Listener/RequestListener.php",
    "chars": 3830,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Listener/ResponseListener.php",
    "chars": 4391,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Logging/AdaptiveHandler.php",
    "chars": 912,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "NewRelic/AdaptiveInteractor.php",
    "chars": 5760,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "NewRelic/BlackholeInteractor.php",
    "chars": 2967,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "NewRelic/Config.php",
    "chars": 2995,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "NewRelic/LoggingInteractorDecorator.php",
    "chars": 7397,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "NewRelic/NewRelicInteractor.php",
    "chars": 5158,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "NewRelic/NewRelicInteractorInterface.php",
    "chars": 6412,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "README.md",
    "chars": 11933,
    "preview": "Ekino NewRelic Bundle\n=====================\n\n[![Build Status](https://secure.travis-ci.org/ekino/EkinoNewRelicBundle.png"
  },
  {
    "path": "Resources/config/command_listener.xml",
    "chars": 450,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/config/deprecation_listener.xml",
    "chars": 469,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/config/exception_listener.xml",
    "chars": 452,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/config/http_listener.xml",
    "chars": 521,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/config/monolog.xml",
    "chars": 678,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/config/services.xml",
    "chars": 971,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/config/twig.xml",
    "chars": 465,
    "preview": "<?xml version=\"1.0\" ?>\n\n<container xmlns=\"http://symfony.com/schema/dic/services\"\n    xmlns:xsi=\"http://www.w3.org/2001/"
  },
  {
    "path": "Resources/recipes/newrelic.rb",
    "chars": 1857,
    "preview": "namespace :newrelic do\n\n  # on all deployments, notify New Relic\n  desc \"Record a deployment in New Relic (newrelic.com)"
  },
  {
    "path": "Tests/AppKernel.php",
    "chars": 4299,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/BundleInitializationTest.php",
    "chars": 1591,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/DependencyInjection/Compiler/MonologHandlerPassTest.php",
    "chars": 3830,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/DependencyInjection/ConfigurationTest.php",
    "chars": 7281,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/DependencyInjection/EkinoNewRelicExtensionTest.php",
    "chars": 3613,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/Listener/CommandListenerTest.php",
    "chars": 4749,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/Listener/DeprecationListenerTest.php",
    "chars": 4411,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/Listener/ExceptionListenerTest.php",
    "chars": 2266,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/Listener/RequestListenerTest.php",
    "chars": 5959,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/Listener/ResponseListenerTest.php",
    "chars": 12603,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/NewRelic/ConfigTest.php",
    "chars": 2338,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/NewRelic/LoggingInteractorDecoratorTest.php",
    "chars": 2830,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/TransactionNamingStrategy/ControllerNamingStrategyTest.php",
    "chars": 1861,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Tests/Twig/NewRelicExtensionTest.php",
    "chars": 4328,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "TransactionNamingStrategy/ControllerNamingStrategy.php",
    "chars": 1480,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "TransactionNamingStrategy/RouteNamingStrategy.php",
    "chars": 640,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "TransactionNamingStrategy/TransactionNamingStrategyInterface.php",
    "chars": 498,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "Twig/NewRelicExtension.php",
    "chars": 3114,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
  },
  {
    "path": "UPGRADE-2.0.md",
    "chars": 5130,
    "preview": "UPGRADE FROM 1.x to 2.0\n=======================\n\n> Many internal things in the bundle has changed. If you only installed"
  },
  {
    "path": "composer.json",
    "chars": 1533,
    "preview": "{\n    \"name\": \"ekino/newrelic-bundle\",\n    \"type\": \"symfony-bundle\",\n    \"description\": \"Integrate New Relic into Symfon"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 837,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         "
  }
]

About this extraction

This page contains the full source code of the ekino/EkinoNewRelicBundle GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (168.9 KB), approximately 41.0k tokens, and a symbol index with 312 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!