/ src / hooks / useTrackedMutation.ts
useTrackedMutation.ts
 1  // Copyright (c) 2026 VPL Solutions. All rights reserved.
 2  // Licensed under the MIT License. See LICENSE for details.
 3  
 4  import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
 5  import { useDiagnostics } from './useDiagnosticsHook';
 6  
 7  interface TrackingMeta {
 8    service: string;
 9    operation: string;
10  }
11  
12  /**
13   * Wraps useMutation to automatically record calls to the Diagnostics panel.
14   * Expects the response to have optional `elapsed_ms` and `request_id` fields.
15   */
16  export function useTrackedMutation<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(
17    meta: TrackingMeta,
18    options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
19  ) {
20    const { recordCall } = useDiagnostics();
21    const start = { current: 0 };
22  
23    return useMutation<TData, TError, TVariables, TOnMutateResult>({
24      ...options,
25      onMutate: (variables, context) => {
26        start.current = performance.now();
27        return options.onMutate?.(variables, context) as TOnMutateResult | Promise<TOnMutateResult>;
28      },
29      onSuccess: (data, variables, onMutateResult, context) => {
30        const elapsed = Math.round(performance.now() - start.current);
31        const resp = data as Record<string, unknown> | null;
32        recordCall({
33          service: meta.service,
34          operation: meta.operation,
35          status: 'ok',
36          latencyMs: (resp?.elapsed_ms as number) ?? elapsed,
37          requestId: resp?.request_id as string | undefined,
38        });
39        return options.onSuccess?.(data, variables, onMutateResult, context);
40      },
41      onError: (error, variables, onMutateResult, context) => {
42        const elapsed = Math.round(performance.now() - start.current);
43        recordCall({
44          service: meta.service,
45          operation: meta.operation,
46          status: 'error',
47          latencyMs: elapsed,
48        });
49        return options.onError?.(error, variables, onMutateResult, context);
50      },
51    });
52  }