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 });