diff --git a/PRIVACY.md b/PRIVACY.md index 25db3a8813..390d981efb 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -40,13 +40,18 @@ go—and, importantly, where they don't. We retain telemetry only as long as needed for product analytics and debugging. Telemetry does **not** collect your code or AI prompts, and you can opt out at any time through the settings. -- **Zoo Code Observability (Authenticated Subscribers Only):** If you sign in to - Zoo Code and have an active subscription, Zoo Code will send LLM usage - telemetry to the Zoo Code backend (zoocode.dev). This includes task ID, AI - provider name, model name, token counts (input/output/cache), and estimated - cost. This data is linked to your authenticated Zoo Code account. You can stop - this collection at any time by signing out via the Zoo Code badge in the chat - area. +- **Zoo Code Observability (All Authenticated Users):** If you sign in to + Zoo Code, Zoo Code will send LLM usage telemetry to the Zoo Code backend + (zoocode.dev). This includes task ID, AI provider name, model name, token + counts (input/output/cache), and estimated cost. This data is linked to your + authenticated Zoo Code account and is retained for up to 90 days as + metadata-only API request logs, as described in the + [zoocode.dev Privacy Policy](https://www.zoocode.dev/legal/privacy). Free + plan users can view their telemetry in the dashboard for the most recent 7 + days; Pro and higher plan users can view the full 90-day window. You can + stop this collection at any time by signing out via the Zoo Code badge in + the chat area, and you may request deletion of your data at any time per + the privacy policy. - **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Zoo Code makes a secure API call to Zoo Code's backend servers to retrieve listing information. These diff --git a/src/services/__tests__/zoo-telemetry.test.ts b/src/services/__tests__/zoo-telemetry.test.ts index 5f5ace80de..d0d7ad44ed 100644 --- a/src/services/__tests__/zoo-telemetry.test.ts +++ b/src/services/__tests__/zoo-telemetry.test.ts @@ -1,20 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" -const { - mockCheckSubscriptionStatus, - mockGetCachedSubscriptionStatus, - mockGetCachedZooCodeToken, - mockGetZooCodeBaseUrl, -} = vi.hoisted(() => ({ - mockCheckSubscriptionStatus: vi.fn(), - mockGetCachedSubscriptionStatus: vi.fn(), +const { mockGetCachedZooCodeToken, mockGetZooCodeBaseUrl } = vi.hoisted(() => ({ mockGetCachedZooCodeToken: vi.fn(), mockGetZooCodeBaseUrl: vi.fn(), })) vi.mock("../zoo-code-auth", () => ({ - checkSubscriptionStatus: mockCheckSubscriptionStatus, - getCachedSubscriptionStatus: mockGetCachedSubscriptionStatus, getCachedZooCodeToken: mockGetCachedZooCodeToken, getZooCodeBaseUrl: mockGetZooCodeBaseUrl, })) @@ -52,21 +43,29 @@ describe("sendLlmTelemetry", () => { expect(global.fetch).not.toHaveBeenCalled() }) - it("refreshes an unknown subscription status before sending", async () => { - mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("unknown") - mockCheckSubscriptionStatus.mockResolvedValue("inactive") - global.fetch = vi.fn() + it("sends telemetry for authenticated users regardless of subscription tier", async () => { + // Privacy policy alignment: server-side retention (up to 90 days) and + // plan-gated dashboard visibility are enforced on zoocode.dev. The + // extension no longer filters telemetry by subscription status, so any + // authenticated user — free or paid — should reach the fetch call. + mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_free_user_token") + global.fetch = vi.fn().mockResolvedValue({ ok: true }) await sendLlmTelemetry(payload) - expect(mockCheckSubscriptionStatus).toHaveBeenCalled() - expect(global.fetch).not.toHaveBeenCalled() + expect(global.fetch).toHaveBeenCalledWith( + "https://www.zoocode.dev/api/observability/events", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: "Bearer zoo_ext_free_user_token", + }), + }), + ) }) it("fires the observability request without waiting for it to settle", async () => { mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("active") let resolveFetch: ((value: unknown) => void) | undefined global.fetch = vi.fn( @@ -104,7 +103,6 @@ describe("sendLlmTelemetry", () => { it("sends cancelled status when provided in payload", async () => { mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("active") global.fetch = vi.fn().mockResolvedValue({ ok: true }) @@ -119,7 +117,6 @@ describe("sendLlmTelemetry", () => { it("defaults to completed status when not provided", async () => { mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("active") global.fetch = vi.fn().mockResolvedValue({ ok: true }) diff --git a/src/services/zoo-telemetry.ts b/src/services/zoo-telemetry.ts index b181ff6d5b..6b6f226813 100644 --- a/src/services/zoo-telemetry.ts +++ b/src/services/zoo-telemetry.ts @@ -1,9 +1,4 @@ -import { - getCachedZooCodeToken, - getZooCodeBaseUrl, - getCachedSubscriptionStatus, - checkSubscriptionStatus, -} from "./zoo-code-auth" +import { getCachedZooCodeToken, getZooCodeBaseUrl } from "./zoo-code-auth" import { Package } from "../shared/package" export type LlmTelemetryPayload = { @@ -22,7 +17,10 @@ export type LlmTelemetryPayload = { /** * Send LLM telemetry to the Zoo Code observability backend. * This is a fire-and-forget operation that silently fails on error. - * Only sends telemetry for authenticated users with active subscriptions. + * Sends telemetry for all authenticated users — free and paid alike. + * Server-side retention follows the zoocode.dev privacy policy (metadata-only + * API request logs are kept up to 90 days). Dashboard visibility is plan-gated + * (7 days for Free; full window for Pro and higher). */ export async function sendLlmTelemetry(payload: LlmTelemetryPayload): Promise { const token = getCachedZooCodeToken() @@ -30,16 +28,6 @@ export async function sendLlmTelemetry(payload: LlmTelemetryPayload): Promise "unknown" as const) - } - - if (status !== "active") { - return - } - const baseUrl = getZooCodeBaseUrl() const body = {