Skip to content

refactor: clean up Outlit SDK integration#1374

Merged
saddlepaddle merged 21 commits into
mainfrom
outlit-cleanup
Mar 3, 2026
Merged

refactor: clean up Outlit SDK integration#1374
saddlepaddle merged 21 commits into
mainfrom
outlit-cleanup

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 10, 2026

Summary

  • Make NEXT_PUBLIC_OUTLIT_KEY required in all env.ts files (matches POSTHOG_KEY pattern), eliminating null checks throughout
  • Replace lazy singleton getOutlit() with simple module-level export const outlit across all 4 apps
  • Fix CookieConsent crash: useOutlit() hook requires OutlitBrowserProvider ancestor — replaced with direct module import
  • Fix desktop renderer autoTrack: true conflicting with manual pageview tracking in index.tsx
  • Deduplicate toOutlitProperties into apps/desktop/src/shared/analytics.ts (used by both main + renderer)
  • Standardize web and docs to use client prop instead of inline publicKey
  • Remove all unnecessary null guards, optional chaining, and wrapper components

Test plan

  • Verify marketing site loads and CookieConsent banner works (accept/opt-out)
  • Verify web app tracks pageviews and identifies authenticated users
  • Verify docs site tracks pageviews
  • Verify desktop app tracks pageviews and project_opened fires user.activate
  • Verify desktop telemetry toggle (enable/disable) works
  • Confirm bun run typecheck passes
  • Confirm bun run lint:fix passes

Summary by CodeRabbit

Release Notes

  • New Features
    • Integrated Outlit Analytics for enhanced user session tracking and event monitoring across desktop, web, and documentation applications.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds Outlit Analytics integration across the desktop, marketing, docs, and web applications. It introduces the NEXT_PUBLIC_OUTLIT_KEY environment variable across configuration files and deployment workflows, adds Outlit dependencies, creates Outlit client instances, wraps component trees with OutlitProvider, and integrates Outlit tracking calls alongside existing PostHog analytics.

Changes

Cohort / File(s) Summary
Environment Configuration
.env.example, .github/workflows/deploy-preview.yml, .github/workflows/deploy-production.yml, apps/desktop/src/main/env.main.ts, apps/desktop/src/renderer/env.renderer.ts, apps/docs/src/env.ts, apps/marketing/src/env.ts, apps/web/src/env.ts
Adds NEXT_PUBLIC_OUTLIT_KEY environment variable to schema definitions and propagates it through CI/CD deployment workflows (preview and production) across all deployment targets (API, Web, Marketing, Admin, Docs).
Build & Dependencies
apps/desktop/electron.vite.config.ts, apps/desktop/package.json, apps/docs/package.json, apps/marketing/package.json, apps/web/package.json
Adds @outlit/browser and @outlit/node dependencies; configures Electron to expose NEXT_PUBLIC_OUTLIT_KEY to both main and renderer processes via define blocks.
Outlit Client Initialization
apps/desktop/src/main/lib/outlit/index.ts, apps/desktop/src/renderer/lib/outlit/index.ts, apps/marketing/src/lib/outlit/index.ts, apps/docs/src/app/providers.tsx, apps/web/src/app/providers.tsx
Creates pre-configured Outlit instances with publicKey from environment; desktop main/renderer have separate configurations; marketing and docs use lazy initialization and provider wrapping respectively.
Analytics Helpers & Utilities
apps/desktop/src/shared/analytics.ts, apps/desktop/src/renderer/lib/analytics/index.ts, apps/marketing/src/lib/analytics/index.ts
Introduces toOutlitProperties helper to filter properties to primitive types; adds track() function that forwards events to both PostHog and Outlit with transformed properties.
Desktop Provider & Layout Integration
apps/desktop/src/main/index.ts, apps/desktop/src/renderer/index.tsx, apps/desktop/src/renderer/providers/OutlitProvider/..., apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx, apps/desktop/src/renderer/providers/TelemetrySync/TelemetrySync.tsx, apps/desktop/src/renderer/routes/-layout.tsx
Wraps component tree with OutlitProvider; adds session-based user identification; integrates outlit.shutdown() in app quit handler; replaces PostHog analytics calls with centralized track() function; removes try/catch from telemetry flow and adds outlit enable/disable logic.
Marketing/Web Provider Integration
apps/marketing/src/app/providers.tsx, apps/marketing/src/components/CookieConsent/CookieConsent.tsx, apps/web/src/app/providers.tsx, apps/web/src/components/OutlitProvider/...
Wraps provider tree with OutlitProvider; adds enableTracking/disableTracking hooks in CookieConsent; creates OutlitProvider component with session-based user payload.
Component Analytics Updates
apps/desktop/src/main/lib/analytics/index.ts, apps/desktop/src/renderer/components/Paywall/Paywall.tsx, apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx, apps/desktop/src/renderer/routes/sign-in/page.tsx, apps/marketing/src/app/components/CTAButtons/HeaderCTA.tsx, apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
Replaces posthog.capture() calls with track() function; adds outlit.track() calls with user context; enhances paywall event payloads with additional metadata (previous_feature_id, time_spent_ms, features_viewed_count); integrates outlit.user.activate() on project_opened event in desktop analytics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A rabbit hops with joy today,
For Outlit tracking finds its way!
With PostHog friends and env keys bright,
Analytics flow from morning to night! ✨
Through providers wrapped and teardowns neat,
This integration is quite sweet! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main refactoring objective of the PR: cleaning up the Outlit SDK integration across multiple apps.
Description check ✅ Passed The description provides clear objectives, specific changes made, and a comprehensive test plan with checkboxes. It covers all major refactoring work including environment variable requirements, singleton pattern changes, bug fixes, deduplication, and standardization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch outlit-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/env.main.ts`:
- Line 24: The env schema currently requires NEXT_PUBLIC_OUTLIT_KEY while
NEXT_PUBLIC_POSTHOG_KEY is optional; update the zod schema so
NEXT_PUBLIC_OUTLIT_KEY uses z.string().optional() to match
NEXT_PUBLIC_POSTHOG_KEY (symbol: NEXT_PUBLIC_OUTLIT_KEY in the env schema), or
if the asymmetry is intentional, add a comment clarifying why it must be
required and adjust CI/local env docs instead; ensure any dependent code that
assumes the key exists handles undefined when you make it optional.

In `@apps/desktop/src/main/index.ts`:
- 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.

In `@apps/web/src/lib/outlit/index.ts`:
- Around line 1-8: This module instantiates the browser SDK (Outlit) at module
scope which can be evaluated during SSR; add the client directive by inserting
"use client" as the very first line of this file so Next.js treats the module as
a client-only module and avoids evaluating the `@outlit/browser` import/Outlit
instantiation on the server; keep the existing imports and the exported outlit
constant (Outlit, outlit) intact so OutlitProvider.tsx continues to consume the
client-only instance.
🧹 Nitpick comments (6)
apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx (1)

14-22: Error log prefix is now slightly misleading.

track("desktop_opened") dispatches to both PostHog and Outlit (see renderer/lib/analytics/index.ts). If outlit.track throws, the catch logs "[posthog] Failed to initialize", which is misleading. Consider updating the prefix/message to reflect the broader scope, e.g. "[analytics]".

Suggested diff
 		} catch (error) {
-			console.error("[posthog] Failed to initialize:", error);
+			console.error("[analytics] Failed to initialize:", error);
 		} finally {
apps/web/src/components/OutlitProvider/OutlitProvider.tsx (1)

1-33: Duplicate OutlitProvider across web and desktop.

This component is identical to apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx. Consider extracting it into a shared package to avoid maintaining two copies. Not blocking for this PR, but worth tracking.

apps/docs/src/app/providers.tsx (1)

7-13: Missing explicit React import for React.ReactNode type.

Line 10 references React.ReactNode but there's no import type React from "react" at the top. The web and desktop OutlitProvider files include this import. While some TypeScript/bundler configurations auto-resolve this, it's inconsistent with the other providers and may cause type errors depending on the tsconfig.

Proposed fix
 "use client";
 
 import { OutlitProvider } from "@outlit/browser/react";
+import type React from "react";
 
 import { outlit } from "@/lib/outlit";
apps/desktop/src/renderer/lib/analytics/index.ts (1)

5-11: Consider isolating analytics calls to prevent one failure from blocking the other.

If posthog.capture throws, outlit.track will never execute (and vice versa). While analytics SDKs rarely throw synchronously, wrapping each call independently would improve resilience. The same pattern exists in apps/marketing/src/lib/analytics/index.ts, so this could be addressed across both in a follow-up.

♻️ Suggested resilience improvement
 export function track(
 	event: string,
 	properties?: Record<string, unknown>,
 ): void {
-	posthog.capture(event, properties);
-	outlit.track(event, toOutlitProperties(properties));
+	try {
+		posthog.capture(event, properties);
+	} catch (e) {
+		console.error("[analytics/posthog] capture failed", e);
+	}
+	try {
+		outlit.track(event, toOutlitProperties(properties));
+	} catch (e) {
+		console.error("[analytics/outlit] track failed", e);
+	}
 }
apps/desktop/src/main/lib/analytics/index.ts (1)

61-64: Consider extracting "project_opened" to a named constant.

This magic string is used as a sentinel for activation logic. Per coding guidelines, hardcoded strings used in conditional logic should be extracted to named constants for maintainability.

Proposed fix
+// At module top or in shared/constants
+const ACTIVATION_EVENT = "project_opened";
+
 // In track():
-	if (event === "project_opened") {
+	if (event === ACTIVATION_EVENT) {
 		outlit.user.activate({ userId });
 	}

As per coding guidelines: "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."

apps/marketing/src/lib/analytics/index.ts (1)

5-21: toOutlitProperties is duplicated from apps/desktop/src/shared/analytics.ts.

This exact function exists in apps/desktop/src/shared/analytics.ts. Since these are separate apps that can't share imports directly, consider extracting it to a shared package (e.g., packages/analytics or packages/utils) if the codebase has that pattern, to keep it DRY.

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(),
NEXT_PUBLIC_OUTLIT_KEY: z.string(),
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 | 🟡 Minor

NEXT_PUBLIC_OUTLIT_KEY is required but NEXT_PUBLIC_POSTHOG_KEY (Line 21) is optional.

The PR objective states this should "match POSTHOG_KEY", but PostHog is .optional() in this file while Outlit is not. If the Outlit key isn't set in CI or local dev (without SKIP_ENV_VALIDATION), env validation will fail and the app won't start. Consider making it .optional() for parity, or confirm this asymmetry is intentional.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/env.main.ts` at line 24, The env schema currently
requires NEXT_PUBLIC_OUTLIT_KEY while NEXT_PUBLIC_POSTHOG_KEY is optional;
update the zod schema so NEXT_PUBLIC_OUTLIT_KEY uses z.string().optional() to
match NEXT_PUBLIC_POSTHOG_KEY (symbol: NEXT_PUBLIC_OUTLIT_KEY in the env
schema), or if the asymmetry is intentional, add a comment clarifying why it
must be required and adjust CI/local env docs instead; ensure any dependent code
that assumes the key exists handles undefined when you make it optional.

// 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.

Comment thread apps/web/src/lib/outlit/index.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/docs/src/env.ts`:
- Around line 24-25: The env schema in apps/docs/src/env.ts is inconsistent:
NEXT_PUBLIC_OUTLIT_KEY is required while NEXT_PUBLIC_POSTHOG_KEY is
optional—unlike apps/web and apps/marketing. Update the Zod schema in that file
to make NEXT_PUBLIC_POSTHOG_KEY required (change NEXT_PUBLIC_POSTHOG_KEY:
z.string().optional() to NEXT_PUBLIC_POSTHOG_KEY: z.string()) so both keys match
the other apps' schemas; ensure the symbol names NEXT_PUBLIC_OUTLIT_KEY and
NEXT_PUBLIC_POSTHOG_KEY are updated accordingly.
🧹 Nitpick comments (3)
apps/desktop/src/renderer/providers/PostHogProvider/PostHogProvider.tsx (1)

17-19: Error log prefix may be misleading now that track() also calls Outlit.

track("desktop_opened") can now throw from either PostHog or Outlit, but the catch logs [posthog] Failed to initialize:. Consider broadening the prefix to [analytics] so the log accurately reflects the possible error source.

Suggested diff
 		} catch (error) {
-			console.error("[posthog] Failed to initialize:", error);
+			console.error("[analytics] Failed to initialize:", error);
 		} finally {
apps/marketing/src/lib/analytics/index.ts (2)

23-29: If either analytics call throws, the other is skipped.

posthog.capture and outlit.track are called sequentially with no error isolation. If posthog.capture throws (e.g., SDK not initialized, network issue during flush), outlit.track will never execute, and the error will propagate to UI event handlers.

Consider wrapping each call independently:

Proposed fix
 export function track(
 	event: string,
 	properties?: Record<string, unknown>,
 ): void {
-	posthog.capture(event, properties);
-	outlit.track(event, toOutlitProperties(properties));
+	try {
+		posthog.capture(event, properties);
+	} catch (e) {
+		console.error("[analytics/posthog] capture failed", e);
+	}
+	try {
+		outlit.track(event, toOutlitProperties(properties));
+	} catch (e) {
+		console.error("[analytics/outlit] track failed", e);
+	}
 }

5-21: toOutlitProperties is duplicated across apps.

This helper is identical to the one in apps/desktop/src/shared/analytics.ts. If you have or plan a shared package (e.g., packages/analytics), consider extracting it there. Otherwise, this is acceptable as cross-app duplication.

Comment thread apps/docs/src/env.ts
Comment on lines +24 to 25
NEXT_PUBLIC_OUTLIT_KEY: z.string(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, verify the actual code in apps/docs/src/env.ts
echo "=== Checking apps/docs/src/env.ts (lines 20-30) ==="
sed -n '20,30p' apps/docs/src/env.ts

echo -e "\n=== Checking apps/web/src/env.ts (lines 20-30) ==="
sed -n '20,30p' apps/web/src/env.ts

echo -e "\n=== Looking for all env.ts files ==="
fd "env.ts" --type f

echo -e "\n=== Checking .env.example files for OUTLIT_KEY ==="
fd ".env.example" --type f | while read f; do echo "File: $f"; grep -i "outlit\|posthog" "$f" || echo "  (no match)"; done

echo -e "\n=== Checking .env files for OUTLIT_KEY ==="
fd ".env$" --type f | while read f; do echo "File: $f"; grep -i "outlit\|posthog" "$f" || echo "  (no match)"; done

Repository: superset-sh/superset

Length of output: 1416


🏁 Script executed:

#!/bin/bash
# Search for OUTLIT_KEY in all env.ts files
echo "=== Searching for OUTLIT_KEY in all env.ts files ==="
rg "OUTLIT" apps/*/src/env.ts packages/*/src/env.ts 2>/dev/null

# Check GitHub workflows for OUTLIT setup
echo -e "\n=== Checking GitHub workflows for OUTLIT_KEY ==="
find .github -name "*.yml" -o -name "*.yaml" 2>/dev/null | while read f; do
  if grep -q "OUTLIT\|outlit" "$f" 2>/dev/null; then
    echo "Found in: $f"
    grep -i "outlit" "$f"
  fi
done

# Check if OUTLIT_KEY is actually used anywhere in code
echo -e "\n=== Searching for OUTLIT_KEY usage in code ==="
rg "OUTLIT_KEY" --type ts --type tsx --type js --type jsx 2>/dev/null | head -20

# Check docker-compose or docker files for env setup
echo -e "\n=== Checking docker files for OUTLIT setup ==="
find . -name "docker-compose*" -o -name "Dockerfile*" 2>/dev/null | while read f; do
  if grep -q -i "outlit" "$f" 2>/dev/null; then
    echo "Found in: $f"
    grep -i "outlit" "$f"
  fi
done

Repository: superset-sh/superset

Length of output: 1627


🏁 Script executed:

#!/bin/bash
# Check POSTHOG_KEY pattern in web and marketing
echo "=== POSTHOG_KEY in apps/web/src/env.ts ==="
rg "POSTHOG_KEY|POSTHOG" apps/web/src/env.ts -A 2 -B 2

echo -e "\n=== POSTHOG_KEY in apps/marketing/src/env.ts ==="
rg "POSTHOG_KEY|POSTHOG" apps/marketing/src/env.ts -A 2 -B 2

echo -e "\n=== POSTHOG_KEY in apps/docs/src/env.ts ==="
rg "POSTHOG_KEY|POSTHOG" apps/docs/src/env.ts -A 2 -B 2

# Check if OUTLIT_KEY is actually used (searching more broadly)
echo -e "\n=== Usage of OUTLIT_KEY in application code ==="
rg "OUTLIT" apps/ packages/ --type ts --type tsx --type js --type jsx 2>/dev/null | grep -v "env.ts" | head -10

Repository: superset-sh/superset

Length of output: 2218


OUTLIT_KEY is required but POSTHOG_KEY is optional in docs — inconsistent with other apps

In apps/docs/src/env.ts, NEXT_PUBLIC_OUTLIT_KEY is required while NEXT_PUBLIC_POSTHOG_KEY is optional. This differs from apps/web and apps/marketing, where both are required. This inconsistency means local development will fail if NEXT_PUBLIC_OUTLIT_KEY is not set (unless SKIP_ENV_VALIDATION is used). Either both keys should be required consistently across all apps, or clarify why docs treats them differently.

🤖 Prompt for AI Agents
In `@apps/docs/src/env.ts` around lines 24 - 25, The env schema in
apps/docs/src/env.ts is inconsistent: NEXT_PUBLIC_OUTLIT_KEY is required while
NEXT_PUBLIC_POSTHOG_KEY is optional—unlike apps/web and apps/marketing. Update
the Zod schema in that file to make NEXT_PUBLIC_POSTHOG_KEY required (change
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional() to NEXT_PUBLIC_POSTHOG_KEY:
z.string()) so both keys match the other apps' schemas; ensure the symbol names
NEXT_PUBLIC_OUTLIT_KEY and NEXT_PUBLIC_POSTHOG_KEY are updated accordingly.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 10, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

@saddlepaddle
Copy link
Copy Markdown
Collaborator Author

/deploy

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/marketing/src/components/CookieConsent/CookieConsent.tsx`:
- Line 10: The Outlit SDK is being initialized with tracking enabled on module
load (Outlit constructor in outlit/index.ts), causing events before consent;
change the Outlit initialization to use trackPageviews: false and autoTrack:
false (and disable trackForms if set) so no tracking starts on import, and then
call outlit.enableTracking() from the CookieConsent component when the user
accepts (modify CookieConsent to invoke outlit.enableTracking() on accept).
Apply the same initialization change to the other outlit/index.ts instances
mentioned and ensure only the explicit enableTracking call enables tracking.
🧹 Nitpick comments (4)
apps/desktop/src/renderer/lib/analytics/index.ts (1)

5-11: No telemetry-enabled guard in renderer track().

The main process track() checks isTelemetryEnabled() before sending events. This renderer version fires unconditionally, relying on SDK-level opt-out (posthog.opt_out_capturing() / outlit.disableTracking() via TelemetrySync). This works but means events will still be called (just suppressed internally). If this is the intended pattern, consider adding a brief comment explaining the difference from the main process approach.

apps/desktop/src/main/lib/analytics/index.ts (1)

61-64: Activation moment on "project_opened" looks correct.

Per coding guidelines, consider extracting "project_opened" to a named constant if it's referenced elsewhere, but this is low priority.

apps/marketing/src/lib/analytics/index.ts (2)

5-21: Duplicate toOutlitProperties — extract to a shared package.

This function is identical to the one in apps/desktop/src/shared/analytics.ts (lines 0-16). Consider extracting it into a shared package (e.g., packages/shared or a dedicated packages/analytics) so both apps import from a single source.


23-29: Consider using an object parameter for track.

Per coding guidelines, functions with 2+ parameters should use object parameters. This also makes the API easier to extend (e.g., adding userId later).

♻️ Suggested refactor
-export function track(
-	event: string,
-	properties?: Record<string, unknown>,
-): void {
-	posthog.capture(event, properties);
-	outlit.track(event, toOutlitProperties(properties));
+export function track({
+	event,
+	properties,
+}: {
+	event: string;
+	properties?: Record<string, unknown>;
+}): void {
+	posthog.capture(event, properties);
+	outlit.track(event, toOutlitProperties(properties));
 }

Note: this would require updating all call sites (track("download_clicked")track({ event: "download_clicked" })). As per coding guidelines, "Use object parameters for functions with 2 or more parameters instead of positional arguments".

import { useEffect, useState } from "react";

import { ANALYTICS_CONSENT_KEY } from "@/lib/constants";
import { outlit } from "@/lib/outlit";
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find files related to outlit and CookieConsent
fd -t f "outlit" . --extension ts --extension tsx
echo "---"
fd -t f "CookieConsent" . --extension ts --extension tsx

Repository: superset-sh/superset

Length of output: 263


🏁 Script executed:

#!/bin/bash
# Search for outlit initialization
find . -path "*/lib/outlit*" -type f

Repository: superset-sh/superset

Length of output: 267


🏁 Script executed:

#!/bin/bash
# Get the structure of the apps/marketing directory
ls -la apps/marketing/src/lib/ 2>/dev/null || find apps -type d -name "lib" | head -5

Repository: superset-sh/superset

Length of output: 854


🏁 Script executed:

#!/bin/bash
# Look for the CookieConsent file and outlit imports
find . -name "CookieConsent.tsx" -exec cat {} \;

Repository: superset-sh/superset

Length of output: 1950


🏁 Script executed:

#!/bin/bash
# Try to find all references to outlit in the codebase
git ls-files | xargs grep -l "outlit" 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 887


🏁 Script executed:

#!/bin/bash
git ls-files | grep -i "outlit" | head -20

Repository: superset-sh/superset

Length of output: 493


🏁 Script executed:

#!/bin/bash
# Check if marketing app exists and look at structure
find . -type f -path "*/marketing*" -name "*.tsx" | head -10

Repository: superset-sh/superset

Length of output: 718


🏁 Script executed:

#!/bin/bash
# List all files and search for outlit
git ls-files | head -50

Repository: superset-sh/superset

Length of output: 1459


🏁 Script executed:

#!/bin/bash
# Simple search for any reference to outlit or CookieConsent
git ls-files | xargs grep -l "outlit\|CookieConsent" 2>/dev/null

Repository: superset-sh/superset

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Check directory structure
ls -la . | grep -E "^d" && echo "---" && ls -la apps/ 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 1632


🏁 Script executed:

#!/bin/bash
test -d apps/marketing && echo "Marketing app exists" || echo "Marketing app does not exist"
find . -type f -name "CookieConsent.tsx" 2>/dev/null
find . -type f -name "*outlit*" 2>/dev/null

Repository: superset-sh/superset

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Search for any Outlit documentation or configuration
rg "enableTracking|disableTracking" -i --type ts --type tsx
rg "new Outlit|Outlit\(" -A 3 --type ts --type tsx

Repository: superset-sh/superset

Length of output: 119


🏁 Script executed:

#!/bin/bash
rg "outlit" package.json
grep -r "outlit" . --include="package*.json" 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 350


🏁 Script executed:

#!/bin/bash
# Search for `@/lib/outlit` references
rg "@/lib/outlit" --type ts --type tsx -B 2 -A 5

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
git ls-files --cached 2>/dev/null | grep -E "(outlit|Cookie)" || echo "No matches found"
find . -type f \( -name "*outlit*" -o -name "*Cookie*" \) 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 436


🏁 Script executed:

#!/bin/bash
tree -L 3 -d . 2>/dev/null | head -50 || find . -maxdepth 3 -type d | head -30

Repository: superset-sh/superset

Length of output: 861


🏁 Script executed:

#!/bin/bash
# Search all TypeScript/JavaScript files for outlit or CookieConsent
find . \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) -type f -exec grep -l "outlit\|CookieConsent" {} \; 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 911


🏁 Script executed:

#!/bin/bash
# Search for consent-related code patterns
rg "opt.*capturing|tracking|consent" -i --type ts --type tsx | head -30

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
rg "trackPageviews|trackForms" -i --type ts --type tsx -B 3 -A 3

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
rg "TODO|FIXME|BUG" -i --type ts --type tsx | grep -i "track\|consent\|gdpr"

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
ls -la apps/marketing/src/lib/outlit/ 2>/dev/null || echo "Path does not exist"
# Try finding any file with outlit in path
find . -path "*outlit*" -type f 2>/dev/null

Repository: superset-sh/superset

Length of output: 433


🏁 Script executed:

#!/bin/bash
# Search for any configuration that mentions tracking or consent
rg "track|consent|gdpr|privacy" -i --type ts --type tsx -l | head -20

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for similar names
rg "outli|outliner|outlaws" --type ts --type tsx
# Search for common analytics tools
rg "posthog|mixpanel|segment|gtag|analytics" --type ts --type tsx | head -20

Repository: superset-sh/superset

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Check if outlit might be a local module or custom implementation
rg "export.*Outlit|class Outlit|function.*outlit" --type ts --type tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Comprehensive search for the exact files mentioned
find . -name "CookieConsent.tsx" -o -name "outlit" -type d 2>/dev/null

Repository: superset-sh/superset

Length of output: 286


🏁 Script executed:

#!/bin/bash
# One final comprehensive search in the repo
find . -type f \( -name "*.ts" -o -name "*.tsx" \) 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 6471


🏁 Script executed:

#!/bin/bash
pwd && ls -la && echo "---" && git config --get remote.origin.url 2>/dev/null || echo "Not a git repo"

Repository: superset-sh/superset

Length of output: 2328


🏁 Script executed:

#!/bin/bash
# Count files and show sample structure
echo "Total files:" && git ls-files | wc -l
echo "Sample files:" && git ls-files | head -30

Repository: superset-sh/superset

Length of output: 1085


🏁 Script executed:

#!/bin/bash
# Look for any reference to Outlit in the entire repo
git log --all --oneline --grep="outlit" -i 2>/dev/null | head -10
git ls-files | xargs grep -i "outlit" 2>/dev/null || echo "No outlit found in repo"

Repository: superset-sh/superset

Length of output: 9886


🏁 Script executed:

#!/bin/bash
# Search everywhere for tracking-related code
rg "enableTracking|disableTracking|opt_in|opt_out" --type ts --type tsx -B 2 -A 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check package.json files for dependencies
find . -name "package.json" -exec grep -l "track\|outlit\|analytics\|consent" {} \;

Repository: superset-sh/superset

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Final attempt - check if maybe it's in node_modules or lock files
find . -name "pnpm-lock.yaml" -o -name "yarn.lock" -o -name "package-lock.json" | head -5

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check if lock files exist and search for outlit
ls -la | grep -E "lock|package"
rg "outlit" pnpm-lock.yaml 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 253


🏁 Script executed:

#!/bin/bash
# Look for any pending or uncommitted changes
git status 2>/dev/null || echo "Not a git repo"

Repository: superset-sh/superset

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Search for PostHog opt_out pattern mentioned in review
rg "opt_out_capturing_by_default|opt_in_capturing" --type ts --type tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check git for any history of these files
git log --all --full-history --name-status -- "*CookieConsent*" 2>/dev/null | head -30
git log --all --full-history --name-status -- "*outlit*" 2>/dev/null | head -30

Repository: superset-sh/superset

Length of output: 1864


🏁 Script executed:

cat -n apps/desktop/src/renderer/lib/outlit/index.ts

Repository: superset-sh/superset

Length of output: 329


🏁 Script executed:

cat -n apps/web/src/lib/outlit/index.ts

Repository: superset-sh/superset

Length of output: 294


🏁 Script executed:

cat apps/marketing/src/app/providers.tsx

Repository: superset-sh/superset

Length of output: 814


🏁 Script executed:

web_search
Outlit npm package `@outlit/browser` documentation

Repository: superset-sh/superset

Length of output: 159


🏁 Script executed:

wc -l apps/marketing/src/lib/outlit/index.ts

Repository: superset-sh/superset

Length of output: 104


🏁 Script executed:

cat apps/marketing/src/lib/outlit/index.ts

Repository: superset-sh/superset

Length of output: 257


🏁 Script executed:

web_search
Outlit analytics tracking enabled by default initialization</bash>

Repository: superset-sh/superset

Length of output: 272


🏁 Script executed:

cat apps/web/src/lib/outlit/index.ts

Repository: superset-sh/superset

Length of output: 238


🏁 Script executed:

cat apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx | head -60

Repository: superset-sh/superset

Length of output: 777


🏁 Script executed:

web_search
Outlit analytics GDPR consent tracking disabled by default

Repository: superset-sh/superset

Length of output: 159


🏁 Script executed:

# Check where CookieConsent is mounted in the app
grep -r "CookieConsent" apps/marketing/src --include="*.tsx" --include="*.ts"

Repository: superset-sh/superset

Length of output: 412


🏁 Script executed:

web_search
"@outlit/browser" npm package documentation enable disable tracking</bash>

Repository: superset-sh/superset

Length of output: 280


Initialize Outlit with tracking disabled until consent is granted.

The Outlit instance in apps/marketing/src/lib/outlit/index.ts is initialized with trackPageviews: true and trackForms: true. Since the SDK initializes on module load—before the user interacts with CookieConsent—tracking begins immediately and events are captured before consent is obtained.

Use the pattern from apps/desktop/src/renderer/lib/outlit/index.ts:

new Outlit({
  publicKey: env.NEXT_PUBLIC_OUTLIT_KEY,
  trackPageviews: false,
  autoTrack: false,
});

Call outlit.enableTracking() only after the user accepts tracking in CookieConsent.

Also applies to: apps/web/src/lib/outlit/index.ts (has trackPageviews: true), 25-27, 29-33

🤖 Prompt for AI Agents
In `@apps/marketing/src/components/CookieConsent/CookieConsent.tsx` at line 10,
The Outlit SDK is being initialized with tracking enabled on module load (Outlit
constructor in outlit/index.ts), causing events before consent; change the
Outlit initialization to use trackPageviews: false and autoTrack: false (and
disable trackForms if set) so no tracking starts on import, and then call
outlit.enableTracking() from the CookieConsent component when the user accepts
(modify CookieConsent to invoke outlit.enableTracking() on accept). Apply the
same initialization change to the other outlit/index.ts instances mentioned and
ensure only the explicit enableTracking call enables tracking.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/marketing/src/components/CookieConsent/CookieConsent.tsx (1)

15-20: ⚠️ Potential issue | 🟠 Major

Stored consent preference is not restored for Outlit on page load.

When the page reloads after a user previously declined, localStorage is checked only to decide whether to show the banner. PostHog likely respects its own persisted opt-out state, but Outlit is re-initialized fresh (with trackPageviews: true in the current outlit module) — effectively ignoring the stored "declined" preference.

Consider reading the stored consent value on mount and calling getOutlit().disableTracking() if it was previously declined (or enableTracking() if accepted), similar to how PostHog's opt_out_capturing_by_default works.

 	useEffect(() => {
 		const consent = localStorage.getItem(ANALYTICS_CONSENT_KEY);
 		if (consent === null) {
 			setShowBanner(true);
+		} else if (consent === "accepted") {
+			getOutlit().enableTracking();
+		} else {
+			getOutlit().disableTracking();
 		}
 	}, []);

This becomes especially important if the outlit module is fixed to initialize with tracking disabled by default.

🤖 Fix all issues with AI agents
In `@apps/marketing/src/lib/analytics/index.ts`:
- Around line 23-29: The track function currently calls posthog.capture and
getOutlit().track directly which can throw and bubble up to UI; update the track
function to call each analytics SDK inside its own try/catch so failures are
contained, log any caught errors with a contextual prefix like
"[analytics/track]" and the event name (do not rethrow), and continue so a
failing posthog.capture does not prevent getOutlit().track from running (and
vice versa); refer to the track function and the calls posthog.capture and
getOutlit().track to locate where to add these guarded try/catch blocks and
logging.
🧹 Nitpick comments (2)
apps/web/src/lib/outlit/index.ts (1)

1-15: Triplicated Outlit singleton — consider extracting to a shared package.

This file is nearly identical to apps/marketing/src/lib/outlit/index.ts and apps/docs/src/lib/outlit/index.ts (the only difference is trackForms: true in marketing). This violates DRY and means bug fixes or API changes need updating in three places.

Consider extracting a shared factory into a workspace package (e.g., packages/analytics) that accepts a config object:

// packages/analytics/src/outlit.ts
export function createOutlitSingleton(config: OutlitConfig) {
  let instance: Outlit | undefined;
  return () => {
    if (!instance) instance = new Outlit(config);
    return instance;
  };
}

Each app would then just call createOutlitSingleton({ publicKey, trackPageviews: true }).

apps/marketing/src/lib/analytics/index.ts (1)

5-21: toOutlitProperties appears duplicated with apps/desktop/src/shared/analytics.ts.

The PR description mentions deduplicating this helper into the desktop shared module, but it also exists here. If the logic is identical, consider extracting it to a shared workspace package to keep it DRY.

Comment on lines +23 to +29
export function track(
event: string,
properties?: Record<string, unknown>,
): void {
posthog.capture(event, properties);
getOutlit().track(event, toOutlitProperties(properties));
}
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

Analytics calls should not crash the calling component.

If either posthog.capture or getOutlit().track throws (e.g., SDK not loaded, network issue, bad state), the error will propagate to the UI component. Analytics failures should be silent to avoid breaking user-facing functionality.

Proposed fix
 export function track(
 	event: string,
 	properties?: Record<string, unknown>,
 ): void {
-	posthog.capture(event, properties);
-	getOutlit().track(event, toOutlitProperties(properties));
+	try {
+		posthog.capture(event, properties);
+	} catch (error) {
+		console.error("[analytics/posthog]", error);
+	}
+	try {
+		getOutlit().track(event, toOutlitProperties(properties));
+	} catch (error) {
+		console.error("[analytics/outlit]", error);
+	}
 }

As per coding guidelines, "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly" — here we log with [domain/operation] prefix and handle by continuing rather than rethrowing, since analytics is non-critical.

📝 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
export function track(
event: string,
properties?: Record<string, unknown>,
): void {
posthog.capture(event, properties);
getOutlit().track(event, toOutlitProperties(properties));
}
export function track(
event: string,
properties?: Record<string, unknown>,
): void {
try {
posthog.capture(event, properties);
} catch (error) {
console.error("[analytics/posthog]", error);
}
try {
getOutlit().track(event, toOutlitProperties(properties));
} catch (error) {
console.error("[analytics/outlit]", error);
}
}
🤖 Prompt for AI Agents
In `@apps/marketing/src/lib/analytics/index.ts` around lines 23 - 29, The track
function currently calls posthog.capture and getOutlit().track directly which
can throw and bubble up to UI; update the track function to call each analytics
SDK inside its own try/catch so failures are contained, log any caught errors
with a contextual prefix like "[analytics/track]" and the event name (do not
rethrow), and continue so a failing posthog.capture does not prevent
getOutlit().track from running (and vice versa); refer to the track function and
the calls posthog.capture and getOutlit().track to locate where to add these
guarded try/catch blocks and logging.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/marketing/src/lib/outlit/index.ts">

<violation number="1" location="apps/marketing/src/lib/outlit/index.ts:17">
P2: Guard the localStorage consent read with try/catch; this unguarded `getItem` can throw in restricted storage environments and crash initialization.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/marketing/src/lib/outlit/index.ts Outdated
Comment on lines +17 to +22
if (
typeof window !== "undefined" &&
localStorage.getItem(ANALYTICS_CONSENT_KEY) === "declined"
) {
instance.disableTracking();
}
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: Guard the localStorage consent read with try/catch; this unguarded getItem can throw in restricted storage environments and crash initialization.

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

<comment>Guard the localStorage consent read with try/catch; this unguarded `getItem` can throw in restricted storage environments and crash initialization.</comment>

<file context>
@@ -11,6 +12,14 @@ export function getOutlit(): Outlit {
 		});
+
+		// Respect prior consent choice — disable tracking if user previously opted out
+		if (
+			typeof window !== "undefined" &&
+			localStorage.getItem(ANALYTICS_CONSENT_KEY) === "declined"
</file context>
Suggested change
if (
typeof window !== "undefined" &&
localStorage.getItem(ANALYTICS_CONSENT_KEY) === "declined"
) {
instance.disableTracking();
}
if (typeof window !== "undefined") {
try {
if (localStorage.getItem(ANALYTICS_CONSENT_KEY) === "declined") {
instance.disableTracking();
}
} catch {
instance.disableTracking();
}
}
Fix with Cubic

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/marketing/src/app/providers.tsx">

<violation number="1" location="apps/marketing/src/app/providers.tsx:13">
P1: Conditionally skipping `OutlitBrowserProvider` can render `useOutlit()` consumers without a provider during server prerender, which can crash the page.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


function OutlitWrapper({ children }: { children: React.ReactNode }) {
const client = getOutlit();
if (!client) return <>{children}</>;
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: Conditionally skipping OutlitBrowserProvider can render useOutlit() consumers without a provider during server prerender, which can crash the page.

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

<comment>Conditionally skipping `OutlitBrowserProvider` can render `useOutlit()` consumers without a provider during server prerender, which can crash the page.</comment>

<file context>
@@ -8,10 +8,16 @@ import { PostHogProvider } from "posthog-js/react";
 
+function OutlitWrapper({ children }: { children: React.ReactNode }) {
+	const client = getOutlit();
+	if (!client) return <>{children}</>;
+	return <OutlitBrowserProvider client={client}>{children}</OutlitBrowserProvider>;
+}
</file context>
Fix with Cubic

@saddlepaddle saddlepaddle merged commit 1fcc540 into main Mar 3, 2026
15 checks passed
@Kitenite Kitenite deleted the outlit-cleanup branch March 15, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants