- 
                Notifications
    You must be signed in to change notification settings 
- Fork 2.4k
feat: Add telemetry tracking to DismissibleUpsell component #8309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -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( | |
| <div | ||
| className={containerClasses} | ||
| onClick={() => { | ||
| // Track telemetry for click | ||
| if (onClick) { | ||
| telemetryClient.capture(TelemetryEventName.UPSELL_CLICKED, { | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Intentional behavior: UPSELL_CLICKED is emitted only when onClick exists. If analytics should also capture container-dismiss without onClick (dismissOnClick=true), consider adding a separate event or documenting this decision. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, let's make sure to track these as dismissals. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if Roomote implemented this change, it doesn't seem like it did There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be right, wdyt? | ||
| upsellId: upsellId, | ||
| }) | ||
| } | ||
|  | ||
| // Call the onClick handler if provided | ||
| onClick?.() | ||
| // Also dismiss if dismissOnClick is true | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -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<string, any>) => 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( | ||
| <DismissibleUpsell upsellId="test-upsell" onDismiss={onDismiss}> | ||
|  | @@ -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( | ||
| <DismissibleUpsell upsellId="test-upsell" onClick={onClick}> | ||
|  | @@ -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 () => { | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Add a test for the scenario where dismissOnClick is true and no onClick is provided. Expect only UPSELL_DISMISSED to be captured (and not UPSELL_CLICKED) to lock in the intended behavior. | ||
| 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( | ||
| <DismissibleUpsell upsellId="test-upsell" onDismiss={onDismiss} dismissOnClick={true}> | ||
| <div>Test content</div> | ||
| </DismissibleUpsell>, | ||
| ) | ||
|  | ||
| // 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() | ||
|  | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: handleDismiss is marked async but doesn’t await anything. Making it synchronous avoids misleading consumers and minor overhead.