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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\GraphQlNewRelic\Helper;

use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\ObjectType;
use Magento\Framework\GraphQl\Schema;

class NewRelicReportData
{
private const TRANSACTION_PREFIX = 'GraphQL-';
private const MULTIPLE_QUERIES_FLAG = 'Multiple';

/**
* Get transaction data from GraphQl schema.
*
* @param Schema $schema
* @param DocumentNode|string $querySource
* @return array
*/
public function getTransactionData(Schema $schema, DocumentNode|string $querySource): array
{
$gqlFieldsInfo = $this->getGqlFieldsInfo($schema);
if (!$gqlFieldsInfo) {
return [];
}

$gqlInfo = $this->extractGqlInfo($gqlFieldsInfo);
$operationName = $this->getOperationNameFromQuery($querySource);

$primaryFieldName = $this->getPrimaryFieldNameFromQuery($querySource);
$finalGqlCallName = $operationName !== ''
? $operationName
: ($gqlInfo['field_count'] > 1
? self::MULTIPLE_QUERIES_FLAG
: ($primaryFieldName !== '' ? $primaryFieldName : $gqlInfo['first_field_name']));

return [
'transactionName' => $this->buildTransactionName($finalGqlCallName),
'fieldCount' => $gqlInfo['field_count'],
'fieldNames' => $gqlInfo['all_field_names'],
];
}

/**
* @param ObjectType $gqlFieldsInfo
* @return array
*/
private function extractGqlInfo(ObjectType $gqlFieldsInfo): array
{
$gqlFields = $gqlFieldsInfo->getFields();

return [
'field_count' => count($gqlFields),
'first_field_name' => array_key_first($gqlFields) ?? '',
'all_field_names' => array_keys($gqlFields),
];
}

/**
* @param Schema|null $schema
* @return ObjectType|null
*/
private function getGqlFieldsInfo(?Schema $schema): ?ObjectType
{
if (!$schema) {
return null;
}

$schemaConfig = $schema->getConfig();
if (!$schemaConfig) {
return null;
}

$mutation = $schemaConfig->getMutation();
$hasMutationFields = $mutation && count($mutation->getFields()) > 0;

return $hasMutationFields ? $mutation : $schemaConfig->getQuery();
}

/**
* Build a transaction name based on operation name.
*
* @param string $operationName
* @return string
*/
private function buildTransactionName(string $operationName): string
{
return self::TRANSACTION_PREFIX . $operationName;
}

/**
* Get operation name from query.
*
* @param DocumentNode|string $querySource
* @return string
*/
public function getOperationNameFromQuery(DocumentNode|string $querySource): string
{
$query = $this->getQueryString($querySource);
if ($query === '') {
return '';
}

$bracePosition = stripos($query, '{');
if ($bracePosition === false) {
return '';
}

$operationBeginningSegment = substr($query, 0, $bracePosition);
if ($operationBeginningSegment === '') {
return '';
}

$operationName = '';
if (preg_match('/(query|mutation)/', $operationBeginningSegment, $matches, PREG_OFFSET_CAPTURE)) {
$strQueryOrMutation = $matches[0][0];
$operationName = trim($this->getSubString($strQueryOrMutation, '(', $operationBeginningSegment));
}

return $operationName;
}

/**
* Prefer the top-level field alias when available.
*
* @param DocumentNode|string $querySource
* @return string
*/
private function getPrimaryFieldNameFromQuery(DocumentNode|string $querySource): string
{
$document = $this->getDocumentNode($querySource);
if (!$document) {
return '';
}

foreach ($document->definitions as $definition) {
if (!$definition instanceof OperationDefinitionNode || !$definition->selectionSet) {
continue;
}

foreach ($definition->selectionSet->selections as $selection) {
if (!$selection instanceof FieldNode) {
continue;
}

if ($selection->alias) {
return $selection->alias->value;
}

return $selection->name->value ?? '';
}
}

return '';
}

/**
* @param DocumentNode|string $querySource
* @return DocumentNode|null
*/
private function getDocumentNode(DocumentNode|string $querySource): ?DocumentNode
{
if ($querySource instanceof DocumentNode) {
return $querySource;
}

if ($querySource === '') {
return null;
}

try {
return Parser::parse($querySource);
} catch (\Exception) {
return null;
}
}

/**
* @param DocumentNode|string $querySource
* @return string
*/
private function getQueryString(DocumentNode|string $querySource): string
{
if ($querySource instanceof DocumentNode) {
return Printer::doPrint($querySource);
}

return $querySource;
}

/**
* Get string in between two strings.
*
* @param string $startingStr
* @param string $endingStr
* @param string $str
* @return string
*/
private function getSubString(string $startingStr, string $endingStr, string $str): string
{
$subStrStart = strpos($str, $startingStr);
$subStrStart += strlen($startingStr);

$hasEndingStr = (strpos($str, $endingStr, $subStrStart)) !== false;
$lengthOfSubstr = $hasEndingStr
? (strpos($str, $endingStr, $subStrStart) - $subStrStart)
: (strlen($str) - $subStrStart);

return substr($str, $subStrStart, $lengthOfSubstr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public function __construct(
*/
public function execute(array $queryDetails)
{
$transactionName = $queryDetails[LoggerInterface::TOP_LEVEL_OPERATION_NAME] ?? '';
$this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName);
if (!$this->config->isNewRelicEnabled()) {
return;
}
Expand Down
61 changes: 61 additions & 0 deletions app/code/Magento/GraphQlNewRelic/Plugin/TransactionNaming.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\GraphQlNewRelic\Plugin;

use GraphQL\Language\AST\DocumentNode;
use Magento\Framework\GraphQl\Query\QueryProcessor;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Schema;
use Magento\GraphQlNewRelic\Helper\NewRelicReportData;
use Magento\NewRelicReporting\Model\NewRelicWrapper;

/**
* Plugin that sets GraphQL transaction names for New Relic.
*/
class TransactionNaming
{
/**
* @param NewRelicWrapper $newRelicWrapper
* @param NewRelicReportData $reportData
*/
public function __construct(
private NewRelicWrapper $newRelicWrapper,
private NewRelicReportData $reportData
) {
}

/**
* Rename a GraphQl transaction for New Relic before processing it.
*
* @param QueryProcessor $subject
* @param Schema $schema
* @param DocumentNode|string $source
* @param ContextInterface|null $contextValue
* @param array|null $variableValues
* @param string|null $operationName
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforeProcess(
QueryProcessor $subject,
Schema $schema,
DocumentNode|string $source,
?ContextInterface $contextValue = null,
?array $variableValues = null,
?string $operationName = null
): void {
$transactionData = $this->reportData->getTransactionData($schema, $source);
if (empty($transactionData)) {
return;
}

$this->newRelicWrapper->setTransactionName($transactionData['transactionName']);
$this->newRelicWrapper->addCustomParameter('GraphqlNumberOfFields', $transactionData['fieldCount']);
$this->newRelicWrapper->addCustomParameter('FieldNames', implode('|', $transactionData['fieldNames']));
}
}
3 changes: 3 additions & 0 deletions app/code/Magento/GraphQlNewRelic/etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\GraphQl\Query\QueryProcessor">
<plugin name="newrelic-graphql-transaction-name" type="Magento\GraphQlNewRelic\Plugin\TransactionNaming" sortOrder="10"/>
</type>
<type name="Magento\GraphQl\Model\Query\Logger\LoggerPool">
<arguments>
<argument name="loggers" xsi:type="array">
Expand Down