Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dist
cdk.out
apps/gateway/target

# Local data (prevent leaking local PGlite, CA certs, secrets)
# Local data (prevent leaking CA certs, secrets)
data
apps/web/data

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ yarn-error.log*
# Prisma
packages/db/prisma/*.db

# PGlite local data
# Local data
data/

# CDK
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ All environment variables are optional for local development:
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | — |
| `SECRET_ENCRYPTION_KEY` | AES-256-GCM encryption key | Auto-generated |

## Telemetry

OneCLI collects anonymous install and update events (version, architecture, edition only). No personal data is collected. Disable with `DO_NOT_TRACK=1` or in **Settings → General**. [Full details](https://onecli.sh/docs/reference/telemetry).

## Contributing

We welcome contributions! Please read our [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) before getting started.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";

import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@onecli/ui/components/card";
import { Switch } from "@onecli/ui/components/switch";
import { Label } from "@onecli/ui/components/label";
import { Skeleton } from "@onecli/ui/components/skeleton";
import { toast } from "sonner";
import {
getTelemetryStatus,
updateTelemetryPreference,
} from "@/lib/actions/telemetry";

export const TelemetryToggle = () => {
const [enabled, setEnabled] = useState(false);
const [forcedByEnv, setForcedByEnv] = useState(false);
const [loading, setLoading] = useState(true);

useEffect(() => {
getTelemetryStatus().then((status) => {
setEnabled(status.enabled);
setForcedByEnv(status.forcedByEnv);
setLoading(false);
});
}, []);

const handleToggle = async (checked: boolean) => {
setEnabled(checked);
try {
await updateTelemetryPreference(checked);
toast.success(checked ? "Telemetry enabled" : "Telemetry disabled");
} catch {
setEnabled(!checked);
toast.error("Failed to update telemetry preference");
}
};

if (loading) {
return (
<Card>
<CardHeader>
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent>
<Skeleton className="h-6 w-10" />
</CardContent>
</Card>
);
}

return (
<Card>
<CardHeader>
<CardTitle>Anonymous Telemetry</CardTitle>
<CardDescription>
Help improve OneCLI by sending anonymous install and update events.
Only version, architecture, and edition are collected. No personal
data or hostnames are ever sent.{" "}
<a
href="https://onecli.sh/docs/reference/telemetry"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Learn more
</a>
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<Switch
id="telemetry"
checked={enabled}
onCheckedChange={handleToggle}
disabled={forcedByEnv}
/>
<Label htmlFor="telemetry">{enabled ? "Enabled" : "Disabled"}</Label>
</div>
{forcedByEnv && (
<p className="text-muted-foreground mt-2 text-xs">
Telemetry is disabled by the <code>DO_NOT_TRACK</code> environment
variable and cannot be changed here.
</p>
)}
</CardContent>
</Card>
);
};
16 changes: 16 additions & 0 deletions apps/web/src/app/(dashboard)/settings/general/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import { PageHeader } from "@dashboard/page-header";
import { TelemetryToggle } from "./_components/telemetry-toggle";

export const metadata: Metadata = {
title: "General",
};

export default function GeneralSettingsPage() {
return (
<div className="flex flex-1 flex-col gap-4 max-w-5xl">
<PageHeader title="General" description="Instance-wide preferences." />
<TelemetryToggle />
</div>
);
}
24 changes: 24 additions & 0 deletions apps/web/src/lib/actions/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use server";

import {
isTelemetryEnabled,
isTelemetryForcedByEnv,
setTelemetryPreference,
} from "@/lib/telemetry";

export const getTelemetryStatus = async () => {
return {
enabled: isTelemetryEnabled(),
forcedByEnv: isTelemetryForcedByEnv(),
};
};

export const updateTelemetryPreference = async (enabled: boolean) => {
if (isTelemetryForcedByEnv()) {
throw new Error(
"Telemetry is disabled by environment variable and cannot be changed from the dashboard.",
);
}
setTelemetryPreference(enabled);
return { enabled };
};
6 changes: 5 additions & 1 deletion apps/web/src/lib/settings-nav-items.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User, KeyRound, ShieldCheck } from "lucide-react";
import { User, KeyRound, ShieldCheck, Settings } from "lucide-react";

export interface SettingsNavItem {
title: string;
Expand All @@ -25,4 +25,8 @@ export const settingsSections: SettingsNavSection[] = [
{ title: "Encryption", url: "/settings/encryption", icon: ShieldCheck },
],
},
{
label: "Instance",
items: [{ title: "General", url: "/settings/general", icon: Settings }],
},
];
43 changes: 43 additions & 0 deletions apps/web/src/lib/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { dirname, join } from "path";
import { homedir } from "os";

/**
* Preference file path — Docker uses /app/data/, local dev falls back to ~/.onecli/.
*/
const PREF_FILE = existsSync("/app/data")
? "/app/data/telemetry-preference"
: join(homedir(), ".onecli", "telemetry-preference");

/**
* Reads the telemetry preference. Priority:
* 1. DO_NOT_TRACK=1 env var → disabled
* 2. Preference file → "on" or "off"
* 3. Default → enabled
*/
export const isTelemetryEnabled = (): boolean => {
if (process.env.DO_NOT_TRACK === "1") return false;

try {
return readFileSync(PREF_FILE, "utf-8").trim() !== "off";
} catch {
return true;
}
};

/**
* Whether the preference is forced by an environment variable
* (cannot be changed from the dashboard).
*/
export const isTelemetryForcedByEnv = (): boolean => {
return process.env.DO_NOT_TRACK === "1";
};

/**
* Persists the telemetry preference to disk.
* In Docker, entrypoint.sh reads this file on next container start.
*/
export const setTelemetryPreference = (enabled: boolean): void => {
mkdirSync(dirname(PREF_FILE), { recursive: true });
writeFileSync(PREF_FILE, enabled ? "on" : "off", "utf-8");
};
2 changes: 1 addition & 1 deletion assets/onecli-architecture-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion assets/onecli-architecture-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,33 @@ if [ "$AUTH_MODE" = "cloud" ] || [ -n "$GOOGLE_CLIENT_ID" ]; then
fi
printf '{"authMode":"%s","oauthConfigured":%s}\n' "$AUTH_MODE" "$OAUTH_CONFIGURED" > /app/data/runtime-config.json

# Anonymous telemetry — install/update events only.
# Disable: DO_NOT_TRACK=1 or Settings → General. Docs: https://onecli.sh/docs/reference/telemetry
ONECLI_VERSION=$(node -p "require('./apps/web/package.json').version" 2>/dev/null || echo "unknown")
VERSION_FILE="/app/data/.onecli-version"
PREF_FILE="/app/data/telemetry-preference"
PREV_VERSION=$(cat "$VERSION_FILE" 2>/dev/null || echo "")

send_telemetry() {
UUID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || od -x /dev/urandom | head -1 | awk '{print $2$3"-"$4"-"$5"-"$6"-"$7$8$9}')
PROPS="\"version\":\"$ONECLI_VERSION\",\"edition\":\"${NEXT_PUBLIC_EDITION:-oss}\",\"auth_mode\":\"$AUTH_MODE\",\"arch\":\"$(uname -m)\""
[ -n "$2" ] && PROPS="$PROPS,\"old_version\":\"$2\""
curl -sf --max-time 5 -X POST https://t.1cli.sh/capture/ \
-H 'Content-Type: application/json' \
-d "{\"api_key\":\"phc_lXPPe71vyF7OWTxnMBhVPIQtdRLkvpZQ9ve7NhANxLN\",\"event\":\"$1\",\"distinct_id\":\"$UUID\",\"properties\":{$PROPS}}" \
> /dev/null 2>&1 &
echo "Telemetry: $1 (anonymous). Disable: DO_NOT_TRACK=1"
}

echo "$ONECLI_VERSION" > "$VERSION_FILE"
if [ "${DO_NOT_TRACK:-}" != "1" ] && { [ ! -f "$PREF_FILE" ] || [ "$(cat "$PREF_FILE")" != "off" ]; }; then
if [ -z "$PREV_VERSION" ]; then
send_telemetry "install_complete"
elif [ "$PREV_VERSION" != "$ONECLI_VERSION" ]; then
send_telemetry "update_complete" "$PREV_VERSION"
fi
fi

# Start gateway in background
echo "Starting gateway on port ${GATEWAY_PORT:-10255}..."
onecli-gateway --port "${GATEWAY_PORT:-10255}" --data-dir /app/data &
Expand Down
3 changes: 2 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"AUTH_MODE",
"MAILERCHECK_API_KEY",
"DISCORD_WEBHOOK_URL",
"ENVIRONMENT"
"ENVIRONMENT",
"DO_NOT_TRACK"
],
"tasks": {
"build": {
Expand Down
Loading