Repository navigation
Conversation
- 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
…esktop - Use single Outlit instance in marketing (pass singleton as client to provider) so consent opt-out actually disables tracking - Ungate desktop main-process Outlit tracking from PostHog availability - Guard against empty publicKey in web/marketing (skip init when missing) - Add posthog.opt_in_capturing() to CookieConsent handleAccept for symmetry - Replace unsafe type casts with toOutlitProperties filter in all track wrappers
📝 WalkthroughWalkthroughThis pull request integrates Outlit Analytics as an alternative analytics provider alongside PostHog across the desktop, docs, marketing, and web applications. It adds environment configuration, dependencies, module initialization, and centralizes event tracking through abstraction layers that send events to both PostHog and Outlit simultaneously. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/index.ts`:
- Line 161: Wrap the call to shutdownOutlit() in a try/catch (or
try/catch/finally) so any rejection is caught and cannot block the process; call
app.exit(0) in the finally (or after the catch) to guarantee the app always
exits. Specifically update the code that invokes shutdownOutlit() and
app.exit(0) so failures from shutdownOutlit() are logged (or ignored) but do not
prevent app.exit(0) from running.
In `@apps/marketing/src/components/CookieConsent/CookieConsent.tsx`:
- Line 3: CookieConsent currently calls useOutlit(), which will throw if
OutlitBrowserProvider (rendered by OutlitWrapper when NEXT_PUBLIC_OUTLIT_KEY
exists) is not mounted; change CookieConsent to avoid calling useOutlit
unguarded by either (a) import and use the singleton API from `@outlit/browser`
(e.g., track, user) instead of the hook so it works without a provider, or (b)
wrap the useOutlit() call in a try/catch and fall back to no-op functions when
the hook throws; locate useOutlit usage in CookieConsent.tsx and implement one
of these safe fallbacks so the component no longer throws when OutlitWrapper
skips the provider.
In `@apps/marketing/src/lib/analytics/index.ts`:
- Around line 23-29: The track function currently calls posthog.capture and then
getOutlit()?.track so if posthog.capture throws the Outlit call is skipped; fix
by invoking each provider in its own try/catch: wrap posthog.capture(event,
properties) in a try block and log any error (e.g., processLogger.error or
console.error) without rethrowing, then separately call
getOutlit()?.track(event, toOutlitProperties(properties)) inside its own
try/catch and likewise log errors; reference the existing function name track
and the calls posthog.capture and getOutlit()?.track (and toOutlitProperties)
when making the changes.
In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx`:
- Around line 25-31: OutlitProvider is currently forwarding user.email into the
Outlit analytics payload (user -> email) which sends PII to a third party;
change the component so it does not forward raw email by default—either remove
the email field, send a hashed/email-hash instead, or gate sending the email
behind an explicit consent flag (e.g., check user.hasConsentedToAnalytics or a
feature flag) before populating user.email; update the OutlitProvider logic
where the user object is constructed (the user prop/traits block) to implement
the chosen option and ensure any relevant tests and privacy docs are updated
accordingly.
- Around line 13-37: OutlitProvider currently passes publicKey and reads env
directly, which creates a separate internal Outlit instance; change it to use
the shared singleton by creating a getOutlit() in
apps/web/src/lib/outlit/index.ts (mirroring desktop/marketing), then update
OutlitProvider to import getOutlit(), call const client = getOutlit(), return
children when !client, and pass client={client} to OutlitBrowserProvider instead
of publicKey; also remove the env import/check and pick the appropriate tracking
options (e.g., trackPageviews/trackForms/autoTrack) consistent with other apps.
🧹 Nitpick comments (7)
apps/desktop/src/renderer/components/TelemetrySync/TelemetrySync.tsx (1)
10-26: Consider wrapping in try/catch for defensive error handling.The previous version had try/catch around these SDK calls. While the
typeofguards and optional chaining handle null cases, an unexpected SDK error (e.g., network failure during opt-in) would propagate unhandled and could break the effect. A lightweight try/catch with a logged warning would be more resilient.🛡️ Suggested defensive wrapper
useEffect(() => { if (telemetryEnabled === undefined) return; + try { const outlit = getOutlit(); if (telemetryEnabled) { if (typeof posthog?.opt_in_capturing === "function") { posthog.opt_in_capturing(); } outlit?.enableTracking(); } else { if (typeof posthog?.opt_out_capturing === "function") { posthog.opt_out_capturing(); } outlit?.disableTracking(); } + } catch (error) { + console.warn("[telemetry/sync] Failed to update tracking state", error); + } }, [telemetryEnabled]);apps/marketing/src/lib/analytics/index.ts (1)
5-21:toOutlitPropertiesis duplicated across apps.This helper is identical to the one in
apps/desktop/src/renderer/lib/analytics/index.ts. Consider extracting it into a shared package (e.g.,packages/sharedor a newpackages/analytics) to avoid drift between the two copies.apps/desktop/src/main/lib/outlit/index.ts (1)
17-22: Consider wrappingshutdown()in a try/catch with logging.If
outlit.shutdown()rejects (e.g., network timeout flushing events), the error propagates to thebefore-quithandler. A failed analytics shutdown shouldn't prevent app exit.Suggested improvement
export async function shutdownOutlit(): Promise<void> { if (outlit) { - await outlit.shutdown(); - outlit = null; + try { + await outlit.shutdown(); + } catch (error) { + console.error("[outlit/shutdown] Failed to shutdown Outlit", error); + } finally { + outlit = null; + } } }apps/desktop/src/renderer/lib/analytics/index.ts (1)
22-27: Missing error handling around Outlit tracking, unlike the main-process counterpart.The main-process
track()inapps/desktop/src/main/lib/analytics/index.tswraps Outlit calls in atry/catchwith aconsole.errorlog. This renderer version silently lets any Outlit exception propagate and potentially crash the caller.Proposed fix
export function track( event: string, properties?: Record<string, unknown>, ): void { - posthog.capture(event, properties); - getOutlit()?.track(event, toOutlitProperties(properties)); + posthog?.capture(event, properties); + try { + getOutlit()?.track(event, toOutlitProperties(properties)); + } catch (error) { + console.error("[analytics/outlit] Failed to track:", error); + } }apps/desktop/src/renderer/index.tsx (1)
35-42: Pageview tracking bypasses the centralizedtrack()helper — consider extracting a shared pattern.The inline
getOutlit()?.track(...)alongside the directposthog.capture(...)is correct (usingtrack()would double-post to PostHog), but it diverges from the unifiedtrack()pattern used everywhere else. If more event-specific overrides emerge, this will be hard to maintain.Consider adding a dedicated
trackPageview()torenderer/lib/analyticsthat encapsulates both calls, keeping the routing logic centralized. Also, wrapping the Outlit call in a try/catch would match the main-process convention.apps/desktop/src/main/lib/analytics/index.ts (2)
76-80: Outlit events lack the enrichment properties sent to PostHog.PostHog receives
app_name,platform, anddesktop_versionin its properties (lines 63-68), but the Outlit call only forwards the raw user-supplied properties. This will make cross-provider analysis inconsistent.If the omission is intentional (e.g., Outlit captures these via its own mechanism), a brief comment would help future readers. Otherwise, enrich the Outlit payload the same way:
♻️ Suggested diff
outlit.track({ eventName: event, userId, - properties: toOutlitProperties(properties), + properties: toOutlitProperties({ + ...properties, + app_name: "desktop", + platform: process.platform, + desktop_version: app.getVersion(), + }), });
83-83: Extract"project_opened"to a named constant.This magic string is used as a behavioural trigger for
user.activate(). A typo or drift between this literal and the caller would silently break activation tracking. Extracting it (and the"desktop"string on line 65) to module-level constants improves maintainability. As per coding guidelines: "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."♻️ Suggested diff
+const PROJECT_OPENED_EVENT = "project_opened"; +const APP_NAME = "desktop"; + export function track( event: string, properties?: Record<string, unknown>, ): void { ... properties: { ...properties, - app_name: "desktop", + app_name: APP_NAME, ... }, ... - if (event === "project_opened") { + if (event === PROJECT_OPENED_EVENT) {
| // Quit confirmed or no confirmation needed - exit immediately | ||
| // Let OS clean up child processes, tray, etc. | ||
| isQuitting = true; | ||
| await shutdownOutlit(); |
There was a problem hiding this comment.
Wrap shutdownOutlit() in try/catch to prevent quit from hanging.
If shutdownOutlit() rejects, app.exit(0) on Line 163 will never execute, potentially leaving the app in a stuck state. Analytics teardown should never block app exit.
🛡️ Proposed fix
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.
| 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, Wrap the call to
shutdownOutlit() in a try/catch (or try/catch/finally) so any rejection is
caught and cannot block the process; call app.exit(0) in the finally (or after
the catch) to guarantee the app always exits. Specifically update the code that
invokes shutdownOutlit() and app.exit(0) so failures from shutdownOutlit() are
logged (or ignored) but do not prevent app.exit(0) from running.
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
|
|
|||
| import { useOutlit } from "@outlit/browser/react"; | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the CookieConsent component
find . -name "CookieConsent.tsx" -type fRepository: superset-sh/superset
Length of output: 127
🏁 Script executed:
# Find and examine the providers.tsx file to understand the OutlitWrapper
find . -name "providers.tsx" -type f | grep -E "(marketing|app)"Repository: superset-sh/superset
Length of output: 204
🏁 Script executed:
# Check if `@outlit` package is in dependencies
find . -name "package.json" -type f | head -5 | xargs grep -l "@outlit"Repository: superset-sh/superset
Length of output: 116
🏁 Script executed:
# Read CookieConsent.tsx to see the import and usage
cat -n ./apps/marketing/src/components/CookieConsent/CookieConsent.tsxRepository: superset-sh/superset
Length of output: 2475
🏁 Script executed:
# Read marketing app providers.tsx to see OutlitWrapper implementation
cat -n ./apps/marketing/src/app/providers.tsxRepository: superset-sh/superset
Length of output: 1297
🏁 Script executed:
# Find the getOutlit implementation
find . -path "*/lib/outlit*" -type fRepository: superset-sh/superset
Length of output: 196
🏁 Script executed:
# Check what getOutlit returns and when
rg "getOutlit|NEXT_PUBLIC_OUTLIT" apps/marketing/src/lib -A 3 -B 1Repository: superset-sh/superset
Length of output: 1249
🏁 Script executed:
# Read the full getOutlit implementation
cat -n ./apps/marketing/src/lib/outlit/index.tsRepository: superset-sh/superset
Length of output: 573
🏁 Script executed:
# Search for other useOutlit usage patterns to see if guarded
rg "useOutlit" apps/marketing/src -B 2 -A 2Repository: superset-sh/superset
Length of output: 1003
🏁 Script executed:
# Check if `@outlit` types are available locally (node_modules or type definitions)
find . -name "package.json" -path "*/apps/marketing/*" -exec cat {} \; | grep -A 10 -B 10 "@outlit" | head -20Repository: superset-sh/superset
Length of output: 636
🏁 Script executed:
# Search for any try/catch or error handling around useOutlit or in CookieConsent usage
rg "CookieConsent|useOutlit" apps/marketing/src -B 5 -A 5 | head -50Repository: superset-sh/superset
Length of output: 3238
🏁 Script executed:
# Check if `@outlit` types/definitions are in node_modules or installed
find . -path "*/node_modules/@outlit/browser*" -name "*.d.ts" -o -name "*.ts" | grep -E "(index|react)" | head -10Repository: superset-sh/superset
Length of output: 792
🌐 Web query:
@outlit/browser react useOutlit hook throws missing provider context error
💡 Result:
That error happens when useOutlit() is called outside of the React context created by OutlitProvider (i.e., the component using the hook isn’t wrapped by the provider). The Outlit SDK’s React usage expects this wrapper. [1]
Fix (React): wrap your app (or at least the subtree that calls useOutlit) with OutlitProvider from @outlit/browser/react:
import { OutlitProvider, useOutlit } from "@outlit/browser/react";
function App() {
return (
<OutlitProvider publicKey="pk_xxx">
<MyComponent />
</OutlitProvider>
);
}
function MyComponent() {
const { track, user } = useOutlit();
// ...
}[1]
If you don’t want a provider: use the singleton API (init, track, user, customer) instead of useOutlit(). [1]
Common gotcha (Next.js App Router): make sure the provider is in a Client Component (add "use client" at the top) and that the component using useOutlit() is also rendered under that client-side provider.
Source
[1] OutlitAI/outlit-sdk README (React + singleton usage examples) (github.com)
Citations:
useOutlit() will throw if OutlitBrowserProvider is not in the tree.
useOutlit() requires the context provider to be present. When NEXT_PUBLIC_OUTLIT_KEY is absent, OutlitWrapper skips rendering OutlitBrowserProvider (providers.tsx, line 14), causing the hook to throw a context error at runtime. Guard the destructuring with a try/catch, or use the singleton API (track, user) from @outlit/browser instead of the hook if Outlit may not be initialized.
Also applies to: 13-13
🤖 Prompt for AI Agents
In `@apps/marketing/src/components/CookieConsent/CookieConsent.tsx` at line 3,
CookieConsent currently calls useOutlit(), which will throw if
OutlitBrowserProvider (rendered by OutlitWrapper when NEXT_PUBLIC_OUTLIT_KEY
exists) is not mounted; change CookieConsent to avoid calling useOutlit
unguarded by either (a) import and use the singleton API from `@outlit/browser`
(e.g., track, user) instead of the hook so it works without a provider, or (b)
wrap the useOutlit() call in a try/catch and fall back to no-op functions when
the hook throws; locate useOutlit usage in CookieConsent.tsx and implement one
of these safe fallbacks so the component no longer throws when OutlitWrapper
skips the provider.
| export function track( | ||
| event: string, | ||
| properties?: Record<string, unknown>, | ||
| ): void { | ||
| posthog.capture(event, properties); | ||
| getOutlit()?.track(event, toOutlitProperties(properties)); | ||
| } |
There was a problem hiding this comment.
An exception from posthog.capture would silently skip the Outlit call.
If posthog.capture throws (e.g., due to an uninitialized client or unexpected input), the Outlit track call on Line 28 is never reached. Wrapping each call independently would ensure one provider's failure doesn't block the other.
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 (e) {
+ console.error("[analytics/posthog] capture failed", e);
+ }
+ try {
+ getOutlit()?.track(event, toOutlitProperties(properties));
+ } catch (e) {
+ console.error("[analytics/outlit] track failed", e);
+ }
}🤖 Prompt for AI Agents
In `@apps/marketing/src/lib/analytics/index.ts` around lines 23 - 29, The track
function currently calls posthog.capture and then getOutlit()?.track so if
posthog.capture throws the Outlit call is skipped; fix by invoking each provider
in its own try/catch: wrap posthog.capture(event, properties) in a try block and
log any error (e.g., processLogger.error or console.error) without rethrowing,
then separately call getOutlit()?.track(event, toOutlitProperties(properties))
inside its own try/catch and likewise log errors; reference the existing
function name track and the calls posthog.capture and getOutlit()?.track (and
toOutlitProperties) when making the changes.
| 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} | ||
| trackPageviews | ||
| user={ | ||
| user | ||
| ? { | ||
| email: user.email, | ||
| userId: user.id, | ||
| traits: { name: user.name }, | ||
| } | ||
| : null | ||
| } | ||
| > | ||
| {children} | ||
| </OutlitBrowserProvider> | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== Search for outlit files in web app ==="
fd -t f "outlit" --full-path "apps/web/src" 2>/dev/null || echo "fd search completed"
echo -e "\n=== Search for getOutlit calls in web app ==="
rg "getOutlit" apps/web/src/ 2>/dev/null || echo "No getOutlit found"
echo -e "\n=== Search for track calls in web analytics ==="
rg "track\(" apps/web/src/lib/ 2>/dev/null | head -20 || echo "No track calls found"
echo -e "\n=== Search for Outlit in entire web app ==="
rg "Outlit" apps/web/src/ 2>/dev/null | head -30
echo -e "\n=== Check desktop OutlitProvider pattern ==="
find apps/desktop -name "*OutlitProvider*" -type f 2>/dev/null | head -5Repository: superset-sh/superset
Length of output: 1306
🏁 Script executed:
#!/bin/bash
echo "=== Desktop OutlitProvider content ==="
cat apps/desktop/src/renderer/providers/OutlitProvider/OutlitProvider.tsx
echo -e "\n=== Search for getOutlit in entire codebase ==="
rg "getOutlit" . 2>/dev/null | head -20
echo -e "\n=== Search for Outlit singleton/initialization ==="
rg "new Outlit\|createOutlit\|initOutlit" . 2>/dev/null | head -20
echo -e "\n=== Check for outlit lib/util files ==="
fd -t f "outlit" --full-path "apps" 2>/dev/nullRepository: superset-sh/superset
Length of output: 2939
🏁 Script executed:
#!/bin/bash
echo "=== Desktop getOutlit() implementation ==="
cat apps/desktop/src/renderer/lib/outlit/index.ts
echo -e "\n=== Check if web/src/lib/outlit exists ==="
ls -la apps/web/src/lib/outlit 2>/dev/null || echo "Does not exist"
echo -e "\n=== Check if web app has any analytics setup ==="
find apps/web/src/lib -name "*analytic*" -o -name "*track*" 2>/dev/null
echo -e "\n=== Check web app's env.ts for OUTLIT references ==="
rg "OUTLIT" apps/web/src/ 2>/dev/null | head -10
echo -e "\n=== Check if web app's OutlitProvider is the only Outlit usage ==="
rg "Outlit" apps/web/src/ 2>/dev/nullRepository: superset-sh/superset
Length of output: 1871
🏁 Script executed:
#!/bin/bash
echo "=== Marketing getOutlit() implementation ==="
cat apps/marketing/src/lib/outlit/index.ts
echo -e "\n=== Check OutlitBrowserProvider API documentation or type hints ==="
rg -A 5 "OutlitBrowserProvider" apps/marketing/src/app/providers.tsx 2>/dev/null | head -20Repository: superset-sh/superset
Length of output: 1000
Web OutlitProvider uses a different pattern than desktop/marketing — should align with the singleton approach.
The PR objective requires: "Single Outlit instance per app is used (getOutlit() singleton passed as client to React providers)." Desktop and marketing both follow this by using getOutlit() singleton with client={getOutlit()}, but this web provider passes publicKey directly instead.
While the web app currently has no track() calls, this inconsistency violates the "follow existing patterns" guideline and will cause problems when analytics tracking is added later. The OutlitBrowserProvider will create its own instance internally instead of using the shared singleton, leading to separate Outlit instances with divergent consent/identity state.
Align with the established pattern by:
- Creating
apps/web/src/lib/outlit/index.tswith a getOutlit() singleton (similar to desktop/marketing) - Passing
client={getOutlit()}instead ofpublicKeyprop - Removing the env import and checking
if (!client)instead
Note: The marketing app uses trackPageviews: true, trackForms: true, autoTrack: false while desktop uses trackPageviews: false, autoTrack: true — determine which configuration fits the web app's analytics needs and apply consistently.
🤖 Prompt for AI Agents
In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx` around lines 13 -
37, OutlitProvider currently passes publicKey and reads env directly, which
creates a separate internal Outlit instance; change it to use the shared
singleton by creating a getOutlit() in apps/web/src/lib/outlit/index.ts
(mirroring desktop/marketing), then update OutlitProvider to import getOutlit(),
call const client = getOutlit(), return children when !client, and pass
client={client} to OutlitBrowserProvider instead of publicKey; also remove the
env import/check and pick the appropriate tracking options (e.g.,
trackPageviews/trackForms/autoTrack) consistent with other apps.
| user={ | ||
| user | ||
| ? { | ||
| email: user.email, | ||
| userId: user.id, | ||
| traits: { name: user.name }, | ||
| } |
There was a problem hiding this comment.
User email is sent to a third-party analytics provider.
user.email is forwarded to Outlit on Line 28. While this is consistent with the desktop provider, sending PII (email) to a third-party analytics service has GDPR/CCPA implications. Confirm this is intentional and covered by your privacy policy and data processing agreements with Outlit.
🤖 Prompt for AI Agents
In `@apps/web/src/components/OutlitProvider/OutlitProvider.tsx` around lines 25 -
31, OutlitProvider is currently forwarding user.email into the Outlit analytics
payload (user -> email) which sends PII to a third party; change the component
so it does not forward raw email by default—either remove the email field, send
a hashed/email-hash instead, or gate sending the email behind an explicit
consent flag (e.g., check user.hasConsentedToAnalytics or a feature flag) before
populating user.email; update the OutlitProvider logic where the user object is
constructed (the user prop/traits block) to implement the chosen option and
ensure any relevant tests and privacy docs are updated accordingly.
|
Closing as stale: created in Jan-Mar with no recent activity. If still relevant, re-open or re-create from a fresh branch. Bulk audit 2026-05-06. |
Summary
@outlit/browser+@outlit/node) to marketing, web, docs, and desktop appstrack()wrappersuser.activate()onproject_openedeventsKey design decisions
getOutlit()) passed asclientto the React provider, ensuring consent toggles andtrack()calls operate on the same instanceNEXT_PUBLIC_OUTLIT_KEYis absent (no empty-string fallback)toOutlitProperties()filters analytics properties to primitives (string | number | boolean | null) instead of using unsafe type castsApps changed
apps/marketing@outlit/browserapps/web@outlit/browserapps/docs@outlit/browserapps/desktop(renderer)@outlit/browserapps/desktop(main)@outlit/nodeuser.activate()on project openTest plan
NEXT_PUBLIC_OUTLIT_KEYis setNEXT_PUBLIC_OUTLIT_KEYis unsetSummary by CodeRabbit
Release Notes
New Features
Chores