LCOV - code coverage report
Current view: top level - src - observability.dart Coverage Total Hit
Test: lcov.info Lines: 79.2 % 53 42
Test Date: 2026-04-30 18:23:23 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:convert';
       2              : import 'dart:developer' as developer;
       3              : import 'package:flutter/foundation.dart';
       4              : import 'events.dart';
       5              : import 'platform_interface.dart';
       6              : 
       7              : /// Register the DevTools service extensions for native_workmanager.
       8              : /// This allows the DevTools Extension to request real-time task metrics,
       9              : /// queue sizes, and DAG states without needing continuous polling.
      10              : bool _devToolsExtensionsRegistered = false;
      11              : 
      12            5 : @pragma('vm:entry-point')
      13              : void registerDevToolsExtensions() {
      14              :   if (!kDebugMode && !kProfileMode) return;
      15              :   if (_devToolsExtensionsRegistered) return;
      16              :   _devToolsExtensionsRegistered = true;
      17              : 
      18            5 :   developer.registerExtension('ext.native_workmanager.getMetrics',
      19            0 :       (method, parameters) async {
      20              :     try {
      21            0 :       final metrics = await NativeWorkManagerPlatform.instance.getMetrics();
      22            0 :       return developer.ServiceExtensionResponse.result(jsonEncode(metrics));
      23              :     } catch (e) {
      24            0 :       return developer.ServiceExtensionResponse.error(
      25              :         developer.ServiceExtensionResponse.extensionError,
      26            0 :         e.toString(),
      27              :       );
      28              :     }
      29              :   });
      30              : 
      31            5 :   developer.registerExtension('ext.native_workmanager.syncQueue',
      32            0 :       (method, parameters) async {
      33              :     try {
      34              :       final success =
      35            0 :           await NativeWorkManagerPlatform.instance.syncOfflineQueue();
      36            0 :       return developer.ServiceExtensionResponse.result(
      37            0 :           jsonEncode({'success': success}));
      38              :     } catch (e) {
      39            0 :       return developer.ServiceExtensionResponse.error(
      40              :         developer.ServiceExtensionResponse.extensionError,
      41            0 :         e.toString(),
      42              :       );
      43              :     }
      44              :   });
      45              : }
      46              : 
      47              : /// Type-safe delegate for forwarding task events to third-party SDKs.
      48              : ///
      49              : /// Implement this class and pass to [NativeWorkManager.configure] via
      50              : /// [ObservabilityConfig] to integrate with any analytics, crash-reporting,
      51              : /// or logging SDK without relying on `dynamic` reflection.
      52              : ///
      53              : /// ## Example — Sentry
      54              : ///
      55              : /// ```dart
      56              : /// class SentryWorkManagerLogger implements WorkManagerLogger {
      57              : ///   @override
      58              : ///   void onTaskStart(String taskId, String workerType) {
      59              : ///     Sentry.addBreadcrumb(Breadcrumb(
      60              : ///       message: 'Background task started: $taskId ($workerType)',
      61              : ///       category: 'native_workmanager',
      62              : ///     ));
      63              : ///   }
      64              : ///
      65              : ///   @override
      66              : ///   void onTaskComplete(TaskEvent event) {
      67              : ///     Sentry.addBreadcrumb(Breadcrumb(
      68              : ///       message: 'Background task completed: ${event.taskId}',
      69              : ///       category: 'native_workmanager',
      70              : ///     ));
      71              : ///   }
      72              : ///
      73              : ///   @override
      74              : ///   void onTaskFail(TaskEvent event) {
      75              : ///     Sentry.captureMessage(
      76              : ///       'Background task failed: ${event.taskId}',
      77              : ///       hint: Hint.withMap({'message': event.message ?? ''}),
      78              : ///     );
      79              : ///   }
      80              : /// }
      81              : ///
      82              : /// // Wire up during initialization:
      83              : /// NativeWorkManager.configure(
      84              : ///   observability: ObservabilityConfig.fromLogger(SentryWorkManagerLogger()),
      85              : /// );
      86              : /// ```
      87              : ///
      88              : /// ## Example — Firebase
      89              : ///
      90              : /// ```dart
      91              : /// class FirebaseWorkManagerLogger implements WorkManagerLogger {
      92              : ///   @override
      93              : ///   void onTaskStart(String taskId, String workerType) {
      94              : ///     FirebaseAnalytics.instance.logEvent(
      95              : ///       name: 'bg_task_start',
      96              : ///       parameters: {'task_id': taskId, 'worker': workerType},
      97              : ///     );
      98              : ///   }
      99              : ///
     100              : ///   @override
     101              : ///   void onTaskComplete(TaskEvent event) {
     102              : ///     FirebaseAnalytics.instance.logEvent(
     103              : ///       name: 'bg_task_success',
     104              : ///       parameters: {'task_id': event.taskId},
     105              : ///     );
     106              : ///   }
     107              : ///
     108              : ///   @override
     109              : ///   void onTaskFail(TaskEvent event) {
     110              : ///     FirebaseCrashlytics.instance.recordError(
     111              : ///       event.message ?? 'Unknown error',
     112              : ///       null,
     113              : ///       reason: 'Background task failed: ${event.taskId}',
     114              : ///     );
     115              : ///   }
     116              : /// }
     117              : /// ```
     118              : abstract class WorkManagerLogger {
     119              :   /// Called when a background task begins execution.
     120              :   void onTaskStart(String taskId, String workerType);
     121              : 
     122              :   /// Called when a background task completes successfully.
     123              :   void onTaskComplete(TaskEvent event);
     124              : 
     125              :   /// Called when a background task fails.
     126              :   void onTaskFail(TaskEvent event);
     127              : }
     128              : 
     129              : /// Configuration for built-in observability hooks.
     130              : ///
     131              : /// Pass to [NativeWorkManager.configure] to receive callbacks whenever
     132              : /// a background task starts, completes, or fails. Useful for analytics,
     133              : /// performance monitoring, and crash reporting — without having to manually
     134              : /// subscribe to the events/progress streams everywhere in your app.
     135              : ///
     136              : /// ## Setup
     137              : ///
     138              : /// ```dart
     139              : /// void main() async {
     140              : ///   WidgetsFlutterBinding.ensureInitialized();
     141              : ///   await NativeWorkManager.initialize();
     142              : ///
     143              : ///   NativeWorkManager.configure(
     144              : ///     observability: ObservabilityConfig(
     145              : ///       onTaskStart: (taskId, workerType) {
     146              : ///         analytics.track('bg_task_start', {'worker': workerType});
     147              : ///       },
     148              : ///       onTaskComplete: (event) {
     149              : ///         performance.record('task_duration', {
     150              : ///           'taskId': event.taskId,
     151              : ///           'elapsed': event.timestamp.difference(startTimes[event.taskId]!),
     152              : ///         });
     153              : ///       },
     154              : ///       onTaskFail: (event) {
     155              : ///         crashlytics.log('Background task failed: ${event.taskId}');
     156              : ///         if (event.message != null) {
     157              : ///           crashlytics.recordError(event.message!, null);
     158              : ///         }
     159              : ///       },
     160              : ///     ),
     161              : ///   );
     162              : ///
     163              : ///   runApp(MyApp());
     164              : /// }
     165              : /// ```
     166              : ///
     167              : /// ## Callback Guarantees
     168              : ///
     169              : /// - All callbacks are invoked on the **main thread** (same as the events/progress streams).
     170              : /// - Callbacks are **fire-and-forget** — exceptions inside them are caught and logged
     171              : ///   to avoid disrupting the events stream.
     172              : /// - `onTaskStart` fires when the native worker **actually begins execution**, driven by
     173              : ///   a dedicated lifecycle event from the native side. It fires for **all** tasks —
     174              : ///   including fast workers that never emit a progress update.
     175              : /// - `onTaskComplete` / `onTaskFail` are mutually exclusive for a given task.
     176              : @immutable
     177              : class ObservabilityConfig {
     178            2 :   const ObservabilityConfig({
     179              :     this.onTaskStart,
     180              :     this.onTaskComplete,
     181              :     this.onTaskFail,
     182              :     this.onProgress,
     183              :   });
     184              : 
     185              :   /// Create an [ObservabilityConfig] from a [WorkManagerLogger] implementation.
     186              :   ///
     187              :   /// Prefer this over the deprecated `useSentry()` / `useFirebase()` stubs.
     188              :   /// Implement [WorkManagerLogger] with your SDK's actual API calls, then pass
     189              :   /// it here — fully type-safe, no `dynamic` reflection involved.
     190              :   ///
     191              :   /// ```dart
     192              :   /// NativeWorkManager.configure(
     193              :   ///   observability: ObservabilityConfig.fromLogger(MyLogger()),
     194              :   /// );
     195              :   /// ```
     196            1 :   factory ObservabilityConfig.fromLogger(WorkManagerLogger logger) {
     197            1 :     return ObservabilityConfig(
     198            1 :       onTaskStart: (taskId, workerType) =>
     199            1 :           logger.onTaskStart(taskId, workerType),
     200            2 :       onTaskComplete: (event) => logger.onTaskComplete(event),
     201            2 :       onTaskFail: (event) => logger.onTaskFail(event),
     202              :     );
     203              :   }
     204              : 
     205              :   /// Called when the native worker begins execution.
     206              :   ///
     207              :   /// The `workerType` parameter is the worker class name (e.g. `'HttpDownloadWorker'`,
     208              :   /// `'HttpUploadWorker'`, `'DartCallbackWorker'`), or an empty string if
     209              :   /// the native side does not report a type.
     210              :   ///
     211              :   /// This callback fires reliably for **all** tasks — including fast workers
     212              :   /// that never emit progress. It is driven by a native "started" lifecycle
     213              :   /// event emitted when the worker actually begins execution, not by the first
     214              :   /// progress update.
     215              :   final void Function(String taskId, String workerType)? onTaskStart;
     216              : 
     217              :   /// Called when a task completes successfully.
     218              :   final void Function(TaskEvent event)? onTaskComplete;
     219              : 
     220              :   /// Called when a task fails.
     221              :   final void Function(TaskEvent event)? onTaskFail;
     222              : 
     223              :   /// Called on every [TaskProgress] update for any task.
     224              :   ///
     225              :   /// Use for a global progress dashboard or logging. For task-specific
     226              :   /// progress, filter by `progress.taskId`.
     227              :   final void Function(TaskProgress progress)? onProgress;
     228              : }
     229              : 
     230              : /// Internal dispatcher that routes events/progress to [ObservabilityConfig].
     231              : ///
     232              : /// Not part of the public API — use [NativeWorkManager.configure] instead.
     233              : class ObservabilityDispatcher {
     234            2 :   ObservabilityDispatcher(this._config);
     235              : 
     236              :   final ObservabilityConfig _config;
     237              : 
     238              :   /// Called by [NativeWorkManager] internals when a progress update arrives.
     239            1 :   void dispatchProgress(TaskProgress progress) {
     240              :     // Phase 2: Post real-time event to DevTools for streaming observability.
     241              :     // This allows the DevTools extension to update progress bars without polling.
     242              :     if (kDebugMode || kProfileMode) {
     243            2 :       developer.postEvent('native_workmanager.progress', {
     244            1 :         'taskId': progress.taskId,
     245            1 :         'progress': progress.progress,
     246            1 :         'message': progress.message,
     247            1 :         'speed': progress.networkSpeed,
     248            1 :         'timeRemaining': progress.timeRemaining?.inSeconds,
     249              :       });
     250              :     }
     251              : 
     252            2 :     if (_config.onProgress != null) {
     253            5 :       _safeCall(() => _config.onProgress!(progress), 'onProgress');
     254              :     }
     255              :   }
     256              : 
     257              :   /// Called by [NativeWorkManager] internals when a task event arrives.
     258              :   ///
     259              :   /// Handles both lifecycle events ([TaskEvent.isStarted]) and completion
     260              :   /// events (success / failure).
     261            1 :   void dispatchEvent(TaskEvent event) {
     262              :     // Phase 2: Post real-time status change to DevTools.
     263              :     if (kDebugMode || kProfileMode) {
     264            2 :       developer.postEvent('native_workmanager.event', {
     265            1 :         'taskId': event.taskId,
     266            1 :         'success': event.success,
     267            1 :         'isStarted': event.isStarted,
     268            1 :         'workerType': event.workerType,
     269            1 :         'message': event.message,
     270            2 :         'timestamp': event.timestamp.toIso8601String(),
     271            1 :         'resultData': event.resultData,
     272              :       });
     273              :     }
     274              : 
     275            1 :     if (event.isStarted) {
     276            2 :       if (_config.onTaskStart != null) {
     277            1 :         _safeCall(
     278            6 :           () => _config.onTaskStart!(event.taskId, event.workerType ?? ''),
     279              :           'onTaskStart',
     280              :         );
     281              :       }
     282              :       return;
     283              :     }
     284              : 
     285            1 :     if (event.success) {
     286            2 :       if (_config.onTaskComplete != null) {
     287            5 :         _safeCall(() => _config.onTaskComplete!(event), 'onTaskComplete');
     288              :       }
     289              :     } else {
     290            2 :       if (_config.onTaskFail != null) {
     291            5 :         _safeCall(() => _config.onTaskFail!(event), 'onTaskFail');
     292              :       }
     293              :     }
     294              :   }
     295              : 
     296            1 :   static void _safeCall(void Function() fn, String callbackName) {
     297              :     try {
     298            1 :       fn();
     299              :     } catch (e, stack) {
     300              :       // Swallow exceptions so a buggy callback can't break the events stream.
     301            2 :       debugPrint(
     302            1 :         '[native_workmanager] ObservabilityConfig.$callbackName threw: $e\n$stack',
     303              :       );
     304              :     }
     305              :   }
     306              : }
        

Generated by: LCOV version 2.4-0