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 \ 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/DDTrace/Integrations/CakePHP/CakePHPIntegration.php b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php index 9adf7691128..9e9f171ba51 100644 --- a/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php +++ b/src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php @@ -71,7 +71,19 @@ 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; + $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 15c8f684f67..aecd169bbd6 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) { + $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,6 +244,7 @@ private static function setHttpRoute($router, $rootSpan) { if (isset($router->routes[$uri])) { $rootSpan->meta[Tag::HTTP_ROUTE] = $uri; + self::setNormalizedRoute($rootSpan, $uri); return; } @@ -244,6 +259,7 @@ private static function setHttpRoute($router, $rootSpan) { if (preg_match('#^'.$key.'$#', $uri)) { $rootSpan->meta[Tag::HTTP_ROUTE] = $origKey; + self::setNormalizedRoute($rootSpan, $origKey); return; } } @@ -251,5 +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; + self::setNormalizedRoute($rootSpan, $uri); } } diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 0d55f3d22a9..b40817338c7 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -281,9 +281,24 @@ 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; + $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 0f00a02b825..9c2fcea9077 100644 --- a/src/DDTrace/Integrations/Laravel/LaravelIntegration.php +++ b/src/DDTrace/Integrations/Laravel/LaravelIntegration.php @@ -140,7 +140,19 @@ 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; + $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 (\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..81731c2a4aa 100644 --- a/src/DDTrace/Integrations/Slim/SlimIntegration.php +++ b/src/DDTrace/Integrations/Slim/SlimIntegration.php @@ -75,11 +75,22 @@ 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\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 (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 +103,18 @@ 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; + $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; + } } ); } @@ -131,10 +153,42 @@ 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 !== '') { + $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) + $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 d0540860fa5..a2c73516f23 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -442,20 +442,35 @@ 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); } } 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; + } } }; } else { @@ -770,4 +785,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/Integrations/WordPress/WordPressIntegrationLoader.php b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php index 4a9cd2a5178..a7ebcce9606 100644 --- a/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php +++ b/src/DDTrace/Integrations/WordPress/WordPressIntegrationLoader.php @@ -732,7 +732,19 @@ 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; + $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; + } } } }); diff --git a/src/DDTrace/Integrations/Yii/YiiIntegration.php b/src/DDTrace/Integrations/Yii/YiiIntegration.php index 91fa862d5f9..32269f49285 100644 --- a/src/DDTrace/Integrations/Yii/YiiIntegration.php +++ b/src/DDTrace/Integrations/Yii/YiiIntegration.php @@ -156,6 +156,17 @@ 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 ($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 new file mode 100644 index 00000000000..76bb2e96d8b --- /dev/null +++ b/src/DDTrace/Util/RouteNormalizer.php @@ -0,0 +1,584 @@ +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 = []) + { + 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. + * + * @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, $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, []); + } + + /** + * 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. + * + * @param string $template Template from httpRouteTemplateFromMatchedRoute() + * @param array $matchedParams Matched params from $routeMatch->getParams() + * @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) + { + $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); + } + + /** + * 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). + * + * 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|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, $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, '$'); + + if (preg_match('#\\\\?/\?$#', $rule, $m)) { + $rule = substr($rule, 0, -strlen($m[0])); + } + + $rule = trim($rule, '/'); + if ($rule === '') { + return '/'; + } + + $segments = self::splitRegexBySlash($rule); + $normalizedSegments = []; + $paramIndex = 1; + + foreach ($segments as $segment) { + if ($segment === '') { + 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) . '}'; + } + } + } else { + $normalizedSegments[] = self::encodeStaticSegment($segment); + } + } + + return '/' . implode('/', $normalizedSegments); + } + + /** + * Split a regex string by '/' but not inside character classes [...]. + * Prevents [^/] from being split 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; + } + + /** + * 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. + * + * @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 + ) { + $route = trim($route); + if ($route === '' || $route === '/') { + return '/'; + } + + $trailingSlash = (strlen($route) > 1 && substr($route, -1) === '/') ? '/' : ''; + $route = rtrim($route, '/'); + + if ($route[0] !== '/') { + $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); + + $raw = ltrim($route, '/'); + $parts = explode('/', $raw); + $normalizedSegments = []; + + foreach ($parts as $segment) { + if ($segment === '') { + continue; + } + + $result = self::normalizeBraceSegment($segment, $matchedParams); + if ($result === null) { + 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) + { + preg_match_all('/\{([^}]+)\}/', $segment, $matches, PREG_SET_ORDER); + + if (empty($matches)) { + return self::encodeStaticSegment($segment); + } + + $paramNames = []; + foreach ($matches as $match) { + $raw = $match[1]; + + $isOptional = (substr($raw, -1) === '?'); + if ($isOptional) { + $raw = substr($raw, 0, -1); + } + + $colon = strpos($raw, ':'); + if ($colon !== false) { + $raw = substr($raw, 0, $colon); + } + + $name = trim($raw); + + if ($isOptional && !array_key_exists($name, $matchedParams)) { + continue; + } + + $paramNames[] = self::encodeParamName($name); + } + + if (empty($paramNames)) { + return null; + } + + if (count($paramNames) === 1) { + return '{' . $paramNames[0] . '}'; + } + + 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. + * + * 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 from incorrectly triggering + * expansion of sections absent from the URL. + */ + private static function expandBracketOptionals( + string $template, + array $matchedParams, + string $paramPrefix = ':', + $urlPath = null + ): string { + $prev = null; + while ($prev !== $template) { + $prev = $template; + $template = preg_replace_callback( + '/\[([^\[\]]*)\]/', + 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]; + + // All params in the section must be present in matched params. + foreach ($innerParams as $param) { + if (!array_key_exists($param, $matchedParams)) { + return ''; + } + } + + 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]; + $innerWithValues = preg_replace( + '/' . preg_quote($paramPrefix . $param, '/') . '/', + $value, + $innerWithValues + ); + } + if (strpos($urlPath, $innerWithValues) === false) { + return ''; + } + } + + return $inner; + }, + $template + ); + } + return $template; + } + + /** + * 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 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. + */ + private static function cakephpToBraces(string $template): string + { + $result = preg_replace('#/\*\*#', '/{catchall}', $template); + $result = preg_replace('#/\*(?!\*)#', '/{catchall}', $result); + $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 in a parameter name. + * The '+' combining marker 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/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/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/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/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/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/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/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/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/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/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/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/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_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/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_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/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/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/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/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/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/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/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/Unit/Util/Normalizer/RouteNormalizerTest.php b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php new file mode 100644 index 00000000000..575dcc4d387 --- /dev/null +++ b/tests/Unit/Util/Normalizer/RouteNormalizerTest.php @@ -0,0 +1,475 @@ +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 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}'); + $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')); + // 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() + { + $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')); + } + + 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() + { + $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 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 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')); + } + + 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 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('^/?$'); + $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() + { + // 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/api/Unit/UserAvailableConstantsTest.php b/tests/api/Unit/UserAvailableConstantsTest.php index 0df908057d9..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'], 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..7122ac864c7 --- /dev/null +++ b/tests/ext/routing_cache/cache_lru_eviction.phpt @@ -0,0 +1,26 @@ +--TEST-- +DDTrace\routing_cache evicts the oldest inserted entry when capacity (500) is exceeded +--FILE-- + +--EXPECT-- +string(6) "value0" +bool(false) +string(6) "value1" +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/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..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 @@ -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/{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 b12796c2391..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 @@ -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/{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 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.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", 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..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,6 +13,7 @@ "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 942275e9995..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,6 +17,7 @@ "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 4cea0bce0da..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,6 +13,7 @@ "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 34e87afcbd1..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,6 +13,7 @@ "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 ad6b6999981..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,6 +17,7 @@ "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 3da297ba272..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,6 +13,7 @@ "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 5fe007192cb..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,6 +13,7 @@ "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 323e0bbfd23..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,6 +17,7 @@ "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 f74b08cfaf3..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,6 +13,7 @@ "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 b2cc305f4fe..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,6 +13,7 @@ "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 310b142ac51..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,6 +17,7 @@ "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 ca8d5fe1348..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,6 +13,7 @@ "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 ebfef74a265..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,6 +13,7 @@ "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 27319620a33..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,6 +17,7 @@ "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 683d022d4e2..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,6 +13,7 @@ "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 a2f281d70d4..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,6 +13,7 @@ "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 c982920760e..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,6 +17,7 @@ "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 671eb3f7d1a..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,6 +13,7 @@ "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 d965665100a..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,6 +13,7 @@ "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 10673208e8e..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,6 +17,7 @@ "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 e055001b0cc..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,6 +13,7 @@ "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 cfbeb419d9c..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,6 +13,7 @@ "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 76919c42279..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,6 +17,7 @@ "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 4b9c2000682..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,6 +13,7 @@ "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", diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index f48dd8af8f9..4c496b1a00a 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" @@ -244,6 +245,7 @@ void ddtrace_ginit(zend_datadog_globals *ddtrace_globals) { UNUSED(ddtrace_globals); #endif zai_hook_ginit(); + ddtrace_routing_cache_ginit(&ddtrace_globals->ddtrace.rcache); } void ddtrace_gshutdown(zend_datadog_globals *datadog_globals) { @@ -252,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(&datadog_globals->ddtrace.rcache); } 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/ddtrace_globals.h b/tracer/ddtrace_globals.h index 07e40feb1a5..0ba590531b2 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -107,6 +107,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 new file mode 100644 index 00000000000..a69f7cd1cbe --- /dev/null +++ b/tracer/routing_cache.c @@ -0,0 +1,59 @@ +#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); +} + +static void ddtrace_routing_cache_evict_oldest(void) { + HashPosition pos; + zend_string *key; + zend_ulong num_idx; + + 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_ginit(HashTable *rcache) { + zend_hash_init(rcache, DDTRACE_ROUTING_CACHE_CAPACITY, NULL, ddtrace_routing_cache_dtor, 1); +} + +void ddtrace_routing_cache_gshutdown(HashTable *rcache) { + zend_hash_destroy(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_G(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_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_G(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..37961f5ba84 --- /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_ginit(HashTable *rcache); +void ddtrace_routing_cache_gshutdown(HashTable *rcache); + +PHP_FUNCTION(DDTrace_routing_cache_get); +PHP_FUNCTION(DDTrace_routing_cache_set); + +#endif /* DDTRACE_ROUTING_CACHE_H */