Skip to content

feat: integrate Outlit analytics SDK across all apps#1278

Closed
leo-paz wants to merge 8 commits into
superset-sh:mainfrom
leo-paz:outlit-integration
Closed

feat: integrate Outlit analytics SDK across all apps#1278
leo-paz wants to merge 8 commits into
superset-sh:mainfrom
leo-paz:outlit-integration

Conversation

@leo-paz
Copy link
Copy Markdown
Contributor

@leo-paz leo-paz commented Feb 7, 2026

Summary

  • Integrates the Outlit analytics SDK (@outlit/browser + @outlit/node) across all four apps (desktop, web, marketing, docs)
  • Adds consent-aware tracking that respects user opt-in/opt-out preferences
  • Links anonymous visitor IDs to authenticated users via OutlitProvider wrappers in desktop and web apps
  • Centralizes analytics track() calls to send events to both PostHog and Outlit in parallel

Changes by app

Desktop (Electron)

  • Main process: @outlit/node for server-side user.activate on project open
  • Renderer: singleton + OutlitProvider with client prop for shared instance and user identity
  • TelemetrySync wires up enableTracking()/disableTracking() alongside PostHog
  • Manual pageview tracking on route changes (SPA)

Marketing (Next.js)

  • OutlitProvider in providers with pageview + form tracking enabled
  • CookieConsent uses useOutlit() hook for enableTracking()/disableTracking()
  • Centralized track() wrapper sends to both PostHog and Outlit

Web (Next.js)

  • OutlitProvider wrapper handles authenticated user identity (setUser/clearUser)
  • Pageview tracking enabled via provider

Docs (Next.js)

  • OutlitProvider for cross-domain pageview tracking

Shared

  • NEXT_PUBLIC_OUTLIT_KEY added to .env.example and all env schemas
  • @outlit/browser@^1.4.0 uses new client prop to share singleton with provider (avoids dual instance issue)

Test plan

  • Verify desktop app tracks pageviews and links to authenticated user
  • Verify marketing consent banner enables/disables Outlit tracking
  • Verify web app identifies authenticated users
  • Verify docs pageviews appear in Outlit dashboard
  • Verify desktop TelemetrySync toggle controls Outlit tracking
  • Verify no tracking occurs when NEXT_PUBLIC_OUTLIT_KEY is unset

Summary by CodeRabbit

  • New Features

    • Integrated Outlit analytics across desktop, web, docs, and marketing for event and pageview tracking.
    • Telemetry consent now enables/disables Outlit alongside existing analytics.
    • Centralized analytics forwarding so UI events emit unified tracking to both providers when available.
    • Added provider wrappers to supply Outlit client and user context in relevant apps.
  • Dependencies

    • Added Outlit client packages and a new NEXT_PUBLIC_OUTLIT_KEY environment variable.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Adds Outlit telemetry: environment keys, dependencies, lazy Outlit singletons (main/renderer/marketing), Outlit providers, centralized track() forwarding to PostHog and Outlit, telemetry enable/disable wiring, and a main-process shutdown hook.

Changes

Cohort / File(s) Summary
Env files & build defines
\.env.example, apps/desktop/electron.vite.config.ts
Adds Outlit env block to .env.example and exposes NEXT_PUBLIC_OUTLIT_KEY in Electron Vite define blocks for main and renderer.
Main process env & lifecycle
apps/desktop/src/main/env.main.ts, apps/desktop/src/main/index.ts, apps/desktop/package.json
Adds NEXT_PUBLIC_OUTLIT_KEY to main env schema/runtimeEnv; adds @outlit/node and calls shutdownOutlit() on before-quit.
Renderer env schemas & build
apps/desktop/src/renderer/env.renderer.ts, apps/docs/src/env.ts, apps/marketing/src/env.ts, apps/web/src/env.ts
Adds optional NEXT_PUBLIC_OUTLIT_KEY to client env schemas and runtime mappings.
Dependencies across apps
apps/desktop/package.json, apps/docs/package.json, apps/marketing/package.json, apps/web/package.json
Adds @outlit/browser (and @outlit/node in desktop) to app dependencies.
Outlit singletons (main/renderer/marketing)
apps/desktop/src/main/lib/outlit/index.ts, apps/desktop/src/renderer/lib/outlit/index.ts, apps/marketing/src/lib/outlit/index.ts
New lazy singletons getOutlit() (returns instance or null); main exposes shutdownOutlit() to gracefully close the instance.
Centralized analytics wrappers
apps/desktop/src/main/lib/analytics/index.ts, apps/desktop/src/renderer/lib/analytics/index.ts, apps/marketing/src/lib/analytics/index.ts
Adds track() functions that forward events to PostHog and getOutlit()?.track(...) with typed property casting and error handling.
Renderer providers & wiring
apps/desktop/src/renderer/providers/OutlitProvider/*, apps/web/src/components/OutlitProvider/*, apps/docs/src/app/providers.tsx, apps/docs/src/app/layout.tsx, apps/marketing/src/app/providers.tsx, apps/web/src/app/providers.tsx, apps/desktop/src/renderer/routes/-layout.tsx
Adds OutlitProvider components/re-exports and wraps app provider trees conditionally based on env; passes user info to Outlit where available.
Renderer instrumentation & tracking calls
apps/desktop/src/renderer/index.tsx, apps/desktop/src/renderer/lib/analytics/index.ts, apps/desktop/src/renderer/components/..., apps/desktop/src/renderer/routes/sign-in/page.tsx, apps/desktop/src/renderer/components/TelemetrySync.tsx, apps/desktop/src/renderer/components/PostHogUserIdentifier.tsx
Replaces direct PostHog calls with centralized track(); adds Outlit pageview tracking and enables/disables Outlit alongside PostHog in telemetry sync.
Marketing components & consent
apps/marketing/src/app/components/CTAButtons/HeaderCTA.tsx, apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx, apps/marketing/src/components/CookieConsent/CookieConsent.tsx
Switches tracking to centralized track() and wires cookie consent to Outlit enable/disable calls.
Barrel exports
apps/desktop/src/renderer/providers/OutlitProvider/index.ts, apps/web/src/components/OutlitProvider/index.ts
Adds re-exports for the newly added OutlitProvider modules.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Renderer (UI)
  participant Track as track()
  participant PostHog as PostHog
  participant Outlit as Outlit (getOutlit)
  UI->>Track: track("event", props)
  Track->>PostHog: posthog.capture("event", props)
  Track->>Outlit: getOutlit()?.track("event", props)
Loading
sequenceDiagram
  participant ElectronMain as Electron Main
  participant OutlitMain as Outlit (main)
  ElectronMain->>OutlitMain: before-quit -> shutdownOutlit()
  OutlitMain-->>ElectronMain: flush/close complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble keys and hop through light,

Outlit listens in the quiet night,
Events get tracked with gentle care,
Pageviews skip and cookies share,
Bunny hops off — tidy, bright, and right.

🚥 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 'feat: integrate Outlit analytics SDK across all apps' directly and clearly summarizes the main change—integrating Outlit analytics across the four apps. It is concise, specific, and directly reflects the primary objective of the changeset.
Description check ✅ Passed The PR description provides a comprehensive summary of changes, organized by app with details on integration approach. While the template sections (Related Issues, Testing, Screenshots) are not explicitly filled, the custom summary effectively communicates the feature, test plan, and implementation details required for review.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@leo-paz leo-paz force-pushed the outlit-integration branch from 0eed2a2 to 89c6b8d Compare February 7, 2026 09:39
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: 5

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/outlit/index.ts`:
- Around line 17-22: shutdownOutlit may throw if outlit.shutdown() fails, which
can block later cleanup (disposeTray and app.exit(0)); wrap the call in a
try-catch inside shutdownOutlit so any thrown error is caught and logged via
your logger (or console.error) and does not rethrow, then ensure outlit is set
to null in a finally block or after catching so subsequent cleanup proceeds;
reference the shutdownOutlit function and the outlit.shutdown() call when making
the change.

In `@apps/desktop/src/renderer/lib/analytics/index.ts`:
- Around line 9-14: The renderer track call (getOutlit()?.track(...)) can throw
but isn't guarded like the main process variant; wrap the call inside a
try-catch in the renderer's track function (the function that invokes
getOutlit()?.track) and handle errors the same way as main process: catch any
thrown error, log it via the renderer logger (or console.error if no logger
exists) with a clear message and swallow the error so it doesn't propagate to UI
callers. Ensure you keep the same typed cast for properties and preserve
optional chaining while only adding the try-catch around the getOutlit()?.track
invocation.

In `@apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx`:
- Around line 22-30: The Outlit SDK is currently being sent user.email from
OutlitProvider (user.email in the user object), which may expose PII; change
this so email is only included when a clear config/consent flag is set (e.g.,
allowSendEmailToOutlit or hasAnalyticsConsent) — otherwise send only non-PII
fields like userId and traits.name (matching PostHogUserIdentifier handling), or
anonymize the email before sending; update the OutlitProvider code path that
constructs the user payload to check this flag/consent before including
user.email and add a unit/test to ensure email is omitted when the flag is
false.

In `@apps/marketing/src/lib/outlit/index.ts`:
- Around line 1-19: Add and export a shutdownOutlit() function that mirrors the
desktop pattern: check the module-level outlit variable and if it exists call
its shutdown()/flush() method to send queued events, await the call if it
returns a Promise, and then set outlit = null; ensure the exported function is
named shutdownOutlit and references the same outlit/Outlit symbols used in
getOutlit().

In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx`:
- Around line 21-29: The current OutlitProvider is sending personally
identifiable info (user.email and traits.name) to Outlit when populating the
user prop; update the implementation to only send email/name when the user has
given consent or when your privacy/data-processing agreements allow it—add a
consent check or feature flag around the user object construction inside
OutlitProvider (the block that references user.email, user.id, and traits.name)
and ensure your privacy policy and DPA with Outlit are in place and referenced
before enabling that branch.
🧹 Nitpick comments (4)
apps/desktop/src/main/lib/analytics/index.ts (1)

53-73: Outlit integration looks solid — error handling is properly in place.

Two minor observations:

  1. The "project_opened" literal on Line 67 could be extracted to a named constant alongside other event names to avoid typo risk across the codebase. As per coding guidelines: "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."

  2. The type cast on Lines 61-63 will silently drop nested objects or arrays from properties if any callers pass them. The same cast is used in the renderer and marketing apps, so this is consistent — but worth being aware of.

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

5-16: Wrap Outlit tracking in try/catch to prevent analytics from breaking the app.

If getOutlit()?.track() throws (e.g., network error, SDK bug), it will propagate up to the caller. The desktop main process version (apps/desktop/src/main/lib/analytics/index.ts, lines 55-72) wraps Outlit calls in try/catch with console.error, but this and the renderer variant don't. Analytics should be fire-and-forget — a failure here should never interrupt the user flow.

🛡️ Proposed fix
 export function track(
 	event: string,
 	properties?: Record<string, unknown>,
 ): void {
 	posthog.capture(event, properties);
-	getOutlit()?.track(
-		event,
-		properties as
-			| Record<string, string | number | boolean | null>
-			| undefined,
-	);
+	try {
+		getOutlit()?.track(
+			event,
+			properties as
+				| Record<string, string | number | boolean | null>
+				| undefined,
+		);
+	} catch (error) {
+		console.error("[analytics/outlit] Failed to track:", error);
+	}
 }

As per coding guidelines: "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly" and "Use prefixed console logging with consistent context pattern: [domain/operation] message".

apps/web/src/components/OutlitProvider/OutlitProvider.tsx (1)

13-33: Inconsistency with desktop's OutlitProvider pattern.

The desktop version uses getOutlit() to create a singleton client and passes it via the client prop, with a null guard that skips rendering the provider when the key is missing. This web version uses the publicKey prop directly and always renders the provider.

This isn't necessarily wrong (different apps, different constraints), but the lack of a null guard here means the provider initializes even when NEXT_PUBLIC_OUTLIT_KEY is unset — same concern as the marketing provider.

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

14-18: Consider guarding against missing publicKey to align with existing patterns.

The docs app guards against an empty publicKey with an early return, and desktop uses a getOutlit() helper that prevents Outlit initialization when the key is unset. Marketing's own lib/outlit/index.ts follows the same defensive pattern—returning null if the key is missing. However, providers.tsx passes an empty string directly to OutlitProvider without a guard.

For consistency with the patterns established across the codebase (docs, desktop, and marketing's own utility), consider guarding:

Proposed guard
 export function Providers({ children }: { children: React.ReactNode }) {
+	const outlitKey = env.NEXT_PUBLIC_OUTLIT_KEY;
 	return (
 		<PostHogProvider client={posthog}>
-			<OutlitProvider
-				publicKey={env.NEXT_PUBLIC_OUTLIT_KEY ?? ""}
-				trackPageviews
-				autoTrack={false}
-			>
+			{outlitKey ? (
+				<OutlitProvider
+					publicKey={outlitKey}
+					trackPageviews
+					autoTrack={false}
+				>
+					<ThemeProvider
+						attribute="class"
+						defaultTheme="dark"
+						forcedTheme="dark"
+						storageKey={THEME_STORAGE_KEY}
+						disableTransitionOnChange
+					>
+						{children}
+					</ThemeProvider>
+				</OutlitProvider>
+			) : (
 				<ThemeProvider
 					attribute="class"
 					defaultTheme="dark"
 					forcedTheme="dark"
 					storageKey={THEME_STORAGE_KEY}
 					disableTransitionOnChange
 				>
 					{children}
 				</ThemeProvider>
-			</OutlitProvider>
+			)}
 		</PostHogProvider>
 	);
 }

Comment thread apps/desktop/src/main/lib/outlit/index.ts
Comment thread apps/desktop/src/renderer/lib/analytics/index.ts
Comment on lines +22 to +30
user={
user
? {
email: user.email,
userId: user.id,
traits: { name: user.name },
}
: null
}
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

PII sent to third-party analytics — verify this is intended and compliant.

user.email is passed to the Outlit SDK as part of user identity. This is consistent with how PostHog receives it in PostHogUserIdentifier, but worth confirming that sending PII (email) to Outlit is covered by your privacy policy and data processing agreements.

🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx` around
lines 22 - 30, The Outlit SDK is currently being sent user.email from
OutlitProvider (user.email in the user object), which may expose PII; change
this so email is only included when a clear config/consent flag is set (e.g.,
allowSendEmailToOutlit or hasAnalyticsConsent) — otherwise send only non-PII
fields like userId and traits.name (matching PostHogUserIdentifier handling), or
anonymize the email before sending; update the OutlitProvider code path that
constructs the user payload to check this flag/consent before including
user.email and add a unit/test to ensure email is omitted when the flag is
false.

Comment thread apps/marketing/src/lib/outlit/index.ts
Comment on lines +21 to +29
user={
user
? {
email: user.email,
userId: user.id,
traits: { name: user.name },
}
: null
}
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

Verify that sending user.email to Outlit is compliant with your privacy policy.

The user's email and name are forwarded to a third-party analytics SDK. Ensure this is covered by your privacy policy / terms and that Outlit's data processing agreement is in place. This is especially relevant for GDPR/CCPA compliance.

🤖 Prompt for AI Agents
In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx` around lines 21 -
29, The current OutlitProvider is sending personally identifiable info
(user.email and traits.name) to Outlit when populating the user prop; update the
implementation to only send email/name when the user has given consent or when
your privacy/data-processing agreements allow it—add a consent check or feature
flag around the user object construction inside OutlitProvider (the block that
references user.email, user.id, and traits.name) and ensure your privacy policy
and DPA with Outlit are in place and referenced before enabling that branch.

@leo-paz leo-paz force-pushed the outlit-integration branch from 89c6b8d to ffb87fb Compare February 9, 2026 20:08
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: 2

🤖 Fix all issues with AI agents
In `@apps/marketing/src/app/providers.tsx`:
- Line 15: The OutlitProvider is receiving an empty string when
NEXT_PUBLIC_OUTLIT_KEY is unset, which can still trigger SDK initialization;
change the prop usage so you pass null when env.NEXT_PUBLIC_OUTLIT_KEY is falsy
(e.g. use env.NEXT_PUBLIC_OUTLIT_KEY ? env.NEXT_PUBLIC_OUTLIT_KEY : null) or
switch to the client prop pattern used by getOutlit() so OutlitProvider (or its
client) receives null when no key exists, ensuring the provider cleanly disables
itself; update the publicKey prop assignment (or replace with client) on
OutlitProvider to pass null instead of "" when the env var is undefined.
- Around line 14-18: OutlitProvider is creating a second Outlit instance with
trackPageviews enabled; instead, obtain the shared singleton via getOutlit()
(from lib/outlit/index.ts) and pass it into OutlitProvider using the client prop
(instead of relying on OutlitProvider to construct its own client), and remove
or disable trackPageviews/other duplicate config on the OutlitProvider call so
that all pageview tracking/configuration is centralized in getOutlit().
🧹 Nitpick comments (3)
apps/desktop/src/main/lib/analytics/index.ts (1)

55-69: Outlit receives un-enriched properties unlike PostHog.

PostHog properties are enriched with app_name, platform, and desktop_version (lines 46–51), but the Outlit call passes raw properties without these fields. If you intend parity between the two analytics sinks, pass the enriched object to Outlit as well.

Also, "project_opened" is a magic string — consider extracting it to a named constant. As per coding guidelines, hardcoded magic strings should be extracted to named constants at module top.

Proposed fix
+const ACTIVATION_EVENT = "project_opened" as const;
+
 export function track(
 	event: string,
 	properties?: Record<string, unknown>,
 ): void {
 	if (!userId) return;
 	if (!isTelemetryEnabled()) return;
 
 	const client = getClient();
 	if (!client) return;
 
+	const enrichedProperties = {
+		...properties,
+		app_name: "desktop",
+		platform: process.platform,
+		desktop_version: app.getVersion(),
+	};
+
 	client.capture({
 		distinctId: userId,
 		event,
-		properties: {
-			...properties,
-			app_name: "desktop",
-			platform: process.platform,
-			desktop_version: app.getVersion(),
-		},
+		properties: enrichedProperties,
 	});
 
 	// Outlit tracking
 	try {
 		const outlit = getOutlit();
 		if (outlit) {
 			outlit.track({
 				eventName: event,
 				userId,
-				properties: properties as
+				properties: enrichedProperties as
 					| Record<string, string | number | boolean | null>
 					| undefined,
 			});
 
-			if (event === "project_opened") {
+			if (event === ACTIVATION_EVENT) {
 				outlit.user.activate({ userId });
 			}
 		}
 	} catch (error) {
 		console.error("[analytics/outlit] Failed to track:", error);
 	}
 }
apps/marketing/src/lib/analytics/index.ts (2)

10-15: Unsafe as cast may silently pass incompatible property types to Outlit.

If callers pass nested objects or arrays in properties, the cast to Record<string, string | number | boolean | null> will mask the type mismatch at compile time, potentially causing silent data loss or SDK errors at runtime. This mirrors the desktop renderer pattern, so it's consistent — but consider a runtime filter for correctness if property shapes aren't guaranteed to be flat.


5-7: Two positional parameters — coding guidelines prefer object parameters.

The guideline says to use object parameters for functions with 2+ params. However, this matches the desktop renderer's track() signature exactly, so changing only the marketing app would introduce inconsistency. Worth aligning in a follow-up across all apps if desired. As per coding guidelines, "Use object parameters for functions with 2 or more parameters instead of positional arguments."

Comment on lines +14 to 18
<OutlitProvider
publicKey={env.NEXT_PUBLIC_OUTLIT_KEY ?? ""}
trackPageviews
autoTrack={false}
>
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

Potential duplicate pageview tracking — two Outlit instances with trackPageviews: true.

OutlitProvider here creates its own internal Outlit instance with trackPageviews enabled. Meanwhile, getOutlit() in lib/outlit/index.ts creates a separate singleton also with trackPageviews: true. This likely results in duplicate pageview events being sent to Outlit.

The desktop app avoids this by passing the shared singleton via the client prop to OutlitProvider. Consider the same approach here:

Proposed fix — share the singleton
+import { getOutlit } from "@/lib/outlit";
+
 <OutlitProvider
-  publicKey={env.NEXT_PUBLIC_OUTLIT_KEY ?? ""}
-  trackPageviews
-  autoTrack={false}
+  client={getOutlit()}
 >

Then ensure trackPageviews and other config remain only in getOutlit() to avoid conflicting options.

🤖 Prompt for AI Agents
In `@apps/marketing/src/app/providers.tsx` around lines 14 - 18, OutlitProvider is
creating a second Outlit instance with trackPageviews enabled; instead, obtain
the shared singleton via getOutlit() (from lib/outlit/index.ts) and pass it into
OutlitProvider using the client prop (instead of relying on OutlitProvider to
construct its own client), and remove or disable trackPageviews/other duplicate
config on the OutlitProvider call so that all pageview tracking/configuration is
centralized in getOutlit().

storageKey={THEME_STORAGE_KEY}
disableTransitionOnChange
<OutlitProvider
publicKey={env.NEXT_PUBLIC_OUTLIT_KEY ?? ""}
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

Passing an empty string as publicKey when the env var is unset may cause unintended SDK initialization.

When NEXT_PUBLIC_OUTLIT_KEY is undefined, this passes "" to OutlitProvider, which may still initialize the SDK and attempt network calls with an invalid key. The getOutlit() pattern correctly returns null when the key is absent. If you adopt the client prop approach above, passing null would cleanly disable the provider.

🤖 Prompt for AI Agents
In `@apps/marketing/src/app/providers.tsx` at line 15, The OutlitProvider is
receiving an empty string when NEXT_PUBLIC_OUTLIT_KEY is unset, which can still
trigger SDK initialization; change the prop usage so you pass null when
env.NEXT_PUBLIC_OUTLIT_KEY is falsy (e.g. use env.NEXT_PUBLIC_OUTLIT_KEY ?
env.NEXT_PUBLIC_OUTLIT_KEY : null) or switch to the client prop pattern used by
getOutlit() so OutlitProvider (or its client) receives null when no key exists,
ensuring the provider cleanly disables itself; update the publicKey prop
assignment (or replace with client) on OutlitProvider to pass null instead of ""
when the env var is undefined.

- Add NEXT_PUBLIC_OUTLIT_KEY to .env.example
- Add Outlit key to Vite define blocks (main + renderer)
- Fix UserIdentity type: move name to traits object
- Fix analytics wrapper type: cast properties for Outlit SDK
…der wrappers, and wire up consent controls

- Bump @outlit/browser from ^1.2.0 to ^1.4.0 across all apps
- Add OutlitProvider wrapper for desktop (passes singleton via client prop for shared instance + user identity)
- Add OutlitProvider wrapper for web (handles authenticated user identity)
- Wire up disableTracking() in desktop TelemetrySync and marketing CookieConsent
- Restore original comment in desktop main process
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/index.ts`:
- Line 161: shutdownOutlit() can reject (via outlit.shutdown()) and prevent
subsequent teardown (disposeTray() and app.exit(0)); wrap the call in a
try-catch (or try-finally) inside the shutdown sequence so any rejection is
caught and logged, then ensure disposeTray() is always called and app.exit(0)
always runs (use a finally block to guarantee disposal and exit even if
shutdownOutlit() throws).

In `@apps/desktop/src/main/lib/analytics/index.ts`:
- Around line 58-64: The current usage of outlit.track passes properties with an
unsafe cast (properties as Record<string, string | number | boolean | null>),
which can let nested objects/arrays through; update the call site that invokes
outlit.track to sanitize/validate the properties object (the local properties
variable) by iterating its keys and only keeping primitive
(string/number/boolean) or null values, converting non-primitive values to safe
string representations or omitting them, and then pass that sanitized map to
outlit.track (referencing outlit.track and the properties variable to locate the
change); ensure the resulting type matches Record<string, string | number |
boolean | null> without using an unconditional type assertion.

In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx`:
- Around line 17-33: The OutlitProvider always mounts OutlitBrowserProvider with
publicKey set to env.NEXT_PUBLIC_OUTLIT_KEY ?? "" which can pass an empty string
and cause SDK initialization/errors; change OutlitProvider to guard on
env.NEXT_PUBLIC_OUTLIT_KEY (or the same runtime check used by the desktop
getOutlit pattern) and only render OutlitBrowserProvider when the key is
present, otherwise return the passthrough children; update the conditional
around OutlitBrowserProvider (refer to OutlitBrowserProvider, OutlitProvider and
env.NEXT_PUBLIC_OUTLIT_KEY) so the SDK is not initialized with an empty
publicKey.
🧹 Nitpick comments (2)
apps/desktop/src/main/lib/analytics/index.ts (1)

67-69: Consider extracting "project_opened" to a named constant.

Per coding guidelines, hardcoded magic strings should be extracted to named constants at module top. This event name represents a domain concept (activation moment) and would benefit from being a shared constant, especially if it's referenced elsewhere.

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

10-15: Unsafe type assertion — non-primitive values will be silently forwarded.

Record<string, unknown> can contain nested objects, arrays, Date, etc. The as cast doesn't coerce values at runtime; it just silences the compiler. If Outlit's SDK truly only accepts string | number | boolean | null, consider filtering or coercing the property values instead of casting.

Proposed fix
-	getOutlit()?.track(
-		event,
-		properties as
-			| Record<string, string | number | boolean | null>
-			| undefined,
-	);
+	if (properties) {
+		const sanitized: Record<string, string | number | boolean | null> = {};
+		for (const [key, value] of Object.entries(properties)) {
+			if (
+				typeof value === "string" ||
+				typeof value === "number" ||
+				typeof value === "boolean" ||
+				value === null
+			) {
+				sanitized[key] = value;
+			}
+		}
+		getOutlit()?.track(event, sanitized);
+	} else {
+		getOutlit()?.track(event);
+	}

// Quit confirmed or no confirmation needed - exit immediately
// Let OS clean up child processes, tray, etc.
isQuitting = true;
await shutdownOutlit();
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

Unhandled rejection from shutdownOutlit() could prevent app.exit(0).

If outlit.shutdown() rejects (e.g., network timeout), the error propagates and skips disposeTray() + app.exit(0), leaving the app hung in a quitting state.

Proposed fix: wrap in try-catch
 	isQuitting = true;
-	await shutdownOutlit();
+	try {
+		await shutdownOutlit();
+	} catch (error) {
+		console.error("[main] Outlit shutdown failed:", error);
+	}
 	disposeTray();
 	app.exit(0);
📝 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 shutdownOutlit();
isQuitting = true;
try {
await shutdownOutlit();
} catch (error) {
console.error("[main] Outlit shutdown failed:", error);
}
disposeTray();
app.exit(0);
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/index.ts` at line 161, shutdownOutlit() can reject (via
outlit.shutdown()) and prevent subsequent teardown (disposeTray() and
app.exit(0)); wrap the call in a try-catch (or try-finally) inside the shutdown
sequence so any rejection is caught and logged, then ensure disposeTray() is
always called and app.exit(0) always runs (use a finally block to guarantee
disposal and exit even if shutdownOutlit() throws).

Comment on lines +58 to +64
outlit.track({
eventName: event,
userId,
properties: properties as
| Record<string, string | number | boolean | null>
| undefined,
});
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

Unsafe type assertion on properties.

properties is typed as Record<string, unknown>, which may contain nested objects, arrays, or other non-primitive values. The as cast silently bypasses this, potentially sending unsupported types to the Outlit SDK.

Consider filtering or validating the properties to only include primitive values:

Proposed fix
-			outlit.track({
-				eventName: event,
-				userId,
-				properties: properties as
-					| Record<string, string | number | boolean | null>
-					| undefined,
-			});
+			const safeProperties = properties
+				? Object.fromEntries(
+						Object.entries(properties).filter(
+							([, v]) =>
+								v === null ||
+								typeof v === "string" ||
+								typeof v === "number" ||
+								typeof v === "boolean",
+						),
+					)
+				: undefined;
+			outlit.track({
+				eventName: event,
+				userId,
+				properties: safeProperties as
+					| Record<string, string | number | boolean | null>
+					| undefined,
+			});
📝 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
outlit.track({
eventName: event,
userId,
properties: properties as
| Record<string, string | number | boolean | null>
| undefined,
});
const safeProperties = properties
? Object.fromEntries(
Object.entries(properties).filter(
([, v]) =>
v === null ||
typeof v === "string" ||
typeof v === "number" ||
typeof v === "boolean",
),
)
: undefined;
outlit.track({
eventName: event,
userId,
properties: safeProperties as
| Record<string, string | number | boolean | null>
| undefined,
});
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/analytics/index.ts` around lines 58 - 64, The
current usage of outlit.track passes properties with an unsafe cast (properties
as Record<string, string | number | boolean | null>), which can let nested
objects/arrays through; update the call site that invokes outlit.track to
sanitize/validate the properties object (the local properties variable) by
iterating its keys and only keeping primitive (string/number/boolean) or null
values, converting non-primitive values to safe string representations or
omitting them, and then pass that sanitized map to outlit.track (referencing
outlit.track and the properties variable to locate the change); ensure the
resulting type matches Record<string, string | number | boolean | null> without
using an unconditional type assertion.

Comment on lines +17 to +33
return (
<OutlitBrowserProvider
publicKey={env.NEXT_PUBLIC_OUTLIT_KEY ?? ""}
trackPageviews
user={
user
? {
email: user.email,
userId: user.id,
traits: { name: user.name },
}
: null
}
>
{children}
</OutlitBrowserProvider>
);
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

publicKey="" when key is unset — may cause unnecessary SDK initialization or errors.

Unlike the desktop OutlitProvider, which returns a passthrough when getOutlit() is null, this always renders OutlitBrowserProvider even when NEXT_PUBLIC_OUTLIT_KEY is unset (passing ""). This could lead to failed API calls or silent errors depending on how the Outlit SDK handles an empty key.

Consider guarding similarly to the desktop pattern:

Proposed fix
 export function OutlitProvider({ children }: OutlitProviderProps) {
 	const { data: session } = authClient.useSession();
 	const user = session?.user;
 
+	if (!env.NEXT_PUBLIC_OUTLIT_KEY) {
+		return <>{children}</>;
+	}
+
 	return (
 		<OutlitBrowserProvider
-			publicKey={env.NEXT_PUBLIC_OUTLIT_KEY ?? ""}
+			publicKey={env.NEXT_PUBLIC_OUTLIT_KEY}
 			trackPageviews
🤖 Prompt for AI Agents
In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx` around lines 17 -
33, The OutlitProvider always mounts OutlitBrowserProvider with publicKey set to
env.NEXT_PUBLIC_OUTLIT_KEY ?? "" which can pass an empty string and cause SDK
initialization/errors; change OutlitProvider to guard on
env.NEXT_PUBLIC_OUTLIT_KEY (or the same runtime check used by the desktop
getOutlit pattern) and only render OutlitBrowserProvider when the key is
present, otherwise return the passthrough children; update the conditional
around OutlitBrowserProvider (refer to OutlitBrowserProvider, OutlitProvider and
env.NEXT_PUBLIC_OUTLIT_KEY) so the SDK is not initialized with an empty
publicKey.

@Kitenite
Copy link
Copy Markdown
Collaborator

Closing: duplicate of #1357 which integrates the same Outlit analytics SDK.

@Kitenite Kitenite closed this Mar 13, 2026
@Kitenite
Copy link
Copy Markdown
Collaborator

Hey — just a heads up, this was closed as part of an automated stale PR cleanup. If you think this was done in error, feel free to reopen it!

@Kitenite
Copy link
Copy Markdown
Collaborator

Hey — this was closed by an automated cleanup of PRs with major merge conflicts that are 3+ weeks old. If you think this was done incorrectly, please feel free to reopen it. Sorry for any inconvenience!

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.

2 participants