Skip to content

Commit

Permalink
[OPIK-340]: project metrics; (#755)
Browse files Browse the repository at this point in the history
* [OPIK-340]: project metrics initial commit;

* [OPIK-340]: finish project metrics;

* [OPIK-340]: project metrics initial commit;

* [OPIK-340]: finish project metrics;

* [OPIK-340]: replace type with interface;

* [OPIK-340]: remove metrics from TRACE_DATA_TYPE;

* [OPIK-340]: improve code readability;

* [OPIK-340]: make a generic ChartTooltipContent component;

---------

Co-authored-by: Sasha <[email protected]>
  • Loading branch information
aadereiko and Sasha authored Nov 28, 2024
1 parent ee3cb50 commit a1623c3
Show file tree
Hide file tree
Showing 17 changed files with 764 additions and 133 deletions.
11 changes: 6 additions & 5 deletions apps/opik-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/opik-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"js-yaml": "^4.1.0",
"json-2-csv": "^5.5.6",
"lodash": "^4.17.21",
"lucide-react": "^0.395.0",
"lucide-react": "^0.461.0",
"md5": "^2.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/opik-frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export const SPANS_KEY = "spans";
export const TRACES_KEY = "traces";
export const TRACE_KEY = "trace";

// stats for feedback
export const STATS_COMET_ENDPOINT = "https://stats.comet.com/notify/event/";
export const STATS_ANONYMOUS_ID = "guest";

export type QueryConfig<TQueryFnData, TData = TQueryFnData> = Omit<
UseQueryOptions<
TQueryFnData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import axios, { AxiosError } from "axios";

import { useToast } from "@/components/ui/use-toast";
import { APP_VERSION } from "@/constants/app";
import { STATS_ANONYMOUS_ID, STATS_COMET_ENDPOINT } from "@/api/api";

type UseProvideFeedbackMutationParams = {
feedback: string;
Expand All @@ -13,7 +14,6 @@ type UseProvideFeedbackMutationParams = {
};

const EVENT_TYPE = "opik_feedback_fe";
const ANONYMOUS_ID = "guest";

const useProvideFeedbackMutation = () => {
const { toast } = useToast();
Expand All @@ -24,10 +24,8 @@ const useProvideFeedbackMutation = () => {
name,
email,
}: UseProvideFeedbackMutationParams) => {
// the app's axios instance is not used here
// as we want to avoid having credentials and a workspace in headers
return axios.post("https://stats.comet.com/notify/event/", {
anonymous_id: ANONYMOUS_ID,
return axios.post(STATS_COMET_ENDPOINT, {
anonymous_id: STATS_ANONYMOUS_ID,
event_type: EVENT_TYPE,
event_properties: {
feedback,
Expand Down
52 changes: 52 additions & 0 deletions apps/opik-frontend/src/api/feedback/useRequestChartMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useMutation } from "@tanstack/react-query";
import get from "lodash/get";

import axios, { AxiosError } from "axios";

import { useToast } from "@/components/ui/use-toast";
import { APP_VERSION } from "@/constants/app";
import { STATS_ANONYMOUS_ID, STATS_COMET_ENDPOINT } from "@/api/api";

type UseRequestChartMutationParams = {
feedback: string;
};

const EVENT_TYPE = "opik_request_chart_fe";

const useRequestChartMutation = () => {
const { toast } = useToast();

return useMutation({
mutationFn: async ({ feedback }: UseRequestChartMutationParams) => {
return axios.post(STATS_COMET_ENDPOINT, {
anonymous_id: STATS_ANONYMOUS_ID,
event_type: EVENT_TYPE,
event_properties: {
feedback,
version: APP_VERSION || null,
},
});
},
onSuccess: () => {
toast({
title: "Feedback sent",
description: "Thank you for sharing your thoughts with us",
});
},
onError: (error: AxiosError) => {
const message = get(
error,
["response", "data", "message"],
error.message,
);

toast({
title: "Error",
description: message,
variant: "destructive",
});
},
});
};

export default useRequestChartMutation;
67 changes: 67 additions & 0 deletions apps/opik-frontend/src/api/projects/useProjectMetric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { QueryFunctionContext, useQuery } from "@tanstack/react-query";
import api, { PROJECTS_REST_ENDPOINT, QueryConfig } from "@/api/api";
import { ProjectMetricTrace } from "@/types/projects";

export enum METRIC_NAME_TYPE {
FEEDBACK_SCORES = "FEEDBACK_SCORES",
TRACE_COUNT = "TRACE_COUNT",
DURATION = "DURATION",
TOKEN_USAGE = "TOKEN_USAGE",
}

export enum INTERVAL_TYPE {
HOURLY = "HOURLY",
DAILY = "DAILY",
WEEKLY = "WEEKLY",
}

type UseProjectMetricsParams = {
projectId: string;
metricName: METRIC_NAME_TYPE;
interval: INTERVAL_TYPE;
intervalStart: string;
intervalEnd: string;
};

interface ProjectMetricsResponse {
results: ProjectMetricTrace[];
}

const getProjectMetric = async (
{ signal }: QueryFunctionContext,
{
projectId,
metricName,
interval,
intervalStart,
intervalEnd,
}: UseProjectMetricsParams,
) => {
const { data } = await api.post<ProjectMetricsResponse>(
`${PROJECTS_REST_ENDPOINT}${projectId}/metrics`,
{
metric_type: metricName,
interval,
interval_start: intervalStart,
interval_end: intervalEnd,
},
{
signal,
},
);

return data?.results;
};

const useProjectMetric = (
params: UseProjectMetricsParams,
config?: QueryConfig<ProjectMetricTrace[]>,
) => {
return useQuery({
queryKey: ["projectMetrics", params],
queryFn: (context) => getProjectMetric(context, params),
...config,
});
};

export default useProjectMetric;
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import React, { useMemo, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { CartesianGrid, Line, LineChart, YAxis } from "recharts";
import isEmpty from "lodash/isEmpty";

import { Dataset } from "@/types/datasets";
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartTooltip,
} from "@/components/ui/chart";
import ExperimentChartTooltipContent from "@/components/pages/ExperimentsPage/charts/ExperimentChartTooltipContent";
import ExperimentChartLegendContent from "@/components/pages/ExperimentsPage/charts/ExperimentChartLegendContent";
import NoData from "@/components/shared/NoData/NoData";
import { TAG_VARIANTS_COLOR_MAP } from "@/components/ui/tag";
import { generateTagVariant } from "@/lib/traces";
import { useObserveResizeNode } from "@/hooks/useObserveResizeNode";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { DEFAULT_CHART_TICK } from "@/constants/chart";
import {
getDefaultChartYTickWidth,
getDefaultHashedColorsChartConfig,
} from "@/lib/charts";
import ChartTooltipContent, {
ChartTooltipRenderHeaderArguments,
} from "@/components/shared/ChartTooltipContent/ChartTooltipContent";

const MIN_LEGEND_WIDTH = 140;
const MAX_LEGEND_WIDTH = 300;
Expand Down Expand Up @@ -47,38 +51,19 @@ const ExperimentChartContainer: React.FC<ExperimentChartContainerProps> = ({
const [hiddenLines, setHiddenLines] = useState<string[]>([]);

const config = useMemo(() => {
return chartData.lines.reduce<ChartConfig>((acc, line) => {
acc[line] = {
label: line,
color: TAG_VARIANTS_COLOR_MAP[generateTagVariant(line)!],
};
return acc;
}, {});
return getDefaultHashedColorsChartConfig(chartData.lines);
}, [chartData.lines]);

const noData = useMemo(() => {
return chartData.data.every((record) => isEmpty(record.scores));
}, [chartData.data]);

const tickWidth = useMemo(() => {
const MIN_WIDTH = 26;
const MAX_WIDTH = 80;
const CHARACTER_WIDTH = 7;
const EXTRA_SPACE = 10;

const values = chartData.data.reduce<number[]>((acc, data) => {
return [
...acc,
...Object.values(data.scores).map(
(v) => Math.round(v).toString().length,
),
];
return [...acc, ...Object.values(data.scores)];
}, []);

return Math.min(
Math.max(MIN_WIDTH, Math.max(...values) * CHARACTER_WIDTH + EXTRA_SPACE),
MAX_WIDTH,
);
return getDefaultChartYTickWidth({ values });
}, [chartData.data]);

const [width, setWidth] = useState<number>(0);
Expand All @@ -95,6 +80,24 @@ const ExperimentChartContainer: React.FC<ExperimentChartContainerProps> = ({
Math.min(width * 0.3, MAX_LEGEND_WIDTH),
);

const renderHeader = useCallback(
({ payload }: ChartTooltipRenderHeaderArguments) => {
const { experimentName, createdDate } = payload[0].payload;

return (
<>
<div className="comet-body-xs-accented mb-0.5 truncate">
{experimentName}
</div>
<div className="comet-body-xs mb-1 text-light-slate">
{createdDate}
</div>
</>
);
},
[],
);

return (
<Card className={cn("min-w-[max(400px,40%)]", className)} ref={ref}>
<CardHeader>
Expand All @@ -117,17 +120,13 @@ const ExperimentChartContainer: React.FC<ExperimentChartContainerProps> = ({
width={tickWidth}
axisLine={false}
tickLine={false}
tick={{
stroke: "#94A3B8",
fontSize: 10,
fontWeight: 200,
}}
tick={DEFAULT_CHART_TICK}
interval="preserveStartEnd"
/>
<ChartTooltip
cursor={false}
isAnimationActive={false}
content={<ExperimentChartTooltipContent />}
content={<ChartTooltipContent renderHeader={renderHeader} />}
/>
<ChartLegend
verticalAlign="top"
Expand Down
Loading

0 comments on commit a1623c3

Please sign in to comment.