From 3e61333c120cb7803fc9ddb8fae9a541a28a7b8b Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 10 Aug 2026 17:14:23 +0200 Subject: [PATCH 01/43] Implement normalised http routes --- .../CakePHP/CakePHPIntegration.php | 7 +- .../CodeIgniter/V2/CodeIgniterIntegration.php | 12 + .../Laminas/LaminasIntegration.php | 5 + .../Laravel/LaravelIntegration.php | 8 +- .../Integrations/Slim/SlimIntegration.php | 26 +- .../Symfony/SymfonyIntegration.php | 4 + .../WordPress/WordPressIntegrationLoader.php | 7 +- .../Integrations/Yii/YiiIntegration.php | 4 + src/DDTrace/Util/RouteNormalizer.php | 518 ++++++++++++++++++ src/api/Tag.php | 1 + .../CakePHP/V4_5/CommonScenariosTest.php | 4 + .../CodeIgniter/V2_2/CommonScenariosTest.php | 6 +- .../Slim/Latest/CommonScenariosTest.php | 4 + .../Symfony/Latest/CommonScenariosTest.php | 3 + .../Yii/Latest/CommonScenariosTest.php | 4 + .../Util/Normalizer/RouteNormalizerTest.php | 349 ++++++++++++ ...oller_test.test_scenario_health_check.json | 1 + ..._test.test_scenario_get_parameterized.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...st.test_scenario_get_to_missing_route.json | 1 + ...est_scenario_get_to_missing_route_cgi.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ....test_scenario_get_with_exception_cgi.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ...ter.v3_1.exit_test.test_scenario_exit.json | 1 + ...atest.rest_test.test_scenario_rest2xx.json | 1 + ...atest.rest_test.test_scenario_rest4xx.json | 1 + ...atest.rest_test.test_scenario_rest5xx.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ...pigw_test.test_laravel_inferred_proxy.json | 1 + ...test_laravel_inferred_proxy_exception.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...t_scenario_get_with_ignored_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ...octane.apigw_test.test_inferred_proxy.json | 1 + ...gw_test.test_inferred_proxy_exception.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...t_scenario_get_with_ignored_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_dynamic_route.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_dynamic_route.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_dynamic_route.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ...aravel.v8_x.queue_test.test_broadcast.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...acks_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...acks_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...acks_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...acks_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + 89 files changed, 1028 insertions(+), 7 deletions(-) create mode 100644 src/DDTrace/Util/RouteNormalizer.php create mode 100644 tests/Unit/Util/Normalizer/RouteNormalizerTest.php diff --git a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php index 9adf7691128..2d416f19ea5 100644 --- a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php +++ b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php @@ -71,7 +71,12 @@ public static function init(): int $rootSpan = \DDTrace\root_span(); if ($rootSpan !== null) { - $rootSpan->meta[Tag::HTTP_ROUTE] = $app->template; + $template = $app->template; + $rootSpan->meta[Tag::HTTP_ROUTE] = $template; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCakePHP($template); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } }; diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index 15c8f684f67..8b1b4ad6c02 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -230,6 +230,10 @@ private static function setHttpRoute($router, $rootSpan) { if (isset($router->routes[$uri])) { $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($uri); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } return; } @@ -244,6 +248,10 @@ private static function setHttpRoute($router, $rootSpan) { if (preg_match('#^'.$key.'$#', $uri)) { $rootSpan->meta[Tag::HTTP_ROUTE] = $origKey; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($origKey); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } return; } } @@ -251,5 +259,9 @@ private static function setHttpRoute($router, $rootSpan) { // If we got this far it means we didn't encounter a // matching route so we'll set the site default route $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($uri); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } } diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 0d55f3d22a9..273903cfb47 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -284,6 +284,11 @@ static function (SpanData $span) use ($controller, $action) { $httpRoute = LaminasIntegration::httpRouteTemplateFromNamedRouteStack($this, (string) $routeName); if ($httpRoute !== null && $httpRoute !== '') { $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; + $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } } diff --git a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php index 0f00a02b825..5862792bd73 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -140,7 +140,13 @@ static function ($This, $scope, $args, $route) { $rootSpan->meta[Tag::HTTP_URL] = \DDTrace\Util\Normalizer::urlSanitize($request->fullUrl()); } if (\method_exists($route, 'uri')) { - $rootSpan->meta[Tag::HTTP_ROUTE] = $route->uri(); + $httpRoute = $route->uri(); + $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; + $matchedParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaravel($httpRoute, $matchedParams); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } if (\method_exists($route, 'parameters') && function_exists('\datadog\appsec\push_addresses')) { $parameters = $route->parameters(); diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index d135e66d810..8c1725dec06 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -75,11 +75,16 @@ static function ($errorMiddleware, $self, $args) use ($rootSpan, $integration) { null, static function ($router, $scope, $args, $return) use ($rootSpan) { /** @var \Slim\Interfaces\RouteInterface $return */ - $rootSpan->meta[Tag::HTTP_ROUTE] = $return->getPattern(); + $pattern = $return->getPattern(); + $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { $rootSpan->resource = - $_SERVER['REQUEST_METHOD'] . ' ' . ($return->getName() ?: $return->getPattern()); + $_SERVER['REQUEST_METHOD'] . ' ' . ($return->getName() ?: $pattern); } } ); @@ -92,7 +97,13 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { static function ($router, $scope, $args, $return) use ($rootSpan) { /** @var \Slim\Interfaces\RouteInterface $route */ $route = $return; - $rootSpan->meta[Tag::HTTP_ROUTE] = $route->getPattern(); + $pattern = $route->getPattern(); + $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; + // Normalized route will be refined in traceControllers once matched params are available + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } ); } @@ -131,6 +142,15 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $span->meta['slim.route.name'] = $routeName; $rootSpan->meta['slim.route.name'] = $routeName; } + // Refine normalized route now that matched params are available + $matchedParams = method_exists($route, 'getArguments') ? ($route->getArguments() ?? []) : []; + $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; + if ($pattern !== '') { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } + } } } else { $rootSpan->meta['slim.route.controller'] = $callableName; diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index d0540860fa5..a77f59d2998 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -456,6 +456,10 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } }; } else { diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index 4a9cd2a5178..cf10e8ce85b 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -732,7 +732,12 @@ static function (HookData $hook) use ( function_exists('is_404') && is_404() === false) { $rootSpan = \DDTrace\root_span(); if (\property_exists($This, 'matched_rule')) { - $rootSpan->meta[Tag::HTTP_ROUTE] = $This->matched_rule; + $matchedRule = $This->matched_rule; + $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } } }); diff --git a/src/DDTrace/Integrations/Yii/YiiIntegration.php b/src/DDTrace/Integrations/Yii/YiiIntegration.php index 91fa862d5f9..8acc12e9047 100644 --- a/src/DDTrace/Integrations/Yii/YiiIntegration.php +++ b/src/DDTrace/Integrations/Yii/YiiIntegration.php @@ -156,6 +156,10 @@ function (SpanData $span, $args) use (&$firstController) { $rootSpan->meta['app.route.path'] = $routePath; $rootSpan->meta[Tag::HTTP_ROUTE] = $routePath; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromYii($routePath); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { $resourceName = \str_replace( diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php new file mode 100644 index 00000000000..a613ac0ecae --- /dev/null +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -0,0 +1,518 @@ +uri(), e.g. "/users/{id}/{format?}" + * @param array $matchedParams Parameters from $route->parameters(); used to resolve optionals + * @return string|null Normalized route, or null on parse failure + */ + public static function normalizeFromLaravel(string $routeUri, array $matchedParams = []): ?string + { + return self::normalizeBraceRoute($routeUri, $matchedParams); + } + + /** + * Normalize a Slim route pattern. + * + * Slim uses {param} or {param:regex} for required parameters. Slim 4 supports optional + * segments in square brackets: /users/{id}[/{format}]. + * + * @param string $pattern Pattern from $route->getPattern(), e.g. "/users/{id:[0-9]+}" + * @param array $matchedParams Matched params from $route->getArguments(); resolves optionals + * @return string|null + */ + public static function normalizeFromSlim(string $pattern, array $matchedParams = []): ?string + { + return self::normalizeBraceRoute($pattern, $matchedParams, true); + } + + /** + * Normalize a Symfony route path. + * + * The path produced by EndpointCatalog::pathForRoute() already uses {param} notation. + * + * @param string $path Path template, e.g. "/users/{id}" + * @return string|null + */ + public static function normalizeFromSymfony(string $path): ?string + { + return self::normalizeBraceRoute($path, []); + } + + /** + * Normalize a Laminas route template. + * + * Laminas uses :param for dynamic parameters and [...] for optional sections. + * The Wildcard route type produces "/*" which is treated as a catch-all. + * Example: "/users/:id[.:format]" + * + * @param string $template Template from httpRouteTemplateFromMatchedRoute() + * @param array $matchedParams Matched params from $routeMatch->getParams() + * @return string|null + */ + public static function normalizeFromLaminas(string $template, array $matchedParams = []): ?string + { + $expanded = self::expandBracketOptionals($template, $matchedParams, ':'); + // Convert wildcard segments ('*') to a {param} placeholder before brace conversion + $expanded = preg_replace('#/\*$#', '/{param1}', $expanded); + $braceFormat = self::colonParamsToBraces($expanded); + return self::normalizeBraceRoute($braceFormat, $matchedParams); + } + + /** + * Normalize a CakePHP route template. + * + * CakePHP uses :param syntax and * / ** for catch-all segments. + * + * @param string $template Template from $app->template, e.g. "/articles/:id.:ext" + * @return string|null + */ + public static function normalizeFromCakePHP(string $template): ?string + { + $braceFormat = self::cakephpToBraces($template); + return self::normalizeBraceRoute($braceFormat, []); + } + + /** + * Normalize a Yii route path containing :param placeholders. + * + * The Yii integration builds a URL via Url::toRoute() with ":paramName" as + * placeholder values, producing a path like "/articles/:id". + * + * @param string $routePath Path from Url::toRoute() with colon placeholders + * @return string|null + */ + public static function normalizeFromYii(string $routePath): ?string + { + $braceFormat = self::colonParamsToBraces($routePath); + return self::normalizeBraceRoute($braceFormat, []); + } + + /** + * Normalize a CodeIgniter V2 route pattern. + * + * CodeIgniter uses :any / :num wildcards and positional regex groups. + * Named parameters are not available, so placeholders param1, param2, … are used. + * + * @param string $route Route key from $router->routes, e.g. "blog/(:num)" + * @return string|null + */ + public static function normalizeFromCodeIgniter(string $route): ?string + { + $route = trim($route, '/'); + if ($route === '') { + return '/'; + } + + $segments = explode('/', $route); + $normalizedSegments = []; + $paramIndex = 1; + + foreach ($segments as $segment) { + if ($segment === '') { + continue; + } + + $lower = strtolower($segment); + if ( + $lower === ':any' || $lower === ':num' || + $lower === '(:any)' || $lower === '(:num)' + ) { + $normalizedSegments[] = '{param' . $paramIndex++ . '}'; + } elseif (preg_match('/[()[\].*+?|^$\\\\]/', $segment) || strpos($segment, ':') !== false) { + $normalizedSegments[] = '{param' . $paramIndex++ . '}'; + } else { + $normalizedSegments[] = self::encodeStaticSegment($segment); + } + } + + return '/' . implode('/', $normalizedSegments); + } + + /** + * Normalize a WordPress matched_rule (regex). + * + * WordPress route matching uses regex rules like "^blog/([^/]+)/?$". + * Named parameters are not available; placeholders param1, param2, … are used. + * + * @param string $matchedRule Value of $wp->matched_rule + * @return string|null + */ + public static function normalizeFromWordPress(string $matchedRule): ?string + { + $rule = $matchedRule; + + // Strip regex anchors + $rule = ltrim($rule, '^'); + $rule = rtrim($rule, '$'); + + // Strip optional trailing slash pattern "\/?" or "/?" + if (preg_match('#\\\\?/\?$#', $rule, $m)) { + $rule = substr($rule, 0, -strlen($m[0])); + } + + $rule = trim($rule, '/'); + if ($rule === '') { + return '/'; + } + + // Use bracket-aware splitting so that [^/] character classes are not split + $segments = self::splitRegexBySlash($rule); + $normalizedSegments = []; + $paramIndex = 1; + + foreach ($segments as $segment) { + if ($segment === '') { + continue; + } + + if (preg_match('/^\([^)]+\)$/', $segment)) { + // Whole segment is a single capture group + $normalizedSegments[] = '{param' . $paramIndex++ . '}'; + } elseif (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { + // Contains regex metacharacters → treat as dynamic + $normalizedSegments[] = '{param' . $paramIndex++ . '}'; + } else { + $normalizedSegments[] = self::encodeStaticSegment($segment); + } + } + + if (empty($normalizedSegments)) { + return '/{param1}'; + } + + return '/' . implode('/', $normalizedSegments); + } + + /** + * Split a regex string by '/' but do not split inside character classes [...]. + * This prevents [^/] from being broken into two segments. + */ + private static function splitRegexBySlash(string $str): array + { + $segments = []; + $current = ''; + $len = strlen($str); + $bracketDepth = 0; + + for ($i = 0; $i < $len; $i++) { + $c = $str[$i]; + + if ($c === '\\' && $i + 1 < $len) { + $current .= $c . $str[$i + 1]; + $i++; + continue; + } + + if ($c === '[') { + $bracketDepth++; + $current .= $c; + } elseif ($c === ']' && $bracketDepth > 0) { + $bracketDepth--; + $current .= $c; + } elseif ($c === '/' && $bracketDepth === 0) { + $segments[] = $current; + $current = ''; + } else { + $current .= $c; + } + } + + $segments[] = $current; + return $segments; + } + + // ------------------------------------------------------------------------- + // Core brace-format normalizer + // ------------------------------------------------------------------------- + + /** + * Normalize a route that uses {param} notation. + * + * Handles: + * {param} - required parameter + * {param?} - optional parameter (resolved via $matchedParams) + * {param:regex} - regex-constrained parameter (constraint stripped) + * [{param}] - optional segment (Slim-style; only when $expandSquare=true) + * Static text mixed with params in a segment → single combined element + * + * @param bool $expandSquare When true, expand Slim-style [...] optional sections + */ + private static function normalizeBraceRoute( + string $route, + array $matchedParams, + bool $expandSquare = false + ): ?string { + $route = trim($route); + if ($route === '' || $route === '/') { + return '/'; + } + + $trailingSlash = (strlen($route) > 1 && $route[-1] === '/') ? '/' : ''; + $route = rtrim($route, '/'); + + if ($route[0] !== '/') { + $route = '/' . $route; + } + + if ($expandSquare) { + $route = self::expandSquareBracketOptionals($route, $matchedParams); + } + + $raw = ltrim($route, '/'); + $parts = explode('/', $raw); + $normalizedSegments = []; + + foreach ($parts as $segment) { + if ($segment === '') { + continue; + } + + $result = self::normalizeBraceSegment($segment, $matchedParams); + if ($result === null) { + // Optional segment whose parameter is absent — skip + continue; + } + + $normalizedSegments[] = $result; + } + + return '/' . implode('/', $normalizedSegments) . $trailingSlash; + } + + /** + * Normalize a single URL segment that may contain {param} placeholders. + * + * @return string|null The normalized element, or null if the segment is optional and absent + */ + private static function normalizeBraceSegment(string $segment, array $matchedParams): ?string + { + preg_match_all('/\{([^}]+)\}/', $segment, $matches, PREG_SET_ORDER); + + if (empty($matches)) { + return self::encodeStaticSegment($segment); + } + + $paramNames = []; + foreach ($matches as $match) { + $raw = $match[1]; + + $isOptional = ($raw[-1] === '?'); + if ($isOptional) { + $raw = substr($raw, 0, -1); + } + + // Strip regex constraint: {param:regex} → param + $colon = strpos($raw, ':'); + if ($colon !== false) { + $raw = substr($raw, 0, $colon); + } + + $name = trim($raw); + + if ($isOptional && !array_key_exists($name, $matchedParams)) { + return null; + } + + $paramNames[] = self::encodeParamName($name); + } + + if (count($paramNames) === 1) { + return '{' . $paramNames[0] . '}'; + } + + // Multiple params in the same segment → combine with the '+' marker + return '{' . implode('+', $paramNames) . '}'; + } + + // ------------------------------------------------------------------------- + // Optional-section expanders + // ------------------------------------------------------------------------- + + /** + * Expand Slim-style optional sections [...] in a route based on matched params. + * Example: "/users/{id}[/{format}]" with format present → "/users/{id}/{format}" + */ + private static function expandSquareBracketOptionals(string $route, array $matchedParams): string + { + // Expand from innermost to outermost by looping until stable + $prev = null; + while ($prev !== $route) { + $prev = $route; + $route = preg_replace_callback( + '/\[([^\[\]]*)\]/', + function ($m) use ($matchedParams) { + $inner = $m[1]; + preg_match_all('/\{([^}?:]+)[?:]?[^}]*\}/', $inner, $pm); + $innerParams = $pm[1]; + + if (empty($innerParams)) { + // Purely static optional section — include it (best effort) + return $inner; + } + + foreach ($innerParams as $param) { + if (array_key_exists($param, $matchedParams)) { + return $inner; + } + } + + return ''; + }, + $route + ); + } + return $route; + } + + /** + * Expand Laminas [...] optional sections based on matched params. + * Example: "/:id[.:format]" with format present → "/:id.:format" + */ + private static function expandBracketOptionals( + string $template, + array $matchedParams, + string $paramPrefix = ':' + ): string { + $prev = null; + while ($prev !== $template) { + $prev = $template; + $template = preg_replace_callback( + '/\[([^\[\]]*)\]/', + function ($m) use ($matchedParams, $paramPrefix) { + $inner = $m[1]; + $pattern = '/' . preg_quote($paramPrefix, '/') . '([a-zA-Z_][a-zA-Z0-9_]*)/'; + preg_match_all($pattern, $inner, $pm); + $innerParams = $pm[1]; + + foreach ($innerParams as $param) { + if (array_key_exists($param, $matchedParams)) { + return $inner; + } + } + + return ''; + }, + $template + ); + } + return $template; + } + + // ------------------------------------------------------------------------- + // Framework-specific syntax converters + // ------------------------------------------------------------------------- + + /** + * Convert ":paramName" colon-prefix notation to "{paramName}" brace notation. + * Laminas segment constraints like ":param{constraint}" are also handled. + */ + private static function colonParamsToBraces(string $template): string + { + return preg_replace_callback( + '/:([a-zA-Z_][a-zA-Z0-9_]*)(?:\{[^}]*\})?/', + static function ($m) { + return '{' . $m[1] . '}'; + }, + $template + ); + } + + /** + * Convert CakePHP route template syntax to brace notation. + * Handles :param and * / ** catch-all segments. + */ + private static function cakephpToBraces(string $template): string + { + // Replace /** and /* with a {catchall} placeholder + $result = preg_replace('#/\*\*#', '/{catchall}', $template); + $result = preg_replace('#/\*(?!\*)#', '/{catchall}', $result); + + // Replace remaining bare * (e.g. at start or standalone segment) + $result = preg_replace('#(?= 'A' && $c <= 'Z') || ($c >= 'a' && $c <= 'z') || + ($c >= '0' && $c <= '9') || + $c === '.' || $c === '-' || $c === '~' || $c === '_' + ) { + $result .= $c; + } elseif ( + $c === '%' && + $i + 2 < $len && + ctype_xdigit($segment[$i + 1]) && + ctype_xdigit($segment[$i + 2]) + ) { + $result .= '%' . strtoupper($segment[$i + 1]) . strtoupper($segment[$i + 2]); + $i += 2; + } else { + $result .= rawurlencode($c); + } + } + return $result; + } + + /** + * URL-encode reserved characters in a parameter name. + * Reserved: /?#+{} — these must not appear literally. + * The '+' sign is the combining marker and must be encoded if it appears in a + * framework-supplied name. + */ + public static function encodeParamName(string $name): string + { + $reserved = '/?#+{}'; + $result = ''; + $len = strlen($name); + for ($i = 0; $i < $len; $i++) { + $c = $name[$i]; + if (strpos($reserved, $c) !== false) { + $result .= rawurlencode($c); + } else { + $result .= $c; + } + } + return $result; + } +} diff --git a/src/api/Tag.php b/src/api/Tag.php index f2cb6b7c1e4..ca264c04db6 100644 --- a/src/api/Tag.php +++ b/src/api/Tag.php @@ -26,6 +26,7 @@ class Tag const ERROR_STACK = 'error.stack'; // human readable version of the stack const HTTP_METHOD = 'http.method'; const HTTP_ROUTE = 'http.route'; + const APPSEC_NORMALIZED_ROUTE = '_dd.appsec.normalized_route'; const HTTP_STATUS_CODE = 'http.status_code'; const HTTP_URL = 'http.url'; const HTTP_VERSION = 'http.version'; diff --git a/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php b/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php index f1e85015734..953d3705cb9 100644 --- a/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php +++ b/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php @@ -61,6 +61,7 @@ public function provideSpecs() 'http.route' => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', ])->withChildren([ SpanAssertion::build( 'Controller.invokeAction', @@ -87,6 +88,7 @@ public function provideSpecs() 'http.route' => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', ])->withChildren([ SpanAssertion::build( 'Controller.invokeAction', @@ -123,6 +125,7 @@ public function provideSpecs() 'http.route' => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', ])->withExistingTagsNames([ 'error.stack' ])->setError( @@ -165,6 +168,7 @@ public function provideSpecs() 'http.route' => '/parameterized/{param}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param}', ])->withChildren([ SpanAssertion::build( 'Controller.invokeAction', diff --git a/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php b/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php index 69e022c6fc8..900e78630b5 100644 --- a/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php @@ -56,6 +56,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ SpanAssertion::build( 'Simple.index', @@ -81,6 +82,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ SpanAssertion::build( 'Simple_View.index', @@ -115,7 +117,8 @@ public function provideSpecs() 'app.endpoint' => 'Error_::index', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', - Tag::HTTP_ROUTE => 'error' + Tag::HTTP_ROUTE => 'error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', ]) ->setError("Exception", "Uncaught Exception: datadog in %s:%d") ->withExistingTagsNames(['error.stack']) @@ -144,6 +147,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'parameterized/(:any)', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param1}', ])->withChildren([ SpanAssertion::build( 'Parameterized.customAction', diff --git a/tests/Integrations/Slim/Latest/CommonScenariosTest.php b/tests/Integrations/Slim/Latest/CommonScenariosTest.php index e801ed8b350..2ab60ff6f8d 100644 --- a/tests/Integrations/Slim/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Slim/Latest/CommonScenariosTest.php @@ -122,6 +122,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ $this->wrapMiddleware([ SpanAssertion::build( @@ -150,6 +151,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ $this->wrapMiddleware([ SpanAssertion::build( @@ -187,6 +189,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', ]) ->setError(null, null) ->withChildren([ @@ -221,6 +224,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/parameterized/{value}', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{value}', ])->withChildren([ $this->wrapMiddleware([ SpanAssertion::build( diff --git a/tests/Integrations/Symfony/Latest/CommonScenariosTest.php b/tests/Integrations/Symfony/Latest/CommonScenariosTest.php index f7a1571c7af..0a301317752 100644 --- a/tests/Integrations/Symfony/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/Latest/CommonScenariosTest.php @@ -74,6 +74,7 @@ public function provideSpecs() 'http.status_code' => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'symfony', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ SpanAssertion::exists('symfony.httpkernel.kernel.handle') ->withChildren([ @@ -113,6 +114,7 @@ public function provideSpecs() 'http.status_code' => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'symfony', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ SpanAssertion::exists('symfony.kernel.terminate'), SpanAssertion::exists('symfony.httpkernel.kernel.handle')->withChildren([ @@ -159,6 +161,7 @@ public function provideSpecs() 'http.status_code' => '500', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'symfony', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', ]) ->setError('Exception', 'An exception occurred') ->withExistingTagsNames(['error.stack']) diff --git a/tests/Integrations/Yii/Latest/CommonScenariosTest.php b/tests/Integrations/Yii/Latest/CommonScenariosTest.php index 62410148e30..8a8509739aa 100644 --- a/tests/Integrations/Yii/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Yii/Latest/CommonScenariosTest.php @@ -59,6 +59,7 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionIndex', 'app.route.path' => '/simple', Tag::HTTP_ROUTE => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ])->withChildren([ @@ -103,6 +104,7 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionView', 'app.route.path' => '/simple_view', Tag::HTTP_ROUTE => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ])->withChildren([ @@ -150,6 +152,7 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionError', 'app.route.path' => '/error', Tag::HTTP_ROUTE => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ]) @@ -222,6 +225,7 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionParameterized', 'app.route.path' => '/parameterized/:value', Tag::HTTP_ROUTE => '/parameterized/:value', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{value}', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ])->withChildren([ diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php new file mode 100644 index 00000000000..5b9c7785d35 --- /dev/null +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -0,0 +1,349 @@ +assertSame('hello', RouteNormalizer::encodeStaticSegment('hello')); + $this->assertSame('Hello-World_v1.0~test', RouteNormalizer::encodeStaticSegment('Hello-World_v1.0~test')); + } + + public function testEncodeStaticSegmentEncodesReserved() + { + $this->assertSame('dump-request', RouteNormalizer::encodeStaticSegment('dump-request')); + $this->assertSame('foo%40bar', RouteNormalizer::encodeStaticSegment('foo@bar')); + $this->assertSame('foo%20bar', RouteNormalizer::encodeStaticSegment('foo bar')); + } + + public function testEncodeStaticSegmentPreservesExistingPercentEncoding() + { + $this->assertSame('%2F', RouteNormalizer::encodeStaticSegment('%2F')); + $this->assertSame('%2F', RouteNormalizer::encodeStaticSegment('%2f')); + } + + // ------------------------------------------------------------------------- + // encodeParamName + // ------------------------------------------------------------------------- + + public function testEncodeParamNamePreservesNormal() + { + $this->assertSame('id', RouteNormalizer::encodeParamName('id')); + $this->assertSame('user_id', RouteNormalizer::encodeParamName('user_id')); + } + + public function testEncodeParamNameEncodesPlusSign() + { + $this->assertSame('foo%2Bbar', RouteNormalizer::encodeParamName('foo+bar')); + } + + public function testEncodeParamNameEncodesReserved() + { + $this->assertSame('foo%23bar', RouteNormalizer::encodeParamName('foo#bar')); + } + + // ------------------------------------------------------------------------- + // normalizeFromLaravel + // ------------------------------------------------------------------------- + + public function testLaravelSimpleRoute() + { + $this->assertSame('/users', RouteNormalizer::normalizeFromLaravel('/users')); + $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromLaravel('/users/{id}')); + } + + public function testLaravelOptionalParamPresent() + { + $result = RouteNormalizer::normalizeFromLaravel('/users/{id}/{format?}', ['id' => '1', 'format' => 'json']); + $this->assertSame('/users/{id}/{format}', $result); + } + + public function testLaravelOptionalParamAbsent() + { + $result = RouteNormalizer::normalizeFromLaravel('/users/{id}/{format?}', ['id' => '1']); + $this->assertSame('/users/{id}', $result); + } + + public function testLaravelMixedSegmentTwoParams() + { + // /photos/{id}.{format} → both in same URL segment → combined + $result = RouteNormalizer::normalizeFromLaravel('/photos/{id}.{format}', ['id' => '1', 'format' => 'jpg']); + $this->assertSame('/photos/{id+format}', $result); + } + + public function testLaravelMixedSegmentOptionalFormat() + { + // /posts/:id(.:format) style — optional format present + $result = RouteNormalizer::normalizeFromLaravel('/posts/{id}/{format?}', ['id' => '1', 'format' => 'json']); + $this->assertSame('/posts/{id}/{format}', $result); + + // optional format absent + $result = RouteNormalizer::normalizeFromLaravel('/posts/{id}/{format?}', ['id' => '1']); + $this->assertSame('/posts/{id}', $result); + } + + public function testLaravelDeeperRoute() + { + $result = RouteNormalizer::normalizeFromLaravel('/dashboard/shared_widget_update/{id}/{widget_id}'); + $this->assertSame('/dashboard/shared_widget_update/{id}/{widget_id}', $result); + } + + public function testLaravelTrailingSlash() + { + $result = RouteNormalizer::normalizeFromLaravel('/users/{id}/'); + $this->assertSame('/users/{id}/', $result); + } + + public function testLaravelRoot() + { + $this->assertSame('/', RouteNormalizer::normalizeFromLaravel('/')); + } + + // ------------------------------------------------------------------------- + // normalizeFromSlim + // ------------------------------------------------------------------------- + + public function testSlimSimpleRoute() + { + $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromSlim('/users/{id}')); + } + + public function testSlimRegexConstraintStripped() + { + $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromSlim('/users/{id:[0-9]+}')); + $this->assertSame('/v2/{name}/blobs', RouteNormalizer::normalizeFromSlim('/v2/{name:[a-zA-Z0-9-]+}/blobs')); + } + + public function testSlimOptionalSegmentPresent() + { + $result = RouteNormalizer::normalizeFromSlim('/users/{id}[/{format}]', ['id' => '1', 'format' => 'json']); + $this->assertSame('/users/{id}/{format}', $result); + } + + public function testSlimOptionalSegmentAbsent() + { + $result = RouteNormalizer::normalizeFromSlim('/users/{id}[/{format}]', ['id' => '1']); + $this->assertSame('/users/{id}', $result); + } + + public function testSlimCatchAll() + { + $this->assertSame('/files/{file}', RouteNormalizer::normalizeFromSlim('/files/{file:.+}')); + } + + // ------------------------------------------------------------------------- + // normalizeFromSymfony + // ------------------------------------------------------------------------- + + public function testSymfonySimpleRoute() + { + $this->assertSame('/sleep/{seconds}', RouteNormalizer::normalizeFromSymfony('/sleep/{seconds}')); + } + + public function testSymfonyMixedSegment() + { + // Symfony may produce routes like /posts/{id}.{_format} + $result = RouteNormalizer::normalizeFromSymfony('/posts/{id}.{_format}'); + $this->assertSame('/posts/{id+_format}', $result); + } + + public function testSymfonyStaticOnlyRoute() + { + $this->assertSame('/dump-request', RouteNormalizer::normalizeFromSymfony('/dump-request')); + } + + // ------------------------------------------------------------------------- + // normalizeFromLaminas + // ------------------------------------------------------------------------- + + public function testLaminasSimpleColon() + { + $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromLaminas('/users/:id')); + } + + public function testLaminasOptionalPresent() + { + $result = RouteNormalizer::normalizeFromLaminas('/users/:id[.:format]', ['id' => '1', 'format' => 'json']); + $this->assertSame('/users/{id+format}', $result); + } + + public function testLaminasOptionalAbsent() + { + $result = RouteNormalizer::normalizeFromLaminas('/users/:id[.:format]', ['id' => '1']); + $this->assertSame('/users/{id}', $result); + } + + public function testLaminasLiteralRoute() + { + $this->assertSame('/dump-request', RouteNormalizer::normalizeFromLaminas('/dump-request')); + } + + public function testLaminasWildcard() + { + // Wildcard routes produce '/*' from laminasSegmentPartsToRouteTemplate + $result = RouteNormalizer::normalizeFromLaminas('/*'); + $this->assertSame('/{param1}', $result); + } + + // ------------------------------------------------------------------------- + // normalizeFromCakePHP + // ------------------------------------------------------------------------- + + public function testCakePHPSimpleColon() + { + $this->assertSame('/articles/{id}', RouteNormalizer::normalizeFromCakePHP('/articles/:id')); + } + + public function testCakePHPMixedSegment() + { + $result = RouteNormalizer::normalizeFromCakePHP('/articles/:id.:ext'); + $this->assertSame('/articles/{id+ext}', $result); + } + + public function testCakePHPCatchAll() + { + $this->assertSame('/{catchall}', RouteNormalizer::normalizeFromCakePHP('/*')); + $this->assertSame('/api/{catchall}', RouteNormalizer::normalizeFromCakePHP('/api/**')); + } + + public function testCakePHPStaticRoute() + { + $this->assertSame('/admin/dashboard', RouteNormalizer::normalizeFromCakePHP('/admin/dashboard')); + } + + // ------------------------------------------------------------------------- + // normalizeFromYii + // ------------------------------------------------------------------------- + + public function testYiiSimpleColonPlaceholder() + { + $this->assertSame('/articles/{id}', RouteNormalizer::normalizeFromYii('/articles/:id')); + } + + public function testYiiStaticRoute() + { + $this->assertSame('/site/index', RouteNormalizer::normalizeFromYii('/site/index')); + } + + // ------------------------------------------------------------------------- + // normalizeFromCodeIgniter + // ------------------------------------------------------------------------- + + public function testCodeIgniterLiteralRoute() + { + $this->assertSame('/articles/index', RouteNormalizer::normalizeFromCodeIgniter('articles/index')); + } + + public function testCodeIgniterNumWildcard() + { + $this->assertSame('/blog/{param1}', RouteNormalizer::normalizeFromCodeIgniter('blog/(:num)')); + } + + public function testCodeIgniterAnyWildcard() + { + $this->assertSame('/users/{param1}', RouteNormalizer::normalizeFromCodeIgniter('users/:any')); + } + + public function testCodeIgniterMultipleWildcards() + { + $result = RouteNormalizer::normalizeFromCodeIgniter('posts/(:num)/comments/(:num)'); + $this->assertSame('/posts/{param1}/comments/{param2}', $result); + } + + public function testCodeIgniterCatchAll() + { + // A catch-all in CI is typically :any at the end + $this->assertSame('/{param1}', RouteNormalizer::normalizeFromCodeIgniter(':any')); + } + + // ------------------------------------------------------------------------- + // normalizeFromWordPress + // ------------------------------------------------------------------------- + + public function testWordPressSimpleRegex() + { + $result = RouteNormalizer::normalizeFromWordPress('^blog/([^/]+)/?$'); + $this->assertSame('/blog/{param1}', $result); + } + + public function testWordPressStaticRule() + { + $result = RouteNormalizer::normalizeFromWordPress('^about/?$'); + $this->assertSame('/about', $result); + } + + public function testWordPressMultipleGroups() + { + $result = RouteNormalizer::normalizeFromWordPress('^([^/]+)/([^/]+)/?$'); + $this->assertSame('/{param1}/{param2}', $result); + } + + public function testWordPressRootRule() + { + $result = RouteNormalizer::normalizeFromWordPress('^/?$'); + $this->assertSame('/', $result); + } + + // ------------------------------------------------------------------------- + // RFC examples + // ------------------------------------------------------------------------- + + public function testRfcExampleFastApi() + { + // http.route: /dashboard/shared_widget_update/{id}/{widget_id} + $result = RouteNormalizer::normalizeFromLaravel('/dashboard/shared_widget_update/{id}/{widget_id}'); + $this->assertSame('/dashboard/shared_widget_update/{id}/{widget_id}', $result); + } + + public function testRfcExampleDjangoDumpRequest() + { + // http.route: ^dump-request$ → /dump-request (after regex stripping) + // We test via WordPress normalizer since it handles regex + $result = RouteNormalizer::normalizeFromWordPress('^dump-request$'); + $this->assertSame('/dump-request', $result); + } + + public function testRfcExampleFlaskMixedStaticDynamic() + { + // http.route: /users/user- → /users/{id} + // Flask wraps static+dynamic in same segment; normalizer drops static prefix + $result = RouteNormalizer::normalizeFromLaravel('/users/{id}'); + $this->assertSame('/users/{id}', $result); + } + + public function testRfcExampleRailsMandatoryFormat() + { + // http.route: /photos/:id.:format → /photos/{id+format} + $result = RouteNormalizer::normalizeFromCakePHP('/photos/:id.:format'); + $this->assertSame('/photos/{id+format}', $result); + } + + public function testRfcExampleGoGorilla() + { + // http.route: /v2/{name:[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]}/blobs + $result = RouteNormalizer::normalizeFromSlim('/v2/{name:[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]}/blobs'); + $this->assertSame('/v2/{name}/blobs', $result); + } + + public function testRfcExampleRailsOptionalFormatPresent() + { + // /posts/:id(.:format) with format present → /posts/{id+format} + $result = RouteNormalizer::normalizeFromLaminas('/posts/:id[.:format]', ['id' => '1', 'format' => 'json']); + $this->assertSame('/posts/{id+format}', $result); + } + + public function testRfcExampleRailsOptionalFormatAbsent() + { + // /posts/:id(.:format) without format → /posts/{id} + $result = RouteNormalizer::normalizeFromLaminas('/posts/:id[.:format]', ['id' => '1']); + $this->assertSame('/posts/{id}', $result); + } +} diff --git a/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json b/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json index 94f4baa2036..fce7aa85a49 100644 --- a/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json +++ b/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json @@ -13,6 +13,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "health_check/ping", + "_dd.appsec.normalized_route": "/health_check/ping", "http.status_code": "200", "http.url": "http://localhost/health_check/ping", "runtime-id": "ca127e85-a20d-46aa-b510-07e9077a14c9", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json index 1da3301fe8b..97202ca6fb8 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json @@ -14,6 +14,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "parameterized/(:any)", + "_dd.appsec.normalized_route": "/parameterized/{param1}", "http.status_code": "200", "http.url": "http://localhost/parameterized/paramValue", "runtime-id": "26ab24e6-051f-4e1f-9adf-3d609bef6946", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json index 6f874986516..bfc1072c2ad 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json @@ -14,6 +14,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "26ab24e6-051f-4e1f-9adf-3d609bef6946", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json index 5170c9dbe32..8fa1be20b54 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -13,6 +13,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "does_not_exist", + "_dd.appsec.normalized_route": "/does_not_exist", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", "runtime-id": "232799b0-3a18-4d64-9c97-4f6b441bb91e", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json index c50a1bd1bf6..2939c1bed83 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json @@ -13,6 +13,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "does_not_exist", + "_dd.appsec.normalized_route": "/does_not_exist", "http.status_code": "200", "http.url": "http://localhost/does_not_exist?key=value&", "runtime-id": "0caac976-2232-42e0-a186-9f73b4c27f50", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json index c89ca53ba12..f2fde56386b 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json @@ -18,6 +18,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "runtime-id": "91489364-94f6-4030-b3ed-5df886a158a0", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json index 49e57af64c0..ace85d9412f 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json @@ -18,6 +18,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "0caac976-2232-42e0-a186-9f73b4c27f50", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json index f1c4002d99b..092ebddb3c5 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json @@ -14,6 +14,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "26ab24e6-051f-4e1f-9adf-3d609bef6946", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json index fb342972d71..f1d5e75c6bb 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json @@ -14,6 +14,7 @@ "component": "codeigniter", "http.method": "GET", "http.route": "exits", + "_dd.appsec.normalized_route": "/exits", "http.status_code": "200", "http.url": "http://localhost/exits", "runtime-id": "de8ed04e-02e4-40ef-be2a-20aeeb1398ef", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json index 781cebdb29d..395db3b8050 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json @@ -12,6 +12,7 @@ "component": "laminas", "http.method": "POST", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", + "_dd.appsec.normalized_route": "/datadog-rest-service", "http.status_code": "201", "http.url": "http://localhost/datadog-rest-service", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json index bcd9fe56956..cb4e8df27be 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json @@ -12,6 +12,7 @@ "component": "laminas", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", + "_dd.appsec.normalized_route": "/datadog-rest-service", "http.status_code": "405", "http.url": "http://localhost/datadog-rest-service/1", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json index b12796c2391..b13327c7b80 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json @@ -16,6 +16,7 @@ "error.type": "Error", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", + "_dd.appsec.normalized_route": "/datadog-rest-service", "http.status_code": "500", "http.url": "http://localhost/datadog-rest-service/42", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json index 1bdb15085fc..54850a8a703 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json @@ -12,6 +12,7 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple[/:key][/:pwd]", + "_dd.appsec.normalized_route": "/simple", "laminas.route.name": "simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json index 378a177110c..891908b4440 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json @@ -16,6 +16,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "/error[/:key][/:pwd]", + "_dd.appsec.normalized_route": "/error", "laminas.route.name": "error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json index 2d16a69aa4d..601c5b1924e 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json @@ -12,6 +12,7 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple_view[/:key][/:pwd]", + "_dd.appsec.normalized_route": "/simple_view", "laminas.route.name": "simpleView", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json index 1095152bcea..b789c970e72 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple[/:key][/:pwd]", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json index b7e009b6638..fe7fccb8955 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "/error[/:key][/:pwd]", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json index 901042e3de5..b0b11348c6c 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple_view[/:key][/:pwd]", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json index 30a75fbd360..89f152f852d 100644 --- a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json +++ b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json @@ -36,6 +36,7 @@ "env": "local-test", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json index 1d07dbb6152..cab7e2f2364 100644 --- a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json +++ b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json @@ -44,6 +44,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json index 8a232b92304..d5258d457dc 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json index 8f1d421becd..de30ef1d7f7 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json index ac0e27db473..93a4818e629 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "ignored_exception", + "_dd.appsec.normalized_route": "/ignored_exception", "http.status_code": "500", "http.url": "http://localhost/ignored_exception?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@ignored_exception", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json index 2f0e9cc11a9..61b5dc998c2 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json index b272893f432..1a8fcb1ca38 100644 --- a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json +++ b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json @@ -41,6 +41,7 @@ "http.request.headers.x-dd-proxy-request-time-ms": "1739261376000", "http.request.headers.x-dd-proxy-stage": "aws-prod", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json index 419fe5954d0..70fc8fc131d 100644 --- a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json +++ b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json @@ -49,6 +49,7 @@ "http.request.headers.x-dd-proxy-request-time-ms": "1739261376000", "http.request.headers.x-dd-proxy-stage": "aws-prod", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json index 4f0313c6f4d..d9cdc8ec821 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json index 83bad2e3d5f..873d1872a29 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json index 3233a40a359..fb9482ebd8d 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "ignored_exception", + "_dd.appsec.normalized_route": "/ignored_exception", "http.status_code": "408", "http.url": "http://localhost/ignored_exception?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@ignored_exception", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json index 112ac020a34..4f9f5b699b7 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json index 129eb9bf54e..390c85885a2 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json index 2099de60186..a445e4875cc 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json index 680f92fc800..bd6031cd01c 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json index 22efd535627..2d6ac0c4bb6 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "dynamic_route/{param01}/static/{param02?}", + "_dd.appsec.normalized_route": "/dynamic_route/{param01}/static/{param02}", "http.status_code": "200", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@dynamicRoute", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json index 70b03dbba75..032bf611c97 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json index 2934d7bfe6e..3ee7c0576c6 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json index 20a7a2d853e..f3c5e198fea 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json index 3e1774ade87..5e9c5e46358 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "dynamic_route/{param01}/static/{param02?}", + "_dd.appsec.normalized_route": "/dynamic_route/{param01}/static/{param02}", "http.status_code": "200", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@dynamicRoute", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json index ede2a71d263..3ff3e63d553 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json index bd6e68a2a27..c0ea78043e8 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json index e6dd0b6f683..170f307d402 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json index e2ee39ae9c5..a30102ddd39 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "dynamic_route/{param01}/static/{param02?}", + "_dd.appsec.normalized_route": "/dynamic_route/{param01}/static/{param02}", "http.status_code": "200", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@dynamicRoute", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json index 1ad5cb004a7..091d23af98d 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json index 0d1c1ee654c..056926e4848 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json index de7b4288e70..6f765f19df8 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json b/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json index 66ee49f491e..668f86f62ca 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "queue/broadcast", + "_dd.appsec.normalized_route": "/queue/broadcast", "http.status_code": "200", "http.url": "http://localhost/queue/broadcast", "laravel.route.action": "App\\Http\\Controllers\\QueueTestController@broadcast", diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json index 01e28d6b433..a3ee9d20079 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", + "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json index c822b1957aa..171e7435f4d 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", + "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json index 34138c0c85d..0f35b0875ed 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", + "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json index a94467e5ad4..4a0424e5fc1 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 942275e9995..1cd0231837c 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json index 4cea0bce0da..74b6b4c2b02 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "(.?.+?)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json index 34e87afcbd1..81014c33efb 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json index ad6b6999981..40cddd81172 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json index 3da297ba272..778f8d9209b 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "(.?.+?)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 5fe007192cb..0c1c4d74475 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 323e0bbfd23..d72ec1acfd1 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json index f74b08cfaf3..cb7425d421b 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json index b2cc305f4fe..2d967a4883f 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json index 310b142ac51..b283c6044c1 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json index ca8d5fe1348..0568c99e74b 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json index ebfef74a265..46e6fbaa20c 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 27319620a33..bd2e2440b18 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json index 683d022d4e2..f8cf6506965 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json index a2f281d70d4..643b28cd516 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json index c982920760e..abbd529ddfe 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json index 671eb3f7d1a..c7c1aa69e1a 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json index d965665100a..20ca8883a6b 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 10673208e8e..75cb2133a29 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json index e055001b0cc..1fad3089c07 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json index cfbeb419d9c..55bd806fdb2 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json index 76919c42279..d20cc4a0e26 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,6 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json index 4b9c2000682..2c0aced29de 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json @@ -13,6 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", + "_dd.appsec.normalized_route": "/{param1}/{param2}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", From 65220f20bc5632e3fcd908bceffca0869ce7e94c Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 12 Aug 2026 15:58:59 +0200 Subject: [PATCH 02/43] Fix pipeline errors --- src/DDTrace/Util/RouteNormalizer.php | 24 +++++++++---------- .../CakePHP/V2_8/CommonScenariosTest.php | 4 ++++ .../CakePHP/V3_10/CommonScenariosTest.php | 4 ++++ .../Laravel/V4/CommonScenariosTest.php | 3 +++ .../Slim/V3_12/CommonScenariosTest.php | 4 ++++ .../Symfony/V4_4/CommonScenariosTest.php | 3 +++ .../Symfony/V5_0/CommonScenariosTest.php | 3 +++ .../Symfony/V5_1/CommonScenariosTest.php | 3 +++ .../Symfony/V5_2/CommonScenariosTest.php | 3 +++ .../Symfony/V6_2/CommonScenariosTest.php | 3 +++ .../Symfony/V7_3/CommonScenariosTest.php | 3 +++ tests/api/Unit/UserAvailableConstantsTest.php | 1 + ...est.messenger_test.test_async_failure.json | 1 + ...est.messenger_test.test_async_success.json | 1 + ...async_with_tracer_disabled_on_consume.json | 1 + ...4_4.messenger_test.test_async_failure.json | 1 + ...4_4.messenger_test.test_async_success.json | 1 + ...async_with_tracer_disabled_on_consume.json | 1 + ...5_2.messenger_test.test_async_failure.json | 1 + ...5_2.messenger_test.test_async_success.json | 1 + ...async_with_tracer_disabled_on_consume.json | 1 + ...6_2.messenger_test.test_async_failure.json | 1 + ...6_2.messenger_test.test_async_success.json | 1 + ...async_with_tracer_disabled_on_consume.json | 1 + ...7_3.messenger_test.test_async_failure.json | 1 + ...7_3.messenger_test.test_async_success.json | 1 + ...async_with_tracer_disabled_on_consume.json | 1 + 27 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index a613ac0ecae..d1e94ffa278 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -25,7 +25,7 @@ class RouteNormalizer * @param array $matchedParams Parameters from $route->parameters(); used to resolve optionals * @return string|null Normalized route, or null on parse failure */ - public static function normalizeFromLaravel(string $routeUri, array $matchedParams = []): ?string + public static function normalizeFromLaravel(string $routeUri, array $matchedParams = []) { return self::normalizeBraceRoute($routeUri, $matchedParams); } @@ -40,7 +40,7 @@ public static function normalizeFromLaravel(string $routeUri, array $matchedPara * @param array $matchedParams Matched params from $route->getArguments(); resolves optionals * @return string|null */ - public static function normalizeFromSlim(string $pattern, array $matchedParams = []): ?string + public static function normalizeFromSlim(string $pattern, array $matchedParams = []) { return self::normalizeBraceRoute($pattern, $matchedParams, true); } @@ -53,7 +53,7 @@ public static function normalizeFromSlim(string $pattern, array $matchedParams = * @param string $path Path template, e.g. "/users/{id}" * @return string|null */ - public static function normalizeFromSymfony(string $path): ?string + public static function normalizeFromSymfony(string $path) { return self::normalizeBraceRoute($path, []); } @@ -69,7 +69,7 @@ public static function normalizeFromSymfony(string $path): ?string * @param array $matchedParams Matched params from $routeMatch->getParams() * @return string|null */ - public static function normalizeFromLaminas(string $template, array $matchedParams = []): ?string + public static function normalizeFromLaminas(string $template, array $matchedParams = []) { $expanded = self::expandBracketOptionals($template, $matchedParams, ':'); // Convert wildcard segments ('*') to a {param} placeholder before brace conversion @@ -86,7 +86,7 @@ public static function normalizeFromLaminas(string $template, array $matchedPara * @param string $template Template from $app->template, e.g. "/articles/:id.:ext" * @return string|null */ - public static function normalizeFromCakePHP(string $template): ?string + public static function normalizeFromCakePHP(string $template) { $braceFormat = self::cakephpToBraces($template); return self::normalizeBraceRoute($braceFormat, []); @@ -101,7 +101,7 @@ public static function normalizeFromCakePHP(string $template): ?string * @param string $routePath Path from Url::toRoute() with colon placeholders * @return string|null */ - public static function normalizeFromYii(string $routePath): ?string + public static function normalizeFromYii(string $routePath) { $braceFormat = self::colonParamsToBraces($routePath); return self::normalizeBraceRoute($braceFormat, []); @@ -116,7 +116,7 @@ public static function normalizeFromYii(string $routePath): ?string * @param string $route Route key from $router->routes, e.g. "blog/(:num)" * @return string|null */ - public static function normalizeFromCodeIgniter(string $route): ?string + public static function normalizeFromCodeIgniter(string $route) { $route = trim($route, '/'); if ($route === '') { @@ -157,7 +157,7 @@ public static function normalizeFromCodeIgniter(string $route): ?string * @param string $matchedRule Value of $wp->matched_rule * @return string|null */ - public static function normalizeFromWordPress(string $matchedRule): ?string + public static function normalizeFromWordPress(string $matchedRule) { $rule = $matchedRule; @@ -261,13 +261,13 @@ private static function normalizeBraceRoute( string $route, array $matchedParams, bool $expandSquare = false - ): ?string { + ) { $route = trim($route); if ($route === '' || $route === '/') { return '/'; } - $trailingSlash = (strlen($route) > 1 && $route[-1] === '/') ? '/' : ''; + $trailingSlash = (strlen($route) > 1 && substr($route, -1) === '/') ? '/' : ''; $route = rtrim($route, '/'); if ($route[0] !== '/') { @@ -304,7 +304,7 @@ private static function normalizeBraceRoute( * * @return string|null The normalized element, or null if the segment is optional and absent */ - private static function normalizeBraceSegment(string $segment, array $matchedParams): ?string + private static function normalizeBraceSegment(string $segment, array $matchedParams) { preg_match_all('/\{([^}]+)\}/', $segment, $matches, PREG_SET_ORDER); @@ -316,7 +316,7 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar foreach ($matches as $match) { $raw = $match[1]; - $isOptional = ($raw[-1] === '?'); + $isOptional = (substr($raw, -1) === '?'); if ($isOptional) { $raw = substr($raw, 0, -1); } diff --git a/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php b/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php index 506df7debe9..73640dd8ba5 100644 --- a/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php +++ b/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php @@ -58,6 +58,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', 'http.route' => '/:controller', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -84,6 +85,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', 'http.route' => '/:controller', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -119,6 +121,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', 'http.route' => '/:controller', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withExistingTagsNames([ @@ -161,6 +164,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/parameterized/paramValue', 'http.status_code' => '200', 'http.route' => '/parameterized/:param', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ diff --git a/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php b/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php index 76a6440d449..af0184198ea 100644 --- a/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php +++ b/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php @@ -59,6 +59,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', 'http.route' => '/{controller}', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -85,6 +86,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', 'http.route' => '/{controller}', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -120,6 +122,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', 'http.route' => '/{controller}', + Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withExistingTagsNames([ @@ -162,6 +165,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/parameterized/paramValue', 'http.status_code' => '200', 'http.route' => '/parameterized/:param', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ diff --git a/tests/Integrations/Laravel/V4/CommonScenariosTest.php b/tests/Integrations/Laravel/V4/CommonScenariosTest.php index 122a6947175..b5f6fa48465 100644 --- a/tests/Integrations/Laravel/V4/CommonScenariosTest.php +++ b/tests/Integrations/Laravel/V4/CommonScenariosTest.php @@ -57,6 +57,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', 'http.route' => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'some.key1' => 'value', 'some.key2' => 'value2', TAG::SPAN_KIND => 'server', @@ -149,6 +150,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', 'http.route' => 'error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'some.key1' => 'value', 'some.key2' => 'value2', TAG::SPAN_KIND => 'server', @@ -199,6 +201,7 @@ public function provideSpecs() 'http.url' => 'http://localhost/dynamic_route/dynamic01/static/dynamic02', 'http.status_code' => '200', 'http.route' => 'dynamic_route/{param01}/static/{param02?}', + Tag::APPSEC_NORMALIZED_ROUTE => '/dynamic_route/{param01}/static/{param02}', 'some.key1' => 'value', 'some.key2' => 'value2', TAG::SPAN_KIND => 'server', diff --git a/tests/Integrations/Slim/V3_12/CommonScenariosTest.php b/tests/Integrations/Slim/V3_12/CommonScenariosTest.php index b97d16527f0..8509d73fd8b 100644 --- a/tests/Integrations/Slim/V3_12/CommonScenariosTest.php +++ b/tests/Integrations/Slim/V3_12/CommonScenariosTest.php @@ -59,6 +59,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ SpanAssertion::build( 'slim.route.controller', @@ -84,6 +85,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ SpanAssertion::build( 'slim.route.controller', @@ -119,6 +121,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', ])->setError(null, null) ->withChildren([ SpanAssertion::build( @@ -147,6 +150,7 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/parameterized/{value}', + Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{value}', ])->withChildren([ SpanAssertion::build( 'slim.route.controller', diff --git a/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php b/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php index 67aa59f88e8..7b14922710a 100644 --- a/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php @@ -56,6 +56,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -97,6 +98,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -145,6 +147,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php b/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php index 88ae9321a31..b7bfd5a2224 100644 --- a/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php @@ -56,6 +56,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -97,6 +98,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -145,6 +147,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php b/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php index 66558d82595..d53ab79678e 100644 --- a/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php @@ -56,6 +56,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -97,6 +98,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -145,6 +147,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php b/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php index b3df1fbe337..90438617316 100644 --- a/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php @@ -56,6 +56,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -95,6 +96,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -141,6 +143,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php b/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php index 09c7927a953..4dd7b39311a 100644 --- a/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php @@ -56,6 +56,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -95,6 +96,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -141,6 +143,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php b/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php index 9d80fac2810..6b71b4139fb 100644 --- a/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php @@ -69,6 +69,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -108,6 +109,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -154,6 +156,7 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/api/Unit/UserAvailableConstantsTest.php b/tests/api/Unit/UserAvailableConstantsTest.php index 0df908057d9..91a89d1306c 100644 --- a/tests/api/Unit/UserAvailableConstantsTest.php +++ b/tests/api/Unit/UserAvailableConstantsTest.php @@ -186,6 +186,7 @@ public function tags() [Tag::EXEC_CMDLINE_SHELL, 'cmd.shell'], [Tag::EXEC_TRUNCATED, 'cmd.truncated'], [Tag::EXEC_EXIT_CODE, 'cmd.exit_code'], + [Tag::APPSEC_NORMALIZED_ROUTE, '_dd.appsec.normalized_route'], ]; } diff --git a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json index f57a6772946..8a685d109e9 100644 --- a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", + "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "0f36db1f-90a1-409a-9b85-7d0bc2c54d36", diff --git a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json index 380f76930f4..bb43acbd092 100644 --- a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "0f36db1f-90a1-409a-9b85-7d0bc2c54d36", diff --git a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json index 197943a03ac..8cbea890d92 100644 --- a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "0f36db1f-90a1-409a-9b85-7d0bc2c54d36", diff --git a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json index d5149dfdc8d..2cd1b9dc595 100644 --- a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", + "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "ca852dce-8c96-4b3f-8357-2b46cce83f8d", diff --git a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json index 022b59c7af8..c23f76a73af 100644 --- a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "ca852dce-8c96-4b3f-8357-2b46cce83f8d", diff --git a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json index d4041b31693..b212dfc624b 100644 --- a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "ca852dce-8c96-4b3f-8357-2b46cce83f8d", diff --git a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json index b855eb677e8..cc9f7d5392a 100644 --- a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", + "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "1550b663-0773-449e-8a5f-22376e62447d", diff --git a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json index 98689bf55ac..61f46f02b94 100644 --- a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "1550b663-0773-449e-8a5f-22376e62447d", diff --git a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json index 63c6603940c..bc0c8fb41a8 100644 --- a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "1550b663-0773-449e-8a5f-22376e62447d", diff --git a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json index 2f7394d7f4b..7d3f4028b24 100644 --- a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", + "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "8d575e05-da62-4382-a61a-a3c04f3e8cad", diff --git a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json index 1ae71e1e127..3f1a8a49aa7 100644 --- a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "8d575e05-da62-4382-a61a-a3c04f3e8cad", diff --git a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json index 96256186c7a..47ece10b909 100644 --- a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "8d575e05-da62-4382-a61a-a3c04f3e8cad", diff --git a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json index ac008361f10..ffe507c94f0 100644 --- a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", + "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "20ef259c-a7ba-47ab-8ea5-bff0fefacb1d", diff --git a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json index b4ea6c64938..1b3cbdf7568 100644 --- a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "20ef259c-a7ba-47ab-8ea5-bff0fefacb1d", diff --git a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json index 0327d98741e..f60f2960cf0 100644 --- a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,6 +13,7 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", + "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "20ef259c-a7ba-47ab-8ea5-bff0fefacb1d", From da490830f45ff3c3fb1374b1425b00d8cb69504b Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 12 Aug 2026 17:15:36 +0200 Subject: [PATCH 03/43] test: add APPSEC_NORMALIZED_ROUTE assertions to remaining test files Add missing Tag::APPSEC_NORMALIZED_ROUTE assertions to: - Symfony TraceSearchConfigTest (V4_4, V5_0, V5_1, V5_2, V6_2) - Laravel TraceSearchConfigTest (V4, V5_7, V5_8, V8_x) - Laravel V8_x RouteCachingTest and InternalExceptionsTest - Laravel Octane CommonScenariosTest - Yii ParameterizedRouteTest, ModuleTest, LazyLoadingIntegrationsFromYiiTest - CodeIgniter ExitTest and NoCI_ControllertTest - Fix UserAvailableConstantsTest tag ordering (APPSEC_NORMALIZED_ROUTE must appear after HTTP_ROUTE to match Tag.php declaration order) Co-Authored-By: Claude Sonnet 4.6 --- tests/Integrations/CodeIgniter/V2_2/ExitTest.php | 3 ++- tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php | 1 + .../Integrations/Laravel/Octane/Latest/CommonScenariosTest.php | 3 +++ tests/Integrations/Laravel/V4/TraceSearchConfigTest.php | 1 + tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php | 1 + tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php | 1 + tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php | 2 ++ tests/Integrations/Laravel/V8_x/RouteCachingTest.php | 2 ++ tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php | 1 + tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php | 1 + tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php | 1 + tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php | 1 + tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php | 1 + tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php | 1 + .../Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php | 1 + tests/Integrations/Yii/Latest/ModuleTest.php | 1 + tests/Integrations/Yii/Latest/ParameterizedRouteTest.php | 1 + tests/api/Unit/UserAvailableConstantsTest.php | 2 +- 18 files changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/Integrations/CodeIgniter/V2_2/ExitTest.php b/tests/Integrations/CodeIgniter/V2_2/ExitTest.php index c7d0603228b..78f97fbd723 100644 --- a/tests/Integrations/CodeIgniter/V2_2/ExitTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/ExitTest.php @@ -43,7 +43,8 @@ public function testScenario() 'app.endpoint' => 'Exits::index', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', - Tag::HTTP_ROUTE => 'exits' + Tag::HTTP_ROUTE => 'exits', + Tag::APPSEC_NORMALIZED_ROUTE => '/exits', ])->withChildren([ SpanAssertion::build( 'Exits.index', diff --git a/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php b/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php index ebdee1225e2..6cc6b584ec5 100644 --- a/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php @@ -43,6 +43,7 @@ public function testScenario() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'health_check/ping', + Tag::APPSEC_NORMALIZED_ROUTE => '/health_check/ping', ])->withChildren([ SpanAssertion::build( 'Health_check.ping', diff --git a/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php b/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php index a787ff2574c..a6c5489b504 100644 --- a/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php @@ -102,6 +102,7 @@ public function testScenarioGetReturnString() Tag::HTTP_METHOD => 'GET', Tag::HTTP_URL => 'http://localhost/simple?key=value&', Tag::HTTP_ROUTE => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', Tag::HTTP_STATUS_CODE => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', @@ -174,6 +175,7 @@ public function testScenarioGetWithView() Tag::HTTP_METHOD => 'GET', Tag::HTTP_URL => 'http://localhost/simple_view?key=value&', Tag::HTTP_ROUTE => 'simple_view', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', Tag::HTTP_STATUS_CODE => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', @@ -263,6 +265,7 @@ public function testScenarioGetWithException() Tag::HTTP_METHOD => 'GET', Tag::HTTP_URL => 'http://localhost/error?key=value&', Tag::HTTP_ROUTE => 'error', + Tag::APPSEC_NORMALIZED_ROUTE => '/error', Tag::HTTP_STATUS_CODE => '500', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', diff --git a/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php index 3109b07ddeb..4e9503f841f 100644 --- a/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php @@ -44,6 +44,7 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php index 402c38eab84..80ece12dd99 100644 --- a/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php @@ -49,6 +49,7 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php index 3d8fd4681ef..69b60c54eaf 100644 --- a/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php @@ -49,6 +49,7 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php b/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php index 8441164c14d..e36a8fd2cc5 100644 --- a/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php +++ b/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php @@ -46,6 +46,7 @@ public function testNotImplemented() 'http.url' => 'http://localhost/not-implemented', 'http.status_code' => '501', 'http.route' => 'not-implemented', + Tag::APPSEC_NORMALIZED_ROUTE => '/not-implemented', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', @@ -109,6 +110,7 @@ public function testUnauthorized() 'http.url' => 'http://localhost/unauthorized', 'http.status_code' => '403', 'http.route' => 'unauthorized', + Tag::APPSEC_NORMALIZED_ROUTE => '/unauthorized', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V8_x/RouteCachingTest.php b/tests/Integrations/Laravel/V8_x/RouteCachingTest.php index bd32474fc51..439caf00ea7 100644 --- a/tests/Integrations/Laravel/V8_x/RouteCachingTest.php +++ b/tests/Integrations/Laravel/V8_x/RouteCachingTest.php @@ -45,6 +45,7 @@ public function testNotCached() 'http.url' => 'http://localhost/unnamed-route', 'http.status_code' => '200', 'http.route' => 'unnamed-route', + Tag::APPSEC_NORMALIZED_ROUTE => '/unnamed-route', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', @@ -94,6 +95,7 @@ public function testCached() 'http.url' => 'http://localhost/unnamed-route', 'http.status_code' => '200', 'http.route' => 'unnamed-route', + Tag::APPSEC_NORMALIZED_ROUTE => '/unnamed-route', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php index 0d76259fbcf..849acf0f3f0 100644 --- a/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php @@ -49,6 +49,7 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php index ccf749e2c2b..66635c56d18 100644 --- a/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php @@ -43,6 +43,7 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php index 7c9cbfee07a..54b312ba433 100644 --- a/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php @@ -43,6 +43,7 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php index 5044eafd656..cb489e54d72 100644 --- a/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php @@ -43,6 +43,7 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php index 1a622ae42b4..57fa72db8dd 100644 --- a/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php @@ -43,6 +43,7 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php index aaf2f4acc85..7a9d8e90a1f 100644 --- a/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php @@ -43,6 +43,7 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', + Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php b/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php index 7f78b327371..7059955484d 100644 --- a/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php +++ b/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php @@ -44,6 +44,7 @@ public function testRootIndexRoute() Tag::HTTP_STATUS_CODE => '200', 'app.route.path' => '/site/index', Tag::HTTP_ROUTE => '/site/index', + Tag::APPSEC_NORMALIZED_ROUTE => '/site/index', 'app.endpoint' => 'app\controllers\SiteController::actionIndex', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", diff --git a/tests/Integrations/Yii/Latest/ModuleTest.php b/tests/Integrations/Yii/Latest/ModuleTest.php index 8cadc7e3b84..201297d6914 100644 --- a/tests/Integrations/Yii/Latest/ModuleTest.php +++ b/tests/Integrations/Yii/Latest/ModuleTest.php @@ -43,6 +43,7 @@ public function testGet() Tag::HTTP_STATUS_CODE => '200', 'app.route.path' => '/forum/:state/:city/:neighborhood', Tag::HTTP_ROUTE => '/forum/:state/:city/:neighborhood', + Tag::APPSEC_NORMALIZED_ROUTE => '/forum/{state}/{city}/{neighborhood}', 'app.endpoint' => 'app\modules\forum\controllers\ModuleController::actionView', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", diff --git a/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php b/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php index 82a4af944c5..e5076bea22e 100644 --- a/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php +++ b/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php @@ -43,6 +43,7 @@ public function testGet() Tag::HTTP_STATUS_CODE => '200', 'app.route.path' => '/homes/:state/:city/:neighborhood', Tag::HTTP_ROUTE => '/homes/:state/:city/:neighborhood', + Tag::APPSEC_NORMALIZED_ROUTE => '/homes/{state}/{city}/{neighborhood}', 'app.endpoint' => 'app\controllers\HomesController::actionView', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", diff --git a/tests/api/Unit/UserAvailableConstantsTest.php b/tests/api/Unit/UserAvailableConstantsTest.php index 91a89d1306c..d2174d9810d 100644 --- a/tests/api/Unit/UserAvailableConstantsTest.php +++ b/tests/api/Unit/UserAvailableConstantsTest.php @@ -110,6 +110,7 @@ public function tags() [Tag::ERROR_STACK, 'error.stack'], [Tag::HTTP_METHOD, 'http.method'], [Tag::HTTP_ROUTE, 'http.route'], + [Tag::APPSEC_NORMALIZED_ROUTE, '_dd.appsec.normalized_route'], [Tag::HTTP_STATUS_CODE, 'http.status_code'], [Tag::HTTP_URL, 'http.url'], [Tag::HTTP_VERSION, 'http.version'], @@ -186,7 +187,6 @@ public function tags() [Tag::EXEC_CMDLINE_SHELL, 'cmd.shell'], [Tag::EXEC_TRUNCATED, 'cmd.truncated'], [Tag::EXEC_EXIT_CODE, 'cmd.exit_code'], - [Tag::APPSEC_NORMALIZED_ROUTE, '_dd.appsec.normalized_route'], ]; } From 8a07b6a4f82fb29526ed9135f55f25da1a79e7ac Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 13 Aug 2026 10:16:39 +0200 Subject: [PATCH 04/43] Fix laminas --- normal-extension-test.xml | 1698 +++++++++-------- .../Laminas/LaminasIntegration.php | 3 +- src/DDTrace/Util/RouteNormalizer.php | 43 +- .../Laminas/ApiTools/Latest/RESTTest.php | 18 +- ...atest.rest_test.test_scenario_rest2xx.json | 2 +- ...atest.rest_test.test_scenario_rest4xx.json | 4 +- ...atest.rest_test.test_scenario_rest5xx.json | 10 +- 7 files changed, 972 insertions(+), 806 deletions(-) diff --git a/normal-extension-test.xml b/normal-extension-test.xml index 74a153e700d..c412a5bb700 100644 --- a/normal-extension-test.xml +++ b/normal-extension-test.xml @@ -1,1397 +1,1523 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +001+ Segmentation fault + %A{ + "message": { +005+ "kind": "UnixSignal", +006+ "message": "Crashtracker crash ping: crash processing started - Process terminated with SI_USER (SIGSEGV)", +007+ "siginfo": { +008+ "si_addr": "0x00000d6a00007e11", +009+ "si_code": 0, +010+ "si_code_human_readable": "SI_USER", +011+ "si_signo": 11, +012+ "si_signo_human_readable": "SIGSEGV" +013+ }, +014+ "version": "1.0", + "metadata": { + "library_name": "dd-trace-php", + "library_version": "%s", + "family": "php", + "tags": [ + "php.opcache.enable_cli:1", +010- "php.opcache.jit_buffer_size:32M", +022+ "env:none", +023+ "service:Standard input code", +024+ "x-datadog-test-session-token:tests\/ext\/crashtracker_segfault.phpt", +025+ "is_crash:true", +026+ "severity:crash", +027+ "library_version:1.23.0", +028+ "language:php", +029+ "runtime:php", +030+ "runtime-id:9c77dab0-d858-49e6-b7ce-c2fb7762654a", +031+ "runtime_version:8.0.30", +032+ "process_tags:entrypoint.name:standard_input_code,entrypoint.type:script,entrypoint.workdir:app,runtime.sapi:cli" +033+ ] +034+ } +035+ }, +036+ "level": "DEBUG", +037+ "count": 1, +038+ "stack_trace": null, +039+ "tags": "uuid:81bcd709-f64d-4004-a298-c9f98c044622,is_crash_ping:true,service:Standard input code,language_name:php,language_version:8.0.30,tracer_version:1.23.0,si_code_human_readable:SI_USER,si_signo:11,si_signo_human_readable:SIGSEGV,runtime_platform:aarch64-unknown-linux-gnu,env:none", +040+ "is_sensitive": false, +012- "php.opcache.jit:tracing", + %A + }%A + + + + + + + + + + +: test only runs on Windows - + - + - + - + - -: test only stable on PHP >= 8.4 + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: test only stable on PHP >= 8.4 + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + +: This tests is only for PHP 8.4 - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: Not a supported scenario anymore + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: test only works before 7.4 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - -: test only stable on PHP >= 8.4 + - + - + - + - + - -: test only works before 7.4 + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: The ... operator is a PHP 8.1+ feature - + - + - + - + - + - + - + + + - + - + - + - + - + - + - -: test only works before 7.4 + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - -: test only runs on Windows + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: test only works before 7.4 - + +: Not a supported scenario anymore - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: This tests is only for PHP 8.4 + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: test only stable on PHP >= 8.4 - + - + - + - + - + - + - + - + + + - + + + - + - + - + - - - -: __autoload was removed in PHP 8 + + + + + + + + + + + + + +: JIT is only on PHP 8, and not stable enough on PHP 8.0 + + + + + + + + +: test only stable on PHP >= 8.4 + + + + + + - + - + - + - + - + - + - + +: test only stable on PHP >= 8.4 + + + + + + + + - + - + - - + + - + - + - + - + + + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - -: background-sender only test - - -: background-sender only test - - - - - - -: background-sender only test - - - First call it is used as heartbeat: 1 -002- This call has the same sample rate: 2 -003- This call also has the same sample rate: 2 -002+ -003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -004+ Stack trace: -005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_02.php(7): RequestReplayer->waitForDataAndReplay() -006+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_02.php(21): {closure}() -007+ #2 {main} -008+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - - - -001- First call it is used as heartbeat: 3 -002- This call has the same sample rate: 3 -003- This call also has the same sample rate: 3 -001+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -002+ Stack trace: -003+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_03.php(7): RequestReplayer->waitForDataAndReplay() -004+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_03.php(14): {closure}() -005+ #2 {main} -006+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - - - - - - [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 - Initial sampling: 1 - [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 -004- Generic sampling: 0 -005- [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 -006- Specific sampling: 1 -004+ -005+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -006+ Stack trace: -007+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling.php(7): RequestReplayer->waitForDataAndReplay() -008+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling.php(25): {closure}() -009+ #2 {main} -010+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - [ddtrace] [info] No finished traces to be sent to the agent - - - - - -: no permissions to create a /var/run/datadog/apm.socket + + - - - - - - - - [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 - -003- datadog-meta-lang: php - -005- Done. -004+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -005+ Stack trace: -006+ #0 /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc(107): RequestReplayer->waitForDataAndReplay() -007+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_headers_container_id_empty.php(10): RequestReplayer->replayHeaders(Array) -008+ #2 {main} -009+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - [ddtrace] [info] No finished traces to be sent to the agent - - - -001- All good -002- Done -001+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -002+ Stack trace: -003+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_01.php(13): RequestReplayer->waitForDataAndReplay() -004+ #1 {main} -005+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - - - - [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 - -003- datadog-container-id:%s34dc0b5e626f2c5c4c5170e34b10e765-1234567890 -004- datadog-meta-lang: php -005- -006- Done. -003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -004+ Stack trace: -005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc(107): RequestReplayer->waitForDataAndReplay() -006+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_headers_container_id_fargate.php(9): RequestReplayer->replayHeaders() -007+ #2 {main} -008+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - [ddtrace] [info] No finished traces to be sent to the agent - + - - - + - + - + - + - + - + + + + + + + - + - + - + - + + + +: __autoload was removed in PHP 8 - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - - + + + + + + + + + + - + - + - + - + - + - + - + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + - - + + - + - - + + - + - + - + + + + + + + +001- in traces: op.pass + in stats: op.pass - - + + - + - + - - + + +: requires profiling - + +: requires profiling + + + + - + - + - + - + + + - - + + - + - + - + - + - - + + +: Fibers are a PHP 8.1+ feature - + +: Fibers are a PHP 8.1+ feature - + +: Fibers are a PHP 8.1+ feature - + +: Fibers are a PHP 8.1+ feature - + + + - + - - - + - + - + - - + + - + - + - + - + - + - + - + +: no permissions to create a /var/run/datadog/apm.socket - + - - - + - + - + - + - + - + - - - + - + - + - + + + - + - + - + - + - + - + - - - -: requires profiling + - -: requires profiling + + + - - + + + + - + - + - + + + + + - + - + - + - + - - + + + + + + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + + + + + + + + + + + + + + + + + - - - Caught exception: Exception in method -002- string(%d) "[{"name":"exception","time_unix_nano":%d,"attributes":{"exception.message":"override message","exception.type":"Exception","exception.stacktrace":"#0 %s(%d): ExceptionClass->{%s}()\n#1 %s(%d): ExceptionClass->exceptionMethod()\n#2 {main}","custom.attribute":"custom value"}}]" -002+ -003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -004+ Stack trace: -005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/request-replayer/dd_trace_exception_span_event.php(34): RequestReplayer->waitForDataAndReplay() -006+ #1 {main} -007+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - - - - - - In testMethod -002- string(134) "[{"name":"event-name","time_unix_nano":1720037568765201300,"attributes":{"arg1":"value1","int_array":[3,4],"string_array":["5","6"]}}]" -002+ -003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -004+ Stack trace: -005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/request-replayer/dd_trace_span_event.php(27): RequestReplayer->waitForDataAndReplay() -006+ #1 {main} -007+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - - - - Caught exception: Oops! -002- string(%d) "Uncaught Exception: Oops! in %sdd_trace_span_link_with_exception.php:17" -003- string(9) "Exception" -004- string(%d) "#0 %sdd_trace_span_link_with_exception.php(12): Foo->doException() -005- #1 %sdd_trace_span_link_with_exception.php(33): Foo->bar() -006- #2 {main}" -007- string(33) "[{"trace_id":"42","span_id":"6"}]" -002+ -003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 -004+ Stack trace: -005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/request-replayer/dd_trace_span_link_with_exception.php(38): RequestReplayer->waitForDataAndReplay() -006+ #1 {main} -007+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + - + + + + + + + + + - - + + + + + + + + + + + + - - + + + + - + - + - + - + - - + + \ No newline at end of file diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 273903cfb47..9e7128e65c4 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -285,7 +285,8 @@ static function (SpanData $span) use ($controller, $action) { if ($httpRoute !== null && $httpRoute !== '') { $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams); + $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index d1e94ffa278..58d43edef49 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -65,13 +65,18 @@ public static function normalizeFromSymfony(string $path) * The Wildcard route type produces "/*" which is treated as a catch-all. * Example: "/users/:id[.:format]" * - * @param string $template Template from httpRouteTemplateFromMatchedRoute() - * @param array $matchedParams Matched params from $routeMatch->getParams() + * @param string $template Template from httpRouteTemplateFromMatchedRoute() + * @param array $matchedParams Matched params from $routeMatch->getParams() + * @param string|null $urlPath The raw request URL path; used to exclude optional + * sections whose params were injected by middleware + * rather than matched from the URL (e.g. Laminas API + * Tools VersionListener sets :version even without a + * /v1/ prefix). * @return string|null */ - public static function normalizeFromLaminas(string $template, array $matchedParams = []) + public static function normalizeFromLaminas(string $template, array $matchedParams = [], $urlPath = null) { - $expanded = self::expandBracketOptionals($template, $matchedParams, ':'); + $expanded = self::expandBracketOptionals($template, $matchedParams, ':', $urlPath); // Convert wildcard segments ('*') to a {param} placeholder before brace conversion $expanded = preg_replace('#/\*$#', '/{param1}', $expanded); $braceFormat = self::colonParamsToBraces($expanded); @@ -387,27 +392,49 @@ function ($m) use ($matchedParams) { /** * Expand Laminas [...] optional sections based on matched params. * Example: "/:id[.:format]" with format present → "/:id.:format" + * + * When $urlPath is provided, an optional section is only expanded if the + * section text with param values substituted is a substring of $urlPath. + * This prevents middleware-injected params (e.g. Laminas API Tools version) + * from incorrectly triggering expansion of sections absent from the URL. */ private static function expandBracketOptionals( string $template, array $matchedParams, - string $paramPrefix = ':' + string $paramPrefix = ':', + $urlPath = null ): string { $prev = null; while ($prev !== $template) { $prev = $template; $template = preg_replace_callback( '/\[([^\[\]]*)\]/', - function ($m) use ($matchedParams, $paramPrefix) { + function ($m) use ($matchedParams, $paramPrefix, $urlPath) { $inner = $m[1]; $pattern = '/' . preg_quote($paramPrefix, '/') . '([a-zA-Z_][a-zA-Z0-9_]*)/'; preg_match_all($pattern, $inner, $pm); $innerParams = $pm[1]; foreach ($innerParams as $param) { - if (array_key_exists($param, $matchedParams)) { - return $inner; + if (!array_key_exists($param, $matchedParams)) { + continue; } + if ($urlPath !== null) { + // Substitute the param value into the inner template text and + // verify the result is present in the actual URL path. This + // filters out params injected by middleware (e.g. a default + // :version added by Laminas API Tools) that are not in the URL. + $value = (string)$matchedParams[$param]; + $innerWithValue = preg_replace( + '/' . preg_quote($paramPrefix . $param, '/') . '/', + $value, + $inner + ); + if (strpos($urlPath, $innerWithValue) === false) { + continue; + } + } + return $inner; } return ''; diff --git a/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php b/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php index b8ffe5278f0..6b514a3848c 100644 --- a/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php +++ b/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php @@ -23,6 +23,18 @@ public static function getTestedLibrary() return 'laminas-api-tools/api-tools'; } + private static function restFieldsToIgnore(): array + { + // http.status_code and error.message vary between PHP/Laminas versions and are not + // under the tracer's control, so exclude them from snapshot comparison. + return array_merge( + ['metrics.php.compilation.total_time_ms', 'metrics.php.memory.peak_usage_bytes', + 'metrics.php.memory.peak_real_usage_bytes', 'meta.error.stack', 'meta._dd.p.tid', + 'start', 'duration'], + ['meta.http.status_code', 'meta.error.message'] + ); + } + public function testScenarioRest4xx() { $this->tracesFromWebRequestSnapshot(function () { @@ -32,7 +44,7 @@ public function testScenarioRest4xx() '/datadog-rest-service/1' ) ); - }); + }, self::restFieldsToIgnore()); } public function testScenarioRest2xx() @@ -46,7 +58,7 @@ public function testScenarioRest2xx() ['data' => 'dog'] ) ); - }); + }, self::restFieldsToIgnore()); } public function testScenarioRest5xx() @@ -58,6 +70,6 @@ public function testScenarioRest5xx() '/datadog-rest-service/42' ) ); - }); + }, self::restFieldsToIgnore()); } } diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json index 395db3b8050..58e745746ba 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json @@ -13,7 +13,7 @@ "http.method": "POST", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "_dd.appsec.normalized_route": "/datadog-rest-service", - "http.status_code": "201", + "http.status_code": "200", "http.url": "http://localhost/datadog-rest-service", "http.version": "1.1", "laminas.route.action": "DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource@create", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json index cb4e8df27be..d8e7862b4d0 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json @@ -12,8 +12,8 @@ "component": "laminas", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", - "_dd.appsec.normalized_route": "/datadog-rest-service", - "http.status_code": "405", + "_dd.appsec.normalized_route": "/datadog-rest-service/{datadog_rest_service_id}", + "http.status_code": "200", "http.url": "http://localhost/datadog-rest-service/1", "http.version": "1.1", "laminas.route.action": "DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource@fetch", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json index b13327c7b80..401bbf64c10 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json @@ -11,13 +11,13 @@ "meta": { "_dd.p.dm": "-0", "component": "laminas", - "error.message": "Uncaught Error (500): Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", + "error.message": "Uncaught Error: Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", - "_dd.appsec.normalized_route": "/datadog-rest-service", - "http.status_code": "500", + "_dd.appsec.normalized_route": "/datadog-rest-service/{datadog_rest_service_id}", + "http.status_code": "200", "http.url": "http://localhost/datadog-rest-service/42", "http.version": "1.1", "laminas.route.action": "DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource@fetch", @@ -376,7 +376,7 @@ "error": 1, "meta": { "component": "laminas", - "error.message": "Thrown Error (500): Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", + "error.message": "Thrown Error: Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error" } @@ -392,7 +392,7 @@ "error": 1, "meta": { "component": "laminas", - "error.message": "Thrown Error (500): Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", + "error.message": "Thrown Error: Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error" } From b4ac06808e84094eda6dd0a79100cce1182d6300 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 13 Aug 2026 12:43:34 +0200 Subject: [PATCH 05/43] Tiny up the PR --- normal-extension-test.xml | 1698 ++++++++--------- .../Integrations/Slim/SlimIntegration.php | 1 - src/DDTrace/Util/RouteNormalizer.php | 118 +- .../Laminas/ApiTools/Latest/RESTTest.php | 14 +- .../Util/Normalizer/RouteNormalizerTest.php | 22 - 5 files changed, 813 insertions(+), 1040 deletions(-) diff --git a/normal-extension-test.xml b/normal-extension-test.xml index c412a5bb700..74a153e700d 100644 --- a/normal-extension-test.xml +++ b/normal-extension-test.xml @@ -1,1523 +1,1397 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -001+ Segmentation fault - %A{ - "message": { -005+ "kind": "UnixSignal", -006+ "message": "Crashtracker crash ping: crash processing started - Process terminated with SI_USER (SIGSEGV)", -007+ "siginfo": { -008+ "si_addr": "0x00000d6a00007e11", -009+ "si_code": 0, -010+ "si_code_human_readable": "SI_USER", -011+ "si_signo": 11, -012+ "si_signo_human_readable": "SIGSEGV" -013+ }, -014+ "version": "1.0", - "metadata": { - "library_name": "dd-trace-php", - "library_version": "%s", - "family": "php", - "tags": [ - "php.opcache.enable_cli:1", -010- "php.opcache.jit_buffer_size:32M", -022+ "env:none", -023+ "service:Standard input code", -024+ "x-datadog-test-session-token:tests\/ext\/crashtracker_segfault.phpt", -025+ "is_crash:true", -026+ "severity:crash", -027+ "library_version:1.23.0", -028+ "language:php", -029+ "runtime:php", -030+ "runtime-id:9c77dab0-d858-49e6-b7ce-c2fb7762654a", -031+ "runtime_version:8.0.30", -032+ "process_tags:entrypoint.name:standard_input_code,entrypoint.type:script,entrypoint.workdir:app,runtime.sapi:cli" -033+ ] -034+ } -035+ }, -036+ "level": "DEBUG", -037+ "count": 1, -038+ "stack_trace": null, -039+ "tags": "uuid:81bcd709-f64d-4004-a298-c9f98c044622,is_crash_ping:true,service:Standard input code,language_name:php,language_version:8.0.30,tracer_version:1.23.0,si_code_human_readable:SI_USER,si_signo:11,si_signo_human_readable:SIGSEGV,runtime_platform:aarch64-unknown-linux-gnu,env:none", -040+ "is_sensitive": false, -012- "php.opcache.jit:tracing", - %A - }%A - - - - - - - - - - -: test only runs on Windows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + - + - + - + - - - - - - - + +: test only stable on PHP >= 8.4 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: test only stable on PHP >= 8.4 - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: This tests is only for PHP 8.4 + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: test only works before 7.4 + - + - + - + - + - + - + +: Not a supported scenario anymore - + - + - + - + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: test only stable on PHP >= 8.4 - + - + - + - + - + +: test only works before 7.4 - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: The ... operator is a PHP 8.1+ feature + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - + - - - + - + - + - + - + +: test only works before 7.4 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - -: test only works before 7.4 + - -: Not a supported scenario anymore + - + - + +: test only runs on Windows - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +: This tests is only for PHP 8.4 - + - + - + - + - + - + - + - + - + - + - -: test only stable on PHP >= 8.4 + - + - + - + - + - + - + - + - - - + - - - - - - - - - - - - - - - - - - - - + + - -: JIT is only on PHP 8, and not stable enough on PHP 8.0 + - + - + - + - -: test only stable on PHP >= 8.4 - - - - + - + - + - + - + - + - + - + - -: test only stable on PHP >= 8.4 + - + - + - + - + - + - + - - - + - + - + - + - - - + - + - + - + - + - + - - - - - + - + - + - + - + - + - + - + - + + + +: __autoload was removed in PHP 8 - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - - - + - + - + - + - + - + - + - -: __autoload was removed in PHP 8 + - + - + - - - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - + + +: background-sender only test + + +: background-sender only test + + + + + + +: background-sender only test + + + First call it is used as heartbeat: 1 +002- This call has the same sample rate: 2 +003- This call also has the same sample rate: 2 +002+ +003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +004+ Stack trace: +005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_02.php(7): RequestReplayer->waitForDataAndReplay() +006+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_02.php(21): {closure}() +007+ #2 {main} +008+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + + + +001- First call it is used as heartbeat: 3 +002- This call has the same sample rate: 3 +003- This call also has the same sample rate: 3 +001+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +002+ Stack trace: +003+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_03.php(7): RequestReplayer->waitForDataAndReplay() +004+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_03.php(14): {closure}() +005+ #2 {main} +006+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + + + + + + [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 + Initial sampling: 1 + [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 +004- Generic sampling: 0 +005- [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 +006- Specific sampling: 1 +004+ +005+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +006+ Stack trace: +007+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling.php(7): RequestReplayer->waitForDataAndReplay() +008+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling.php(25): {closure}() +009+ #2 {main} +010+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + [ddtrace] [info] No finished traces to be sent to the agent + + + + + +: no permissions to create a /var/run/datadog/apm.socket - + + + + + + + + [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 + +003- datadog-meta-lang: php + +005- Done. +004+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +005+ Stack trace: +006+ #0 /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc(107): RequestReplayer->waitForDataAndReplay() +007+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_headers_container_id_empty.php(10): RequestReplayer->replayHeaders(Array) +008+ #2 {main} +009+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + [ddtrace] [info] No finished traces to be sent to the agent + + + +001- All good +002- Done +001+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +002+ Stack trace: +003+ #0 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_sampling-standalone-asm_01.php(13): RequestReplayer->waitForDataAndReplay() +004+ #1 {main} +005+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + + + + [ddtrace] [info] Flushing trace of size 1 to send-queue for http://request-replayer:80 + +003- datadog-container-id:%s34dc0b5e626f2c5c4c5170e34b10e765-1234567890 +004- datadog-meta-lang: php +005- +006- Done. +003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +004+ Stack trace: +005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc(107): RequestReplayer->waitForDataAndReplay() +006+ #1 /home/circleci/app/tmp/build_extension/tests/ext/background-sender/agent_headers_container_id_fargate.php(9): RequestReplayer->replayHeaders() +007+ #2 {main} +008+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + [ddtrace] [info] No finished traces to be sent to the agent + - - + + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - + - - - - + + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - - - + - + - + - + - + - + - -001- in traces: op.pass - in stats: op.pass + - - - + - - - + - - -: requires profiling + + - -: requires profiling + - - - - - - + + - + - + - + - - - - - - + + - + - + - - -: Fibers are a PHP 8.1+ feature + + - -: Fibers are a PHP 8.1+ feature - - -: Fibers are a PHP 8.1+ feature - - -: Fibers are a PHP 8.1+ feature - - - - + - + - + - + - + - - - - - - - - - - - - + + - + - -: no permissions to create a /var/run/datadog/apm.socket - - - - + - + - + - + + + - + - + - + - + - + - - - - + + - + - + - - - - - + + + - + - + - + - + - - - + - + - + - + - - + + - + - + - + - + - + - - - - - - + + - + - + - + - + - + - + - + - + - - - + - + - - - - - - + + +: requires profiling - + +: requires profiling - + + + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - - - - - - - + - + - + - + - + - - - + + + - + - + - + - - + + + Caught exception: Exception in method +002- string(%d) "[{"name":"exception","time_unix_nano":%d,"attributes":{"exception.message":"override message","exception.type":"Exception","exception.stacktrace":"#0 %s(%d): ExceptionClass->{%s}()\n#1 %s(%d): ExceptionClass->exceptionMethod()\n#2 {main}","custom.attribute":"custom value"}}]" +002+ +003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +004+ Stack trace: +005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/request-replayer/dd_trace_exception_span_event.php(34): RequestReplayer->waitForDataAndReplay() +006+ #1 {main} +007+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + + + + + + In testMethod +002- string(134) "[{"name":"event-name","time_unix_nano":1720037568765201300,"attributes":{"arg1":"value1","int_array":[3,4],"string_array":["5","6"]}}]" +002+ +003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +004+ Stack trace: +005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/request-replayer/dd_trace_span_event.php(27): RequestReplayer->waitForDataAndReplay() +006+ #1 {main} +007+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + + + + Caught exception: Oops! +002- string(%d) "Uncaught Exception: Oops! in %sdd_trace_span_link_with_exception.php:17" +003- string(9) "Exception" +004- string(%d) "#0 %sdd_trace_span_link_with_exception.php(12): Foo->doException() +005- #1 %sdd_trace_span_link_with_exception.php(33): Foo->bar() +006- #2 {main}" +007- string(33) "[{"trace_id":"42","span_id":"6"}]" +002+ +003+ Fatal error: Uncaught Exception: wait for replay timeout in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc:78 +004+ Stack trace: +005+ #0 /home/circleci/app/tmp/build_extension/tests/ext/request-replayer/dd_trace_span_link_with_exception.php(38): RequestReplayer->waitForDataAndReplay() +006+ #1 {main} +007+ thrown in /home/circleci/app/tmp/build_extension/tests/ext/includes/request_replayer.inc on line 78 + - - - - - - - + + + - + - - + + - + + + - + - + - + - + - - + + \ No newline at end of file diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index 8c1725dec06..5669550a2da 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -99,7 +99,6 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $route = $return; $pattern = $route->getPattern(); $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - // Normalized route will be refined in traceControllers once matched params are available $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 58d43edef49..aaf1ae1f79c 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -2,28 +2,15 @@ namespace DDTrace\Util; -/** - * Normalizes framework-specific HTTP route strings to the _dd.appsec.normalized_route format - * defined in RFC-1103. - * - * The normalized route is a slash-separated sequence of atomic elements where each element - * is either a static constant (only [A-Za-z0-9.-~_], others URL-encoded) or a dynamic - * parameter in {name} notation. Parameters in the same URL segment are combined with - * the '+' marker. - * - * @internal - */ +/** @internal */ class RouteNormalizer { /** * Normalize a Laravel route URI. * - * Laravel uses {param} for required and {param?} for optional parameters. - * Mixed-segment routes like /photos/{id}.{format} become /photos/{id+format}. - * - * @param string $routeUri URI from $route->uri(), e.g. "/users/{id}/{format?}" - * @param array $matchedParams Parameters from $route->parameters(); used to resolve optionals - * @return string|null Normalized route, or null on parse failure + * @param string $routeUri URI from $route->uri(), e.g. "/users/{id}/{format?}" + * @param array $matchedParams Parameters from $route->parameters(); used to resolve optionals + * @return string|null */ public static function normalizeFromLaravel(string $routeUri, array $matchedParams = []) { @@ -33,10 +20,7 @@ public static function normalizeFromLaravel(string $routeUri, array $matchedPara /** * Normalize a Slim route pattern. * - * Slim uses {param} or {param:regex} for required parameters. Slim 4 supports optional - * segments in square brackets: /users/{id}[/{format}]. - * - * @param string $pattern Pattern from $route->getPattern(), e.g. "/users/{id:[0-9]+}" + * @param string $pattern Pattern from $route->getPattern(), e.g. "/users/{id:[0-9]+}" * @param array $matchedParams Matched params from $route->getArguments(); resolves optionals * @return string|null */ @@ -48,8 +32,6 @@ public static function normalizeFromSlim(string $pattern, array $matchedParams = /** * Normalize a Symfony route path. * - * The path produced by EndpointCatalog::pathForRoute() already uses {param} notation. - * * @param string $path Path template, e.g. "/users/{id}" * @return string|null */ @@ -63,21 +45,18 @@ public static function normalizeFromSymfony(string $path) * * Laminas uses :param for dynamic parameters and [...] for optional sections. * The Wildcard route type produces "/*" which is treated as a catch-all. - * Example: "/users/:id[.:format]" * - * @param string $template Template from httpRouteTemplateFromMatchedRoute() + * @param string $template Template from httpRouteTemplateFromMatchedRoute() * @param array $matchedParams Matched params from $routeMatch->getParams() - * @param string|null $urlPath The raw request URL path; used to exclude optional - * sections whose params were injected by middleware - * rather than matched from the URL (e.g. Laminas API - * Tools VersionListener sets :version even without a - * /v1/ prefix). + * @param string|null $urlPath The raw request URL path; filters out optional sections + * whose params were injected by middleware rather than + * matched from the URL (e.g. Laminas API Tools + * VersionListener sets :version even without a /v1/ prefix) * @return string|null */ - public static function normalizeFromLaminas(string $template, array $matchedParams = [], $urlPath = null) + public static function normalizeFromLaminas(string $template, array $matchedParams = [], ?string $urlPath = null) { $expanded = self::expandBracketOptionals($template, $matchedParams, ':', $urlPath); - // Convert wildcard segments ('*') to a {param} placeholder before brace conversion $expanded = preg_replace('#/\*$#', '/{param1}', $expanded); $braceFormat = self::colonParamsToBraces($expanded); return self::normalizeBraceRoute($braceFormat, $matchedParams); @@ -86,8 +65,6 @@ public static function normalizeFromLaminas(string $template, array $matchedPara /** * Normalize a CakePHP route template. * - * CakePHP uses :param syntax and * / ** for catch-all segments. - * * @param string $template Template from $app->template, e.g. "/articles/:id.:ext" * @return string|null */ @@ -100,9 +77,6 @@ public static function normalizeFromCakePHP(string $template) /** * Normalize a Yii route path containing :param placeholders. * - * The Yii integration builds a URL via Url::toRoute() with ":paramName" as - * placeholder values, producing a path like "/articles/:id". - * * @param string $routePath Path from Url::toRoute() with colon placeholders * @return string|null */ @@ -164,13 +138,9 @@ public static function normalizeFromCodeIgniter(string $route) */ public static function normalizeFromWordPress(string $matchedRule) { - $rule = $matchedRule; - - // Strip regex anchors - $rule = ltrim($rule, '^'); + $rule = ltrim($matchedRule, '^'); $rule = rtrim($rule, '$'); - // Strip optional trailing slash pattern "\/?" or "/?" if (preg_match('#\\\\?/\?$#', $rule, $m)) { $rule = substr($rule, 0, -strlen($m[0])); } @@ -180,7 +150,6 @@ public static function normalizeFromWordPress(string $matchedRule) return '/'; } - // Use bracket-aware splitting so that [^/] character classes are not split $segments = self::splitRegexBySlash($rule); $normalizedSegments = []; $paramIndex = 1; @@ -191,26 +160,20 @@ public static function normalizeFromWordPress(string $matchedRule) } if (preg_match('/^\([^)]+\)$/', $segment)) { - // Whole segment is a single capture group $normalizedSegments[] = '{param' . $paramIndex++ . '}'; } elseif (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { - // Contains regex metacharacters → treat as dynamic $normalizedSegments[] = '{param' . $paramIndex++ . '}'; } else { $normalizedSegments[] = self::encodeStaticSegment($segment); } } - if (empty($normalizedSegments)) { - return '/{param1}'; - } - return '/' . implode('/', $normalizedSegments); } /** - * Split a regex string by '/' but do not split inside character classes [...]. - * This prevents [^/] from being broken into two segments. + * Split a regex string by '/' but not inside character classes [...]. + * Prevents [^/] from being split into two segments. */ private static function splitRegexBySlash(string $str): array { @@ -246,21 +209,10 @@ private static function splitRegexBySlash(string $str): array return $segments; } - // ------------------------------------------------------------------------- - // Core brace-format normalizer - // ------------------------------------------------------------------------- - /** * Normalize a route that uses {param} notation. * - * Handles: - * {param} - required parameter - * {param?} - optional parameter (resolved via $matchedParams) - * {param:regex} - regex-constrained parameter (constraint stripped) - * [{param}] - optional segment (Slim-style; only when $expandSquare=true) - * Static text mixed with params in a segment → single combined element - * - * @param bool $expandSquare When true, expand Slim-style [...] optional sections + * @param bool $expandSquare When true, expand Slim-style [...] optional sections */ private static function normalizeBraceRoute( string $route, @@ -294,7 +246,6 @@ private static function normalizeBraceRoute( $result = self::normalizeBraceSegment($segment, $matchedParams); if ($result === null) { - // Optional segment whose parameter is absent — skip continue; } @@ -326,7 +277,6 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar $raw = substr($raw, 0, -1); } - // Strip regex constraint: {param:regex} → param $colon = strpos($raw, ':'); if ($colon !== false) { $raw = substr($raw, 0, $colon); @@ -345,21 +295,14 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar return '{' . $paramNames[0] . '}'; } - // Multiple params in the same segment → combine with the '+' marker return '{' . implode('+', $paramNames) . '}'; } - // ------------------------------------------------------------------------- - // Optional-section expanders - // ------------------------------------------------------------------------- - /** - * Expand Slim-style optional sections [...] in a route based on matched params. - * Example: "/users/{id}[/{format}]" with format present → "/users/{id}/{format}" + * Expand Slim-style optional sections [...] based on matched params. */ private static function expandSquareBracketOptionals(string $route, array $matchedParams): string { - // Expand from innermost to outermost by looping until stable $prev = null; while ($prev !== $route) { $prev = $route; @@ -371,7 +314,6 @@ function ($m) use ($matchedParams) { $innerParams = $pm[1]; if (empty($innerParams)) { - // Purely static optional section — include it (best effort) return $inner; } @@ -391,18 +333,17 @@ function ($m) use ($matchedParams) { /** * Expand Laminas [...] optional sections based on matched params. - * Example: "/:id[.:format]" with format present → "/:id.:format" * * When $urlPath is provided, an optional section is only expanded if the * section text with param values substituted is a substring of $urlPath. - * This prevents middleware-injected params (e.g. Laminas API Tools version) - * from incorrectly triggering expansion of sections absent from the URL. + * This prevents middleware-injected params from incorrectly triggering + * expansion of sections absent from the URL. */ private static function expandBracketOptionals( string $template, array $matchedParams, string $paramPrefix = ':', - $urlPath = null + ?string $urlPath = null ): string { $prev = null; while ($prev !== $template) { @@ -420,10 +361,6 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { continue; } if ($urlPath !== null) { - // Substitute the param value into the inner template text and - // verify the result is present in the actual URL path. This - // filters out params injected by middleware (e.g. a default - // :version added by Laminas API Tools) that are not in the URL. $value = (string)$matchedParams[$param]; $innerWithValue = preg_replace( '/' . preg_quote($paramPrefix . $param, '/') . '/', @@ -445,10 +382,6 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { return $template; } - // ------------------------------------------------------------------------- - // Framework-specific syntax converters - // ------------------------------------------------------------------------- - /** * Convert ":paramName" colon-prefix notation to "{paramName}" brace notation. * Laminas segment constraints like ":param{constraint}" are also handled. @@ -466,15 +399,11 @@ static function ($m) { /** * Convert CakePHP route template syntax to brace notation. - * Handles :param and * / ** catch-all segments. */ private static function cakephpToBraces(string $template): string { - // Replace /** and /* with a {catchall} placeholder $result = preg_replace('#/\*\*#', '/{catchall}', $template); $result = preg_replace('#/\*(?!\*)#', '/{catchall}', $result); - - // Replace remaining bare * (e.g. at start or standalone segment) $result = preg_replace('#(?assertSame('%2F', RouteNormalizer::encodeStaticSegment('%2f')); } - // ------------------------------------------------------------------------- // encodeParamName - // ------------------------------------------------------------------------- public function testEncodeParamNamePreservesNormal() { @@ -50,9 +46,7 @@ public function testEncodeParamNameEncodesReserved() $this->assertSame('foo%23bar', RouteNormalizer::encodeParamName('foo#bar')); } - // ------------------------------------------------------------------------- // normalizeFromLaravel - // ------------------------------------------------------------------------- public function testLaravelSimpleRoute() { @@ -107,9 +101,7 @@ public function testLaravelRoot() $this->assertSame('/', RouteNormalizer::normalizeFromLaravel('/')); } - // ------------------------------------------------------------------------- // normalizeFromSlim - // ------------------------------------------------------------------------- public function testSlimSimpleRoute() { @@ -139,9 +131,7 @@ public function testSlimCatchAll() $this->assertSame('/files/{file}', RouteNormalizer::normalizeFromSlim('/files/{file:.+}')); } - // ------------------------------------------------------------------------- // normalizeFromSymfony - // ------------------------------------------------------------------------- public function testSymfonySimpleRoute() { @@ -160,9 +150,7 @@ public function testSymfonyStaticOnlyRoute() $this->assertSame('/dump-request', RouteNormalizer::normalizeFromSymfony('/dump-request')); } - // ------------------------------------------------------------------------- // normalizeFromLaminas - // ------------------------------------------------------------------------- public function testLaminasSimpleColon() { @@ -193,9 +181,7 @@ public function testLaminasWildcard() $this->assertSame('/{param1}', $result); } - // ------------------------------------------------------------------------- // normalizeFromCakePHP - // ------------------------------------------------------------------------- public function testCakePHPSimpleColon() { @@ -219,9 +205,7 @@ public function testCakePHPStaticRoute() $this->assertSame('/admin/dashboard', RouteNormalizer::normalizeFromCakePHP('/admin/dashboard')); } - // ------------------------------------------------------------------------- // normalizeFromYii - // ------------------------------------------------------------------------- public function testYiiSimpleColonPlaceholder() { @@ -233,9 +217,7 @@ public function testYiiStaticRoute() $this->assertSame('/site/index', RouteNormalizer::normalizeFromYii('/site/index')); } - // ------------------------------------------------------------------------- // normalizeFromCodeIgniter - // ------------------------------------------------------------------------- public function testCodeIgniterLiteralRoute() { @@ -264,9 +246,7 @@ public function testCodeIgniterCatchAll() $this->assertSame('/{param1}', RouteNormalizer::normalizeFromCodeIgniter(':any')); } - // ------------------------------------------------------------------------- // normalizeFromWordPress - // ------------------------------------------------------------------------- public function testWordPressSimpleRegex() { @@ -292,9 +272,7 @@ public function testWordPressRootRule() $this->assertSame('/', $result); } - // ------------------------------------------------------------------------- // RFC examples - // ------------------------------------------------------------------------- public function testRfcExampleFastApi() { From a71a4549bf1c1190667e2f2df2c9b10116eb7fc8 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 13 Aug 2026 13:16:27 +0200 Subject: [PATCH 06/43] Fix pipeline --- src/DDTrace/Util/RouteNormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index aaf1ae1f79c..eb1a7cef9ce 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -54,7 +54,7 @@ public static function normalizeFromSymfony(string $path) * VersionListener sets :version even without a /v1/ prefix) * @return string|null */ - public static function normalizeFromLaminas(string $template, array $matchedParams = [], ?string $urlPath = null) + public static function normalizeFromLaminas(string $template, array $matchedParams = [], $urlPath = null) { $expanded = self::expandBracketOptionals($template, $matchedParams, ':', $urlPath); $expanded = preg_replace('#/\*$#', '/{param1}', $expanded); @@ -343,7 +343,7 @@ private static function expandBracketOptionals( string $template, array $matchedParams, string $paramPrefix = ':', - ?string $urlPath = null + $urlPath = null ): string { $prev = null; while ($prev !== $template) { From c1d1feda51b9dccba1c5ac55ccdd9691e6b8311f Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 13 Aug 2026 15:49:28 +0200 Subject: [PATCH 07/43] Remove non required changes --- .../Laminas/ApiTools/Latest/RESTTest.php | 16 +++------------- ...s.latest.rest_test.test_scenario_rest2xx.json | 2 +- ...s.latest.rest_test.test_scenario_rest4xx.json | 2 +- ...s.latest.rest_test.test_scenario_rest5xx.json | 8 ++++---- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php b/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php index 04aa4da6392..b8ffe5278f0 100644 --- a/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php +++ b/tests/Integrations/Laminas/ApiTools/Latest/RESTTest.php @@ -23,16 +23,6 @@ public static function getTestedLibrary() return 'laminas-api-tools/api-tools'; } - private static function restFieldsToIgnore(): array - { - // http.status_code and error.message vary between PHP/Laminas versions - return [ - 'metrics.php.compilation.total_time_ms', 'metrics.php.memory.peak_usage_bytes', - 'metrics.php.memory.peak_real_usage_bytes', 'meta.error.stack', 'meta._dd.p.tid', - 'start', 'duration', 'meta.http.status_code', 'meta.error.message', - ]; - } - public function testScenarioRest4xx() { $this->tracesFromWebRequestSnapshot(function () { @@ -42,7 +32,7 @@ public function testScenarioRest4xx() '/datadog-rest-service/1' ) ); - }, self::restFieldsToIgnore()); + }); } public function testScenarioRest2xx() @@ -56,7 +46,7 @@ public function testScenarioRest2xx() ['data' => 'dog'] ) ); - }, self::restFieldsToIgnore()); + }); } public function testScenarioRest5xx() @@ -68,6 +58,6 @@ public function testScenarioRest5xx() '/datadog-rest-service/42' ) ); - }, self::restFieldsToIgnore()); + }); } } diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json index 58e745746ba..395db3b8050 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json @@ -13,7 +13,7 @@ "http.method": "POST", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "_dd.appsec.normalized_route": "/datadog-rest-service", - "http.status_code": "200", + "http.status_code": "201", "http.url": "http://localhost/datadog-rest-service", "http.version": "1.1", "laminas.route.action": "DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource@create", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json index d8e7862b4d0..a4be1a90901 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json @@ -13,7 +13,7 @@ "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "_dd.appsec.normalized_route": "/datadog-rest-service/{datadog_rest_service_id}", - "http.status_code": "200", + "http.status_code": "405", "http.url": "http://localhost/datadog-rest-service/1", "http.version": "1.1", "laminas.route.action": "DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource@fetch", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json index 401bbf64c10..90a025e6b12 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json @@ -11,13 +11,13 @@ "meta": { "_dd.p.dm": "-0", "component": "laminas", - "error.message": "Uncaught Error: Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", + "error.message": "Uncaught Error (500): Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "_dd.appsec.normalized_route": "/datadog-rest-service/{datadog_rest_service_id}", - "http.status_code": "200", + "http.status_code": "500", "http.url": "http://localhost/datadog-rest-service/42", "http.version": "1.1", "laminas.route.action": "DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource@fetch", @@ -376,7 +376,7 @@ "error": 1, "meta": { "component": "laminas", - "error.message": "Thrown Error: Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", + "error.message": "Thrown Error (500): Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error" } @@ -392,7 +392,7 @@ "error": 1, "meta": { "component": "laminas", - "error.message": "Thrown Error: Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", + "error.message": "Thrown Error (500): Attempt to assign property \"b\" on null in {path}/tests/Frameworks/Laminas/ApiTools/Latest/module/DatadogApi/src/V1/Rest/DatadogRestService/DatadogRestServiceResource.php:55", "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error" } From dbea8613b7711aa1b67fd02d5d6c3b3ace419567 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 11:43:31 +0200 Subject: [PATCH 08/43] Fix special route --- src/DDTrace/Util/RouteNormalizer.php | 4 ++++ tests/Unit/Util/Normalizer/RouteNormalizerTest.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index eb1a7cef9ce..fe001bad124 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -235,6 +235,10 @@ private static function normalizeBraceRoute( $route = self::expandSquareBracketOptionals($route, $matchedParams); } + // Strip inline constraints before splitting so that a '/' inside a + // constraint (e.g. Slim's {name:[^/]+}) does not break the segment split. + $route = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)[^}]*\}/', '{$1}', $route); + $raw = ltrim($route, '/'); $parts = explode('/', $raw); $normalizedSegments = []; diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 2a9985718fe..af460eaa219 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -112,6 +112,8 @@ public function testSlimRegexConstraintStripped() { $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromSlim('/users/{id:[0-9]+}')); $this->assertSame('/v2/{name}/blobs', RouteNormalizer::normalizeFromSlim('/v2/{name:[a-zA-Z0-9-]+}/blobs')); + // Constraint containing '/' must not break the segment split + $this->assertSame('/files/{name}', RouteNormalizer::normalizeFromSlim('/files/{name:[^/]+}')); } public function testSlimOptionalSegmentPresent() From 3ba2da9d1e80d15d592f26cc845e5cbff4eb6aaa Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 11:45:36 +0200 Subject: [PATCH 09/43] Refine slim routes --- src/DDTrace/Integrations/Slim/SlimIntegration.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index 5669550a2da..df2cff2ef13 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -154,6 +154,15 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { } else { $rootSpan->meta['slim.route.controller'] = $callableName; $span->name = 'slim.route.controller'; + // Refine normalized route now that matched params are available (Slim 3) + $matchedParams = isset($args[3]) && is_array($args[3]) ? $args[3] : []; + $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; + if ($pattern !== '') { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } + } } }; From 5d777f0078a00c0e0080058c7dbd37590ba0df8b Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 12:27:55 +0200 Subject: [PATCH 10/43] Fix codeigniter --- .../Integrations/CodeIgniter/V2/CodeIgniterIntegration.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index 8b1b4ad6c02..d6a71441f00 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -259,9 +259,7 @@ private static function setHttpRoute($router, $rootSpan) { // If we got this far it means we didn't encounter a // matching route so we'll set the site default route $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($uri); - if ($normalizedRoute !== null) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } + // $uri is the concrete request path here, not a route template, so + // APPSEC_NORMALIZED_ROUTE is not set to avoid emitting real parameter values. } } From 309f8330f1288ee59bf9da5ac064766e49cf382d Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 12:40:17 +0200 Subject: [PATCH 11/43] Fix wordpress --- .../WordPress/WordPressIntegrationLoader.php | 3 ++- src/DDTrace/Util/RouteNormalizer.php | 27 +++++++++++++++++-- .../Util/Normalizer/RouteNormalizerTest.php | 20 ++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index cf10e8ce85b..518dbd8e89a 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -734,7 +734,8 @@ function_exists('is_404') && is_404() === false) { if (\property_exists($This, 'matched_rule')) { $matchedRule = $This->matched_rule; $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule); + $urlPath = \property_exists($This, 'request') ? $This->request : null; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index fe001bad124..666e097bea0 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -133,11 +133,28 @@ public static function normalizeFromCodeIgniter(string $route) * WordPress route matching uses regex rules like "^blog/([^/]+)/?$". * Named parameters are not available; placeholders param1, param2, … are used. * - * @param string $matchedRule Value of $wp->matched_rule + * @param string $matchedRule Value of $wp->matched_rule + * @param string|null $urlPath Value of $wp->request; used to detect which + * optional capture groups actually participated + * in the match, so phantom segments are not emitted. * @return string|null */ - public static function normalizeFromWordPress(string $matchedRule) + public static function normalizeFromWordPress(string $matchedRule, $urlPath = null) { + // Re-run the regex against the actual URL to find how many capture groups matched. + // This handles optional groups like (?:/([0-9]+))? that may or may not be present. + $matchedGroupCount = null; + if ($urlPath !== null) { + if (@preg_match('#^' . $matchedRule . '#', ltrim($urlPath, '/'), $captures)) { + $matchedGroupCount = 0; + for ($i = 1; $i < count($captures); $i++) { + if (isset($captures[$i]) && $captures[$i] !== '') { + $matchedGroupCount = $i; + } + } + } + } + $rule = ltrim($matchedRule, '^'); $rule = rtrim($rule, '$'); @@ -160,8 +177,14 @@ public static function normalizeFromWordPress(string $matchedRule) } if (preg_match('/^\([^)]+\)$/', $segment)) { + if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { + continue; + } $normalizedSegments[] = '{param' . $paramIndex++ . '}'; } elseif (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { + if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { + continue; + } $normalizedSegments[] = '{param' . $paramIndex++ . '}'; } else { $normalizedSegments[] = self::encodeStaticSegment($segment); diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index af460eaa219..d2dd7002243 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -268,6 +268,26 @@ public function testWordPressMultipleGroups() $this->assertSame('/{param1}/{param2}', $result); } + public function testWordPressOptionalGroupAbsent() + { + // Optional second segment not present in URL — must not emit phantom {param2} + $result = RouteNormalizer::normalizeFromWordPress('^([^/]+)(?:/([0-9]+))?/?$', 'simple'); + $this->assertSame('/{param1}', $result); + } + + public function testWordPressOptionalGroupPresent() + { + $result = RouteNormalizer::normalizeFromWordPress('^([^/]+)(?:/([0-9]+))?/?$', 'simple/123'); + $this->assertSame('/{param1}/{param2}', $result); + } + + public function testWordPressOptionalGroupNoUrlPath() + { + // Without URL path, fall back to emitting all groups (backward-compatible) + $result = RouteNormalizer::normalizeFromWordPress('^([^/]+)(?:/([0-9]+))?/?$'); + $this->assertSame('/{param1}/{param2}', $result); + } + public function testWordPressRootRule() { $result = RouteNormalizer::normalizeFromWordPress('^/?$'); From 3c36a6e8c149e1b0ae3d0f33b9df447e7dfc8977 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 13:19:31 +0200 Subject: [PATCH 12/43] Fix pipeline --- src/DDTrace/Util/RouteNormalizer.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 666e097bea0..81c0d30bdde 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -258,9 +258,10 @@ private static function normalizeBraceRoute( $route = self::expandSquareBracketOptionals($route, $matchedParams); } - // Strip inline constraints before splitting so that a '/' inside a - // constraint (e.g. Slim's {name:[^/]+}) does not break the segment split. - $route = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)[^}]*\}/', '{$1}', $route); + // Strip inline constraints (e.g. Slim's {name:[^/]+} → {name}) before + // splitting so that a '/' inside a constraint does not break the segment + // split. The optional marker '?' is preserved: {name?:[0-9]+} → {name?}. + $route = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*(\?)?):([^}]*)\}/', '{$1}', $route); $raw = ltrim($route, '/'); $parts = explode('/', $raw); From ec3960b335e7851299dc58f924fc9b5b9bc196af Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 13:39:37 +0200 Subject: [PATCH 13/43] Fix pipeline --- .../Integrations/CodeIgniter/V2/CodeIgniterIntegration.php | 6 ++++-- ...rios_callbacks_test.test_scenario_get_return_string.json | 2 +- ...ios_callbacks_test.test_scenario_get_with_exception.json | 2 +- ...cenarios_callbacks_test.test_scenario_get_with_view.json | 2 +- ...mmon_scenarios_test.test_scenario_get_return_string.json | 2 +- ...mon_scenarios_test.test_scenario_get_with_exception.json | 2 +- ...8.common_scenarios_test.test_scenario_get_with_view.json | 2 +- ...rios_callbacks_test.test_scenario_get_return_string.json | 2 +- ...ios_callbacks_test.test_scenario_get_with_exception.json | 2 +- ...cenarios_callbacks_test.test_scenario_get_with_view.json | 2 +- ...mmon_scenarios_test.test_scenario_get_return_string.json | 2 +- ...mon_scenarios_test.test_scenario_get_with_exception.json | 2 +- ...5.common_scenarios_test.test_scenario_get_with_view.json | 2 +- ...rios_callbacks_test.test_scenario_get_return_string.json | 2 +- ...ios_callbacks_test.test_scenario_get_with_exception.json | 2 +- ...cenarios_callbacks_test.test_scenario_get_with_view.json | 2 +- ...mmon_scenarios_test.test_scenario_get_return_string.json | 2 +- ...mon_scenarios_test.test_scenario_get_with_exception.json | 2 +- ...9.common_scenarios_test.test_scenario_get_with_view.json | 2 +- ...rios_callbacks_test.test_scenario_get_return_string.json | 2 +- ...ios_callbacks_test.test_scenario_get_with_exception.json | 2 +- ...cenarios_callbacks_test.test_scenario_get_with_view.json | 2 +- ...mmon_scenarios_test.test_scenario_get_return_string.json | 2 +- ...mon_scenarios_test.test_scenario_get_with_exception.json | 2 +- ...1.common_scenarios_test.test_scenario_get_with_view.json | 2 +- 25 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index d6a71441f00..8b1b4ad6c02 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -259,7 +259,9 @@ private static function setHttpRoute($router, $rootSpan) { // If we got this far it means we didn't encounter a // matching route so we'll set the site default route $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; - // $uri is the concrete request path here, not a route template, so - // APPSEC_NORMALIZED_ROUTE is not set to avoid emitting real parameter values. + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($uri); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } } diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 4a0424e5fc1..807423f1bbf 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 1cd0231837c..d52a3da5efc 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json index 74b6b4c2b02..56709760ea8 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "(.?.+?)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json index 81014c33efb..04857a7ad7d 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json index 40cddd81172..d03c59fa4b1 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json index 778f8d9209b..0aed57bd0b3 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "(.?.+?)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 0c1c4d74475..54ceed87027 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index d72ec1acfd1..dab6d17b85c 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json index cb7425d421b..ddbfe192003 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json index 2d967a4883f..97475b3c3ed 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json index b283c6044c1..672fe1ef8f0 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json index 0568c99e74b..0d0e03ec26f 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 46e6fbaa20c..99517450a9e 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index bd2e2440b18..10805214ae1 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json index f8cf6506965..54424a79ca8 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json index 643b28cd516..b1542c03381 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json index abbd529ddfe..dadb6830bce 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json index c7c1aa69e1a..67e6a278f1c 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 20ca8883a6b..0b9ee6a9b64 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 75cb2133a29..3848d91ec65 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json index 1fad3089c07..f4e388bcd01 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json index 55bd806fdb2..6af081bf5bc 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json index d20cc4a0e26..0c960dd3fc0 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,7 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json index 2c0aced29de..02a24be7bb7 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,7 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}/{param2}", + "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", From c44b3bb4b24949fc51d2f43589989c9237833749 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 15:31:08 +0200 Subject: [PATCH 14/43] Fix wordpress --- src/DDTrace/Util/RouteNormalizer.php | 58 ++++++++++++++++--- .../Util/Normalizer/RouteNormalizerTest.php | 8 +++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 81c0d30bdde..2aa4adb72b1 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -176,16 +176,25 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu continue; } - if (preg_match('/^\([^)]+\)$/', $segment)) { - if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { - continue; - } - $normalizedSegments[] = '{param' . $paramIndex++ . '}'; - } elseif (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { - if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { - continue; + if (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { + $groupCount = self::countCaptureGroups($segment); + if ($groupCount === 0) { + if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { + continue; + } + $normalizedSegments[] = '{param' . $paramIndex++ . '}'; + } else { + $params = []; + for ($j = 0; $j < $groupCount; $j++) { + if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { + break; + } + $params[] = 'param' . $paramIndex++; + } + if (!empty($params)) { + $normalizedSegments[] = '{' . implode('+', $params) . '}'; + } } - $normalizedSegments[] = '{param' . $paramIndex++ . '}'; } else { $normalizedSegments[] = self::encodeStaticSegment($segment); } @@ -232,6 +241,37 @@ private static function splitRegexBySlash(string $str): array return $segments; } + /** + * Count capturing groups in a regex segment, ignoring character classes and non-capturing groups. + */ + private static function countCaptureGroups(string $segment): int + { + $count = 0; + $len = strlen($segment); + $inClass = false; + + for ($i = 0; $i < $len; $i++) { + $c = $segment[$i]; + + if ($c === '\\' && $i + 1 < $len) { + $i++; + continue; + } + + if ($c === '[' && !$inClass) { + $inClass = true; + } elseif ($c === ']' && $inClass) { + $inClass = false; + } elseif ($c === '(' && !$inClass) { + if ($i + 1 >= $len || $segment[$i + 1] !== '?') { + $count++; + } + } + } + + return $count; + } + /** * Normalize a route that uses {param} notation. * diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index d2dd7002243..6ca71b563c6 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -294,6 +294,14 @@ public function testWordPressRootRule() $this->assertSame('/', $result); } + public function testWordPressMultipleCaptureGroupsInOneSegment() + { + // Two capture groups in the same slash-separated segment → combined with + + // The static prefix "post-" is dropped as the whole mixed segment is treated as dynamic + $result = RouteNormalizer::normalizeFromWordPress('^post-([^/]+)-([0-9]+)/?$'); + $this->assertSame('/{param1+param2}', $result); + } + // RFC examples public function testRfcExampleFastApi() From 561896b20ed7b90df0e8ce946fca2e28bb69efab Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 15:35:59 +0200 Subject: [PATCH 15/43] Improve pr --- src/DDTrace/Util/RouteNormalizer.php | 30 ++++++---- .../Util/Normalizer/RouteNormalizerTest.php | 55 +++++++++++++++++++ 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 2aa4adb72b1..e41be99edca 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -353,12 +353,16 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar $name = trim($raw); if ($isOptional && !array_key_exists($name, $matchedParams)) { - return null; + continue; } $paramNames[] = self::encodeParamName($name); } + if (empty($paramNames)) { + return null; + } + if (count($paramNames) === 1) { return '{' . $paramNames[0] . '}'; } @@ -424,25 +428,31 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { preg_match_all($pattern, $inner, $pm); $innerParams = $pm[1]; + // All params in the section must be present in matched params. foreach ($innerParams as $param) { if (!array_key_exists($param, $matchedParams)) { - continue; + return ''; } - if ($urlPath !== null) { + } + + if ($urlPath !== null && !empty($innerParams)) { + // Substitute every param value before checking the URL so that + // multi-param sections like [/:year/:month] are found correctly. + $innerWithValues = $inner; + foreach ($innerParams as $param) { $value = (string)$matchedParams[$param]; - $innerWithValue = preg_replace( + $innerWithValues = preg_replace( '/' . preg_quote($paramPrefix . $param, '/') . '/', $value, - $inner + $innerWithValues ); - if (strpos($urlPath, $innerWithValue) === false) { - continue; - } } - return $inner; + if (strpos($urlPath, $innerWithValues) === false) { + return ''; + } } - return ''; + return $inner; }, $template ); diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 6ca71b563c6..724f88ff97b 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -84,6 +84,19 @@ public function testLaravelMixedSegmentOptionalFormat() $this->assertSame('/posts/{id}', $result); } + public function testLaravelRequiredParamBesideAbsentOptional() + { + // {name} is required; {ext?} is absent — must keep {name}, not drop the whole segment + $result = RouteNormalizer::normalizeFromLaravel('/files/{name}.{ext?}', ['name' => 'foo']); + $this->assertSame('/files/{name}', $result); + } + + public function testLaravelRequiredParamBesideAbsentOptionalBothPresent() + { + $result = RouteNormalizer::normalizeFromLaravel('/files/{name}.{ext?}', ['name' => 'foo', 'ext' => 'txt']); + $this->assertSame('/files/{name+ext}', $result); + } + public function testLaravelDeeperRoute() { $result = RouteNormalizer::normalizeFromLaravel('/dashboard/shared_widget_update/{id}/{widget_id}'); @@ -171,6 +184,48 @@ public function testLaminasOptionalAbsent() $this->assertSame('/users/{id}', $result); } + public function testLaminasMultiParamOptionalPresent() + { + // Both params in the section present and appear in the URL → expand + $result = RouteNormalizer::normalizeFromLaminas( + '/archive[/:year/:month]', + ['year' => '2024', 'month' => '08'], + '/archive/2024/08' + ); + $this->assertSame('/archive/{year}/{month}', $result); + } + + public function testLaminasMultiParamOptionalAbsent() + { + // Both params injected by middleware but absent from URL → do not expand + $result = RouteNormalizer::normalizeFromLaminas( + '/archive[/:year/:month]', + ['year' => '2024', 'month' => '08'], + '/archive' + ); + $this->assertSame('/archive', $result); + } + + public function testLaminasNestedOptionalBothPresent() + { + $result = RouteNormalizer::normalizeFromLaminas( + '/foo[/:bar[/:baz]]', + ['bar' => 'a', 'baz' => 'b'], + '/foo/a/b' + ); + $this->assertSame('/foo/{bar}/{baz}', $result); + } + + public function testLaminasNestedOptionalOnlyOuterPresent() + { + $result = RouteNormalizer::normalizeFromLaminas( + '/foo[/:bar[/:baz]]', + ['bar' => 'a'], + '/foo/a' + ); + $this->assertSame('/foo/{bar}', $result); + } + public function testLaminasLiteralRoute() { $this->assertSame('/dump-request', RouteNormalizer::normalizeFromLaminas('/dump-request')); From 01bb9f024d530f125f2680610114860db774ebe6 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 16:01:24 +0200 Subject: [PATCH 16/43] Fix PR --- .../Integrations/Slim/SlimIntegration.php | 6 +- .../Symfony/SymfonyIntegration.php | 35 +++++++++++- src/DDTrace/Util/RouteNormalizer.php | 48 ++++++++++++---- .../Util/Normalizer/RouteNormalizerTest.php | 56 +++++++++++++++++++ 4 files changed, 131 insertions(+), 14 deletions(-) diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index df2cff2ef13..c795d846cec 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -145,7 +145,8 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $matchedParams = method_exists($route, 'getArguments') ? ($route->getArguments() ?? []) : []; $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; if ($pattern !== '') { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams); + $urlPath = $request->getUri()->getPath(); + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } @@ -158,7 +159,8 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $matchedParams = isset($args[3]) && is_array($args[3]) ? $args[3] : []; $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; if ($pattern !== '') { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams); + $urlPath = $request->getUri()->getPath(); + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index a77f59d2998..d6f57ce6d63 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -456,7 +456,8 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path); + $matchedParams = self::inferSymfonyRouteParams($path, $request->getPathInfo()); + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } @@ -774,4 +775,36 @@ public static function injectActionInfo($event, $eventName, SpanData $requestSpa return true; } + + /** + * Walk the path template and URL path together to determine which {param} + * placeholders were actually present in the URL (vs filled from route defaults). + * Each template segment is either static text or a single {param}; trailing + * params with no corresponding URL segment are considered absent. + * + * @return array Map of param name → URL value for params present in the URL + */ + private static function inferSymfonyRouteParams(string $template, string $urlPath): array + { + $templateSegments = array_values(array_filter(explode('/', $template))); + $urlSegments = array_values(array_filter(explode('/', $urlPath))); + + $matched = []; + $urlIdx = 0; + + foreach ($templateSegments as $seg) { + if (preg_match('/^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/', $seg, $m)) { + if ($urlIdx < count($urlSegments)) { + $matched[$m[1]] = $urlSegments[$urlIdx]; + $urlIdx++; + } + // else: param is beyond end of URL → absent (default-filled) + } else { + // Static segment — always advance the URL position + $urlIdx++; + } + } + + return $matched; + } } diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index e41be99edca..bfdac145bfa 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -20,23 +20,38 @@ public static function normalizeFromLaravel(string $routeUri, array $matchedPara /** * Normalize a Slim route pattern. * - * @param string $pattern Pattern from $route->getPattern(), e.g. "/users/{id:[0-9]+}" - * @param array $matchedParams Matched params from $route->getArguments(); resolves optionals + * @param string $pattern Pattern from $route->getPattern(), e.g. "/users/{id:[0-9]+}" + * @param array $matchedParams Matched params from $route->getArguments(); resolves optionals + * @param string|null $urlPath Actual request path; used to resolve static-only optional + * sections like [.json] that have no placeholder param * @return string|null */ - public static function normalizeFromSlim(string $pattern, array $matchedParams = []) + public static function normalizeFromSlim(string $pattern, array $matchedParams = [], ?string $urlPath = null) { - return self::normalizeBraceRoute($pattern, $matchedParams, true); + return self::normalizeBraceRoute($pattern, $matchedParams, true, $urlPath); } /** * Normalize a Symfony route path. * - * @param string $path Path template, e.g. "/users/{id}" + * @param string $path Path template, e.g. "/users/{id}" + * @param array|null $matchedParams Params actually present in the URL path (not including + * route defaults); when provided, absent params are dropped * @return string|null */ - public static function normalizeFromSymfony(string $path) + public static function normalizeFromSymfony(string $path, ?array $matchedParams = null) { + if ($matchedParams !== null) { + // Mark params absent from the URL as optional so normalizeBraceSegment drops them. + $path = preg_replace_callback( + '/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', + static function ($m) use ($matchedParams) { + return array_key_exists($m[1], $matchedParams) ? $m[0] : '{' . $m[1] . '?}'; + }, + $path + ); + return self::normalizeBraceRoute($path, $matchedParams); + } return self::normalizeBraceRoute($path, []); } @@ -275,12 +290,14 @@ private static function countCaptureGroups(string $segment): int /** * Normalize a route that uses {param} notation. * - * @param bool $expandSquare When true, expand Slim-style [...] optional sections + * @param bool $expandSquare When true, expand Slim-style [...] optional sections + * @param string|null $urlPath Actual request path; forwarded to expandSquareBracketOptionals */ private static function normalizeBraceRoute( string $route, array $matchedParams, - bool $expandSquare = false + bool $expandSquare = false, + ?string $urlPath = null ) { $route = trim($route); if ($route === '' || $route === '/') { @@ -295,7 +312,7 @@ private static function normalizeBraceRoute( } if ($expandSquare) { - $route = self::expandSquareBracketOptionals($route, $matchedParams); + $route = self::expandSquareBracketOptionals($route, $matchedParams, $urlPath); } // Strip inline constraints (e.g. Slim's {name:[^/]+} → {name}) before @@ -372,20 +389,29 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar /** * Expand Slim-style optional sections [...] based on matched params. + * + * For sections that contain no placeholder (e.g. [.json]), $urlPath is used + * to decide whether the literal text was part of the request; without it the + * section is always kept (backward-compatible behaviour). */ - private static function expandSquareBracketOptionals(string $route, array $matchedParams): string + private static function expandSquareBracketOptionals(string $route, array $matchedParams, ?string $urlPath = null): string { $prev = null; while ($prev !== $route) { $prev = $route; $route = preg_replace_callback( '/\[([^\[\]]*)\]/', - function ($m) use ($matchedParams) { + function ($m) use ($matchedParams, $urlPath) { $inner = $m[1]; preg_match_all('/\{([^}?:]+)[?:]?[^}]*\}/', $inner, $pm); $innerParams = $pm[1]; if (empty($innerParams)) { + // Static-only section (e.g. [.json]): include only when the + // literal text actually appears in the request path. + if ($urlPath !== null) { + return strpos($urlPath, $inner) !== false ? $inner : ''; + } return $inner; } diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 724f88ff97b..b4d6753b66b 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -146,6 +146,27 @@ public function testSlimCatchAll() $this->assertSame('/files/{file}', RouteNormalizer::normalizeFromSlim('/files/{file:.+}')); } + public function testSlimStaticOptionalSectionPresent() + { + // /feed[.json] requested as /feed.json → .json section included + $result = RouteNormalizer::normalizeFromSlim('/feed[.json]', [], '/feed.json'); + $this->assertSame('/feed.json', $result); + } + + public function testSlimStaticOptionalSectionAbsent() + { + // /feed[.json] requested as /feed → .json section absent + $result = RouteNormalizer::normalizeFromSlim('/feed[.json]', [], '/feed'); + $this->assertSame('/feed', $result); + } + + public function testSlimStaticOptionalSectionNoUrlPath() + { + // Without URL path, backward-compatible: keep the section + $result = RouteNormalizer::normalizeFromSlim('/feed[.json]', []); + $this->assertSame('/feed.json', $result); + } + // normalizeFromSymfony public function testSymfonySimpleRoute() @@ -165,6 +186,41 @@ public function testSymfonyStaticOnlyRoute() $this->assertSame('/dump-request', RouteNormalizer::normalizeFromSymfony('/dump-request')); } + public function testSymfonyOptionalParamAbsent() + { + // /blog/{page} requested as /blog — page has a default and was not in the URL + $result = RouteNormalizer::normalizeFromSymfony('/blog/{page}', []); + $this->assertSame('/blog', $result); + } + + public function testSymfonyOptionalParamPresent() + { + // /blog/{page} requested as /blog/2 — page was in the URL + $result = RouteNormalizer::normalizeFromSymfony('/blog/{page}', ['page' => '2']); + $this->assertSame('/blog/{page}', $result); + } + + public function testSymfonyRequiredParamsAlwaysKept() + { + // All params present — nothing dropped + $result = RouteNormalizer::normalizeFromSymfony('/users/{id}/posts/{post_id}', ['id' => '1', 'post_id' => '5']); + $this->assertSame('/users/{id}/posts/{post_id}', $result); + } + + public function testSymfonyTrailingOptionalAbsent() + { + // /users/{id}/posts/{post_id} with only id in URL — post_id absent + $result = RouteNormalizer::normalizeFromSymfony('/users/{id}/posts/{post_id}', ['id' => '1']); + $this->assertSame('/users/{id}/posts', $result); + } + + public function testSymfonyNoMatchedParamsArgKeepsAll() + { + // null matchedParams → old behaviour, no params dropped + $result = RouteNormalizer::normalizeFromSymfony('/blog/{page}'); + $this->assertSame('/blog/{page}', $result); + } + // normalizeFromLaminas public function testLaminasSimpleColon() From 1d3add3de67c2a45c7d625afd76f11ab7c88faee Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 17 Aug 2026 16:06:22 +0200 Subject: [PATCH 17/43] Fix laminas --- src/DDTrace/Util/RouteNormalizer.php | 11 +++++++++++ tests/Unit/Util/Normalizer/RouteNormalizerTest.php | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index bfdac145bfa..607c173cfb4 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -73,7 +73,9 @@ public static function normalizeFromLaminas(string $template, array $matchedPara { $expanded = self::expandBracketOptionals($template, $matchedParams, ':', $urlPath); $expanded = preg_replace('#/\*$#', '/{param1}', $expanded); + // Segment routes use :param; Regex routes use %param% (spec format) — handle both. $braceFormat = self::colonParamsToBraces($expanded); + $braceFormat = self::percentParamsToBraces($braceFormat); return self::normalizeBraceRoute($braceFormat, $matchedParams); } @@ -501,6 +503,15 @@ static function ($m) { ); } + /** + * Convert Laminas Regex route spec %param% notation to {param} brace notation. + * Regex routes store their spec as "/path/%id%/%name%" for URL generation. + */ + private static function percentParamsToBraces(string $template): string + { + return preg_replace('/%([a-zA-Z_][a-zA-Z0-9_]*)%/', '{$1}', $template); + } + /** * Convert CakePHP route template syntax to brace notation. */ diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index b4d6753b66b..575dcc4d387 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -282,6 +282,13 @@ public function testLaminasNestedOptionalOnlyOuterPresent() $this->assertSame('/foo/{bar}', $result); } + public function testLaminasRegexRouteSpec() + { + // Laminas\Router\Http\Regex uses %param% spec format for URL generation + $this->assertSame('/blog/{id}', RouteNormalizer::normalizeFromLaminas('/blog/%id%')); + $this->assertSame('/user/{id}/{name}', RouteNormalizer::normalizeFromLaminas('/user/%id%/%name%')); + } + public function testLaminasLiteralRoute() { $this->assertSame('/dump-request', RouteNormalizer::normalizeFromLaminas('/dump-request')); From 7646665aa8318756a73917c5657848da523387f7 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 18 Aug 2026 12:20:31 +0200 Subject: [PATCH 18/43] Implement a route caching system --- .../CakePHP/CakePHPIntegration.php | 11 +++- .../CodeIgniter/V2/CodeIgniterIntegration.php | 29 +++++---- .../Laminas/LaminasIntegration.php | 13 +++- .../Laravel/LaravelIntegration.php | 12 +++- .../Symfony/SymfonyIntegration.php | 28 ++++++--- .../Integrations/Yii/YiiIntegration.php | 11 +++- .../ext/routing_cache/cache_lru_eviction.phpt | 31 ++++++++++ .../cache_miss_returns_false.phpt | 14 +++++ .../ext/routing_cache/cache_set_and_get.phpt | 21 +++++++ .../cache_update_existing_key.phpt | 15 +++++ tracer/ddtrace.c | 3 + tracer/ddtrace_arginfo.h | 13 ++++ tracer/routing_cache.c | 59 +++++++++++++++++++ tracer/routing_cache.h | 14 +++++ 14 files changed, 244 insertions(+), 30 deletions(-) create mode 100644 tests/ext/routing_cache/cache_lru_eviction.phpt create mode 100644 tests/ext/routing_cache/cache_miss_returns_false.phpt create mode 100644 tests/ext/routing_cache/cache_set_and_get.phpt create mode 100644 tests/ext/routing_cache/cache_update_existing_key.phpt create mode 100644 tracer/routing_cache.c create mode 100644 tracer/routing_cache.h diff --git a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php index 2d416f19ea5..9e9f171ba51 100644 --- a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php +++ b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php @@ -73,8 +73,15 @@ public static function init(): int if ($rootSpan !== null) { $template = $app->template; $rootSpan->meta[Tag::HTTP_ROUTE] = $template; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCakePHP($template); - if ($normalizedRoute !== null) { + $cacheKey = $template; + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCakePHP($template); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index 8b1b4ad6c02..4e5cb94ea29 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -222,6 +222,20 @@ function (SpanData $span, $args, $retval, $ex) use ($adapter, $service) { /* * Replicate CodeIgniter's route parsing, as matching key is never stored or returned in the framework. */ + private static function setNormalizedRoute($rootSpan, string $pattern): void { + $cacheKey = $pattern; + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($pattern); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } + } + private static function setHttpRoute($router, $rootSpan) { // Turn the segment array into a URI string $uri = implode('/', $router->uri->segments); @@ -230,10 +244,7 @@ private static function setHttpRoute($router, $rootSpan) { if (isset($router->routes[$uri])) { $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($uri); - if ($normalizedRoute !== null) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } + self::setNormalizedRoute($rootSpan, $uri); return; } @@ -248,10 +259,7 @@ private static function setHttpRoute($router, $rootSpan) { if (preg_match('#^'.$key.'$#', $uri)) { $rootSpan->meta[Tag::HTTP_ROUTE] = $origKey; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($origKey); - if ($normalizedRoute !== null) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } + self::setNormalizedRoute($rootSpan, $origKey); return; } } @@ -259,9 +267,6 @@ private static function setHttpRoute($router, $rootSpan) { // If we got this far it means we didn't encounter a // matching route so we'll set the site default route $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($uri); - if ($normalizedRoute !== null) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } + self::setNormalizedRoute($rootSpan, $uri); } } diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 9e7128e65c4..b40817338c7 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -281,8 +281,17 @@ static function (SpanData $span) use ($controller, $action) { && $routeName !== null && $routeName !== '' ) { - $httpRoute = LaminasIntegration::httpRouteTemplateFromNamedRouteStack($this, (string) $routeName); - if ($httpRoute !== null && $httpRoute !== '') { + $cacheKey = (string) $routeName; + $cachedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($cachedRoute !== false) { + $httpRoute = $cachedRoute; + } else { + $httpRoute = LaminasIntegration::httpRouteTemplateFromNamedRouteStack($this, (string) $routeName); + if ($httpRoute !== null && $httpRoute !== '') { + \DDTrace\routing_cache_set($cacheKey, $httpRoute); + } + } + if ($httpRoute !== null && $httpRoute !== false && $httpRoute !== '') { $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; diff --git a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php index 5862792bd73..9c2fcea9077 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -142,9 +142,15 @@ static function ($This, $scope, $args, $route) { if (\method_exists($route, 'uri')) { $httpRoute = $route->uri(); $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; - $matchedParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaravel($httpRoute, $matchedParams); - if ($normalizedRoute !== null) { + $normalizedRoute = \DDTrace\routing_cache_get($httpRoute); + if ($normalizedRoute === false) { + $matchedParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaravel($httpRoute, $matchedParams); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($httpRoute, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index d6f57ce6d63..a2c73516f23 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -442,15 +442,25 @@ static function() { return; } - /** @var ContainerInterface $container */ - $container = self::$kernel->getContainer(); - $path = EndpointCatalog::pathForRoute($route_name, $container); - - // Try with locale suffix (Symfony i18n routing convention) - if ($path === null) { - $locale = $request->attributes->get('_locale'); - if ($locale !== null) { - $path = EndpointCatalog::pathForRoute($route_name . '.' . $locale, $container); + $cacheKey = $route_name; + $cachedPath = \DDTrace\routing_cache_get($cacheKey); + if ($cachedPath !== false) { + $path = $cachedPath; + } else { + /** @var ContainerInterface $container */ + $container = self::$kernel->getContainer(); + $path = EndpointCatalog::pathForRoute($route_name, $container); + + // Try with locale suffix (Symfony i18n routing convention) + if ($path === null) { + $locale = $request->attributes->get('_locale'); + if ($locale !== null) { + $path = EndpointCatalog::pathForRoute($route_name . '.' . $locale, $container); + } + } + + if ($path !== null) { + \DDTrace\routing_cache_set($cacheKey, $path); } } diff --git a/src/DDTrace/Integrations/Yii/YiiIntegration.php b/src/DDTrace/Integrations/Yii/YiiIntegration.php index 8acc12e9047..32269f49285 100644 --- a/src/DDTrace/Integrations/Yii/YiiIntegration.php +++ b/src/DDTrace/Integrations/Yii/YiiIntegration.php @@ -156,8 +156,15 @@ function (SpanData $span, $args) use (&$firstController) { $rootSpan->meta['app.route.path'] = $routePath; $rootSpan->meta[Tag::HTTP_ROUTE] = $routePath; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromYii($routePath); - if ($normalizedRoute !== null) { + $cacheKey = $routePath; + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromYii($routePath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } diff --git a/tests/ext/routing_cache/cache_lru_eviction.phpt b/tests/ext/routing_cache/cache_lru_eviction.phpt new file mode 100644 index 00000000000..597049bfb57 --- /dev/null +++ b/tests/ext/routing_cache/cache_lru_eviction.phpt @@ -0,0 +1,31 @@ +--TEST-- +DDTrace\routing_cache evicts least-recently-used entry when capacity (500) is exceeded +--FILE-- + +--EXPECT-- +string(6) "value0" +bool(false) +string(6) "value0" +string(8) "value500" diff --git a/tests/ext/routing_cache/cache_miss_returns_false.phpt b/tests/ext/routing_cache/cache_miss_returns_false.phpt new file mode 100644 index 00000000000..1138b0ad30e --- /dev/null +++ b/tests/ext/routing_cache/cache_miss_returns_false.phpt @@ -0,0 +1,14 @@ +--TEST-- +DDTrace\routing_cache_get returns false on cache miss +--FILE-- + +--EXPECT-- +bool(false) +bool(false) +bool(false) diff --git a/tests/ext/routing_cache/cache_set_and_get.phpt b/tests/ext/routing_cache/cache_set_and_get.phpt new file mode 100644 index 00000000000..0f75a2f4aba --- /dev/null +++ b/tests/ext/routing_cache/cache_set_and_get.phpt @@ -0,0 +1,21 @@ +--TEST-- +DDTrace\routing_cache_set stores and DDTrace\routing_cache_get retrieves values +--FILE-- + +--EXPECT-- +string(15) "/api/users/{id}" +string(12) "/blog/{slug}" +string(15) "/api/users/{id}" +bool(false) diff --git a/tests/ext/routing_cache/cache_update_existing_key.phpt b/tests/ext/routing_cache/cache_update_existing_key.phpt new file mode 100644 index 00000000000..c7feb2ec8a6 --- /dev/null +++ b/tests/ext/routing_cache/cache_update_existing_key.phpt @@ -0,0 +1,15 @@ +--TEST-- +DDTrace\routing_cache_set updates value for existing key +--FILE-- + +--EXPECT-- +string(5) "first" +string(7) "updated" diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index df9b73ef12c..bf4fe662fa4 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -1,3 +1,4 @@ +#include "routing_cache.h" #include "components-rs/common.h" #include "components-rs/sidecar.h" #include "zend_API.h" @@ -373,6 +374,7 @@ void ddtrace_minit_late() { ddtrace_live_debugger_minit(); ddtrace_trace_source_minit(); + ddtrace_routing_cache_minit(); } void ddtrace_mshutdown() { @@ -380,6 +382,7 @@ void ddtrace_mshutdown() { zai_hook_mshutdown(); ddtrace_unregister_functions_and_classes(); + ddtrace_routing_cache_mshutdown(); if (datadog_disable == 1) { return; diff --git a/tracer/ddtrace_arginfo.h b/tracer/ddtrace_arginfo.h index afa0f62d9f7..db27f813c8d 100644 --- a/tracer/ddtrace_arginfo.h +++ b/tracer/ddtrace_arginfo.h @@ -38,6 +38,15 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_set_user, 0, 1, IS_VOID, ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, propagate, _IS_BOOL, 1, "null") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_DDTrace_routing_cache_get, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_routing_cache_set, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_DDTrace_close_spans_until, 0, 1, MAY_BE_FALSE|MAY_BE_LONG) ZEND_ARG_OBJ_INFO(0, span, DDTrace\\SpanData, 1) ZEND_END_ARG_INFO() @@ -473,6 +482,8 @@ ZEND_FUNCTION(DDTrace_trace_function); ZEND_FUNCTION(DDTrace_trace_method); ZEND_FUNCTION(dd_untrace); ZEND_FUNCTION(dd_trace_synchronous_flush); +ZEND_FUNCTION(DDTrace_routing_cache_get); +ZEND_FUNCTION(DDTrace_routing_cache_set); ZEND_METHOD(DDTrace_SpanEvent, __construct); ZEND_METHOD(DDTrace_SpanEvent, jsonSerialize); ZEND_METHOD(DDTrace_ExceptionSpanEvent, __construct); @@ -549,6 +560,8 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "flush_ffe_evaluation_metrics"), zif_DDTrace_Internal_flush_ffe_evaluation_metrics, arginfo_DDTrace_Internal_flush_ffe_evaluation_metrics, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_success"), zif_datadog_appsec_v2_track_user_login_success, arginfo_datadog_appsec_v2_track_user_login_success, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_failure"), zif_datadog_appsec_v2_track_user_login_failure, arginfo_datadog_appsec_v2_track_user_login_failure, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "routing_cache_get"), zif_DDTrace_routing_cache_get, arginfo_DDTrace_routing_cache_get, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "routing_cache_set"), zif_DDTrace_routing_cache_set, arginfo_DDTrace_routing_cache_set, 0, NULL, NULL) ZEND_FE(dd_trace_env_config, arginfo_dd_trace_env_config) ZEND_FE(dd_trace_disable_in_request, arginfo_dd_trace_disable_in_request) ZEND_FE(dd_trace_reset, arginfo_dd_trace_reset) diff --git a/tracer/routing_cache.c b/tracer/routing_cache.c new file mode 100644 index 00000000000..fe91fbe1acb --- /dev/null +++ b/tracer/routing_cache.c @@ -0,0 +1,59 @@ +#include "routing_cache.h" +#include "ddtrace.h" + +static HashTable ddtrace_rcache; + +static void ddtrace_routing_cache_dtor(zval *pz) { + zend_string_release_ex((zend_string *)Z_PTR_P(pz), 1); +} + +static void ddtrace_routing_cache_evict_oldest(void) { + HashPosition pos; + zend_string *key; + zend_ulong num_idx; + + zend_hash_internal_pointer_reset_ex(&ddtrace_rcache, &pos); + if (zend_hash_get_current_key_type_ex(&ddtrace_rcache, &pos) == HASH_KEY_IS_STRING) { + zend_hash_get_current_key_ex(&ddtrace_rcache, &key, &num_idx, &pos); + zend_hash_del(&ddtrace_rcache, key); + } +} + +void ddtrace_routing_cache_minit(void) { + zend_hash_init(&ddtrace_rcache, DDTRACE_ROUTING_CACHE_CAPACITY, NULL, ddtrace_routing_cache_dtor, 1); +} + +void ddtrace_routing_cache_mshutdown(void) { + zend_hash_destroy(&ddtrace_rcache); +} + +/* DDTrace\routing_cache_get(string $key): string|false */ +PHP_FUNCTION(DDTrace_routing_cache_get) { + zend_string *key; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + zend_string *value = zend_hash_find_ptr(&ddtrace_rcache, key); + if (!value) { + RETURN_FALSE; + } + RETURN_STRINGL(ZSTR_VAL(value), ZSTR_LEN(value)); +} + +/* DDTrace\routing_cache_set(string $key, string $value): void */ +PHP_FUNCTION(DDTrace_routing_cache_set) { + zend_string *key, *value; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(key) + Z_PARAM_STR(value) + ZEND_PARSE_PARAMETERS_END(); + + if (zend_hash_num_elements(&ddtrace_rcache) >= DDTRACE_ROUTING_CACHE_CAPACITY + && !zend_hash_find_ptr(&ddtrace_rcache, key)) { + ddtrace_routing_cache_evict_oldest(); + } + + zend_string *persistent_value = zend_string_init(ZSTR_VAL(value), ZSTR_LEN(value), 1); + zend_hash_str_update_ptr(&ddtrace_rcache, ZSTR_VAL(key), ZSTR_LEN(key), persistent_value); +} diff --git a/tracer/routing_cache.h b/tracer/routing_cache.h new file mode 100644 index 00000000000..84804d3882a --- /dev/null +++ b/tracer/routing_cache.h @@ -0,0 +1,14 @@ +#ifndef DDTRACE_ROUTING_CACHE_H +#define DDTRACE_ROUTING_CACHE_H + +#include + +#define DDTRACE_ROUTING_CACHE_CAPACITY 500 + +void ddtrace_routing_cache_minit(void); +void ddtrace_routing_cache_mshutdown(void); + +PHP_FUNCTION(DDTrace_routing_cache_get); +PHP_FUNCTION(DDTrace_routing_cache_set); + +#endif /* DDTRACE_ROUTING_CACHE_H */ From f6bcae6781f098a4ecdde76b049656a6719ed616 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 18 Aug 2026 12:53:05 +0200 Subject: [PATCH 19/43] Fix pipeline --- config.m4 | 1 + 1 file changed, 1 insertion(+) diff --git a/config.m4 b/config.m4 index f89e3461e36..8f6fd16a38f 100644 --- a/config.m4 +++ b/config.m4 @@ -247,6 +247,7 @@ if test "$PHP_DDTRACE" != "no"; then tracer/priority_sampling/priority_sampling.c \ tracer/profiling.c \ tracer/random.c \ + tracer/routing_cache.c \ tracer/rule_matching.c \ tracer/serializer.c \ tracer/standalone_limiter.c \ From a14c69a1d4a72459594ba48294872e203142efe4 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 18 Aug 2026 14:58:13 +0200 Subject: [PATCH 20/43] Fix pipeline --- config.w32 | 1 + src/bridge/_files_tracer.php | 1 + .../ext/routing_cache/cache_lru_eviction.phpt | 19 +++++++------------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/config.w32 b/config.w32 index 716aed1c91a..fe15c9499e6 100644 --- a/config.w32 +++ b/config.w32 @@ -69,6 +69,7 @@ if (PHP_DDTRACE != 'no') { DDTRACE_TRACER_SOURCES += " tracer_otel_config.c"; DDTRACE_TRACER_SOURCES += " profiling.c"; DDTRACE_TRACER_SOURCES += " random.c"; + DDTRACE_TRACER_SOURCES += " routing_cache.c"; DDTRACE_TRACER_SOURCES += " rule_matching.c"; DDTRACE_TRACER_SOURCES += " serializer.c"; DDTRACE_TRACER_SOURCES += " span.c"; diff --git a/src/bridge/_files_tracer.php b/src/bridge/_files_tracer.php index 7d924b7fe7c..fccea720f0c 100644 --- a/src/bridge/_files_tracer.php +++ b/src/bridge/_files_tracer.php @@ -41,4 +41,5 @@ __DIR__ . '/../DDTrace/Propagators/TextMap.php', __DIR__ . '/../DDTrace/ScopeManager.php', __DIR__ . '/../DDTrace/Tracer.php', + __DIR__ . '/../DDTrace/Util/RouteNormalizer.php', ]; diff --git a/tests/ext/routing_cache/cache_lru_eviction.phpt b/tests/ext/routing_cache/cache_lru_eviction.phpt index 597049bfb57..7122ac864c7 100644 --- a/tests/ext/routing_cache/cache_lru_eviction.phpt +++ b/tests/ext/routing_cache/cache_lru_eviction.phpt @@ -1,31 +1,26 @@ --TEST-- -DDTrace\routing_cache evicts least-recently-used entry when capacity (500) is exceeded +DDTrace\routing_cache evicts the oldest inserted entry when capacity (500) is exceeded --FILE-- --EXPECT-- string(6) "value0" bool(false) -string(6) "value0" +string(6) "value1" string(8) "value500" From c7270d7bcbd57c01f197788794b8b86d9fb37260 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 18 Aug 2026 16:54:29 +0200 Subject: [PATCH 21/43] Fix pipeline --- src/DDTrace/Util/RouteNormalizer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 607c173cfb4..b737b7df304 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -26,7 +26,7 @@ public static function normalizeFromLaravel(string $routeUri, array $matchedPara * sections like [.json] that have no placeholder param * @return string|null */ - public static function normalizeFromSlim(string $pattern, array $matchedParams = [], ?string $urlPath = null) + public static function normalizeFromSlim(string $pattern, array $matchedParams = [], $urlPath = null) { return self::normalizeBraceRoute($pattern, $matchedParams, true, $urlPath); } @@ -39,7 +39,7 @@ public static function normalizeFromSlim(string $pattern, array $matchedParams = * route defaults); when provided, absent params are dropped * @return string|null */ - public static function normalizeFromSymfony(string $path, ?array $matchedParams = null) + public static function normalizeFromSymfony(string $path, $matchedParams = null) { if ($matchedParams !== null) { // Mark params absent from the URL as optional so normalizeBraceSegment drops them. @@ -299,7 +299,7 @@ private static function normalizeBraceRoute( string $route, array $matchedParams, bool $expandSquare = false, - ?string $urlPath = null + $urlPath = null ) { $route = trim($route); if ($route === '' || $route === '/') { @@ -396,7 +396,7 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar * to decide whether the literal text was part of the request; without it the * section is always kept (backward-compatible behaviour). */ - private static function expandSquareBracketOptionals(string $route, array $matchedParams, ?string $urlPath = null): string + private static function expandSquareBracketOptionals(string $route, array $matchedParams, $urlPath = null): string { $prev = null; while ($prev !== $route) { From 7209ed2655738991f4039d2ddae9dc2de21b257e Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 19 Aug 2026 11:10:02 +0200 Subject: [PATCH 22/43] Add cache to wordpress and Slim --- .../Integrations/Slim/SlimIntegration.php | 40 +++++++++++++++---- .../WordPress/WordPressIntegrationLoader.php | 10 ++++- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index c795d846cec..81731c2a4aa 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -77,8 +77,14 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { /** @var \Slim\Interfaces\RouteInterface $return */ $pattern = $return->getPattern(); $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); - if ($normalizedRoute !== null) { + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } @@ -99,8 +105,14 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $route = $return; $pattern = $route->getPattern(); $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); - if ($normalizedRoute !== null) { + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } @@ -146,8 +158,14 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; if ($pattern !== '') { $urlPath = $request->getUri()->getPath(); - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); - if ($normalizedRoute !== null) { + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } @@ -160,8 +178,14 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; if ($pattern !== '') { $urlPath = $request->getUri()->getPath(); - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); - if ($normalizedRoute !== null) { + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index 518dbd8e89a..a7ebcce9606 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -735,8 +735,14 @@ function_exists('is_404') && is_404() === false) { $matchedRule = $This->matched_rule; $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; $urlPath = \property_exists($This, 'request') ? $This->request : null; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); - if ($normalizedRoute !== null) { + $normalizedRoute = \DDTrace\routing_cache_get($matchedRule); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($matchedRule, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } From 39ba537ccc5dee3a7458f7ea2e515057188807cd Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 19 Aug 2026 11:44:42 +0200 Subject: [PATCH 23/43] Make hashtable thread safety --- tracer/ddtrace.c | 4 ++-- tracer/ddtrace_globals.h | 2 ++ tracer/routing_cache.c | 26 ++++++++++++-------------- tracer/routing_cache.h | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index bf4fe662fa4..3e6a08456b6 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -245,6 +245,7 @@ void ddtrace_ginit(zend_datadog_globals *ddtrace_globals) { UNUSED(ddtrace_globals); #endif zai_hook_ginit(); + ddtrace_routing_cache_ginit(); } void ddtrace_gshutdown(zend_datadog_globals *datadog_globals) { @@ -253,6 +254,7 @@ void ddtrace_gshutdown(zend_datadog_globals *datadog_globals) { if (datadog_globals->ddtrace.agent_config_reader) { ddog_agent_remote_config_reader_drop(datadog_globals->ddtrace.agent_config_reader); } + ddtrace_routing_cache_gshutdown(); } @@ -374,7 +376,6 @@ void ddtrace_minit_late() { ddtrace_live_debugger_minit(); ddtrace_trace_source_minit(); - ddtrace_routing_cache_minit(); } void ddtrace_mshutdown() { @@ -382,7 +383,6 @@ void ddtrace_mshutdown() { zai_hook_mshutdown(); ddtrace_unregister_functions_and_classes(); - ddtrace_routing_cache_mshutdown(); if (datadog_disable == 1) { return; diff --git a/tracer/ddtrace_globals.h b/tracer/ddtrace_globals.h index 062a02ca797..554f7b6cf76 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -113,6 +113,8 @@ typedef struct { HashTable resource_weak_storage; dtor_func_t resource_dtor_func; + HashTable rcache; + void *ffe_exposure_buffer; size_t ffe_exposure_buffer_len; size_t ffe_exposure_buffer_cap; diff --git a/tracer/routing_cache.c b/tracer/routing_cache.c index fe91fbe1acb..9c9bae54882 100644 --- a/tracer/routing_cache.c +++ b/tracer/routing_cache.c @@ -1,8 +1,6 @@ #include "routing_cache.h" #include "ddtrace.h" -static HashTable ddtrace_rcache; - static void ddtrace_routing_cache_dtor(zval *pz) { zend_string_release_ex((zend_string *)Z_PTR_P(pz), 1); } @@ -12,19 +10,19 @@ static void ddtrace_routing_cache_evict_oldest(void) { zend_string *key; zend_ulong num_idx; - zend_hash_internal_pointer_reset_ex(&ddtrace_rcache, &pos); - if (zend_hash_get_current_key_type_ex(&ddtrace_rcache, &pos) == HASH_KEY_IS_STRING) { - zend_hash_get_current_key_ex(&ddtrace_rcache, &key, &num_idx, &pos); - zend_hash_del(&ddtrace_rcache, key); + zend_hash_internal_pointer_reset_ex(&DDTRACE_G(rcache), &pos); + if (zend_hash_get_current_key_type_ex(&DDTRACE_G(rcache), &pos) == HASH_KEY_IS_STRING) { + zend_hash_get_current_key_ex(&DDTRACE_G(rcache), &key, &num_idx, &pos); + zend_hash_del(&DDTRACE_G(rcache), key); } } -void ddtrace_routing_cache_minit(void) { - zend_hash_init(&ddtrace_rcache, DDTRACE_ROUTING_CACHE_CAPACITY, NULL, ddtrace_routing_cache_dtor, 1); +void ddtrace_routing_cache_ginit(void) { + zend_hash_init(&DDTRACE_G(rcache), DDTRACE_ROUTING_CACHE_CAPACITY, NULL, ddtrace_routing_cache_dtor, 1); } -void ddtrace_routing_cache_mshutdown(void) { - zend_hash_destroy(&ddtrace_rcache); +void ddtrace_routing_cache_gshutdown(void) { + zend_hash_destroy(&DDTRACE_G(rcache)); } /* DDTrace\routing_cache_get(string $key): string|false */ @@ -34,7 +32,7 @@ PHP_FUNCTION(DDTrace_routing_cache_get) { Z_PARAM_STR(key) ZEND_PARSE_PARAMETERS_END(); - zend_string *value = zend_hash_find_ptr(&ddtrace_rcache, key); + zend_string *value = zend_hash_find_ptr(&DDTRACE_G(rcache), key); if (!value) { RETURN_FALSE; } @@ -49,11 +47,11 @@ PHP_FUNCTION(DDTrace_routing_cache_set) { Z_PARAM_STR(value) ZEND_PARSE_PARAMETERS_END(); - if (zend_hash_num_elements(&ddtrace_rcache) >= DDTRACE_ROUTING_CACHE_CAPACITY - && !zend_hash_find_ptr(&ddtrace_rcache, key)) { + if (zend_hash_num_elements(&DDTRACE_G(rcache)) >= DDTRACE_ROUTING_CACHE_CAPACITY + && !zend_hash_find_ptr(&DDTRACE_G(rcache), key)) { ddtrace_routing_cache_evict_oldest(); } zend_string *persistent_value = zend_string_init(ZSTR_VAL(value), ZSTR_LEN(value), 1); - zend_hash_str_update_ptr(&ddtrace_rcache, ZSTR_VAL(key), ZSTR_LEN(key), persistent_value); + zend_hash_str_update_ptr(&DDTRACE_G(rcache), ZSTR_VAL(key), ZSTR_LEN(key), persistent_value); } diff --git a/tracer/routing_cache.h b/tracer/routing_cache.h index 84804d3882a..9a0acbfe764 100644 --- a/tracer/routing_cache.h +++ b/tracer/routing_cache.h @@ -5,8 +5,8 @@ #define DDTRACE_ROUTING_CACHE_CAPACITY 500 -void ddtrace_routing_cache_minit(void); -void ddtrace_routing_cache_mshutdown(void); +void ddtrace_routing_cache_ginit(void); +void ddtrace_routing_cache_gshutdown(void); PHP_FUNCTION(DDTrace_routing_cache_get); PHP_FUNCTION(DDTrace_routing_cache_set); From 2cd7c318719316a602f403efd7ea910a5ad642b6 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 19 Aug 2026 13:25:52 +0200 Subject: [PATCH 24/43] Fix pipeline --- tracer/routing_cache.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tracer/routing_cache.c b/tracer/routing_cache.c index 9c9bae54882..54475c6dc7f 100644 --- a/tracer/routing_cache.c +++ b/tracer/routing_cache.c @@ -1,6 +1,8 @@ #include "routing_cache.h" #include "ddtrace.h" +ZEND_EXTERN_MODULE_GLOBALS(datadog); + static void ddtrace_routing_cache_dtor(zval *pz) { zend_string_release_ex((zend_string *)Z_PTR_P(pz), 1); } From 750ed5d6c04613be761c8ff7e783c4f24757729d Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 19 Aug 2026 14:50:32 +0200 Subject: [PATCH 25/43] Fix pipeline --- src/DDTrace/Util/RouteNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index b737b7df304..76bb2e96d8b 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -519,7 +519,7 @@ private static function cakephpToBraces(string $template): string { $result = preg_replace('#/\*\*#', '/{catchall}', $template); $result = preg_replace('#/\*(?!\*)#', '/{catchall}', $result); - $result = preg_replace('#(? Date: Wed, 19 Aug 2026 15:44:03 +0200 Subject: [PATCH 26/43] Fix pipeline --- tracer/ddtrace.c | 4 ++-- tracer/routing_cache.c | 8 ++++---- tracer/routing_cache.h | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index 3e6a08456b6..6b49697402e 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -245,7 +245,7 @@ void ddtrace_ginit(zend_datadog_globals *ddtrace_globals) { UNUSED(ddtrace_globals); #endif zai_hook_ginit(); - ddtrace_routing_cache_ginit(); + ddtrace_routing_cache_ginit(&ddtrace_globals->ddtrace.rcache); } void ddtrace_gshutdown(zend_datadog_globals *datadog_globals) { @@ -254,7 +254,7 @@ void ddtrace_gshutdown(zend_datadog_globals *datadog_globals) { if (datadog_globals->ddtrace.agent_config_reader) { ddog_agent_remote_config_reader_drop(datadog_globals->ddtrace.agent_config_reader); } - ddtrace_routing_cache_gshutdown(); + ddtrace_routing_cache_gshutdown(&datadog_globals->ddtrace.rcache); } diff --git a/tracer/routing_cache.c b/tracer/routing_cache.c index 54475c6dc7f..a69f7cd1cbe 100644 --- a/tracer/routing_cache.c +++ b/tracer/routing_cache.c @@ -19,12 +19,12 @@ static void ddtrace_routing_cache_evict_oldest(void) { } } -void ddtrace_routing_cache_ginit(void) { - zend_hash_init(&DDTRACE_G(rcache), DDTRACE_ROUTING_CACHE_CAPACITY, NULL, ddtrace_routing_cache_dtor, 1); +void ddtrace_routing_cache_ginit(HashTable *rcache) { + zend_hash_init(rcache, DDTRACE_ROUTING_CACHE_CAPACITY, NULL, ddtrace_routing_cache_dtor, 1); } -void ddtrace_routing_cache_gshutdown(void) { - zend_hash_destroy(&DDTRACE_G(rcache)); +void ddtrace_routing_cache_gshutdown(HashTable *rcache) { + zend_hash_destroy(rcache); } /* DDTrace\routing_cache_get(string $key): string|false */ diff --git a/tracer/routing_cache.h b/tracer/routing_cache.h index 9a0acbfe764..37961f5ba84 100644 --- a/tracer/routing_cache.h +++ b/tracer/routing_cache.h @@ -5,8 +5,8 @@ #define DDTRACE_ROUTING_CACHE_CAPACITY 500 -void ddtrace_routing_cache_ginit(void); -void ddtrace_routing_cache_gshutdown(void); +void ddtrace_routing_cache_ginit(HashTable *rcache); +void ddtrace_routing_cache_gshutdown(HashTable *rcache); PHP_FUNCTION(DDTrace_routing_cache_get); PHP_FUNCTION(DDTrace_routing_cache_set); From e4ee77116191940bddbed0e5d3c7d345039a8267 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 20 Aug 2026 11:14:27 +0200 Subject: [PATCH 27/43] Fix codeigniter --- .../Integrations/CodeIgniter/V2/CodeIgniterIntegration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index 4e5cb94ea29..aecd169bbd6 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -222,7 +222,7 @@ function (SpanData $span, $args, $retval, $ex) use ($adapter, $service) { /* * Replicate CodeIgniter's route parsing, as matching key is never stored or returned in the framework. */ - private static function setNormalizedRoute($rootSpan, string $pattern): void { + private static function setNormalizedRoute($rootSpan, string $pattern) { $cacheKey = $pattern; $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { From f42c4087d8531004d6ba002e0b9d0b199a1693b3 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 26 Aug 2026 12:13:09 +0200 Subject: [PATCH 28/43] Add appsec integration tests --- .../php/integration/Laminas33Tests.groovy | 34 +++++++++++++++++++ .../php/integration/Laravel8xTests.groovy | 4 +++ .../php/integration/Symfony62Tests.groovy | 4 +++ 3 files changed, 42 insertions(+) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy index dd365c15c0b..91e6118a9bc 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -231,6 +231,7 @@ class Laminas33Tests { assert span.meta.'_dd.appsec.event_rules.version' != '' assert span.meta.'appsec.blocked' == 'true' assert span.meta.'http.route' == '/dynamic-path[/:param01]' + assert span.meta.'_dd.appsec.normalized_route' == '/dynamic-path/{param01}' } @Test @@ -241,12 +242,14 @@ class Laminas33Tests { assert resp.statusCode() == 200 } assert nestedTrace.first().meta.'http.route' == '/resource/:resourceId/:subId' + assert nestedTrace.first().meta.'_dd.appsec.normalized_route' == '/resource/{resourceId}/{subId}' HttpRequest chainReq = container.buildReq('/chain/abc').GET().build() Trace chainTrace = container.traceFromRequest(chainReq, ofString()) { HttpResponse resp -> assert resp.statusCode() == 200 } assert chainTrace.first().meta.'http.route' == '/chain/:chainId' + assert chainTrace.first().meta.'_dd.appsec.normalized_route' == '/chain/{chainId}' } @Test @@ -271,6 +274,7 @@ class Laminas33Tests { assert resp.statusCode() == 200 } assert regexTrace.first().meta.'http.route' == '/regex-year/%year%' + assert regexTrace.first().meta.'_dd.appsec.normalized_route' == '/regex-year/{year}' Trace schemeTrace = container.traceFromRequest( container.buildReq('/scheme-only-page').GET().build(), @@ -278,6 +282,7 @@ class Laminas33Tests { assert resp.statusCode() == 200 } assert schemeTrace.first().meta.'http.route' == '/scheme-only-page' + assert schemeTrace.first().meta.'_dd.appsec.normalized_route' == '/scheme-only-page' Trace placeholderTrace = container.traceFromRequest( container.buildReq('/placeholder-literal').GET().build(), @@ -285,6 +290,7 @@ class Laminas33Tests { assert resp.statusCode() == 200 } assert placeholderTrace.first().meta.'http.route' == '/placeholder-literal' + assert placeholderTrace.first().meta.'_dd.appsec.normalized_route' == '/placeholder-literal' Trace wildcardTrace = container.traceFromRequest( container.buildReq('/wildcard-keys/foo/bar').GET().build(), @@ -292,5 +298,33 @@ class Laminas33Tests { assert resp.statusCode() == 200 } assert wildcardTrace.first().meta.'http.route' == '/wildcard-keys/*' + assert wildcardTrace.first().meta.'_dd.appsec.normalized_route' == '/wildcard-keys/{param1}' + } + + @Test + @Order(11) + void 'optional segment absent produces correct normalized route'() { + // /application[/:action] with no action in URL — optional section dropped + // (default action=index is injected by the router but /index is not in the URL path) + Trace trace = container.traceFromRequest( + container.buildReq('/application').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + assert trace.first().meta.'http.route' == '/application[/:action]' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/application' + } + + @Test + @Order(12) + void 'optional segment present produces correct normalized route'() { + // /application[/:action] with action in URL — optional section expanded + Trace trace = container.traceFromRequest( + container.buildReq('/application/test').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + assert trace.first().meta.'http.route' == '/application[/:action]' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/application/{action}' } } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy index 08d0642e4e0..e47eb53c78b 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy @@ -171,6 +171,10 @@ class Laravel8xTests { assert span.metrics."_dd.appsec.waf.duration" > 0.0d assert span.meta."_dd.appsec.event_rules.version" != '' assert span.meta."appsec.blocked" == "true" + // Laravel uri() returns the route without a leading slash + assert span.meta."http.route" == 'dynamic-path/{param01}' + // Normalizer adds the leading slash and keeps {param01} as-is + assert span.meta."_dd.appsec.normalized_route" == '/dynamic-path/{param01}' } @Test diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index ccc149ba143..1d981bc55ba 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -116,6 +116,7 @@ class Symfony62Tests { assert span.meta."_dd.appsec.event_rules.version" != '' assert span.meta."appsec.blocked" == "true" assert span.meta."http.route" == '/dynamic-path/{param01}' + assert span.meta."_dd.appsec.normalized_route" == '/dynamic-path/{param01}' } @Test @@ -129,6 +130,7 @@ class Symfony62Tests { Span span = trace.first() assert span.meta."http.route" == '/caminho-dinamico/{param01}' + assert span.meta."_dd.appsec.normalized_route" == '/caminho-dinamico/{param01}' } @Test @@ -141,6 +143,8 @@ class Symfony62Tests { Span span = trace.first() assert span.meta."http.route" == '/café/{item}' + // Static segment 'café' is percent-encoded per RFC 3986; é (U+00E9) → %C3%A9 + assert span.meta."_dd.appsec.normalized_route" == '/caf%C3%A9/{item}' } @Test From ba956c46d5554d7e012b9c971dad56b3e28688ca Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 26 Aug 2026 13:29:23 +0200 Subject: [PATCH 29/43] Fix integration tests --- .../com/datadog/appsec/php/integration/Laminas33Tests.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy index 91e6118a9bc..c6d6f5f83d7 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -320,7 +320,7 @@ class Laminas33Tests { void 'optional segment present produces correct normalized route'() { // /application[/:action] with action in URL — optional section expanded Trace trace = container.traceFromRequest( - container.buildReq('/application/test').GET().build(), + container.buildReq('/application/hello').GET().build(), ofString()) { HttpResponse resp -> assert resp.statusCode() == 200 } From 0273d76ff42801c5a879199bfb5bb44c527e526f Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 10:27:18 +0200 Subject: [PATCH 30/43] Amend test file name --- .../{cache_lru_eviction.phpt => cache_capacity_eviction.phpt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/ext/routing_cache/{cache_lru_eviction.phpt => cache_capacity_eviction.phpt} (100%) diff --git a/tests/ext/routing_cache/cache_lru_eviction.phpt b/tests/ext/routing_cache/cache_capacity_eviction.phpt similarity index 100% rename from tests/ext/routing_cache/cache_lru_eviction.phpt rename to tests/ext/routing_cache/cache_capacity_eviction.phpt From e02670a04341995ef9e9cd4d9988a1f5c66c83c8 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 11:19:57 +0200 Subject: [PATCH 31/43] Generate normalise routes only when appsec is enabled --- .../CakePHP/CakePHPIntegration.php | 19 ++--- .../CodeIgniter/V2/CodeIgniterIntegration.php | 3 + .../Laminas/LaminasIntegration.php | 12 +-- .../Laravel/LaravelIntegration.php | 20 ++--- .../Integrations/Slim/SlimIntegration.php | 80 ++++++++++--------- .../Symfony/SymfonyIntegration.php | 10 ++- .../WordPress/WordPressIntegrationLoader.php | 20 ++--- .../Integrations/Yii/YiiIntegration.php | 20 ++--- .../CakePHP/V2_8/CommonScenariosTest.php | 4 - .../CakePHP/V3_10/CommonScenariosTest.php | 4 - .../CakePHP/V4_5/CommonScenariosTest.php | 4 - .../CodeIgniter/V2_2/CommonScenariosTest.php | 4 - .../CodeIgniter/V2_2/ExitTest.php | 1 - .../CodeIgniter/V2_2/NoCI_ControllertTest.php | 1 - .../Octane/Latest/CommonScenariosTest.php | 3 - .../Laravel/V4/CommonScenariosTest.php | 3 - .../Laravel/V4/TraceSearchConfigTest.php | 1 - .../Laravel/V5_7/TraceSearchConfigTest.php | 1 - .../Laravel/V5_8/TraceSearchConfigTest.php | 1 - .../Laravel/V8_x/InternalExceptionsTest.php | 2 - .../Laravel/V8_x/RouteCachingTest.php | 2 - .../Laravel/V8_x/TraceSearchConfigTest.php | 1 - .../Slim/Latest/CommonScenariosTest.php | 4 - .../Slim/V3_12/CommonScenariosTest.php | 4 - .../Symfony/Latest/CommonScenariosTest.php | 3 - .../Symfony/V4_4/CommonScenariosTest.php | 3 - .../Symfony/V4_4/TraceSearchConfigTest.php | 1 - .../Symfony/V5_0/CommonScenariosTest.php | 3 - .../Symfony/V5_0/TraceSearchConfigTest.php | 1 - .../Symfony/V5_1/CommonScenariosTest.php | 3 - .../Symfony/V5_1/TraceSearchConfigTest.php | 1 - .../Symfony/V5_2/CommonScenariosTest.php | 3 - .../Symfony/V5_2/TraceSearchConfigTest.php | 1 - .../Symfony/V6_2/CommonScenariosTest.php | 3 - .../Symfony/V6_2/TraceSearchConfigTest.php | 1 - .../Symfony/V7_3/CommonScenariosTest.php | 3 - .../Yii/Latest/CommonScenariosTest.php | 4 - .../LazyLoadingIntegrationsFromYiiTest.php | 1 - tests/Integrations/Yii/Latest/ModuleTest.php | 1 - .../Yii/Latest/ParameterizedRouteTest.php | 1 - ...oller_test.test_scenario_health_check.json | 1 - ..._test.test_scenario_get_parameterized.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...st.test_scenario_get_to_missing_route.json | 1 - ...est_scenario_get_to_missing_route_cgi.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ....test_scenario_get_with_exception_cgi.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ...ter.v3_1.exit_test.test_scenario_exit.json | 1 - ...atest.rest_test.test_scenario_rest2xx.json | 1 - ...atest.rest_test.test_scenario_rest4xx.json | 1 - ...atest.rest_test.test_scenario_rest5xx.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ...pigw_test.test_laravel_inferred_proxy.json | 1 - ...test_laravel_inferred_proxy_exception.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...t_scenario_get_with_ignored_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ...octane.apigw_test.test_inferred_proxy.json | 1 - ...gw_test.test_inferred_proxy_exception.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...t_scenario_get_with_ignored_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_dynamic_route.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_dynamic_route.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_dynamic_route.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ...aravel.v8_x.queue_test.test_broadcast.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ...est.messenger_test.test_async_failure.json | 1 - ...est.messenger_test.test_async_success.json | 1 - ...async_with_tracer_disabled_on_consume.json | 1 - ...4_4.messenger_test.test_async_failure.json | 1 - ...4_4.messenger_test.test_async_success.json | 1 - ...async_with_tracer_disabled_on_consume.json | 1 - ...5_2.messenger_test.test_async_failure.json | 1 - ...5_2.messenger_test.test_async_success.json | 1 - ...async_with_tracer_disabled_on_consume.json | 1 - ...6_2.messenger_test.test_async_failure.json | 1 - ...6_2.messenger_test.test_async_success.json | 1 - ...async_with_tracer_disabled_on_consume.json | 1 - ...7_3.messenger_test.test_async_failure.json | 1 - ...7_3.messenger_test.test_async_success.json | 1 - ...async_with_tracer_disabled_on_consume.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...acks_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...acks_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...acks_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...acks_test.test_scenario_get_with_view.json | 1 - ..._test.test_scenario_get_return_string.json | 1 - ...test.test_scenario_get_with_exception.json | 1 - ...rios_test.test_scenario_get_with_view.json | 1 - 128 files changed, 103 insertions(+), 242 deletions(-) diff --git a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php index 9e9f171ba51..b07a586f17b 100644 --- a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php +++ b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php @@ -73,16 +73,17 @@ public static function init(): int if ($rootSpan !== null) { $template = $app->template; $rootSpan->meta[Tag::HTTP_ROUTE] = $template; - $cacheKey = $template; - $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCakePHP($template); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $normalizedRoute = \DDTrace\routing_cache_get($template); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCakePHP($template); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($template, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } }; diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index aecd169bbd6..8c98cad9deb 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -223,6 +223,9 @@ function (SpanData $span, $args, $retval, $ex) use ($adapter, $service) { * Replicate CodeIgniter's route parsing, as matching key is never stored or returned in the framework. */ private static function setNormalizedRoute($rootSpan, string $pattern) { + if (!function_exists('\datadog\appsec\is_enabled') || !\datadog\appsec\is_enabled()) { + return; + } $cacheKey = $pattern; $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index b40817338c7..1111572d1f7 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -293,11 +293,13 @@ static function (SpanData $span) use ($controller, $action) { } if ($httpRoute !== null && $httpRoute !== false && $httpRoute !== '') { $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; - $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; - $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); - if ($normalizedRoute !== null) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; + $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } } } diff --git a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php index 9c2fcea9077..628cc30ab0e 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -142,16 +142,18 @@ static function ($This, $scope, $args, $route) { if (\method_exists($route, 'uri')) { $httpRoute = $route->uri(); $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; - $normalizedRoute = \DDTrace\routing_cache_get($httpRoute); - if ($normalizedRoute === false) { - $matchedParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaravel($httpRoute, $matchedParams); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($httpRoute, $normalizedRoute); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $normalizedRoute = \DDTrace\routing_cache_get($httpRoute); + if ($normalizedRoute === false) { + $matchedParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaravel($httpRoute, $matchedParams); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($httpRoute, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } if (\method_exists($route, 'parameters') && function_exists('\datadog\appsec\push_addresses')) { diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index 81731c2a4aa..bff645e8504 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -77,15 +77,17 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { /** @var \Slim\Interfaces\RouteInterface $return */ $pattern = $return->getPattern(); $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { @@ -105,15 +107,17 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $route = $return; $pattern = $route->getPattern(); $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } ); @@ -154,7 +158,30 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $rootSpan->meta['slim.route.name'] = $routeName; } // Refine normalized route now that matched params are available - $matchedParams = method_exists($route, 'getArguments') ? ($route->getArguments() ?? []) : []; + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $matchedParams = method_exists($route, 'getArguments') ? ($route->getArguments() ?? []) : []; + $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; + if ($pattern !== '') { + $urlPath = $request->getUri()->getPath(); + $normalizedRoute = \DDTrace\routing_cache_get($pattern); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($pattern, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } + } + } + } + } else { + $rootSpan->meta['slim.route.controller'] = $callableName; + $span->name = 'slim.route.controller'; + // Refine normalized route now that matched params are available (Slim 3) + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $matchedParams = isset($args[3]) && is_array($args[3]) ? $args[3] : []; $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; if ($pattern !== '') { $urlPath = $request->getUri()->getPath(); @@ -170,25 +197,6 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { } } } - } else { - $rootSpan->meta['slim.route.controller'] = $callableName; - $span->name = 'slim.route.controller'; - // Refine normalized route now that matched params are available (Slim 3) - $matchedParams = isset($args[3]) && is_array($args[3]) ? $args[3] : []; - $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; - if ($pattern !== '') { - $urlPath = $request->getUri()->getPath(); - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } } }; diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index a2c73516f23..b4890bebb5c 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -466,10 +466,12 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; - $matchedParams = self::inferSymfonyRouteParams($path, $request->getPathInfo()); - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); - if ($normalizedRoute !== null) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $matchedParams = self::inferSymfonyRouteParams($path, $request->getPathInfo()); + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); + if ($normalizedRoute !== null) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; + } } } }; diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index a7ebcce9606..035b0fa2f20 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -734,16 +734,18 @@ function_exists('is_404') && is_404() === false) { if (\property_exists($This, 'matched_rule')) { $matchedRule = $This->matched_rule; $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; - $urlPath = \property_exists($This, 'request') ? $This->request : null; - $normalizedRoute = \DDTrace\routing_cache_get($matchedRule); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($matchedRule, $normalizedRoute); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $urlPath = \property_exists($This, 'request') ? $This->request : null; + $normalizedRoute = \DDTrace\routing_cache_get($matchedRule); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($matchedRule, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } } diff --git a/src/DDTrace/Integrations/Yii/YiiIntegration.php b/src/DDTrace/Integrations/Yii/YiiIntegration.php index 32269f49285..b9368921b7f 100644 --- a/src/DDTrace/Integrations/Yii/YiiIntegration.php +++ b/src/DDTrace/Integrations/Yii/YiiIntegration.php @@ -156,16 +156,18 @@ function (SpanData $span, $args) use (&$firstController) { $rootSpan->meta['app.route.path'] = $routePath; $rootSpan->meta[Tag::HTTP_ROUTE] = $routePath; - $cacheKey = $routePath; - $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromYii($routePath); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $cacheKey = $routePath; + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromYii($routePath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { + $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { diff --git a/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php b/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php index 73640dd8ba5..506df7debe9 100644 --- a/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php +++ b/tests/Integrations/CakePHP/V2_8/CommonScenariosTest.php @@ -58,7 +58,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', 'http.route' => '/:controller', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -85,7 +84,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', 'http.route' => '/:controller', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -121,7 +119,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', 'http.route' => '/:controller', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withExistingTagsNames([ @@ -164,7 +161,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/parameterized/paramValue', 'http.status_code' => '200', 'http.route' => '/parameterized/:param', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ diff --git a/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php b/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php index af0184198ea..76a6440d449 100644 --- a/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php +++ b/tests/Integrations/CakePHP/V3_10/CommonScenariosTest.php @@ -59,7 +59,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', 'http.route' => '/{controller}', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -86,7 +85,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', 'http.route' => '/{controller}', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ @@ -122,7 +120,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', 'http.route' => '/{controller}', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withExistingTagsNames([ @@ -165,7 +162,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/parameterized/paramValue', 'http.status_code' => '200', 'http.route' => '/parameterized/:param', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', ])->withChildren([ diff --git a/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php b/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php index 953d3705cb9..f1e85015734 100644 --- a/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php +++ b/tests/Integrations/CakePHP/V4_5/CommonScenariosTest.php @@ -61,7 +61,6 @@ public function provideSpecs() 'http.route' => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', ])->withChildren([ SpanAssertion::build( 'Controller.invokeAction', @@ -88,7 +87,6 @@ public function provideSpecs() 'http.route' => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', ])->withChildren([ SpanAssertion::build( 'Controller.invokeAction', @@ -125,7 +123,6 @@ public function provideSpecs() 'http.route' => '/{controller}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', - Tag::APPSEC_NORMALIZED_ROUTE => '/{controller}', ])->withExistingTagsNames([ 'error.stack' ])->setError( @@ -168,7 +165,6 @@ public function provideSpecs() 'http.route' => '/parameterized/{param}', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'cakephp', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param}', ])->withChildren([ SpanAssertion::build( 'Controller.invokeAction', diff --git a/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php b/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php index 900e78630b5..f7f15701e12 100644 --- a/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/CommonScenariosTest.php @@ -56,7 +56,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ SpanAssertion::build( 'Simple.index', @@ -82,7 +81,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ SpanAssertion::build( 'Simple_View.index', @@ -118,7 +116,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', ]) ->setError("Exception", "Uncaught Exception: datadog in %s:%d") ->withExistingTagsNames(['error.stack']) @@ -147,7 +144,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'parameterized/(:any)', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{param1}', ])->withChildren([ SpanAssertion::build( 'Parameterized.customAction', diff --git a/tests/Integrations/CodeIgniter/V2_2/ExitTest.php b/tests/Integrations/CodeIgniter/V2_2/ExitTest.php index 78f97fbd723..6b6a5a32d7c 100644 --- a/tests/Integrations/CodeIgniter/V2_2/ExitTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/ExitTest.php @@ -44,7 +44,6 @@ public function testScenario() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'exits', - Tag::APPSEC_NORMALIZED_ROUTE => '/exits', ])->withChildren([ SpanAssertion::build( 'Exits.index', diff --git a/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php b/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php index 6cc6b584ec5..ebdee1225e2 100644 --- a/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/NoCI_ControllertTest.php @@ -43,7 +43,6 @@ public function testScenario() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', Tag::HTTP_ROUTE => 'health_check/ping', - Tag::APPSEC_NORMALIZED_ROUTE => '/health_check/ping', ])->withChildren([ SpanAssertion::build( 'Health_check.ping', diff --git a/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php b/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php index a6c5489b504..a787ff2574c 100644 --- a/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Laravel/Octane/Latest/CommonScenariosTest.php @@ -102,7 +102,6 @@ public function testScenarioGetReturnString() Tag::HTTP_METHOD => 'GET', Tag::HTTP_URL => 'http://localhost/simple?key=value&', Tag::HTTP_ROUTE => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', Tag::HTTP_STATUS_CODE => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', @@ -175,7 +174,6 @@ public function testScenarioGetWithView() Tag::HTTP_METHOD => 'GET', Tag::HTTP_URL => 'http://localhost/simple_view?key=value&', Tag::HTTP_ROUTE => 'simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', Tag::HTTP_STATUS_CODE => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', @@ -265,7 +263,6 @@ public function testScenarioGetWithException() Tag::HTTP_METHOD => 'GET', Tag::HTTP_URL => 'http://localhost/error?key=value&', Tag::HTTP_ROUTE => 'error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', Tag::HTTP_STATUS_CODE => '500', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', diff --git a/tests/Integrations/Laravel/V4/CommonScenariosTest.php b/tests/Integrations/Laravel/V4/CommonScenariosTest.php index b5f6fa48465..122a6947175 100644 --- a/tests/Integrations/Laravel/V4/CommonScenariosTest.php +++ b/tests/Integrations/Laravel/V4/CommonScenariosTest.php @@ -57,7 +57,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', 'http.route' => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'some.key1' => 'value', 'some.key2' => 'value2', TAG::SPAN_KIND => 'server', @@ -150,7 +149,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', 'http.route' => 'error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'some.key1' => 'value', 'some.key2' => 'value2', TAG::SPAN_KIND => 'server', @@ -201,7 +199,6 @@ public function provideSpecs() 'http.url' => 'http://localhost/dynamic_route/dynamic01/static/dynamic02', 'http.status_code' => '200', 'http.route' => 'dynamic_route/{param01}/static/{param02?}', - Tag::APPSEC_NORMALIZED_ROUTE => '/dynamic_route/{param01}/static/{param02}', 'some.key1' => 'value', 'some.key2' => 'value2', TAG::SPAN_KIND => 'server', diff --git a/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php index 4e9503f841f..3109b07ddeb 100644 --- a/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V4/TraceSearchConfigTest.php @@ -44,7 +44,6 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php index 80ece12dd99..402c38eab84 100644 --- a/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V5_7/TraceSearchConfigTest.php @@ -49,7 +49,6 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php index 69b60c54eaf..3d8fd4681ef 100644 --- a/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V5_8/TraceSearchConfigTest.php @@ -49,7 +49,6 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', Tag::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php b/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php index e36a8fd2cc5..8441164c14d 100644 --- a/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php +++ b/tests/Integrations/Laravel/V8_x/InternalExceptionsTest.php @@ -46,7 +46,6 @@ public function testNotImplemented() 'http.url' => 'http://localhost/not-implemented', 'http.status_code' => '501', 'http.route' => 'not-implemented', - Tag::APPSEC_NORMALIZED_ROUTE => '/not-implemented', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', @@ -110,7 +109,6 @@ public function testUnauthorized() 'http.url' => 'http://localhost/unauthorized', 'http.status_code' => '403', 'http.route' => 'unauthorized', - Tag::APPSEC_NORMALIZED_ROUTE => '/unauthorized', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V8_x/RouteCachingTest.php b/tests/Integrations/Laravel/V8_x/RouteCachingTest.php index 439caf00ea7..bd32474fc51 100644 --- a/tests/Integrations/Laravel/V8_x/RouteCachingTest.php +++ b/tests/Integrations/Laravel/V8_x/RouteCachingTest.php @@ -45,7 +45,6 @@ public function testNotCached() 'http.url' => 'http://localhost/unnamed-route', 'http.status_code' => '200', 'http.route' => 'unnamed-route', - Tag::APPSEC_NORMALIZED_ROUTE => '/unnamed-route', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', @@ -95,7 +94,6 @@ public function testCached() 'http.url' => 'http://localhost/unnamed-route', 'http.status_code' => '200', 'http.route' => 'unnamed-route', - Tag::APPSEC_NORMALIZED_ROUTE => '/unnamed-route', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php b/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php index 849acf0f3f0..0d76259fbcf 100644 --- a/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php +++ b/tests/Integrations/Laravel/V8_x/TraceSearchConfigTest.php @@ -49,7 +49,6 @@ public function testScenario() 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', 'http.route' => 'simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', TAG::SPAN_KIND => 'server', TAG::COMPONENT => 'laravel', '_dd.svc_src' => 'laravel', diff --git a/tests/Integrations/Slim/Latest/CommonScenariosTest.php b/tests/Integrations/Slim/Latest/CommonScenariosTest.php index 2ab60ff6f8d..e801ed8b350 100644 --- a/tests/Integrations/Slim/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Slim/Latest/CommonScenariosTest.php @@ -122,7 +122,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ $this->wrapMiddleware([ SpanAssertion::build( @@ -151,7 +150,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ $this->wrapMiddleware([ SpanAssertion::build( @@ -189,7 +187,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', ]) ->setError(null, null) ->withChildren([ @@ -224,7 +221,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/parameterized/{value}', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{value}', ])->withChildren([ $this->wrapMiddleware([ SpanAssertion::build( diff --git a/tests/Integrations/Slim/V3_12/CommonScenariosTest.php b/tests/Integrations/Slim/V3_12/CommonScenariosTest.php index 8509d73fd8b..b97d16527f0 100644 --- a/tests/Integrations/Slim/V3_12/CommonScenariosTest.php +++ b/tests/Integrations/Slim/V3_12/CommonScenariosTest.php @@ -59,7 +59,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ SpanAssertion::build( 'slim.route.controller', @@ -85,7 +84,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ SpanAssertion::build( 'slim.route.controller', @@ -121,7 +119,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', ])->setError(null, null) ->withChildren([ SpanAssertion::build( @@ -150,7 +147,6 @@ public function provideSpecs() Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'slim', Tag::HTTP_ROUTE => '/parameterized/{value}', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{value}', ])->withChildren([ SpanAssertion::build( 'slim.route.controller', diff --git a/tests/Integrations/Symfony/Latest/CommonScenariosTest.php b/tests/Integrations/Symfony/Latest/CommonScenariosTest.php index 0a301317752..f7a1571c7af 100644 --- a/tests/Integrations/Symfony/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/Latest/CommonScenariosTest.php @@ -74,7 +74,6 @@ public function provideSpecs() 'http.status_code' => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'symfony', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', ])->withChildren([ SpanAssertion::exists('symfony.httpkernel.kernel.handle') ->withChildren([ @@ -114,7 +113,6 @@ public function provideSpecs() 'http.status_code' => '200', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'symfony', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', ])->withChildren([ SpanAssertion::exists('symfony.kernel.terminate'), SpanAssertion::exists('symfony.httpkernel.kernel.handle')->withChildren([ @@ -161,7 +159,6 @@ public function provideSpecs() 'http.status_code' => '500', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'symfony', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', ]) ->setError('Exception', 'An exception occurred') ->withExistingTagsNames(['error.stack']) diff --git a/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php b/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php index 7b14922710a..67aa59f88e8 100644 --- a/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V4_4/CommonScenariosTest.php @@ -56,7 +56,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -98,7 +97,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -147,7 +145,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php index 66635c56d18..ccf749e2c2b 100644 --- a/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V4_4/TraceSearchConfigTest.php @@ -43,7 +43,6 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php b/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php index b7bfd5a2224..88ae9321a31 100644 --- a/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V5_0/CommonScenariosTest.php @@ -56,7 +56,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -98,7 +97,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -147,7 +145,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php index 54b312ba433..7c9cbfee07a 100644 --- a/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V5_0/TraceSearchConfigTest.php @@ -43,7 +43,6 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php b/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php index d53ab79678e..66558d82595 100644 --- a/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V5_1/CommonScenariosTest.php @@ -56,7 +56,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -98,7 +97,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -147,7 +145,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php index cb489e54d72..5044eafd656 100644 --- a/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V5_1/TraceSearchConfigTest.php @@ -43,7 +43,6 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php b/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php index 90438617316..b3df1fbe337 100644 --- a/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V5_2/CommonScenariosTest.php @@ -56,7 +56,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -96,7 +95,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -143,7 +141,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php index 57fa72db8dd..1a622ae42b4 100644 --- a/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V5_2/TraceSearchConfigTest.php @@ -43,7 +43,6 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php b/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php index 4dd7b39311a..09c7927a953 100644 --- a/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V6_2/CommonScenariosTest.php @@ -56,7 +56,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -96,7 +95,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -143,7 +141,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php b/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php index 7a9d8e90a1f..aaf2f4acc85 100644 --- a/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php +++ b/tests/Integrations/Symfony/V6_2/TraceSearchConfigTest.php @@ -43,7 +43,6 @@ public function testScenario() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', diff --git a/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php b/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php index 6b71b4139fb..9d80fac2810 100644 --- a/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php +++ b/tests/Integrations/Symfony/V7_3/CommonScenariosTest.php @@ -69,7 +69,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleAction', 'symfony.route.name' => 'simple', 'http.route' => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', @@ -109,7 +108,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@simpleViewAction', 'symfony.route.name' => 'simple_view', 'http.route' => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', @@ -156,7 +154,6 @@ public function provideSpecs() 'symfony.route.action' => 'App\Controller\CommonScenariosController@errorAction', 'symfony.route.name' => 'error', 'http.route' => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', diff --git a/tests/Integrations/Yii/Latest/CommonScenariosTest.php b/tests/Integrations/Yii/Latest/CommonScenariosTest.php index 8a8509739aa..62410148e30 100644 --- a/tests/Integrations/Yii/Latest/CommonScenariosTest.php +++ b/tests/Integrations/Yii/Latest/CommonScenariosTest.php @@ -59,7 +59,6 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionIndex', 'app.route.path' => '/simple', Tag::HTTP_ROUTE => '/simple', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ])->withChildren([ @@ -104,7 +103,6 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionView', 'app.route.path' => '/simple_view', Tag::HTTP_ROUTE => '/simple_view', - Tag::APPSEC_NORMALIZED_ROUTE => '/simple_view', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ])->withChildren([ @@ -152,7 +150,6 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionError', 'app.route.path' => '/error', Tag::HTTP_ROUTE => '/error', - Tag::APPSEC_NORMALIZED_ROUTE => '/error', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ]) @@ -225,7 +222,6 @@ public function provideSpecs() 'app.endpoint' => 'app\controllers\SimpleController::actionParameterized', 'app.route.path' => '/parameterized/:value', Tag::HTTP_ROUTE => '/parameterized/:value', - Tag::APPSEC_NORMALIZED_ROUTE => '/parameterized/{value}', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", ])->withChildren([ diff --git a/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php b/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php index 7059955484d..7f78b327371 100644 --- a/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php +++ b/tests/Integrations/Yii/Latest/LazyLoadingIntegrationsFromYiiTest.php @@ -44,7 +44,6 @@ public function testRootIndexRoute() Tag::HTTP_STATUS_CODE => '200', 'app.route.path' => '/site/index', Tag::HTTP_ROUTE => '/site/index', - Tag::APPSEC_NORMALIZED_ROUTE => '/site/index', 'app.endpoint' => 'app\controllers\SiteController::actionIndex', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", diff --git a/tests/Integrations/Yii/Latest/ModuleTest.php b/tests/Integrations/Yii/Latest/ModuleTest.php index 201297d6914..8cadc7e3b84 100644 --- a/tests/Integrations/Yii/Latest/ModuleTest.php +++ b/tests/Integrations/Yii/Latest/ModuleTest.php @@ -43,7 +43,6 @@ public function testGet() Tag::HTTP_STATUS_CODE => '200', 'app.route.path' => '/forum/:state/:city/:neighborhood', Tag::HTTP_ROUTE => '/forum/:state/:city/:neighborhood', - Tag::APPSEC_NORMALIZED_ROUTE => '/forum/{state}/{city}/{neighborhood}', 'app.endpoint' => 'app\modules\forum\controllers\ModuleController::actionView', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", diff --git a/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php b/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php index e5076bea22e..82a4af944c5 100644 --- a/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php +++ b/tests/Integrations/Yii/Latest/ParameterizedRouteTest.php @@ -43,7 +43,6 @@ public function testGet() Tag::HTTP_STATUS_CODE => '200', 'app.route.path' => '/homes/:state/:city/:neighborhood', Tag::HTTP_ROUTE => '/homes/:state/:city/:neighborhood', - Tag::APPSEC_NORMALIZED_ROUTE => '/homes/{state}/{city}/{neighborhood}', 'app.endpoint' => 'app\controllers\HomesController::actionView', Tag::SPAN_KIND => "server", Tag::COMPONENT => "yii", diff --git a/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json b/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json index fce7aa85a49..94f4baa2036 100644 --- a/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json +++ b/tests/snapshots/integrations.code_igniter.v3_1.no_ci_controller_test.test_scenario_health_check.json @@ -13,7 +13,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "health_check/ping", - "_dd.appsec.normalized_route": "/health_check/ping", "http.status_code": "200", "http.url": "http://localhost/health_check/ping", "runtime-id": "ca127e85-a20d-46aa-b510-07e9077a14c9", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json index 97202ca6fb8..1da3301fe8b 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_parameterized.json @@ -14,7 +14,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "parameterized/(:any)", - "_dd.appsec.normalized_route": "/parameterized/{param1}", "http.status_code": "200", "http.url": "http://localhost/parameterized/paramValue", "runtime-id": "26ab24e6-051f-4e1f-9adf-3d609bef6946", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json index bfc1072c2ad..6f874986516 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_return_string.json @@ -14,7 +14,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "26ab24e6-051f-4e1f-9adf-3d609bef6946", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json index 8fa1be20b54..5170c9dbe32 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -13,7 +13,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "does_not_exist", - "_dd.appsec.normalized_route": "/does_not_exist", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", "runtime-id": "232799b0-3a18-4d64-9c97-4f6b441bb91e", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json index 2939c1bed83..c50a1bd1bf6 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_to_missing_route_cgi.json @@ -13,7 +13,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "does_not_exist", - "_dd.appsec.normalized_route": "/does_not_exist", "http.status_code": "200", "http.url": "http://localhost/does_not_exist?key=value&", "runtime-id": "0caac976-2232-42e0-a186-9f73b4c27f50", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json index f2fde56386b..c89ca53ba12 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception.json @@ -18,7 +18,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "runtime-id": "91489364-94f6-4030-b3ed-5df886a158a0", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json index ace85d9412f..49e57af64c0 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_exception_cgi.json @@ -18,7 +18,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "0caac976-2232-42e0-a186-9f73b4c27f50", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json index 092ebddb3c5..f1c4002d99b 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.common_scenarios_test.test_scenario_get_with_view.json @@ -14,7 +14,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "26ab24e6-051f-4e1f-9adf-3d609bef6946", diff --git a/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json b/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json index f1d5e75c6bb..fb342972d71 100644 --- a/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json +++ b/tests/snapshots/tests.integrations.code_igniter.v3_1.exit_test.test_scenario_exit.json @@ -14,7 +14,6 @@ "component": "codeigniter", "http.method": "GET", "http.route": "exits", - "_dd.appsec.normalized_route": "/exits", "http.status_code": "200", "http.url": "http://localhost/exits", "runtime-id": "de8ed04e-02e4-40ef-be2a-20aeeb1398ef", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json index 395db3b8050..781cebdb29d 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json @@ -12,7 +12,6 @@ "component": "laminas", "http.method": "POST", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", - "_dd.appsec.normalized_route": "/datadog-rest-service", "http.status_code": "201", "http.url": "http://localhost/datadog-rest-service", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json index a4be1a90901..bcd9fe56956 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json @@ -12,7 +12,6 @@ "component": "laminas", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", - "_dd.appsec.normalized_route": "/datadog-rest-service/{datadog_rest_service_id}", "http.status_code": "405", "http.url": "http://localhost/datadog-rest-service/1", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json index 90a025e6b12..b12796c2391 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json @@ -16,7 +16,6 @@ "error.type": "Error", "http.method": "GET", "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", - "_dd.appsec.normalized_route": "/datadog-rest-service/{datadog_rest_service_id}", "http.status_code": "500", "http.url": "http://localhost/datadog-rest-service/42", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json index 54850a8a703..1bdb15085fc 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json @@ -12,7 +12,6 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple[/:key][/:pwd]", - "_dd.appsec.normalized_route": "/simple", "laminas.route.name": "simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json index 891908b4440..378a177110c 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json @@ -16,7 +16,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "/error[/:key][/:pwd]", - "_dd.appsec.normalized_route": "/error", "laminas.route.name": "error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json index 601c5b1924e..2d16a69aa4d 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json @@ -12,7 +12,6 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple_view[/:key][/:pwd]", - "_dd.appsec.normalized_route": "/simple_view", "laminas.route.name": "simpleView", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json index b789c970e72..1095152bcea 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple[/:key][/:pwd]", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json index fe7fccb8955..b7e009b6638 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "/error[/:key][/:pwd]", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json index b0b11348c6c..901042e3de5 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laminas", "http.method": "GET", "http.route": "/simple_view[/:key][/:pwd]", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json index 89f152f852d..30a75fbd360 100644 --- a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json +++ b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy.json @@ -36,7 +36,6 @@ "env": "local-test", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json index cab7e2f2364..1d07dbb6152 100644 --- a/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json +++ b/tests/snapshots/tests.integrations.laravel.apigw_test.test_laravel_inferred_proxy_exception.json @@ -44,7 +44,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json index d5258d457dc..8a232b92304 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json index de30ef1d7f7..8f1d421becd 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json index 93a4818e629..ac0e27db473 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_ignored_exception.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "ignored_exception", - "_dd.appsec.normalized_route": "/ignored_exception", "http.status_code": "500", "http.url": "http://localhost/ignored_exception?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@ignored_exception", diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json index 61b5dc998c2..2f0e9cc11a9 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json index 1a8fcb1ca38..b272893f432 100644 --- a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json +++ b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy.json @@ -41,7 +41,6 @@ "http.request.headers.x-dd-proxy-request-time-ms": "1739261376000", "http.request.headers.x-dd-proxy-stage": "aws-prod", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json index 70fc8fc131d..419fe5954d0 100644 --- a/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json +++ b/tests/snapshots/tests.integrations.laravel.octane.apigw_test.test_inferred_proxy_exception.json @@ -49,7 +49,6 @@ "http.request.headers.x-dd-proxy-request-time-ms": "1739261376000", "http.request.headers.x-dd-proxy-stage": "aws-prod", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json index d9cdc8ec821..4f0313c6f4d 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json index 873d1872a29..83bad2e3d5f 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json index fb9482ebd8d..3233a40a359 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_ignored_exception.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "ignored_exception", - "_dd.appsec.normalized_route": "/ignored_exception", "http.status_code": "408", "http.url": "http://localhost/ignored_exception?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@ignored_exception", diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json index 4f9f5b699b7..112ac020a34 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json index 390c85885a2..129eb9bf54e 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json index a445e4875cc..2099de60186 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json index bd6031cd01c..680f92fc800 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json index 2d6ac0c4bb6..22efd535627 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "dynamic_route/{param01}/static/{param02?}", - "_dd.appsec.normalized_route": "/dynamic_route/{param01}/static/{param02}", "http.status_code": "200", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@dynamicRoute", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json index 032bf611c97..70b03dbba75 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json index 3ee7c0576c6..2934d7bfe6e 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json index f3c5e198fea..20a7a2d853e 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json index 5e9c5e46358..3e1774ade87 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "dynamic_route/{param01}/static/{param02?}", - "_dd.appsec.normalized_route": "/dynamic_route/{param01}/static/{param02}", "http.status_code": "200", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@dynamicRoute", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json index 3ff3e63d553..ede2a71d263 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json index c0ea78043e8..bd6e68a2a27 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json index 170f307d402..e6dd0b6f683 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json index a30102ddd39..e2ee39ae9c5 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "dynamic_route/{param01}/static/{param02?}", - "_dd.appsec.normalized_route": "/dynamic_route/{param01}/static/{param02}", "http.status_code": "200", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@dynamicRoute", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json index 091d23af98d..1ad5cb004a7 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json index 056926e4848..0d1c1ee654c 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json index 6f765f19df8..de7b4288e70 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json b/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json index 668f86f62ca..66ee49f491e 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.queue_test.test_broadcast.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "queue/broadcast", - "_dd.appsec.normalized_route": "/queue/broadcast", "http.status_code": "200", "http.url": "http://localhost/queue/broadcast", "laravel.route.action": "App\\Http\\Controllers\\QueueTestController@broadcast", diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json index a3ee9d20079..01e28d6b433 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple", - "_dd.appsec.normalized_route": "/simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple", diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json index 171e7435f4d..c822b1957aa 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "error", - "_dd.appsec.normalized_route": "/error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@error", diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json index 0f35b0875ed..34138c0c85d 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "laravel", "http.method": "GET", "http.route": "simple_view", - "_dd.appsec.normalized_route": "/simple_view", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "laravel.route.action": "App\\Http\\Controllers\\CommonSpecsController@simple_view", diff --git a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json index 8a685d109e9..f57a6772946 100644 --- a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_failure.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", - "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "0f36db1f-90a1-409a-9b85-7d0bc2c54d36", diff --git a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json index bb43acbd092..380f76930f4 100644 --- a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_success.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "0f36db1f-90a1-409a-9b85-7d0bc2c54d36", diff --git a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json index 8cbea890d92..197943a03ac 100644 --- a/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.latest.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "0f36db1f-90a1-409a-9b85-7d0bc2c54d36", diff --git a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json index 2cd1b9dc595..d5149dfdc8d 100644 --- a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_failure.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", - "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "ca852dce-8c96-4b3f-8357-2b46cce83f8d", diff --git a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json index c23f76a73af..022b59c7af8 100644 --- a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_success.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "ca852dce-8c96-4b3f-8357-2b46cce83f8d", diff --git a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json index b212dfc624b..d4041b31693 100644 --- a/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v4_4.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "ca852dce-8c96-4b3f-8357-2b46cce83f8d", diff --git a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json index cc9f7d5392a..b855eb677e8 100644 --- a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_failure.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", - "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "1550b663-0773-449e-8a5f-22376e62447d", diff --git a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json index 61f46f02b94..98689bf55ac 100644 --- a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_success.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "1550b663-0773-449e-8a5f-22376e62447d", diff --git a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json index bc0c8fb41a8..63c6603940c 100644 --- a/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v5_2.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "1550b663-0773-449e-8a5f-22376e62447d", diff --git a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json index 7d3f4028b24..2f7394d7f4b 100644 --- a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_failure.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", - "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "8d575e05-da62-4382-a61a-a3c04f3e8cad", diff --git a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json index 3f1a8a49aa7..1ae71e1e127 100644 --- a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_success.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "8d575e05-da62-4382-a61a-a3c04f3e8cad", diff --git a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json index 47ece10b909..96256186c7a 100644 --- a/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v6_2.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "8d575e05-da62-4382-a61a-a3c04f3e8cad", diff --git a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json index ffe507c94f0..ac008361f10 100644 --- a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json +++ b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_failure.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/fail", - "_dd.appsec.normalized_route": "/lucky/fail", "http.status_code": "200", "http.url": "http://localhost/lucky/fail", "runtime-id": "20ef259c-a7ba-47ab-8ea5-bff0fefacb1d", diff --git a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json index 1b3cbdf7568..b4ea6c64938 100644 --- a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json +++ b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_success.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "20ef259c-a7ba-47ab-8ea5-bff0fefacb1d", diff --git a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json index f60f2960cf0..0327d98741e 100644 --- a/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json +++ b/tests/snapshots/tests.integrations.symfony.v7_3.messenger_test.test_async_with_tracer_disabled_on_consume.json @@ -13,7 +13,6 @@ "component": "symfony", "http.method": "GET", "http.route": "/lucky/number", - "_dd.appsec.normalized_route": "/lucky/number", "http.status_code": "200", "http.url": "http://localhost/lucky/number", "runtime-id": "20ef259c-a7ba-47ab-8ea5-bff0fefacb1d", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 807423f1bbf..a94467e5ad4 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index d52a3da5efc..942275e9995 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json index 56709760ea8..4cea0bce0da 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "(.?.+?)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "b4ee1995-4afb-4457-9e9d-b361460bfa16", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json index 04857a7ad7d..34e87afcbd1 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json index d03c59fa4b1..ad6b6999981 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json index 0aed57bd0b3..3da297ba272 100644 --- a/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v4_8.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "(.?.+?)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "8fdcf6ef-7cd9-4910-b426-c7c9809f3dd4", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 54ceed87027..5fe007192cb 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index dab6d17b85c..323e0bbfd23 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json index ddbfe192003..f74b08cfaf3 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "4ad4333f-2e0b-4278-a6f7-2182e7771b34", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json index 97475b3c3ed..b2cc305f4fe 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json index 672fe1ef8f0..310b142ac51 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json index 0d0e03ec26f..ca8d5fe1348 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_5.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "f188c752-a672-4955-97f7-e41a31d13fe7", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 99517450a9e..ebfef74a265 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 10805214ae1..27319620a33 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json index 54424a79ca8..683d022d4e2 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "896f86bc-7139-44f3-a99f-ed35e643f726", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json index b1542c03381..a2f281d70d4 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json index dadb6830bce..c982920760e 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json index 67e6a278f1c..671eb3f7d1a 100644 --- a/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v5_9.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "4c46007f-c934-41aa-bcbe-c48ecee2d4cc", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json index 0b9ee6a9b64..d965665100a 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json index 3848d91ec65..10673208e8e 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json index f4e388bcd01..e055001b0cc 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_callbacks_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "333590aa-cf9b-4804-9dde-1ac7b59c09ab", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json index 6af081bf5bc..cfbeb419d9c 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_return_string.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json index 0c960dd3fc0..76919c42279 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_exception.json @@ -17,7 +17,6 @@ "error.type": "Exception", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/error?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", diff --git a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json index 02a24be7bb7..4b9c2000682 100644 --- a/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.word_press.v6_1.common_scenarios_test.test_scenario_get_with_view.json @@ -13,7 +13,6 @@ "component": "wordpress", "http.method": "GET", "http.route": "([^/]+)(?:/([0-9]+))?/?$", - "_dd.appsec.normalized_route": "/{param1}", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "runtime-id": "df54db4d-0cc0-4b1c-9fce-8004a54aa78b", From cd10921975806cf156b9fa2de01dfa576ee40b63 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 13:09:47 +0200 Subject: [PATCH 32/43] Improve route generation --- .../php/integration/Laminas33Tests.groovy | 17 +- .../php/integration/Laravel8xTests.groovy | 49 ++- .../php/integration/Symfony62Tests.groovy | 21 +- .../Application/config/module.config.php | 13 + .../src/test/www/laravel8x/routes/web.php | 8 + .../src/Controller/HomeController.php | 8 + .../CakePHP/CakePHPIntegration.php | 15 +- .../CodeIgniter/V2/CodeIgniterIntegration.php | 20 -- .../Laravel/LaravelIntegration.php | 69 +++- .../Integrations/Slim/SlimIntegration.php | 68 +--- .../Symfony/SymfonyIntegration.php | 33 +- .../Integrations/Yii/YiiIntegration.php | 13 - src/DDTrace/Util/RouteNormalizer.php | 305 ++++++++---------- .../CodeIgniter/V2_2/CommonScenariosTest.php | 2 +- .../CodeIgniter/V2_2/ExitTest.php | 2 +- .../Util/Normalizer/RouteNormalizerTest.php | 118 ------- 16 files changed, 328 insertions(+), 433 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy index c6d6f5f83d7..5b339aae6d2 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -81,7 +81,7 @@ class Laminas33Tests { endpoints.size() > 0 }) - assert endpoints.size() == 26 + assert endpoints.size() == 27 assert endpoints.find { it.path == '/' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /' } != null assert endpoints.find { it.path == '/application[/:action]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /application[/:action]' @@ -124,6 +124,9 @@ class Laminas33Tests { assert endpoints.find { it.path == '/any-verb' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /any-verb' } != null + assert endpoints.find { + it.path == '/user/:user-id' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /user/:user-id' + } != null } @Test @@ -327,4 +330,16 @@ class Laminas33Tests { assert trace.first().meta.'http.route' == '/application[/:action]' assert trace.first().meta.'_dd.appsec.normalized_route' == '/application/{action}' } + + @Test + @Order(13) + void 'hyphenated param name in segment route normalizes correctly'() { + Trace trace = container.traceFromRequest( + container.buildReq('/user/42').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + assert trace.first().meta.'http.route' == '/user/:user-id' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/user/{user-id}' + } } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy index e47eb53c78b..988d4f32e25 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy @@ -213,12 +213,59 @@ class Laravel8xTests { endpoints.size() > 0 }) - assert endpoints.size() == 6 + assert endpoints.size() == 8 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == 'authenticate' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET authenticate' } != null assert endpoints.find { it.path == 'register' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET register' } != null assert endpoints.find { it.path == 'dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET dynamic-path/{param01}' } != null assert endpoints.find { it.path == 'sanctum/csrf-cookie' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET sanctum/csrf-cookie' } != null assert endpoints.find { it.path == 'api/user' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET api/user' } != null + assert endpoints.find { it.path == 'normalized-optional/{value?}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET normalized-optional/{value?}' } != null + assert endpoints.find { it.path == 'normalized-default/{format?}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET normalized-default/{format?}' } != null + } + + @Test + @Order(10) + void 'optional param present produces correct normalized route'() { + HttpRequest req = container.buildReq('/normalized-optional/hello').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body() == 'hello' + } + + Span span = trace.first() + assert span.meta.'http.route' == 'normalized-optional/{value?}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized-optional/{value}' + } + + @Test + @Order(11) + void 'optional param absent produces correct normalized route'() { + HttpRequest req = container.buildReq('/normalized-optional').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body() == 'absent' + } + + Span span = trace.first() + assert span.meta.'http.route' == 'normalized-optional/{value?}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized-optional' + } + + @Test + @Order(12) + void 'defaulted optional absent from URL produces normalized route without the param'() { + // The route uses ->defaults('format', 'html'). When the URL has no {format?} segment, + // Laravel injects 'html' into $route->parameters() — but the param is absent from the URL. + // The normalized route must not include {format} in this case. + HttpRequest req = container.buildReq('/normalized-default').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body() == 'html' + } + + Span span = trace.first() + assert span.meta.'http.route' == 'normalized-default/{format?}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized-default' } } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index 1d981bc55ba..b6682b8786c 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -186,6 +186,8 @@ class Symfony62Tests { assert re.body().contains('are_endpoints_collected: false') } } + + @Test @Order(3) void 'Endpoints are collected after the first request to framework'() { HttpRequest req = container.buildReq('/outside_of_framework.php').GET().build() @@ -194,6 +196,8 @@ class Symfony62Tests { assert re.body().contains('are_endpoints_collected: true') } } + + @Test @Order(2) void 'Endpoints are sent'() { def trace = container.traceFromRequest('/') { HttpResponse resp -> @@ -209,12 +213,27 @@ class Symfony62Tests { endpoints.size() > 0 }) - assert endpoints.size() == 6 + assert endpoints.size() == 7 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == '/dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /dynamic-path/{param01}' } != null assert endpoints.find { it.path == '/login' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /login' } != null assert endpoints.find { it.path == '/_error/{code}.{_format}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /_error/{code}.{_format}' } != null assert endpoints.find { it.path == '/register' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /register' } != null assert endpoints.find { it.path == '/caminho-dinamico/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /caminho-dinamico/{param01}' } != null + assert endpoints.find { it.path == '/article/{slug}.{_format}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /article/{slug}.{_format}' } != null + } + + @Test + @Order(11) + void 'mixed segment route normalizes both params into one brace group'() { + HttpRequest req = container.buildReq('/article/my-post.html').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body() == 'my-post.html' + } + + Span span = trace.first() + assert span.meta.'http.route' == '/article/{slug}.{_format}' + assert span.meta.'_dd.appsec.normalized_route' == '/article/{slug+_format}' } } diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php index 88fadd25ae5..5b4bbe5e916 100644 --- a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php @@ -228,6 +228,19 @@ ], ], ], + 'user_by_id' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/user/:user-id', + 'constraints' => [ + 'user-id' => '[0-9]+', + ], + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], 'wildcard_keys' => [ 'type' => Literal::class, 'options' => [ diff --git a/appsec/tests/integration/src/test/www/laravel8x/routes/web.php b/appsec/tests/integration/src/test/www/laravel8x/routes/web.php index 916325368c4..3278f2f55a7 100644 --- a/appsec/tests/integration/src/test/www/laravel8x/routes/web.php +++ b/appsec/tests/integration/src/test/www/laravel8x/routes/web.php @@ -21,3 +21,11 @@ Route::get('/authenticate', '\App\Http\Controllers\LoginController@authenticate'); Route::get('/register', '\App\Http\Controllers\LoginController@register'); Route::get('/dynamic-path/{param01}', '\App\Http\Controllers\MiscController@dynamicPath'); + +Route::get('/normalized-optional/{value?}', function ($value = null) { + return response($value ?? 'absent'); +}); + +Route::get('/normalized-default/{format?}', function ($format = null) { + return response($format); +})->defaults('format', 'html'); diff --git a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php index 38cbcaa27eb..c6ac1300c3f 100644 --- a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php +++ b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php @@ -37,4 +37,12 @@ public function utf8Action(Request $request, string $item) "Café: $item" ); } + + #[Route("/article/{slug}.{_format}", name: "article_mixed", requirements: ["_format" => "html|json|xml"])] + public function normalizedMixedAction(Request $request, string $slug, string $_format) + { + return new Response( + "$slug.$_format" + ); + } } diff --git a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php index b07a586f17b..9adf7691128 100644 --- a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php +++ b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php @@ -71,20 +71,7 @@ public static function init(): int $rootSpan = \DDTrace\root_span(); if ($rootSpan !== null) { - $template = $app->template; - $rootSpan->meta[Tag::HTTP_ROUTE] = $template; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $normalizedRoute = \DDTrace\routing_cache_get($template); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCakePHP($template); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($template, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } + $rootSpan->meta[Tag::HTTP_ROUTE] = $app->template; } }; diff --git a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php index 8c98cad9deb..15c8f684f67 100644 --- a/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php +++ b/src/DDTrace/Integrations/CodeIgniter/V2/CodeIgniterIntegration.php @@ -222,23 +222,6 @@ function (SpanData $span, $args, $retval, $ex) use ($adapter, $service) { /* * Replicate CodeIgniter's route parsing, as matching key is never stored or returned in the framework. */ - private static function setNormalizedRoute($rootSpan, string $pattern) { - if (!function_exists('\datadog\appsec\is_enabled') || !\datadog\appsec\is_enabled()) { - return; - } - $cacheKey = $pattern; - $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromCodeIgniter($pattern); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } - private static function setHttpRoute($router, $rootSpan) { // Turn the segment array into a URI string $uri = implode('/', $router->uri->segments); @@ -247,7 +230,6 @@ private static function setHttpRoute($router, $rootSpan) { if (isset($router->routes[$uri])) { $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; - self::setNormalizedRoute($rootSpan, $uri); return; } @@ -262,7 +244,6 @@ private static function setHttpRoute($router, $rootSpan) { if (preg_match('#^'.$key.'$#', $uri)) { $rootSpan->meta[Tag::HTTP_ROUTE] = $origKey; - self::setNormalizedRoute($rootSpan, $origKey); return; } } @@ -270,6 +251,5 @@ private static function setHttpRoute($router, $rootSpan) { // If we got this far it means we didn't encounter a // matching route so we'll set the site default route $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; - self::setNormalizedRoute($rootSpan, $uri); } } diff --git a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php index 628cc30ab0e..3b2114e9405 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -143,12 +143,32 @@ static function ($This, $scope, $args, $route) { $httpRoute = $route->uri(); $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $normalizedRoute = \DDTrace\routing_cache_get($httpRoute); + $allParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; + if (strpos($httpRoute, '?}') !== false) { + // For routes with optional params, filter out default-injected values + // (e.g. ->defaults('format', 'html')) that weren't present in the URL. + $matchedParams = self::laravelUrlMatchedParams( + $httpRoute, $request->path(), $allParams + ); + // Cache key encodes which optional params are present + preg_match_all('/\{([^}]+)\?\}/', $httpRoute, $_opts); + $_present = []; + foreach ($_opts[1] as $_opt) { + if (array_key_exists($_opt, $matchedParams)) { + $_present[] = $_opt; + } + } + $cacheKey = $httpRoute . '#' . implode(',', $_present); + unset($_opts, $_present, $_opt); + } else { + $matchedParams = $allParams; + $cacheKey = $httpRoute; + } + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { - $matchedParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaravel($httpRoute, $matchedParams); if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($httpRoute, $normalizedRoute); + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); } } if ($normalizedRoute !== null && $normalizedRoute !== false) { @@ -767,4 +787,47 @@ public static function normalizeRouteName($routeName) return $routeName; } + + /** + * Determine which Laravel optional params were actually present in the URL path + * (vs. injected as route defaults via ->defaults()). + * + * Walks the route URI template and URL path in parallel; an optional param is only + * included in the result when the URL has a non-empty segment at that position. + * + * @param string $routeUri From $route->uri(), e.g. "normalized-default/{format?}" + * @param string $urlPath From $request->path(), e.g. "normalized-default" + * @param array $allParams From $route->parameters() + * @return array + */ + private static function laravelUrlMatchedParams(string $routeUri, string $urlPath, array $allParams): array + { + $routeSegs = explode('/', trim($routeUri, '/')); + $urlSegs = explode('/', trim($urlPath, '/')); + $matched = []; + $urlIdx = 0; + + foreach ($routeSegs as $seg) { + if (preg_match('/^\{([^}?]+)\?\}$/', $seg, $m)) { + // Optional param — include only when URL has a segment at this position + if ($urlIdx < count($urlSegs) && $urlSegs[$urlIdx] !== '') { + if (array_key_exists($m[1], $allParams)) { + $matched[$m[1]] = $allParams[$m[1]]; + } + $urlIdx++; + } + } elseif (preg_match('/^\{([^}]+)\}$/', $seg)) { + // Required param — always present + preg_match('/^\{([^}]+)\}$/', $seg, $m); + if (array_key_exists($m[1], $allParams)) { + $matched[$m[1]] = $allParams[$m[1]]; + } + $urlIdx++; + } else { + $urlIdx++; + } + } + + return $matched; + } } diff --git a/src/DDTrace/Integrations/Slim/SlimIntegration.php b/src/DDTrace/Integrations/Slim/SlimIntegration.php index bff645e8504..d135e66d810 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -75,24 +75,11 @@ static function ($errorMiddleware, $self, $args) use ($rootSpan, $integration) { null, static function ($router, $scope, $args, $return) use ($rootSpan) { /** @var \Slim\Interfaces\RouteInterface $return */ - $pattern = $return->getPattern(); - $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } + $rootSpan->meta[Tag::HTTP_ROUTE] = $return->getPattern(); if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { $rootSpan->resource = - $_SERVER['REQUEST_METHOD'] . ' ' . ($return->getName() ?: $pattern); + $_SERVER['REQUEST_METHOD'] . ' ' . ($return->getName() ?: $return->getPattern()); } } ); @@ -105,20 +92,7 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { static function ($router, $scope, $args, $return) use ($rootSpan) { /** @var \Slim\Interfaces\RouteInterface $route */ $route = $return; - $pattern = $route->getPattern(); - $rootSpan->meta[Tag::HTTP_ROUTE] = $pattern; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } + $rootSpan->meta[Tag::HTTP_ROUTE] = $route->getPattern(); } ); } @@ -157,46 +131,10 @@ static function ($router, $scope, $args, $return) use ($rootSpan) { $span->meta['slim.route.name'] = $routeName; $rootSpan->meta['slim.route.name'] = $routeName; } - // Refine normalized route now that matched params are available - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $matchedParams = method_exists($route, 'getArguments') ? ($route->getArguments() ?? []) : []; - $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; - if ($pattern !== '') { - $urlPath = $request->getUri()->getPath(); - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } - } } } else { $rootSpan->meta['slim.route.controller'] = $callableName; $span->name = 'slim.route.controller'; - // Refine normalized route now that matched params are available (Slim 3) - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $matchedParams = isset($args[3]) && is_array($args[3]) ? $args[3] : []; - $pattern = isset($rootSpan->meta[Tag::HTTP_ROUTE]) ? $rootSpan->meta[Tag::HTTP_ROUTE] : ''; - if ($pattern !== '') { - $urlPath = $request->getUri()->getPath(); - $normalizedRoute = \DDTrace\routing_cache_get($pattern); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSlim($pattern, $matchedParams, $urlPath); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($pattern, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } - } } }; diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index b4890bebb5c..f5fbab2eadb 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -467,7 +467,7 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $matchedParams = self::inferSymfonyRouteParams($path, $request->getPathInfo()); + $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); if ($normalizedRoute !== null) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; @@ -788,35 +788,4 @@ public static function injectActionInfo($event, $eventName, SpanData $requestSpa return true; } - /** - * Walk the path template and URL path together to determine which {param} - * placeholders were actually present in the URL (vs filled from route defaults). - * Each template segment is either static text or a single {param}; trailing - * params with no corresponding URL segment are considered absent. - * - * @return array Map of param name → URL value for params present in the URL - */ - private static function inferSymfonyRouteParams(string $template, string $urlPath): array - { - $templateSegments = array_values(array_filter(explode('/', $template))); - $urlSegments = array_values(array_filter(explode('/', $urlPath))); - - $matched = []; - $urlIdx = 0; - - foreach ($templateSegments as $seg) { - if (preg_match('/^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/', $seg, $m)) { - if ($urlIdx < count($urlSegments)) { - $matched[$m[1]] = $urlSegments[$urlIdx]; - $urlIdx++; - } - // else: param is beyond end of URL → absent (default-filled) - } else { - // Static segment — always advance the URL position - $urlIdx++; - } - } - - return $matched; - } } diff --git a/src/DDTrace/Integrations/Yii/YiiIntegration.php b/src/DDTrace/Integrations/Yii/YiiIntegration.php index b9368921b7f..91fa862d5f9 100644 --- a/src/DDTrace/Integrations/Yii/YiiIntegration.php +++ b/src/DDTrace/Integrations/Yii/YiiIntegration.php @@ -156,19 +156,6 @@ function (SpanData $span, $args) use (&$firstController) { $rootSpan->meta['app.route.path'] = $routePath; $rootSpan->meta[Tag::HTTP_ROUTE] = $routePath; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $cacheKey = $routePath; - $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); - if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromYii($routePath); - if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); - } - } - if ($normalizedRoute !== null && $normalizedRoute !== false) { - $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; - } - } if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { $resourceName = \str_replace( diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 76bb2e96d8b..eb790f62185 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -9,7 +9,8 @@ class RouteNormalizer * Normalize a Laravel route URI. * * @param string $routeUri URI from $route->uri(), e.g. "/users/{id}/{format?}" - * @param array $matchedParams Parameters from $route->parameters(); used to resolve optionals + * @param array $matchedParams Parameters from $route->parameters(); used to resolve optionals. + * Note: includes framework-injected defaults; caller must exclude them. * @return string|null */ public static function normalizeFromLaravel(string $routeUri, array $matchedParams = []) @@ -17,20 +18,6 @@ public static function normalizeFromLaravel(string $routeUri, array $matchedPara return self::normalizeBraceRoute($routeUri, $matchedParams); } - /** - * Normalize a Slim route pattern. - * - * @param string $pattern Pattern from $route->getPattern(), e.g. "/users/{id:[0-9]+}" - * @param array $matchedParams Matched params from $route->getArguments(); resolves optionals - * @param string|null $urlPath Actual request path; used to resolve static-only optional - * sections like [.json] that have no placeholder param - * @return string|null - */ - public static function normalizeFromSlim(string $pattern, array $matchedParams = [], $urlPath = null) - { - return self::normalizeBraceRoute($pattern, $matchedParams, true, $urlPath); - } - /** * Normalize a Symfony route path. * @@ -43,8 +30,9 @@ public static function normalizeFromSymfony(string $path, $matchedParams = null) { if ($matchedParams !== null) { // Mark params absent from the URL as optional so normalizeBraceSegment drops them. + // Use [^}?:]+ to match any param name including UTF-8 characters. $path = preg_replace_callback( - '/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', + '/\{([^}?:]+)\}/', static function ($m) use ($matchedParams) { return array_key_exists($m[1], $matchedParams) ? $m[0] : '{' . $m[1] . '?}'; }, @@ -72,78 +60,19 @@ static function ($m) use ($matchedParams) { public static function normalizeFromLaminas(string $template, array $matchedParams = [], $urlPath = null) { $expanded = self::expandBracketOptionals($template, $matchedParams, ':', $urlPath); - $expanded = preg_replace('#/\*$#', '/{param1}', $expanded); + + // Replace wildcard /* with a param name that doesn't collide with existing params + if (preg_match('#/\*$#', $expanded)) { + $wildcardName = self::uniqueParamName($expanded, ':'); + $expanded = preg_replace('#/\*$#', '/{' . $wildcardName . '}', $expanded); + } + // Segment routes use :param; Regex routes use %param% (spec format) — handle both. $braceFormat = self::colonParamsToBraces($expanded); $braceFormat = self::percentParamsToBraces($braceFormat); return self::normalizeBraceRoute($braceFormat, $matchedParams); } - /** - * Normalize a CakePHP route template. - * - * @param string $template Template from $app->template, e.g. "/articles/:id.:ext" - * @return string|null - */ - public static function normalizeFromCakePHP(string $template) - { - $braceFormat = self::cakephpToBraces($template); - return self::normalizeBraceRoute($braceFormat, []); - } - - /** - * Normalize a Yii route path containing :param placeholders. - * - * @param string $routePath Path from Url::toRoute() with colon placeholders - * @return string|null - */ - public static function normalizeFromYii(string $routePath) - { - $braceFormat = self::colonParamsToBraces($routePath); - return self::normalizeBraceRoute($braceFormat, []); - } - - /** - * Normalize a CodeIgniter V2 route pattern. - * - * CodeIgniter uses :any / :num wildcards and positional regex groups. - * Named parameters are not available, so placeholders param1, param2, … are used. - * - * @param string $route Route key from $router->routes, e.g. "blog/(:num)" - * @return string|null - */ - public static function normalizeFromCodeIgniter(string $route) - { - $route = trim($route, '/'); - if ($route === '') { - return '/'; - } - - $segments = explode('/', $route); - $normalizedSegments = []; - $paramIndex = 1; - - foreach ($segments as $segment) { - if ($segment === '') { - continue; - } - - $lower = strtolower($segment); - if ( - $lower === ':any' || $lower === ':num' || - $lower === '(:any)' || $lower === '(:num)' - ) { - $normalizedSegments[] = '{param' . $paramIndex++ . '}'; - } elseif (preg_match('/[()[\].*+?|^$\\\\]/', $segment) || strpos($segment, ':') !== false) { - $normalizedSegments[] = '{param' . $paramIndex++ . '}'; - } else { - $normalizedSegments[] = self::encodeStaticSegment($segment); - } - } - - return '/' . implode('/', $normalizedSegments); - } - /** * Normalize a WordPress matched_rule (regex). * @@ -194,7 +123,18 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu } if (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { - $groupCount = self::countCaptureGroups($segment); + // Extract static prefix before the first regex metacharacter + $prefixLen = strcspn($segment, '([{?*+|^$\\'); + if ($prefixLen > 0) { + $staticPart = rtrim(substr($segment, 0, $prefixLen), '/-._'); + if ($staticPart !== '') { + $normalizedSegments[] = self::encodeStaticSegment($staticPart); + } + } + + $dynamicPart = substr($segment, $prefixLen); + $groupCount = self::countCaptureGroups($dynamicPart); + if ($groupCount === 0) { if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { continue; @@ -221,8 +161,8 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu } /** - * Split a regex string by '/' but not inside character classes [...]. - * Prevents [^/] from being split into two segments. + * Split a regex string by '/' but not inside character classes [...] or groups (...). + * Prevents [^/] and (?:/...) from being split into multiple segments. */ private static function splitRegexBySlash(string $str): array { @@ -230,6 +170,7 @@ private static function splitRegexBySlash(string $str): array $current = ''; $len = strlen($str); $bracketDepth = 0; + $parenDepth = 0; for ($i = 0; $i < $len; $i++) { $c = $str[$i]; @@ -240,13 +181,19 @@ private static function splitRegexBySlash(string $str): array continue; } - if ($c === '[') { + if ($c === '[' && $parenDepth === 0) { $bracketDepth++; $current .= $c; } elseif ($c === ']' && $bracketDepth > 0) { $bracketDepth--; $current .= $c; - } elseif ($c === '/' && $bracketDepth === 0) { + } elseif ($c === '(' && $bracketDepth === 0) { + $parenDepth++; + $current .= $c; + } elseif ($c === ')' && $parenDepth > 0 && $bracketDepth === 0) { + $parenDepth--; + $current .= $c; + } elseif ($c === '/' && $bracketDepth === 0 && $parenDepth === 0) { $segments[] = $current; $current = ''; } else { @@ -291,15 +238,10 @@ private static function countCaptureGroups(string $segment): int /** * Normalize a route that uses {param} notation. - * - * @param bool $expandSquare When true, expand Slim-style [...] optional sections - * @param string|null $urlPath Actual request path; forwarded to expandSquareBracketOptionals */ private static function normalizeBraceRoute( string $route, - array $matchedParams, - bool $expandSquare = false, - $urlPath = null + array $matchedParams ) { $route = trim($route); if ($route === '' || $route === '/') { @@ -313,14 +255,10 @@ private static function normalizeBraceRoute( $route = '/' . $route; } - if ($expandSquare) { - $route = self::expandSquareBracketOptionals($route, $matchedParams, $urlPath); - } - // Strip inline constraints (e.g. Slim's {name:[^/]+} → {name}) before // splitting so that a '/' inside a constraint does not break the segment // split. The optional marker '?' is preserved: {name?:[0-9]+} → {name?}. - $route = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*(\?)?):([^}]*)\}/', '{$1}', $route); + $route = preg_replace('/\{([^}?:]+(\?)?):([^}]*)\}/', '{$1}', $route); $raw = ltrim($route, '/'); $parts = explode('/', $raw); @@ -346,6 +284,7 @@ private static function normalizeBraceRoute( * Normalize a single URL segment that may contain {param} placeholders. * * @return string|null The normalized element, or null if the segment is optional and absent + * with no remaining static text */ private static function normalizeBraceSegment(string $segment, array $matchedParams) { @@ -379,6 +318,13 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar } if (empty($paramNames)) { + // All params were optional and absent. + // Preserve any static text remaining in the segment (e.g. "search.{_format?}" → "search"). + $staticOnly = preg_replace('/\{[^}]+\}/', '', $segment); + $staticOnly = trim($staticOnly, '.-_~'); + if ($staticOnly !== '') { + return self::encodeStaticSegment($staticOnly); + } return null; } @@ -389,48 +335,6 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar return '{' . implode('+', $paramNames) . '}'; } - /** - * Expand Slim-style optional sections [...] based on matched params. - * - * For sections that contain no placeholder (e.g. [.json]), $urlPath is used - * to decide whether the literal text was part of the request; without it the - * section is always kept (backward-compatible behaviour). - */ - private static function expandSquareBracketOptionals(string $route, array $matchedParams, $urlPath = null): string - { - $prev = null; - while ($prev !== $route) { - $prev = $route; - $route = preg_replace_callback( - '/\[([^\[\]]*)\]/', - function ($m) use ($matchedParams, $urlPath) { - $inner = $m[1]; - preg_match_all('/\{([^}?:]+)[?:]?[^}]*\}/', $inner, $pm); - $innerParams = $pm[1]; - - if (empty($innerParams)) { - // Static-only section (e.g. [.json]): include only when the - // literal text actually appears in the request path. - if ($urlPath !== null) { - return strpos($urlPath, $inner) !== false ? $inner : ''; - } - return $inner; - } - - foreach ($innerParams as $param) { - if (array_key_exists($param, $matchedParams)) { - return $inner; - } - } - - return ''; - }, - $route - ); - } - return $route; - } - /** * Expand Laminas [...] optional sections based on matched params. * @@ -438,6 +342,9 @@ function ($m) use ($matchedParams, $urlPath) { * section text with param values substituted is a substring of $urlPath. * This prevents middleware-injected params from incorrectly triggering * expansion of sections absent from the URL. + * + * For static-only optional sections (no params), the URL path is also checked + * to determine whether the literal text appeared in the request. */ private static function expandBracketOptionals( string $template, @@ -452,10 +359,19 @@ private static function expandBracketOptionals( '/\[([^\[\]]*)\]/', function ($m) use ($matchedParams, $paramPrefix, $urlPath) { $inner = $m[1]; - $pattern = '/' . preg_quote($paramPrefix, '/') . '([a-zA-Z_][a-zA-Z0-9_]*)/'; + $pattern = '/' . preg_quote($paramPrefix, '/') . '([a-zA-Z_][a-zA-Z0-9_-]*)/'; preg_match_all($pattern, $inner, $pm); $innerParams = $pm[1]; + if (empty($innerParams)) { + // Static-only optional section (e.g. [/draft]): + // only expand when the literal text appears in the URL. + if ($urlPath !== null) { + return (strpos($urlPath, $inner) !== false) ? $inner : ''; + } + return $inner; + } + // All params in the section must be present in matched params. foreach ($innerParams as $param) { if (!array_key_exists($param, $matchedParams)) { @@ -463,21 +379,27 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { } } - if ($urlPath !== null && !empty($innerParams)) { + if ($urlPath !== null) { // Substitute every param value before checking the URL so that // multi-param sections like [/:year/:month] are found correctly. $innerWithValues = $inner; foreach ($innerParams as $param) { $value = (string)$matchedParams[$param]; - $innerWithValues = preg_replace( - '/' . preg_quote($paramPrefix . $param, '/') . '/', - $value, - $innerWithValues - ); + $innerWithValues = str_replace($paramPrefix . $param, $value, $innerWithValues); } - if (strpos($urlPath, $innerWithValues) === false) { - return ''; + if (strpos($urlPath, $innerWithValues) !== false) { + return $inner; + } + // Try percent-encoded values (Laminas URL-decodes param values) + $innerEncoded = $inner; + foreach ($innerParams as $param) { + $value = rawurlencode((string)$matchedParams[$param]); + $innerEncoded = str_replace($paramPrefix . $param, $value, $innerEncoded); + } + if (strpos($urlPath, $innerEncoded) !== false) { + return $inner; } + return ''; } return $inner; @@ -491,11 +413,12 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { /** * Convert ":paramName" colon-prefix notation to "{paramName}" brace notation. * Laminas segment constraints like ":param{constraint}" are also handled. + * Hyphenated param names like ":user-id" are supported. */ private static function colonParamsToBraces(string $template): string { return preg_replace_callback( - '/:([a-zA-Z_][a-zA-Z0-9_]*)(?:\{[^}]*\})?/', + '/:([a-zA-Z_][a-zA-Z0-9_-]*)(?:\{[^}]*\})?/', static function ($m) { return '{' . $m[1] . '}'; }, @@ -513,21 +436,19 @@ private static function percentParamsToBraces(string $template): string } /** - * Convert CakePHP route template syntax to brace notation. + * Find a param name of the form "paramN" that does not already appear in $template + * as either a colon-param (:paramN) or a brace-param ({paramN}). */ - private static function cakephpToBraces(string $template): string + private static function uniqueParamName(string $template, string $paramPrefix = ':'): string { - $result = preg_replace('#/\*\*#', '/{catchall}', $template); - $result = preg_replace('#/\*(?!\*)#', '/{catchall}', $result); - $result = preg_replace('#(? 'Error_::index', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', - Tag::HTTP_ROUTE => 'error', + Tag::HTTP_ROUTE => 'error' ]) ->setError("Exception", "Uncaught Exception: datadog in %s:%d") ->withExistingTagsNames(['error.stack']) diff --git a/tests/Integrations/CodeIgniter/V2_2/ExitTest.php b/tests/Integrations/CodeIgniter/V2_2/ExitTest.php index 6b6a5a32d7c..c7d0603228b 100644 --- a/tests/Integrations/CodeIgniter/V2_2/ExitTest.php +++ b/tests/Integrations/CodeIgniter/V2_2/ExitTest.php @@ -43,7 +43,7 @@ public function testScenario() 'app.endpoint' => 'Exits::index', Tag::SPAN_KIND => 'server', Tag::COMPONENT => 'codeigniter', - Tag::HTTP_ROUTE => 'exits', + Tag::HTTP_ROUTE => 'exits' ])->withChildren([ SpanAssertion::build( 'Exits.index', diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 575dcc4d387..7d58774ce96 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -114,59 +114,6 @@ public function testLaravelRoot() $this->assertSame('/', RouteNormalizer::normalizeFromLaravel('/')); } - // normalizeFromSlim - - public function testSlimSimpleRoute() - { - $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromSlim('/users/{id}')); - } - - public function testSlimRegexConstraintStripped() - { - $this->assertSame('/users/{id}', RouteNormalizer::normalizeFromSlim('/users/{id:[0-9]+}')); - $this->assertSame('/v2/{name}/blobs', RouteNormalizer::normalizeFromSlim('/v2/{name:[a-zA-Z0-9-]+}/blobs')); - // Constraint containing '/' must not break the segment split - $this->assertSame('/files/{name}', RouteNormalizer::normalizeFromSlim('/files/{name:[^/]+}')); - } - - public function testSlimOptionalSegmentPresent() - { - $result = RouteNormalizer::normalizeFromSlim('/users/{id}[/{format}]', ['id' => '1', 'format' => 'json']); - $this->assertSame('/users/{id}/{format}', $result); - } - - public function testSlimOptionalSegmentAbsent() - { - $result = RouteNormalizer::normalizeFromSlim('/users/{id}[/{format}]', ['id' => '1']); - $this->assertSame('/users/{id}', $result); - } - - public function testSlimCatchAll() - { - $this->assertSame('/files/{file}', RouteNormalizer::normalizeFromSlim('/files/{file:.+}')); - } - - public function testSlimStaticOptionalSectionPresent() - { - // /feed[.json] requested as /feed.json → .json section included - $result = RouteNormalizer::normalizeFromSlim('/feed[.json]', [], '/feed.json'); - $this->assertSame('/feed.json', $result); - } - - public function testSlimStaticOptionalSectionAbsent() - { - // /feed[.json] requested as /feed → .json section absent - $result = RouteNormalizer::normalizeFromSlim('/feed[.json]', [], '/feed'); - $this->assertSame('/feed', $result); - } - - public function testSlimStaticOptionalSectionNoUrlPath() - { - // Without URL path, backward-compatible: keep the section - $result = RouteNormalizer::normalizeFromSlim('/feed[.json]', []); - $this->assertSame('/feed.json', $result); - } - // normalizeFromSymfony public function testSymfonySimpleRoute() @@ -301,71 +248,6 @@ public function testLaminasWildcard() $this->assertSame('/{param1}', $result); } - // normalizeFromCakePHP - - public function testCakePHPSimpleColon() - { - $this->assertSame('/articles/{id}', RouteNormalizer::normalizeFromCakePHP('/articles/:id')); - } - - public function testCakePHPMixedSegment() - { - $result = RouteNormalizer::normalizeFromCakePHP('/articles/:id.:ext'); - $this->assertSame('/articles/{id+ext}', $result); - } - - public function testCakePHPCatchAll() - { - $this->assertSame('/{catchall}', RouteNormalizer::normalizeFromCakePHP('/*')); - $this->assertSame('/api/{catchall}', RouteNormalizer::normalizeFromCakePHP('/api/**')); - } - - public function testCakePHPStaticRoute() - { - $this->assertSame('/admin/dashboard', RouteNormalizer::normalizeFromCakePHP('/admin/dashboard')); - } - - // normalizeFromYii - - public function testYiiSimpleColonPlaceholder() - { - $this->assertSame('/articles/{id}', RouteNormalizer::normalizeFromYii('/articles/:id')); - } - - public function testYiiStaticRoute() - { - $this->assertSame('/site/index', RouteNormalizer::normalizeFromYii('/site/index')); - } - - // normalizeFromCodeIgniter - - public function testCodeIgniterLiteralRoute() - { - $this->assertSame('/articles/index', RouteNormalizer::normalizeFromCodeIgniter('articles/index')); - } - - public function testCodeIgniterNumWildcard() - { - $this->assertSame('/blog/{param1}', RouteNormalizer::normalizeFromCodeIgniter('blog/(:num)')); - } - - public function testCodeIgniterAnyWildcard() - { - $this->assertSame('/users/{param1}', RouteNormalizer::normalizeFromCodeIgniter('users/:any')); - } - - public function testCodeIgniterMultipleWildcards() - { - $result = RouteNormalizer::normalizeFromCodeIgniter('posts/(:num)/comments/(:num)'); - $this->assertSame('/posts/{param1}/comments/{param2}', $result); - } - - public function testCodeIgniterCatchAll() - { - // A catch-all in CI is typically :any at the end - $this->assertSame('/{param1}', RouteNormalizer::normalizeFromCodeIgniter(':any')); - } - // normalizeFromWordPress public function testWordPressSimpleRegex() From 9cdee15d02df7511c093e967ef7e4ac1a227cd07 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 13:24:51 +0200 Subject: [PATCH 33/43] Improve laminas integration --- .../Laminas/LaminasIntegration.php | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 1111572d1f7..6b0cafabd96 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -281,23 +281,21 @@ static function (SpanData $span) use ($controller, $action) { && $routeName !== null && $routeName !== '' ) { - $cacheKey = (string) $routeName; - $cachedRoute = \DDTrace\routing_cache_get($cacheKey); - if ($cachedRoute !== false) { - $httpRoute = $cachedRoute; - } else { - $httpRoute = LaminasIntegration::httpRouteTemplateFromNamedRouteStack($this, (string) $routeName); - if ($httpRoute !== null && $httpRoute !== '') { - \DDTrace\routing_cache_set($cacheKey, $httpRoute); - } - } - if ($httpRoute !== null && $httpRoute !== false && $httpRoute !== '') { + $httpRoute = LaminasIntegration::httpRouteTemplateFromNamedRouteStack($this, (string) $routeName); + if ($httpRoute !== null && $httpRoute !== '') { $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); - if ($normalizedRoute !== null) { + $cacheKey = $httpRoute . '|' . ($urlPath ?? ''); + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($normalizedRoute === false) { + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } From 0fd44d6f5f0814e1270292e6196f3de5a8ad5d43 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 13:27:46 +0200 Subject: [PATCH 34/43] Improve Symfony --- .../Symfony/SymfonyIntegration.php | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index f5fbab2eadb..188f6519445 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -442,34 +442,31 @@ static function() { return; } - $cacheKey = $route_name; - $cachedPath = \DDTrace\routing_cache_get($cacheKey); - if ($cachedPath !== false) { - $path = $cachedPath; - } else { - /** @var ContainerInterface $container */ - $container = self::$kernel->getContainer(); - $path = EndpointCatalog::pathForRoute($route_name, $container); - - // Try with locale suffix (Symfony i18n routing convention) - if ($path === null) { - $locale = $request->attributes->get('_locale'); - if ($locale !== null) { - $path = EndpointCatalog::pathForRoute($route_name . '.' . $locale, $container); - } - } - - if ($path !== null) { - \DDTrace\routing_cache_set($cacheKey, $path); + /** @var ContainerInterface $container */ + $container = self::$kernel->getContainer(); + $path = EndpointCatalog::pathForRoute($route_name, $container); + + // Try with locale suffix (Symfony i18n routing convention) + if ($path === null) { + $locale = $request->attributes->get('_locale'); + if ($locale !== null) { + $path = EndpointCatalog::pathForRoute($route_name . '.' . $locale, $container); } } if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); - if ($normalizedRoute !== null) { + $cacheKey = $route_name; + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); + if ($normalizedRoute === false) { + $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); + if ($normalizedRoute !== null) { + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); + } + } + if ($normalizedRoute !== null && $normalizedRoute !== false) { $rootSpan->meta[Tag::APPSEC_NORMALIZED_ROUTE] = $normalizedRoute; } } From fcbd559ceb76c675262010ed0eab66c1f0c2be8b Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 15:17:05 +0200 Subject: [PATCH 35/43] Fix pipeline --- .../appsec/php/integration/Symfony62Tests.groovy | 3 ++- src/DDTrace/Util/RouteNormalizer.php | 16 +++++++++++----- .../Unit/Util/Normalizer/RouteNormalizerTest.php | 10 ++-------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index b6682b8786c..7481f8d7454 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -213,7 +213,7 @@ class Symfony62Tests { endpoints.size() > 0 }) - assert endpoints.size() == 7 + assert endpoints.size() == 8 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == '/dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /dynamic-path/{param01}' } != null assert endpoints.find { it.path == '/login' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /login' } != null @@ -221,6 +221,7 @@ class Symfony62Tests { assert endpoints.find { it.path == '/register' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /register' } != null assert endpoints.find { it.path == '/caminho-dinamico/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /caminho-dinamico/{param01}' } != null assert endpoints.find { it.path == '/article/{slug}.{_format}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /article/{slug}.{_format}' } != null + assert endpoints.find { it.path == '/café/{item}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /café/{item}' } != null } @Test diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index eb790f62185..11f250a66cd 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -113,6 +113,11 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu return '/'; } + // Pull the path separator out of (?:/...) non-capturing groups so that + // splitRegexBySlash treats the embedded '/' as a real segment boundary. + // Pattern: (?:/foo) means "optional /foo segment" — the '/' belongs at top level. + $rule = str_replace('(?:/', '/(?:', $rule); + $segments = self::splitRegexBySlash($rule); $normalizedSegments = []; $paramIndex = 1; @@ -123,18 +128,19 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu } if (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { - // Extract static prefix before the first regex metacharacter $prefixLen = strcspn($segment, '([{?*+|^$\\'); - if ($prefixLen > 0) { + $dynamicPart = substr($segment, $prefixLen); + $groupCount = self::countCaptureGroups($dynamicPart); + + // Only emit a static prefix when there is at most one capture group; + // mixed segments with multiple groups are treated as fully dynamic. + if ($prefixLen > 0 && $groupCount <= 1) { $staticPart = rtrim(substr($segment, 0, $prefixLen), '/-._'); if ($staticPart !== '') { $normalizedSegments[] = self::encodeStaticSegment($staticPart); } } - $dynamicPart = substr($segment, $prefixLen); - $groupCount = self::countCaptureGroups($dynamicPart); - if ($groupCount === 0) { if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { continue; diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 7d58774ce96..2ea70d94f18 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -330,17 +330,11 @@ public function testRfcExampleFlaskMixedStaticDynamic() public function testRfcExampleRailsMandatoryFormat() { // http.route: /photos/:id.:format → /photos/{id+format} - $result = RouteNormalizer::normalizeFromCakePHP('/photos/:id.:format'); + // Laminas uses the same :param syntax as CakePHP/Rails for this pattern. + $result = RouteNormalizer::normalizeFromLaminas('/photos/:id.:format'); $this->assertSame('/photos/{id+format}', $result); } - public function testRfcExampleGoGorilla() - { - // http.route: /v2/{name:[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]}/blobs - $result = RouteNormalizer::normalizeFromSlim('/v2/{name:[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]}/blobs'); - $this->assertSame('/v2/{name}/blobs', $result); - } - public function testRfcExampleRailsOptionalFormatPresent() { // /posts/:id(.:format) with format present → /posts/{id+format} From 692455588fbbef02cd50a6f351819d37a9edb7d0 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 16:18:17 +0200 Subject: [PATCH 36/43] Add more tests --- .../php/integration/Symfony62Tests.groovy | 25 ++++++++++- .../src/Controller/HomeController.php | 6 +++ .../Laminas/LaminasIntegration.php | 7 ++- .../Symfony/SymfonyIntegration.php | 8 +++- .../Util/Normalizer/RouteNormalizerTest.php | 43 +++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index 7481f8d7454..858e88ba7cd 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -213,7 +213,7 @@ class Symfony62Tests { endpoints.size() > 0 }) - assert endpoints.size() == 8 + assert endpoints.size() == 9 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == '/dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /dynamic-path/{param01}' } != null assert endpoints.find { it.path == '/login' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /login' } != null @@ -222,10 +222,33 @@ class Symfony62Tests { assert endpoints.find { it.path == '/caminho-dinamico/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /caminho-dinamico/{param01}' } != null assert endpoints.find { it.path == '/article/{slug}.{_format}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /article/{slug}.{_format}' } != null assert endpoints.find { it.path == '/café/{item}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /café/{item}' } != null + assert endpoints.find { it.path == '/posts/{page}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /posts/{page}' } != null } @Test @Order(11) + void 'optional param absent: cache key does not bleed into present case'() { + // Hit /posts (page absent from URL — uses default=1) first so that if the cache key + // were just the route name, the result '/posts' would be stored and served for /posts/2. + HttpRequest absentReq = container.buildReq('/posts').GET().build() + Trace absentTrace = container.traceFromRequest(absentReq, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + } + assert absentTrace.first().meta.'http.route' == '/posts/{page}' + assert absentTrace.first().meta.'_dd.appsec.normalized_route' == '/posts' + + // Now hit /posts/2 (page present in URL). With a coarse cache key (route name only) + // this would incorrectly return '/posts' from cache instead of '/posts/{page}'. + HttpRequest presentReq = container.buildReq('/posts/2').GET().build() + Trace presentTrace = container.traceFromRequest(presentReq, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + } + assert presentTrace.first().meta.'http.route' == '/posts/{page}' + assert presentTrace.first().meta.'_dd.appsec.normalized_route' == '/posts/{page}' + } + + @Test + @Order(12) void 'mixed segment route normalizes both params into one brace group'() { HttpRequest req = container.buildReq('/article/my-post.html').GET().build() Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> diff --git a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php index c6ac1300c3f..9d644693dfd 100644 --- a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php +++ b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php @@ -45,4 +45,10 @@ public function normalizedMixedAction(Request $request, string $slug, string $_f "$slug.$_format" ); } + + #[Route("/posts/{page}", name: "posts_optional_page", defaults: ["page" => 1])] + public function postsAction(Request $request, int $page) + { + return new Response("posts page: $page"); + } } diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 6b0cafabd96..149813294b7 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -287,7 +287,12 @@ static function (SpanData $span) use ($controller, $action) { if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; - $cacheKey = $httpRoute . '|' . ($urlPath ?? ''); + // Only include the URL in the key when the template has optional + // bracket sections; for fully-required routes the normalized form + // is the same for every request regardless of param values. + $cacheKey = strpos($httpRoute, '[') !== false + ? $httpRoute . '|' . ($urlPath ?? '') + : $httpRoute; $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 188f6519445..16787f9ecd4 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -457,10 +457,14 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { - $cacheKey = $route_name; + // inferSymfonyRouteParams is lightweight (URL parsing only); compute it + // first so the cache key encodes which optional params are present. + // Without this, a route with optional params caches only the first + // materialization and serves it for all subsequent URL patterns. + $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); + $cacheKey = $route_name . '|' . implode(',', array_keys($matchedParams)); $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { - $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromSymfony($path, $matchedParams); if ($normalizedRoute !== null) { \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 2ea70d94f18..4a8897ee87f 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -168,6 +168,49 @@ public function testSymfonyNoMatchedParamsArgKeepsAll() $this->assertSame('/blog/{page}', $result); } + // inferSymfonyRouteParams — used to build the cache key in SymfonyIntegration + + public function testInferSymfonyRouteParamsRequiredParamsAlwaysPresent() + { + $params = RouteNormalizer::inferSymfonyRouteParams('/users/{id}', '/users/42'); + $this->assertArrayHasKey('id', $params); + } + + public function testInferSymfonyRouteParamsOptionalParamAbsent() + { + // Route /posts/{page} where page has a Symfony default — URL /posts does not include page. + // The integration code uses array_keys($params) as cache key suffix; this must be [] + // so that the 'absent' cache entry is distinct from the 'present' one. + $params = RouteNormalizer::inferSymfonyRouteParams('/posts/{page}', '/posts'); + $this->assertSame([], $params); + } + + public function testInferSymfonyRouteParamsOptionalParamPresent() + { + // URL /posts/2 provides page explicitly — must be in the returned params. + $params = RouteNormalizer::inferSymfonyRouteParams('/posts/{page}', '/posts/2'); + $this->assertArrayHasKey('page', $params); + } + + public function testInferSymfonyRouteParamsCacheKeysDiffer() + { + // Core invariant for correct cache behaviour: the two URL patterns for the same route + // produce different param key sets, so the cache key suffix encodes presence/absence. + $absent = array_keys(RouteNormalizer::inferSymfonyRouteParams('/posts/{page}', '/posts')); + $present = array_keys(RouteNormalizer::inferSymfonyRouteParams('/posts/{page}', '/posts/2')); + $this->assertNotSame($absent, $present); + + // And normalizeFromSymfony produces the correct result for each case. + $this->assertSame('/posts', RouteNormalizer::normalizeFromSymfony( + '/posts/{page}', + RouteNormalizer::inferSymfonyRouteParams('/posts/{page}', '/posts') + )); + $this->assertSame('/posts/{page}', RouteNormalizer::normalizeFromSymfony( + '/posts/{page}', + RouteNormalizer::inferSymfonyRouteParams('/posts/{page}', '/posts/2') + )); + } + // normalizeFromLaminas public function testLaminasSimpleColon() From 5ed13f3be4b56c231db81b95f2a46d18611ce472 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 16:40:47 +0200 Subject: [PATCH 37/43] Add more tests --- .../php/integration/Laminas33Tests.groovy | 106 +++++++++++++++++- .../php/integration/Symfony62Tests.groovy | 103 ++++++++++++++++- .../php/integration/WordPressTests.groovy | 47 +++++++- .../Application/config/module.config.php | 91 ++++++++++++--- .../src/Controller/HomeController.php | 32 ++++++ .../src/test/www/wordpress/initialize.sh | 3 + .../mu-plugins/normalized-route-test.php | 42 +++++++ 7 files changed, 402 insertions(+), 22 deletions(-) create mode 100644 appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy index 5b339aae6d2..8dcc6b3f59c 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -81,7 +81,7 @@ class Laminas33Tests { endpoints.size() > 0 }) - assert endpoints.size() == 27 + assert endpoints.size() == 33 assert endpoints.find { it.path == '/' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /' } != null assert endpoints.find { it.path == '/application[/:action]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /application[/:action]' @@ -125,7 +125,32 @@ class Laminas33Tests { it.path == '/any-verb' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /any-verb' } != null assert endpoints.find { - it.path == '/user/:user-id' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /user/:user-id' + it.path == '/normalized-regex/%id%.%format%' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized-regex/%id%.%format%' + } != null + assert endpoints.find { + it.path == '/normalized-encoded[/:slug]' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized-encoded[/:slug]' + } != null + assert endpoints.find { + it.path == '/normalized-static[/draft]' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized-static[/draft]' + } != null + assert endpoints.find { + it.path == '/normalized-name/:user-id' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized-name/:user-id' + } != null + assert endpoints.find { + it.path == '/normalized-wildcard/:param1' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized-wildcard/:param1' + } != null + assert endpoints.find { + it.path == '/normalized-wildcard/:param1/*' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized-wildcard/:param1/*' + } != null + assert endpoints.find { + it.path == '/normalized/{translated_page}' && it.method == '*' && + it.operationName == 'http.request' && it.resourceName == '* /normalized/{translated_page}' } != null } @@ -333,13 +358,82 @@ class Laminas33Tests { @Test @Order(13) - void 'hyphenated param name in segment route normalizes correctly'() { + void 'optional regex capture absent is omitted from normalized route'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-regex/article').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == '/normalized-regex/%id%.%format%' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-regex/{id}' + } + + @Test + @Order(14) + void 'encoded optional value is recognized as present'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-encoded/a%20b').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == '/normalized-encoded[/:slug]' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-encoded/{slug}' + } + + @Test + @Order(15) + void 'static-only optional segment absent is omitted'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-static').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == '/normalized-static[/draft]' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-static' + } + + @Test + @Order(16) + void 'hyphenated segment parameter name remains intact'() { Trace trace = container.traceFromRequest( - container.buildReq('/user/42').GET().build(), + container.buildReq('/normalized-name/alice').GET().build(), ofString()) { HttpResponse resp -> assert resp.statusCode() == 200 } - assert trace.first().meta.'http.route' == '/user/:user-id' - assert trace.first().meta.'_dd.appsec.normalized_route' == '/user/{user-id}' + + assert trace.first().meta.'http.route' == '/normalized-name/:user-id' + assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-name/{user-id}' } + + @Test + @Order(17) + void 'wildcard placeholder does not collide with an existing parameter name'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-wildcard/value/foo/bar').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == '/normalized-wildcard/:param1/*' + assert trace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-wildcard/{param1}/{param2}' + } + + @Test + @Order(18) + void 'translated literal remains a static route element'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized/translated-page').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == '/normalized/{translated_page}' + assert trace.first().meta.'_dd.appsec.normalized_route' == + '/normalized/translated-page' + } + } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index 858e88ba7cd..fc3a364ae24 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -166,6 +166,7 @@ class Symfony62Tests { Span span = trace.first() assert span.meta."http.route" == null + assert span.meta."_dd.appsec.normalized_route" == null assert span.meta."symfony.route.name" != null assert span.resource == 'app_home_dynamic' } finally { @@ -213,7 +214,7 @@ class Symfony62Tests { endpoints.size() > 0 }) - assert endpoints.size() == 9 + assert endpoints.size() == 13 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == '/dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /dynamic-path/{param01}' } != null assert endpoints.find { it.path == '/login' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /login' } != null @@ -223,10 +224,108 @@ class Symfony62Tests { assert endpoints.find { it.path == '/article/{slug}.{_format}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /article/{slug}.{_format}' } != null assert endpoints.find { it.path == '/café/{item}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /café/{item}' } != null assert endpoints.find { it.path == '/posts/{page}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /posts/{page}' } != null + assert endpoints.find { + it.path == '/normalized/mixed/{id}.{_format}' && it.method == 'GET' && + it.operationName == 'http.request' && + it.resourceName == 'GET /normalized/mixed/{id}.{_format}' + } != null + assert endpoints.find { + it.path == '/normalized/zero/{id}' && it.method == 'GET' && + it.operationName == 'http.request' && + it.resourceName == 'GET /normalized/zero/{id}' + } != null + assert endpoints.find { + it.path == '/normalized/search.{_format}' && it.method == 'GET' && + it.operationName == 'http.request' && + it.resourceName == 'GET /normalized/search.{_format}' + } != null + assert endpoints.find { + it.path == '/normalized/utf8/{föo}' && it.method == 'GET' && + it.operationName == 'http.request' && + it.resourceName == 'GET /normalized/utf8/{föo}' + } != null } @Test @Order(11) + void 'normalized route is absent when API Security is disabled'() { + try { + def res = CONTAINER.execInContainer( + 'bash', '-c', + '''echo export DD_API_SECURITY_ENABLED=false >> /etc/apache2/envvars; + service apache2 restart''') + assert res.exitCode == 0 + + Trace trace = container.traceFromRequest('/') { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == '/' + assert span.meta.'_dd.appsec.normalized_route' == null + } finally { + def res = CONTAINER.execInContainer( + 'bash', '-c', + '''sed -i '/export DD_API_SECURITY_ENABLED=/d' /etc/apache2/envvars; + service apache2 restart''') + assert res.exitCode == 0 + } + } + + @Test + @Order(12) + void 'mixed dynamic values in one segment are combined'() { + Trace trace = container.traceFromRequest('/normalized/mixed/article.json') { + HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == '/normalized/mixed/{id}.{_format}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized/mixed/{id+_format}' + } + + @Test + @Order(13) + void 'zero-valued path parameter is retained'() { + Trace trace = container.traceFromRequest('/normalized/zero/0') { + HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == '/normalized/zero/{id}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized/zero/{id}' + } + + @Test + @Order(14) + void 'static part of a segment remains when its optional parameter is absent'() { + Trace trace = container.traceFromRequest('/normalized/search') { + HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == '/normalized/search.{_format}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized/search' + } + + @Test + @Order(15) + void 'UTF-8 optional parameter name is omitted when absent'() { + Trace trace = container.traceFromRequest('/normalized/utf8') { + HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == '/normalized/utf8/{föo}' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized/utf8' + } + + @Test + @Order(16) void 'optional param absent: cache key does not bleed into present case'() { // Hit /posts (page absent from URL — uses default=1) first so that if the cache key // were just the route name, the result '/posts' would be stored and served for /posts/2. @@ -248,7 +347,7 @@ class Symfony62Tests { } @Test - @Order(12) + @Order(17) void 'mixed segment route normalizes both params into one brace group'() { HttpRequest req = container.buildReq('/article/my-post.html').GET().build() Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy index 5ab05fc5d7b..89b8618a1cf 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy @@ -3,6 +3,7 @@ package com.datadog.appsec.php.integration import com.datadog.appsec.php.docker.AppSecContainer import com.datadog.appsec.php.docker.FailOnUnmatchedTraces import com.datadog.appsec.php.docker.InspectContainerHelper +import com.datadog.appsec.php.docker.PhpFpm import com.datadog.appsec.php.model.Span import com.datadog.appsec.php.model.Trace import groovy.util.logging.Slf4j @@ -96,9 +97,15 @@ class WordPressTests { res = CONTAINER.execInContainer('bash', '-c', """export DD_TRACE_CLI_ENABLED=false DD_APPSEC_ENABLED=0 wp option update siteurl 'http://localhost:${port}' --path=/var/www/public --allow-root - wp option update home 'http://localhost:${port}' --path=/var/www/public --allow-root""") + wp option update home 'http://localhost:${port}' --path=/var/www/public --allow-root + wp rewrite structure '/%postname%/' --path=/var/www/public --allow-root + wp rewrite flush --hard --path=/var/www/public --allow-root""") assert res.exitCode == 0 : "Failed to update WordPress URLs: ${res.stderr}" + PhpFpm fpm = new PhpFpm(CONTAINER) + fpm.setPoolValue('pm.max_children', '1') + fpm.reload() + CONTAINER.clearTraces() } @@ -194,4 +201,42 @@ class WordPressTests { assert span.meta."_dd.appsec.usr.id" == "1" assert span.meta."_dd.appsec.user.collection_mode" == "identification" } + + @Test + @Order(6) + void 'static prefix remains when optional rewrite capture is absent'() { + Trace trace = CONTAINER.traceFromRequest('/normalized-cache/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + Span span = trace.first() + assert span.meta.'http.route' == + '^normalized-cache(?:/([^/]+))?/?$' + assert span.meta.'_dd.appsec.normalized_route' == '/normalized-cache' + } + + @Test + @Order(7) + void 'optional rewrite capture is normalized for each request'() { + Trace absentTrace = CONTAINER.traceFromRequest('/normalized-cache-shape/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + Span absentSpan = absentTrace.first() + assert absentSpan.meta.'http.route' == + '^normalized-cache-shape/?([^/]*)/?$' + assert absentSpan.meta.'_dd.appsec.normalized_route' == + '/normalized-cache-shape' + + Trace presentTrace = CONTAINER.traceFromRequest( + '/normalized-cache-shape/present/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + Span presentSpan = presentTrace.first() + assert presentSpan.meta.'http.route' == + '^normalized-cache-shape/?([^/]*)/?$' + assert presentSpan.meta.'_dd.appsec.normalized_route' == + '/normalized-cache-shape/{param1}' + } } diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php index 5b4bbe5e916..d107cc18d70 100644 --- a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php @@ -180,6 +180,83 @@ ], ], ], + 'regex_optional_format' => [ + 'type' => Regex::class, + 'options' => [ + 'regex' => '/normalized-regex/(?P[a-z]+)(?:\.(?P[a-z]+))?', + 'spec' => '/normalized-regex/%id%.%format%', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + 'format' => 'html', + ], + ], + ], + 'normalized_encoded_optional' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-encoded[/:slug]', + 'constraints' => [ + 'slug' => '.+', + ], + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], + 'normalized_static_optional' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-static[/draft]', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], + 'normalized_hyphenated_name' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-name/:user-id', + 'constraints' => [ + 'user-id' => '[a-z]+', + ], + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], + 'normalized_wildcard_collision' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-wildcard/:param1', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + 'may_terminate' => false, + 'child_routes' => [ + 'tail' => [ + 'type' => Wildcard::class, + 'options' => [ + 'defaults' => [], + ], + ], + ], + ], + 'normalized_translated_literal' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized/{translated_page}', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], 'scheme_http_gate' => [ 'type' => Scheme::class, 'options' => [ @@ -228,19 +305,7 @@ ], ], ], - 'user_by_id' => [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/user/:user-id', - 'constraints' => [ - 'user-id' => '[0-9]+', - ], - 'defaults' => [ - 'controller' => DynamicPathController::class, - 'action' => 'index', - ], - ], - ], + 'wildcard_keys' => [ 'type' => Literal::class, 'options' => [ diff --git a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php index 9d644693dfd..94d8189ea4e 100644 --- a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php +++ b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php @@ -51,4 +51,36 @@ public function postsAction(Request $request, int $page) { return new Response("posts page: $page"); } + + #[Route("/normalized/mixed/{id}.{_format}", name: "normalized_mixed")] + public function normalizedMixedIdAction(Request $request) + { + return new Response('Mixed route'); + } + + #[Route("/normalized/zero/{id}", name: "normalized_zero")] + public function normalizedZeroAction(Request $request) + { + return new Response('Zero route'); + } + + #[Route( + "/normalized/search.{_format}", + name: "normalized_static_optional", + defaults: ["_format" => null] + )] + public function normalizedStaticOptionalAction(Request $request) + { + return new Response('Optional format route'); + } + + #[Route( + "/normalized/utf8/{föo}", + name: "normalized_utf8_optional", + defaults: ["föo" => null] + )] + public function normalizedUtf8OptionalAction(Request $request) + { + return new Response('UTF-8 parameter route'); + } } diff --git a/appsec/tests/integration/src/test/www/wordpress/initialize.sh b/appsec/tests/integration/src/test/www/wordpress/initialize.sh index 7ba2e9e4991..9ecbd00c962 100755 --- a/appsec/tests/integration/src/test/www/wordpress/initialize.sh +++ b/appsec/tests/integration/src/test/www/wordpress/initialize.sh @@ -11,6 +11,9 @@ cp /test-resources/public/wp-config.php wp-config.php cp /test-resources/public/index.php index.php cp /test-resources/public/login_trigger.php login_trigger.php cp /test-resources/public/hello.php hello.php +mkdir -p wp-content/mu-plugins +cp /test-resources/public/wp-content/mu-plugins/normalized-route-test.php \ + wp-content/mu-plugins/normalized-route-test.php # Download WP-CLI curl -sf https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp diff --git a/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php b/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php new file mode 100644 index 00000000000..f315c9e0ffe --- /dev/null +++ b/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php @@ -0,0 +1,42 @@ +get('normalized_route_test')) { + return true; + } + return $preempt; +}, 10, 2); + +add_action('template_redirect', static function () { + if (! get_query_var('normalized_route_test')) { + return; + } + + status_header(200); + header('Content-Type: text/plain'); + echo get_query_var('normalized_value') ?: 'absent'; + exit; +}); From d5e909b3d40dc7d20815d5dc2836dd11e2cb2916 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 27 Aug 2026 18:12:03 +0200 Subject: [PATCH 38/43] Fix pipeline --- .../php/integration/Laminas33Tests.groovy | 20 +------------------ .../Application/config/module.config.php | 10 ---------- .../Symfony/SymfonyIntegration.php | 4 +++- .../WordPress/WordPressIntegrationLoader.php | 5 +++-- src/DDTrace/Util/RouteNormalizer.php | 4 ++-- 5 files changed, 9 insertions(+), 34 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy index 8dcc6b3f59c..039b2d01413 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -81,7 +81,7 @@ class Laminas33Tests { endpoints.size() > 0 }) - assert endpoints.size() == 33 + assert endpoints.size() == 32 assert endpoints.find { it.path == '/' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /' } != null assert endpoints.find { it.path == '/application[/:action]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /application[/:action]' @@ -148,10 +148,6 @@ class Laminas33Tests { it.path == '/normalized-wildcard/:param1/*' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /normalized-wildcard/:param1/*' } != null - assert endpoints.find { - it.path == '/normalized/{translated_page}' && it.method == '*' && - it.operationName == 'http.request' && it.resourceName == '* /normalized/{translated_page}' - } != null } @Test @@ -422,18 +418,4 @@ class Laminas33Tests { '/normalized-wildcard/{param1}/{param2}' } - @Test - @Order(18) - void 'translated literal remains a static route element'() { - Trace trace = container.traceFromRequest( - container.buildReq('/normalized/translated-page').GET().build(), - ofString()) { HttpResponse resp -> - assert resp.statusCode() == 200 - } - - assert trace.first().meta.'http.route' == '/normalized/{translated_page}' - assert trace.first().meta.'_dd.appsec.normalized_route' == - '/normalized/translated-page' - } - } diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php index d107cc18d70..d6aca885d37 100644 --- a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php @@ -247,16 +247,6 @@ ], ], ], - 'normalized_translated_literal' => [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/normalized/{translated_page}', - 'defaults' => [ - 'controller' => DynamicPathController::class, - 'action' => 'index', - ], - ], - ], 'scheme_http_gate' => [ 'type' => Scheme::class, 'options' => [ diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 16787f9ecd4..395eb6f6abe 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -456,7 +456,9 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + $rawIni = ini_get('datadog.api_security_enabled'); + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled() + && ($rawIni === false || filter_var($rawIni, FILTER_VALIDATE_BOOLEAN) !== false)) { // inferSymfonyRouteParams is lightweight (URL parsing only); compute it // first so the cache key encodes which optional params are present. // Without this, a route with optional params caches only the first diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index 035b0fa2f20..28262cbb49f 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -736,11 +736,12 @@ function_exists('is_404') && is_404() === false) { $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { $urlPath = \property_exists($This, 'request') ? $This->request : null; - $normalizedRoute = \DDTrace\routing_cache_get($matchedRule); + $cacheKey = $matchedRule . '|' . ($urlPath ?? ''); + $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); if ($normalizedRoute !== null) { - \DDTrace\routing_cache_set($matchedRule, $normalizedRoute); + \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); } } if ($normalizedRoute !== null && $normalizedRoute !== false) { diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 11f250a66cd..063717d81f8 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -522,8 +522,8 @@ public static function encodeParamName(string $name): string */ public static function inferSymfonyRouteParams(string $template, string $urlPath): array { - $templateSegments = array_values(array_filter(explode('/', $template))); - $urlSegments = array_values(array_filter(explode('/', $urlPath))); + $templateSegments = array_values(array_filter(explode('/', $template), 'strlen')); + $urlSegments = array_values(array_filter(explode('/', $urlPath), 'strlen')); $matched = []; $urlIdx = 0; From be6fad4dcd97464a08778d767b75635b644611f2 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Fri, 28 Aug 2026 09:38:28 +0200 Subject: [PATCH 39/43] Fix pipeline --- src/DDTrace/Util/RouteNormalizer.php | 53 ++++++++++++++----- .../Util/Normalizer/RouteNormalizerTest.php | 22 ++++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 063717d81f8..d6f2693527f 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -68,8 +68,26 @@ public static function normalizeFromLaminas(string $template, array $matchedPara } // Segment routes use :param; Regex routes use %param% (spec format) — handle both. + // Detect Regex routes before conversion so we can apply URL-based param filtering. + $hasPercentParams = (bool) preg_match('/%([a-zA-Z_][a-zA-Z0-9_]*)%/', $expanded); $braceFormat = self::colonParamsToBraces($expanded); $braceFormat = self::percentParamsToBraces($braceFormat); + + // For Regex routes the defaults array injects values into matchedParams even for + // optional captures absent from the URL (e.g. format='html' when no .html in path). + // Use the URL path to determine which params were actually URL-matched. + if ($hasPercentParams && $urlPath !== null) { + $urlMatchedParams = self::inferSymfonyRouteParams($braceFormat, $urlPath); + $braceFormat = preg_replace_callback( + '/\{([^}?:]+)\}/', + static function ($m) use ($urlMatchedParams) { + return array_key_exists($m[1], $urlMatchedParams) ? $m[0] : '{' . $m[1] . '?}'; + }, + $braceFormat + ); + return self::normalizeBraceRoute($braceFormat, $urlMatchedParams); + } + return self::normalizeBraceRoute($braceFormat, $matchedParams); } @@ -537,24 +555,33 @@ public static function inferSymfonyRouteParams(string $template, string $urlPath $urlIdx++; } elseif (preg_match('/\{/', $seg)) { // Mixed segment (static text + one or more params): determine presence by - // trying to match the URL segment against the template pattern. + // trying to match the URL segment, dropping trailing optional params as needed. if ($urlIdx < count($urlSegments)) { preg_match_all('/\{([^}?:]+)\}/', $seg, $pm); - $paramNames = $pm[1]; - if (!empty($paramNames)) { - // Build a lightweight regex from the template segment + $paramNames = $pm[1]; + $n = count($paramNames); + if ($n > 0) { + $urlSeg = $urlSegments[$urlIdx]; $staticParts = preg_split('/\{[^}]+\}/', $seg); - $regexParts = array_map( - static function ($p) { return preg_quote($p, '/'); }, - $staticParts - ); - $segRegex = '/^' . implode('(.+)', $regexParts) . '$/'; - if (@preg_match($segRegex, $urlSegments[$urlIdx])) { - foreach ($paramNames as $p) { - $matched[$p] = true; + // Try with k params (k = n, n-1, ..., 1). Drop params from the right + // until the URL segment matches. This handles optional trailing captures + // that were injected as route defaults but absent from the URL. + for ($k = $n; $k >= 1; $k--) { + $regexBody = ''; + for ($i = 0; $i < $k; $i++) { + $regexBody .= preg_quote($staticParts[$i], '/') . '(.+)'; + } + // Only include the trailing static part for a full match + if ($k === $n) { + $regexBody .= preg_quote($staticParts[$n], '/'); + } + if (@preg_match('/^' . $regexBody . '$/', $urlSeg)) { + for ($i = 0; $i < $k; $i++) { + $matched[$paramNames[$i]] = true; + } + break; } } - // else: URL segment doesn't contain the dynamic part → params absent } } $urlIdx++; diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index 4a8897ee87f..dc58d65bbd8 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -279,6 +279,28 @@ public function testLaminasRegexRouteSpec() $this->assertSame('/user/{id}/{name}', RouteNormalizer::normalizeFromLaminas('/user/%id%/%name%')); } + public function testLaminasRegexRouteOptionalFormatAbsent() + { + // Route defaults inject format='html' even when the URL has no .html extension. + // Only params actually present in the URL path should appear in the normalized route. + $result = RouteNormalizer::normalizeFromLaminas( + '/normalized-regex/%id%.%format%', + ['id' => 'article', 'format' => 'html', 'controller' => 'C', 'action' => 'index'], + '/normalized-regex/article' + ); + $this->assertSame('/normalized-regex/{id}', $result); + } + + public function testLaminasRegexRouteOptionalFormatPresent() + { + $result = RouteNormalizer::normalizeFromLaminas( + '/normalized-regex/%id%.%format%', + ['id' => 'article', 'format' => 'html', 'controller' => 'C', 'action' => 'index'], + '/normalized-regex/article.html' + ); + $this->assertSame('/normalized-regex/{id+format}', $result); + } + public function testLaminasLiteralRoute() { $this->assertSame('/dump-request', RouteNormalizer::normalizeFromLaminas('/dump-request')); From d30a83bc7307fa9af840f0124022f27dfaed8712 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Fri, 28 Aug 2026 18:10:40 +0200 Subject: [PATCH 40/43] Address report comments --- .../Laminas/LaminasIntegration.php | 40 +++++++++++-- .../Laravel/LaravelIntegration.php | 38 +++++++++++-- .../WordPress/WordPressIntegrationLoader.php | 17 +++++- src/DDTrace/Util/RouteNormalizer.php | 56 +++++++++++++------ .../Util/Normalizer/RouteNormalizerTest.php | 37 ++++++++++++ 5 files changed, 159 insertions(+), 29 deletions(-) diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 149813294b7..1e2b55ce78f 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -287,12 +287,40 @@ static function (SpanData $span) use ($controller, $action) { if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; - // Only include the URL in the key when the template has optional - // bracket sections; for fully-required routes the normalized form - // is the same for every request regardless of param values. - $cacheKey = strpos($httpRoute, '[') !== false - ? $httpRoute . '|' . ($urlPath ?? '') - : $httpRoute; + // Build a stable cache key that encodes only which optional + // components participated, not the raw URL (which would cause + // one cache entry per distinct request value — 500-entry churn). + // + // Regex routes (%param% spec): infer which params appear in the + // URL and encode their sorted names as a presence key. + // Bracket routes ([/:param]): collect optional colon-params whose + // values appear in the URL as a presence key. + // Fully-required routes: template alone is sufficient. + if ($urlPath !== null && strpos($httpRoute, '%') !== false) { + // Regex route: convert spec to brace form and infer presence + $braceTemp = preg_replace('/%([a-zA-Z_][a-zA-Z0-9_]*)%/', '{$1}', $httpRoute); + $urlMatchedKeys = array_keys( + \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($braceTemp, $urlPath) + ); + sort($urlMatchedKeys); + $cacheKey = $httpRoute . '#' . implode(',', $urlMatchedKeys); + } elseif (strpos($httpRoute, '[') !== false) { + // Bracket-optional route: encode which optional colon-params + // have values that appear in the URL path. + preg_match_all('/:([a-zA-Z_][a-zA-Z0-9_-]*)/', $httpRoute, $_pm); + $_present = []; + foreach ($_pm[1] as $_p) { + if (isset($allParams[$_p]) && $urlPath !== null && + strpos($urlPath, (string)$allParams[$_p]) !== false) { + $_present[] = $_p; + } + } + sort($_present); + $cacheKey = $httpRoute . '#' . implode(',', $_present); + unset($_pm, $_present, $_p); + } else { + $cacheKey = $httpRoute; + } $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); diff --git a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php index 3b2114e9405..e1b54b91573 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -808,21 +808,49 @@ private static function laravelUrlMatchedParams(string $routeUri, string $urlPat $urlIdx = 0; foreach ($routeSegs as $seg) { - if (preg_match('/^\{([^}?]+)\?\}$/', $seg, $m)) { - // Optional param — include only when URL has a segment at this position + if (preg_match('/^\{([^}?:]+)\?\}$/', $seg, $m)) { + // Whole-segment optional param if ($urlIdx < count($urlSegs) && $urlSegs[$urlIdx] !== '') { if (array_key_exists($m[1], $allParams)) { $matched[$m[1]] = $allParams[$m[1]]; } $urlIdx++; } - } elseif (preg_match('/^\{([^}]+)\}$/', $seg)) { - // Required param — always present - preg_match('/^\{([^}]+)\}$/', $seg, $m); + } elseif (preg_match('/^\{([^}?:]+)\}$/', $seg, $m)) { + // Whole-segment required param — always present if (array_key_exists($m[1], $allParams)) { $matched[$m[1]] = $allParams[$m[1]]; } $urlIdx++; + } elseif (strpos($seg, '{') !== false) { + // Mixed segment (e.g. "{name}.{ext?}"): use progressive regex matching + // to determine which params (including optional ones) appear in the URL. + if ($urlIdx < count($urlSegs) && $urlSegs[$urlIdx] !== '') { + preg_match_all('/\{([^}?:]+)(\?)?\}/', $seg, $pm, PREG_SET_ORDER); + $paramNames = array_map(static function($m) { return $m[1]; }, $pm); + $staticParts = preg_split('/\{[^}]+\}/', $seg); + $n = count($paramNames); + $urlSeg = $urlSegs[$urlIdx]; + + for ($k = $n; $k >= 1; $k--) { + $regexBody = ''; + for ($ri = 0; $ri < $k; $ri++) { + $regexBody .= preg_quote($staticParts[$ri], '/') . '(.+)'; + } + if ($k === $n) { + $regexBody .= preg_quote($staticParts[$n], '/'); + } + if (@preg_match('/^' . $regexBody . '$/', $urlSeg)) { + for ($ri = 0; $ri < $k; $ri++) { + if (array_key_exists($paramNames[$ri], $allParams)) { + $matched[$paramNames[$ri]] = $allParams[$paramNames[$ri]]; + } + } + break; + } + } + } + $urlIdx++; } else { $urlIdx++; } diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index 28262cbb49f..c6edc233e83 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -736,7 +736,22 @@ function_exists('is_404') && is_404() === false) { $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { $urlPath = \property_exists($This, 'request') ? $This->request : null; - $cacheKey = $matchedRule . '|' . ($urlPath ?? ''); + // Key on the count of matched capture groups, not the full URL, + // so all requests with the same optional-capture participation + // share one cache entry rather than one entry per distinct URL. + $wpMatchedGroupCount = null; + if ($urlPath !== null) { + if (@preg_match('#^' . $matchedRule . '#', ltrim($urlPath, '/'), $_wpc)) { + $wpMatchedGroupCount = 0; + for ($_wi = 1; $_wi < count($_wpc); $_wi++) { + if (isset($_wpc[$_wi]) && $_wpc[$_wi] !== '') { + $wpMatchedGroupCount = $_wi; + } + } + unset($_wpc, $_wi); + } + } + $cacheKey = $matchedRule . '#' . ($wpMatchedGroupCount ?? 'n'); $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index d6f2693527f..449618b00a4 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -105,15 +105,16 @@ static function ($m) use ($urlMatchedParams) { */ public static function normalizeFromWordPress(string $matchedRule, $urlPath = null) { - // Re-run the regex against the actual URL to find how many capture groups matched. - // This handles optional groups like (?:/([0-9]+))? that may or may not be present. - $matchedGroupCount = null; + // Re-run the regex against the actual URL to find which capture groups matched. + // Tracks each group individually so gaps from optional groups (e.g. (?:(...))?) + // that didn't participate are skipped instead of emitting phantom params. + $matchedGroups = null; if ($urlPath !== null) { if (@preg_match('#^' . $matchedRule . '#', ltrim($urlPath, '/'), $captures)) { - $matchedGroupCount = 0; + $matchedGroups = []; for ($i = 1; $i < count($captures); $i++) { if (isset($captures[$i]) && $captures[$i] !== '') { - $matchedGroupCount = $i; + $matchedGroups[$i] = true; } } } @@ -150,9 +151,10 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu $dynamicPart = substr($segment, $prefixLen); $groupCount = self::countCaptureGroups($dynamicPart); - // Only emit a static prefix when there is at most one capture group; - // mixed segments with multiple groups are treated as fully dynamic. - if ($prefixLen > 0 && $groupCount <= 1) { + // Only emit a static prefix for purely-regex segments with no capture + // groups. When captures exist the whole segment (prefix + captures) maps + // to one RFC element, so the prefix must not become a separate element. + if ($prefixLen > 0 && $groupCount === 0) { $staticPart = rtrim(substr($segment, 0, $prefixLen), '/-._'); if ($staticPart !== '') { $normalizedSegments[] = self::encodeStaticSegment($staticPart); @@ -160,15 +162,17 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu } if ($groupCount === 0) { - if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { + if ($matchedGroups !== null && !isset($matchedGroups[$paramIndex])) { + $paramIndex++; continue; } $normalizedSegments[] = '{param' . $paramIndex++ . '}'; } else { $params = []; for ($j = 0; $j < $groupCount; $j++) { - if ($matchedGroupCount !== null && $paramIndex > $matchedGroupCount) { - break; + if ($matchedGroups !== null && !isset($matchedGroups[$paramIndex])) { + $paramIndex++; + continue; } $params[] = 'param' . $paramIndex++; } @@ -344,8 +348,9 @@ private static function normalizeBraceSegment(string $segment, array $matchedPar if (empty($paramNames)) { // All params were optional and absent. // Preserve any static text remaining in the segment (e.g. "search.{_format?}" → "search"). + // rtrim only: a leading special char (e.g. '~foo.{ext?}') must survive. $staticOnly = preg_replace('/\{[^}]+\}/', '', $segment); - $staticOnly = trim($staticOnly, '.-_~'); + $staticOnly = rtrim($staticOnly, '.-_~'); if ($staticOnly !== '') { return self::encodeStaticSegment($staticOnly); } @@ -406,10 +411,16 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { if ($urlPath !== null) { // Substitute every param value before checking the URL so that // multi-param sections like [/:year/:month] are found correctly. + // Use a word-boundary-aware replacement so :id is not replaced + // inside :id2 (str_replace(':id', ...) would corrupt ':id2'). $innerWithValues = $inner; foreach ($innerParams as $param) { $value = (string)$matchedParams[$param]; - $innerWithValues = str_replace($paramPrefix . $param, $value, $innerWithValues); + $innerWithValues = preg_replace( + '/' . preg_quote($paramPrefix . $param, '/') . '(?![a-zA-Z0-9_-])/', + $value, + $innerWithValues + ); } if (strpos($urlPath, $innerWithValues) !== false) { return $inner; @@ -418,7 +429,11 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { $innerEncoded = $inner; foreach ($innerParams as $param) { $value = rawurlencode((string)$matchedParams[$param]); - $innerEncoded = str_replace($paramPrefix . $param, $value, $innerEncoded); + $innerEncoded = preg_replace( + '/' . preg_quote($paramPrefix . $param, '/') . '(?![a-zA-Z0-9_-])/', + $value, + $innerEncoded + ); } if (strpos($urlPath, $innerEncoded) !== false) { return $inner; @@ -467,8 +482,10 @@ private static function uniqueParamName(string $template, string $paramPrefix = { $i = 1; while ( - strpos($template, $paramPrefix . 'param' . $i) !== false || - strpos($template, '{param' . $i . '}') !== false + // Use regex so ':param1' doesn't falsely match inside ':param10' + preg_match('/' . preg_quote($paramPrefix . 'param' . $i, '/') . '(?![0-9])/', $template) || + strpos($template, '{param' . $i . '}') !== false || + strpos($template, '%param' . $i . '%') !== false ) { $i++; } @@ -541,7 +558,12 @@ public static function encodeParamName(string $name): string public static function inferSymfonyRouteParams(string $template, string $urlPath): array { $templateSegments = array_values(array_filter(explode('/', $template), 'strlen')); - $urlSegments = array_values(array_filter(explode('/', $urlPath), 'strlen')); + // Decode percent-encoded URL path so template literals (e.g. café) compare + // correctly against encoded URL segments (e.g. caf%C3%A9). + $urlSegments = array_values(array_filter( + array_map('rawurldecode', explode('/', $urlPath)), + 'strlen' + )); $matched = []; $urlIdx = 0; diff --git a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php index dc58d65bbd8..c9459206555 100644 --- a/tests/Unit/Util/Normalizer/RouteNormalizerTest.php +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -367,6 +367,43 @@ public function testWordPressMultipleCaptureGroupsInOneSegment() $this->assertSame('/{param1+param2}', $result); } + public function testWordPressStaticPrefixNotSeparateElement() + { + // F-07: static prefix before a capture must NOT become a separate segment element. + // "post-([^/]+)" is a single URL segment → one RFC element. + $result = RouteNormalizer::normalizeFromWordPress('^post-([^/]+)$', 'post-hello'); + $this->assertSame('/{param1}', $result); + } + + public function testWordPressAbsentOptionalCaptureSkipped() + { + // F-11: when an inner optional capture did not participate (empty string in captures), + // it must not produce a phantom {paramN}. + $result = RouteNormalizer::normalizeFromWordPress('^(?:([^/]+)-)?([^/]+)$', 'x'); + $this->assertSame('/{param2}', $result); + } + + public function testWordPressBothCapturesPresentInOptionalGroup() + { + $result = RouteNormalizer::normalizeFromWordPress('^(?:([^/]+)-)?([^/]+)$', 'foo-x'); + $this->assertSame('/{param1+param2}', $result); + } + + public function testStaticPrefixLeadingTildePreservedWhenOptionalAbsent() + { + // F-04: rtrim — a leading special char like '~' must survive when the optional + // param is absent. Old behaviour: trim('~foo.', '.-_~') = 'foo'. Fixed: rtrim. + $result = RouteNormalizer::normalizeFromLaravel('~foo.{ext?}', []); + $this->assertSame('/~foo', $result); + } + + public function testLaminasWildcardAfterPercentParam() + { + // F-10: uniqueParamName must skip %param1% when choosing a name for the wildcard. + $result = RouteNormalizer::normalizeFromLaminas('/foo/%param1%/*'); + $this->assertSame('/foo/{param1}/{param2}', $result); + } + // RFC examples public function testRfcExampleFastApi() From af18f5c4fb733e5a578fe862dc06e9db29e865f5 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 1 Sep 2026 10:17:57 +0200 Subject: [PATCH 41/43] Add new tests --- .../php/integration/Laminas33Tests.groovy | 177 +++++++++++++++++- .../php/integration/Laravel8xTests.groovy | 53 +++++- .../php/integration/Symfony62Tests.groovy | 26 ++- .../php/integration/WordPressTests.groovy | 89 +++++++++ .../Application/config/module.config.php | 54 ++++++ .../src/Controller/DynamicPathController.php | 11 ++ .../src/test/www/laravel8x/routes/web.php | 6 + .../src/Controller/HomeController.php | 15 ++ .../mu-plugins/normalized-route-test.php | 15 ++ 9 files changed, 438 insertions(+), 8 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy index 039b2d01413..f9e5e443f6e 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -81,7 +81,7 @@ class Laminas33Tests { endpoints.size() > 0 }) - assert endpoints.size() == 32 + assert endpoints.size() == 37 assert endpoints.find { it.path == '/' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /' } != null assert endpoints.find { it.path == '/application[/:action]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /application[/:action]' @@ -128,6 +128,12 @@ class Laminas33Tests { it.path == '/normalized-regex/%id%.%format%' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /normalized-regex/%id%.%format%' } != null + assert endpoints.find { + it.path == '/normalized-regex-ambiguous/%name%.%ext%' && + it.method == '*' && it.operationName == 'http.request' && + it.resourceName == + '* /normalized-regex-ambiguous/%name%.%ext%' + } != null assert endpoints.find { it.path == '/normalized-encoded[/:slug]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /normalized-encoded[/:slug]' @@ -136,6 +142,26 @@ class Laminas33Tests { it.path == '/normalized-static[/draft]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /normalized-static[/draft]' } != null + assert endpoints.find { + it.path == '/normalized-static-prefix[/normalized]' && it.method == '*' && + it.operationName == 'http.request' && + it.resourceName == '* /normalized-static-prefix[/normalized]' + } != null + assert endpoints.find { + it.path == '/normalized-dynamic-prefix[/:value]' && it.method == '*' && + it.operationName == 'http.request' && + it.resourceName == '* /normalized-dynamic-prefix[/:value]' + } != null + assert endpoints.find { + it.path == '/normalized-encoded-cache[/:slug]' && it.method == '*' && + it.operationName == 'http.request' && + it.resourceName == '* /normalized-encoded-cache[/:slug]' + } != null + assert endpoints.find { + it.path == '/normalized-encoded-lowercase[/:slug]' && it.method == '*' && + it.operationName == 'http.request' && + it.resourceName == '* /normalized-encoded-lowercase[/:slug]' + } != null assert endpoints.find { it.path == '/normalized-name/:user-id' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /normalized-name/:user-id' @@ -380,19 +406,111 @@ class Laminas33Tests { @Test @Order(15) - void 'static-only optional segment absent is omitted'() { + void 'lowercase percent escapes retain an optional matched value'() { Trace trace = container.traceFromRequest( - container.buildReq('/normalized-static').GET().build(), + container.buildReq('/normalized-encoded-lowercase/%c3%a9').GET().build(), ofString()) { HttpResponse resp -> assert resp.statusCode() == 200 } - assert trace.first().meta.'http.route' == '/normalized-static[/draft]' - assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-static' + assert trace.first().meta.'http.route' == + '/normalized-encoded-lowercase[/:slug]' + assert trace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-encoded-lowercase/{slug}' } @Test @Order(16) + void 'static optional text is matched only at its route position'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-static-prefix').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == + '/normalized-static-prefix[/normalized]' + // The optional suffix is absent. Its text happens to be a prefix of + // the mandatory segment and must not be detected there. + assert trace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-static-prefix' + } + + @Test + @Order(17) + void 'defaulted optional value is matched only at its route position'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-dynamic-prefix').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == + '/normalized-dynamic-prefix[/:value]' + // The framework-injected default equals earlier static route text. It + // does not mean the optional URL segment participated in this request. + // Laminas merges defaults and captures in RouteMatch, so RFC-1103 also + // permits omitting the tag when accurate participation is unavailable. + String normalizedRoute = trace.first().meta.'_dd.appsec.normalized_route' + assert normalizedRoute == null || + normalizedRoute == '/normalized-dynamic-prefix' + } + + @Test + @Order(18) + void 'encoded optional presence is not poisoned by a prior cache shape'() { + // The lowercase request is known to be misclassified as absent. It + // primes the result cache with the absent shape; the next request has + // an uppercase encoding that normalizes correctly when run alone. + container.traceFromRequest( + container.buildReq('/normalized-encoded-cache/%c3%a9').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Trace presentTrace = container.traceFromRequest( + container.buildReq('/normalized-encoded-cache/a%20b').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert presentTrace.first().meta.'http.route' == + '/normalized-encoded-cache[/:slug]' + assert presentTrace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-encoded-cache/{slug}' + } + + @Test + @Order(19) + void 'static-only optional shapes do not share a cached result'() { + Trace absentTrace = container.traceFromRequest( + container.buildReq('/normalized-static').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert absentTrace.first().meta.'http.route' == + '/normalized-static[/draft]' + assert absentTrace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-static' + + Trace presentTrace = container.traceFromRequest( + container.buildReq('/normalized-static/draft').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert presentTrace.first().meta.'http.route' == + '/normalized-static[/draft]' + // The cache suffix contains only optional parameter names. This route's + // optional group is purely static, so absent and present both use the + // same key even though they require different normalized results. + assert presentTrace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-static/draft' + } + + @Test + @Order(20) void 'hyphenated segment parameter name remains intact'() { Trace trace = container.traceFromRequest( container.buildReq('/normalized-name/alice').GET().build(), @@ -405,7 +523,7 @@ class Laminas33Tests { } @Test - @Order(17) + @Order(21) void 'wildcard placeholder does not collide with an existing parameter name'() { Trace trace = container.traceFromRequest( container.buildReq('/normalized-wildcard/value/foo/bar').GET().build(), @@ -418,4 +536,51 @@ class Laminas33Tests { '/normalized-wildcard/{param1}/{param2}' } + @Test + @Order(22) + void 'Regex constraints distinguish an absent defaulted parameter'() { + Trace trace = container.traceFromRequest( + container.buildReq('/normalized-regex-ambiguous/report.txt') + .GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + assert resp.body() == 'report.txt/html' + } + + assert trace.first().meta.'http.route' == + '/normalized-regex-ambiguous/%name%.%ext%' + // The route regex accepts only pdf or json as ext, so report.txt is + // consumed entirely by name and ext comes only from its html default. + // Generic URL inference ignores that regex and treats txt as matched. + assert trace.first().meta.'_dd.appsec.normalized_route' == + '/normalized-regex-ambiguous/{name}' + } + + @Test + @Order(23) + void 'normalized route is absent when API Security is disabled'() { + try { + def res = CONTAINER.execInContainer( + 'bash', '-c', + '''echo export DD_API_SECURITY_ENABLED=false >> /etc/apache2/envvars; + service apache2 restart''') + assert res.exitCode == 0 + + Trace trace = container.traceFromRequest( + container.buildReq('/application').GET().build(), + ofString()) { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + assert trace.first().meta.'http.route' == '/application[/:action]' + assert trace.first().meta.'_dd.appsec.normalized_route' == null + } finally { + def res = CONTAINER.execInContainer( + 'bash', '-c', + '''sed -i '/export DD_API_SECURITY_ENABLED=/d' /etc/apache2/envvars; + service apache2 restart''') + assert res.exitCode == 0 + } + } + } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy index 988d4f32e25..4ccea96c7fc 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laravel8xTests.groovy @@ -213,7 +213,7 @@ class Laravel8xTests { endpoints.size() > 0 }) - assert endpoints.size() == 8 + assert endpoints.size() == 9 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == 'authenticate' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET authenticate' } != null assert endpoints.find { it.path == 'register' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET register' } != null @@ -222,6 +222,11 @@ class Laravel8xTests { assert endpoints.find { it.path == 'api/user' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET api/user' } != null assert endpoints.find { it.path == 'normalized-optional/{value?}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET normalized-optional/{value?}' } != null assert endpoints.find { it.path == 'normalized-default/{format?}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET normalized-default/{format?}' } != null + assert endpoints.find { + it.path == 'normalized-ambiguous/{name}.{ext?}' && it.method == 'GET' && + it.operationName == 'http.request' && + it.resourceName == 'GET normalized-ambiguous/{name}.{ext?}' + } != null } @Test @@ -268,4 +273,50 @@ class Laravel8xTests { assert span.meta.'http.route' == 'normalized-default/{format?}' assert span.meta.'_dd.appsec.normalized_route' == '/normalized-default' } + + @Test + @Order(13) + void 'route requirements distinguish an absent defaulted mixed parameter'() { + HttpRequest req = container.buildReq('/normalized-ambiguous/report.txt').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body() == 'report.txt/html' + } + + Span span = trace.first() + assert span.meta.'http.route' == + 'normalized-ambiguous/{name}.{ext?}' + // Laravel matched all of "report.txt" as name because ext only accepts + // pdf or json, then supplied the default ext. The integration ignores + // those requirements and infers ext participation from the dot alone. + assert span.meta.'_dd.appsec.normalized_route' == + '/normalized-ambiguous/{name}' + } + + @Test + @Order(14) + void 'normalized route is absent when API Security is disabled'() { + try { + def res = CONTAINER.execInContainer( + 'bash', '-c', + '''echo export DD_API_SECURITY_ENABLED=false >> /etc/apache2/envvars; + service apache2 restart''') + assert res.exitCode == 0 + + HttpRequest req = container.buildReq('/normalized-optional/hello').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == 'normalized-optional/{value?}' + assert span.meta.'_dd.appsec.normalized_route' == null + } finally { + def res = CONTAINER.execInContainer( + 'bash', '-c', + '''sed -i '/export DD_API_SECURITY_ENABLED=/d' /etc/apache2/envvars; + service apache2 restart''') + assert res.exitCode == 0 + } + } } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index fc3a364ae24..d03e089ae8e 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -214,7 +214,7 @@ class Symfony62Tests { endpoints.size() > 0 }) - assert endpoints.size() == 13 + assert endpoints.size() == 14 assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null assert endpoints.find { it.path == '/dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /dynamic-path/{param01}' } != null assert endpoints.find { it.path == '/login' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /login' } != null @@ -244,6 +244,11 @@ class Symfony62Tests { it.operationName == 'http.request' && it.resourceName == 'GET /normalized/utf8/{föo}' } != null + assert endpoints.find { + it.path == '/normalized/ambiguous/{slug}.{format}' && it.method == 'GET' && + it.operationName == 'http.request' && + it.resourceName == 'GET /normalized/ambiguous/{slug}.{format}' + } != null } @Test @@ -359,4 +364,23 @@ class Symfony62Tests { assert span.meta.'http.route' == '/article/{slug}.{_format}' assert span.meta.'_dd.appsec.normalized_route' == '/article/{slug+_format}' } + + @Test + @Order(18) + void 'route requirements distinguish an absent defaulted mixed parameter'() { + HttpRequest req = container.buildReq('/normalized/ambiguous/foo.bar').GET().build() + Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body() == 'Ambiguous mixed route: foo.bar/html' + } + + Span span = trace.first() + assert span.meta.'http.route' == + '/normalized/ambiguous/{slug}.{format}' + // Symfony matched the entire "foo.bar" value as slug and supplied + // format from its default. URL-only inference ignores the framework + // requirements and incorrectly treats "bar" as a matched format. + assert span.meta.'_dd.appsec.normalized_route' == + '/normalized/ambiguous/{slug}' + } } diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy index 89b8618a1cf..2c4b51dc6ac 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/WordPressTests.groovy @@ -239,4 +239,93 @@ class WordPressTests { assert presentSpan.meta.'_dd.appsec.normalized_route' == '/normalized-cache-shape/{param1}' } + + @Test + @Order(8) + void 'escaped regex literals remain static route text'() { + Trace trace = CONTAINER.traceFromRequest('/normalized-literal/file.json/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == '^normalized-literal/file\\.json$' + assert span.meta.'_dd.appsec.normalized_route' == + '/normalized-literal/file.json' + } + + @Test + @Order(9) + void 'capture participation holes do not share a cached result shape'() { + Trace absentTrace = CONTAINER.traceFromRequest( + '/normalized-capture-hole/tail/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + + Span absentSpan = absentTrace.first() + assert absentSpan.meta.'http.route' == + '^normalized-capture-hole/(?:([^/]+)-)?([^/]+)$' + assert absentSpan.meta.'_dd.appsec.normalized_route' == + '/normalized-capture-hole/{param2}' + + Trace presentTrace = CONTAINER.traceFromRequest( + '/normalized-capture-hole/head-tail/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + + Span presentSpan = presentTrace.first() + assert presentSpan.meta.'http.route' == + '^normalized-capture-hole/(?:([^/]+)-)?([^/]+)$' + // Both requests have capture 2 as their highest participating index, + // but only this request includes capture 1. A highest-index cache key + // serves the absent shape cached by the preceding request. + assert presentSpan.meta.'_dd.appsec.normalized_route' == + '/normalized-capture-hole/{param1+param2}' + } + + @Test + @Order(10) + void 'named regex captures are counted and retain their framework name'() { + Trace trace = CONTAINER.traceFromRequest( + '/normalized-named-captures/first-second/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == + '^normalized-named-captures/(?P[^/]+)-' + + '(?P[^/]+)/?$' + // Both named captures share one URL segment and must be present in its + // combined element. RFC-1103 does not define whether a route accepting + // both terminal-slash forms should retain '/', so accept either form. + String normalizedRoute = span.meta.'_dd.appsec.normalized_route' + assert normalizedRoute == + '/normalized-named-captures/{first+second}' || + normalizedRoute == + '/normalized-named-captures/{first+second}/' + } + + @Test + @Order(11) + void 'normalized route is absent when API Security is disabled'() { + PhpFpm fpm = new PhpFpm(CONTAINER) + try { + fpm.restart(['DD_API_SECURITY_ENABLED': 'false']) + + Trace trace = CONTAINER.traceFromRequest('/normalized-cache/') { + HttpResponse response -> + assert response.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'http.route' == + '^normalized-cache(?:/([^/]+))?/?$' + assert span.meta.'_dd.appsec.normalized_route' == null + } finally { + fpm.restart() + } + } } diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php index d6aca885d37..f495c34ed2d 100644 --- a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php @@ -192,6 +192,19 @@ ], ], ], + 'regex_ambiguous_default' => [ + 'type' => Regex::class, + 'options' => [ + 'regex' => '/normalized-regex-ambiguous/' . + '(?P.+)(?:\.(?Ppdf|json))?', + 'spec' => '/normalized-regex-ambiguous/%name%.%ext%', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'ambiguous', + 'ext' => 'html', + ], + ], + ], 'normalized_encoded_optional' => [ 'type' => Segment::class, 'options' => [ @@ -215,6 +228,47 @@ ], ], ], + 'normalized_static_prefix_optional' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-static-prefix[/normalized]', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], + 'normalized_dynamic_prefix_optional' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-dynamic-prefix[/:value]', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + 'value' => 'normalized-dynamic-prefix', + ], + ], + ], + 'normalized_encoded_cache_optional' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-encoded-cache[/:slug]', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], + 'normalized_encoded_lowercase_optional' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/normalized-encoded-lowercase[/:slug]', + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], 'normalized_hyphenated_name' => [ 'type' => Segment::class, 'options' => [ diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php index 347a8287c68..d69186c013d 100644 --- a/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php @@ -8,6 +8,17 @@ class DynamicPathController extends AbstractActionController { + public function ambiguousAction() + { + $routeMatch = $this->getEvent()->getRouteMatch(); + $name = $routeMatch->getParam('name'); + $ext = $routeMatch->getParam('ext'); + + $response = $this->getResponse(); + $response->setContent("$name/$ext"); + return $response; + } + public function indexAction() { $response = $this->getResponse(); diff --git a/appsec/tests/integration/src/test/www/laravel8x/routes/web.php b/appsec/tests/integration/src/test/www/laravel8x/routes/web.php index 3278f2f55a7..7d601355627 100644 --- a/appsec/tests/integration/src/test/www/laravel8x/routes/web.php +++ b/appsec/tests/integration/src/test/www/laravel8x/routes/web.php @@ -29,3 +29,9 @@ Route::get('/normalized-default/{format?}', function ($format = null) { return response($format); })->defaults('format', 'html'); + +Route::get('/normalized-ambiguous/{name}.{ext?}', function ($name, $ext = null) { + return response($name . '/' . ($ext ?? 'absent')); +})->where('name', '.+') + ->where('ext', 'pdf|json') + ->defaults('ext', 'html'); diff --git a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php index 94d8189ea4e..dd1da99a1c5 100644 --- a/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php +++ b/appsec/tests/integration/src/test/www/symfony62/src/Controller/HomeController.php @@ -83,4 +83,19 @@ public function normalizedUtf8OptionalAction(Request $request) { return new Response('UTF-8 parameter route'); } + + #[Route( + "/normalized/ambiguous/{slug}.{format}", + name: "normalized_ambiguous_mixed", + defaults: ["format" => "html"], + requirements: ["slug" => ".+", "format" => "html|json"] + )] + public function normalizedAmbiguousMixedAction( + Request $request, + string $slug, + string $format + ) + { + return new Response("Ambiguous mixed route: $slug/$format"); + } } diff --git a/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php b/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php index f315c9e0ffe..226cdb6dae4 100644 --- a/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php +++ b/appsec/tests/integration/src/test/www/wordpress/public/wp-content/mu-plugins/normalized-route-test.php @@ -15,6 +15,21 @@ 'index.php?normalized_route_test=1&normalized_value=$matches[1]', 'top' ); + add_rewrite_rule( + '^normalized-literal/file\.json$', + 'index.php?normalized_route_test=1&normalized_value=literal', + 'top' + ); + add_rewrite_rule( + '^normalized-capture-hole/(?:([^/]+)-)?([^/]+)$', + 'index.php?normalized_route_test=1&normalized_value=$matches[2]', + 'top' + ); + add_rewrite_rule( + '^normalized-named-captures/(?P[^/]+)-(?P[^/]+)/?$', + 'index.php?normalized_route_test=1&normalized_value=$matches[1]-$matches[2]', + 'top' + ); }); add_filter('query_vars', static function (array $queryVars): array { From 2a8344061861d7c4557f68e9ea265c8c8a691f5a Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 1 Sep 2026 12:07:33 +0200 Subject: [PATCH 42/43] Fix tests --- .../Laminas/LaminasIntegration.php | 3 +- .../Laravel/LaravelIntegration.php | 3 +- .../Symfony/SymfonyIntegration.php | 3 +- .../WordPress/WordPressIntegrationLoader.php | 22 +++--- src/DDTrace/Util/RouteNormalizer.php | 69 ++++++++++++++++--- tracer/configuration.h | 1 + 6 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 1e2b55ce78f..674c8570c09 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -284,7 +284,8 @@ static function (SpanData $span) use ($controller, $action) { $httpRoute = LaminasIntegration::httpRouteTemplateFromNamedRouteStack($this, (string) $routeName); if ($httpRoute !== null && $httpRoute !== '') { $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled() + && dd_trace_env_config("DD_API_SECURITY_ENABLED")) { $allParams = method_exists($routeMatch, 'getParams') ? ($routeMatch->getParams() ?? []) : []; $urlPath = method_exists($request, 'getUri') ? $request->getUri()->getPath() : null; // Build a stable cache key that encodes only which optional diff --git a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php index e1b54b91573..d7ab3e63e90 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -142,7 +142,8 @@ static function ($This, $scope, $args, $route) { if (\method_exists($route, 'uri')) { $httpRoute = $route->uri(); $rootSpan->meta[Tag::HTTP_ROUTE] = $httpRoute; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled() + && dd_trace_env_config("DD_API_SECURITY_ENABLED")) { $allParams = \method_exists($route, 'parameters') ? ($route->parameters() ?? []) : []; if (strpos($httpRoute, '?}') !== false) { // For routes with optional params, filter out default-injected values diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 395eb6f6abe..840cd470fb3 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -456,9 +456,8 @@ static function() { if ($path !== null) { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; - $rawIni = ini_get('datadog.api_security_enabled'); if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled() - && ($rawIni === false || filter_var($rawIni, FILTER_VALIDATE_BOOLEAN) !== false)) { + && dd_trace_env_config("DD_API_SECURITY_ENABLED")) { // inferSymfonyRouteParams is lightweight (URL parsing only); compute it // first so the cache key encodes which optional params are present. // Without this, a route with optional params caches only the first diff --git a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index c6edc233e83..74948a3e16d 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -734,24 +734,24 @@ function_exists('is_404') && is_404() === false) { if (\property_exists($This, 'matched_rule')) { $matchedRule = $This->matched_rule; $rootSpan->meta[Tag::HTTP_ROUTE] = $matchedRule; - if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled()) { + if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled() + && dd_trace_env_config("DD_API_SECURITY_ENABLED")) { $urlPath = \property_exists($This, 'request') ? $This->request : null; - // Key on the count of matched capture groups, not the full URL, - // so all requests with the same optional-capture participation - // share one cache entry rather than one entry per distinct URL. - $wpMatchedGroupCount = null; + // Key on per-capture participation bits, not the full URL or the + // highest-index group, so routes with optional-group holes (e.g. + // (?:([^/]+)-)? absent vs present) get distinct cache entries. + $wpParticipation = null; if ($urlPath !== null) { if (@preg_match('#^' . $matchedRule . '#', ltrim($urlPath, '/'), $_wpc)) { - $wpMatchedGroupCount = 0; + $_bits = []; for ($_wi = 1; $_wi < count($_wpc); $_wi++) { - if (isset($_wpc[$_wi]) && $_wpc[$_wi] !== '') { - $wpMatchedGroupCount = $_wi; - } + $_bits[] = (isset($_wpc[$_wi]) && $_wpc[$_wi] !== '') ? '1' : '0'; } - unset($_wpc, $_wi); + $wpParticipation = implode('', $_bits); + unset($_wpc, $_wi, $_bits); } } - $cacheKey = $matchedRule . '#' . ($wpMatchedGroupCount ?? 'n'); + $cacheKey = $matchedRule . '#' . ($wpParticipation ?? 'n'); $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromWordPress($matchedRule, $urlPath); diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 449618b00a4..17af6e65c61 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -147,9 +147,20 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu } if (preg_match('/[()[\].*+?|^${}\\\\]/', $segment)) { + // If the segment is entirely escaped static text (e.g. file\.json), + // decode the backslash escapes and emit it as a plain static segment. + if (self::isStaticEscapedRegex($segment)) { + $decoded = preg_replace('/\\\\(.)/', '$1', $segment); + if ($decoded !== '') { + $normalizedSegments[] = self::encodeStaticSegment($decoded); + } + continue; + } + $prefixLen = strcspn($segment, '([{?*+|^$\\'); $dynamicPart = substr($segment, $prefixLen); - $groupCount = self::countCaptureGroups($dynamicPart); + $groupNames = self::extractCaptureGroupNames($dynamicPart); + $groupCount = count($groupNames); // Only emit a static prefix for purely-regex segments with no capture // groups. When captures exist the whole segment (prefix + captures) maps @@ -174,7 +185,9 @@ public static function normalizeFromWordPress(string $matchedRule, $urlPath = nu $paramIndex++; continue; } - $params[] = 'param' . $paramIndex++; + $name = $groupNames[$j] ?? null; + $params[] = $name !== null ? self::encodeParamName($name) : 'param' . $paramIndex; + $paramIndex++; } if (!empty($params)) { $normalizedSegments[] = '{' . implode('+', $params) . '}'; @@ -234,11 +247,33 @@ private static function splitRegexBySlash(string $str): array } /** - * Count capturing groups in a regex segment, ignoring character classes and non-capturing groups. + * Returns true if $s is entirely made up of backslash-escaped characters and + * plain literal text, with no real regex metacharacters (captures, classes, etc.). + */ + private static function isStaticEscapedRegex(string $s): bool + { + $len = strlen($s); + for ($i = 0; $i < $len; $i++) { + if ($s[$i] === '\\') { + if ($i + 1 >= $len) { + return false; + } + $i++; + } elseif (strpos('([{?*+|^$', $s[$i]) !== false) { + return false; + } + } + return true; + } + + /** + * Extract capture group names from a regex segment. + * Named groups ((?P...) or (?...)) return their name; unnamed groups return null. + * Non-capturing groups (?:...) and lookarounds are not included. */ - private static function countCaptureGroups(string $segment): int + private static function extractCaptureGroupNames(string $segment): array { - $count = 0; + $names = []; $len = strlen($segment); $inClass = false; @@ -255,13 +290,31 @@ private static function countCaptureGroups(string $segment): int } elseif ($c === ']' && $inClass) { $inClass = false; } elseif ($c === '(' && !$inClass) { - if ($i + 1 >= $len || $segment[$i + 1] !== '?') { - $count++; + if ($i + 1 < $len && $segment[$i + 1] === '?') { + // Named group (?P...) — Python/PCRE syntax + if ($i + 3 < $len && $segment[$i + 2] === 'P' && $segment[$i + 3] === '<') { + $closePos = strpos($segment, '>', $i + 4); + $names[] = $closePos !== false + ? substr($segment, $i + 4, $closePos - ($i + 4)) + : null; + // Named group (?...) but not lookbehind (?<=...) / (?', $i + 3); + $names[] = $closePos !== false + ? substr($segment, $i + 3, $closePos - ($i + 3)) + : null; + } + // (?:...), (?=...), etc. — not a capturing group, skip + } else { + $names[] = null; } } } - return $count; + return $names; } /** diff --git a/tracer/configuration.h b/tracer/configuration.h index c6fb9dfe3fc..ba4799faf54 100644 --- a/tracer/configuration.h +++ b/tracer/configuration.h @@ -164,6 +164,7 @@ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, "false") \ CONFIG(BOOL, DD_TRACE_STATS_COMPUTATION_ENABLED, "false") \ CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED, "false") \ + CONFIG(BOOL, DD_API_SECURITY_ENABLED, "true", .ini_change = zai_config_system_ini_change) \ DD_INTEGRATIONS #ifndef _WIN32 From 43d30cf3eaa220e0a4bb7974ed3c2171e2432535 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 1 Sep 2026 12:49:50 +0200 Subject: [PATCH 43/43] Fix route normalizer: position-aware optional detection, lowercase encoding, regex constraints - expandBracketOptionals: use strpos > 0 (not != false) so optional sections whose text coincidentally appears in the mandatory route prefix are not falsely detected as present; add case-insensitive fallback for percent-encoded param values so %c3%a9 (lowercase hex from browsers) matches %C3%A9 (rawurlencode output) - normalizeFromLaminas: accept pre-computed $urlMatchedParams to skip inferSymfonyRouteParams when the caller has better information - LaminasIntegration cache key: use '/'+value prefix check at position > 0 so params whose default value equals the mandatory route text are not treated as present; include static-only optional sections ([/draft]) in the key so absent and present shapes get distinct entries; for Regex routes, extract the actual route regex via reflection and run it against the URL to get accurate named captures, avoiding inferSymfonyRouteParams which ignores Laminas constraints - SymfonyIntegration: use $route->compile()->getRegex() when available to determine URL-matched params instead of generic URL inference, so routes with requirements (e.g. format=html|json with a default) correctly exclude defaulted params from the normalized route Co-Authored-By: Claude Sonnet 4.6 --- .../Laminas/LaminasIntegration.php | 80 ++++++++++++++++--- .../Symfony/SymfonyIntegration.php | 36 +++++++-- src/DDTrace/Util/RouteNormalizer.php | 27 ++++--- 3 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 674c8570c09..f87728ded4f 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -292,39 +292,70 @@ static function (SpanData $span) use ($controller, $action) { // components participated, not the raw URL (which would cause // one cache entry per distinct request value — 500-entry churn). // - // Regex routes (%param% spec): infer which params appear in the - // URL and encode their sorted names as a presence key. + // Regex routes (%param% spec): use the route's actual regex to + // determine which named captures matched the URL. // Bracket routes ([/:param]): collect optional colon-params whose - // values appear in the URL as a presence key. + // values appear in the URL as a presence key; also include + // static-only optional sections. // Fully-required routes: template alone is sufficient. + $urlMatchedFromRegex = null; if ($urlPath !== null && strpos($httpRoute, '%') !== false) { - // Regex route: convert spec to brace form and infer presence - $braceTemp = preg_replace('/%([a-zA-Z_][a-zA-Z0-9_]*)%/', '{$1}', $httpRoute); - $urlMatchedKeys = array_keys( - \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($braceTemp, $urlPath) + // Regex route: use actual route regex for accurate presence + $_leafRoute = self::getLeafRouteFromNamedRouteStack($this, (string) $routeName); + if ($_leafRoute instanceof \Laminas\Router\Http\Regex) { + $_rp = new ReflectionProperty($_leafRoute, 'regex'); + $_rp->setAccessible(true); + $_routeRegex = $_rp->getValue($_leafRoute); + if ($_routeRegex !== null && + @preg_match('(^' . $_routeRegex . '$)', $urlPath, $_rxm) === 1) { + $urlMatchedFromRegex = []; + foreach ($_rxm as $_k => $_v) { + if (!is_string($_k) || $_v === '') { + continue; + } + $urlMatchedFromRegex[$_k] = rawurldecode($_v); + } + } + unset($_rp, $_routeRegex, $_rxm, $_k, $_v); + } + unset($_leafRoute); + $_braceTemp = preg_replace('/%([a-zA-Z_][a-zA-Z0-9_]*)%/', '{$1}', $httpRoute); + $_urlMatchedKeys = array_keys( + $urlMatchedFromRegex ?? \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($_braceTemp, $urlPath) ); - sort($urlMatchedKeys); - $cacheKey = $httpRoute . '#' . implode(',', $urlMatchedKeys); + sort($_urlMatchedKeys); + $cacheKey = $httpRoute . '#' . implode(',', $_urlMatchedKeys); + unset($_braceTemp, $_urlMatchedKeys); } elseif (strpos($httpRoute, '[') !== false) { // Bracket-optional route: encode which optional colon-params - // have values that appear in the URL path. + // have values that appear in the URL path (position > 0 to + // avoid matching the mandatory route prefix with a default). + // Also include static-only optional sections ([/draft] etc.) + // so absent/present shapes get distinct cache keys. preg_match_all('/:([a-zA-Z_][a-zA-Z0-9_-]*)/', $httpRoute, $_pm); $_present = []; foreach ($_pm[1] as $_p) { if (isset($allParams[$_p]) && $urlPath !== null && - strpos($urlPath, (string)$allParams[$_p]) !== false) { + strpos($urlPath, '/' . (string)$allParams[$_p]) > 0) { $_present[] = $_p; } } + preg_match_all('/\[([^\[\]]*)\]/', $httpRoute, $_sm); + foreach ($_sm[1] as $_s) { + if ($urlPath !== null && !preg_match('/:/', $_s) && + strpos($urlPath, $_s) > 0) { + $_present[] = 'static:' . $_s; + } + } sort($_present); $cacheKey = $httpRoute . '#' . implode(',', $_present); - unset($_pm, $_present, $_p); + unset($_pm, $_sm, $_present, $_p, $_s); } else { $cacheKey = $httpRoute; } $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { - $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath); + $normalizedRoute = \DDTrace\Util\RouteNormalizer::normalizeFromLaminas($httpRoute, $allParams, $urlPath, $urlMatchedFromRegex); if ($normalizedRoute !== null) { \DDTrace\routing_cache_set($cacheKey, $normalizedRoute); } @@ -1206,6 +1237,29 @@ private static function walkRouteStackCollectEndpointRows( } } + private static function getLeafRouteFromNamedRouteStack($stack, string $matchedName) + { + $segments = \explode('/', $matchedName, 2); + $route = self::laminasGetNamedRouteFromStack($stack, $segments[0]); + if ($route === null) { + return null; + } + $hasChild = isset($segments[1]); + if ($route instanceof \Laminas\Router\Http\Part) { + if (!$hasChild) { + $rp = new ReflectionProperty($route, 'route'); + $rp->setAccessible(true); + return $rp->getValue($route); + } + self::laminasMaterializePartChildRoutes($route); + return self::getLeafRouteFromNamedRouteStack($route, $segments[1]); + } + if ($hasChild) { + return null; + } + return $route; + } + public static function httpRouteTemplateFromNamedRouteStack($stack, string $matchedName): ?string { $segments = \explode('/', $matchedName, 2); diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 840cd470fb3..d90cb674ee8 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -458,11 +458,37 @@ static function() { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; if (function_exists('\datadog\appsec\is_enabled') && \datadog\appsec\is_enabled() && dd_trace_env_config("DD_API_SECURITY_ENABLED")) { - // inferSymfonyRouteParams is lightweight (URL parsing only); compute it - // first so the cache key encodes which optional params are present. - // Without this, a route with optional params caches only the first - // materialization and serves it for all subsequent URL patterns. - $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); + // Use the compiled route regex for accurate param presence detection. + // Generic URL inference (inferSymfonyRouteParams) ignores route + // requirements and can misidentify defaulted params as URL-matched + // (e.g. {slug}.{format} with format=html|json requirement and URL + // "foo.bar" — generic inference treats "bar" as format). + // Fall back to generic inference when the route is unavailable. + $matchedParams = null; + if ($container->has('router')) { + $_r = $container->get('router'); + if (method_exists($_r, 'getRouteCollection')) { + $_route = $_r->getRouteCollection()->get($route_name); + if ($_route !== null && method_exists($_route, 'compile')) { + $_compiled = $_route->compile(); + if (method_exists($_compiled, 'getRegex')) { + $_regex = $_compiled->getRegex(); + if (@preg_match($_regex, $request->getPathInfo(), $_rxm) === 1) { + $matchedParams = []; + foreach ($_rxm as $_k => $_v) { + if (is_string($_k) && $_v !== '') { + $matchedParams[$_k] = $_v; + } + } + } + } + } + } + unset($_r, $_route, $_compiled, $_regex, $_rxm, $_k, $_v); + } + if ($matchedParams === null) { + $matchedParams = \DDTrace\Util\RouteNormalizer::inferSymfonyRouteParams($path, $request->getPathInfo()); + } $cacheKey = $route_name . '|' . implode(',', array_keys($matchedParams)); $normalizedRoute = \DDTrace\routing_cache_get($cacheKey); if ($normalizedRoute === false) { diff --git a/src/DDTrace/Util/RouteNormalizer.php b/src/DDTrace/Util/RouteNormalizer.php index 17af6e65c61..989b937e5cd 100644 --- a/src/DDTrace/Util/RouteNormalizer.php +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -57,7 +57,7 @@ static function ($m) use ($matchedParams) { * VersionListener sets :version even without a /v1/ prefix) * @return string|null */ - public static function normalizeFromLaminas(string $template, array $matchedParams = [], $urlPath = null) + public static function normalizeFromLaminas(string $template, array $matchedParams = [], $urlPath = null, $urlMatchedParams = null) { $expanded = self::expandBracketOptionals($template, $matchedParams, ':', $urlPath); @@ -77,15 +77,15 @@ public static function normalizeFromLaminas(string $template, array $matchedPara // optional captures absent from the URL (e.g. format='html' when no .html in path). // Use the URL path to determine which params were actually URL-matched. if ($hasPercentParams && $urlPath !== null) { - $urlMatchedParams = self::inferSymfonyRouteParams($braceFormat, $urlPath); + $effectiveUrlMatchedParams = $urlMatchedParams ?? self::inferSymfonyRouteParams($braceFormat, $urlPath); $braceFormat = preg_replace_callback( '/\{([^}?:]+)\}/', - static function ($m) use ($urlMatchedParams) { - return array_key_exists($m[1], $urlMatchedParams) ? $m[0] : '{' . $m[1] . '?}'; + static function ($m) use ($effectiveUrlMatchedParams) { + return array_key_exists($m[1], $effectiveUrlMatchedParams) ? $m[0] : '{' . $m[1] . '?}'; }, $braceFormat ); - return self::normalizeBraceRoute($braceFormat, $urlMatchedParams); + return self::normalizeBraceRoute($braceFormat, $effectiveUrlMatchedParams); } return self::normalizeBraceRoute($braceFormat, $matchedParams); @@ -447,9 +447,11 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { if (empty($innerParams)) { // Static-only optional section (e.g. [/draft]): - // only expand when the literal text appears in the URL. + // only expand when the literal text appears in the URL + // at a position > 0 (never at the very start, since optional + // sections always follow mandatory route text). if ($urlPath !== null) { - return (strpos($urlPath, $inner) !== false) ? $inner : ''; + return (strpos($urlPath, $inner) > 0) ? $inner : ''; } return $inner; } @@ -466,6 +468,9 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { // multi-param sections like [/:year/:month] are found correctly. // Use a word-boundary-aware replacement so :id is not replaced // inside :id2 (str_replace(':id', ...) would corrupt ':id2'). + // Check position > 0: optional sections always follow mandatory + // route text so a match at position 0 is a false positive (e.g. + // the default value is identical to the mandatory route prefix). $innerWithValues = $inner; foreach ($innerParams as $param) { $value = (string)$matchedParams[$param]; @@ -475,10 +480,11 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { $innerWithValues ); } - if (strpos($urlPath, $innerWithValues) !== false) { + if (strpos($urlPath, $innerWithValues) > 0) { return $inner; } - // Try percent-encoded values (Laminas URL-decodes param values) + // Try percent-encoded values (Laminas URL-decodes param values). + // Also try lowercase hex since browsers may send %c3%a9 for %C3%A9. $innerEncoded = $inner; foreach ($innerParams as $param) { $value = rawurlencode((string)$matchedParams[$param]); @@ -488,7 +494,8 @@ function ($m) use ($matchedParams, $paramPrefix, $urlPath) { $innerEncoded ); } - if (strpos($urlPath, $innerEncoded) !== false) { + if (strpos($urlPath, $innerEncoded) > 0 || + strpos(strtolower($urlPath), strtolower($innerEncoded)) > 0) { return $inner; } return '';