diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index bba1485722..81c20176ff 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -61,6 +61,9 @@ export enum TelemetryEventName { ACCOUNT_LOGOUT_CLICKED = "Account Logout Clicked", ACCOUNT_LOGOUT_SUCCESS = "Account Logout Success", + 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", @@ -181,6 +184,8 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.ACCOUNT_CONNECT_SUCCESS, TelemetryEventName.ACCOUNT_LOGOUT_CLICKED, TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS, + TelemetryEventName.UPSELL_DISMISSED, + TelemetryEventName.UPSELL_CLICKED, TelemetryEventName.SCHEMA_VALIDATION_ERROR, TelemetryEventName.DIFF_APPLICATION_ERROR, TelemetryEventName.SHELL_INTEGRATION_ERROR, diff --git a/webview-ui/src/components/common/DismissibleUpsell.tsx b/webview-ui/src/components/common/DismissibleUpsell.tsx index 2eefb89972..6f0e1ad2dc 100644 --- a/webview-ui/src/components/common/DismissibleUpsell.tsx +++ b/webview-ui/src/components/common/DismissibleUpsell.tsx @@ -1,6 +1,8 @@ import { memo, ReactNode, useEffect, useState, useRef } from "react" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" +import { telemetryClient } from "@src/utils/TelemetryClient" +import { TelemetryEventName } from "@roo-code/types" interface DismissibleUpsellProps { /** Required unique identifier for this upsell */ @@ -76,7 +78,12 @@ const DismissibleUpsell = memo( } }, [upsellId]) - const handleDismiss = async () => { + const handleDismiss = () => { + // Track telemetry for dismissal + telemetryClient.capture(TelemetryEventName.UPSELL_DISMISSED, { + upsellId: upsellId, + }) + // First notify the extension to persist the dismissal // This ensures the message is sent even if the component unmounts quickly vscode.postMessage({ @@ -134,6 +141,13 @@ const DismissibleUpsell = memo(
{ + // Track telemetry for click + if (onClick) { + telemetryClient.capture(TelemetryEventName.UPSELL_CLICKED, { + upsellId: upsellId, + }) + } + // Call the onClick handler if provided onClick?.() // Also dismiss if dismissOnClick is true diff --git a/webview-ui/src/components/common/__tests__/DismissibleUpsell.spec.tsx b/webview-ui/src/components/common/__tests__/DismissibleUpsell.spec.tsx index 3af66dfdf1..69e4e94a4f 100644 --- a/webview-ui/src/components/common/__tests__/DismissibleUpsell.spec.tsx +++ b/webview-ui/src/components/common/__tests__/DismissibleUpsell.spec.tsx @@ -1,6 +1,7 @@ import { render, screen, fireEvent, waitFor, act } from "@testing-library/react" import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" import DismissibleUpsell from "../DismissibleUpsell" +import { TelemetryEventName } from "@roo-code/types" // Mock the vscode API const mockPostMessage = vi.fn() @@ -10,6 +11,14 @@ vi.mock("@src/utils/vscode", () => ({ }, })) +// Mock telemetryClient +const mockCapture = vi.fn() +vi.mock("@src/utils/TelemetryClient", () => ({ + telemetryClient: { + capture: (eventName: string, properties?: Record) => mockCapture(eventName, properties), + }, +})) + // Mock the translation hook vi.mock("@src/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ @@ -26,6 +35,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ describe("DismissibleUpsell", () => { beforeEach(() => { mockPostMessage.mockClear() + mockCapture.mockClear() vi.clearAllTimers() }) @@ -72,7 +82,7 @@ describe("DismissibleUpsell", () => { }) }) - it("hides the upsell when dismiss button is clicked", async () => { + it("hides the upsell when dismiss button is clicked and tracks telemetry", async () => { const onDismiss = vi.fn() const { container } = render( @@ -92,6 +102,11 @@ describe("DismissibleUpsell", () => { const dismissButton = screen.getByRole("button", { name: /dismiss/i }) fireEvent.click(dismissButton) + // Check that telemetry was tracked + expect(mockCapture).toHaveBeenCalledWith(TelemetryEventName.UPSELL_DISMISSED, { + upsellId: "test-upsell", + }) + // Check that the dismiss message was sent BEFORE hiding expect(mockPostMessage).toHaveBeenCalledWith({ type: "dismissUpsell", @@ -351,7 +366,7 @@ describe("DismissibleUpsell", () => { }) }) - it("calls onClick when the container is clicked", async () => { + it("calls onClick when the container is clicked and tracks telemetry", async () => { const onClick = vi.fn() render( @@ -372,6 +387,11 @@ describe("DismissibleUpsell", () => { fireEvent.click(container) expect(onClick).toHaveBeenCalledTimes(1) + + // Check that telemetry was tracked + expect(mockCapture).toHaveBeenCalledWith(TelemetryEventName.UPSELL_CLICKED, { + upsellId: "test-upsell", + }) }) it("does not call onClick when dismiss button is clicked", async () => { @@ -470,7 +490,7 @@ describe("DismissibleUpsell", () => { }) }) - it("dismisses when clicked if dismissOnClick is true", async () => { + it("dismisses when clicked if dismissOnClick is true and tracks both telemetry events", async () => { const onClick = vi.fn() const onDismiss = vi.fn() const { container } = render( @@ -493,6 +513,14 @@ describe("DismissibleUpsell", () => { expect(onClick).toHaveBeenCalledTimes(1) expect(onDismiss).toHaveBeenCalledTimes(1) + // Check that both telemetry events were tracked + expect(mockCapture).toHaveBeenCalledWith(TelemetryEventName.UPSELL_CLICKED, { + upsellId: "test-upsell", + }) + expect(mockCapture).toHaveBeenCalledWith(TelemetryEventName.UPSELL_DISMISSED, { + upsellId: "test-upsell", + }) + expect(mockPostMessage).toHaveBeenCalledWith({ type: "dismissUpsell", upsellId: "test-upsell", @@ -503,6 +531,46 @@ describe("DismissibleUpsell", () => { }) }) + it("dismisses on container click when dismissOnClick is true and no onClick is provided; tracks only dismissal", async () => { + const onDismiss = vi.fn() + const { container } = render( + +
Test content
+
, + ) + + // Make component visible + makeUpsellVisible() + + // Wait for component to be visible + await waitFor(() => { + expect(screen.getByText("Test content")).toBeInTheDocument() + }) + + // Click on the container (not the dismiss button) + const containerDiv = screen.getByText("Test content").parentElement as HTMLElement + fireEvent.click(containerDiv) + + // onDismiss should be called + expect(onDismiss).toHaveBeenCalledTimes(1) + + // Telemetry: only dismissal should be tracked + expect(mockCapture).toHaveBeenCalledWith(TelemetryEventName.UPSELL_DISMISSED, { + upsellId: "test-upsell", + }) + expect(mockCapture).not.toHaveBeenCalledWith(TelemetryEventName.UPSELL_CLICKED, expect.anything()) + + // Dismiss message should be sent + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "dismissUpsell", + upsellId: "test-upsell", + }) + + // Component should be hidden + await waitFor(() => { + expect(container.firstChild).toBeNull() + }) + }) it("does not dismiss when clicked if dismissOnClick is false", async () => { const onClick = vi.fn() const onDismiss = vi.fn()