From 869dba1ef42097cd60ab14bb8bc5432aeea53872 Mon Sep 17 00:00:00 2001 From: Nicolas Chaintron Date: Fri, 7 Nov 2025 12:54:58 +0100 Subject: [PATCH] refactor(analytics): replace useGetAnalytics with individual queries for decision outcomes, rule hits, and screening hits --- .../factory/create-analytics-query.ts | 45 ++++++++++ .../src/queries/analytics/get-analytics.ts | 57 ------------- .../get-decisions-outcomes-per-day.ts | 5 ++ .../queries/analytics/get-rule-hit-table.ts | 4 + .../analytics/get-rule-vs-decision-outcome.ts | 4 + .../analytics/get-screening-hits-table.ts | 4 + .../src/queries/analytics/index.ts | 5 ++ .../_analytics+/analytics.$scenarioId.tsx | 52 +++++++----- .../$scenarioId+/query.$queryName.ts | 85 +++++++++++++++++++ .../analytics+/$scenarioId+/query.ts | 68 --------------- .../app-builder/src/utils/routes/routes.ts | 6 +- .../app-builder/src/utils/routes/types.ts | 4 +- 12 files changed, 186 insertions(+), 153 deletions(-) create mode 100644 packages/app-builder/src/queries/analytics/factory/create-analytics-query.ts delete mode 100644 packages/app-builder/src/queries/analytics/get-analytics.ts create mode 100644 packages/app-builder/src/queries/analytics/get-decisions-outcomes-per-day.ts create mode 100644 packages/app-builder/src/queries/analytics/get-rule-hit-table.ts create mode 100644 packages/app-builder/src/queries/analytics/get-rule-vs-decision-outcome.ts create mode 100644 packages/app-builder/src/queries/analytics/get-screening-hits-table.ts create mode 100644 packages/app-builder/src/queries/analytics/index.ts create mode 100644 packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.$queryName.ts delete mode 100644 packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.ts diff --git a/packages/app-builder/src/queries/analytics/factory/create-analytics-query.ts b/packages/app-builder/src/queries/analytics/factory/create-analytics-query.ts new file mode 100644 index 0000000000..5c5237f6ee --- /dev/null +++ b/packages/app-builder/src/queries/analytics/factory/create-analytics-query.ts @@ -0,0 +1,45 @@ +import { useAgnosticNavigation } from '@app-builder/contexts/AgnosticNavigationContext'; +import { type AnalyticsFiltersQuery } from '@app-builder/models/analytics'; +import { getRoute } from '@app-builder/utils/routes'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; + +export function createAnalyticsQuery(queryName: string) { + return ({ scenarioId, queryString }: { scenarioId: string; queryString: string }) => { + const navigate = useAgnosticNavigation(); + const endpoint = getRoute('/ressources/analytics/:scenarioId/query/:queryName', { + scenarioId, + queryName, + }); + console.log('endpoint', endpoint); + const qs = queryString ? atob(queryString) : null; + const parsed: AnalyticsFiltersQuery = JSON.parse(qs || '{}'); + + const { range, compareRange, scenarioVersion, trigger } = parsed; + + return useQuery({ + queryKey: ['analytics', 'query', scenarioId, queryName, queryString], + enabled: Boolean(qs && range), + queryFn: async () => { + const response = await fetch(endpoint, { + method: 'POST', + body: JSON.stringify({ + scenarioVersion, + range, + compareRange, + trigger, + }), + }); + + const responseData = (await response.json()) as { redirectTo: string } | { data: TData }; + + if ('redirectTo' in responseData) { + navigate(responseData.redirectTo); + return; + } + + return responseData.data; + }, + placeholderData: keepPreviousData, + }); + }; +} diff --git a/packages/app-builder/src/queries/analytics/get-analytics.ts b/packages/app-builder/src/queries/analytics/get-analytics.ts deleted file mode 100644 index 7980819dbd..0000000000 --- a/packages/app-builder/src/queries/analytics/get-analytics.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useAgnosticNavigation } from '@app-builder/contexts/AgnosticNavigationContext'; -import { - type AnalyticsFiltersQuery, - type DecisionOutcomesPerPeriod, - type RuleVsDecisionOutcome, -} from '@app-builder/models/analytics'; -import { RuleHitTableResponse } from '@app-builder/models/analytics/rule-hit'; -import { ScreeningHitTableResponse } from '@app-builder/models/analytics/screening-hit'; -import { getRoute } from '@app-builder/utils/routes'; -import { keepPreviousData, useQuery } from '@tanstack/react-query'; - -export const useGetAnalytics = ({ scenarioId, queryString }: { scenarioId: string; queryString: string }) => { - const navigate = useAgnosticNavigation(); - const endpoint = getRoute('/ressources/analytics/:scenarioId/query', { - scenarioId, - }); - const qs = queryString ? atob(queryString) : null; - const parsed: AnalyticsFiltersQuery = JSON.parse(qs || '{}'); - - const { range, compareRange, scenarioVersion, trigger } = parsed; - - return useQuery({ - queryKey: ['analytics', 'query', scenarioId, queryString], - enabled: Boolean(qs && range), - queryFn: async () => { - const response = await fetch(endpoint, { - method: 'POST', - body: JSON.stringify({ - scenarioVersion, - range, - compareRange, - trigger, - }), - }); - - const result = await response.json(); - - if (result.redirectTo) { - navigate(result.redirectTo); - return; - } - - if (!result.success) { - throw new Error(result.errors?.join(', ') ?? 'Failed to fetch analytics'); - } - - return result.data as { - decisionOutcomesPerDay: DecisionOutcomesPerPeriod | null; - ruleHitTable: RuleHitTableResponse[] | null; - screeningHitsTable: ScreeningHitTableResponse[] | null; - // decisionsScoreDistribution: DecisionsScoreDistribution | null; - ruleVsDecisionOutcome: RuleVsDecisionOutcome[] | null; - }; - }, - placeholderData: keepPreviousData, - }); -}; diff --git a/packages/app-builder/src/queries/analytics/get-decisions-outcomes-per-day.ts b/packages/app-builder/src/queries/analytics/get-decisions-outcomes-per-day.ts new file mode 100644 index 0000000000..5bd83c4175 --- /dev/null +++ b/packages/app-builder/src/queries/analytics/get-decisions-outcomes-per-day.ts @@ -0,0 +1,5 @@ +import { DecisionOutcomesPerPeriod } from '@app-builder/models/analytics'; +import { createAnalyticsQuery } from './factory/create-analytics-query'; + +export const useGetDecisionsOutcomesPerDay = + createAnalyticsQuery('decision-outcomes-per-day'); diff --git a/packages/app-builder/src/queries/analytics/get-rule-hit-table.ts b/packages/app-builder/src/queries/analytics/get-rule-hit-table.ts new file mode 100644 index 0000000000..da0035cd1e --- /dev/null +++ b/packages/app-builder/src/queries/analytics/get-rule-hit-table.ts @@ -0,0 +1,4 @@ +import { RuleHitTableResponse } from '@app-builder/models/analytics/rule-hit'; +import { createAnalyticsQuery } from './factory/create-analytics-query'; + +export const useGetRuleHitTable = createAnalyticsQuery('rule-hit-table'); diff --git a/packages/app-builder/src/queries/analytics/get-rule-vs-decision-outcome.ts b/packages/app-builder/src/queries/analytics/get-rule-vs-decision-outcome.ts new file mode 100644 index 0000000000..8d50db9829 --- /dev/null +++ b/packages/app-builder/src/queries/analytics/get-rule-vs-decision-outcome.ts @@ -0,0 +1,4 @@ +import { RuleVsDecisionOutcome } from '@app-builder/models/analytics'; +import { createAnalyticsQuery } from './factory/create-analytics-query'; + +export const useGetRuleVsDecisionOutcome = createAnalyticsQuery('rule-vs-decision-outcome'); diff --git a/packages/app-builder/src/queries/analytics/get-screening-hits-table.ts b/packages/app-builder/src/queries/analytics/get-screening-hits-table.ts new file mode 100644 index 0000000000..c6f59b8831 --- /dev/null +++ b/packages/app-builder/src/queries/analytics/get-screening-hits-table.ts @@ -0,0 +1,4 @@ +import { ScreeningHitTableResponse } from '@app-builder/models/analytics'; +import { createAnalyticsQuery } from './factory/create-analytics-query'; + +export const useGetScreeningHitsTable = createAnalyticsQuery('screening-hits-table'); diff --git a/packages/app-builder/src/queries/analytics/index.ts b/packages/app-builder/src/queries/analytics/index.ts new file mode 100644 index 0000000000..2ad1b16af0 --- /dev/null +++ b/packages/app-builder/src/queries/analytics/index.ts @@ -0,0 +1,5 @@ +export * from './get-available-filters'; +export * from './get-decisions-outcomes-per-day'; +export * from './get-rule-hit-table'; +export * from './get-rule-vs-decision-outcome'; +export * from './get-screening-hits-table'; diff --git a/packages/app-builder/src/routes/_builder+/_analytics+/analytics.$scenarioId.tsx b/packages/app-builder/src/routes/_builder+/_analytics+/analytics.$scenarioId.tsx index 6b202e0d6c..9ebaa6d254 100644 --- a/packages/app-builder/src/routes/_builder+/_analytics+/analytics.$scenarioId.tsx +++ b/packages/app-builder/src/routes/_builder+/_analytics+/analytics.$scenarioId.tsx @@ -16,8 +16,13 @@ import { FilterSource, } from '@app-builder/models/analytics'; import { type Scenario } from '@app-builder/models/scenario'; -import { useGetAnalytics } from '@app-builder/queries/analytics/get-analytics'; -import { useGetAvailableFilters } from '@app-builder/queries/analytics/get-available-filters'; +import { + useGetAvailableFilters, + useGetDecisionsOutcomesPerDay, + useGetRuleHitTable, + useGetRuleVsDecisionOutcome, + useGetScreeningHitsTable, +} from '@app-builder/queries/analytics'; import { initServerServices } from '@app-builder/services/init.server'; import { formatDateTimeWithoutPresets, formatDuration } from '@app-builder/utils/format'; import { getRoute } from '@app-builder/utils/routes'; @@ -204,22 +209,20 @@ export default function Analytics() { return Array.from(descriptors.values()); }, [availableFilters, seenAvailableFilters]); - const { - data: { - decisionOutcomesPerDay: decisionsData, - ruleHitTable: ruleHitTableData, - screeningHitsTable: screeningHitsTableData, - // decisionsScoreDistribution: decisionsScoreDistributionData, - ruleVsDecisionOutcome: ruleVsDecisionOutcomeData, - } = { - decisionOutcomesPerDay: null, - ruleHitTable: null, - screeningHitsTable: null, - // decisionsScoreDistribution: null, - ruleVsDecisionOutcome: null, - }, - isFetching: isAnalyticsFetching, - } = useGetAnalytics({ + const { data: decisionsOutcomesPerDayData, isFetching: isDecisionsOutcomesPerDayFetching } = + useGetDecisionsOutcomesPerDay({ + scenarioId, + queryString: queryString ?? '', + }); + const { data: ruleHitTableData, isFetching: isRuleHitTableFetching } = useGetRuleHitTable({ + scenarioId, + queryString: queryString ?? '', + }); + const { data: ruleVsDecisionOutcomeData, isFetching: isRuleVsDecisionOutcomeFetching } = useGetRuleVsDecisionOutcome({ + scenarioId, + queryString: queryString ?? '', + }); + const { data: screeningHitsTableData, isFetching: isScreeningHitsTableFetching } = useGetScreeningHitsTable({ scenarioId, queryString: queryString ?? '', }); @@ -375,9 +378,9 @@ export default function Analytics() {
{/*
@@ -385,9 +388,12 @@ export default function Analytics() {
*/}
- - - + + + diff --git a/packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.$queryName.ts b/packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.$queryName.ts new file mode 100644 index 0000000000..a62d54a4df --- /dev/null +++ b/packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.$queryName.ts @@ -0,0 +1,85 @@ +import { createServerFn } from '@app-builder/core/requests'; +import { authMiddleware } from '@app-builder/middlewares/auth-middleware'; +import { handleRedirectMiddleware } from '@app-builder/middlewares/handle-redirect-middleware'; +import { analyticsQuery } from '@app-builder/models/analytics'; +import invariant from 'tiny-invariant'; +import { z } from 'zod'; + +const urlParamsSchema = z.object({ + scenarioId: z.uuidv4(), + queryName: z.string(), +}); + +export const action = createServerFn( + [handleRedirectMiddleware, authMiddleware], + async function createAnalyticsQueryAction({ params, request, context }) { + try { + const urlParams = urlParamsSchema.parse(params); + invariant(urlParams.queryName, 'queryName is required'); + console.log('urlParams', urlParams); + + const body = await request.json(); + + const queryParams = analyticsQuery.parse({ + ...body, + scenarioId: urlParams.scenarioId, + }); + + switch (urlParams.queryName) { + case 'decision-outcomes-per-day': { + const data = await context.authInfo.analytics.getDecisionOutcomesPerDay({ + ...queryParams, + scenarioId: urlParams.scenarioId, + }); + return { + success: true, + data, + }; + } + + case 'rule-hit-table': { + const data = await context.authInfo.analytics.getRuleHitTable({ + ...queryParams, + scenarioId: urlParams.scenarioId, + }); + return { + success: true, + data, + }; + } + + case 'rule-vs-decision-outcome': { + const data = await context.authInfo.analytics.getRuleVsDecisionOutcome({ + ...queryParams, + scenarioId: urlParams.scenarioId, + }); + return { + success: true, + data, + }; + } + + case 'screening-hits-table': { + const data = await context.authInfo.analytics.getScreeningHitsTable({ + ...queryParams, + scenarioId: urlParams.scenarioId, + }); + return { + success: true, + data, + }; + } + + default: { + return { + success: false, + errors: 'Invalid query name', + }; + } + } + } catch (error) { + console.error('error in analytics query', error); + return { success: false, errors: 'Internal server error' }; + } + }, +); diff --git a/packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.ts b/packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.ts deleted file mode 100644 index db8db3a247..0000000000 --- a/packages/app-builder/src/routes/ressources+/analytics+/$scenarioId+/query.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { createServerFn } from '@app-builder/core/requests'; -import { authMiddleware } from '@app-builder/middlewares/auth-middleware'; -import { handleRedirectMiddleware } from '@app-builder/middlewares/handle-redirect-middleware'; -import { analyticsQuery } from '@app-builder/models/analytics'; -import { z } from 'zod'; - -const urlParamsSchema = z.object({ - scenarioId: z.uuidv4(), -}); - -export const action = createServerFn( - [handleRedirectMiddleware, authMiddleware], - async function createAnalyticsQueryAction({ params, request, context }) { - try { - const urlParams = urlParamsSchema.parse(params); - - const body = await request.json(); - - const queryParams = analyticsQuery.parse({ - ...body, - scenarioId: urlParams.scenarioId, - }); - - const [ - decisionOutcomesPerDay, - ruleHitTable, - screeningHitsTable, - // decisionsScoreDistribution, - ruleVsDecisionOutcome, - ] = await Promise.all([ - context.authInfo.analytics.getDecisionOutcomesPerDay({ - ...queryParams, - scenarioId: urlParams.scenarioId, - }), - context.authInfo.analytics.getRuleHitTable({ - ...queryParams, - scenarioId: urlParams.scenarioId, - }), - context.authInfo.analytics.getScreeningHitsTable({ - ...queryParams, - scenarioId: urlParams.scenarioId, - }), - // context.authInfo.analytics.getDecisionsScoreDistribution({ - // ...queryParams, - // scenarioId: urlParams.scenarioId, - // }), - context.authInfo.analytics.getRuleVsDecisionOutcome({ - ...queryParams, - scenarioId: urlParams.scenarioId, - }), - ]); - - return { - success: true, - data: { - decisionOutcomesPerDay, - ruleHitTable, - screeningHitsTable, - // decisionsScoreDistribution, - ruleVsDecisionOutcome, - }, - }; - } catch (error) { - console.error('error in analytics query', error); - return { success: false, errors: 'Internal server error' }; - } - }, -); diff --git a/packages/app-builder/src/utils/routes/routes.ts b/packages/app-builder/src/utils/routes/routes.ts index ff34406cd0..53a83b0abc 100644 --- a/packages/app-builder/src/utils/routes/routes.ts +++ b/packages/app-builder/src/utils/routes/routes.ts @@ -391,9 +391,9 @@ export const routes = [ "file": "routes/ressources+/analytics+/$scenarioId+/available_filters.ts" }, { - "id": "routes/ressources+/analytics+/$scenarioId+/query", - "path": "ressources/analytics/:scenarioId/query", - "file": "routes/ressources+/analytics+/$scenarioId+/query.ts" + "id": "routes/ressources+/analytics+/$scenarioId+/query.$queryName", + "path": "ressources/analytics/:scenarioId/query/:queryName", + "file": "routes/ressources+/analytics+/$scenarioId+/query.$queryName.ts" }, { "id": "routes/ressources+/annotations+/download-file.$annotationId.$fileId", diff --git a/packages/app-builder/src/utils/routes/types.ts b/packages/app-builder/src/utils/routes/types.ts index bd7de7fd49..be7b134cdd 100644 --- a/packages/app-builder/src/utils/routes/types.ts +++ b/packages/app-builder/src/utils/routes/types.ts @@ -57,7 +57,7 @@ export type RoutePath = | '/oidc/auth' | '/oidc/callback' | '/ressources/analytics/:scenarioId/available_filters' - | '/ressources/analytics/:scenarioId/query' + | '/ressources/analytics/:scenarioId/query/:queryName' | '/ressources/annotations/download-file/:annotationId/:fileId' | '/ressources/app-config' | '/ressources/auth/logout' @@ -245,7 +245,7 @@ export type RouteID = | 'routes/oidc+/auth' | 'routes/oidc+/callback' | 'routes/ressources+/analytics+/$scenarioId+/available_filters' - | 'routes/ressources+/analytics+/$scenarioId+/query' + | 'routes/ressources+/analytics+/$scenarioId+/query.$queryName' | 'routes/ressources+/annotations+/download-file.$annotationId.$fileId' | 'routes/ressources+/app-config' | 'routes/ressources+/auth+/logout'