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
5 changes: 5 additions & 0 deletions .changeset/cloud-anonymous-analytics-optout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roomote/web': patch
---

Allow Cloud administrators to disable anonymous Roomote telemetry, preserve their choice through setup, and drop queued events after opt-out.
2 changes: 1 addition & 1 deletion apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ as per-task auth tokens or workspace paths.
| `R_API_SHUTDOWN_DRAIN_MS` | Optional | Milliseconds the API allows in-flight session turns to finish during shutdown before aborting the remainder so they resume on the next process. Defaults to `20000`; set to `0` to abort immediately. Keep it below the hosting platform's SIGTERM-to-SIGKILL grace period. Also the fallback for `R_BULLMQ_SHUTDOWN_DRAIN_MS`. |
| `R_BULLMQ_SHUTDOWN_DRAIN_MS` | Optional | Same window for the bullmq service, which executes the session turns the queue resumes. Defaults to `R_API_SHUTDOWN_DRAIN_MS`, then `20000`. |
| `R_FAST_DURABLE_RETRY_DISABLED` | Optional | Set to `true` to keep inference retry waits in the current process instead of parking in-flight session turns durably. Durable admission remains enabled. |
| `R_CLOUD_ENABLED` | Roomote Cloud only | Deployment-managed switch for Roomote Cloud behavior, including required anonymous analytics and Cloud support integrations. Do not set this for self-hosted deployments. |
| `R_CLOUD_ENABLED` | Roomote Cloud only | Deployment-managed switch for Roomote Cloud support and product-experience integrations. Anonymous Roomote telemetry remains controlled by **Settings > Deployment > Privacy**. Do not set this for self-hosted deployments. |
| `R_CURATED_INTEGRATIONS_DISABLED` | Optional | Operator policy for the curated **Integrations** catalog, which is enabled by default. Set to `true` and restart Roomote to prevent those integrations from being configured or used. Existing connections remain stored while disabled and become available again once the value is unset. Communications, source-control, inference, sandbox providers, and environment-defined MCP servers are unaffected. |
| `R_GITHUB_APP_SLUG` | GitHub setup | Primary GitHub App slug used by server-rendered setup, mentions, and GitHub integration flows. |
| `R_GITHUB_ADDITIONAL_APP_SLUGS` | Optional GitHub setup | Comma-separated additional GitHub App slugs that Roomote should trust as managed bot identities for review attribution and notifications. |
Expand Down
26 changes: 13 additions & 13 deletions apps/docs/privacy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ description: Understand Roomote data handling, anonymous telemetry, and the cont

Roomote keeps the data boundaries for a deployment explicit. Product content,
credentials, prompts, code, and repository names are not part of the default
anonymous telemetry payload. Deployment administrators control whether a
self-hosted deployment shares that telemetry, while Roomote Cloud follows its
own privacy policy.
anonymous telemetry payload. Deployment administrators control whether
self-hosted and Roomote Cloud deployments share that telemetry.

This page covers the telemetry payload, its identifiers, the controls that turn
it off, and the separate version-availability check. It does not replace the
Expand All @@ -30,7 +29,7 @@ integration providers you connect.
## Anonymous telemetry

Roomote can share **anonymous telemetry** with the Roomote team to help improve
the product. This is on by default for self-hosted deployments, controlled
the product. This is on by default for self-hosted and Roomote Cloud deployments, controlled
entirely by deployment admins, and designed so that Roomote-generated
identifiers and the default payload cannot identify your company, users, code,
or repositories.
Expand Down Expand Up @@ -82,19 +81,20 @@ sends the configured value with telemetry and version checks.

Anonymous telemetry is opt-out:

- during setup, the final step includes an **Anonymous analytics** switch
- afterwards, admins can change it any time in **Settings > Deployment**
- admins can change it any time in **Settings > Deployment > Privacy**
- turn off **Anonymous analytics** to opt the deployment out

The setting applies to everyone in the deployment. When disabled, no
analytics code runs in the browser and no usage events or daily reports
leave your servers.
The setting applies to everyone in the deployment. When disabled, anonymous
usage events and daily reports are not sent, and unsent event batches are
dropped. Other open pages receive the updated browser setting when refreshed;
the server also enforces the choice on incoming telemetry and before delivery.

## Roomote Cloud Specifics

Roomote Cloud deployments always keep Roomote anonymous analytics enabled and
do not show this switch, as described in our privacy policy. Cookie consent
controls only optional support and product-experience services such as Intercom
and PostHog.
Roomote Cloud administrators use the same **Anonymous analytics** setting.
Cookie consent separately controls optional support and product-experience
services such as Intercom and PostHog. Changing anonymous analytics does not
change that consent.

## Version checks

Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/app/api/telemetry/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from '@roomote/telemetry/server';

import { authorize } from '@/lib/server/auth-context';
import { Env, isRoomoteCloudEnabled } from '@/lib/server/env';

export const runtime = 'nodejs';

Expand Down Expand Up @@ -55,17 +54,12 @@ export async function POST(request: NextRequest) {

// captureEvent re-checks this per event; checking once up front just
// avoids pointless id resolution work when analytics is disabled.
if (
!(await isAnonymousAnalyticsEnabled(
isRoomoteCloudEnabled(Env.R_CLOUD_ENABLED),
))
) {
if (!(await isAnonymousAnalyticsEnabled())) {
return NextResponse.json({ accepted: 0 }, { status: 202 });
}

for (const event of parsed.data.events) {
void captureEvent(event.event, {
cloudEnabled: isRoomoteCloudEnabled(Env.R_CLOUD_ENABLED),
userId: authResult.userId,
properties: event.properties,
timestamp: event.timestamp,
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/settings/MiscSettings.client.test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 21 additions & 20 deletions apps/web/src/components/settings/MiscSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

import { useTRPC } from '@/trpc/client';
Expand Down Expand Up @@ -32,6 +33,7 @@ function getBugReportUrl(diagnostics: string): string {
}

export function MiscSettings() {
const router = useRouter();
const trpc = useTRPC();
const queryClient = useQueryClient();
const queryKey = trpc.miscSettings.get.queryKey();
Expand All @@ -52,6 +54,7 @@ export function MiscSettings() {
try {
const updated = await updateMutation.mutateAsync({ enabled: nextValue });
queryClient.setQueryData<MiscSettingsData>(queryKey, updated);
router.refresh();
toast.success(
`Anonymous analytics ${nextValue ? 'enabled' : 'disabled'}`,
);
Expand Down Expand Up @@ -130,27 +133,25 @@ export function MiscSettings() {
</div>
</Section>

{!settingsQuery.data.cloudEnabled && (
<Section icon={EarOff} title="Privacy">
<div className="flex gap-3">
<Switch
aria-label="Toggle anonymous analytics"
checked={settingsQuery.data.anonymousAnalyticsEnabled}
disabled={updateMutation.isPending}
onCheckedChange={(checked) => void handleToggle(checked === true)}
/>
<div className="space-y-1">
<p className="text-sm font-semibold">Anonymous analytics</p>
<p className="text-sm text-muted-foreground">
Share anonymous usage analytics with the Roomote team to help
improve the product. Activity is identified only by random IDs
that are never linked to your company, users, or repositories.
No prompts, conversations or code is ever shared.
</p>
</div>
<Section icon={EarOff} title="Privacy">
<div className="flex gap-3">
<Switch
aria-label="Toggle anonymous analytics"
checked={settingsQuery.data.anonymousAnalyticsEnabled}
disabled={updateMutation.isPending}
onCheckedChange={(checked) => void handleToggle(checked === true)}
/>
<div className="space-y-1">
<p className="text-sm font-semibold">Anonymous analytics</p>
<p className="text-sm text-muted-foreground">
Share anonymous usage analytics with the Roomote team to help
improve the product. Activity is identified only by random IDs
that are never linked to your company, users, or repositories. No
prompts, conversations or code is ever shared.
</p>
</div>
</Section>
)}
</div>
</Section>

<Section icon={Stethoscope} title="Diagnostics">
<div className="space-y-3">
Expand Down
5 changes: 1 addition & 4 deletions apps/web/src/lib/server/auth-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,7 @@ export async function authorize(

const anonymousAnalyticsEnabled =
isTelemetryEnvAllowed() &&
isAnonymousAnalyticsEnabledFromMetadata(
authContext.deploymentMetadata,
isRoomoteCloudEnabled(Env.R_CLOUD_ENABLED),
);
isAnonymousAnalyticsEnabledFromMetadata(authContext.deploymentMetadata);

return {
success: true,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 3 additions & 6 deletions apps/web/src/trpc/commands/misc-settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,10 +586,8 @@ export async function getMiscSettingsCommand(
]);

return {
anonymousAnalyticsEnabled: isAnonymousAnalyticsEnabledFromMetadata(
metadata,
isRoomoteCloudEnabled(Env.R_CLOUD_ENABLED),
),
anonymousAnalyticsEnabled:
isAnonymousAnalyticsEnabledFromMetadata(metadata),
cloudEnabled: isRoomoteCloudEnabled(Env.R_CLOUD_ENABLED),
telemetryEnvAllowed: isTelemetryEnvAllowed(),
diagnostics,
Expand Down Expand Up @@ -687,10 +685,9 @@ export async function setAnonymousAnalyticsCommand(
columns: { metadata: true },
});

const cloudEnabled = isRoomoteCloudEnabled(Env.R_CLOUD_ENABLED);
const nextMetadata: Record<string, unknown> = {
...normalizeMetadata(existingSettings?.metadata),
[ANONYMOUS_ANALYTICS_METADATA_KEY]: cloudEnabled ? true : input.enabled,
[ANONYMOUS_ANALYTICS_METADATA_KEY]: input.enabled,
};

if (!existingSettings) {
Expand Down
Loading
Loading