From a472c5865e07b582b06c3f9a2e1687a0367c958f Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:08:33 -0700 Subject: [PATCH 1/2] fix(byo): count the "Run it here" offer when it renders and when it is picked (TASK-644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Growth's activation script reads hosted-offer-shown and hosted-offer-clicked from byo_telemetry. Both steps were allowlisted on the server (#2664), but the client only emitted them for the post-issue fallback. The "Run it here" offer that people see on first paint was never counted, so the shown count stayed near zero while the offer was on screen. - Record hosted-offer-shown once per page view when the hosted chooser or the compact hosted form renders on a hosting instance (recordByoStepOnce). - Record hosted-offer-clicked when the person picks "Run it here", and when the compact hosted submit is pressed. - Tests: first-paint exposure and pick, once per load; existing expectations now count the first-paint exposure alongside the fallback exposure. Written by Dart, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../__tests__/V2AgentBYOHostedOffer.test.tsx | 23 +++++++++++++------ frontend/src/v2/components/V2AgentBYO.tsx | 21 ++++++++++++++--- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx index 02251ec979..cae35df643 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx @@ -9,6 +9,7 @@ import { render, screen, fireEvent, waitFor, act } from '@testing-library/react' import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom'; import V2AgentBYO from '../components/V2AgentBYO'; import { AuthContext } from '../../context/AuthContext'; +import { resetByoStepsForTest } from '../utils/byoTelemetry'; jest.mock('axios', () => { const mock = { @@ -63,7 +64,7 @@ const renderPage = () => render( ); beforeEach(() => { - sessionStorage.clear(); + sessionStorage.clear(); resetByoStepsForTest(); window.matchMedia = jest.fn().mockReturnValue({ matches: true }); jest.useFakeTimers(); mockGet({ configured: true }); axios.post.mockImplementation((url) => Promise.resolve({ data: url.includes('runtime-tokens') ? { token: 'fixture-only-key' } : { ready: true } })); @@ -101,7 +102,7 @@ test('page entry is recorded once under StrictMode; command rendering is separat render(); await screen.findByTestId('byo-mode-byo'); fireEvent.click(screen.getByTestId('byo-mode-byo')); await waitFor(() => expect(screen.getByRole('button', { name: /get my connect command/i })).toBeEnabled()); fireEvent.click(screen.getByRole('button', { name: /get my connect command/i })); await screen.findByText(/your connect command for/i); - expect(axios.post.mock.calls.filter(([url]) => url === '/api/stats/byo-step').map(([, body]) => body)).toEqual([{ step: 'page-viewed' }, { step: 'seat-created' }]); + expect(axios.post.mock.calls.filter(([url]) => url === '/api/stats/byo-step').map(([, body]) => body)).toEqual([{ step: 'page-viewed' }, { step: 'hosted-offer-shown' }, { step: 'seat-created' }]); }); test('a failed clipboard write leaves the fallback available', async () => { Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: jest.fn().mockRejectedValue(new Error('Denied')) } }); @@ -172,15 +173,16 @@ const offerEvents = () => axios.post.mock.calls .map(([, body]) => body); test.each(['timer', 'exit'])('records offer shown once via %s despite later timers and exit intent', async (trigger) => { const view = await issue(); - expect(offerEvents()).toEqual([]); + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }]); if (trigger === 'timer') await tick(30000); else fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); expect(offer()).toBeInTheDocument(); await tick(60000); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); - expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }]); + // The first-paint exposure is counted too; the fallback adds its own, once. + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }, { step: 'hosted-offer-shown' }]); view.unmount(); await issue(); await tick(30000); - expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }]); + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }, { step: 'hosted-offer-shown' }]); }); test('records accepted offer activation once while hosted lookup is pending', async () => { await issue(); await tick(30000); @@ -188,12 +190,19 @@ test('records accepted offer activation once while hosted lookup is pending', as axios.get.mockImplementation((url) => url.startsWith('/api/registry/agents/next-free-name?') ? new Promise(resolve => { resolveName = resolve; }) : get(url)); const button = offer(); fireEvent.click(button); fireEvent.click(button); - expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }, { step: 'hosted-offer-clicked' }]); + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }, { step: 'hosted-offer-shown' }, { step: 'hosted-offer-clicked' }]); await act(async () => resolveName({ data: { name: 'sam-agent' } })); await waitFor(() => expect(axios.post).toHaveBeenCalledWith('/api/hosted/provision', { agentName: 'sam-agent' }, expect.anything())); }); test('copy suppression emits neither hosted-offer event', async () => { await issue(); fireEvent.copy(screen.getByTestId('byo-listen').querySelector('.v2-byo__command')); await tick(60000); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); - expect(offerEvents()).toEqual([]); + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }]); +}); +test('"Run it here" exposure is counted on first paint, once, and a pick counts as a click', async () => { + renderPage(); await screen.findByTestId('byo-mode-hosted'); + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }]); + fireEvent.click(screen.getByTestId('byo-mode-byo')); + fireEvent.click(screen.getByTestId('byo-mode-hosted')); + expect(offerEvents()).toEqual([{ step: 'hosted-offer-shown' }, { step: 'hosted-offer-clicked' }]); }); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 95caeccef0..15e6ef1d0c 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; -import { recordByoStep, type ByoStep } from '../utils/byoTelemetry'; +import { recordByoStep, recordByoStepOnce, type ByoStep } from '../utils/byoTelemetry'; import getApiBaseUrl from '../../utils/apiBaseUrl'; import { daemonSetupCommand } from '../utils/daemonSetupCommand'; import V2FeaturePage from './V2FeaturePage'; @@ -906,6 +906,15 @@ const V2AgentBYO: React.FC = () => { recordByoStep(api, 'page-viewed'); }, [api, currentUser]); + // "Run it here" is on screen from first paint whenever the instance hosts, + // so its exposure is counted here, once per page view. The post-issue + // fallback below counts a separate, later exposure. + const hostedChooserOnScreen = Boolean(hosting?.configured) && !issued && !hosted && !placed && !reconnectRequest; + useEffect(() => { + if (!currentUser || !hostedChooserOnScreen) return; + recordByoStepOnce(api, 'hosted-offer-shown', currentUser._id); + }, [api, currentUser, hostedChooserOnScreen]); + useEffect(() => { if (commandViewed.current === issued) return; commandViewed.current = issued; @@ -964,7 +973,10 @@ const V2AgentBYO: React.FC = () => { } }; - const chooseMode = (nextMode: 'hosted' | 'byo' | 'machine') => setMode(nextMode); + const chooseMode = (nextMode: 'hosted' | 'byo' | 'machine') => { + if (nextMode === 'hosted') recordByoStep(api, 'hosted-offer-clicked'); + setMode(nextMode); + }; const apiUrl = getApiBaseUrl().replace(/\/+$/, ''); const daemonCommand = daemonSetupCommand(apiUrl); @@ -1061,7 +1073,10 @@ const V2AgentBYO: React.FC = () => { {error &&
{error}
}