diff --git a/apps/opik-frontend/index.html b/apps/opik-frontend/index.html index d77ef8b67c..457a9e4262 100644 --- a/apps/opik-frontend/index.html +++ b/apps/opik-frontend/index.html @@ -5,6 +5,7 @@ Comet Opik +
diff --git a/apps/opik-frontend/package-lock.json b/apps/opik-frontend/package-lock.json index c6bbb686ea..46cff5e706 100644 --- a/apps/opik-frontend/package-lock.json +++ b/apps/opik-frontend/package-lock.json @@ -31,6 +31,7 @@ "@tanstack/react-router": "^1.36.3", "@tanstack/react-table": "^8.17.3", "@types/md5": "^2.3.5", + "@types/segment-analytics": "^0.0.38", "@uiw/react-codemirror": "^4.23.0", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", @@ -5613,6 +5614,11 @@ "@types/react": "*" } }, + "node_modules/@types/segment-analytics": { + "version": "0.0.38", + "resolved": "https://registry.npmjs.org/@types/segment-analytics/-/segment-analytics-0.0.38.tgz", + "integrity": "sha512-0clAuA7t6HxtpyXl4veE/oNVdcQFhlxnxgYlk0LeEYSoW3s3zZV5xa6DVHcOtozUO4u15Ipet5TdP88GbwKHvg==" + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", diff --git a/apps/opik-frontend/package.json b/apps/opik-frontend/package.json index e2e813a875..ad576c9c54 100644 --- a/apps/opik-frontend/package.json +++ b/apps/opik-frontend/package.json @@ -48,6 +48,7 @@ "@tanstack/react-router": "^1.36.3", "@tanstack/react-table": "^8.17.3", "@types/md5": "^2.3.5", + "@types/segment-analytics": "^0.0.38", "@uiw/react-codemirror": "^4.23.0", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", diff --git a/apps/opik-frontend/public/config.js b/apps/opik-frontend/public/config.js new file mode 100644 index 0000000000..2d75c55798 --- /dev/null +++ b/apps/opik-frontend/public/config.js @@ -0,0 +1 @@ +window.environmentVariablesOverwrite = {}; diff --git a/apps/opik-frontend/src/plugins/comet/WorkspacePreloader.tsx b/apps/opik-frontend/src/plugins/comet/WorkspacePreloader.tsx index 93639b9688..515a21d698 100644 --- a/apps/opik-frontend/src/plugins/comet/WorkspacePreloader.tsx +++ b/apps/opik-frontend/src/plugins/comet/WorkspacePreloader.tsx @@ -13,6 +13,7 @@ import { MoveLeft } from "lucide-react"; import useUser from "./useUser"; import { buildUrl } from "./utils"; import imageLogoUrl from "/images/logo_and_text.png"; +import useSegment from "@/plugins/comet/analytics/useSegment"; type WorkspacePreloaderProps = { children: React.ReactNode; @@ -29,6 +30,8 @@ const WorkspacePreloader: React.FunctionComponent = ({ }); const isRootPath = matchRoute({ to: "/" }); + useSegment(user?.userName); + if (isLoading) { return ; } diff --git a/apps/opik-frontend/src/plugins/comet/analytics/index.ts b/apps/opik-frontend/src/plugins/comet/analytics/index.ts new file mode 100644 index 0000000000..089319ade8 --- /dev/null +++ b/apps/opik-frontend/src/plugins/comet/analytics/index.ts @@ -0,0 +1,16 @@ +import initSnippet from "./snippet"; + +export const initAnalytics = (writeKey?: string) => { + if (writeKey) { + initSnippet(writeKey); + } +}; + +export const trackEvent = ( + name: string, + properties: Record, +) => { + if (window.analytics) { + window.analytics.track(name, properties); + } +}; diff --git a/apps/opik-frontend/src/plugins/comet/analytics/snippet.js b/apps/opik-frontend/src/plugins/comet/analytics/snippet.js new file mode 100644 index 0000000000..f2488b8fda --- /dev/null +++ b/apps/opik-frontend/src/plugins/comet/analytics/snippet.js @@ -0,0 +1,83 @@ +export default function initSnippet(writeKey) { + var i = "analytics", + analytics = (window[i] = window[i] || []); + if (!analytics.initialize) + if (analytics.invoked) + window.console && + console.error && + console.error("Segment snippet included twice."); + else { + analytics.invoked = !0; + analytics.methods = [ + "trackSubmit", + "trackClick", + "trackLink", + "trackForm", + "pageview", + "identify", + "reset", + "group", + "track", + "ready", + "alias", + "debug", + "page", + "screen", + "once", + "off", + "on", + "addSourceMiddleware", + "addIntegrationMiddleware", + "setAnonymousId", + "addDestinationMiddleware", + "register", + ]; + analytics.factory = function (e) { + return function () { + if (window[i].initialized) + return window[i][e].apply(window[i], arguments); + var n = Array.prototype.slice.call(arguments); + if ( + ["track", "screen", "alias", "group", "page", "identify"].indexOf( + e, + ) > -1 + ) { + var c = document.querySelector("link[rel='canonical']"); + n.push({ + __t: "bpc", + c: (c && c.getAttribute("href")) || void 0, + p: location.pathname, + u: location.href, + s: location.search, + t: document.title, + r: document.referrer, + }); + } + n.unshift(e); + analytics.push(n); + return analytics; + }; + }; + for (var n = 0; n < analytics.methods.length; n++) { + var key = analytics.methods[n]; + analytics[key] = analytics.factory(key); + } + analytics.load = function (key, n) { + var t = document.createElement("script"); + t.type = "text/javascript"; + t.async = !0; + t.setAttribute("data-global-segment-analytics-key", i); + t.src = + "https://cdn.segment.com/analytics.js/v1/" + + key + + "/analytics.min.js"; + var r = document.getElementsByTagName("script")[0]; + r.parentNode.insertBefore(t, r); + analytics._loadOptions = n; + }; + analytics._writeKey = writeKey; + analytics.SNIPPET_VERSION = "5.2.0"; + analytics.load(writeKey); + analytics.page(); + } +} diff --git a/apps/opik-frontend/src/plugins/comet/analytics/useSegment.ts b/apps/opik-frontend/src/plugins/comet/analytics/useSegment.ts new file mode 100644 index 0000000000..cc45401d7a --- /dev/null +++ b/apps/opik-frontend/src/plugins/comet/analytics/useSegment.ts @@ -0,0 +1,11 @@ +import { useEffect } from "react"; + +const useSegment = (username?: string) => { + useEffect(() => { + if (window.analytics && username) { + window.analytics.identify(username); + } + }, [username]); +}; + +export default useSegment; diff --git a/apps/opik-frontend/src/plugins/comet/init.tsx b/apps/opik-frontend/src/plugins/comet/init.tsx new file mode 100644 index 0000000000..aa672f6f66 --- /dev/null +++ b/apps/opik-frontend/src/plugins/comet/init.tsx @@ -0,0 +1,16 @@ +/// + +import { initAnalytics } from "./analytics"; + +type EnvironmentVariablesOverwrite = { + OPIK_SEGMENT_ID?: string; +}; + +declare global { + interface Window { + analytics: SegmentAnalytics.AnalyticsJS; + environmentVariablesOverwrite: EnvironmentVariablesOverwrite; + } +} + +initAnalytics(window.environmentVariablesOverwrite.OPIK_SEGMENT_ID); diff --git a/apps/opik-frontend/src/store/PluginsStore.ts b/apps/opik-frontend/src/store/PluginsStore.ts index 27e1c78cb2..73de67fb61 100644 --- a/apps/opik-frontend/src/store/PluginsStore.ts +++ b/apps/opik-frontend/src/store/PluginsStore.ts @@ -7,16 +7,23 @@ type PluginStore = { GetStartedPage: React.ComponentType | null; UserMenu: React.ComponentType | null; WorkspacePreloader: React.ComponentType<{ children: React.ReactNode }> | null; + init: unknown; setupPlugins: (folderName: string) => Promise; }; const VALID_PLUGIN_FOLDER_NAMES = ["comet"]; -const PLUGIN_NAMES = ["GetStartedPage", "UserMenu", "WorkspacePreloader"]; +const PLUGIN_NAMES = [ + "GetStartedPage", + "UserMenu", + "WorkspacePreloader", + "init", +]; const usePluginsStore = create((set) => ({ GetStartedPage: null, UserMenu: null, WorkspacePreloader: null, + init: null, setupPlugins: async (folderName: string) => { if (!VALID_PLUGIN_FOLDER_NAMES.includes(folderName)) { return set({ WorkspacePreloader });