flush method

Future<void> flush()

Flush pending telemetry events

Sends all queued events to the backend immediately. Call this on app background/exit to ensure events are sent.

Implementation

Future<void> flush() async {
  if (!_enabled || _eventQueue.isEmpty || _isFlushInProgress) {
    return;
  }

  _isFlushInProgress = true;

  try {
    // Take current batch
    final batch = List<TelemetryEvent>.from(_eventQueue);
    _eventQueue.clear();

    // Get telemetry endpoint based on environment
    final endpoint = _getTelemetryEndpoint();

    if (_environment == SDKEnvironment.development) {
      // Supabase: Send events ONE AT A TIME to avoid "All object keys must match" error
      // Each event can have different keys based on its properties
      int successCount = 0;
      for (final event in batch) {
        try {
          final payload = event.toSupabaseJson(
            deviceId: _deviceId ?? 'unknown',
            sdkVersion: SDKConstants.version,
            platform: SDKConstants.platform,
          );

          // Debug: Log the payload being sent
          _logger.debug('Sending telemetry event: ${event.type}');
          _logger.debug('Payload: $payload');

          final response = await HTTPService.shared.post<dynamic>(
            endpoint,
            payload,
            requiresAuth: false,
          );

          // Debug: Log the response
          _logger.debug('Response for ${event.type}: $response');

          successCount++;
        } catch (e) {
          _logger.error('Failed to send event ${event.type}: $e');
        }
      }
      _logger.debug('Flushed $successCount/${batch.length} events');
    } else {
      // Production/Staging: Group events by modality and send separate batches
      // This matches the C++ telemetry manager which groups by modality
      // V2 modalities: llm, stt, tts, model (get modality field at batch level)
      // V1 modality: system/null (SDK lifecycle, storage, device, network events)
      final v2Modalities = {'llm', 'stt', 'tts', 'model'};
      final Map<String?, List<TelemetryEvent>> byModality = {};

      // Group events by modality
      for (final event in batch) {
        final modality = event.category.value;
        // V2 modalities get their modality name, V1 events get null
        final key = v2Modalities.contains(modality) ? modality : null;
        byModality.putIfAbsent(key, () => []).add(event);
      }

      // Send batches by modality (matching C++ telemetry_manager.cpp)
      int successCount = 0;
      for (final entry in byModality.entries) {
        final modality = entry.key;
        final modalityEvents = entry.value;

        final payload = <String, dynamic>{
          'events': modalityEvents.map((e) => e.toProductionJson()).toList(),
          'device_id': _deviceId,
          'timestamp': DateTime.now().toUtc().toIso8601String(),
        };

        // Include modality at batch level for V2 events
        if (modality != null) {
          payload['modality'] = modality;
        }

        try {
          await HTTPService.shared.post<dynamic>(
            endpoint,
            payload,
            requiresAuth: true,
          );
          successCount += modalityEvents.length;
          _logger.debug(
              'Flushed ${modalityEvents.length} ${modality ?? "system"} events');
        } catch (e) {
          _logger.error(
              'Failed to flush ${modality ?? "system"} events: $e');
        }
      }
      _logger.debug('Flushed $successCount/${batch.length} events total');
    }
  } catch (e) {
    _logger.error('Failed to flush telemetry: $e');
    // Events are already removed from queue, so they're lost on failure
    // This is acceptable for telemetry to avoid memory buildup
  } finally {
    _isFlushInProgress = false;
  }
}