Remove all telemetry (#12324)

* Remove all telemetry

* Fix webview tests after telemetry removal

* Fix embedder tests after telemetry removal

* Fix tests after telemetry removal
This commit is contained in:
Matt Rubens 2026-05-11 17:34:58 -04:00 committed by GitHub
parent 3d37e054dd
commit ff16c9c297
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
220 changed files with 402 additions and 7964 deletions

View file

@ -1,11 +0,0 @@
# PostHog Analytics Configuration
# Replace these values with your actual PostHog API key and host
NEXT_PUBLIC_POSTHOG_KEY=your_posthog_api_key_here
# Basin Form Endpoint for Static Form Submissions
# Replace this with your actual Basin form endpoint (e.g., https://usebasin.com/f/your-form-id)
NEXT_PUBLIC_BASIN_ENDPOINT=https://usebasin.com/f/your-form-id-here
# Site URL Configuration
# Used for generating absolute URLs in sitemap and other contexts
NEXT_PUBLIC_SITE_URL=https://roocode.com

View file

@ -31,7 +31,6 @@
"lucide-react": "^0.563.0",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"posthog-js": "^1.336.4",
"react": "^18.3.1",
"react-cookie-consent": "^9.0.0",
"react-dom": "^18.3.1",

View file

@ -24,7 +24,6 @@ import {
} from "@/lib/blog"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { BlogPostAnalytics } from "@/components/blog/BlogAnalytics"
import { BlogContent } from "@/components/blog/BlogContent"
import { BlogFAQ, type FAQItem } from "@/components/blog/BlogFAQ"
@ -241,8 +240,6 @@ export default async function BlogPostPage({ params }: Props) {
/>
)}
<BlogPostAnalytics post={post} />
<article className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl">
{/* Visual Breadcrumb Navigation */}

View file

@ -15,7 +15,6 @@ import { Suspense } from "react"
import { getPaginatedBlogPosts, getAllBlogPosts, getCuratedBlogPosts } from "@/lib/blog"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { BlogIndexAnalytics } from "@/components/blog/BlogAnalytics"
import { BlogPostList } from "@/components/blog/BlogPostList"
import { BlogPagination } from "@/components/blog/BlogPagination"
@ -141,8 +140,6 @@ export default async function BlogIndexPage({ searchParams }: Props) {
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
<BlogIndexAnalytics />
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl">
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">Blog</h1>

View file

@ -12,7 +12,6 @@ import Script from "next/script"
import { getPaginatedBlogPosts, getAllBlogPosts, POSTS_PER_PAGE } from "@/lib/blog"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { BlogIndexAnalytics } from "@/components/blog/BlogAnalytics"
import { BlogPostList } from "@/components/blog/BlogPostList"
import { BlogPagination } from "@/components/blog/BlogPagination"
@ -149,8 +148,6 @@ export default async function BlogPaginatedPage({ params }: PageProps) {
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
<BlogIndexAnalytics />
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl">
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">{title}</h1>

View file

@ -90,7 +90,7 @@ export default function CookiePolicy() {
</td>
</tr>
<tr className="bg-muted/25">
<td className="border border-border px-4 py-3 font-medium">PostHog</td>
<td className="border border-border px-4 py-3 font-medium">Analytics</td>
<td className="border border-border px-4 py-3">
Product analytics and feature usage tracking
</td>
@ -126,15 +126,6 @@ export default function CookiePolicy() {
Clerk Privacy Policy
</a>
</p>
<p>
<a
href="https://posthog.com/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline">
PostHog Privacy Policy
</a>
</p>
<p>
<a
href="https://legal.hubspot.com/privacy-policy"
@ -155,10 +146,10 @@ export default function CookiePolicy() {
<h2 className="mt-12 text-2xl font-bold">Analytics cookies</h2>
<p>
We use PostHog and HubSpot analytics cookies to understand how visitors interact with our
website. This helps us improve our services, user experience, and marketing efforts. Analytics
cookies are placed only if you give consent through our cookie banner. The lawful basis for
processing these cookies is your consent, which you can withdraw at any time.
We use HubSpot analytics cookies to understand how visitors interact with our website. This
helps us improve our services, user experience, and marketing efforts. Analytics cookies are
placed only if you give consent through our cookie banner. The lawful basis for processing these
cookies is your consent, which you can withdraw at any time.
</p>
<h2 className="mt-12 text-2xl font-bold">Third-party services</h2>

View file

@ -137,7 +137,7 @@ export default function SubProcessors() {
</td>
</tr>
<tr>
<td className="border border-border px-4 py-3 font-medium">PostHog</td>
<td className="border border-border px-4 py-3 font-medium">Analytics</td>
<td className="border border-border px-4 py-3">Data Services</td>
<td className="border border-border px-4 py-3">United States</td>
<td className="border border-border px-4 py-3">Product analytics</td>

View file

@ -143,9 +143,9 @@ export default function Privacy() {
<tr className="bg-muted/25">
<td className="border border-border px-4 py-2 font-medium">Usage Data</td>
<td className="border border-border px-4 py-2">
Feature clicks, error logs, performance metrics (captured via PostHog)
Feature clicks, error logs, performance metrics
</td>
<td className="border border-border px-4 py-2">Services automatically (PostHog)</td>
<td className="border border-border px-4 py-2">Services automatically</td>
</tr>
<tr>
<td className="border border-border px-4 py-2 font-medium">Payment Data</td>
@ -241,9 +241,7 @@ export default function Privacy() {
</tr>
<tr className="bg-muted/25">
<td className="border border-border px-4 py-2 font-medium">Usage & Telemetry</td>
<td className="border border-border px-4 py-2">
PostHog (selfhosted analytics platform)
</td>
<td className="border border-border px-4 py-2">None</td>
<td className="border border-border px-4 py-2">Ad networks or data brokers</td>
</tr>
<tr>

View file

@ -1,50 +0,0 @@
"use client"
/**
* Client-side blog analytics components
* MKT-74: Blog Analytics (PostHog)
*/
import { useEffect } from "react"
import { trackBlogIndexView, trackBlogPostView } from "@/lib/blog/analytics"
import type { BlogPost } from "@/lib/blog/types"
/**
* Tracks blog index page view
* Place this component on the blog index page
*/
export function BlogIndexAnalytics() {
useEffect(() => {
trackBlogIndexView()
}, [])
return null
}
/**
* Tracks individual blog post view
* Place this component on blog post pages
*/
export function BlogPostAnalytics({ post }: { post: BlogPost }) {
useEffect(() => {
trackBlogPostView(post)
}, [post])
return null
}
/**
* Serializable post data for client component
* Use this type when passing post data to BlogPostAnalytics
*/
export interface SerializablePostData {
slug: string
title: string
publish_date: string
publish_time_pt: string
tags: string[]
status: "draft" | "published"
description: string
content: string
filepath: string
}

View file

@ -1,92 +0,0 @@
"use client"
import { usePathname, useSearchParams } from "next/navigation"
import posthog from "posthog-js"
import { PostHogProvider as OriginalPostHogProvider } from "posthog-js/react"
import { useEffect, useRef, Suspense } from "react"
import { hasConsent } from "@/lib/analytics/consent-manager"
function PageViewTracker() {
const pathname = usePathname()
const searchParams = useSearchParams()
const previousUrl = useRef<string | null>(null)
// Track page views with proper referrer for SPA navigations
useEffect(() => {
if (pathname && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
const searchString = searchParams?.toString() ?? ""
const currentUrl = window.location.origin + pathname + (searchString ? `?${searchString}` : "")
// Get referrer - for SPA navigations, use previous URL; otherwise use document.referrer
const referrer = previousUrl.current ?? document.referrer
let referringDomain = ""
if (referrer) {
try {
referringDomain = new URL(referrer).hostname
} catch {
// Invalid URL, leave empty
}
}
posthog.capture("$pageview", {
$current_url: currentUrl,
$referrer: referrer,
$referring_domain: referringDomain,
})
// Update previous URL for next navigation
previousUrl.current = currentUrl
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, searchParams?.toString()])
return null
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Initialize PostHog immediately on the client side
if (typeof window !== "undefined" && !posthog.__loaded) {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
// Check if environment variables are set
if (!posthogKey) {
console.warn(
"PostHog API key is missing. Analytics will be disabled. " +
"Please set NEXT_PUBLIC_POSTHOG_KEY in your .env file.",
)
return
}
// Check if user has already consented to cookies
const userHasConsented = hasConsent()
// Initialize PostHog with appropriate persistence based on consent
posthog.init(posthogKey, {
api_host: "https://ph.roocode.com",
ui_host: "https://us.posthog.com",
capture_pageview: false, // We handle pageview tracking manually
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === "development") {
posthogInstance.debug()
}
},
save_referrer: true, // Save referrer information
save_campaign_params: true, // Save UTM parameters
respect_dnt: true, // Respect Do Not Track
persistence: userHasConsented ? "localStorage+cookie" : "memory", // Use localStorage if consented, otherwise memory-only
opt_out_capturing_by_default: false, // Start tracking immediately
})
}
}, [])
return (
<OriginalPostHogProvider client={posthog}>
<Suspense fallback={null}>
<PageViewTracker />
</Suspense>
{children}
</OriginalPostHogProvider>
)
}

View file

@ -5,7 +5,6 @@ import { ThemeProvider } from "next-themes"
import { GoogleTagManagerProvider } from "./google-tag-manager-provider"
import { HubSpotProvider } from "./hubspot-provider"
import { PostHogProvider } from "./posthog-provider"
const queryClient = new QueryClient()
@ -14,11 +13,9 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
<QueryClientProvider client={queryClient}>
<GoogleTagManagerProvider>
<HubSpotProvider>
<PostHogProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</PostHogProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</HubSpotProvider>
</GoogleTagManagerProvider>
</QueryClientProvider>

View file

@ -1,72 +1,42 @@
/**
* Simple consent event system
* Dispatches events when cookie consent changes
*/
const CONSENT_EVENT = "roo-cookie-consent-change"
import { getCookieConsentValue } from "react-cookie-consent"
import { CONSENT_COOKIE_NAME } from "@roo-code/types"
import posthog from "posthog-js"
type ConsentListener = (consented: boolean) => void
export const CONSENT_EVENT = "cookieConsentChanged"
/**
* Check if user has given consent for analytics cookies
* Uses react-cookie-consent's built-in function
*/
export function hasConsent(): boolean {
if (typeof window === "undefined") return false
return getCookieConsentValue(CONSENT_COOKIE_NAME) === "true"
}
/**
* Dispatch a consent change event
*/
export function dispatchConsentEvent(consented: boolean): void {
if (typeof window !== "undefined") {
const event = new CustomEvent(CONSENT_EVENT, {
detail: { consented },
})
window.dispatchEvent(event)
if (typeof window === "undefined") {
return false
}
return window.localStorage.getItem("roo-cookie-consent") === "accepted"
}
/**
* Listen for consent changes
*/
export function onConsentChange(callback: (consented: boolean) => void): () => void {
export function handleConsentAccept(): void {
if (typeof window === "undefined") {
return
}
window.localStorage.setItem("roo-cookie-consent", "accepted")
window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: true }))
}
export function handleConsentReject(): void {
if (typeof window === "undefined") {
return
}
window.localStorage.setItem("roo-cookie-consent", "rejected")
window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: false }))
}
export function onConsentChange(listener: ConsentListener): () => void {
if (typeof window === "undefined") {
return () => {}
}
const handler = (event: Event) => {
const customEvent = event as CustomEvent<{ consented: boolean }>
callback(customEvent.detail.consented)
listener(Boolean((event as CustomEvent<boolean>).detail))
}
window.addEventListener(CONSENT_EVENT, handler)
return () => window.removeEventListener(CONSENT_EVENT, handler)
}
/**
* Handle user accepting cookies
* Opts PostHog back into cookie-based tracking
*/
export function handleConsentAccept(): void {
if (typeof window !== "undefined" && posthog.__loaded) {
// User accepted - ensure localStorage+cookie persistence is enabled
posthog.opt_in_capturing()
posthog.set_config({
persistence: "localStorage+cookie",
})
}
dispatchConsentEvent(true)
}
/**
* Handle user rejecting cookies
* Switches PostHog to cookieless (memory-only) mode
*/
export function handleConsentReject(): void {
// User rejected - stick to cookieless mode
dispatchConsentEvent(false)
}

View file

@ -1,40 +0,0 @@
/**
* Blog-specific PostHog analytics events
* MKT-74: Blog Analytics (PostHog)
*/
import posthog from "posthog-js"
import type { BlogPost } from "./types"
/**
* Track blog index page view
*/
export function trackBlogIndexView(): void {
if (typeof window !== "undefined" && posthog.__loaded) {
posthog.capture("blog_index_view")
}
}
/**
* Track individual blog post view
*/
export function trackBlogPostView(post: BlogPost): void {
if (typeof window !== "undefined" && posthog.__loaded) {
posthog.capture("blog_post_view", {
slug: post.slug,
title: post.title,
publish_date: post.publish_date,
publish_time_pt: post.publish_time_pt,
tags: post.tags,
})
}
}
/**
* Track Substack subscribe click
*/
export function trackSubstackClick(): void {
if (typeof window !== "undefined" && posthog.__loaded) {
posthog.capture("blog_substack_click")
}
}

View file

@ -34,4 +34,3 @@ export {
export { BlogFrontmatterSchema, type ValidatedFrontmatter } from "./validation"
// Analytics
export { trackBlogIndexView, trackBlogPostView, trackSubstackClick } from "./analytics"

View file

@ -20,7 +20,7 @@
"entry": ["src/index.tsx"],
"project": ["src/**/*.{ts,tsx}", "../src/shared/*.ts"]
},
"packages/{build,cloud,evals,ipc,telemetry,types}": {
"packages/{build,cloud,evals,ipc,types}": {
"project": ["src/**/*.ts"]
}
}

View file

@ -2,7 +2,6 @@ import type { Disposable, ExtensionContext } from "vscode"
import EventEmitter from "events"
import type {
TelemetryEvent,
ClineMessage,
CloudServiceEvents,
AuthService,
@ -22,7 +21,6 @@ import { WebAuthService } from "./WebAuthService.js"
import { StaticTokenAuthService } from "./StaticTokenAuthService.js"
import { CloudSettingsService } from "./CloudSettingsService.js"
import { StaticSettingsService } from "./StaticSettingsService.js"
import { CloudTelemetryClient as TelemetryClient } from "./TelemetryClient.js"
import { CloudShareService } from "./CloudShareService.js"
import { CloudAPI } from "./CloudAPI.js"
import { RetryQueue } from "./retry-queue/index.js"
@ -59,12 +57,6 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this._settingsService
}
private _telemetryClient: TelemetryClient | null = null
public get telemetryClient() {
return this._telemetryClient
}
private _shareService: CloudShareService | null = null
public get shareService() {
@ -165,8 +157,6 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
},
)
this._telemetryClient = new TelemetryClient(this._authService, this._settingsService, this._retryQueue)
this._shareService = new CloudShareService(this._cloudAPI, this._settingsService, this.log)
this.isInitialized = true
@ -303,13 +293,6 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this.settingsService!.isTaskSyncEnabled()
}
// TelemetryClient
public captureEvent(event: TelemetryEvent): void {
this.ensureInitialized()
this.telemetryClient!.capture(event)
}
// ShareService
public async shareTask(
@ -323,9 +306,7 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return await this.shareService!.shareTask(taskId, visibility)
} catch (error) {
if (error instanceof TaskNotFoundError && clineMessages) {
// Backfill messages and retry.
await this.telemetryClient!.backfillMessages(clineMessages, taskId)
return await this.shareService!.shareTask(taskId, visibility)
this.log(`[CloudService] Task ${taskId} was not found while sharing ${clineMessages.length} messages`)
}
throw error

View file

@ -1,290 +0,0 @@
import {
type TelemetryClient,
type TelemetryEvent,
type ClineMessage,
type AuthService,
type SettingsService,
TelemetryEventName,
rooCodeTelemetryEventSchema,
TelemetryPropertiesProvider,
TelemetryEventSubscription,
} from "@roo-code/types"
import { getRooCodeApiUrl } from "./config.js"
import type { RetryQueue } from "./retry-queue/index.js"
abstract class BaseTelemetryClient implements TelemetryClient {
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
protected telemetryEnabled: boolean = false
constructor(
public readonly subscription?: TelemetryEventSubscription,
protected readonly debug = false,
) {}
protected isEventCapturable(eventName: TelemetryEventName): boolean {
if (!this.subscription) {
return true
}
return this.subscription.type === "include"
? this.subscription.events.includes(eventName)
: !this.subscription.events.includes(eventName)
}
/**
* Determines if a specific property should be included in telemetry events
* Override in subclasses to filter specific properties
*/
protected isPropertyCapturable(_propertyName: string): boolean {
return true
}
protected async getEventProperties(event: TelemetryEvent): Promise<TelemetryEvent["properties"]> {
let providerProperties: TelemetryEvent["properties"] = {}
const provider = this.providerRef?.deref()
if (provider) {
try {
// Get properties from the provider
providerProperties = await provider.getTelemetryProperties()
} catch (error) {
// Log error but continue with capturing the event.
console.error(
`Error getting telemetry properties: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
// Merge provider properties with event-specific properties.
// Event properties take precedence in case of conflicts.
const mergedProperties = {
...providerProperties,
...(event.properties || {}),
}
// Filter out properties that shouldn't be captured by this client
return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key)))
}
public abstract capture(event: TelemetryEvent): Promise<void>
public async captureException(_error: Error, _additionalProperties?: Record<string, unknown>): Promise<void> {}
public setProvider(provider: TelemetryPropertiesProvider): void {
this.providerRef = new WeakRef(provider)
}
public abstract updateTelemetryState(didUserOptIn: boolean): void
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
public abstract shutdown(): Promise<void>
}
export class CloudTelemetryClient extends BaseTelemetryClient {
private retryQueue: RetryQueue | null = null
constructor(
private authService: AuthService,
private settingsService: SettingsService,
retryQueue?: RetryQueue,
) {
super({
type: "exclude",
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
})
this.retryQueue = retryQueue || null
}
private async fetch(path: string, options: RequestInit, allowQueueing = true) {
if (!this.authService.isAuthenticated()) {
return
}
const token = this.authService.getSessionToken()
if (!token) {
console.error(`[TelemetryClient#fetch] Unauthorized: No session token available.`)
return
}
const url = `${getRooCodeApiUrl()}/api/${path}`
const fetchOptions: RequestInit = {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}
try {
const response = await fetch(url, fetchOptions)
if (!response.ok) {
console.error(
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
)
// Queue for retry on server errors (5xx) or rate limiting (429)
// Do NOT retry on client errors (4xx) except 429 - they won't succeed
if (this.retryQueue && allowQueueing && (response.status >= 500 || response.status === 429)) {
await this.retryQueue.enqueue(url, fetchOptions, "telemetry")
}
}
return response
} catch (error) {
console.error(`[TelemetryClient#fetch] Network error for ${options.method} ${path}: ${error}`)
// Queue for retry on network failures (typically TypeError with "fetch failed" message)
// These are transient network issues that may succeed on retry
if (
this.retryQueue &&
allowQueueing &&
error instanceof TypeError &&
error.message.includes("fetch failed")
) {
await this.retryQueue.enqueue(url, fetchOptions, "telemetry")
}
throw error
}
}
public override async capture(event: TelemetryEvent) {
if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
if (this.debug) {
console.info(`[TelemetryClient#capture] Skipping event: ${event.event}`)
}
return
}
const payload = {
type: event.event,
properties: await this.getEventProperties(event),
}
if (this.debug) {
console.info(`[TelemetryClient#capture] ${JSON.stringify(payload)}`)
}
const result = rooCodeTelemetryEventSchema.safeParse(payload)
if (!result.success) {
console.error(
`[TelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`,
)
return
}
try {
await this.fetch(`events`, {
method: "POST",
body: JSON.stringify(result.data),
})
} catch (error) {
console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
// Error is already queued for retry in the fetch method
}
}
public async backfillMessages(messages: ClineMessage[], taskId: string): Promise<void> {
if (!this.isTelemetryEnabled()) {
return
}
if (!this.authService.isAuthenticated()) {
if (this.debug) {
console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`)
}
return
}
const token = this.authService.getSessionToken()
if (!token) {
console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`)
return
}
try {
const mergedProperties = await this.getEventProperties({
event: TelemetryEventName.TASK_MESSAGE,
properties: { taskId },
})
const formData = new FormData()
formData.append("taskId", taskId)
formData.append("properties", JSON.stringify(mergedProperties))
formData.append(
"file",
new File([JSON.stringify(messages)], "task.json", {
type: "application/json",
}),
)
if (this.debug) {
console.info(
`[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`,
)
}
const url = `${getRooCodeApiUrl()}/api/events/backfill`
const fetchOptions: RequestInit = {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
}
try {
const response = await fetch(url, fetchOptions)
if (!response.ok) {
console.error(
`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
)
}
} catch (fetchError) {
console.error(`[TelemetryClient#backfillMessages] Network error: ${fetchError}`)
throw fetchError
}
} catch (error) {
console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`)
}
}
public override updateTelemetryState(_didUserOptIn: boolean) {}
public override isTelemetryEnabled(): boolean {
if (process.env.ROO_CODE_DISABLE_TELEMETRY === "1") {
return false
}
return true
}
protected override isEventCapturable(eventName: TelemetryEventName): boolean {
// Ensure that this event type is supported by the telemetry client
if (!super.isEventCapturable(eventName)) {
return false
}
// Only record message telemetry if task sync is enabled
if (eventName === TelemetryEventName.TASK_MESSAGE) {
return this.settingsService.isTaskSyncEnabled()
}
// Other telemetry types are capturable at this point
return true
}
public override async shutdown() {}
}

View file

@ -9,7 +9,6 @@ import { CloudService } from "../CloudService.js"
import { WebAuthService } from "../WebAuthService.js"
import { CloudSettingsService } from "../CloudSettingsService.js"
import { CloudShareService } from "../CloudShareService.js"
import { CloudTelemetryClient as TelemetryClient } from "../TelemetryClient.js"
vi.mock("vscode", () => ({
ExtensionContext: vi.fn(),
@ -31,8 +30,6 @@ vi.mock("../CloudSettingsService")
vi.mock("../CloudShareService")
vi.mock("../TelemetryClient")
describe("CloudService", () => {
let mockContext: vscode.ExtensionContext
@ -70,10 +67,6 @@ describe("CloudService", () => {
canShareTask: ReturnType<typeof vi.fn>
}
let mockTelemetryClient: {
backfillMessages: ReturnType<typeof vi.fn>
}
beforeEach(() => {
CloudService.resetInstance()
@ -142,17 +135,11 @@ describe("CloudService", () => {
canShareTask: vi.fn().mockResolvedValue(true),
}
mockTelemetryClient = {
backfillMessages: vi.fn().mockResolvedValue(undefined),
}
vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService)
vi.mocked(CloudShareService).mockImplementation(() => mockShareService as unknown as CloudShareService)
vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
})
afterEach(() => {
@ -530,11 +517,10 @@ describe("CloudService", () => {
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility)
expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
expect(result).toEqual(expectedResult)
})
it("should retry with backfill when TaskNotFoundError occurs", async () => {
it("should throw when TaskNotFoundError occurs", async () => {
const taskId = "test-task-id"
const visibility = "organization"
const clineMessages: ClineMessage[] = [
@ -546,24 +532,12 @@ describe("CloudService", () => {
},
]
const expectedResult = {
success: true,
shareUrl: "https://example.com/share/123",
}
mockShareService.shareTask.mockRejectedValueOnce(new TaskNotFoundError(taskId))
// First call throws TaskNotFoundError, second call succeeds
mockShareService.shareTask
.mockRejectedValueOnce(new TaskNotFoundError(taskId))
.mockResolvedValueOnce(expectedResult)
await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(TaskNotFoundError)
const result = await cloudService.shareTask(taskId, visibility, clineMessages)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(2)
expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility)
expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility)
expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1)
expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId)
expect(result).toEqual(expectedResult)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility)
})
it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => {
@ -576,7 +550,6 @@ describe("CloudService", () => {
await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
})
it("should not retry when non-TaskNotFoundError occurs", async () => {
@ -597,7 +570,6 @@ describe("CloudService", () => {
await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
})
it("should work with default parameters", async () => {

View file

@ -1,675 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// npx vitest run src/__tests__/TelemetryClient.test.ts
import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
import { CloudTelemetryClient as TelemetryClient } from "../TelemetryClient.js"
const mockFetch = vi.fn()
global.fetch = mockFetch as any
describe("TelemetryClient", () => {
const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
return instance[propertyName]
}
let mockAuthService: any
let mockSettingsService: any
beforeEach(() => {
vi.clearAllMocks()
// Create a mock AuthService instead of using the singleton
mockAuthService = {
getSessionToken: vi.fn().mockReturnValue("mock-token"),
getState: vi.fn().mockReturnValue("active-session"),
isAuthenticated: vi.fn().mockReturnValue(true),
hasActiveSession: vi.fn().mockReturnValue(true),
}
// Create a mock SettingsService
mockSettingsService = {
getSettings: vi.fn().mockReturnValue({
cloudSettings: {
recordTaskMessages: true,
},
}),
getUserSettings: vi.fn().mockReturnValue({
features: {},
settings: {
taskSyncEnabled: true,
},
version: 1,
}),
isTaskSyncEnabled: vi.fn().mockReturnValue(true),
}
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({}),
})
vi.spyOn(console, "info").mockImplementation(() => {})
vi.spyOn(console, "error").mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("isEventCapturable", () => {
it("should return true for events not in exclude list", () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true)
expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true)
})
it("should return false for events in exclude list", () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
})
it("should return true for TASK_MESSAGE events when isTaskSyncEnabled returns true", () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
it("should return false for TASK_MESSAGE events when isTaskSyncEnabled returns false", () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
})
describe("getEventProperties", () => {
it("should merge provider properties with event properties", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}),
}
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
mode: "override", // This should override the provider's mode.
},
})
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "override", // Event property takes precedence.
customProp: "value",
})
expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
})
it("should handle errors from provider gracefully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
const consoleErrorSpy = vi.spyOn(console, "error")
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining("Error getting telemetry properties: Provider error"),
)
})
it("should return event properties when no provider is set", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
})
})
describe("capture", () => {
it("should not capture events that are not capturable", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list.
properties: { test: "value" },
})
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not capture TASK_MESSAGE events when recordTaskMessages is false", async () => {
mockSettingsService.getSettings.mockReturnValue({
cloudSettings: {
recordTaskMessages: false,
},
})
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
event: TelemetryEventName.TASK_MESSAGE,
properties: {
taskId: "test-task-id",
message: {
ts: 1,
type: "say",
say: "text",
text: "test message",
},
},
})
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not capture TASK_MESSAGE events when isTaskSyncEnabled returns false", async () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
event: TelemetryEventName.TASK_MESSAGE,
properties: {
taskId: "test-task-id",
message: {
ts: 1,
type: "say",
say: "text",
text: "test message",
},
},
})
expect(mockFetch).not.toHaveBeenCalled()
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
it("should not send request when schema validation fails", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})
expect(mockFetch).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
})
it("should send request when event is capturable and validation passes", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}
const eventProperties = {
taskId: "test-task-id",
}
const mockValidatedData = {
type: TelemetryEventName.TASK_CREATED,
properties: {
...providerProperties,
taskId: "test-task-id",
},
}
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}
client.setProvider(mockProvider)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: eventProperties,
})
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events",
expect.objectContaining({
method: "POST",
body: JSON.stringify(mockValidatedData),
}),
)
})
it("should attempt to capture TASK_MESSAGE events when isTaskSyncEnabled returns true", async () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
const eventProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
taskId: "test-task-id",
message: {
ts: 1,
type: "say",
say: "text",
text: "test message",
},
}
const mockValidatedData = {
type: TelemetryEventName.TASK_MESSAGE,
properties: eventProperties,
}
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
event: TelemetryEventName.TASK_MESSAGE,
properties: eventProperties,
})
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events",
expect.objectContaining({
method: "POST",
body: JSON.stringify(mockValidatedData),
}),
)
})
it("should handle fetch errors gracefully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
mockFetch.mockRejectedValue(new Error("Network error"))
await expect(
client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
}),
).resolves.not.toThrow()
})
})
describe("telemetry state methods", () => {
it("should always return true for isTelemetryEnabled", () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
expect(client.isTelemetryEnabled()).toBe(true)
})
it("should have empty implementations for updateTelemetryState and shutdown", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
client.updateTelemetryState(true)
await client.shutdown()
})
})
describe("backfillMessages", () => {
it("should not send request when not authenticated", async () => {
mockAuthService.isAuthenticated.mockReturnValue(false)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not send request when no session token available", async () => {
mockAuthService.getSessionToken.mockReturnValue(null)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] Unauthorized: No session token available.",
)
})
it("should send FormData request with correct structure when authenticated", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}
client.setProvider(mockProvider)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message 1",
},
{
ts: 2,
type: "ask" as const,
ask: "followup" as const,
text: "test question",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents
const call = mockFetch.mock.calls[0]
const formData = call?.[1]?.body as FormData
expect(formData.get("taskId")).toBe("test-task-id")
// Parse and compare properties as objects since JSON.stringify order can vary
const propertiesJson = formData.get("properties") as string
const parsedProperties = JSON.parse(propertiesJson)
expect(parsedProperties).toEqual({
taskId: "test-task-id",
...providerProperties,
})
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the messages
const fileContent = await fileField.text()
expect(fileContent).toBe(JSON.stringify(messages))
})
it("should handle provider errors gracefully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
client.setProvider(mockProvider)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents - should still work with just taskId
const call = mockFetch.mock.calls[0]
const formData = call?.[1]?.body as FormData
expect(formData.get("taskId")).toBe("test-task-id")
expect(formData.get("properties")).toBe(
JSON.stringify({
taskId: "test-task-id",
}),
)
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the messages
const fileContent = await fileField.text()
expect(fileContent).toBe(JSON.stringify(messages))
})
it("should work without provider set", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents - should work with just taskId
const call = mockFetch.mock.calls[0]
const formData = call?.[1]?.body as FormData
expect(formData.get("taskId")).toBe("test-task-id")
expect(formData.get("properties")).toBe(
JSON.stringify({
taskId: "test-task-id",
}),
)
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the messages
const fileContent = await fileField.text()
expect(fileContent).toBe(JSON.stringify(messages))
})
it("should handle fetch errors gracefully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
mockFetch.mockRejectedValue(new Error("Network error"))
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow()
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining(
"[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error",
),
)
})
it("should handle HTTP error responses", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
mockFetch.mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
})
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(console.error).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found",
)
})
it("should handle empty messages array", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.backfillMessages([], "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents
const call = mockFetch.mock.calls[0]
const formData = call?.[1]?.body as FormData
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the empty messages array
const fileContent = await fileField.text()
expect(fileContent).toBe("[]")
})
})
})

View file

@ -35,11 +35,11 @@ describe("RetryQueue", () => {
const url = "https://api.example.com/test"
const options = { method: "POST", body: JSON.stringify({ test: "data" }) }
await retryQueue.enqueue(url, options, "telemetry")
await retryQueue.enqueue(url, options, "event")
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1)
expect(stats.byType["telemetry"]).toBe(1)
expect(stats.byType["event"]).toBe(1)
})
it("should enforce max queue size with FIFO eviction", async () => {
@ -48,7 +48,7 @@ describe("RetryQueue", () => {
// Add 4 requests
for (let i = 1; i <= 4; i++) {
await retryQueue.enqueue(`https://api.example.com/test${i}`, { method: "POST" }, "telemetry")
await retryQueue.enqueue(`https://api.example.com/test${i}`, { method: "POST" }, "event")
}
const stats = retryQueue.getStats()
@ -58,14 +58,14 @@ describe("RetryQueue", () => {
describe("persistence", () => {
it("should persist queue to workspace state", async () => {
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
expect(mockContext.workspaceState.update).toHaveBeenCalledWith(
"roo.retryQueue",
expect.arrayContaining([
expect.objectContaining({
url: "https://api.example.com/test",
type: "telemetry",
type: "event",
}),
]),
)
@ -79,7 +79,7 @@ describe("RetryQueue", () => {
options: { method: "POST" },
timestamp: Date.now(),
retryCount: 0,
type: "telemetry",
type: "event",
},
]
@ -102,7 +102,7 @@ describe("RetryQueue", () => {
describe("clear", () => {
it("should clear all queued requests", async () => {
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "api-call")
let stats = retryQueue.getStats()
@ -119,14 +119,14 @@ describe("RetryQueue", () => {
it("should return correct statistics", async () => {
const now = Date.now()
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "api-call")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "event")
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(3)
expect(stats.byType["telemetry"]).toBe(2)
expect(stats.byType["event"]).toBe(2)
expect(stats.byType["api-call"]).toBe(1)
expect(stats.oldestRequest).toBeDefined()
expect(stats.newestRequest).toBeDefined()
@ -140,12 +140,12 @@ describe("RetryQueue", () => {
const listener = vi.fn()
retryQueue.on("request-queued", listener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/test",
type: "telemetry",
type: "event",
}),
)
})
@ -175,7 +175,7 @@ describe("RetryQueue", () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
global.fetch = fetchMock
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// Pause the queue
retryQueue.pause()
@ -209,8 +209,8 @@ describe("RetryQueue", () => {
it("should clear queue when user changes", async () => {
// Add some requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
@ -237,8 +237,8 @@ describe("RetryQueue", () => {
retryQueue.setCurrentUserId("user_123")
// Add some requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
@ -253,8 +253,8 @@ describe("RetryQueue", () => {
it("should not clear on first login (no previous user)", async () => {
// Add some requests before any user is set
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
@ -273,7 +273,7 @@ describe("RetryQueue", () => {
// First user logs in
retryQueue.clearIfUserChanged("user_123")
await retryQueue.enqueue("https://api.example.com/user1-req", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/user1-req", { method: "POST" }, "event")
// User logs out
const clearedOnLogout = retryQueue.clearIfUserChanged(undefined)
@ -281,13 +281,13 @@ describe("RetryQueue", () => {
expect(clearListener).toHaveBeenCalledTimes(1)
// Different user logs in
await retryQueue.enqueue("https://api.example.com/user2-req", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/user2-req", { method: "POST" }, "event")
const clearedOnNewUser = retryQueue.clearIfUserChanged("user_456")
expect(clearedOnNewUser).toBe(true)
expect(clearListener).toHaveBeenCalledTimes(2)
// Same user logs back in
await retryQueue.enqueue("https://api.example.com/user2-req2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/user2-req2", { method: "POST" }, "event")
const notCleared = retryQueue.clearIfUserChanged("user_456")
expect(notCleared).toBe(false)
expect(clearListener).toHaveBeenCalledTimes(2) // Still 2, not cleared
@ -315,9 +315,9 @@ describe("RetryQueue", () => {
retryQueue.on("request-retry-success", successListener)
// Add multiple requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "event")
// Mock successful responses
fetchMock.mockResolvedValue({ ok: true })
@ -342,7 +342,7 @@ describe("RetryQueue", () => {
const failListener = vi.fn()
retryQueue.on("request-retry-failed", failListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// Mock failed response
fetchMock.mockRejectedValue(new Error("Network error"))
@ -371,7 +371,7 @@ describe("RetryQueue", () => {
const maxRetriesListener = vi.fn()
retryQueue.on("request-max-retries-exceeded", maxRetriesListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// Mock failed responses
fetchMock.mockRejectedValue(new Error("Network error"))
@ -400,7 +400,7 @@ describe("RetryQueue", () => {
it("should not process if already processing", async () => {
// Add a request
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// Mock a slow response
fetchMock.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 100)))
@ -439,7 +439,7 @@ describe("RetryQueue", () => {
method: "POST",
headers: { "Content-Type": "application/json" },
},
"telemetry",
"event",
)
fetchMock.mockResolvedValue({ ok: true })
@ -465,7 +465,7 @@ describe("RetryQueue", () => {
// Create queue with custom timeout (short timeout for testing)
retryQueue = new RetryQueue(mockContext, { requestTimeout: 100 })
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// Mock fetch to reject with abort error
const abortError = new Error("The operation was aborted")
@ -496,7 +496,7 @@ describe("RetryQueue", () => {
retryQueue.on("request-retry-failed", failListener)
retryQueue.on("request-retry-success", successListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// First attempt: 500 error
fetchMock.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Internal Server Error" })
@ -529,9 +529,9 @@ describe("RetryQueue", () => {
it("should pause entire queue on 429 rate limiting with Retry-After header", async () => {
// Add multiple requests to test queue-wide pause
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "event")
// Mock 429 response with Retry-After header (in seconds) for the first request
const retryAfterResponse = {
@ -567,8 +567,8 @@ describe("RetryQueue", () => {
it("should process all requests after rate limit period expires", async () => {
// Add multiple requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
// Mock 429 response with very short Retry-After (for testing)
const retryAfterResponse = {
@ -608,7 +608,7 @@ describe("RetryQueue", () => {
const successListener = vi.fn()
retryQueue.on("request-retry-success", successListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "event")
// Mock 401 error
fetchMock.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" })
@ -621,7 +621,7 @@ describe("RetryQueue", () => {
expect(stats.totalQueued).toBe(0)
// Test 403 as well
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "event")
fetchMock.mockResolvedValueOnce({ ok: false, status: 403, statusText: "Forbidden" })
await retryQueue.retryAll()
@ -644,11 +644,7 @@ describe("RetryQueue", () => {
]
for (const error of clientErrors) {
await retryQueue.enqueue(
`https://api.example.com/test-${error.status}`,
{ method: "POST" },
"telemetry",
)
await retryQueue.enqueue(`https://api.example.com/test-${error.status}`, { method: "POST" }, "event")
fetchMock.mockResolvedValueOnce({ ok: false, ...error })
}
@ -662,7 +658,7 @@ describe("RetryQueue", () => {
it("should prevent concurrent processing", async () => {
// Add a single request
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "event")
// Mock slow response
let resolveFirst: () => void

View file

@ -4,7 +4,7 @@ export interface QueuedRequest {
options: RequestInit
timestamp: number
retryCount: number
type: "api-call" | "telemetry" | "settings" | "other"
type: "api-call" | "event" | "settings" | "other"
operation?: string
lastError?: string
}

View file

@ -115,7 +115,6 @@ RUN mkdir -p \
packages/core \
packages/evals \
packages/ipc \
packages/telemetry \
packages/types \
packages/cloud \
packages/vscode-shim \
@ -133,7 +132,6 @@ COPY ./packages/config-typescript/package.json ./packages/config-typescript/
COPY ./packages/core/package.json ./packages/core/
COPY ./packages/evals/package.json ./packages/evals/
COPY ./packages/ipc/package.json ./packages/ipc/
COPY ./packages/telemetry/package.json ./packages/telemetry/
COPY ./packages/types/package.json ./packages/types/
COPY ./packages/cloud/package.json ./packages/cloud/
COPY ./packages/vscode-shim/package.json ./packages/vscode-shim/

View file

@ -1,3 +0,0 @@
# @roo-code/telemetry
## 0.0.1

View file

@ -1,4 +0,0 @@
import { config } from "@roo-code/config-eslint/base"
/** @type {import("eslint").Linter.Config} */
export default [...config]

View file

@ -1,25 +0,0 @@
{
"name": "@roo-code/telemetry",
"description": "Roo Code telemetry service and clients.",
"version": "0.0.1",
"type": "module",
"exports": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "vitest run",
"clean": "rimraf .turbo"
},
"dependencies": {
"@roo-code/types": "workspace:^",
"posthog-node": "^5.0.0",
"zod": "^3.25.61"
},
"devDependencies": {
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@types/node": "20.x",
"@types/vscode": "^1.84.0",
"vitest": "^3.2.3"
}
}

View file

@ -1,75 +0,0 @@
import {
TelemetryEvent,
TelemetryEventName,
TelemetryClient,
TelemetryPropertiesProvider,
TelemetryEventSubscription,
} from "@roo-code/types"
export abstract class BaseTelemetryClient implements TelemetryClient {
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
protected telemetryEnabled: boolean = false
constructor(
public readonly subscription?: TelemetryEventSubscription,
protected readonly debug = false,
) {}
protected isEventCapturable(eventName: TelemetryEventName): boolean {
if (!this.subscription) {
return true
}
return this.subscription.type === "include"
? this.subscription.events.includes(eventName)
: !this.subscription.events.includes(eventName)
}
/**
* Determines if a specific property should be included in telemetry events
* Override in subclasses to filter specific properties
*/
protected isPropertyCapturable(_propertyName: string): boolean {
return true
}
protected async getEventProperties(event: TelemetryEvent): Promise<TelemetryEvent["properties"]> {
let providerProperties: TelemetryEvent["properties"] = {}
const provider = this.providerRef?.deref()
if (provider) {
try {
// Get properties from the provider
providerProperties = await provider.getTelemetryProperties()
} catch (error) {
// Log error but continue with capturing the event.
console.error(
`Error getting telemetry properties: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
// Merge provider properties with event-specific properties.
// Event properties take precedence in case of conflicts.
const mergedProperties = { ...providerProperties, ...(event.properties || {}) }
// Filter out properties that shouldn't be captured by this client
return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key)))
}
public abstract capture(event: TelemetryEvent): Promise<void>
public abstract captureException(error: Error, additionalProperties?: Record<string, unknown>): Promise<void>
public setProvider(provider: TelemetryPropertiesProvider): void {
this.providerRef = new WeakRef(provider)
}
public abstract updateTelemetryState(didUserOptIn: boolean): void
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
public abstract shutdown(): Promise<void>
}

View file

@ -1,170 +0,0 @@
import { PostHog } from "posthog-node"
import * as vscode from "vscode"
import {
type TelemetryProperties,
type TelemetryEvent,
TelemetryEventName,
getErrorStatusCode,
getErrorMessage,
shouldReportApiErrorToTelemetry,
isApiProviderError,
extractApiProviderErrorProperties,
isConsecutiveMistakeError,
extractConsecutiveMistakeErrorProperties,
} from "@roo-code/types"
import { BaseTelemetryClient } from "./BaseTelemetryClient"
/**
* PostHogTelemetryClient handles telemetry event tracking for the Roo Code extension.
* Uses PostHog analytics to track user interactions and system events.
* Respects user privacy settings and VSCode's global telemetry configuration.
*/
export class PostHogTelemetryClient extends BaseTelemetryClient {
private client: PostHog
private distinctId: string = vscode.env.machineId
// Git repository properties that should be filtered out
private readonly gitPropertyNames = ["repositoryUrl", "repositoryName", "defaultBranch"]
constructor(debug = false) {
super(
{
type: "exclude",
events: [TelemetryEventName.TASK_MESSAGE, TelemetryEventName.LLM_COMPLETION],
},
debug,
)
this.client = new PostHog(process.env.POSTHOG_API_KEY || "", { host: "https://ph.roocode.com" })
}
/**
* Filter out git repository properties for PostHog telemetry
* @param propertyName The property name to check
* @returns Whether the property should be included in telemetry events
*/
protected override isPropertyCapturable(propertyName: string): boolean {
// Filter out git repository properties
if (this.gitPropertyNames.includes(propertyName)) {
return false
}
return true
}
public override async capture(event: TelemetryEvent): Promise<void> {
if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
if (this.debug) {
console.info(`[PostHogTelemetryClient#capture] Skipping event: ${event.event}`)
}
return
}
if (this.debug) {
console.info(`[PostHogTelemetryClient#capture] ${event.event}`)
}
const properties = await this.getEventProperties(event)
this.client.capture({
distinctId: this.distinctId,
event: event.event,
properties,
})
}
public override async captureException(
error: Error,
additionalProperties?: Record<string, unknown>,
): Promise<void> {
if (!this.isTelemetryEnabled()) {
if (this.debug) {
console.info(`[PostHogTelemetryClient#captureException] Skipping exception: ${error.message}`)
}
return
}
// Extract error status code and message for filtering.
const errorCode = getErrorStatusCode(error)
const errorMessage = getErrorMessage(error) ?? error.message
// Filter out expected errors (e.g., 402 billing, 429 rate limits)
if (!shouldReportApiErrorToTelemetry(errorCode, errorMessage)) {
if (this.debug) {
console.info(
`[PostHogTelemetryClient#captureException] Filtering out expected error: ${errorCode} - ${errorMessage}`,
)
}
return
}
if (this.debug) {
console.info(`[PostHogTelemetryClient#captureException] ${error.message}`)
}
// Auto-extract properties from known error types and merge with additionalProperties.
// Explicit additionalProperties take precedence over auto-extracted properties.
let mergedProperties = additionalProperties
if (isApiProviderError(error)) {
const extractedProperties = extractApiProviderErrorProperties(error)
mergedProperties = { ...extractedProperties, ...additionalProperties }
} else if (isConsecutiveMistakeError(error)) {
const extractedProperties = extractConsecutiveMistakeErrorProperties(error)
mergedProperties = { ...extractedProperties, ...additionalProperties }
}
// Override the error message with the extracted error message.
error.message = errorMessage
const provider = this.providerRef?.deref()
let telemetryProperties: TelemetryProperties | undefined = undefined
if (provider) {
try {
telemetryProperties = await provider.getTelemetryProperties()
} catch (_error) {
// Ignore.
}
}
const exceptionProperties = {
...mergedProperties,
$app_version: telemetryProperties?.appVersion,
}
this.client.captureException(error, this.distinctId, exceptionProperties)
}
/**
* Updates the telemetry state based on user preferences and VSCode settings.
* Only enables telemetry if both VSCode global telemetry is enabled and
* user has opted in.
* @param didUserOptIn Whether the user has explicitly opted into telemetry
*/
public override updateTelemetryState(didUserOptIn: boolean): void {
this.telemetryEnabled = false
// First check global telemetry level - telemetry should only be enabled when level is "all".
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
const globalTelemetryEnabled = telemetryLevel === "all"
// We only enable telemetry if global vscode telemetry is enabled.
if (globalTelemetryEnabled) {
this.telemetryEnabled = didUserOptIn
}
// Update PostHog client state based on telemetry preference.
if (this.telemetryEnabled) {
this.client.optIn()
} else {
this.client.optOut()
}
}
public override async shutdown(): Promise<void> {
await this.client.shutdown()
}
}

View file

@ -1,291 +0,0 @@
import { ZodError } from "zod"
import {
type TelemetryClient,
type TelemetryPropertiesProvider,
TelemetryEventName,
type TelemetrySetting,
} from "@roo-code/types"
/**
* TelemetryService wrapper class that defers initialization.
* This ensures that we only create the various clients after environment
* variables are loaded.
*/
export class TelemetryService {
constructor(private clients: TelemetryClient[]) {}
public register(client: TelemetryClient): void {
this.clients.push(client)
}
/**
* Sets the ClineProvider reference to use for global properties
* @param provider A ClineProvider instance to use
*/
public setProvider(provider: TelemetryPropertiesProvider): void {
// If client is initialized, pass the provider reference.
if (this.isReady) {
this.clients.forEach((client) => client.setProvider(provider))
}
}
/**
* Base method for all telemetry operations
* Checks if the service is initialized before performing any operation
* @returns Whether the service is ready to use
*/
private get isReady(): boolean {
return this.clients.length > 0
}
/**
* Updates the telemetry state based on user preferences and VSCode settings
* @param isOptedIn Whether the user is opted into telemetry
*/
public updateTelemetryState(isOptedIn: boolean): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.updateTelemetryState(isOptedIn))
}
/**
* Generic method to capture any type of event with specified properties
* @param eventName The event name to capture
* @param properties The event properties
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public captureEvent(eventName: TelemetryEventName, properties?: Record<string, any>): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.capture({ event: eventName, properties }))
}
/**
* Captures an exception using PostHog's error tracking
* @param error The error to capture
* @param additionalProperties Additional properties to include with the exception
*/
public captureException(error: Error, additionalProperties?: Record<string, unknown>): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.captureException(error, additionalProperties))
}
public captureTaskCreated(taskId: string): void {
this.captureEvent(TelemetryEventName.TASK_CREATED, { taskId })
}
public captureTaskRestarted(taskId: string): void {
this.captureEvent(TelemetryEventName.TASK_RESTARTED, { taskId })
}
public captureTaskCompleted(taskId: string): void {
this.captureEvent(TelemetryEventName.TASK_COMPLETED, { taskId })
}
public captureConversationMessage(taskId: string, source: "user" | "assistant"): void {
this.captureEvent(TelemetryEventName.TASK_CONVERSATION_MESSAGE, { taskId, source })
}
public captureLlmCompletion(
taskId: string,
properties: {
inputTokens: number
outputTokens: number
cacheWriteTokens: number
cacheReadTokens: number
cost?: number
},
): void {
this.captureEvent(TelemetryEventName.LLM_COMPLETION, { taskId, ...properties })
}
public captureModeSwitch(taskId: string, newMode: string): void {
this.captureEvent(TelemetryEventName.MODE_SWITCH, { taskId, newMode })
}
public captureToolUsage(taskId: string, tool: string): void {
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool })
}
public captureCheckpointCreated(taskId: string): void {
this.captureEvent(TelemetryEventName.CHECKPOINT_CREATED, { taskId })
}
public captureCheckpointDiffed(taskId: string): void {
this.captureEvent(TelemetryEventName.CHECKPOINT_DIFFED, { taskId })
}
public captureCheckpointRestored(taskId: string): void {
this.captureEvent(TelemetryEventName.CHECKPOINT_RESTORED, { taskId })
}
public captureContextCondensed(taskId: string, isAutomaticTrigger: boolean, usedCustomPrompt?: boolean): void {
this.captureEvent(TelemetryEventName.CONTEXT_CONDENSED, {
taskId,
isAutomaticTrigger,
...(usedCustomPrompt !== undefined && { usedCustomPrompt }),
})
}
public captureSlidingWindowTruncation(taskId: string): void {
this.captureEvent(TelemetryEventName.SLIDING_WINDOW_TRUNCATION, { taskId })
}
public captureCodeActionUsed(actionType: string): void {
this.captureEvent(TelemetryEventName.CODE_ACTION_USED, { actionType })
}
public capturePromptEnhanced(taskId?: string): void {
this.captureEvent(TelemetryEventName.PROMPT_ENHANCED, { ...(taskId && { taskId }) })
}
public captureSchemaValidationError({ schemaName, error }: { schemaName: string; error: ZodError }): void {
// https://zod.dev/ERROR_HANDLING?id=formatting-errors
this.captureEvent(TelemetryEventName.SCHEMA_VALIDATION_ERROR, { schemaName, error: error.format() })
}
public captureDiffApplicationError(taskId: string, consecutiveMistakeCount: number): void {
this.captureEvent(TelemetryEventName.DIFF_APPLICATION_ERROR, { taskId, consecutiveMistakeCount })
}
public captureShellIntegrationError(taskId: string): void {
this.captureEvent(TelemetryEventName.SHELL_INTEGRATION_ERROR, { taskId })
}
public captureConsecutiveMistakeError(taskId: string): void {
this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId })
}
/**
* Captures when a tab is shown due to user action
* @param tab The tab that was shown
*/
public captureTabShown(tab: string): void {
this.captureEvent(TelemetryEventName.TAB_SHOWN, { tab })
}
/**
* Captures when a setting is changed in ModesView
* @param settingName The name of the setting that was changed
*/
public captureModeSettingChanged(settingName: string): void {
this.captureEvent(TelemetryEventName.MODE_SETTINGS_CHANGED, { settingName })
}
/**
* Captures when a user creates a new custom mode
* @param modeSlug The slug of the custom mode
* @param modeName The name of the custom mode
*/
public captureCustomModeCreated(modeSlug: string, modeName: string): void {
this.captureEvent(TelemetryEventName.CUSTOM_MODE_CREATED, { modeSlug, modeName })
}
/**
* Captures a marketplace item installation event
* @param itemId The unique identifier of the marketplace item
* @param itemType The type of item (mode or mcp)
* @param itemName The human-readable name of the item
* @param target The installation target (project or global)
* @param properties Additional properties like hasParameters, installationMethod
*/
public captureMarketplaceItemInstalled(
itemId: string,
itemType: string,
itemName: string,
target: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
properties?: Record<string, any>,
): void {
this.captureEvent(TelemetryEventName.MARKETPLACE_ITEM_INSTALLED, {
itemId,
itemType,
itemName,
target,
...(properties || {}),
})
}
/**
* Captures a marketplace item removal event
* @param itemId The unique identifier of the marketplace item
* @param itemType The type of item (mode or mcp)
* @param itemName The human-readable name of the item
* @param target The removal target (project or global)
*/
public captureMarketplaceItemRemoved(itemId: string, itemType: string, itemName: string, target: string): void {
this.captureEvent(TelemetryEventName.MARKETPLACE_ITEM_REMOVED, {
itemId,
itemType,
itemName,
target,
})
}
/**
* Captures a title button click event
* @param button The button that was clicked
*/
public captureTitleButtonClicked(button: string): void {
this.captureEvent(TelemetryEventName.TITLE_BUTTON_CLICKED, { button })
}
/**
* Captures when telemetry settings are changed
* @param previousSetting The previous telemetry setting
* @param newSetting The new telemetry setting
*/
public captureTelemetrySettingsChanged(previousSetting: TelemetrySetting, newSetting: TelemetrySetting): void {
this.captureEvent(TelemetryEventName.TELEMETRY_SETTINGS_CHANGED, {
previousSetting,
newSetting,
})
}
/**
* Checks if telemetry is currently enabled
* @returns Whether telemetry is enabled
*/
public isTelemetryEnabled(): boolean {
return this.isReady && this.clients.some((client) => client.isTelemetryEnabled())
}
public async shutdown(): Promise<void> {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.shutdown())
}
private static _instance: TelemetryService | null = null
static createInstance(clients: TelemetryClient[] = []) {
if (this._instance) {
throw new Error("TelemetryService instance already created")
}
this._instance = new TelemetryService(clients)
return this._instance
}
static get instance() {
if (!this._instance) {
throw new Error("TelemetryService not initialized")
}
return this._instance
}
static hasInstance(): boolean {
return this._instance !== null
}
}

View file

@ -1,629 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// pnpm --filter @roo-code/telemetry test src/__tests__/PostHogTelemetryClient.test.ts
import * as vscode from "vscode"
import { PostHog } from "posthog-node"
import { type TelemetryPropertiesProvider, TelemetryEventName, ApiProviderError } from "@roo-code/types"
import { PostHogTelemetryClient } from "../PostHogTelemetryClient"
vi.mock("posthog-node")
vi.mock("vscode", () => ({
env: {
machineId: "test-machine-id",
},
workspace: {
getConfiguration: vi.fn(),
},
}))
describe("PostHogTelemetryClient", () => {
const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
return instance[propertyName]
}
let mockPostHogClient: any
beforeEach(() => {
vi.clearAllMocks()
mockPostHogClient = {
capture: vi.fn(),
captureException: vi.fn(),
optIn: vi.fn(),
optOut: vi.fn(),
shutdown: vi.fn().mockResolvedValue(undefined),
}
;(PostHog as any).mockImplementation(() => mockPostHogClient)
// @ts-expect-error - Accessing private static property for testing
PostHogTelemetryClient._instance = undefined
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("all"),
})
})
describe("isEventCapturable", () => {
it("should return true for events not in exclude list", () => {
const client = new PostHogTelemetryClient()
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
})
it("should return false for events in exclude list", () => {
const client = new PostHogTelemetryClient()
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(false)
})
})
describe("isPropertyCapturable", () => {
it("should filter out git repository properties", () => {
const client = new PostHogTelemetryClient()
const isPropertyCapturable = getPrivateProperty<(propertyName: string) => boolean>(
client,
"isPropertyCapturable",
).bind(client)
// Git properties should be filtered out
expect(isPropertyCapturable("repositoryUrl")).toBe(false)
expect(isPropertyCapturable("repositoryName")).toBe(false)
expect(isPropertyCapturable("defaultBranch")).toBe(false)
// Other properties should be included
expect(isPropertyCapturable("appVersion")).toBe(true)
expect(isPropertyCapturable("vscodeVersion")).toBe(true)
expect(isPropertyCapturable("platform")).toBe(true)
expect(isPropertyCapturable("mode")).toBe(true)
expect(isPropertyCapturable("customProperty")).toBe(true)
})
})
describe("getEventProperties", () => {
it("should merge provider properties with event properties", async () => {
const client = new PostHogTelemetryClient()
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}),
}
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
mode: "override", // This should override the provider's mode.
},
})
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "override", // Event property takes precedence.
customProp: "value",
})
expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
})
it("should filter out git repository properties", async () => {
const client = new PostHogTelemetryClient()
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
// Git properties that should be filtered out
repositoryUrl: "https://github.com/example/repo",
repositoryName: "example/repo",
defaultBranch: "main",
}),
}
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
},
})
// Git properties should be filtered out
expect(result).not.toHaveProperty("repositoryUrl")
expect(result).not.toHaveProperty("repositoryName")
expect(result).not.toHaveProperty("defaultBranch")
// Other properties should be included
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
customProp: "value",
})
})
it("should handle errors from provider gracefully", async () => {
const client = new PostHogTelemetryClient()
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining("Error getting telemetry properties: Provider error"),
)
consoleErrorSpy.mockRestore()
})
it("should return event properties when no provider is set", async () => {
const client = new PostHogTelemetryClient()
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
})
})
describe("capture", () => {
it("should not capture events when telemetry is disabled", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(false)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})
expect(mockPostHogClient.capture).not.toHaveBeenCalled()
})
it("should not capture events that are not capturable", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
await client.capture({
event: TelemetryEventName.LLM_COMPLETION, // This is in the exclude list.
properties: { test: "value" },
})
expect(mockPostHogClient.capture).not.toHaveBeenCalled()
})
it("should capture events when telemetry is enabled and event is capturable", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}),
}
client.setProvider(mockProvider)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})
expect(mockPostHogClient.capture).toHaveBeenCalledWith({
distinctId: "test-machine-id",
event: TelemetryEventName.TASK_CREATED,
properties: expect.objectContaining({
appVersion: "1.0.0",
test: "value",
}),
})
})
it("should filter out git repository properties when capturing events", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
// Git properties that should be filtered out
repositoryUrl: "https://github.com/example/repo",
repositoryName: "example/repo",
defaultBranch: "main",
}),
}
client.setProvider(mockProvider)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})
expect(mockPostHogClient.capture).toHaveBeenCalledWith({
distinctId: "test-machine-id",
event: TelemetryEventName.TASK_CREATED,
properties: expect.objectContaining({
appVersion: "1.0.0",
test: "value",
}),
})
// Verify git properties are not included
const captureCall = mockPostHogClient.capture.mock.calls[0][0]
expect(captureCall.properties).not.toHaveProperty("repositoryUrl")
expect(captureCall.properties).not.toHaveProperty("repositoryName")
expect(captureCall.properties).not.toHaveProperty("defaultBranch")
})
})
describe("updateTelemetryState", () => {
it("should enable telemetry when user opts in and global telemetry is enabled", () => {
const client = new PostHogTelemetryClient()
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("all"),
})
client.updateTelemetryState(true)
expect(client.isTelemetryEnabled()).toBe(true)
expect(mockPostHogClient.optIn).toHaveBeenCalled()
})
it("should disable telemetry when user opts out", () => {
const client = new PostHogTelemetryClient()
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("all"),
})
client.updateTelemetryState(false)
expect(client.isTelemetryEnabled()).toBe(false)
expect(mockPostHogClient.optOut).toHaveBeenCalled()
})
it("should disable telemetry when global telemetry is disabled, regardless of user opt-in", () => {
const client = new PostHogTelemetryClient()
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("off"),
})
client.updateTelemetryState(true)
expect(client.isTelemetryEnabled()).toBe(false)
expect(mockPostHogClient.optOut).toHaveBeenCalled()
})
})
describe("captureException", () => {
it("should not capture exceptions when telemetry is disabled", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(false)
const error = new Error("Test error")
await client.captureException(error)
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})
it("should capture exceptions with app version from provider", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}),
}
client.setProvider(mockProvider)
const error = new Error("Test error")
await client.captureException(error, { customProp: "value" })
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(
error,
"test-machine-id",
expect.objectContaining({
customProp: "value",
$app_version: "1.0.0",
}),
)
})
it("should capture exceptions with undefined app version when no provider is set", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new Error("Test error")
await client.captureException(error)
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
$app_version: undefined,
})
})
})
describe("shutdown", () => {
it("should call shutdown on the PostHog client", async () => {
const client = new PostHogTelemetryClient()
await client.shutdown()
expect(mockPostHogClient.shutdown).toHaveBeenCalled()
})
})
describe("captureException error filtering", () => {
it("should filter out 429 rate limit errors (via status property)", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
// Create an error with status property (like OpenAI SDK errors)
const error = Object.assign(new Error("Rate limit exceeded"), { status: 429 })
client.captureException(error)
// Should NOT capture 429 errors
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})
it("should filter out 402 billing errors (via status property)", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
// Create an error with status 402 (Payment Required)
const error = Object.assign(new Error("Payment required"), { status: 402 })
client.captureException(error)
// Should NOT capture 402 errors
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})
it("should filter out errors with '429' in message", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new Error("429 Rate limit exceeded: free-models-per-day")
client.captureException(error)
// Should NOT capture errors with 429 in message
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})
it("should filter out errors containing 'rate limit' (case insensitive)", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new Error("Request failed due to Rate Limit")
client.captureException(error)
// Should NOT capture rate limit errors
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})
it("should capture non-rate-limit errors", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new Error("Internal server error")
client.captureException(error)
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
$app_version: undefined,
})
})
it("should capture errors with non-429 status codes", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = Object.assign(new Error("Internal server error"), { status: 500 })
client.captureException(error)
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
$app_version: undefined,
})
})
it("should use nested error message from OpenAI SDK error structure for filtering", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
// Create an error with nested metadata (like OpenRouter upstream errors)
const error = Object.assign(new Error("Request failed"), {
status: 429,
error: {
message: "Error details",
metadata: { raw: "Rate limit exceeded: free-models-per-day" },
},
})
client.captureException(error)
// Should NOT capture - the nested metadata.raw contains rate limit message
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})
it("should capture errors with nested metadata and override error.message with extracted message", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
// Create an OpenAI SDK-like error with nested metadata (non-rate-limit error)
const error = Object.assign(new Error("Generic request failed"), {
status: 500,
error: {
message: "Nested error message",
metadata: { raw: "Upstream provider error: model overloaded" },
},
})
client.captureException(error)
// The implementation overrides error.message with the extracted message from getErrorMessage
expect(error.message).toBe("Upstream provider error: model overloaded")
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
$app_version: undefined,
})
})
it("should capture errors with nested error.message and override error.message with extracted message", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
// Create an OpenAI SDK-like error with nested message but no metadata.raw
const error = Object.assign(new Error("Generic request failed"), {
status: 500,
error: {
message: "Upstream provider: connection timeout",
},
})
client.captureException(error)
// The implementation overrides error.message with the extracted message from getErrorMessage
expect(error.message).toBe("Upstream provider: connection timeout")
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
$app_version: undefined,
})
})
it("should use primary message when no nested error structure exists", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
// Create an OpenAI SDK-like error without nested error object
const error = Object.assign(new Error("Primary error message"), {
status: 500,
})
client.captureException(error)
// Verify error message remains the primary message
expect(error.message).toBe("Primary error message")
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
$app_version: undefined,
})
})
it("should capture ApiProviderError and auto-extract properties", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
client.captureException(error)
// The implementation auto-extracts properties from ApiProviderError
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
provider: "OpenRouter",
modelId: "gpt-4",
operation: "createMessage",
errorCode: 500,
$app_version: undefined,
})
})
it("should capture ApiProviderError with additionalProperties merged with auto-extracted properties", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
client.captureException(error, { customProperty: "value" })
// additionalProperties take precedence over auto-extracted properties
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
provider: "OpenRouter",
modelId: "gpt-4",
operation: "createMessage",
customProperty: "value",
$app_version: undefined,
})
})
it("should capture regular errors with additionalProperties", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const error = new Error("Regular error")
client.captureException(error, { customProperty: "value" })
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
customProperty: "value",
$app_version: undefined,
})
})
})
})

View file

@ -1,3 +0,0 @@
export * from "./BaseTelemetryClient"
export * from "./PostHogTelemetryClient"
export * from "./TelemetryService"

View file

@ -1,5 +0,0 @@
{
"extends": "@roo-code/config-typescript/vscode-library.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View file

@ -1,9 +0,0 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
globals: true,
environment: "node",
watch: false,
},
})

View file

@ -1,576 +0,0 @@
// pnpm --filter @roo-code/types test src/__tests__/telemetry.test.ts
import {
getErrorStatusCode,
getErrorMessage,
extractMessageFromJsonPayload,
shouldReportApiErrorToTelemetry,
EXPECTED_API_ERROR_CODES,
ApiProviderError,
isApiProviderError,
extractApiProviderErrorProperties,
ConsecutiveMistakeError,
isConsecutiveMistakeError,
extractConsecutiveMistakeErrorProperties,
} from "../telemetry.js"
describe("telemetry error utilities", () => {
describe("getErrorStatusCode", () => {
it("should return undefined for non-object errors", () => {
expect(getErrorStatusCode(null)).toBeUndefined()
expect(getErrorStatusCode(undefined)).toBeUndefined()
expect(getErrorStatusCode("error string")).toBeUndefined()
expect(getErrorStatusCode(42)).toBeUndefined()
})
it("should return undefined for objects without status property", () => {
expect(getErrorStatusCode({})).toBeUndefined()
expect(getErrorStatusCode({ message: "error" })).toBeUndefined()
expect(getErrorStatusCode({ code: 500 })).toBeUndefined()
})
it("should return undefined for objects with non-numeric status", () => {
expect(getErrorStatusCode({ status: "500" })).toBeUndefined()
expect(getErrorStatusCode({ status: null })).toBeUndefined()
expect(getErrorStatusCode({ status: undefined })).toBeUndefined()
})
it("should return status for OpenAI SDK-like errors", () => {
const error = { status: 429, message: "Rate limit exceeded" }
expect(getErrorStatusCode(error)).toBe(429)
})
it("should return status for errors with additional properties", () => {
const error = {
status: 500,
code: "internal_error",
message: "Internal server error",
error: { message: "Upstream error" },
}
expect(getErrorStatusCode(error)).toBe(500)
})
})
describe("getErrorMessage", () => {
it("should return undefined for null, undefined, or objects without message", () => {
expect(getErrorMessage(null)).toBeUndefined()
expect(getErrorMessage(undefined)).toBeUndefined()
expect(getErrorMessage({})).toBeUndefined()
expect(getErrorMessage({ code: 500 })).toBeUndefined()
})
it("should return the primary message for simple OpenAI SDK errors", () => {
const error = { status: 400, message: "Bad request" }
expect(getErrorMessage(error)).toBe("Bad request")
})
it("should return message from plain objects with message property", () => {
expect(getErrorMessage({ message: "error" })).toBe("error")
})
it("should prioritize nested error.message over primary message", () => {
const error = {
status: 500,
message: "Request failed",
error: { message: "Upstream provider error" },
}
expect(getErrorMessage(error)).toBe("Upstream provider error")
})
it("should prioritize metadata.raw over other messages", () => {
const error = {
status: 429,
message: "Request failed",
error: {
message: "Error details",
metadata: { raw: "Rate limit exceeded: free-models-per-day" },
},
}
expect(getErrorMessage(error)).toBe("Rate limit exceeded: free-models-per-day")
})
it("should fallback to nested error.message when metadata.raw is undefined", () => {
const error = {
status: 400,
message: "Request failed",
error: {
message: "Detailed error message",
metadata: {},
},
}
expect(getErrorMessage(error)).toBe("Detailed error message")
})
it("should fallback to primary message when no nested messages exist", () => {
const error = {
status: 403,
message: "Forbidden",
error: {},
}
expect(getErrorMessage(error)).toBe("Forbidden")
})
it("should extract message from JSON payload in error message", () => {
const error = {
status: 503,
message: '503 {"error":{"code":"","message":"Model unavailable"}}',
}
expect(getErrorMessage(error)).toBe("Model unavailable")
})
it("should extract message from JSON payload with status prefix", () => {
const error = {
status: 503,
message:
'503 {"error":{"code":"","message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道,请更换分组尝试"}}',
}
expect(getErrorMessage(error)).toBe(
"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道,请更换分组尝试",
)
})
it("should extract message from nested error.message containing JSON", () => {
const error = {
status: 500,
message: "Request failed",
error: { message: '{"error":{"message":"Upstream provider error"}}' },
}
expect(getErrorMessage(error)).toBe("Upstream provider error")
})
it("should return original message when JSON has no message field", () => {
const error = {
status: 500,
message: '{"error":{"code":"123"}}',
}
expect(getErrorMessage(error)).toBe('{"error":{"code":"123"}}')
})
it("should return original message when JSON is invalid", () => {
const error = {
status: 500,
message: "503 {invalid json}",
}
expect(getErrorMessage(error)).toBe("503 {invalid json}")
})
it("should extract message from standard Error object", () => {
const error = new Error("Simple error message")
expect(getErrorMessage(error)).toBe("Simple error message")
})
it("should extract message from standard Error with JSON payload", () => {
const error = new Error('503 {"error":{"code":"","message":"Model unavailable"}}')
expect(getErrorMessage(error)).toBe("Model unavailable")
})
it("should extract message from ApiProviderError", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
expect(getErrorMessage(error)).toBe("Test error")
})
it("should extract message from ApiProviderError with JSON payload", () => {
const jsonMessage =
'503 {"error":{"code":"","message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道"}}'
const error = new ApiProviderError(jsonMessage, "Anthropic", "claude-sonnet-4-5", "createMessage")
expect(getErrorMessage(error)).toBe("所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道")
})
it("should handle ApiProviderError with errorCode but no status property", () => {
const error = new ApiProviderError("Test error", "Anthropic", "claude-3-opus", "createMessage", 500)
expect(getErrorMessage(error)).toBe("Test error")
})
})
describe("extractMessageFromJsonPayload", () => {
it("should return undefined for messages without JSON", () => {
expect(extractMessageFromJsonPayload("Simple error message")).toBeUndefined()
expect(extractMessageFromJsonPayload("Error: something went wrong")).toBeUndefined()
expect(extractMessageFromJsonPayload("")).toBeUndefined()
})
it("should extract message from error.message structure", () => {
const json = '{"error":{"message":"Model unavailable"}}'
expect(extractMessageFromJsonPayload(json)).toBe("Model unavailable")
})
it("should extract message from error.message with code structure", () => {
const json = '{"error":{"code":"","message":"Model unavailable"}}'
expect(extractMessageFromJsonPayload(json)).toBe("Model unavailable")
})
it("should extract message from status prefix followed by JSON", () => {
const message = '503 {"error":{"code":"","message":"Model unavailable"}}'
expect(extractMessageFromJsonPayload(message)).toBe("Model unavailable")
})
it("should extract message from simple message structure", () => {
const json = '{"message":"Simple error"}'
expect(extractMessageFromJsonPayload(json)).toBe("Simple error")
})
it("should return undefined for JSON without message field", () => {
const json = '{"error":{"code":"500"}}'
expect(extractMessageFromJsonPayload(json)).toBeUndefined()
})
it("should return undefined for invalid JSON", () => {
expect(extractMessageFromJsonPayload("{invalid json}")).toBeUndefined()
expect(extractMessageFromJsonPayload("503 {not: valid: json}")).toBeUndefined()
})
it("should handle nested error structure with empty code", () => {
const json = '{"error":{"code":"","message":"Token quota exceeded"}}'
expect(extractMessageFromJsonPayload(json)).toBe("Token quota exceeded")
})
it("should handle Unicode messages correctly", () => {
const json = '{"error":{"message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道"}}'
expect(extractMessageFromJsonPayload(json)).toBe(
"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道",
)
})
it("should return undefined when message field is not a string", () => {
const json = '{"error":{"message":123}}'
expect(extractMessageFromJsonPayload(json)).toBeUndefined()
})
})
describe("shouldReportApiErrorToTelemetry", () => {
it("should return false for expected error codes", () => {
for (const code of EXPECTED_API_ERROR_CODES) {
expect(shouldReportApiErrorToTelemetry(code)).toBe(false)
}
})
it("should return false for 402 billing errors", () => {
expect(shouldReportApiErrorToTelemetry(402)).toBe(false)
expect(shouldReportApiErrorToTelemetry(402, "Payment required")).toBe(false)
})
it("should return false for 429 rate limit errors", () => {
expect(shouldReportApiErrorToTelemetry(429)).toBe(false)
expect(shouldReportApiErrorToTelemetry(429, "Rate limit exceeded")).toBe(false)
})
it("should return false for messages starting with 429", () => {
expect(shouldReportApiErrorToTelemetry(undefined, "429 Rate limit exceeded")).toBe(false)
expect(shouldReportApiErrorToTelemetry(undefined, "429: Too many requests")).toBe(false)
})
it("should return false for messages containing 'rate limit' (case insensitive)", () => {
expect(shouldReportApiErrorToTelemetry(undefined, "Rate limit exceeded")).toBe(false)
expect(shouldReportApiErrorToTelemetry(undefined, "RATE LIMIT error")).toBe(false)
expect(shouldReportApiErrorToTelemetry(undefined, "Request failed due to rate limit")).toBe(false)
})
it("should return true for non-rate-limit errors", () => {
expect(shouldReportApiErrorToTelemetry(500)).toBe(true)
expect(shouldReportApiErrorToTelemetry(400, "Bad request")).toBe(true)
expect(shouldReportApiErrorToTelemetry(401, "Unauthorized")).toBe(true)
})
it("should return true when no error code or message is provided", () => {
expect(shouldReportApiErrorToTelemetry()).toBe(true)
expect(shouldReportApiErrorToTelemetry(undefined, undefined)).toBe(true)
})
it("should return true for regular error messages without rate limit keywords", () => {
expect(shouldReportApiErrorToTelemetry(undefined, "Internal server error")).toBe(true)
expect(shouldReportApiErrorToTelemetry(undefined, "Connection timeout")).toBe(true)
})
})
describe("EXPECTED_API_ERROR_CODES", () => {
it("should contain 402 (payment required)", () => {
expect(EXPECTED_API_ERROR_CODES.has(402)).toBe(true)
})
it("should contain 429 (rate limit)", () => {
expect(EXPECTED_API_ERROR_CODES.has(429)).toBe(true)
})
})
describe("ApiProviderError", () => {
it("should create an error with correct properties", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
expect(error.message).toBe("Test error")
expect(error.name).toBe("ApiProviderError")
expect(error.provider).toBe("OpenRouter")
expect(error.modelId).toBe("gpt-4")
expect(error.operation).toBe("createMessage")
expect(error.errorCode).toBe(500)
})
it("should work without optional errorCode", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
expect(error.message).toBe("Test error")
expect(error.provider).toBe("OpenRouter")
expect(error.modelId).toBe("gpt-4")
expect(error.operation).toBe("createMessage")
expect(error.errorCode).toBeUndefined()
})
it("should be an instance of Error", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
expect(error).toBeInstanceOf(Error)
})
})
describe("isApiProviderError", () => {
it("should return true for ApiProviderError instances", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
expect(isApiProviderError(error)).toBe(true)
})
it("should return true for ApiProviderError with errorCode", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 429)
expect(isApiProviderError(error)).toBe(true)
})
it("should return false for regular Error instances", () => {
const error = new Error("Test error")
expect(isApiProviderError(error)).toBe(false)
})
it("should return false for null and undefined", () => {
expect(isApiProviderError(null)).toBe(false)
expect(isApiProviderError(undefined)).toBe(false)
})
it("should return false for non-error objects", () => {
expect(isApiProviderError({})).toBe(false)
expect(isApiProviderError({ provider: "test", modelId: "test", operation: "test" })).toBe(false)
})
it("should return false for Error with wrong name", () => {
const error = new Error("Test error")
error.name = "CustomError"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).provider = "OpenRouter"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).modelId = "gpt-4"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).operation = "createMessage"
expect(isApiProviderError(error)).toBe(false)
})
})
describe("extractApiProviderErrorProperties", () => {
it("should extract all properties from ApiProviderError", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
const properties = extractApiProviderErrorProperties(error)
expect(properties).toEqual({
provider: "OpenRouter",
modelId: "gpt-4",
operation: "createMessage",
errorCode: 500,
})
})
it("should not include errorCode when undefined", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
const properties = extractApiProviderErrorProperties(error)
expect(properties).toEqual({
provider: "OpenRouter",
modelId: "gpt-4",
operation: "createMessage",
})
expect(properties).not.toHaveProperty("errorCode")
})
it("should include errorCode when it is 0", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 0)
const properties = extractApiProviderErrorProperties(error)
// errorCode of 0 is falsy but !== undefined, so it should be included
expect(properties).toHaveProperty("errorCode", 0)
})
})
describe("ConsecutiveMistakeError", () => {
it("should create an error with correct properties", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 5, 3, "no_tools_used")
expect(error.message).toBe("Test error")
expect(error.name).toBe("ConsecutiveMistakeError")
expect(error.taskId).toBe("task-123")
expect(error.consecutiveMistakeCount).toBe(5)
expect(error.consecutiveMistakeLimit).toBe(3)
expect(error.reason).toBe("no_tools_used")
})
it("should create an error with provider and modelId", () => {
const error = new ConsecutiveMistakeError(
"Test error",
"task-123",
5,
3,
"no_tools_used",
"anthropic",
"claude-3-sonnet-20240229",
)
expect(error.message).toBe("Test error")
expect(error.name).toBe("ConsecutiveMistakeError")
expect(error.taskId).toBe("task-123")
expect(error.consecutiveMistakeCount).toBe(5)
expect(error.consecutiveMistakeLimit).toBe(3)
expect(error.reason).toBe("no_tools_used")
expect(error.provider).toBe("anthropic")
expect(error.modelId).toBe("claude-3-sonnet-20240229")
})
it("should be an instance of Error", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3)
expect(error).toBeInstanceOf(Error)
})
it("should handle zero values", () => {
const error = new ConsecutiveMistakeError("Zero test", "task-000", 0, 0)
expect(error.taskId).toBe("task-000")
expect(error.consecutiveMistakeCount).toBe(0)
expect(error.consecutiveMistakeLimit).toBe(0)
})
it("should default reason to unknown when not provided", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3)
expect(error.reason).toBe("unknown")
})
it("should accept tool_repetition reason", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3, "tool_repetition")
expect(error.reason).toBe("tool_repetition")
})
it("should accept no_tools_used reason", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3, "no_tools_used")
expect(error.reason).toBe("no_tools_used")
})
it("should have undefined provider and modelId when not provided", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3, "no_tools_used")
expect(error.provider).toBeUndefined()
expect(error.modelId).toBeUndefined()
})
})
describe("isConsecutiveMistakeError", () => {
it("should return true for ConsecutiveMistakeError instances", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3)
expect(isConsecutiveMistakeError(error)).toBe(true)
})
it("should return false for regular Error instances", () => {
const error = new Error("Test error")
expect(isConsecutiveMistakeError(error)).toBe(false)
})
it("should return false for ApiProviderError instances", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
expect(isConsecutiveMistakeError(error)).toBe(false)
})
it("should return false for null and undefined", () => {
expect(isConsecutiveMistakeError(null)).toBe(false)
expect(isConsecutiveMistakeError(undefined)).toBe(false)
})
it("should return false for non-error objects", () => {
expect(isConsecutiveMistakeError({})).toBe(false)
expect(
isConsecutiveMistakeError({
taskId: "task-123",
consecutiveMistakeCount: 3,
consecutiveMistakeLimit: 3,
}),
).toBe(false)
})
it("should return false for Error with wrong name", () => {
const error = new Error("Test error")
error.name = "CustomError"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).taskId = "task-123"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).consecutiveMistakeCount = 3
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).consecutiveMistakeLimit = 3
expect(isConsecutiveMistakeError(error)).toBe(false)
})
})
describe("extractConsecutiveMistakeErrorProperties", () => {
it("should extract all properties from ConsecutiveMistakeError", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 5, 3, "no_tools_used")
const properties = extractConsecutiveMistakeErrorProperties(error)
expect(properties).toEqual({
taskId: "task-123",
consecutiveMistakeCount: 5,
consecutiveMistakeLimit: 3,
reason: "no_tools_used",
})
})
it("should extract all properties including provider and modelId", () => {
const error = new ConsecutiveMistakeError(
"Test error",
"task-123",
5,
3,
"no_tools_used",
"anthropic",
"claude-3-sonnet-20240229",
)
const properties = extractConsecutiveMistakeErrorProperties(error)
expect(properties).toEqual({
taskId: "task-123",
consecutiveMistakeCount: 5,
consecutiveMistakeLimit: 3,
reason: "no_tools_used",
provider: "anthropic",
modelId: "claude-3-sonnet-20240229",
})
})
it("should not include provider and modelId when undefined", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 5, 3, "no_tools_used")
const properties = extractConsecutiveMistakeErrorProperties(error)
expect(properties).not.toHaveProperty("provider")
expect(properties).not.toHaveProperty("modelId")
})
it("should handle zero values correctly", () => {
const error = new ConsecutiveMistakeError("Zero test", "task-000", 0, 0)
const properties = extractConsecutiveMistakeErrorProperties(error)
expect(properties).toEqual({
taskId: "task-000",
consecutiveMistakeCount: 0,
consecutiveMistakeLimit: 0,
reason: "unknown",
})
})
it("should handle large numbers", () => {
const error = new ConsecutiveMistakeError("Large test", "task-large", 1000, 500, "tool_repetition")
const properties = extractConsecutiveMistakeErrorProperties(error)
expect(properties).toEqual({
taskId: "task-large",
consecutiveMistakeCount: 1000,
consecutiveMistakeLimit: 500,
reason: "tool_repetition",
})
})
})
})

View file

@ -8,7 +8,21 @@ import { globalSettingsSchema } from "./global-settings.js"
import { providerSettingsWithIdSchema } from "./provider-settings.js"
import { mcpMarketplaceItemSchema } from "./marketplace.js"
import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js"
import { staticAppPropertiesSchema, gitPropertiesSchema } from "./telemetry.js"
const extensionAppPropertiesSchema = z.object({
appName: z.string(),
appVersion: z.string(),
vscodeVersion: z.string(),
platform: z.string(),
editorName: z.string(),
hostname: z.string().optional(),
})
const extensionGitPropertiesSchema = z.object({
repositoryUrl: z.string().optional(),
repositoryName: z.string().optional(),
defaultBranch: z.string().optional(),
})
/**
* JWTPayload
@ -409,8 +423,8 @@ export const extensionInstanceSchema = z.object({
instanceId: z.string(),
userId: z.string(),
workspacePath: z.string(),
appProperties: staticAppPropertiesSchema,
gitProperties: gitPropertiesSchema.optional(),
appProperties: extensionAppPropertiesSchema,
gitProperties: extensionGitPropertiesSchema.optional(),
lastHeartbeat: z.coerce.number(),
task: extensionTaskSchema,
taskAsk: clineMessageSchema.optional(),

View file

@ -10,7 +10,6 @@ import {
import { historyItemSchema } from "./history.js"
import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js"
import { experimentsSchema } from "./experiment.js"
import { telemetrySettingsSchema } from "./telemetry.js"
import { modeConfigSchema } from "./mode.js"
import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js"
import { toolNamesSchema } from "./tool.js"
@ -188,8 +187,6 @@ export const globalSettingsSchema = z.object({
language: languagesSchema.optional(),
telemetrySetting: telemetrySettingsSchema.optional(),
mcpEnabled: z.boolean().optional(),
mode: z.string().optional(),
@ -371,8 +368,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
maxDiagnosticMessages: 50,
language: "en",
telemetrySetting: "enabled",
mcpEnabled: false,
mode: "code", // "architect",

View file

@ -23,7 +23,6 @@ export * from "./provider-settings.js"
export * from "./task.js"
export * from "./todo.js"
export * from "./skills.js"
export * from "./telemetry.js"
export * from "./terminal.js"
export * from "./tool.js"
export * from "./tool-params.js"

View file

@ -4,7 +4,6 @@ import { RooCodeEventName } from "./events.js"
import type { RooCodeSettings } from "./global-settings.js"
import type { ClineMessage, QueuedMessage, TokenUsage } from "./message.js"
import type { ToolUsage, ToolName } from "./tool.js"
import type { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js"
import type { TodoItem } from "./todo.js"
/**
@ -36,10 +35,6 @@ export interface TaskProviderLike {
getProviderProfile(): Promise<string>
setProviderProfile(providerProfile: string): Promise<void>
// Telemetry
readonly appProperties: StaticAppProperties
readonly gitProperties: GitProperties | undefined
getTelemetryProperties(): Promise<TelemetryProperties>
readonly cwd: string
// Event Emitter

View file

@ -1,535 +0,0 @@
import { z } from "zod"
import { providerNames } from "./provider-settings.js"
import { clineMessageSchema } from "./message.js"
/**
* TelemetrySetting
*/
export const telemetrySettings = ["unset", "enabled", "disabled"] as const
export const telemetrySettingsSchema = z.enum(telemetrySettings)
export type TelemetrySetting = z.infer<typeof telemetrySettingsSchema>
/**
* TelemetryEventName
*/
export enum TelemetryEventName {
TASK_CREATED = "Task Created",
TASK_RESTARTED = "Task Reopened",
TASK_COMPLETED = "Task Completed",
TASK_MESSAGE = "Task Message",
TASK_CONVERSATION_MESSAGE = "Conversation Message",
LLM_COMPLETION = "LLM Completion",
MODE_SWITCH = "Mode Switched",
MODE_SELECTOR_OPENED = "Mode Selector Opened",
TOOL_USED = "Tool Used",
CHECKPOINT_CREATED = "Checkpoint Created",
CHECKPOINT_RESTORED = "Checkpoint Restored",
CHECKPOINT_DIFFED = "Checkpoint Diffed",
TAB_SHOWN = "Tab Shown",
MODE_SETTINGS_CHANGED = "Mode Setting Changed",
CUSTOM_MODE_CREATED = "Custom Mode Created",
CONTEXT_CONDENSED = "Context Condensed",
SLIDING_WINDOW_TRUNCATION = "Sliding Window Truncation",
CODE_ACTION_USED = "Code Action Used",
PROMPT_ENHANCED = "Prompt Enhanced",
TITLE_BUTTON_CLICKED = "Title Button Clicked",
AUTHENTICATION_INITIATED = "Authentication Initiated",
MARKETPLACE_ITEM_INSTALLED = "Marketplace Item Installed",
MARKETPLACE_ITEM_REMOVED = "Marketplace Item Removed",
MARKETPLACE_TAB_VIEWED = "Marketplace Tab Viewed",
MARKETPLACE_INSTALL_BUTTON_CLICKED = "Marketplace Install Button Clicked",
SHARE_BUTTON_CLICKED = "Share Button Clicked",
SHARE_ORGANIZATION_CLICKED = "Share Organization Clicked",
SHARE_PUBLIC_CLICKED = "Share Public Clicked",
SHARE_CONNECT_TO_CLOUD_CLICKED = "Share Connect To Cloud Clicked",
ACCOUNT_CONNECT_CLICKED = "Account Connect Clicked",
ACCOUNT_CONNECT_SUCCESS = "Account Connect Success",
ACCOUNT_LOGOUT_CLICKED = "Account Logout Clicked",
ACCOUNT_LOGOUT_SUCCESS = "Account Logout Success",
FEATURED_PROVIDER_CLICKED = "Featured Provider Clicked",
UPSELL_DISMISSED = "Upsell Dismissed",
UPSELL_CLICKED = "Upsell Clicked",
SCHEMA_VALIDATION_ERROR = "Schema Validation Error",
DIFF_APPLICATION_ERROR = "Diff Application Error",
SHELL_INTEGRATION_ERROR = "Shell Integration Error",
CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error",
CODE_INDEX_ERROR = "Code Index Error",
TELEMETRY_SETTINGS_CHANGED = "Telemetry Settings Changed",
MODEL_CACHE_EMPTY_RESPONSE = "Model Cache Empty Response",
READ_FILE_LEGACY_FORMAT_USED = "Read File Legacy Format Used",
}
/**
* TelemetryProperties
*/
export const staticAppPropertiesSchema = z.object({
appName: z.string(),
appVersion: z.string(),
vscodeVersion: z.string(),
platform: z.string(),
editorName: z.string(),
hostname: z.string().optional(),
})
export type StaticAppProperties = z.infer<typeof staticAppPropertiesSchema>
export const dynamicAppPropertiesSchema = z.object({
language: z.string(),
mode: z.string(),
})
export type DynamicAppProperties = z.infer<typeof dynamicAppPropertiesSchema>
export const cloudAppPropertiesSchema = z.object({
cloudIsAuthenticated: z.boolean().optional(),
})
export type CloudAppProperties = z.infer<typeof cloudAppPropertiesSchema>
export const appPropertiesSchema = z.object({
...staticAppPropertiesSchema.shape,
...dynamicAppPropertiesSchema.shape,
...cloudAppPropertiesSchema.shape,
})
export type AppProperties = z.infer<typeof appPropertiesSchema>
export const taskPropertiesSchema = z.object({
taskId: z.string().optional(),
parentTaskId: z.string().optional(),
apiProvider: z.enum(providerNames).optional(),
modelId: z.string().optional(),
diffStrategy: z.string().optional(),
isSubtask: z.boolean().optional(),
todos: z
.object({
total: z.number(),
completed: z.number(),
inProgress: z.number(),
pending: z.number(),
})
.optional(),
})
export type TaskProperties = z.infer<typeof taskPropertiesSchema>
export const gitPropertiesSchema = z.object({
repositoryUrl: z.string().optional(),
repositoryName: z.string().optional(),
defaultBranch: z.string().optional(),
})
export type GitProperties = z.infer<typeof gitPropertiesSchema>
export const telemetryPropertiesSchema = z.object({
...appPropertiesSchema.shape,
...taskPropertiesSchema.shape,
...gitPropertiesSchema.shape,
})
export type TelemetryProperties = z.infer<typeof telemetryPropertiesSchema>
/**
* TelemetryEvent
*/
export type TelemetryEvent = {
event: TelemetryEventName
// eslint-disable-next-line @typescript-eslint/no-explicit-any
properties?: Record<string, any>
}
/**
* RooCodeTelemetryEvent
*/
export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
z.object({
type: z.enum([
TelemetryEventName.TASK_CREATED,
TelemetryEventName.TASK_RESTARTED,
TelemetryEventName.TASK_COMPLETED,
TelemetryEventName.TASK_CONVERSATION_MESSAGE,
TelemetryEventName.MODE_SWITCH,
TelemetryEventName.MODE_SELECTOR_OPENED,
TelemetryEventName.TOOL_USED,
TelemetryEventName.CHECKPOINT_CREATED,
TelemetryEventName.CHECKPOINT_RESTORED,
TelemetryEventName.CHECKPOINT_DIFFED,
TelemetryEventName.CODE_ACTION_USED,
TelemetryEventName.PROMPT_ENHANCED,
TelemetryEventName.TITLE_BUTTON_CLICKED,
TelemetryEventName.AUTHENTICATION_INITIATED,
TelemetryEventName.MARKETPLACE_ITEM_INSTALLED,
TelemetryEventName.MARKETPLACE_ITEM_REMOVED,
TelemetryEventName.MARKETPLACE_TAB_VIEWED,
TelemetryEventName.MARKETPLACE_INSTALL_BUTTON_CLICKED,
TelemetryEventName.SHARE_BUTTON_CLICKED,
TelemetryEventName.SHARE_ORGANIZATION_CLICKED,
TelemetryEventName.SHARE_PUBLIC_CLICKED,
TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED,
TelemetryEventName.ACCOUNT_CONNECT_CLICKED,
TelemetryEventName.ACCOUNT_CONNECT_SUCCESS,
TelemetryEventName.ACCOUNT_LOGOUT_CLICKED,
TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS,
TelemetryEventName.FEATURED_PROVIDER_CLICKED,
TelemetryEventName.UPSELL_DISMISSED,
TelemetryEventName.UPSELL_CLICKED,
TelemetryEventName.SCHEMA_VALIDATION_ERROR,
TelemetryEventName.DIFF_APPLICATION_ERROR,
TelemetryEventName.SHELL_INTEGRATION_ERROR,
TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR,
TelemetryEventName.CODE_INDEX_ERROR,
TelemetryEventName.MODEL_CACHE_EMPTY_RESPONSE,
TelemetryEventName.CONTEXT_CONDENSED,
TelemetryEventName.SLIDING_WINDOW_TRUNCATION,
TelemetryEventName.TAB_SHOWN,
TelemetryEventName.MODE_SETTINGS_CHANGED,
TelemetryEventName.CUSTOM_MODE_CREATED,
TelemetryEventName.READ_FILE_LEGACY_FORMAT_USED,
]),
properties: telemetryPropertiesSchema,
}),
z.object({
type: z.literal(TelemetryEventName.TELEMETRY_SETTINGS_CHANGED),
properties: z.object({
...telemetryPropertiesSchema.shape,
previousSetting: telemetrySettingsSchema,
newSetting: telemetrySettingsSchema,
}),
}),
z.object({
type: z.literal(TelemetryEventName.TASK_MESSAGE),
properties: z.object({
...telemetryPropertiesSchema.shape,
taskId: z.string(),
message: clineMessageSchema,
}),
}),
z.object({
type: z.literal(TelemetryEventName.LLM_COMPLETION),
properties: z.object({
...telemetryPropertiesSchema.shape,
inputTokens: z.number(),
outputTokens: z.number(),
cacheReadTokens: z.number().optional(),
cacheWriteTokens: z.number().optional(),
cost: z.number().optional(),
}),
}),
])
export type RooCodeTelemetryEvent = z.infer<typeof rooCodeTelemetryEventSchema>
/**
* TelemetryEventSubscription
*/
export type TelemetryEventSubscription =
| { type: "include"; events: TelemetryEventName[] }
| { type: "exclude"; events: TelemetryEventName[] }
/**
* TelemetryPropertiesProvider
*/
export interface TelemetryPropertiesProvider {
getTelemetryProperties(): Promise<TelemetryProperties>
}
/**
* TelemetryClient
*/
export interface TelemetryClient {
subscription?: TelemetryEventSubscription
setProvider(provider: TelemetryPropertiesProvider): void
capture(options: TelemetryEvent): Promise<void>
captureException(error: Error, additionalProperties?: Record<string, unknown>): Promise<void>
updateTelemetryState(isOptedIn: boolean): void
isTelemetryEnabled(): boolean
shutdown(): Promise<void>
}
/**
* Expected API error codes that should not be reported to telemetry.
* These are normal/expected errors that users can't do much about.
*/
export const EXPECTED_API_ERROR_CODES = new Set([
402, // Payment required - billing issues
429, // Rate limit - expected when hitting API limits
])
/**
* Patterns in error messages that indicate expected errors (rate limits, etc.)
* These are checked when no numeric error code is available.
*/
const EXPECTED_ERROR_MESSAGE_PATTERNS = [
/^429\b/, // Message starts with "429"
/rate limit/i, // Contains "rate limit" (case insensitive)
]
/**
* Interface representing the error structure from OpenAI SDK.
* OpenAI SDK errors (APIError, AuthenticationError, RateLimitError, etc.)
* have a numeric `status` property and may contain nested error metadata.
*
* @see https://github.com/openai/openai-node/blob/master/src/error.ts
*/
interface OpenAISdkError {
/** HTTP status code of the error response */
status: number
/** Optional error code (may be numeric or string) */
code?: number | string
/** Primary error message */
message: string
/** Nested error object containing additional details from the API response */
error?: {
message?: string
metadata?: {
/** Raw error message from upstream provider (e.g., OpenRouter upstream errors) */
raw?: string
}
}
}
/**
* Type guard to check if an error object is an OpenAI SDK error.
* OpenAI SDK errors (APIError and subclasses) have: status, code, message properties.
*/
function isOpenAISdkError(error: unknown): error is OpenAISdkError {
return (
typeof error === "object" &&
error !== null &&
"status" in error &&
typeof (error as OpenAISdkError).status === "number"
)
}
/**
* Extracts the HTTP status code from an error object.
* Supports OpenAI SDK errors that have a status property.
* @param error - The error to extract status from
* @returns The status code if available, undefined otherwise
*/
export function getErrorStatusCode(error: unknown): number | undefined {
if (isOpenAISdkError(error)) {
return error.status
}
return undefined
}
/**
* Extracts a message from a JSON payload embedded in an error string.
* Handles cases like "503 {"error":{"message":"actual error message"}}"
* or just '{"error":{"message":"actual error message"}}'
*
* @param message - The message string that may contain JSON
* @returns The extracted message from the JSON payload, or undefined if not found
*/
export function extractMessageFromJsonPayload(message: string): string | undefined {
// Find the first occurrence of '{' which may indicate JSON content
const jsonStartIndex = message.indexOf("{")
if (jsonStartIndex === -1) {
return undefined
}
const potentialJson = message.slice(jsonStartIndex)
try {
const parsed = JSON.parse(potentialJson)
// Handle structure: {"error":{"message":"..."}} or {"error":{"code":"","message":"..."}}
if (parsed?.error?.message && typeof parsed.error.message === "string") {
return parsed.error.message
}
// Handle structure: {"message":"..."}
if (parsed?.message && typeof parsed.message === "string") {
return parsed.message
}
} catch {
// JSON parsing failed - not valid JSON
}
return undefined
}
/**
* Extracts the most descriptive error message from an error object.
* Prioritizes nested metadata (upstream provider errors) over the standard message.
* Also handles JSON payloads embedded in error messages.
* @param error - The error to extract message from
* @returns The best available error message, or undefined if not extractable
*/
export function getErrorMessage(error: unknown): string | undefined {
let message: string | undefined
if (isOpenAISdkError(error)) {
// Prioritize nested metadata which may contain upstream provider details
message = error.error?.metadata?.raw || error.error?.message || error.message
} else if (error instanceof Error) {
// Handle standard Error objects (including ApiProviderError)
message = error.message
} else if (typeof error === "object" && error !== null && "message" in error) {
// Handle plain objects with a message property
const msgValue = (error as { message: unknown }).message
if (typeof msgValue === "string") {
message = msgValue
}
}
if (!message) {
return undefined
}
// If the message contains JSON, try to extract the message from it
const extractedMessage = extractMessageFromJsonPayload(message)
if (extractedMessage) {
return extractedMessage
}
return message
}
/**
* Helper to check if an API error should be reported to telemetry.
* Filters out expected errors like rate limits by checking both error codes and messages.
* @param errorCode - The HTTP error code (if available)
* @param errorMessage - The error message (if available)
* @returns true if the error should be reported, false if it should be filtered out
*/
export function shouldReportApiErrorToTelemetry(errorCode?: number, errorMessage?: string): boolean {
// Check numeric error code
if (errorCode !== undefined && EXPECTED_API_ERROR_CODES.has(errorCode)) {
return false
}
// Check error message for expected patterns (e.g., "429 Rate limit exceeded")
if (errorMessage) {
for (const pattern of EXPECTED_ERROR_MESSAGE_PATTERNS) {
if (pattern.test(errorMessage)) {
return false
}
}
}
return true
}
/**
* Generic API provider error class for structured error tracking via PostHog.
* Can be reused by any API provider.
*/
export class ApiProviderError extends Error {
constructor(
message: string,
public readonly provider: string,
public readonly modelId: string,
public readonly operation: string,
public readonly errorCode?: number,
) {
super(message)
this.name = "ApiProviderError"
}
}
/**
* Type guard to check if an error is an ApiProviderError.
* Used by telemetry to automatically extract structured properties.
*/
export function isApiProviderError(error: unknown): error is ApiProviderError {
return (
error instanceof Error &&
error.name === "ApiProviderError" &&
"provider" in error &&
"modelId" in error &&
"operation" in error
)
}
/**
* Extracts properties from an ApiProviderError for telemetry.
* Returns the structured properties that can be merged with additionalProperties.
*/
export function extractApiProviderErrorProperties(error: ApiProviderError): Record<string, unknown> {
return {
provider: error.provider,
modelId: error.modelId,
operation: error.operation,
...(error.errorCode !== undefined && { errorCode: error.errorCode }),
}
}
/**
* Reason why the consecutive mistake limit was reached.
*/
export type ConsecutiveMistakeReason = "no_tools_used" | "tool_repetition" | "unknown"
/**
* Error class for "Roo is having trouble" consecutive mistake scenarios.
* Triggered when the task reaches the configured consecutive mistake limit.
* Used for structured exception tracking via PostHog.
*/
export class ConsecutiveMistakeError extends Error {
constructor(
message: string,
public readonly taskId: string,
public readonly consecutiveMistakeCount: number,
public readonly consecutiveMistakeLimit: number,
public readonly reason: ConsecutiveMistakeReason = "unknown",
public readonly provider?: string,
public readonly modelId?: string,
) {
super(message)
this.name = "ConsecutiveMistakeError"
}
}
/**
* Type guard to check if an error is a ConsecutiveMistakeError.
* Used by telemetry to automatically extract structured properties.
*/
export function isConsecutiveMistakeError(error: unknown): error is ConsecutiveMistakeError {
return (
error instanceof Error &&
error.name === "ConsecutiveMistakeError" &&
"taskId" in error &&
"consecutiveMistakeCount" in error &&
"consecutiveMistakeLimit" in error
)
}
/**
* Extracts properties from a ConsecutiveMistakeError for telemetry.
* Returns the structured properties that can be merged with additionalProperties.
*/
export function extractConsecutiveMistakeErrorProperties(error: ConsecutiveMistakeError): Record<string, unknown> {
return {
taskId: error.taskId,
consecutiveMistakeCount: error.consecutiveMistakeCount,
consecutiveMistakeLimit: error.consecutiveMistakeLimit,
reason: error.reason,
...(error.provider !== undefined && { provider: error.provider }),
...(error.modelId !== undefined && { modelId: error.modelId }),
}
}

View file

@ -4,7 +4,6 @@ import type { GlobalSettings, RooCodeSettings } from "./global-settings.js"
import type { ProviderSettings, ProviderSettingsEntry } from "./provider-settings.js"
import type { HistoryItem } from "./history.js"
import type { ModeConfig, PromptComponent } from "./mode.js"
import type { TelemetrySetting } from "./telemetry.js"
import type { Experiments } from "./experiment.js"
import type { ClineMessage, QueuedMessage } from "./message.js"
import {
@ -340,10 +339,6 @@ export type ExtensionState = Pick<
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true})
cwd?: string // Current working directory
telemetrySetting: TelemetrySetting
telemetryKey?: string
machineId?: string
renderContext: "sidebar" | "editor"
settingsImportedAt?: number
historyPreviewCollapsed?: boolean
@ -493,7 +488,6 @@ export interface WebviewMessage {
| "checkpointRestore"
| "deleteMcpServer"
| "codebaseIndexEnabled"
| "telemetrySetting"
| "searchFiles"
| "toggleApiConfigPin"
| "hasOpenedModeSelector"

353
pnpm-lock.yaml generated
View file

@ -388,9 +388,6 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
posthog-js:
specifier: ^1.336.4
version: 1.336.4
react:
specifier: ^18.3.1
version: 18.3.1
@ -679,34 +676,6 @@ importers:
specifier: ^3.2.3
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
packages/telemetry:
dependencies:
'@roo-code/types':
specifier: workspace:^
version: link:../types
posthog-node:
specifier: ^5.0.0
version: 5.1.1
zod:
specifier: 3.25.76
version: 3.25.76
devDependencies:
'@roo-code/config-eslint':
specifier: workspace:^
version: link:../config-eslint
'@roo-code/config-typescript':
specifier: workspace:^
version: link:../config-typescript
'@types/node':
specifier: 20.x
version: 20.17.57
'@types/vscode':
specifier: ^1.84.0
version: 1.100.0
vitest:
specifier: ^3.2.3
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
packages/types:
dependencies:
ai-sdk-provider-poe:
@ -818,9 +787,6 @@ importers:
'@roo-code/ipc':
specifier: workspace:^
version: link:../packages/ipc
'@roo-code/telemetry':
specifier: workspace:^
version: link:../packages/telemetry
'@roo-code/types':
specifier: workspace:^
version: link:../packages/types
@ -1266,9 +1232,6 @@ importers:
mermaid:
specifier: ^11.4.1
version: 11.10.0
posthog-js:
specifier: ^1.227.2
version: 1.242.1
pretty-bytes:
specifier: ^7.0.0
version: 7.0.0
@ -2877,78 +2840,10 @@ packages:
ai: ^6.0.0
zod: 3.25.76
'@opentelemetry/api-logs@0.208.0':
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
engines: {node: '>=8.0.0'}
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
'@opentelemetry/core@2.2.0':
resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/core@2.5.0':
resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/exporter-logs-otlp-http@0.208.0':
resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-exporter-base@0.208.0':
resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-transformer@0.208.0':
resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/resources@2.2.0':
resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/resources@2.5.0':
resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-logs@0.208.0':
resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.4.0 <1.10.0'
'@opentelemetry/sdk-metrics@2.2.0':
resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.9.0 <1.10.0'
'@opentelemetry/sdk-trace-base@2.2.0':
resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/semantic-conventions@1.39.0':
resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==}
engines: {node: '>=14'}
'@oxc-resolver/binding-darwin-arm64@11.2.0':
resolution: {integrity: sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==}
cpu: [arm64]
@ -3020,42 +2915,6 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/core@1.17.0':
resolution: {integrity: sha512-8pDNL+/u9ojzXloA5wILVDXBCV5daJ7w2ipCALQlEEZmL752cCKhRpbyiHn3tjKXh3Hy6aOboJneYa1JdlVHrQ==}
'@posthog/types@1.336.4':
resolution: {integrity: sha512-BY3cq/8segbXEvHbEXx9SWmaKJEM0AGgsOgMFH2yy13AV+rUHsGcp4Z5LDI5pU25DURN9EAZvzcoVyYy/Iokmw==}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
'@protobufjs/base64@1.1.2':
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
'@protobufjs/codegen@2.0.4':
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
'@protobufjs/eventemitter@1.1.0':
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
'@protobufjs/fetch@1.1.0':
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
'@protobufjs/float@1.0.2':
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
'@protobufjs/inquire@1.1.0':
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
'@protobufjs/path@1.1.2':
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
'@protobufjs/pool@1.1.0':
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@puppeteer/browsers@2.10.5':
resolution: {integrity: sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==}
engines: {node: '>=18'}
@ -5625,9 +5484,6 @@ packages:
resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==}
hasBin: true
core-js@3.42.0:
resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -6087,9 +5943,6 @@ packages:
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@ -6690,9 +6543,6 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.7.4:
resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
@ -8040,9 +7890,6 @@ packages:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@ -9012,30 +8859,6 @@ packages:
resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==}
engines: {node: '>=12'}
posthog-js@1.242.1:
resolution: {integrity: sha512-j2mzw0eukyuw/Qm3tNZ6pfaXmc7eglWj6ftmvR1Lz9GtMr85ndGNXJvIGO+6PBrQW2o0D1G0k/KV93ehta0hFA==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
posthog-js@1.336.4:
resolution: {integrity: sha512-NX81XaqOjS/gue3UsbAAuJxi6vD0AGy1HUvywBIhAArCwbTXKS04NhEFwUcYJdrmwXUf94MntEIWGoc1pTFDtg==}
posthog-node@5.1.1:
resolution: {integrity: sha512-6VISkNdxO24ehXiDA4dugyCSIV7lpGVaEu5kn/dlAj+SJ1lgcDru9PQ8p/+GSXsXVxohd1t7kHL2JKc9NoGb0w==}
engines: {node: '>=20'}
preact@10.26.6:
resolution: {integrity: sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==}
preact@10.28.2:
resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@ -9105,10 +8928,6 @@ packages:
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
protobufjs@7.5.4:
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
engines: {node: '>=12.0.0'}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@ -9154,9 +8973,6 @@ packages:
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
query-selector-shadow-dom@1.0.1:
resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -10833,12 +10649,6 @@ packages:
web-tree-sitter@0.25.6:
resolution: {integrity: sha512-WG+/YGbxw8r+rLlzzhV+OvgiOJCWdIpOucG3qBf3RCBFMkGDb1CanUi2BxCxjnkpzU3/hLWPT8VO5EKsMk9Fxg==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
web-vitals@5.1.0:
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -12999,82 +12809,8 @@ snapshots:
ai: 6.0.77(zod@3.25.76)
zod: 3.25.76
'@opentelemetry/api-logs@0.208.0':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api@1.9.0': {}
'@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
protobufjs: 7.5.4
'@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/semantic-conventions@1.39.0': {}
'@oxc-resolver/binding-darwin-arm64@11.2.0':
optional: true
@ -13121,35 +12857,6 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@posthog/core@1.17.0':
dependencies:
cross-spawn: 7.0.6
'@posthog/types@1.336.4': {}
'@protobufjs/aspromise@1.1.2': {}
'@protobufjs/base64@1.1.2': {}
'@protobufjs/codegen@2.0.4': {}
'@protobufjs/eventemitter@1.1.0': {}
'@protobufjs/fetch@1.1.0':
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/float@1.0.2': {}
'@protobufjs/inquire@1.1.0': {}
'@protobufjs/path@1.1.2': {}
'@protobufjs/pool@1.1.0': {}
'@protobufjs/utf8@1.1.0': {}
'@puppeteer/browsers@2.10.5':
dependencies:
debug: 4.4.1(supports-color@8.1.1)
@ -16008,8 +15715,6 @@ snapshots:
untildify: 4.0.0
yargs: 16.2.0
core-js@3.42.0: {}
core-util-is@1.0.3: {}
cors@2.8.5:
@ -16458,10 +16163,6 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.3.1:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
@ -17172,8 +16873,6 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.4.8: {}
fflate@0.7.4: {}
fflate@0.8.2: {}
@ -18631,8 +18330,6 @@ snapshots:
strip-ansi: 7.1.2
wrap-ansi: 9.0.0
long@5.3.2: {}
longest-streak@3.1.0: {}
loose-envify@1.4.0:
@ -19877,35 +19574,6 @@ snapshots:
postgres@3.4.7: {}
posthog-js@1.242.1:
dependencies:
core-js: 3.42.0
fflate: 0.4.8
preact: 10.26.6
web-vitals: 4.2.4
posthog-js@1.336.4:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
'@posthog/core': 1.17.0
'@posthog/types': 1.336.4
core-js: 3.42.0
dompurify: 3.3.1
fflate: 0.4.8
preact: 10.28.2
query-selector-shadow-dom: 1.0.1
web-vitals: 5.1.0
posthog-node@5.1.1: {}
preact@10.26.6: {}
preact@10.28.2: {}
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.0.4
@ -19979,21 +19647,6 @@ snapshots:
property-information@7.1.0: {}
protobufjs@7.5.4:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
'@protobufjs/codegen': 2.0.4
'@protobufjs/eventemitter': 1.1.0
'@protobufjs/fetch': 1.1.0
'@protobufjs/float': 1.0.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 24.2.1
long: 5.3.2
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@ -20073,8 +19726,6 @@ snapshots:
quansync@0.2.11: {}
query-selector-shadow-dom@1.0.1: {}
queue-microtask@1.2.3: {}
randombytes@2.1.0:
@ -22181,10 +21832,6 @@ snapshots:
web-tree-sitter@0.25.6: {}
web-vitals@4.2.4: {}
web-vitals@5.1.0: {}
webidl-conversions@3.0.1: {}
webidl-conversions@4.0.2: {}

View file

@ -72,24 +72,6 @@ vi.mock("@roo-code/cloud", () => ({
getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
createInstance: vi.fn().mockReturnValue({
register: vi.fn(),
setProvider: vi.fn(),
shutdown: vi.fn(),
}),
get instance() {
return {
register: vi.fn(),
setProvider: vi.fn(),
shutdown: vi.fn(),
}
},
},
PostHogTelemetryClient: vi.fn(),
}))
vi.mock("../utils/outputChannelLogger", () => ({
createOutputChannelLogger: vi.fn().mockReturnValue(vi.fn()),
createDualLogger: vi.fn().mockReturnValue(vi.fn()),
@ -276,7 +258,6 @@ describe("extension.ts", () => {
return {
off: vi.fn(),
on: vi.fn(),
telemetryClient: null,
authService: mockAuthService,
hasActiveSession: vi.fn().mockReturnValue(false),
} as any
@ -316,7 +297,6 @@ describe("extension.ts", () => {
return {
off: vi.fn(),
on: vi.fn(),
telemetryClient: null,
authService: null,
hasActiveSession: vi.fn().mockReturnValue(false),
} as any

View file

@ -4,18 +4,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"
import { RooCodeEventName } from "@roo-code/types"
// Mock safe-stable-stringify to avoid runtime error
vi.mock("safe-stable-stringify", () => ({
default: (obj: any) => JSON.stringify(obj),
}))
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTaskCompleted: vi.fn(),
},
},
}))
// vscode mock for Task/Provider imports
vi.mock("vscode", () => {

View file

@ -2,7 +2,6 @@ import * as vscode from "vscode"
import delay from "delay"
import type { CommandId } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Package } from "../shared/package"
import { getCommand } from "../utils/commands"
@ -78,8 +77,6 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
TelemetryService.instance.captureTitleButtonClicked("cloud")
visibleProvider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
},
plusButtonClicked: async () => {
@ -89,8 +86,6 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
TelemetryService.instance.captureTitleButtonClicked("plus")
await visibleProvider.removeClineFromStack()
await visibleProvider.refreshWorkspace()
await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
@ -99,8 +94,6 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" })
},
popoutButtonClicked: () => {
TelemetryService.instance.captureTitleButtonClicked("popout")
return openClineInNewTab({ context, outputChannel })
},
openInNewTab: () => openClineInNewTab({ context, outputChannel }),
@ -111,8 +104,6 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
TelemetryService.instance.captureTitleButtonClicked("settings")
visibleProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
// Also explicitly post the visibility message to trigger scroll reliably
visibleProvider.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
@ -124,8 +115,6 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
TelemetryService.instance.captureTitleButtonClicked("history")
visibleProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
},
marketplaceButtonClicked: () => {

View file

@ -3,15 +3,6 @@
import { AnthropicHandler } from "../anthropic"
import { ApiHandlerOptions } from "../../../shared/api"
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: vitest.fn(),
},
},
}))
const mockCreate = vitest.fn()
vitest.mock("@anthropic-ai/sdk", () => {

View file

@ -1,12 +1,4 @@
// Mock TelemetryService - must come before other imports
const mockCaptureException = vi.hoisted(() => vi.fn())
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: mockCaptureException,
},
},
}))
// Mock BedrockRuntimeClient and commands
const mockSend = vi.fn()

View file

@ -1,14 +1,3 @@
// Mock TelemetryService before other imports
const mockCaptureException = vi.fn()
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: (...args: unknown[]) => mockCaptureException(...args),
},
},
}))
// Mock AWS SDK credential providers
vi.mock("@aws-sdk/credential-providers", () => {
const mockFromIni = vi.fn().mockReturnValue({
@ -36,12 +25,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => {
import { AwsBedrockHandler } from "../bedrock"
import { ConverseStreamCommand, BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime"
import {
BEDROCK_1M_CONTEXT_MODEL_IDS,
BEDROCK_SERVICE_TIER_MODEL_IDS,
bedrockModels,
ApiProviderError,
} from "@roo-code/types"
import { BEDROCK_1M_CONTEXT_MODEL_IDS, BEDROCK_SERVICE_TIER_MODEL_IDS, bedrockModels } from "@roo-code/types"
import type { Anthropic } from "@anthropic-ai/sdk"
@ -1141,141 +1125,6 @@ describe("AwsBedrockHandler", () => {
})
})
describe("error telemetry", () => {
let mockSend: ReturnType<typeof vi.fn>
beforeEach(() => {
mockCaptureException.mockClear()
// Get access to the mock send function from the mocked client
mockSend = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value?.send
})
it("should capture telemetry on createMessage error", async () => {
// Create a handler with a fresh mock
const errorHandler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
})
// Get the mock send from the new handler instance
const clientInstance =
vi.mocked(BedrockRuntimeClient).mock.results[vi.mocked(BedrockRuntimeClient).mock.results.length - 1]
?.value
const mockSendFn = clientInstance?.send as ReturnType<typeof vi.fn>
// Mock the send to throw an error
mockSendFn.mockRejectedValueOnce(new Error("Bedrock API error"))
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Hello",
},
]
const generator = errorHandler.createMessage("You are a helpful assistant", messages)
// Consume the generator - it should throw
await expect(async () => {
for await (const _chunk of generator) {
// Should throw before or during iteration
}
}).rejects.toThrow()
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Bedrock API error",
provider: "Bedrock",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
operation: "createMessage",
}),
)
// Verify it's an ApiProviderError
const capturedError = mockCaptureException.mock.calls[0][0]
expect(capturedError).toBeInstanceOf(ApiProviderError)
})
it("should capture telemetry on completePrompt error", async () => {
// Create a handler with a fresh mock
const errorHandler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
})
// Get the mock send from the new handler instance
const clientInstance =
vi.mocked(BedrockRuntimeClient).mock.results[vi.mocked(BedrockRuntimeClient).mock.results.length - 1]
?.value
const mockSendFn = clientInstance?.send as ReturnType<typeof vi.fn>
// Mock the send to throw an error for ConverseCommand
mockSendFn.mockRejectedValueOnce(new Error("Bedrock completion error"))
// Call completePrompt - it should throw
await expect(errorHandler.completePrompt("Test prompt")).rejects.toThrow()
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Bedrock completion error",
provider: "Bedrock",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
operation: "completePrompt",
}),
)
// Verify it's an ApiProviderError
const capturedError = mockCaptureException.mock.calls[0][0]
expect(capturedError).toBeInstanceOf(ApiProviderError)
})
it("should still throw the error after capturing telemetry", async () => {
// Create a handler with a fresh mock
const errorHandler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
})
// Get the mock send from the new handler instance
const clientInstance =
vi.mocked(BedrockRuntimeClient).mock.results[vi.mocked(BedrockRuntimeClient).mock.results.length - 1]
?.value
const mockSendFn = clientInstance?.send as ReturnType<typeof vi.fn>
// Mock the send to throw an error
mockSendFn.mockRejectedValueOnce(new Error("Test error for throw verification"))
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Hello",
},
]
const generator = errorHandler.createMessage("You are a helpful assistant", messages)
// Verify the error is still thrown after telemetry capture
await expect(async () => {
for await (const _chunk of generator) {
// Should throw
}
}).rejects.toThrow()
// Telemetry should have been captured before the error was thrown
expect(mockCaptureException).toHaveBeenCalled()
})
})
describe("prompt cache default behavior", () => {
beforeEach(() => {
mockConverseStreamCommand.mockReset()

View file

@ -1,18 +1,8 @@
// npx vitest run src/api/providers/__tests__/gemini.spec.ts
const mockCaptureException = vitest.fn()
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: (...args: unknown[]) => mockCaptureException(...args),
},
},
}))
import { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, geminiDefaultModelId, ApiProviderError } from "@roo-code/types"
import { type ModelInfo, geminiDefaultModelId } from "@roo-code/types"
import { t } from "i18next"
import { GeminiHandler } from "../gemini"
@ -24,7 +14,6 @@ describe("GeminiHandler", () => {
beforeEach(() => {
// Reset mocks
mockCaptureException.mockClear()
// Create mock functions
const mockGenerateContentStream = vitest.fn()
@ -256,82 +245,4 @@ describe("GeminiHandler", () => {
expect(cost).toBeUndefined()
})
})
describe("error telemetry", () => {
const mockMessages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Hello",
},
]
const systemPrompt = "You are a helpful assistant"
it("should capture telemetry on createMessage error", async () => {
const mockError = new Error("Gemini API error")
;(handler["client"].models.generateContentStream as any).mockRejectedValue(mockError)
const stream = handler.createMessage(systemPrompt, mockMessages)
await expect(async () => {
for await (const _chunk of stream) {
// Should throw before yielding any chunks
}
}).rejects.toThrow()
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Gemini API error",
provider: "Gemini",
modelId: GEMINI_MODEL_NAME,
operation: "createMessage",
}),
)
// Verify it's an ApiProviderError
const capturedError = mockCaptureException.mock.calls[0][0]
expect(capturedError).toBeInstanceOf(ApiProviderError)
})
it("should capture telemetry on completePrompt error", async () => {
const mockError = new Error("Gemini completion error")
;(handler["client"].models.generateContent as any).mockRejectedValue(mockError)
await expect(handler.completePrompt("Test prompt")).rejects.toThrow()
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Gemini completion error",
provider: "Gemini",
modelId: GEMINI_MODEL_NAME,
operation: "completePrompt",
}),
)
// Verify it's an ApiProviderError
const capturedError = mockCaptureException.mock.calls[0][0]
expect(capturedError).toBeInstanceOf(ApiProviderError)
})
it("should still throw the error after capturing telemetry", async () => {
const mockError = new Error("Gemini API error")
;(handler["client"].models.generateContentStream as any).mockRejectedValue(mockError)
const stream = handler.createMessage(systemPrompt, mockMessages)
// Verify the error is still thrown
await expect(async () => {
for await (const _chunk of stream) {
// Should throw
}
}).rejects.toThrow()
// Telemetry should have been captured before the error was thrown
expect(mockCaptureException).toHaveBeenCalled()
})
})
})

View file

@ -1,12 +1,4 @@
// Mock TelemetryService - must come before other imports
const mockCaptureException = vi.hoisted(() => vi.fn())
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: mockCaptureException,
},
},
}))
// Mock Mistral client - must come before other imports
const mockCreate = vi.fn()

View file

@ -1,19 +1,9 @@
// npx vitest run api/providers/__tests__/openai-native.spec.ts
const mockCaptureException = vitest.fn()
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: (...args: unknown[]) => mockCaptureException(...args),
},
},
}))
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiProviderError } from "@roo-code/types"
import {} from "@roo-code/types"
import { OpenAiNativeHandler } from "../openai-native"
import { ApiHandlerOptions } from "../../../shared/api"
@ -50,7 +40,6 @@ describe("OpenAiNativeHandler", () => {
}
handler = new OpenAiNativeHandler(mockOptions)
mockResponsesCreate.mockClear()
mockCaptureException.mockClear()
// Clear fetch mock if it exists
if ((global as any).fetch) {
delete (global as any).fetch
@ -1113,150 +1102,6 @@ describe("OpenAiNativeHandler", () => {
}
})
})
describe("error telemetry", () => {
const errorMessages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Hello",
},
]
const errorSystemPrompt = "You are a helpful assistant"
beforeEach(() => {
mockCaptureException.mockClear()
})
it("should capture telemetry on createMessage error", async () => {
// Mock fetch to return error
const mockFetch = vitest.fn().mockResolvedValue({
ok: false,
status: 500,
text: async () => "Internal Server Error",
})
global.fetch = mockFetch as any
// Mock SDK to fail so it falls back to fetch
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const stream = handler.createMessage(errorSystemPrompt, errorMessages)
await expect(async () => {
for await (const _chunk of stream) {
// Should throw before yielding any chunks
}
}).rejects.toThrow()
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("OpenAI service error"),
provider: "OpenAI Native",
modelId: "gpt-4.1",
operation: "createMessage",
}),
)
// Verify it's an ApiProviderError
const capturedError = mockCaptureException.mock.calls[0][0]
expect(capturedError).toBeInstanceOf(ApiProviderError)
})
it("should capture telemetry on stream processing error", async () => {
// Mock fetch to return a stream with an error event
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'data: {"type":"response.error","error":{"message":"Model overloaded"}}\n\n',
),
)
controller.close()
},
}),
})
global.fetch = mockFetch as any
// Mock SDK to fail so it falls back to fetch
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const stream = handler.createMessage(errorSystemPrompt, errorMessages)
await expect(async () => {
for await (const _chunk of stream) {
// Should throw when encountering error event
}
}).rejects.toThrow()
// Verify telemetry was captured (may be called multiple times due to error propagation)
expect(mockCaptureException).toHaveBeenCalled()
// Find the call with the stream error message
const streamErrorCall = mockCaptureException.mock.calls.find((call: any[]) =>
call[0]?.message?.includes("Model overloaded"),
)
expect(streamErrorCall).toBeDefined()
expect(streamErrorCall![0]).toMatchObject({
provider: "OpenAI Native",
modelId: "gpt-4.1",
operation: "createMessage",
})
// Verify it's an ApiProviderError
expect(streamErrorCall![0]).toBeInstanceOf(ApiProviderError)
})
it("should capture telemetry on completePrompt error", async () => {
// Mock SDK to throw an error
mockResponsesCreate.mockRejectedValue(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow()
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "API Error",
provider: "OpenAI Native",
modelId: "gpt-4.1",
operation: "completePrompt",
}),
)
// Verify it's an ApiProviderError
const capturedError = mockCaptureException.mock.calls[0][0]
expect(capturedError).toBeInstanceOf(ApiProviderError)
})
it("should still throw the error after capturing telemetry", async () => {
// Mock fetch to return error
const mockFetch = vitest.fn().mockResolvedValue({
ok: false,
status: 500,
text: async () => "Internal Server Error",
})
global.fetch = mockFetch as any
// Mock SDK to fail
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const stream = handler.createMessage(errorSystemPrompt, errorMessages)
// Verify the error is still thrown
await expect(async () => {
for await (const _chunk of stream) {
// Should throw
}
}).rejects.toThrow()
// Telemetry should have been captured before the error was thrown
expect(mockCaptureException).toHaveBeenCalled()
})
})
})
// Additional tests for GPT-5 streaming event coverage

View file

@ -12,16 +12,6 @@ import { Package } from "../../../shared/package"
vitest.mock("openai")
vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
const mockCaptureException = vitest.fn()
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: (...args: unknown[]) => mockCaptureException(...args),
},
},
}))
vitest.mock("../fetchers/modelCache", () => ({
getModels: vitest.fn().mockImplementation(() => {
return Promise.resolve({
@ -312,7 +302,7 @@ describe("OpenRouterHandler", () => {
)
})
it("handles API errors and captures telemetry", async () => {
it("handles API errors", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
@ -327,20 +317,9 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "API Error",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
errorCode: 500,
status: 500,
}),
)
})
it("captures telemetry when createMessage throws an exception", async () => {
it("propagates createMessage exceptions", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("Connection failed"))
;(OpenAI as any).prototype.chat = {
@ -349,18 +328,9 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow()
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Connection failed",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
)
})
it("passes SDK exceptions with status 429 to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates SDK exceptions with status 429", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Rate limit exceeded: free-models-per-day") as any
error.status = 429
@ -372,18 +342,9 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("Rate limit exceeded")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
)
})
it("passes SDK exceptions with 429 in message to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates SDK exceptions with 429 in the message", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("429 Rate limit exceeded: free-models-per-day")
const mockCreate = vitest.fn().mockRejectedValue(error)
@ -393,18 +354,9 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("429 Rate limit exceeded")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "429 Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
)
})
it("passes SDK exceptions containing 'rate limit' to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates SDK exceptions containing 'rate limit'", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Request failed due to rate limit")
const mockCreate = vitest.fn().mockRejectedValue(error)
@ -414,18 +366,9 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("rate limit")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Request failed due to rate limit",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
)
})
it("passes 429 rate limit errors from stream to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates 429 rate limit errors from stream", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
@ -440,17 +383,6 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 429: Rate limit exceeded")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
errorCode: 429,
status: 429,
}),
)
})
it("yields tool_call_end events when finish_reason is tool_calls", async () => {
@ -553,7 +485,7 @@ describe("OpenRouterHandler", () => {
)
})
it("handles API errors and captures telemetry", async () => {
it("handles API errors", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockError = {
error: {
@ -568,21 +500,9 @@ describe("OpenRouterHandler", () => {
} as any
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenRouter API Error 500: API Error")
// Verify telemetry was captured
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "API Error",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
errorCode: 500,
status: 500,
}),
)
})
it("handles unexpected errors and captures telemetry", async () => {
it("handles unexpected errors", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Unexpected error")
const mockCreate = vitest.fn().mockRejectedValue(error)
@ -591,19 +511,9 @@ describe("OpenRouterHandler", () => {
} as any
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Unexpected error")
// Verify telemetry was captured (filtering now happens inside PostHogTelemetryClient)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Unexpected error",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
)
})
it("passes SDK exceptions with status 429 to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates SDK exceptions with status 429", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Rate limit exceeded: free-models-per-day") as any
error.status = 429
@ -613,19 +523,9 @@ describe("OpenRouterHandler", () => {
} as any
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Rate limit exceeded")
// captureException is called, but PostHogTelemetryClient filters out 429 errors internally
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
)
})
it("passes SDK exceptions with 429 in message to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates SDK exceptions with 429 in the message", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("429 Rate limit exceeded: free-models-per-day")
const mockCreate = vitest.fn().mockRejectedValue(error)
@ -634,19 +534,9 @@ describe("OpenRouterHandler", () => {
} as any
await expect(handler.completePrompt("test prompt")).rejects.toThrow("429 Rate limit exceeded")
// captureException is called, but PostHogTelemetryClient filters out 429 errors internally
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "429 Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
)
})
it("passes SDK exceptions containing 'rate limit' to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates SDK exceptions containing 'rate limit'", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Request failed due to rate limit")
const mockCreate = vitest.fn().mockRejectedValue(error)
@ -655,19 +545,9 @@ describe("OpenRouterHandler", () => {
} as any
await expect(handler.completePrompt("test prompt")).rejects.toThrow("rate limit")
// captureException is called, but PostHogTelemetryClient filters out rate limit errors internally
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Request failed due to rate limit",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
)
})
it("passes 429 rate limit errors from response to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
it("propagates 429 rate limit errors from response", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockError = {
error: {
@ -684,18 +564,6 @@ describe("OpenRouterHandler", () => {
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
"OpenRouter API Error 429: Rate limit exceeded",
)
// captureException is called, but PostHogTelemetryClient filters out 429 errors internally
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded",
provider: "OpenRouter",
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
errorCode: 429,
status: 429,
}),
)
})
})
})

View file

@ -1,15 +1,5 @@
// npx vitest api/providers/__tests__/xai.spec.ts
// Mock TelemetryService - must come before other imports
const mockCaptureException = vitest.hoisted(() => vitest.fn())
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: mockCaptureException,
},
},
}))
const mockResponsesCreate = vitest.fn()
vitest.mock("openai", () => {
@ -53,7 +43,6 @@ describe("XAIHandler", () => {
beforeEach(() => {
vi.clearAllMocks()
mockResponsesCreate.mockClear()
mockCaptureException.mockClear()
handler = new XAIHandler({})
})

View file

@ -9,9 +9,7 @@ import {
anthropicDefaultModelId,
anthropicModels,
ANTHROPIC_DEFAULT_MAX_TOKENS,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import type { ApiHandlerOptions } from "../../shared/api"
@ -112,96 +110,72 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
try {
stream = await this.client.messages.create(
{
model: modelId,
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
temperature,
thinking,
// Setting cache breakpoint for system prompt so new tasks can reuse it.
system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }],
messages: sanitizedMessages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
typeof message.content === "string"
? [{ type: "text", text: message.content, cache_control: cacheControl }]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? { ...content, cache_control: cacheControl }
: content,
),
}
}
return message
}),
stream: true,
...nativeToolParams,
},
(() => {
// prompt caching: https://x.com/alexalbert__/status/1823751995901272068
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
// Then check for models that support prompt caching
switch (modelId) {
case "claude-sonnet-4-6":
case "claude-sonnet-4-5":
case "claude-sonnet-4-20250514":
case "claude-opus-4-6":
case "claude-opus-4-5-20251101":
case "claude-opus-4-1-20250805":
case "claude-opus-4-20250514":
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
case "claude-haiku-4-5-20251001":
case "claude-3-haiku-20240307":
betas.push("prompt-caching-2024-07-31")
return { headers: { "anthropic-beta": betas.join(",") } }
default:
return undefined
}
})(),
)
} catch (error) {
TelemetryService.instance.captureException(
new ApiProviderError(
error instanceof Error ? error.message : String(error),
this.providerName,
modelId,
"createMessage",
),
)
throw error
}
break
}
default: {
try {
stream = (await this.client.messages.create({
stream = await this.client.messages.create(
{
model: modelId,
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
temperature,
system: [{ text: systemPrompt, type: "text" }],
messages: sanitizedMessages,
thinking,
// Setting cache breakpoint for system prompt so new tasks can reuse it.
system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }],
messages: sanitizedMessages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
typeof message.content === "string"
? [{ type: "text", text: message.content, cache_control: cacheControl }]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? { ...content, cache_control: cacheControl }
: content,
),
}
}
return message
}),
stream: true,
...nativeToolParams,
})) as any
} catch (error) {
TelemetryService.instance.captureException(
new ApiProviderError(
error instanceof Error ? error.message : String(error),
this.providerName,
modelId,
"createMessage",
),
)
throw error
}
},
(() => {
// prompt caching: https://x.com/alexalbert__/status/1823751995901272068
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
// Then check for models that support prompt caching
switch (modelId) {
case "claude-sonnet-4-6":
case "claude-sonnet-4-5":
case "claude-sonnet-4-20250514":
case "claude-opus-4-6":
case "claude-opus-4-5-20251101":
case "claude-opus-4-1-20250805":
case "claude-opus-4-20250514":
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
case "claude-haiku-4-5-20251001":
case "claude-3-haiku-20240307":
betas.push("prompt-caching-2024-07-31")
return { headers: { "anthropic-beta": betas.join(",") } }
default:
return undefined
}
})(),
)
break
}
default: {
stream = (await this.client.messages.create({
model: modelId,
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
temperature,
system: [{ text: systemPrompt, type: "text" }],
messages: sanitizedMessages,
stream: true,
...nativeToolParams,
})) as any
break
}
}
@ -382,27 +356,14 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
async completePrompt(prompt: string) {
let { id: model, temperature } = this.getModel()
let message
try {
message = await this.client.messages.create({
model,
max_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
thinking: undefined,
temperature,
messages: [{ role: "user", content: prompt }],
stream: false,
})
} catch (error) {
TelemetryService.instance.captureException(
new ApiProviderError(
error instanceof Error ? error.message : String(error),
this.providerName,
model,
"completePrompt",
),
)
throw error
}
const message = await this.client.messages.create({
model,
max_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
thinking: undefined,
temperature,
messages: [{ role: "user", content: prompt }],
stream: false,
})
const content = message.content.find(({ type }) => type === "text")
return content?.type === "text" ? content.text : ""

View file

@ -30,9 +30,7 @@ import {
BEDROCK_GLOBAL_INFERENCE_MODEL_IDS,
BEDROCK_SERVICE_TIER_MODEL_IDS,
BEDROCK_SERVICE_TIER_PRICING,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiStream } from "../transform/stream"
import { BaseProvider } from "./base-provider"
@ -682,10 +680,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
// Clear timeout on error
clearTimeout(timeoutId)
// Capture error in telemetry before processing
// Capture error message before processing
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, modelConfig.id, "createMessage")
TelemetryService.instance.captureException(apiError)
// Check if this is a throttling error that should trigger retry logic
const errorType = this.getErrorType(error)
@ -790,11 +786,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
return ""
} catch (error) {
// Capture error in telemetry
// Capture error message
const model = this.getModel()
const telemetryErrorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(telemetryErrorMessage, this.providerName, model.id, "completePrompt")
TelemetryService.instance.captureException(apiError)
const providerErrorMessage = error instanceof Error ? error.message : String(error)
// Use the extracted error handling method for all errors
const errorResult = this.handleBedrockError(error, false) // false for non-streaming context

View file

@ -1,14 +1,5 @@
// Mocks must come first, before imports
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))
// Mock NodeCache to allow controlling cache behavior
vi.mock("node-cache", () => {
const mockGet = vi.fn().mockReturnValue(undefined)

View file

@ -6,8 +6,7 @@ import NodeCache from "node-cache"
import { z } from "zod"
import type { ProviderName, ModelRecord } from "@roo-code/types"
import { modelInfoSchema, TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { modelInfoSchema } from "@roo-code/types"
import { safeWriteJson } from "../../../utils/safeWriteJson"
@ -129,20 +128,14 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
models = await fetchModelsFromProvider(options)
const modelCount = Object.keys(models).length
// Only cache non-empty results to prevent persisting failed API responses
// Empty results could indicate API failure rather than "no models exist"
// Only cache non-empty results to prevent persisting failed API responses.
// Empty results could indicate API failure rather than "no models exist".
if (modelCount > 0) {
memoryCache.set(provider, models)
await writeModels(provider, models).catch((err) =>
console.error(`[MODEL_CACHE] Error writing ${provider} models to file cache:`, err),
)
} else {
TelemetryService.instance.captureEvent(TelemetryEventName.MODEL_CACHE_EMPTY_RESPONSE, {
provider,
context: "getModels",
hasExistingCache: false,
})
}
return models
@ -186,17 +179,7 @@ export const refreshModels = async (options: GetModelsOptions): Promise<ModelRec
const existingCount = existingCache ? Object.keys(existingCache).length : 0
if (modelCount === 0) {
TelemetryService.instance.captureEvent(TelemetryEventName.MODEL_CACHE_EMPTY_RESPONSE, {
provider,
context: "refreshModels",
hasExistingCache: existingCount > 0,
existingCacheSize: existingCount,
})
if (existingCount > 0) {
return existingCache!
} else {
return {}
}
return existingCount > 0 ? existingCache! : {}
}
// Update memory cache first

View file

@ -9,15 +9,8 @@ import {
} from "@google/genai"
import type { JWTInput } from "google-auth-library"
import {
type ModelInfo,
type GeminiModelId,
geminiDefaultModelId,
geminiModels,
ApiProviderError,
} from "@roo-code/types"
import { type ModelInfo, type GeminiModelId, geminiDefaultModelId, geminiModels } from "@roo-code/types"
import { safeJsonParse } from "@roo-code/core"
import { TelemetryService } from "@roo-code/telemetry"
import type { ApiHandlerOptions } from "../../shared/api"
@ -335,8 +328,6 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "createMessage")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
throw new Error(t("common:errors.gemini.generate_stream", { error: error.message }))
@ -444,8 +435,6 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
return text
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "completePrompt")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
throw new Error(t("common:errors.gemini.generate_complete_prompt", { error: error.message }))

View file

@ -2,14 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import OpenAI from "openai"
import {
type MistralModelId,
mistralDefaultModelId,
mistralModels,
MISTRAL_DEFAULT_TEMPERATURE,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { type MistralModelId, mistralDefaultModelId, mistralModels, MISTRAL_DEFAULT_TEMPERATURE } from "@roo-code/types"
import { ApiHandlerOptions } from "../../shared/api"
@ -106,8 +99,6 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
response = await this.client.chat.stream(requestOptions)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "createMessage")
TelemetryService.instance.captureException(apiError)
throw new Error(`Mistral completion error: ${errorMessage}`)
}
@ -216,8 +207,6 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
return content || ""
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "completePrompt")
TelemetryService.instance.captureException(apiError)
throw new Error(`Mistral completion error: ${errorMessage}`)
}
}

View file

@ -10,9 +10,7 @@ import {
openAiCodexModels,
type ReasoningEffort,
type ReasoningEffortExtended,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Package } from "../../shared/package"
import type { ApiHandlerOptions } from "../../shared/api"
@ -581,8 +579,6 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
yield* this.handleStreamResponse(response.body, model)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
if (error.message.includes("Codex API")) {
@ -858,8 +854,6 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
throw new Error(t("common:errors.openAiCodex.streamProcessingError", { message: error.message }))
@ -1246,8 +1240,6 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
} catch (error) {
const errorModel = this.getModel()
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, errorModel.id, "completePrompt")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
throw new Error(t("common:errors.openAiCodex.completionError", { message: error.message }))

View file

@ -14,9 +14,7 @@ import {
type VerbosityLevel,
type ReasoningEffortExtended,
type ServiceTier,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import type { ApiHandlerOptions } from "../../shared/api"
@ -644,8 +642,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
} catch (error) {
const model = this.getModel()
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
// Re-throw with the original error message if it's already formatted
@ -1126,8 +1122,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
// This can happen in certain edge cases and shouldn't break the flow
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
throw new Error(`Error processing response stream: ${error.message}`)
@ -1571,8 +1565,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
} catch (error) {
const errorModel = this.getModel()
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, errorModel.id, "completePrompt")
TelemetryService.instance.captureException(apiError)
if (error instanceof Error) {
throw new Error(`OpenAI Native completion error: ${error.message}`)

View file

@ -4,14 +4,12 @@ import { z } from "zod"
import {
type ModelRecord,
ApiProviderError,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
OPENROUTER_DEFAULT_PROVIDER_NAME,
OPEN_ROUTER_PROMPT_CACHING_MODELS,
DEEP_SEEK_DEFAULT_TEMPERATURE,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser"
@ -187,7 +185,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
}
/**
* Handle OpenRouter streaming error response and report to telemetry.
* Handle OpenRouter streaming error response.
* OpenRouter may include metadata.raw with the actual upstream provider error.
* @param error The error object (not wrapped - receives the error directly)
*/
@ -196,13 +194,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const parsedError = extractErrorFromMetadataRaw(rawString)
const rawErrorMessage = parsedError || error?.message || "Unknown error"
const apiError = Object.assign(
new ApiProviderError(rawErrorMessage, this.providerName, modelId, operation, error?.code),
{ status: error?.code, error },
)
TelemetryService.instance.captureException(apiError)
throw new Error(`OpenRouter API Error ${error?.code}: ${rawErrorMessage}`)
}
@ -342,36 +333,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
} catch (error) {
// Try to parse as OpenRouter error structure using Zod
const parseResult = OpenRouterErrorResponseSchema.safeParse(error)
if (parseResult.success && parseResult.data.error) {
const openRouterError = parseResult.data
const rawString = openRouterError.error?.metadata?.raw
const parsedError = extractErrorFromMetadataRaw(rawString)
const rawErrorMessage = parsedError || openRouterError.error?.message || "Unknown error"
const apiError = Object.assign(
new ApiProviderError(
rawErrorMessage,
this.providerName,
modelId,
"createMessage",
openRouterError.error?.code,
),
{
status: openRouterError.error?.code,
error: openRouterError.error,
},
)
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
} else {
// Fallback for non-OpenRouter errors
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
void parseResult
throw handleOpenAIError(error, this.providerName)
}
let lastUsage: CompletionUsage | undefined = undefined
@ -607,36 +570,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
} catch (error) {
// Try to parse as OpenRouter error structure using Zod
const parseResult = OpenRouterErrorResponseSchema.safeParse(error)
if (parseResult.success && parseResult.data.error) {
const openRouterError = parseResult.data
const rawString = openRouterError.error?.metadata?.raw
const parsedError = extractErrorFromMetadataRaw(rawString)
const rawErrorMessage = parsedError || openRouterError.error?.message || "Unknown error"
const apiError = Object.assign(
new ApiProviderError(
rawErrorMessage,
this.providerName,
modelId,
"completePrompt",
openRouterError.error?.code,
),
{
status: openRouterError.error?.code,
error: openRouterError.error,
},
)
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
} else {
// Fallback for non-OpenRouter errors
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
void parseResult
throw handleOpenAIError(error, this.providerName)
}
if ("error" in response) {

View file

@ -8,9 +8,7 @@ import {
getPoeDefaultModelInfo,
type ModelInfo,
type ReasoningEffortExtended,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { shouldUseReasoningBudget, shouldUseReasoningEffort, type ApiHandlerOptions } from "../../shared/api"
@ -104,7 +102,6 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "createMessage"))
throw new Error(`Poe completion error: ${errorMessage}`)
}
@ -129,7 +126,6 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "createMessage"))
throw new Error(`Poe streaming error: ${errorMessage}`)
}
}
@ -144,7 +140,6 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler
return text
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "completePrompt"))
throw new Error(`Poe completion error: ${errorMessage}`)
}
}

View file

@ -6,7 +6,7 @@
* - Preserves HTTP status codes for UI-aware error display
* - Maintains error details for retry logic (e.g., RetryInfo for 429 errors)
* - Provides consistent error message formatting
* - Enables telemetry and debugging with complete error context
* - Enables debugging with complete error context
*/
import i18n from "../../../i18n/setup"

View file

@ -1,8 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { type XAIModelId, xaiDefaultModelId, xaiModels, ApiProviderError } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { type XAIModelId, xaiDefaultModelId, xaiModels } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
@ -132,8 +131,6 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
} as any)) as unknown as AsyncIterable<any>
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
@ -155,8 +152,6 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
return response.output_text || ""
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "completePrompt")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
}

View file

@ -391,7 +391,7 @@ export class NativeToolCallParser {
// Build partial nativeArgs based on what we have so far
let nativeArgs: any = undefined
// Track if legacy format was used (for telemetry)
// Track whether legacy format was used
let usedLegacyFormat = false
switch (name) {
@ -654,7 +654,7 @@ export class NativeToolCallParser {
result.originalName = originalName
}
// Track legacy format usage for telemetry
// Track legacy format usage
if (usedLegacyFormat) {
result.usedLegacyFormat = true
}
@ -722,7 +722,7 @@ export class NativeToolCallParser {
// nativeArgs object. If validation fails, we treat the tool call as invalid and fail fast.
let nativeArgs: NativeArgsFor<TName> | undefined = undefined
// Track if legacy format was used (for telemetry)
// Track whether legacy format was used
let usedLegacyFormat = false
switch (resolvedName) {
@ -1017,7 +1017,7 @@ export class NativeToolCallParser {
result.originalName = toolCall.name
}
// Track legacy format usage for telemetry
// Track legacy format usage
if (usedLegacyFormat) {
result.usedLegacyFormat = true
}

View file

@ -23,16 +23,6 @@ vi.mock("@roo-code/core", () => ({
},
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureToolUsage: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
import { TelemetryService } from "@roo-code/telemetry"
import { customToolRegistry } from "@roo-code/core"
describe("presentAssistantMessage - Custom Tool Recording", () => {
@ -118,7 +108,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should record as "custom_tool", not "my_custom_tool"
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("custom_tool")
expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(mockTask.taskId, "custom_tool")
})
})
@ -171,7 +160,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should record as "read_file", not "custom_tool"
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("read_file")
expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(mockTask.taskId, "read_file")
})
it("should record MCP tool usage as 'use_mcp_tool' (not custom_tool)", async () => {
@ -213,7 +201,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should record as "use_mcp_tool", not "custom_tool"
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("use_mcp_tool")
expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(mockTask.taskId, "use_mcp_tool")
})
})
@ -355,7 +342,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should not record usage for partial blocks
expect(mockTask.recordToolUsage).not.toHaveBeenCalled()
expect(TelemetryService.instance.captureToolUsage).not.toHaveBeenCalled()
})
})
})

View file

@ -6,23 +6,6 @@ import { presentAssistantMessage } from "../presentAssistantMessage"
import { Task } from "../../task/Task"
// Mock dependencies
vi.mock("../../task/Task")
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
isValidToolName: vi.fn((toolName: string) =>
["read_file", "write_to_file", "ask_followup_question", "attempt_completion", "use_mcp_tool"].includes(
toolName,
),
),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureToolUsage: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
describe("presentAssistantMessage - Image Handling in Native Tool Calling", () => {
let mockTask: any

View file

@ -9,14 +9,6 @@ vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
isValidToolName: vi.fn(() => false),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureToolUsage: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
describe("presentAssistantMessage - Unknown Tool Handling", () => {
let mockTask: any

View file

@ -2,8 +2,6 @@ import { serializeError } from "serialize-error"
import { Anthropic } from "@anthropic-ai/sdk"
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
import { ConsecutiveMistakeError, TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { customToolRegistry } from "@roo-code/core"
import { t } from "../../i18n"
@ -234,8 +232,7 @@ export async function presentAssistantMessage(cline: Task) {
}
if (!mcpBlock.partial) {
cline.recordToolUsage("use_mcp_tool") // Record as use_mcp_tool for analytics
TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool")
cline.recordToolUsage("use_mcp_tool")
}
// Resolve sanitized server name back to original server name
@ -302,7 +299,7 @@ export async function presentAssistantMessage(cline: Task) {
if (!toolCallId) {
const errorMessage =
"Invalid tool call: missing tool_use.id. XML tool calls are no longer supported. Remove any XML tool markup (e.g. <read_file>...</read_file>) and use native tool calling instead."
// Record a tool error for visibility/telemetry. Use the reported tool name if present.
// Record a tool error for visibility. Use the reported tool name if present.
try {
if (
typeof (cline as any).recordToolError === "function" &&
@ -558,16 +555,6 @@ export async function presentAssistantMessage(cline: Task) {
const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name)
const recordName = isCustomTool ? "custom_tool" : block.name
cline.recordToolUsage(recordName)
TelemetryService.instance.captureToolUsage(cline.taskId, recordName)
// Track legacy format usage for read_file tool (for migration monitoring)
if (block.name === "read_file" && block.usedLegacyFormat) {
const modelInfo = cline.api.getModel()
TelemetryService.instance.captureEvent(TelemetryEventName.READ_FILE_LEGACY_FORMAT_USED, {
taskId: cline.taskId,
model: modelInfo?.id,
})
}
}
// Validate tool use before execution - ONLY for complete (non-partial) blocks.
@ -651,20 +638,6 @@ export async function presentAssistantMessage(cline: Task) {
await cline.say("user_feedback", text, images)
}
// Track tool repetition in telemetry via PostHog exception tracking and event.
TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
TelemetryService.instance.captureException(
new ConsecutiveMistakeError(
`Tool repetition limit reached for ${block.name}`,
cline.taskId,
cline.consecutiveMistakeCount,
cline.consecutiveMistakeLimit,
"tool_repetition",
cline.apiConfiguration.apiProvider,
cline.api.getModel().id,
),
)
// Return tool result message about the repetition
pushToolResult(
formatResponse.toolError(

View file

@ -22,15 +22,6 @@ vi.mock("vscode", () => ({
}))
// Mock other dependencies
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureCheckpointCreated: vi.fn(),
captureCheckpointRestored: vi.fn(),
captureCheckpointDiffed: vi.fn(),
},
},
}))
vi.mock("../../../utils/path", () => ({
getWorkspacePath: vi.fn(() => "/test/workspace"),

View file

@ -2,7 +2,6 @@ import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import type { ClineApiReqInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../task/Task"
@ -216,8 +215,6 @@ export async function checkpointSave(task: Task, force = false, suppressMessage
return
}
TelemetryService.instance.captureCheckpointCreated(task.taskId)
// Start the checkpoint process in the background.
return service
.saveCheckpoint(`Task: ${task.taskId}, Time: ${Date.now()}`, { allowEmpty: force, suppressMessage })
@ -254,7 +251,6 @@ export async function checkpointRestore(
try {
await service.restoreCheckpoint(commitHash)
TelemetryService.instance.captureCheckpointRestored(task.taskId)
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
if (mode === "restore") {
@ -321,8 +317,6 @@ export async function checkpointDiff(task: Task, { ts, previousCommitHash, commi
return
}
TelemetryService.instance.captureCheckpointDiffed(task.taskId)
let fromHash: string | undefined
let toHash: string | undefined
let title: string

View file

@ -2,7 +2,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import type { ModelInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { BaseProvider } from "../../../api/providers/base-provider"
import { ApiMessage } from "../../task-persistence/apiMessages"
@ -57,11 +56,7 @@ const mockApiHandler = new MockApiHandler()
const taskId = "test-task-id"
describe("Condense", () => {
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
})
beforeEach(() => {})
describe("extractCommandBlocks", () => {
it("should extract command blocks from string content", () => {

View file

@ -3,7 +3,6 @@
import * as path from "path"
import { Anthropic } from "@anthropic-ai/sdk"
import type { ModelInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { BaseProvider } from "../../../api/providers/base-provider"
// Mock the tree-sitter module
@ -213,11 +212,7 @@ describe("foldedFileContext", () => {
})
describe("summarizeConversation with foldedFileContext", () => {
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
})
beforeEach(() => {})
// Mock API handler for testing
class MockApiHandler extends BaseProvider {

View file

@ -3,7 +3,6 @@
import type { Mock } from "vitest"
import { Anthropic } from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiHandler } from "../../../api"
import { ApiMessage } from "../../task-persistence/apiMessages"
@ -25,14 +24,6 @@ vi.mock("../../../api/transform/image-cleaning", () => ({
maybeRemoveImageBlocks: vi.fn((messages: ApiMessage[], _apiHandler: ApiHandler) => [...messages]),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureContextCondensed: vi.fn(),
},
},
}))
const taskId = "test-task-id"
describe("extractCommandBlocks", () => {
@ -1150,9 +1141,6 @@ describe("summarizeConversation with custom settings", () => {
// Reset mocks
vi.clearAllMocks()
// Reset telemetry mock
;(TelemetryService.instance.captureContextCondensed as Mock).mockClear()
// Setup mock API handler
mockMainApiHandler = {
createMessage: vi.fn().mockImplementation(() => {
@ -1243,48 +1231,6 @@ describe("summarizeConversation with custom settings", () => {
)
expect(createMessageCalls[0][0]).toContain("CRITICAL: This is a summarization-only request")
})
/**
* Test that telemetry is called for custom prompt usage
*/
it("should capture telemetry when using custom prompt", async () => {
await summarizeConversation({
messages: sampleMessages,
apiHandler: mockMainApiHandler,
systemPrompt: defaultSystemPrompt,
taskId: localTaskId,
isAutomaticTrigger: false,
customCondensingPrompt: "Custom prompt",
})
// Verify telemetry was called with custom prompt flag
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
localTaskId,
false,
true, // usedCustomPrompt
)
})
/**
* Test that telemetry is called with isAutomaticTrigger flag
*/
it("should capture telemetry with isAutomaticTrigger flag", async () => {
await summarizeConversation({
messages: sampleMessages,
apiHandler: mockMainApiHandler,
systemPrompt: defaultSystemPrompt,
taskId: localTaskId,
isAutomaticTrigger: true,
customCondensingPrompt: "Custom prompt",
})
// Verify telemetry was called with isAutomaticTrigger flag
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
localTaskId,
true, // isAutomaticTrigger
true, // usedCustomPrompt
)
})
})
describe("toolUseToText", () => {

View file

@ -9,17 +9,11 @@
* so they can be sent to the API again.
*/
import { TelemetryService } from "@roo-code/telemetry"
import { getEffectiveApiHistory, cleanupAfterTruncation } from "../index"
import { ApiMessage } from "../../task-persistence/apiMessages"
describe("Rewind After Condense - Issue #8295", () => {
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
})
beforeEach(() => {})
describe("getEffectiveApiHistory", () => {
it("should return summary and messages after summary (fresh start model)", () => {

View file

@ -1,8 +1,6 @@
import Anthropic from "@anthropic-ai/sdk"
import crypto from "crypto"
import { TelemetryService } from "@roo-code/telemetry"
import { t } from "../../i18n"
import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
import { ApiMessage } from "../task-persistence/apiMessages"
@ -267,11 +265,6 @@ export async function summarizeConversation(options: SummarizeConversationOption
cwd,
rooIgnoreController,
} = options
TelemetryService.instance.captureContextCondensed(
taskId,
isAutomaticTrigger ?? false,
!!customCondensingPrompt?.trim(),
)
const response: SummarizeResponse = { messages, cost: 0, summary: "" }

View file

@ -18,7 +18,6 @@ import {
isProviderName,
isRetiredProvider,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { logger } from "../../utils/logging"
import { supportPrompt } from "../../shared/support-prompt"
@ -406,10 +405,6 @@ export class ContextProxy {
try {
return globalSettingsSchema.parse(values)
} catch (error) {
if (error instanceof ZodError) {
TelemetryService.instance.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
}
return GLOBAL_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as GlobalSettings)
}
}
@ -424,16 +419,12 @@ export class ContextProxy {
// Sanitize invalid/removed apiProvider values before parsing
// This handles cases where a user had a provider selected that was later removed
// from the extension (e.g., "glama"). We sanitize here to avoid repeated
// schema validation errors that can cause infinite loops in telemetry.
// schema validation errors that can cause infinite update loops.
const sanitizedValues = this.sanitizeProviderValues(values)
try {
return providerSettingsSchema.parse(sanitizedValues)
} catch (error) {
if (error instanceof ZodError) {
TelemetryService.instance.captureSchemaValidationError({ schemaName: "ProviderSettings", error })
}
return PROVIDER_SETTINGS_KEYS.reduce(
(acc, key) => ({ ...acc, [key]: sanitizedValues[key] }),
{} as ProviderSettings,
@ -538,10 +529,6 @@ export class ContextProxy {
return Object.fromEntries(Object.entries(globalSettings).filter(([_, value]) => value !== undefined))
} catch (error) {
if (error instanceof ZodError) {
TelemetryService.instance.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
}
return undefined
}
}

View file

@ -14,7 +14,6 @@ import {
isProviderName,
isRetiredProvider,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Mode, modes } from "../../shared/modes"
import { buildApiHandler } from "../../api"
@ -623,13 +622,6 @@ export class ProviderSettingsManager {
),
}
} catch (error) {
if (error instanceof ZodError) {
TelemetryService.instance.captureSchemaValidationError({
schemaName: "ProviderProfiles",
error,
})
}
throw new Error(`Failed to read provider profiles from secrets: ${error}`)
}
}

View file

@ -6,7 +6,6 @@ import * as path from "path"
import * as vscode from "vscode"
import type { ProviderName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { importSettings, importSettingsFromFile, importSettingsWithFeedback, exportSettings } from "../importExport"
import { ProviderSettingsManager } from "../ProviderSettingsManager"
@ -103,10 +102,6 @@ describe("importExport", () => {
beforeEach(() => {
vi.clearAllMocks()
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
mockProviderSettingsManager = {
export: vi.fn(),
import: vi.fn(),

View file

@ -12,7 +12,6 @@ import {
isProviderName,
type ProviderSettingsWithId,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
import { ContextProxy } from "./ContextProxy"
@ -188,7 +187,6 @@ export async function importSettingsFromPath(
if (e instanceof ZodError) {
error = e.issues.map((issue) => `[${issue.path.join(".")}]: ${issue.message}`).join("\n")
TelemetryService.instance.captureSchemaValidationError({ schemaName: "ImportExport", error: e })
} else if (e instanceof Error) {
error = e.message
}

View file

@ -3,7 +3,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import type { ModelInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { BaseProvider } from "../../../api/providers/base-provider"
import { ApiMessage } from "../../task-persistence/apiMessages"
@ -51,11 +50,7 @@ const mockApiHandler = new MockApiHandler()
const taskId = "test-task-id"
describe("Context Management", () => {
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
})
beforeEach(() => {})
/**
* Tests for the truncateConversation function
*/

View file

@ -1,5 +1,4 @@
import { describe, it, expect, beforeEach } from "vitest"
import { TelemetryService } from "@roo-code/telemetry"
import { truncateConversation } from "../index"
import { getEffectiveApiHistory, cleanupAfterTruncation } from "../../condense"
import { ApiMessage } from "../../task-persistence/apiMessages"
@ -8,11 +7,6 @@ describe("Non-Destructive Sliding Window Truncation", () => {
let messages: ApiMessage[]
beforeEach(() => {
// Initialize TelemetryService for tests
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
// Create a sample conversation with 11 messages (1 initial + 10 conversation messages)
messages = [
{ role: "user", content: "Initial task", ts: 1000 },

View file

@ -1,8 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import crypto from "crypto"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense"
import { ApiMessage } from "../task-persistence/apiMessages"
@ -61,12 +59,10 @@ export type TruncationResult = {
*
* @param {ApiMessage[]} messages - The conversation messages.
* @param {number} fracToRemove - The fraction (between 0 and 1) of messages (excluding the first) to hide.
* @param {string} taskId - The task ID for the conversation, used for telemetry
* @param {string} taskId - The task ID for the conversation
* @returns {TruncationResult} Object containing the tagged messages, truncation ID, and count of messages removed.
*/
export function truncateConversation(messages: ApiMessage[], fracToRemove: number, taskId: string): TruncationResult {
TelemetryService.instance.captureSlidingWindowTruncation(taskId)
const truncationId = crypto.randomUUID()
// Filter to only visible messages (those not already truncated)

View file

@ -36,7 +36,6 @@ import {
type ClineApiReqCancelReason,
type ClineApiReqInfo,
RooCodeEventName,
TelemetryEventName,
TaskStatus,
TodoItem,
getApiProtocol,
@ -50,12 +49,9 @@ import {
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
MAX_CHECKPOINT_TIMEOUT_SECONDS,
MIN_CHECKPOINT_TIMEOUT_SECONDS,
ConsecutiveMistakeError,
MAX_MCP_TOOLS_THRESHOLD,
countEnabledMcpTools,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService } from "@roo-code/cloud"
// api
import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api"
@ -409,8 +405,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
private debouncedEmitTokenUsage: ReturnType<typeof debounce>
// Cloud Sync Tracking
private cloudSyncedMessageTimestamps: Set<number> = new Set()
// Initial status for the task's history item (set at creation time to avoid race conditions)
private readonly initialStatus?: "active" | "delegated" | "completed"
@ -498,24 +492,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.taskNumber = taskNumber
this.initialStatus = initialStatus
// Store the task's mode and API config name when it's created.
// For history items, use the stored values; for new tasks, we'll set them
// after getting state.
if (historyItem) {
this._taskMode = historyItem.mode || defaultModeSlug
this._taskApiConfigName = historyItem.apiConfigName
this.taskModeReady = Promise.resolve()
this.taskApiConfigReady = Promise.resolve()
TelemetryService.instance.captureTaskRestarted(this.taskId)
} else {
// For new tasks, don't set the mode/apiConfigName yet - wait for async initialization.
this._taskMode = undefined
this._taskApiConfigName = undefined
this.taskModeReady = this.initializeTaskMode(provider)
this.taskApiConfigReady = this.initializeTaskApiConfigName(provider)
TelemetryService.instance.captureTaskCreated(this.taskId)
}
this.assistantMessageParser = undefined
this.messageQueueService = new MessageQueueService()
@ -563,6 +539,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
{ leading: true, trailing: true, maxWait: this.TOKEN_USAGE_EMIT_INTERVAL_MS },
)
if (historyItem) {
this._taskMode = historyItem.mode || defaultModeSlug
this._taskApiConfigName = historyItem.apiConfigName
this.taskModeReady = Promise.resolve()
this.taskApiConfigReady = Promise.resolve()
} else {
this._taskMode = undefined
this._taskApiConfigName = undefined
this.taskModeReady = this.initializeTaskMode(provider)
this.taskApiConfigReady = this.initializeTaskApiConfigName(provider)
}
onCreated?.(this)
if (startTask) {
@ -1161,51 +1149,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await provider?.postStateToWebviewWithoutTaskHistory()
this.emit(RooCodeEventName.Message, { action: "created", message })
await this.saveClineMessages()
const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()
if (shouldCaptureMessage) {
CloudService.instance.captureEvent({
event: TelemetryEventName.TASK_MESSAGE,
properties: { taskId: this.taskId, message },
})
// Track that this message has been synced to cloud
this.cloudSyncedMessageTimestamps.add(message.ts)
}
}
public async overwriteClineMessages(newMessages: ClineMessage[]) {
this.clineMessages = newMessages
restoreTodoListForTask(this)
await this.saveClineMessages()
// When overwriting messages (e.g., during task resume), repopulate the cloud sync tracking Set
// with timestamps from all non-partial messages to prevent re-syncing previously synced messages
this.cloudSyncedMessageTimestamps.clear()
for (const msg of newMessages) {
if (msg.partial !== true) {
this.cloudSyncedMessageTimestamps.add(msg.ts)
}
}
}
private async updateClineMessage(message: ClineMessage) {
const provider = this.providerRef.deref()
await provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message })
this.emit(RooCodeEventName.Message, { action: "updated", message })
// Check if we should sync to cloud and haven't already synced this message
const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()
const hasNotBeenSynced = !this.cloudSyncedMessageTimestamps.has(message.ts)
if (shouldCaptureMessage && hasNotBeenSynced) {
CloudService.instance.captureEvent({
event: TelemetryEventName.TASK_MESSAGE,
properties: { taskId: this.taskId, message },
})
// Track that this message has been synced to cloud
this.cloudSyncedMessageTimestamps.add(message.ts)
}
}
private async saveClineMessages(): Promise<boolean> {
@ -2526,22 +2481,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
if (this.consecutiveMistakeLimit > 0 && this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) {
// Track consecutive mistake errors in telemetry via event and PostHog exception tracking.
// The reason is "no_tools_used" because this limit is reached via initiateTaskLoop
// which increments consecutiveMistakeCount when the model doesn't use any tools.
TelemetryService.instance.captureConsecutiveMistakeError(this.taskId)
TelemetryService.instance.captureException(
new ConsecutiveMistakeError(
`Task reached consecutive mistake limit (${this.consecutiveMistakeLimit})`,
this.taskId,
this.consecutiveMistakeCount,
this.consecutiveMistakeLimit,
"no_tools_used",
this.apiConfiguration.apiProvider,
getModelId(this.apiConfiguration),
),
)
const { response, text, images } = await this.ask(
"mistake_limit_reached",
t("common:errors.mistake_limit_guidance"),
@ -2657,7 +2596,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
((currentItem.retryAttempt ?? 0) === 0 && !isEmptyUserContent) || currentItem.userMessageWasRemoved
if (shouldAddUserMessage) {
await this.addToApiConversationHistory({ role: "user", content: finalUserContent })
TelemetryService.instance.captureConversationMessage(this.taskId, "user")
}
// Since we sent off a placeholder api_req_started message to update the
@ -3088,7 +3026,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
let bgCacheReadTokens = currentTokens.cacheRead
let bgTotalCost = currentTokens.total
// Helper function to capture telemetry and update messages
// Helper function to update messages
const captureUsageData = async (
tokens: {
input: number
@ -3105,56 +3043,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
tokens.cacheWrite > 0 ||
tokens.cacheRead > 0
) {
// Update the shared variables atomically
inputTokens = tokens.input
outputTokens = tokens.output
cacheWriteTokens = tokens.cacheWrite
cacheReadTokens = tokens.cacheRead
totalCost = tokens.total
// Update the API request message with the latest usage data
updateApiReqMsg()
await this.saveClineMessages()
// Update the specific message in the webview
const apiReqMessage = this.clineMessages[messageIndex]
if (apiReqMessage) {
await this.updateClineMessage(apiReqMessage)
}
// Capture telemetry with provider-aware cost calculation
const modelId = getModelId(this.apiConfiguration)
const apiProvider = this.apiConfiguration.apiProvider
const apiProtocol = getApiProtocol(
apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId,
)
// Use the appropriate cost function based on the API protocol
const costResult =
apiProtocol === "anthropic"
? calculateApiCostAnthropic(
streamModelInfo,
tokens.input,
tokens.output,
tokens.cacheWrite,
tokens.cacheRead,
)
: calculateApiCostOpenAI(
streamModelInfo,
tokens.input,
tokens.output,
tokens.cacheWrite,
tokens.cacheRead,
)
TelemetryService.instance.captureLlmCompletion(this.taskId, {
inputTokens: costResult.totalInputTokens,
outputTokens: costResult.totalOutputTokens,
cacheWriteTokens: tokens.cacheWrite,
cacheReadTokens: tokens.cacheRead,
cost: tokens.total ?? costResult.totalCost,
})
}
}
@ -3468,8 +3369,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
assistantContent.push({
type: "tool_use" as const,
id: sanitizedId,
name: mcpBlock.name, // Original dynamic name
input: mcpBlock.arguments, // Direct tool arguments
name: mcpBlock.name,
input: mcpBlock.arguments,
})
}
} else {
@ -3486,7 +3387,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
continue
}
seenToolUseIds.add(sanitizedId)
// nativeArgs is already in the correct API format for all tools
const input = toolUse.nativeArgs || toolUse.params
// Use originalName (alias) if present for API history consistency.
@ -3513,14 +3413,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
)
if (newTaskIndex !== -1 && newTaskIndex < assistantContent.length - 1) {
// new_task found but not last - truncate subsequent tools
const truncatedTools = assistantContent.slice(newTaskIndex + 1)
assistantContent.length = newTaskIndex + 1 // Truncate API history array
assistantContent.length = newTaskIndex + 1
// ALSO truncate the execution array (assistantMessageContent) to prevent
// tools after new_task from being executed by presentAssistantMessage().
// Find new_task index in assistantMessageContent (may differ from assistantContent
// due to text blocks being structured differently).
const executionNewTaskIndex = this.assistantMessageContent.findIndex(
(block) => block.type === "tool_use" && block.name === "new_task",
)
@ -3528,7 +3423,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.assistantMessageContent.length = executionNewTaskIndex + 1
}
// Pre-inject error tool_results for truncated tools
for (const tool of truncatedTools) {
if (tool.type === "tool_use" && (tool as Anthropic.ToolUseBlockParam).id) {
this.pushToolResultToUserContent({
@ -3542,17 +3436,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}
// Save assistant message BEFORE executing tools
// This is critical for new_task: when it triggers delegation, flushPendingToolResultsToHistory()
// will save the user message with tool_results. The assistant message must already be in history
// so that tool_result blocks appear AFTER their corresponding tool_use blocks.
// Save assistant message BEFORE executing tools.
await this.addToApiConversationHistory(
{ role: "assistant", content: assistantContent },
reasoningMessage || undefined,
)
this.assistantMessageSavedToHistory = true
TelemetryService.instance.captureConversationMessage(this.taskId, "assistant")
}
// Present any partial blocks that were just completed.

View file

@ -21,16 +21,6 @@ vi.mock("../../../api", () => ({
})),
}))
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTaskCreated: vi.fn(),
captureTaskRestarted: vi.fn(),
},
},
}))
describe("Task dispose method", () => {
let mockProvider: any
let mockApiConfiguration: ProviderSettings

View file

@ -5,7 +5,6 @@ import * as path from "path"
import * as vscode from "vscode"
import type { GlobalState, ProviderSettings } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../Task"
import { ClineProvider } from "../../webview/ClineProvider"
@ -203,10 +202,6 @@ describe("Task persistence", () => {
beforeEach(() => {
vi.clearAllMocks()
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
const storageUri = { fsPath: path.join(os.tmpdir(), "test-storage") }
mockExtensionContext = {

View file

@ -7,7 +7,6 @@ import * as vscode from "vscode"
import { Anthropic } from "@anthropic-ai/sdk"
import type { GlobalState, ProviderSettings, ModelInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../Task"
import { ClineProvider } from "../../webview/ClineProvider"
@ -200,10 +199,6 @@ describe("Cline", () => {
let mockExtensionContext: vscode.ExtensionContext
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
// Setup mock extension context
const storageUri = {
fsPath: path.join(os.tmpdir(), "test-storage"),

View file

@ -6,25 +6,6 @@ import type { ProviderSettings } from "@roo-code/types"
import { Task } from "../Task"
import { ClineProvider } from "../../webview/ClineProvider"
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
hasInstance: vi.fn().mockReturnValue(true),
createInstance: vi.fn(),
get instance() {
return {
captureTaskCreated: vi.fn(),
captureTaskRestarted: vi.fn(),
captureModeSwitch: vi.fn(),
captureConversationMessage: vi.fn(),
captureLlmCompletion: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
captureCodeActionUsed: vi.fn(),
setProvider: vi.fn(),
}
},
},
}))
vi.mock("vscode", () => {
const mockDisposable = { dispose: vi.fn() }
const mockEventEmitter = { event: vi.fn(), fire: vi.fn() }

View file

@ -22,16 +22,6 @@ vi.mock("../../../api", () => ({
})),
}))
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTaskCreated: vi.fn(),
captureTaskRestarted: vi.fn(),
},
},
}))
// Mock task persistence to avoid disk writes
vi.mock("../../task-persistence", () => ({
readApiMessages: vi.fn().mockResolvedValue([]),

View file

@ -5,7 +5,6 @@ import * as path from "path"
import * as vscode from "vscode"
import type { GlobalState, ProviderSettings } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../Task"
import { ClineProvider } from "../../webview/ClineProvider"
@ -159,10 +158,6 @@ describe("flushPendingToolResultsToHistory", () => {
let mockExtensionContext: vscode.ExtensionContext
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
const storageUri = {
fsPath: path.join(os.tmpdir(), "test-storage"),
}

View file

@ -5,7 +5,6 @@ import * as path from "path"
import * as vscode from "vscode"
import type { GlobalState, ProviderSettings } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../Task"
import { ClineProvider } from "../../webview/ClineProvider"
@ -147,10 +146,6 @@ describe("Grace Retry Error Handling", () => {
let mockExtensionContext: vscode.ExtensionContext
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
const storageUri = {
fsPath: path.join(os.tmpdir(), "test-storage"),
}

View file

@ -51,29 +51,6 @@ vi.mock("vscode", () => ({
}))
// Mock other dependencies
vi.mock("../../services/mcp/McpServerManager", () => ({
McpServerManager: {
getInstance: vi.fn().mockResolvedValue(null),
},
}))
vi.mock("../../integrations/terminal/TerminalRegistry", () => ({
TerminalRegistry: {
releaseTerminalsForTask: vi.fn(),
},
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTaskCreated: vi.fn(),
captureTaskRestarted: vi.fn(),
captureConversationMessage: vi.fn(),
captureLlmCompletion: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
vi.mock("@roo-code/cloud", () => ({
CloudService: {

View file

@ -51,29 +51,6 @@ vi.mock("vscode", () => ({
}))
// Mock other dependencies
vi.mock("../../services/mcp/McpServerManager", () => ({
McpServerManager: {
getInstance: vi.fn().mockResolvedValue(null),
},
}))
vi.mock("../../integrations/terminal/TerminalRegistry", () => ({
TerminalRegistry: {
releaseTerminalsForTask: vi.fn(),
},
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTaskCreated: vi.fn(),
captureTaskRestarted: vi.fn(),
captureConversationMessage: vi.fn(),
captureLlmCompletion: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
vi.mock("@roo-code/cloud", () => ({
CloudService: {

View file

@ -1,21 +1,10 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import {
validateAndFixToolResultIds,
ToolResultIdMismatchError,
MissingToolResultError,
} from "../validateToolResultIds"
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
hasInstance: vi.fn(() => true),
instance: {
captureException: vi.fn(),
},
},
}))
describe("validateAndFixToolResultIds", () => {
beforeEach(() => {
vi.clearAllMocks()
@ -397,7 +386,7 @@ describe("validateAndFixToolResultIds", () => {
})
it("should filter out duplicate tool_results when one already has a valid ID", () => {
// This is the exact scenario from the PostHog error:
// This is the exact mismatched tool result scenario:
// 2 tool_results (call_08230257, call_55577629), 1 tool_use (call_55577629)
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
@ -773,85 +762,6 @@ describe("validateAndFixToolResultIds", () => {
})
})
describe("telemetry", () => {
it("should call captureException for both missing and mismatch when there is a mismatch", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "correct-id",
name: "read_file",
input: { path: "test.txt" },
},
],
}
const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "wrong-id",
content: "Content",
},
],
}
validateAndFixToolResultIds(userMessage, [assistantMessage])
// A mismatch also triggers missing detection since the wrong-id doesn't match any tool_use
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(2)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(MissingToolResultError),
expect.objectContaining({
missingToolUseIds: ["correct-id"],
existingToolResultIds: ["wrong-id"],
toolUseCount: 1,
toolResultCount: 1,
}),
)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(ToolResultIdMismatchError),
expect.objectContaining({
toolResultIds: ["wrong-id"],
toolUseIds: ["correct-id"],
toolResultCount: 1,
toolUseCount: 1,
}),
)
})
it("should not call captureException when IDs match", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-123",
name: "read_file",
input: { path: "test.txt" },
},
],
}
const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-123",
content: "Content",
},
],
}
validateAndFixToolResultIds(userMessage, [assistantMessage])
expect(TelemetryService.instance.captureException).not.toHaveBeenCalled()
})
})
describe("ToolResultIdMismatchError", () => {
it("should create error with correct properties", () => {
const error = new ToolResultIdMismatchError(
@ -881,117 +791,4 @@ describe("validateAndFixToolResultIds", () => {
expect(error.existingToolResultIds).toEqual(["existing-result-1"])
})
})
describe("telemetry for missing tool_results", () => {
it("should call captureException when tool_results are missing", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-123",
name: "read_file",
input: { path: "test.txt" },
},
],
}
const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "text",
text: "No tool results here",
},
],
}
validateAndFixToolResultIds(userMessage, [assistantMessage])
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(1)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(MissingToolResultError),
expect.objectContaining({
missingToolUseIds: ["tool-123"],
existingToolResultIds: [],
toolUseCount: 1,
toolResultCount: 0,
}),
)
})
it("should call captureException twice when both mismatch and missing occur", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "read_file",
input: { path: "a.txt" },
},
{
type: "tool_use",
id: "tool-2",
name: "read_file",
input: { path: "b.txt" },
},
],
}
const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "wrong-id", // Wrong ID (mismatch)
content: "Content",
},
// Missing tool_result for tool-2
],
}
validateAndFixToolResultIds(userMessage, [assistantMessage])
// Should be called twice: once for missing, once for mismatch
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(2)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(MissingToolResultError),
expect.any(Object),
)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(ToolResultIdMismatchError),
expect.any(Object),
)
})
it("should not call captureException for missing when all tool_results exist", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-123",
name: "read_file",
input: { path: "test.txt" },
},
],
}
const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-123",
content: "Content",
},
],
}
validateAndFixToolResultIds(userMessage, [assistantMessage])
expect(TelemetryService.instance.captureException).not.toHaveBeenCalled()
})
})
})

View file

@ -1,10 +1,9 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import { findLastIndex } from "../../shared/array"
/**
* Custom error class for tool result ID mismatches.
* Used for structured error tracking via PostHog.
* Used for structured error tracking via external service.
*/
export class ToolResultIdMismatchError extends Error {
constructor(
@ -19,7 +18,7 @@ export class ToolResultIdMismatchError extends Error {
/**
* Custom error class for missing tool results.
* Used for structured error tracking via PostHog when tool_use blocks
* Used for structured error tracking via external service when tool_use blocks
* don't have corresponding tool_result blocks.
*/
export class MissingToolResultError extends Error {
@ -131,40 +130,6 @@ export function validateAndFixToolResultIds(
const toolResultIdList = toolResults.map((r) => r.tool_use_id)
const toolUseIdList = toolUseBlocks.map((b) => b.id)
// Report missing tool_results to PostHog error tracking
if (missingToolUseIds.length > 0 && TelemetryService.hasInstance()) {
TelemetryService.instance.captureException(
new MissingToolResultError(
`Detected missing tool_result blocks. Missing tool_use IDs: [${missingToolUseIds.join(", ")}], existing tool_result IDs: [${toolResultIdList.join(", ")}]`,
missingToolUseIds,
toolResultIdList,
),
{
missingToolUseIds,
existingToolResultIds: toolResultIdList,
toolUseCount: toolUseBlocks.length,
toolResultCount: toolResults.length,
},
)
}
// Report ID mismatches to PostHog error tracking
if (hasInvalidIds && TelemetryService.hasInstance()) {
TelemetryService.instance.captureException(
new ToolResultIdMismatchError(
`Detected tool_result ID mismatch. tool_result IDs: [${toolResultIdList.join(", ")}], tool_use IDs: [${toolUseIdList.join(", ")}]`,
toolResultIdList,
toolUseIdList,
),
{
toolResultIds: toolResultIdList,
toolUseIds: toolUseIdList,
toolResultCount: toolResults.length,
toolUseCount: toolUseBlocks.length,
},
)
}
// Match tool_results to tool_uses by position and fix incorrect IDs
const usedToolUseIds = new Set<string>()
const contentArray = userMessage.content as Anthropic.Messages.ContentBlockParam[]

View file

@ -2,7 +2,6 @@ import path from "path"
import fs from "fs/promises"
import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { getReadablePath } from "../../utils/path"
import { Task } from "../task/Task"
@ -85,7 +84,6 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
const currentCount = (task.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
task.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
let formattedError = ""
TelemetryService.instance.captureDiffApplicationError(task.taskId, currentCount)
if (diffResult.failParts && diffResult.failParts.length > 0) {
for (const failPart of diffResult.failParts) {

View file

@ -1,7 +1,6 @@
import * as vscode from "vscode"
import { RooCodeEventName, type HistoryItem } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../task/Task"
import { formatResponse } from "../prompts/responses"
@ -200,8 +199,6 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> {
// Force final token usage update before emitting TaskCompleted.
// This ensures the latest stats are captured regardless of throttle timer.
task.emitFinalTokenUsageUpdate()
TelemetryService.instance.captureTaskCompleted(task.taskId)
task.emit(RooCodeEventName.TaskCompleted, task.taskId, task.getTokenUsage(), task.toolUsage)
}
}

View file

@ -5,7 +5,6 @@ import * as vscode from "vscode"
import delay from "delay"
import { CommandExecutionStatus, DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE, PersistedCommandOutput } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../task/Task"
@ -359,13 +358,6 @@ export async function executeCommandInTerminal(
},
}
if (terminalProvider === "vscode") {
callbacks.onNoShellIntegration = async (error: string) => {
TelemetryService.instance.captureShellIntegrationError(task.taskId)
shellIntegrationError = error
}
}
const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, task.taskId, terminalProvider)
if (terminal instanceof Terminal) {

View file

@ -11,17 +11,6 @@ vi.mock("../../prompts/responses", () => ({
},
}))
const { mockCaptureTaskCompleted } = vi.hoisted(() => ({
mockCaptureTaskCompleted: vi.fn(),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTaskCompleted: mockCaptureTaskCompleted,
},
},
}))
// Mock vscode module
vi.mock("vscode", () => ({
workspace: {
@ -52,7 +41,6 @@ describe("attemptCompletionTool", () => {
let mockGetConfiguration: ReturnType<typeof vi.fn>
beforeEach(() => {
mockCaptureTaskCompleted.mockReset()
mockPushToolResult = vi.fn()
mockAskApproval = vi.fn()
mockHandleError = vi.fn()
@ -506,7 +494,6 @@ describe("attemptCompletionTool", () => {
await attemptCompletionTool.handle(mockTask as Task, block, callbacks)
expect(mockHandleError).not.toHaveBeenCalled()
expect(mockCaptureTaskCompleted).toHaveBeenCalledWith("task_1")
expect(mockTask.emit).toHaveBeenCalledWith(
RooCodeEventName.TaskCompleted,
"task_1",
@ -541,7 +528,6 @@ describe("attemptCompletionTool", () => {
await attemptCompletionTool.handle(mockTask as Task, block, callbacks)
expect(mockHandleError).not.toHaveBeenCalled()
expect(mockCaptureTaskCompleted).not.toHaveBeenCalled()
expect(mockTask.emit).not.toHaveBeenCalledWith(
RooCodeEventName.TaskCompleted,
expect.anything(),

View file

@ -17,13 +17,6 @@ import {
type ProviderSettings,
type RooCodeSettings,
type ProviderSettingsEntry,
type StaticAppProperties,
type DynamicAppProperties,
type CloudAppProperties,
type TaskProperties,
type GitProperties,
type TelemetryProperties,
type TelemetryPropertiesProvider,
type CodeActionId,
type CodeActionName,
type TerminalActionId,
@ -48,7 +41,6 @@ import {
isRetiredProvider,
} from "@roo-code/types"
import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud"
import { Package } from "../../shared/package"
@ -124,7 +116,7 @@ interface PendingEditOperation {
export class ClineProvider
extends EventEmitter<TaskProviderEvents>
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
implements vscode.WebviewViewProvider, TaskProviderLike
{
// Used in package.json as the view's id. This value cannot be changed due
// to how VSCode caches views based on their id, and updating the id would
@ -196,13 +188,6 @@ export class ClineProvider
this.log(`Failed to initialize TaskHistoryStore: ${error}`)
})
// Start configuration loading (which might trigger indexing) in the background.
// Don't await, allowing activation to continue immediately.
// Register this provider with the telemetry service to enable it to add
// properties like mode and provider.
TelemetryService.instance.setProvider(this)
this._workspaceTracker = new WorkspaceTracker(this)
this.providerSettingsManager = new ProviderSettingsManager(this.context)
@ -761,9 +746,6 @@ export class ClineProvider
promptType: CodeActionName,
params: Record<string, string | any[]>,
): Promise<void> {
// Capture telemetry for code action usage
TelemetryService.instance.captureCodeActionUsed(promptType)
const visibleProvider = await ClineProvider.getInstance()
if (!visibleProvider) {
@ -793,8 +775,6 @@ export class ClineProvider
promptType: TerminalActionPromptType,
params: Record<string, string | any[]>,
): Promise<void> {
TelemetryService.instance.captureCodeActionUsed(promptType)
const visibleProvider = await ClineProvider.getInstance()
if (!visibleProvider) {
@ -1261,8 +1241,8 @@ export class ClineProvider
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
`media-src ${webview.cspSource}`,
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src ${webview.cspSource} ${openRouterDomain} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
`script-src 'unsafe-eval' ${webview.cspSource} https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src ${webview.cspSource} ${openRouterDomain} https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
@ -1350,7 +1330,7 @@ export class ClineProvider
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://ph.roocode.com 'strict-dynamic'; connect-src ${webview.cspSource} ${openRouterDomain} https://api.requesty.ai https://ph.roocode.com;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' 'strict-dynamic'; connect-src ${webview.cspSource} ${openRouterDomain} https://api.requesty.ai;">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<script nonce="${nonce}">
@ -1391,7 +1371,6 @@ export class ClineProvider
const task = this.getCurrentTask()
if (task) {
TelemetryService.instance.captureModeSwitch(task.taskId, newMode)
task.emit(RooCodeEventName.TaskModeSwitched, task.taskId, newMode)
try {
@ -1412,7 +1391,6 @@ export class ClineProvider
`Failed to persist mode switch for task ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`,
)
// Optionally, we could emit an event to notify about the failure.
// This ensures the in-memory state remains consistent with persisted state.
throw error
}
@ -2156,7 +2134,6 @@ export class ClineProvider
maxOpenTabsContext,
maxWorkspaceFiles,
disabledTools,
telemetrySetting,
showRooIgnoredFiles,
enableSubfolderRules,
language,
@ -2211,9 +2188,6 @@ export class ClineProvider
} catch (error) {
// Ignore this error.
}
const telemetryKey = process.env.POSTHOG_API_KEY
const machineId = vscode.env.machineId
const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands)
const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands)
const cwd = this.cwd
@ -2248,8 +2222,7 @@ export class ClineProvider
ttsSpeed: ttsSpeed ?? 1.0,
enableCheckpoints: enableCheckpoints ?? true,
checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
shouldShowAnnouncement:
telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands: mergedAllowedCommands,
deniedCommands: mergedDeniedCommands,
soundVolume: soundVolume ?? 0.5,
@ -2278,9 +2251,6 @@ export class ClineProvider
maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
cwd,
disabledTools,
telemetrySetting,
telemetryKey,
machineId,
showRooIgnoredFiles: showRooIgnoredFiles ?? false,
enableSubfolderRules: enableSubfolderRules ?? false,
language: language ?? formatLanguage(vscode.env.language),
@ -2503,7 +2473,6 @@ export class ClineProvider
maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
disabledTools: stateValues.disabledTools,
telemetrySetting: stateValues.telemetrySetting || "unset",
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,
enableSubfolderRules: stateValues.enableSubfolderRules ?? false,
maxImageFileSize: stateValues.maxImageFileSize ?? 5,
@ -2784,7 +2753,7 @@ export class ClineProvider
}
/**
* TaskProviderLike, TelemetryPropertiesProvider
* TaskProviderLike
*/
public getCurrentTask(): Task | undefined {
@ -3081,100 +3050,6 @@ export class ClineProvider
await this.activateProviderProfile({ name })
}
// Telemetry
private _appProperties?: StaticAppProperties
private _gitProperties?: GitProperties
private getAppProperties(): StaticAppProperties {
if (!this._appProperties) {
const packageJSON = this.context.extension?.packageJSON
this._appProperties = {
appName: packageJSON?.name ?? Package.name,
appVersion: packageJSON?.version ?? Package.version,
vscodeVersion: vscode.version,
platform: process.platform,
editorName: vscode.env.appName,
}
}
return this._appProperties
}
public get appProperties(): StaticAppProperties {
return this._appProperties ?? this.getAppProperties()
}
private getCloudProperties(): CloudAppProperties {
let cloudIsAuthenticated: boolean | undefined
try {
if (CloudService.hasInstance()) {
cloudIsAuthenticated = CloudService.instance.isAuthenticated()
}
} catch (error) {
// Silently handle errors to avoid breaking telemetry collection.
this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`)
}
return {
cloudIsAuthenticated,
}
}
private async getTaskProperties(): Promise<DynamicAppProperties & TaskProperties> {
const { language = "en", mode, apiConfiguration } = await this.getState()
const task = this.getCurrentTask()
const todoList = task?.todoList
let todos: { total: number; completed: number; inProgress: number; pending: number } | undefined
if (todoList && todoList.length > 0) {
todos = {
total: todoList.length,
completed: todoList.filter((todo) => todo.status === "completed").length,
inProgress: todoList.filter((todo) => todo.status === "in_progress").length,
pending: todoList.filter((todo) => todo.status === "pending").length,
}
}
const apiProvider = apiConfiguration?.apiProvider
return {
language,
mode,
taskId: task?.taskId,
parentTaskId: task?.parentTaskId,
apiProvider: apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined,
modelId: task?.api?.getModel().id,
diffStrategy: task?.diffStrategy?.getName(),
isSubtask: task ? !!task.parentTaskId : undefined,
...(todos && { todos }),
}
}
private async getGitProperties(): Promise<GitProperties> {
if (!this._gitProperties) {
this._gitProperties = await getWorkspaceGitInfo()
}
return this._gitProperties
}
public get gitProperties(): GitProperties | undefined {
return this._gitProperties
}
public async getTelemetryProperties(): Promise<TelemetryProperties> {
return {
...this.getAppProperties(),
...this.getCloudProperties(),
...(await this.getTaskProperties()),
...(await this.getGitProperties()),
}
}
public get cwd() {
return this.currentWorkspacePath || getWorkspacePath()
}

View file

@ -2,7 +2,6 @@
import * as vscode from "vscode"
import { TelemetryService } from "@roo-code/telemetry"
import { getModelId } from "@roo-code/types"
import { ContextProxy } from "../../config/ContextProxy"
@ -144,10 +143,6 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
beforeEach(async () => {
vi.clearAllMocks()
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
const globalState: Record<string, any> = {
mode: "code",
currentApiConfigName: "test-config",

Some files were not shown because too many files have changed in this diff Show more