# frozen_string_literal: true class Api::EventsController < ApplicationController skip_before_action :verify_authenticity_token # POST /api/:project_id/events def create project = authenticate_project! return head :not_found unless project # Parse the incoming WAF event data event_data = parse_event_data(request) # Create event asynchronously ProcessWafEventJob.perform_later( project_id: project.id, event_data: event_data, headers: extract_serializable_headers(request) ) # Always return 200 OK to avoid agent retries head :ok rescue DsnAuthenticationService::AuthenticationError => e Rails.logger.warn "DSN authentication failed: #{e.message}" head :unauthorized rescue JSON::ParserError => e Rails.logger.error "Invalid JSON in event data: #{e.message}" head :bad_request end private def authenticate_project! DsnAuthenticationService.authenticate(request, params[:project_id]) end def parse_event_data(request) # Handle different content types content_type = request.content_type || "application/json" case content_type when /application\/json/ JSON.parse(request.body.read) when /application\/x-www-form-urlencoded/ # Convert form data to JSON-like hash request.request_parameters else # Try to parse as JSON anyway JSON.parse(request.body.read) end rescue => e Rails.logger.error "Failed to parse event data: #{e.message}" {} ensure request.body.rewind if request.body.respond_to?(:rewind) end def extract_serializable_headers(request) # Only extract the headers we need for analytics, avoiding IO objects important_headers = %w[ User-Agent Content-Type Content-Length Accept X-Forwarded-For X-Real-IP X-Forwarded-Proto Authorization X-Baffle-Auth X-Sentry-Auth Referer Accept-Language Accept-Encoding ] headers = {} important_headers.each do |header| value = request.headers[header] headers[header] = value if value.present? end headers end end