From 382d1e768910d3e08a9743b868939d1c49971199 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Jan 2026 14:03:28 +0000 Subject: [PATCH 1/2] Add GraphQL New Relic transaction naming Co-authored-by: lewisvoncken --- .../Helper/NewRelicReportData.php | 163 ++++++++++++++++++ .../Model/Query/Logger/NewRelic.php | 2 - .../Plugin/TransactionNaming.php | 61 +++++++ app/code/Magento/GraphQlNewRelic/etc/di.xml | 3 + 4 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php create mode 100644 app/code/Magento/GraphQlNewRelic/Plugin/TransactionNaming.php diff --git a/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php b/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php new file mode 100644 index 0000000000000..ffbc9bf3d5bb3 --- /dev/null +++ b/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php @@ -0,0 +1,163 @@ +getGqlFieldsInfo($schema); + if (!$gqlFieldsInfo) { + return []; + } + + $gqlInfo = $this->extractGqlInfo($gqlFieldsInfo); + $operationName = $this->getOperationNameFromQuery($querySource); + + $finalGqlCallName = $operationName !== '' + ? $operationName + : ($gqlInfo['field_count'] > 1 ? self::MULTIPLE_QUERIES_FLAG : $gqlInfo['first_field_name']); + + return [ + 'transactionName' => $this->buildTransactionName($gqlInfo['gql_call_type'], $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), + 'gql_call_type' => $gqlFieldsInfo->name, + '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 query type and operation name. + * + * @param string $gqlCallType + * @param string $operationName + * @return string + */ + private function buildTransactionName(string $gqlCallType, string $operationName): string + { + return self::PREFIX . $gqlCallType . self::BACKSLASH . $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; + } + + /** + * @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); + } +} diff --git a/app/code/Magento/GraphQlNewRelic/Model/Query/Logger/NewRelic.php b/app/code/Magento/GraphQlNewRelic/Model/Query/Logger/NewRelic.php index 582caf1084507..d73ef97989e8e 100644 --- a/app/code/Magento/GraphQlNewRelic/Model/Query/Logger/NewRelic.php +++ b/app/code/Magento/GraphQlNewRelic/Model/Query/Logger/NewRelic.php @@ -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; } diff --git a/app/code/Magento/GraphQlNewRelic/Plugin/TransactionNaming.php b/app/code/Magento/GraphQlNewRelic/Plugin/TransactionNaming.php new file mode 100644 index 0000000000000..a4d661ff40e0d --- /dev/null +++ b/app/code/Magento/GraphQlNewRelic/Plugin/TransactionNaming.php @@ -0,0 +1,61 @@ +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'])); + } +} diff --git a/app/code/Magento/GraphQlNewRelic/etc/di.xml b/app/code/Magento/GraphQlNewRelic/etc/di.xml index 71383f1689be9..ba9e10a923c10 100644 --- a/app/code/Magento/GraphQlNewRelic/etc/di.xml +++ b/app/code/Magento/GraphQlNewRelic/etc/di.xml @@ -6,6 +6,9 @@ */ --> + + + From d757c2d600af28fb82a47634b210e8c2f459845b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Jan 2026 14:29:05 +0000 Subject: [PATCH 2/2] Shorten GraphQL transaction naming Co-authored-by: lewisvoncken --- .../Helper/NewRelicReportData.php | 76 ++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php b/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php index ffbc9bf3d5bb3..d27e1537f2a9a 100644 --- a/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php +++ b/app/code/Magento/GraphQlNewRelic/Helper/NewRelicReportData.php @@ -8,14 +8,16 @@ 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 PREFIX = '/GraphQl/Controller/GraphQl\\'; - private const BACKSLASH = '\\'; + private const TRANSACTION_PREFIX = 'GraphQL-'; private const MULTIPLE_QUERIES_FLAG = 'Multiple'; /** @@ -35,12 +37,15 @@ public function getTransactionData(Schema $schema, DocumentNode|string $querySou $gqlInfo = $this->extractGqlInfo($gqlFieldsInfo); $operationName = $this->getOperationNameFromQuery($querySource); + $primaryFieldName = $this->getPrimaryFieldNameFromQuery($querySource); $finalGqlCallName = $operationName !== '' ? $operationName - : ($gqlInfo['field_count'] > 1 ? self::MULTIPLE_QUERIES_FLAG : $gqlInfo['first_field_name']); + : ($gqlInfo['field_count'] > 1 + ? self::MULTIPLE_QUERIES_FLAG + : ($primaryFieldName !== '' ? $primaryFieldName : $gqlInfo['first_field_name'])); return [ - 'transactionName' => $this->buildTransactionName($gqlInfo['gql_call_type'], $finalGqlCallName), + 'transactionName' => $this->buildTransactionName($finalGqlCallName), 'fieldCount' => $gqlInfo['field_count'], 'fieldNames' => $gqlInfo['all_field_names'], ]; @@ -56,7 +61,6 @@ private function extractGqlInfo(ObjectType $gqlFieldsInfo): array return [ 'field_count' => count($gqlFields), - 'gql_call_type' => $gqlFieldsInfo->name, 'first_field_name' => array_key_first($gqlFields) ?? '', 'all_field_names' => array_keys($gqlFields), ]; @@ -84,15 +88,14 @@ private function getGqlFieldsInfo(?Schema $schema): ?ObjectType } /** - * Build a transaction name based on query type and operation name. + * Build a transaction name based on operation name. * - * @param string $gqlCallType * @param string $operationName * @return string */ - private function buildTransactionName(string $gqlCallType, string $operationName): string + private function buildTransactionName(string $operationName): string { - return self::PREFIX . $gqlCallType . self::BACKSLASH . $operationName; + return self::TRANSACTION_PREFIX . $operationName; } /** @@ -127,6 +130,61 @@ public function getOperationNameFromQuery(DocumentNode|string $querySource): str 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