sendRequest method

Future<ApiResponse> sendRequest({
  1. required String apiKey,
  2. required Map<String, dynamic> requestBody,
})

Send API request using Firebase configuration

Implementation

Future<ApiResponse> sendRequest({
  required String apiKey,
  required Map<String, dynamic> requestBody,
}) async {
  final startTime = DateTime.now();
  Trace? trace;

  try {
    // Get API definition from cache
    final apiDefinition = _firebaseService.getApiDefinition(apiKey);
    if (apiDefinition == null) {
      return ApiResponse.error('API definition not found for key: $apiKey');
    }

    // Get base URL from cache
    final baseUrl = _firebaseService.baseUrl;
    if (baseUrl == null) {
      return ApiResponse.error('Base URL not configured');
    }

    // Construct final URL (service prefix already included in endpoint if needed)
    final finalUrl = '$baseUrl${apiDefinition.endpoint}';

    // Prepare headers with defaults
    final headers = <String, String>{};

    // Add default headers first
    headers['Content-Type'] = 'application/json';
    headers['Accept'] = 'application/json';

    // Add bearer token if set
    final bearerToken = _firebaseService.bearerToken;
    if (bearerToken != null && bearerToken.isNotEmpty) {
      headers['Authorization'] = 'Bearer $bearerToken';
    }

    // Override with API definition headers if provided
    if (apiDefinition.headers.isNotEmpty) {
      headers.addAll(apiDefinition.headers.cast<String, String>());
    }

    // Create Firebase Performance trace for this network call
    final performance = FirebasePerformance.instance;
    trace = performance.newTrace('api_request_$apiKey');
    await trace.start();

    // Set custom attributes for tracking (without exposing sensitive URLs)
    trace.putAttribute('api_key', apiKey);
    trace.putAttribute('request_type', apiDefinition.requestType.toUpperCase());
    // Note: endpoint, service, and URL are not logged for security

    // Making request...

    // Make HTTP request
    http.StreamedResponse streamed;
    final requestType = apiDefinition.requestType.toUpperCase();

    switch (requestType) {
      case 'GET':
      case 'DELETE':
        // For GET/DELETE: Merge default params with requestBody for query parameters
        final queryParams = Map<String, dynamic>.from(apiDefinition.params);
        queryParams.addAll(requestBody);

        final uri = Uri.parse(finalUrl).replace(queryParameters: queryParams.map((k, v) => MapEntry(k, '$v')));
        final req = http.Request(requestType, uri)..headers.addAll(headers.cast<String, String>());
        streamed = await _client.send(req);

        // Calculate request payload size for query params
        final requestPayloadSize = queryParams.isNotEmpty ? jsonEncode(queryParams).length : 0;
        trace.setMetric('request_payload_size', requestPayloadSize);
        break;

      case 'POST':
      case 'PUT':
      case 'PATCH':
        // For POST/PUT/PATCH: Use requestBody directly as the body (no merging with default params)

        final uri = Uri.parse(finalUrl);
        final req = http.Request(requestType, uri)
          ..headers.addAll(headers.cast<String, String>())
          ..body = jsonEncode(requestBody);
        streamed = await _client.send(req);

        // Calculate request payload size
        final requestPayloadSize = requestBody.isNotEmpty ? jsonEncode(requestBody).length : 0;
        trace.setMetric('request_payload_size', requestPayloadSize);
        break;

      default:
        await trace.stop();
        return ApiResponse.error('Unsupported request type: ${apiDefinition.requestType}');
    }

    // Read response body
    final bodyStr = await streamed.stream.bytesToString();
    dynamic body;
    try {
      body = bodyStr.isNotEmpty ? jsonDecode(bodyStr) : null;
    } catch (_) {
      body = bodyStr; // not JSON
    }

    final duration = DateTime.now().difference(startTime);

    // Update trace with response metrics
    trace.putAttribute('status_code', streamed.statusCode.toString());
    trace.setMetric('response_payload_size', bodyStr.length);
    trace.setMetric('duration_ms', duration.inMilliseconds);

    // Stop the trace and record it
    await trace.stop();

    return ApiResponse.success(
      body,
      statusCode: streamed.statusCode,
    );
  } catch (e) {
    final duration = DateTime.now().difference(startTime);

    // Stop trace with error status
    if (trace != null) {
      try {
        trace.putAttribute('error', e.toString());
        trace.setMetric('duration_ms', duration.inMilliseconds);
        await trace.stop();
      } catch (_) {
        // Ignore trace errors
      }
    }

    // Error occurred - logging disabled for security
    return ApiResponse.error('Unexpected error: ${e.toString()}');
  }
}