Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8734063
feat(marketing): add Outlit SDK with consent-gated tracking
leo-paz Feb 7, 2026
3c7f013
refactor(marketing): centralize analytics track calls
leo-paz Feb 7, 2026
e67ab6a
feat(docs): add Outlit SDK for cross-domain pageview tracking
leo-paz Feb 7, 2026
51626eb
feat(web): add Outlit SDK with authenticated user identity
leo-paz Feb 7, 2026
1deca92
feat(desktop): add Outlit SDK to renderer with analytics wrapper and …
leo-paz Feb 7, 2026
d4aac0f
feat(desktop): add @outlit/node to main process with user.activate on…
leo-paz Feb 7, 2026
e3b14b4
chore: add NEXT_PUBLIC_OUTLIT_KEY env config and fix type errors
leo-paz Feb 7, 2026
1fc0ff4
feat: bump @outlit/browser to 1.4.0 with client prop, add OutlitProvi…
leo-paz Feb 7, 2026
36a682e
fix: resolve Outlit SDK integration bugs across marketing, web, and d…
Kitenite Feb 10, 2026
b049fd7
refactor: clean up Outlit SDK integration
saddlepaddle Feb 10, 2026
1b06971
refactor(desktop): remove unnecessary try/catch around outlit tracking
saddlepaddle Feb 10, 2026
6362160
ci: add NEXT_PUBLIC_OUTLIT_KEY to deploy workflows
saddlepaddle Feb 10, 2026
3b9d80e
fix: use lazy getOutlit() in Next.js apps to avoid SSR window crash
saddlepaddle Feb 10, 2026
f65082b
fix: use publicKey prop in Next.js providers for SSR compatibility
saddlepaddle Feb 10, 2026
1cc4077
fix: make NEXT_PUBLIC_OUTLIT_KEY optional to match existing env patte…
leo-paz Feb 19, 2026
1678be4
Merge remote-tracking branch 'origin/main' into outlit-cleanup
saddlepaddle Mar 3, 2026
c9fab41
fix: make NEXT_PUBLIC_OUTLIT_KEY required across all apps
saddlepaddle Mar 3, 2026
4e6dd2c
ci: add NEXT_PUBLIC_OUTLIT_KEY to CI and desktop build workflows
saddlepaddle Mar 3, 2026
12c6b58
fix(marketing): share single Outlit instance so consent opt-out gates…
saddlepaddle Mar 3, 2026
b407c4b
fix(marketing): guard Outlit construction against SSR (window is not …
saddlepaddle Mar 3, 2026
80bfd83
style(marketing): fix biome formatting
saddlepaddle Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ NEXT_PUBLIC_POSTHOG_KEY=
POSTHOG_API_KEY=
POSTHOG_PROJECT_ID=

# -----------------------------------------------------------------------------
# Outlit Analytics
# -----------------------------------------------------------------------------
NEXT_PUBLIC_OUTLIT_KEY=

# -----------------------------------------------------------------------------
# Freestyle
# -----------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ jobs:
run: bun install --frozen

- name: Test
env:
NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using a repository secret directly for this required env var makes pull_request runs from forks unreliable, because fork-triggered workflows do not receive secrets. Add a safe fallback value for CI (or gate secret-dependent steps) so external PR checks remain runnable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 81:

<comment>Using a repository secret directly for this required env var makes `pull_request` runs from forks unreliable, because fork-triggered workflows do not receive secrets. Add a safe fallback value for CI (or gate secret-dependent steps) so external PR checks remain runnable.</comment>

<file context>
@@ -77,6 +77,8 @@ jobs:
 
       - name: Test
+        env:
+          NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }}
         run: bun run test
 
</file context>
Suggested change
NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY }}
NEXT_PUBLIC_OUTLIT_KEY: ${{ secrets.NEXT_PUBLIC_OUTLIT_KEY || 'ci-outlit-key' }}
Fix with Cubic

run: bun run test

typecheck:
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 \
Expand Down Expand Up @@ -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 }}
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 \
Expand Down Expand Up @@ -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 }}
Expand All @@ -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
6 changes: 6 additions & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/env.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: NEXT_PUBLIC_OUTLIT_KEY is required (z.string()) but should be optional to match the NEXT_PUBLIC_POSTHOG_KEY pattern on the line above. A missing analytics key should degrade gracefully (no tracking), not crash the app at startup. If this key is ever absent in a production build, createEnv will throw and the desktop app won't launch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/env.main.ts, line 27:

<comment>`NEXT_PUBLIC_OUTLIT_KEY` is required (`z.string()`) but should be optional to match the `NEXT_PUBLIC_POSTHOG_KEY` pattern on the line above. A missing analytics key should degrade gracefully (no tracking), not crash the app at startup. If this key is ever absent in a production build, `createEnv` will throw and the desktop app won't launch.</comment>

<file context>
@@ -23,6 +23,8 @@ export const env = createEnv({
 		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(),
 	},
 
</file context>
Suggested change
NEXT_PUBLIC_OUTLIT_KEY: z.string(),
NEXT_PUBLIC_OUTLIT_KEY: z.string().optional(),
Fix with Cubic

},

runtimeEnv: {
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

outlit.shutdown() can prevent app exit if it rejects or hangs.

If the SDK call throws or never resolves, disposeTray() and app.exit(0) are never reached, leaving the app in a zombie state. Wrap in a race with a timeout.

🛡️ Proposed fix
-	await outlit.shutdown();
+	await Promise.race([
+		outlit.shutdown().catch((err) =>
+			console.error("[main] Outlit shutdown failed:", err),
+		),
+		new Promise((resolve) => setTimeout(resolve, 2000)),
+	]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await outlit.shutdown();
await Promise.race([
outlit.shutdown().catch((err) =>
console.error("[main] Outlit shutdown failed:", err),
),
new Promise((resolve) => setTimeout(resolve, 2000)),
]);
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/index.ts` at line 161, The call to outlit.shutdown()
can hang or reject and block subsequent cleanup (disposeTray() and app.exit(0));
wrap the shutdown in a cancellable timeout race (e.g., Promise.race between
outlit.shutdown() and a timeout promise) and await it with a try/catch so
rejections are handled, then ensure disposeTray() and app.exit(0) are executed
in a finally block; update the code around outlit.shutdown() to use this pattern
so shutdown failures or hangs won't prevent disposal and process exit.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unhandled rejection in outlit.shutdown() will prevent the app from quitting. Since isQuitting is set to true before this await, a rejection means disposeTray() and app.exit(0) never run, and subsequent quit attempts are no-ops due to the isQuitting guard. Wrap this in a try/catch so the app always exits.

(Based on your team's feedback about handling async calls with proper await and catch.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/index.ts, line 194:

<comment>Unhandled rejection in `outlit.shutdown()` will prevent the app from quitting. Since `isQuitting` is set to `true` before this `await`, a rejection means `disposeTray()` and `app.exit(0)` never run, and subsequent quit attempts are no-ops due to the `isQuitting` guard. Wrap this in a try/catch so the app always exits.

(Based on your team's feedback about handling async calls with proper await and catch.) </comment>

<file context>
@@ -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);
</file context>
Fix with Cubic

disposeTray();
app.exit(0);
});
Expand Down
35 changes: 24 additions & 11 deletions apps/desktop/src/main/lib/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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({
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Outlit tracking is missing the enriched properties (app_name, platform, desktop_version) that PostHog receives. This means Outlit events will lack desktop-specific context. Consider enriching properties before passing to both PostHog and Outlit, or passing the same enriched set to toOutlitProperties.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/lib/analytics/index.ts, line 55:

<comment>Outlit tracking is missing the enriched properties (`app_name`, `platform`, `desktop_version`) that PostHog receives. This means Outlit events will lack desktop-specific context. Consider enriching properties before passing to both PostHog and Outlit, or passing the same enriched set to `toOutlitProperties`.</comment>

<file context>
@@ -37,16 +39,27 @@ export function track(
+		});
+	}
+
+	outlit.track({
+		eventName: event,
+		userId,
</file context>
Fix with Cubic

eventName: event,
userId,
properties: toOutlitProperties(properties),
});

// Fire user.activate() on project_opened (activation moment)
if (event === "project_opened") {
outlit.user.activate({ userId });
}
}
6 changes: 6 additions & 0 deletions apps/desktop/src/main/lib/outlit/index.ts
Original file line number Diff line number Diff line change
@@ -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,
});
10 changes: 5 additions & 5 deletions apps/desktop/src/renderer/components/Paywall/Paywall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Removing the try-catch around telemetry state changes risks unhandled exceptions in the Electron renderer. If outlit.enableTracking() / disableTracking() (or a posthog call) throws, the error now propagates unhandled from the useEffect. Wrap the block in a try-catch and log a warning so telemetry failures don't crash the component.

(Based on your team's feedback about handling async calls with proper await and catch.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx, line 13:

<comment>Removing the `try-catch` around telemetry state changes risks unhandled exceptions in the Electron renderer. If `outlit.enableTracking()` / `disableTracking()` (or a posthog call) throws, the error now propagates unhandled from the `useEffect`. Wrap the block in a try-catch and log a warning so telemetry failures don't crash the component.

(Based on your team's feedback about handling async calls with proper await and catch.) </comment>

<file context>
@@ -9,21 +10,16 @@ export function TelemetrySync() {
-				if (typeof posthog?.opt_out_capturing === "function") {
-					posthog.opt_out_capturing();
-				}
+		if (telemetryEnabled) {
+			if (typeof posthog?.opt_in_capturing === "function") {
+				posthog.opt_in_capturing();
</file context>
Fix with Cubic

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

Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/env.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});

Expand All @@ -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,
};

Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) => {
Expand Down
Loading