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
19 changes: 12 additions & 7 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 17 additions & 20 deletions src/services/__tests__/zoo-telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -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,
}))
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 })

Expand All @@ -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 })

Expand Down
22 changes: 5 additions & 17 deletions src/services/zoo-telemetry.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -22,24 +17,17 @@ 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<void> {
const token = getCachedZooCodeToken()
if (!token) {
return
}

// Check subscription status before sending (uses 5-minute cache)
let status = getCachedSubscriptionStatus()
if (status === "unknown") {
status = await checkSubscriptionStatus().catch(() => "unknown" as const)
}

if (status !== "active") {
return
}

const baseUrl = getZooCodeBaseUrl()

const body = {
Expand Down
Loading