diff --git a/python/src/aiconfig/editor/client/package.json b/python/src/aiconfig/editor/client/package.json index 9408bf160..ef9aa9696 100644 --- a/python/src/aiconfig/editor/client/package.json +++ b/python/src/aiconfig/editor/client/package.json @@ -23,6 +23,7 @@ ] }, "dependencies": { + "@datadog/browser-logs": "^5.7.0", "@emotion/react": "^11.11.1", "@mantine/carousel": "^6.0.7", "@mantine/core": "^6.0.7", @@ -63,4 +64,4 @@ "eslint-plugin-react-hooks": "^4.6.0", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/python/src/aiconfig/editor/client/src/LocalEditor.tsx b/python/src/aiconfig/editor/client/src/LocalEditor.tsx index e967ac34a..10fa22f98 100644 --- a/python/src/aiconfig/editor/client/src/LocalEditor.tsx +++ b/python/src/aiconfig/editor/client/src/LocalEditor.tsx @@ -5,17 +5,13 @@ import AIConfigEditor, { RunPromptStreamErrorEvent, } from "./components/AIConfigEditor"; import { Flex, Loader, MantineProvider, Image } from "@mantine/core"; -import { - AIConfig, - InferenceSettings, - JSONObject, - Output, - Prompt, -} from "aiconfig"; +import { AIConfig, InferenceSettings, JSONObject, Output, Prompt } from "aiconfig"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ufetch } from "ufetch"; import { ROUTE_TABLE } from "./utils/api"; import { streamingApiChain } from "./utils/oboeHelpers"; +import { datadogLogs } from "@datadog/browser-logs"; +import { LogEvent, LogEventData } from "./shared/types"; export default function Editor() { const [aiconfig, setAiConfig] = useState(); @@ -29,6 +25,34 @@ export default function Editor() { loadConfig(); }, [loadConfig]); + const setupTelemetryIfAllowed = useCallback(async () => { + const isDev = (process.env.NODE_ENV ?? "development") === "development"; + + // Don't enable telemetry in dev mode because hot reload will spam the logs. + if (isDev) { + return; + } + + const res = await ufetch.get(ROUTE_TABLE.GET_AICONFIGRC, {}); + + const enableTelemetry = res.allow_usage_data_sharing; + + if (enableTelemetry) { + datadogLogs.init({ + clientToken: "pub356987caf022337989e492681d1944a8", + env: process.env.NODE_ENV ?? "development", + service: "aiconfig-editor", + site: "us5.datadoghq.com", + forwardErrorsToLogs: true, + sessionSampleRate: 100, + }); + } + }, []); + + useEffect(() => { + setupTelemetryIfAllowed(); + }, [setupTelemetryIfAllowed]); + const save = useCallback(async (aiconfig: AIConfig) => { const res = await ufetch.post(ROUTE_TABLE.SAVE, { // path: file path, @@ -171,6 +195,14 @@ export default function Editor() { return await ufetch.get(ROUTE_TABLE.SERVER_STATUS); }, []); + const logEvent = useCallback((event: LogEvent, data?: LogEventData) => { + try { + datadogLogs.logger.info(event, data); + } catch (e) { + // Ignore logger errors for now + } + }, []); + const callbacks: AIConfigCallbacks = useMemo( () => ({ addPrompt, @@ -179,6 +211,7 @@ export default function Editor() { deletePrompt, getModels, getServerStatus, + logEvent, runPrompt, save, setConfigDescription, @@ -194,6 +227,7 @@ export default function Editor() { deletePrompt, getModels, getServerStatus, + logEvent, runPrompt, save, setConfigDescription, diff --git a/python/src/aiconfig/editor/client/src/components/AIConfigContext.tsx b/python/src/aiconfig/editor/client/src/components/AIConfigContext.tsx index 5dae05077..1c182f087 100644 --- a/python/src/aiconfig/editor/client/src/components/AIConfigContext.tsx +++ b/python/src/aiconfig/editor/client/src/components/AIConfigContext.tsx @@ -1,5 +1,5 @@ import { createContext } from "react"; -import { ClientAIConfig } from "../shared/types"; +import { ClientAIConfig, LogEvent, LogEventData } from "../shared/types"; /** * Context for overall editor config state. This context should @@ -7,6 +7,7 @@ import { ClientAIConfig } from "../shared/types"; */ const AIConfigContext = createContext<{ getState: () => ClientAIConfig; + logEvent?: (event: LogEvent, data?: LogEventData) => void; }>({ getState: () => ({ prompts: [], _ui: { isDirty: false } }), }); diff --git a/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx b/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx index 1073a217c..66eb20d86 100644 --- a/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx +++ b/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx @@ -31,6 +31,8 @@ import { v4 as uuidv4 } from "uuid"; import aiconfigReducer, { AIConfigReducerAction } from "./aiconfigReducer"; import { ClientPrompt, + LogEvent, + LogEventData, aiConfigToClientConfig, clientConfigToAIConfig, clientPromptToAIConfigPrompt, @@ -98,6 +100,7 @@ export type AIConfigCallbacks = { deletePrompt: (promptName: string) => Promise; getModels: (search: string) => Promise; getServerStatus?: () => Promise<{ status: "OK" | "ERROR" }>; + logEvent?: (event: LogEvent, data?: LogEventData) => void; runPrompt: ( promptName: string, onStream: RunPromptStreamCallback, @@ -167,6 +170,8 @@ export default function EditorContainer({ const stateRef = useRef(aiconfigState); stateRef.current = aiconfigState; + const logEventCallback = callbacks.logEvent; + const saveCallback = callbacks.save; const onSave = useCallback(async () => { setIsSaving(true); @@ -515,6 +520,7 @@ export default function EditorContainer({ }; dispatch(action); + logEventCallback?.("ADD_PROMPT", { model, promptIndex }); try { const serverConfigRes = await addPromptCallback( @@ -536,7 +542,7 @@ export default function EditorContainer({ }); } }, - [addPromptCallback, dispatch] + [addPromptCallback, logEventCallback] ); const deletePromptCallback = callbacks.deletePrompt; @@ -761,8 +767,9 @@ export default function EditorContainer({ const contextValue = useMemo( () => ({ getState, + logEvent: logEventCallback, }), - [getState] + [getState, logEventCallback] ); const isDirty = aiconfigState._ui.isDirty !== false; @@ -870,7 +877,10 @@ export default function EditorContainer({