diff --git a/.env.example b/.env.example index 464107ea1a4..7e67262aa2a 100644 --- a/.env.example +++ b/.env.example @@ -60,6 +60,11 @@ NEXT_PUBLIC_POSTHOG_KEY= POSTHOG_API_KEY= POSTHOG_PROJECT_ID= +# ----------------------------------------------------------------------------- +# Outlit Analytics +# ----------------------------------------------------------------------------- +NEXT_PUBLIC_OUTLIT_KEY= + # ----------------------------------------------------------------------------- # Freestyle # ----------------------------------------------------------------------------- diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index f62b6de29f4..9f1d7d400bb 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -86,6 +86,7 @@ jobs: env: NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} @@ -190,6 +191,7 @@ jobs: env: NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f747b7f45..3bf16358bbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,8 @@ jobs: run: bun install --frozen - name: Test + env: + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} run: bun run test typecheck: @@ -125,4 +127,6 @@ jobs: run: bun install --frozen - name: Build Desktop + env: + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} run: bun turbo run build --filter=@superset/desktop diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 4bf8f5e1870..fd8940fdf65 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -333,6 +333,7 @@ jobs: NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} NEXT_PUBLIC_SENTRY_DSN_WEB: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_WEB }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -362,6 +363,7 @@ jobs: --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ + --env NEXT_PUBLIC_OUTLIT_KEY=$NEXT_PUBLIC_OUTLIT_KEY \ --env NEXT_PUBLIC_SENTRY_DSN_WEB=$NEXT_PUBLIC_SENTRY_DSN_WEB \ --env NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \ --env KV_REST_API_URL=$KV_REST_API_URL \ @@ -432,6 +434,7 @@ jobs: NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} NEXT_PUBLIC_SENTRY_DSN_MARKETING: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_MARKETING }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -458,6 +461,7 @@ jobs: --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ + --env NEXT_PUBLIC_OUTLIT_KEY=$NEXT_PUBLIC_OUTLIT_KEY \ --env NEXT_PUBLIC_SENTRY_DSN_MARKETING=$NEXT_PUBLIC_SENTRY_DSN_MARKETING \ --env NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \ --env KV_REST_API_URL=$KV_REST_API_URL \ @@ -640,6 +644,7 @@ jobs: NEXT_PUBLIC_MARKETING_URL: https://${{ env.MARKETING_ALIAS }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} NEXT_PUBLIC_SENTRY_DSN_DOCS: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_DOCS }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -650,6 +655,7 @@ jobs: VERCEL_URL=$(vercel deploy --prebuilt --archive=tgz --token=$VERCEL_TOKEN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ + --env NEXT_PUBLIC_OUTLIT_KEY=$NEXT_PUBLIC_OUTLIT_KEY \ --env NEXT_PUBLIC_SENTRY_DSN_DOCS=$NEXT_PUBLIC_SENTRY_DSN_DOCS \ --env NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \ --env SECRETS_ENCRYPTION_KEY=$SECRETS_ENCRYPTION_KEY) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index ba1115973a5..7824fcf45a2 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -210,6 +210,7 @@ jobs: NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} NEXT_PUBLIC_SENTRY_DSN_WEB: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_WEB }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -239,6 +240,7 @@ jobs: --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ + --env NEXT_PUBLIC_OUTLIT_KEY=$NEXT_PUBLIC_OUTLIT_KEY \ --env NEXT_PUBLIC_SENTRY_DSN_WEB=$NEXT_PUBLIC_SENTRY_DSN_WEB \ --env NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \ --env KV_REST_API_URL=$KV_REST_API_URL \ @@ -293,6 +295,7 @@ jobs: NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} NEXT_PUBLIC_SENTRY_DSN_MARKETING: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_MARKETING }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -319,6 +322,7 @@ jobs: --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ + --env NEXT_PUBLIC_OUTLIT_KEY=$NEXT_PUBLIC_OUTLIT_KEY \ --env NEXT_PUBLIC_SENTRY_DSN_MARKETING=$NEXT_PUBLIC_SENTRY_DSN_MARKETING \ --env NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \ --env KV_REST_API_URL=$KV_REST_API_URL \ @@ -485,6 +489,7 @@ jobs: NEXT_PUBLIC_MARKETING_URL: ${{ secrets.NEXT_PUBLIC_MARKETING_URL }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }} NEXT_PUBLIC_SENTRY_DSN_DOCS: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_DOCS }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -495,6 +500,7 @@ jobs: vercel deploy --prod --prebuilt --archive=tgz --token=$VERCEL_TOKEN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ + --env NEXT_PUBLIC_OUTLIT_KEY=$NEXT_PUBLIC_OUTLIT_KEY \ --env NEXT_PUBLIC_SENTRY_DSN_DOCS=$NEXT_PUBLIC_SENTRY_DSN_DOCS \ --env NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \ --env SECRETS_ENCRYPTION_KEY=$SECRETS_ENCRYPTION_KEY diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index e49a2df401b..8dead39fa66 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -87,6 +87,9 @@ export default defineConfig({ "process.env.SUPERSET_WORKSPACE_NAME": defineEnv( process.env.SUPERSET_WORKSPACE_NAME, ), + "process.env.NEXT_PUBLIC_OUTLIT_KEY": defineEnv( + process.env.NEXT_PUBLIC_OUTLIT_KEY, + ), }, build: { @@ -195,6 +198,9 @@ export default defineConfig({ "process.env.SUPERSET_WORKSPACE_NAME": defineEnv( process.env.SUPERSET_WORKSPACE_NAME, ), + "import.meta.env.NEXT_PUBLIC_OUTLIT_KEY": defineEnv( + process.env.NEXT_PUBLIC_OUTLIT_KEY, + ), }, server: { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 62296d8741f..396c90db1ea 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -46,6 +46,8 @@ "@headless-tree/react": "^1.6.3", "@hookform/resolvers": "^5.2.2", "@monaco-editor/react": "^4.7.0", + "@outlit/browser": "^1.4.0", + "@outlit/node": "^1.1.0", "@pierre/diffs": "^1.0.10", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", diff --git a/apps/desktop/src/main/env.main.ts b/apps/desktop/src/main/env.main.ts index 4e832791a7f..0665cd1a1bd 100644 --- a/apps/desktop/src/main/env.main.ts +++ b/apps/desktop/src/main/env.main.ts @@ -23,6 +23,8 @@ export const env = createEnv({ NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), SENTRY_DSN_DESKTOP: z.string().optional(), + STREAMS_URL: z.url().default("https://superset-stream.fly.dev"), + NEXT_PUBLIC_OUTLIT_KEY: z.string(), }, runtimeEnv: { @@ -37,6 +39,8 @@ export const env = createEnv({ NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, SENTRY_DSN_DESKTOP: process.env.SENTRY_DSN_DESKTOP, + STREAMS_URL: process.env.STREAMS_URL, + NEXT_PUBLIC_OUTLIT_KEY: process.env.NEXT_PUBLIC_OUTLIT_KEY, }, emptyStringAsUndefined: true, // Only allow skipping validation in development (never in production) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 6ce28a37bb1..dc7c56ed990 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -28,6 +28,7 @@ import { setupAutoUpdater } from "./lib/auto-updater"; import { setWorkspaceDockIcon } from "./lib/dock-icon"; import { loadWebviewBrowserExtension } from "./lib/extensions"; import { localDb } from "./lib/local-db"; +import { outlit } from "./lib/outlit"; import { ensureProjectIconsDir, getProjectIconPath } from "./lib/project-icons"; import { initSentry } from "./lib/sentry"; import { @@ -187,7 +188,10 @@ app.on("before-quit", async (event) => { } } + // Quit confirmed or no confirmation needed - exit immediately + // Let OS clean up child processes, tray, etc. isQuitting = true; + await outlit.shutdown(); disposeTray(); app.exit(0); }); diff --git a/apps/desktop/src/main/lib/analytics/index.ts b/apps/desktop/src/main/lib/analytics/index.ts index a81044fee55..6c40960eb8e 100644 --- a/apps/desktop/src/main/lib/analytics/index.ts +++ b/apps/desktop/src/main/lib/analytics/index.ts @@ -1,6 +1,8 @@ import { app } from "electron"; import { env } from "main/env.main"; +import { outlit } from "main/lib/outlit"; import { PostHog } from "posthog-node"; +import { toOutlitProperties } from "shared/analytics"; import { DEFAULT_TELEMETRY_ENABLED } from "shared/constants"; export let posthog: PostHog | null = null; @@ -37,16 +39,27 @@ export function track( if (!isTelemetryEnabled()) return; const client = getClient(); - if (!client) return; - - client.capture({ - distinctId: userId, - event, - properties: { - ...properties, - app_name: "desktop", - platform: process.platform, - desktop_version: app.getVersion(), - }, + if (client) { + client.capture({ + distinctId: userId, + event, + properties: { + ...properties, + app_name: "desktop", + platform: process.platform, + desktop_version: app.getVersion(), + }, + }); + } + + outlit.track({ + eventName: event, + userId, + properties: toOutlitProperties(properties), }); + + // Fire user.activate() on project_opened (activation moment) + if (event === "project_opened") { + outlit.user.activate({ userId }); + } } diff --git a/apps/desktop/src/main/lib/outlit/index.ts b/apps/desktop/src/main/lib/outlit/index.ts new file mode 100644 index 00000000000..2868cece0d3 --- /dev/null +++ b/apps/desktop/src/main/lib/outlit/index.ts @@ -0,0 +1,6 @@ +import { Outlit } from "@outlit/node"; +import { env } from "main/env.main"; + +export const outlit = new Outlit({ + publicKey: env.NEXT_PUBLIC_OUTLIT_KEY, +}); diff --git a/apps/desktop/src/renderer/components/Paywall/Paywall.tsx b/apps/desktop/src/renderer/components/Paywall/Paywall.tsx index 945b3151d43..3aba9b61e77 100644 --- a/apps/desktop/src/renderer/components/Paywall/Paywall.tsx +++ b/apps/desktop/src/renderer/components/Paywall/Paywall.tsx @@ -2,7 +2,7 @@ import { Button } from "@superset/ui/button"; import { Dialog, DialogContent } from "@superset/ui/dialog"; import { useNavigate } from "@tanstack/react-router"; import { useEffect, useRef, useState } from "react"; -import { posthog } from "../../lib/posthog"; +import { track } from "renderer/lib/analytics"; import { FeaturePreview } from "./components/FeaturePreview"; import { FeatureSidebar } from "./components/FeatureSidebar"; import type { GatedFeature } from "./constants"; @@ -51,7 +51,7 @@ export const Paywall = () => { featuresViewedRef.current = new Set([initialFeatureId]); const feature = PRO_FEATURES.find((f) => f.id === initialFeatureId); - posthog.capture("paywall_opened", { + track("paywall_opened", { trigger_source: paywallOptions.feature, feature_id: initialFeatureId, feature_title: feature?.title, @@ -72,7 +72,7 @@ export const Paywall = () => { const handleSelectFeature = (featureId: string) => { if (featureId !== selectedFeatureId) { const feature = PRO_FEATURES.find((f) => f.id === featureId); - posthog.capture("paywall_feature_clicked", { + track("paywall_feature_clicked", { trigger_source: triggerSource, feature_id: featureId, feature_title: feature?.title, @@ -88,7 +88,7 @@ export const Paywall = () => { const timeSpent = openTimeRef.current ? Date.now() - openTimeRef.current : 0; - posthog.capture("paywall_cancelled", { + track("paywall_cancelled", { trigger_source: triggerSource, feature_id: selectedFeatureId, features_viewed_count: featuresViewedRef.current.size, @@ -109,7 +109,7 @@ export const Paywall = () => { const timeSpent = openTimeRef.current ? Date.now() - openTimeRef.current : 0; - posthog.capture("paywall_upgrade_clicked", { + track("paywall_upgrade_clicked", { trigger_source: triggerSource, feature_id: selectedFeatureId, feature_title: selectedFeature.title, diff --git a/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx b/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx index a050a86115f..0e9cfb0e149 100644 --- a/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx +++ b/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; +import { track } from "renderer/lib/analytics"; import { authClient } from "renderer/lib/auth-client"; import { electronTrpc } from "renderer/lib/electron-trpc"; - import { posthog } from "../../lib/posthog"; const AUTH_COMPLETED_KEY = "superset_auth_completed"; @@ -23,7 +23,7 @@ export function PostHogUserIdentifier() { const trackedUserId = localStorage.getItem(AUTH_COMPLETED_KEY); if (trackedUserId !== user.id) { - posthog.capture("auth_completed"); + track("auth_completed"); localStorage.setItem(AUTH_COMPLETED_KEY, user.id); } } else if (session !== undefined && !user) { diff --git a/apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx b/apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx index 4bbb7b27e29..83cdf6b8bdd 100644 --- a/apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx +++ b/apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { outlit } from "renderer/lib/outlit"; import { posthog } from "renderer/lib/posthog"; export function TelemetrySync() { @@ -9,21 +10,16 @@ export function TelemetrySync() { useEffect(() => { if (telemetryEnabled === undefined) return; - try { - if (telemetryEnabled) { - if (typeof posthog?.opt_in_capturing === "function") { - posthog.opt_in_capturing(); - } - } else { - if (typeof posthog?.opt_out_capturing === "function") { - posthog.opt_out_capturing(); - } + if (telemetryEnabled) { + if (typeof posthog?.opt_in_capturing === "function") { + posthog.opt_in_capturing(); } - } catch (error) { - console.error( - "[telemetry-sync] Failed to update telemetry state:", - error, - ); + outlit.enableTracking(); + } else { + if (typeof posthog?.opt_out_capturing === "function") { + posthog.opt_out_capturing(); + } + outlit.disableTracking(); } }, [telemetryEnabled]); diff --git a/apps/desktop/src/renderer/env.renderer.ts b/apps/desktop/src/renderer/env.renderer.ts index 404ce33ec36..8c38108f8e7 100644 --- a/apps/desktop/src/renderer/env.renderer.ts +++ b/apps/desktop/src/renderer/env.renderer.ts @@ -22,6 +22,7 @@ const envSchema = z.object({ .default("https://api.superset.sh/api/electric"), NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), + NEXT_PUBLIC_OUTLIT_KEY: z.string(), SENTRY_DSN_DESKTOP: z.string().optional(), }); @@ -43,6 +44,9 @@ const rawEnv = { NEXT_PUBLIC_POSTHOG_HOST: import.meta.env.NEXT_PUBLIC_POSTHOG_HOST as | string | undefined, + NEXT_PUBLIC_OUTLIT_KEY: import.meta.env.NEXT_PUBLIC_OUTLIT_KEY as + | string + | undefined, SENTRY_DSN_DESKTOP: import.meta.env.SENTRY_DSN_DESKTOP as string | undefined, }; diff --git a/apps/desktop/src/renderer/index.tsx b/apps/desktop/src/renderer/index.tsx index f636d9f3341..8d0b08ac1a4 100644 --- a/apps/desktop/src/renderer/index.tsx +++ b/apps/desktop/src/renderer/index.tsx @@ -12,6 +12,7 @@ import { markBootMounted, reportBootError, } from "./lib/boot-errors"; +import { outlit } from "./lib/outlit"; import { persistentHistory } from "./lib/persistent-hash-history"; import { posthog } from "./lib/posthog"; import { electronQueryClient } from "./providers/ElectronTRPCProvider"; @@ -35,6 +36,9 @@ const unsubscribe = router.subscribe("onResolved", (event) => { posthog.capture("$pageview", { $current_url: event.toLocation.pathname, }); + outlit.track("pageview", { + url: event.toLocation.pathname, + }); }); const handleDeepLink = (path: string) => { diff --git a/apps/desktop/src/renderer/lib/analytics/index.ts b/apps/desktop/src/renderer/lib/analytics/index.ts new file mode 100644 index 00000000000..bad28040dd6 --- /dev/null +++ b/apps/desktop/src/renderer/lib/analytics/index.ts @@ -0,0 +1,11 @@ +import { outlit } from "renderer/lib/outlit"; +import { posthog } from "renderer/lib/posthog"; +import { toOutlitProperties } from "shared/analytics"; + +export function track( + event: string, + properties?: Record, +): void { + posthog.capture(event, properties); + outlit.track(event, toOutlitProperties(properties)); +} diff --git a/apps/desktop/src/renderer/lib/outlit/index.ts b/apps/desktop/src/renderer/lib/outlit/index.ts new file mode 100644 index 00000000000..855e1eebbc1 --- /dev/null +++ b/apps/desktop/src/renderer/lib/outlit/index.ts @@ -0,0 +1,8 @@ +import { Outlit } from "@outlit/browser"; +import { env } from "renderer/env.renderer"; + +export const outlit = new Outlit({ + publicKey: env.NEXT_PUBLIC_OUTLIT_KEY, + trackPageviews: false, + autoTrack: false, +}); diff --git a/apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx b/apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx new file mode 100644 index 00000000000..dfbdd2251ff --- /dev/null +++ b/apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx @@ -0,0 +1,30 @@ +import { OutlitProvider as OutlitBrowserProvider } from "@outlit/browser/react"; +import type React from "react"; +import { authClient } from "renderer/lib/auth-client"; +import { outlit } from "renderer/lib/outlit"; + +interface OutlitProviderProps { + children: React.ReactNode; +} + +export function OutlitProvider({ children }: OutlitProviderProps) { + const { data: session } = authClient.useSession(); + const user = session?.user; + + return ( + + {children} + + ); +} diff --git a/apps/desktop/src/renderer/providers/OutlitProvider/index.ts b/apps/desktop/src/renderer/providers/OutlitProvider/index.ts new file mode 100644 index 00000000000..aabfc36b4b5 --- /dev/null +++ b/apps/desktop/src/renderer/providers/OutlitProvider/index.ts @@ -0,0 +1 @@ +export { OutlitProvider } from "./OutlitProvider"; diff --git a/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx b/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx index 985421d5e7c..29a2dcaecce 100644 --- a/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx +++ b/apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx @@ -1,6 +1,7 @@ import { PostHogProvider as PHProvider } from "posthog-js/react"; import type React from "react"; import { useEffect, useState } from "react"; +import { track } from "renderer/lib/analytics"; import { initPostHog, posthog } from "renderer/lib/posthog"; interface PostHogProviderProps { @@ -13,7 +14,7 @@ export function PostHogProvider({ children }: PostHogProviderProps) { useEffect(() => { try { initPostHog(); - posthog.capture("desktop_opened"); + track("desktop_opened"); } catch (error) { console.error("[posthog] Failed to initialize:", error); } finally { diff --git a/apps/desktop/src/renderer/routes/-layout.tsx b/apps/desktop/src/renderer/routes/-layout.tsx index 630b49d7722..fda9e4b844a 100644 --- a/apps/desktop/src/renderer/routes/-layout.tsx +++ b/apps/desktop/src/renderer/routes/-layout.tsx @@ -6,22 +6,25 @@ import { ThemedToaster } from "renderer/components/ThemedToaster"; import { AuthProvider } from "renderer/providers/AuthProvider"; import { ElectronTRPCProvider } from "renderer/providers/ElectronTRPCProvider"; import { MonacoProvider } from "renderer/providers/MonacoProvider"; +import { OutlitProvider } from "renderer/providers/OutlitProvider"; import { PostHogProvider } from "renderer/providers/PostHogProvider"; export function RootLayout({ children }: { children: ReactNode }) { return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ); } diff --git a/apps/desktop/src/renderer/routes/sign-in/page.tsx b/apps/desktop/src/renderer/routes/sign-in/page.tsx index b1679314e82..2a3fe8193ed 100644 --- a/apps/desktop/src/renderer/routes/sign-in/page.tsx +++ b/apps/desktop/src/renderer/routes/sign-in/page.tsx @@ -5,9 +5,9 @@ import { createFileRoute, Navigate } from "@tanstack/react-router"; import { FaGithub } from "react-icons/fa"; import { FcGoogle } from "react-icons/fc"; import { env } from "renderer/env.renderer"; +import { track } from "renderer/lib/analytics"; import { authClient } from "renderer/lib/auth-client"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { posthog } from "renderer/lib/posthog"; import { SupersetLogo } from "./components/SupersetLogo"; export const Route = createFileRoute("/sign-in/")({ @@ -38,7 +38,7 @@ function SignInPage() { } const signIn = (provider: AuthProvider) => { - posthog.capture("auth_started", { provider }); + track("auth_started", { provider }); signInMutation.mutate({ provider }); }; diff --git a/apps/desktop/src/shared/analytics.ts b/apps/desktop/src/shared/analytics.ts new file mode 100644 index 00000000000..41b4eb941be --- /dev/null +++ b/apps/desktop/src/shared/analytics.ts @@ -0,0 +1,17 @@ +export function toOutlitProperties( + properties?: Record, +): Record | undefined { + if (!properties) return undefined; + const result: Record = {}; + for (const [key, value] of Object.entries(properties)) { + if ( + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + result[key] = value; + } + } + return result; +} diff --git a/apps/docs/package.json b/apps/docs/package.json index c892cdeede5..1fb2b8ce2f8 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -11,6 +11,7 @@ "postinstall": "fumadocs-mdx" }, "dependencies": { + "@outlit/browser": "^1.4.0", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-scroll-area": "^1.2.10", "@sentry/nextjs": "^10.36.0", diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index ed4f3b8ddf2..330ab4a4539 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -5,6 +5,7 @@ import { COMPANY } from "@superset/shared/constants"; import { Inter } from "next/font/google"; import { NavigationBar } from "@/app/components/NavigationBar"; import { NavbarProvider } from "@/app/components/NavigationBar/components/NavigationMobile"; +import { OutlitProviderWrapper } from "@/app/providers"; const inter = Inter({ subsets: ["latin"], @@ -67,12 +68,14 @@ export default function Layout({ children }: LayoutProps<"/">) { suppressHydrationWarning > - - - - {children} - - + + + + + {children} + + + ); diff --git a/apps/docs/src/app/providers.tsx b/apps/docs/src/app/providers.tsx new file mode 100644 index 00000000000..f1f28d66150 --- /dev/null +++ b/apps/docs/src/app/providers.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { OutlitProvider } from "@outlit/browser/react"; + +import { env } from "@/env"; + +export function OutlitProviderWrapper({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/docs/src/env.ts b/apps/docs/src/env.ts index 0343e017bba..594d8c54822 100644 --- a/apps/docs/src/env.ts +++ b/apps/docs/src/env.ts @@ -16,6 +16,7 @@ export const env = createEnv({ client: { NEXT_PUBLIC_MARKETING_URL: z.string().url().optional(), + NEXT_PUBLIC_OUTLIT_KEY: z.string(), NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().url().optional(), NEXT_PUBLIC_SENTRY_DSN_DOCS: z.string().optional(), @@ -27,6 +28,7 @@ export const env = createEnv({ experimental__runtimeEnv: { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_MARKETING_URL: process.env.NEXT_PUBLIC_MARKETING_URL, + NEXT_PUBLIC_OUTLIT_KEY: process.env.NEXT_PUBLIC_OUTLIT_KEY, NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, NEXT_PUBLIC_SENTRY_DSN_DOCS: process.env.NEXT_PUBLIC_SENTRY_DSN_DOCS, diff --git a/apps/marketing/package.json b/apps/marketing/package.json index b50d7b7fb8e..970a378ac76 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -11,6 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@outlit/browser": "^1.4.0", "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", "@sentry/nextjs": "^10.36.0", diff --git a/apps/marketing/src/app/components/CTAButtons/HeaderCTA.tsx b/apps/marketing/src/app/components/CTAButtons/HeaderCTA.tsx index c56ab7a3e1f..95876019dad 100644 --- a/apps/marketing/src/app/components/CTAButtons/HeaderCTA.tsx +++ b/apps/marketing/src/app/components/CTAButtons/HeaderCTA.tsx @@ -2,10 +2,10 @@ import { DOWNLOAD_URL_MAC_ARM64 } from "@superset/shared/constants"; import { Download } from "lucide-react"; -import posthog from "posthog-js"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { HiMiniClock } from "react-icons/hi2"; +import { track } from "@/lib/analytics"; import { usePlatform } from "../../hooks/useOS"; import { DownloadButton } from "../DownloadButton"; import { WaitlistModal } from "../WaitlistModal"; @@ -65,7 +65,7 @@ export function HeaderCTA({ isLoggedIn, dashboardUrl }: HeaderCTAProps) { posthog.capture("download_clicked")} + onClick={() => track("download_clicked")} > Download for macOS