# frozen_string_literal: true module Analytics class PathNormalizer UUID_RE = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i NUMERIC_RE = /\A\d{6,}\z/ TOKENISH_RE = /\A[A-Za-z0-9_\-]{22,}\z/ EXCLUDED_PREFIXES = %w[/admin /regia /analytics /cable /api /webhooks /internal].freeze class << self def normalize(raw_path) path = raw_path.to_s.split("?", 2).first.to_s path = "/" if path.blank? path = "/#{path}" unless path.start_with?("/") path = path.gsub(%r{/{2,}}, "/").chomp("/") path = "/" if path.blank? parts = path.split("/").map do |segment| next segment if segment.blank? next ":id" if UUID_RE.match?(segment) next ":id" if NUMERIC_RE.match?(segment) next ":token" if TOKENISH_RE.match?(segment) && segment.match?(/[A-Z]/) segment end normalized = parts.join("/") normalized = "/" if normalized.blank? normalized end def excluded?(raw_or_normalized) path = normalize(raw_or_normalized) return true if EXCLUDED_PREFIXES.any? { |p| path == p || path.start_with?("#{p}/") } return true if path.match?(%r{\A/live/[^/]+}) return true if path.match?(%r{\A/replay/[^/]+}) false end end end end