mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
3d37e054dd
commit
ff16c9c297
220 changed files with 402 additions and 7964 deletions
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (self‑hosted 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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -34,4 +34,3 @@ export {
|
|||
export { BlogFrontmatterSchema, type ValidatedFrontmatter } from "./validation"
|
||||
|
||||
// Analytics
|
||||
export { trackBlogIndexView, trackBlogPostView, trackSubstackClick } from "./analytics"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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("[]")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# @roo-code/telemetry
|
||||
|
||||
## 0.0.1
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./BaseTelemetryClient"
|
||||
export * from "./PostHogTelemetryClient"
|
||||
export * from "./TelemetryService"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/vscode-library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
},
|
||||
})
|
||||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
353
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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: () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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({})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 : ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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)", () => {
|
||||
|
|
|
|||
|
|
@ -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: "" }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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([]),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue