Add tracking for Automations, Plugin Detail & Loop (#3103)

* Add tracking for Automations, Plugin Detail, and Plugin Loop

- RoutinesSection: track new_automation, create, save, cancel, run_now,
  edit, pause, resume, delete, history button clicks
- PluginDetailView: track back navigation and use_plugin action
- PluginLoopHome: track clear_active, submit, card_details, card_use
- Extend AutomationsClickProps with new CRUD elements
- Add PluginDetailClickProps and PluginLoopClickProps contracts

* fix: address review comments on plugin/automation tracking

- Extract onBack handler in PluginDetailView to cover both error-path
  and success-path back buttons with tracking
- Move create/save tracking from submit button onClick into the form
  submit handler to capture keyboard submissions and avoid false
  positives from validation failures

* fix: move submit tracking into submit() handler in PluginLoopHome

Same fix as RoutinesSection: tracking now fires inside submit() so
keyboard Enter submissions are captured and the !trimmed guard
prevents false positives.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
This commit is contained in:
elihahah666 2026-05-28 00:48:41 +08:00 committed by GitHub
parent 279da4f3b6
commit dadf8a5bc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 73 additions and 10 deletions

View file

@ -36,6 +36,8 @@ import type {
PluginsTemplatesDropdownClickProps,
PluginsAvailableTabClickProps,
PluginsSourcesTabClickProps,
PluginDetailClickProps,
PluginLoopClickProps,
DesignSystemsTopClickProps,
DesignSystemsTemplateCardClickProps,
DesignSystemsTemplatesModalClickProps,
@ -326,6 +328,20 @@ export function trackPluginsSourcesTabClick(
send(track, 'ui_click', props);
}
export function trackPluginDetailClick(
track: Track,
props: PluginDetailClickProps,
): void {
send(track, 'ui_click', props);
}
export function trackPluginLoopClick(
track: Track,
props: PluginLoopClickProps,
): void {
send(track, 'ui_click', props);
}
export function trackDesignSystemsTopClick(
track: Track,
props: DesignSystemsTopClickProps,

View file

@ -12,6 +12,8 @@ import type { ApplyResult, InstalledPluginRecord } from '@open-design/contracts'
import { applyPlugin } from '../state/projects';
import { navigate } from '../router';
import { useI18n } from '../i18n';
import { useAnalytics } from '../analytics/provider';
import { trackPluginDetailClick } from '../analytics/events';
interface Props {
pluginId: string;
@ -19,11 +21,17 @@ interface Props {
export function PluginDetailView(props: Props) {
const { locale } = useI18n();
const analytics = useAnalytics();
const [plugin, setPlugin] = useState<InstalledPluginRecord | null>(null);
const [error, setError] = useState<string | null>(null);
const [applying, setApplying] = useState(false);
const [applied, setApplied] = useState<ApplyResult | null>(null);
const onBack = () => {
trackPluginDetailClick(analytics.track, { page_name: 'plugins', area: 'plugin_detail', element: 'back', plugin_id: props.pluginId });
navigate({ kind: 'marketplace' });
};
useEffect(() => {
let cancelled = false;
void fetch(`/api/plugins/${encodeURIComponent(props.pluginId)}`)
@ -47,7 +55,7 @@ export function PluginDetailView(props: Props) {
if (error) {
return (
<div className="plugin-detail" data-testid="plugin-detail">
<button type="button" onClick={() => navigate({ kind: 'marketplace' })}>
<button type="button" onClick={onBack}>
Marketplace
</button>
<div role="alert">Failed to load plugin: {error}</div>
@ -80,6 +88,7 @@ export function PluginDetailView(props: Props) {
}>;
const onUse = async () => {
trackPluginDetailClick(analytics.track, { page_name: 'plugins', area: 'plugin_detail', element: 'use_plugin', plugin_id: plugin.id });
setApplying(true);
setError(null);
const result = await applyPlugin(plugin.id, { locale });
@ -100,7 +109,7 @@ export function PluginDetailView(props: Props) {
<button
type="button"
className="plugin-detail__back"
onClick={() => navigate({ kind: 'marketplace' })}
onClick={onBack}
>
Marketplace
</button>

View file

@ -15,6 +15,8 @@ import { Icon } from './Icon';
import { PluginDetailsModal } from './PluginDetailsModal';
import { TrustBadge } from './TrustBadge';
import { authorInitials, derivePluginSourceLinks } from '../runtime/plugin-source';
import { useAnalytics } from '../analytics/provider';
import { trackPluginLoopClick } from '../analytics/events';
export interface PluginLoopSubmit {
prompt: string;
@ -56,6 +58,7 @@ interface ActivePlugin {
export function PluginLoopHome({ onSubmit }: Props) {
const { locale } = useI18n();
const analytics = useAnalytics();
const [plugins, setPlugins] = useState<InstalledPluginRecord[]>([]);
const [loading, setLoading] = useState(true);
const [pendingApplyId, setPendingApplyId] = useState<string | null>(null);
@ -128,6 +131,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
function submit() {
const trimmed = prompt.trim();
if (!trimmed) return;
trackPluginLoopClick(analytics.track, { page_name: 'plugins', area: 'plugin_loop', element: 'submit', plugin_id: active?.record.id });
onSubmit({
prompt: trimmed,
pluginId: active?.record.id ?? null,
@ -168,7 +172,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
<button
type="button"
className="plugin-loop-home__active-clear"
onClick={clearActive}
onClick={() => { trackPluginLoopClick(analytics.track, { page_name: 'plugins', area: 'plugin_loop', element: 'clear_active', plugin_id: active?.record.id }); clearActive(); }}
aria-label="Clear active plugin"
title="Clear active plugin"
>
@ -293,7 +297,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
<button
type="button"
className="plugin-loop-home__card-details"
onClick={() => openDetails(p)}
onClick={() => { trackPluginLoopClick(analytics.track, { page_name: 'plugins', area: 'plugin_loop', element: 'card_details', plugin_id: p.id }); openDetails(p); }}
aria-label={`View details for ${p.title}`}
data-testid={`view-details-${p.id}`}
title="View plugin details"
@ -304,7 +308,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
<button
type="button"
className="plugin-loop-home__card-action"
onClick={() => void usePlugin(p)}
onClick={() => { trackPluginLoopClick(analytics.track, { page_name: 'plugins', area: 'plugin_loop', element: 'card_use', plugin_id: p.id }); void usePlugin(p); }}
disabled={isPending || pendingApplyId !== null}
aria-busy={isPending ? 'true' : undefined}
data-testid={`use-example-${p.id}`}

View file

@ -13,6 +13,8 @@ import { Icon } from './Icon';
import { navigate } from '../router';
import { useT } from '../i18n';
import type { Dict } from '../i18n/types';
import { useAnalytics } from '../analytics/provider';
import { trackAutomationsClick } from '../analytics/events';
// Shared translator signature: every sub-component in this file is module-scoped,
// so `t` from `useT()` is threaded down as a prop rather than re-hooked.
@ -457,6 +459,10 @@ function RunHistory({
export function RoutinesSection({ onClose }: RoutinesSectionProps) {
const t = useT();
const analytics = useAnalytics();
const fireAutomation = (element: 'new_automation' | 'create' | 'save' | 'cancel' | 'run_now' | 'edit' | 'pause' | 'resume' | 'delete' | 'history') => {
trackAutomationsClick(analytics.track, { page_name: 'automations', area: 'automations', element });
};
const [routines, setRoutines] = useState<Routine[]>([]);
const [projects, setProjects] = useState<ProjectSummary[]>([]);
const [loading, setLoading] = useState(true);
@ -515,6 +521,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
const submit = async (e: FormEvent) => {
e.preventDefault();
fireAutomation(editingId ? 'save' : 'create');
setSubmitting(true);
setError(null);
try {
@ -625,6 +632,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
type="button"
className="btn btn-primary"
onClick={() => {
fireAutomation('new_automation');
setForm(emptyForm());
setShowForm(true);
}}
@ -715,6 +723,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
type="button"
className="btn"
onClick={() => {
fireAutomation('cancel');
setShowForm(false);
setEditingId(null);
setForm(emptyForm());
@ -789,7 +798,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
<button
type="button"
className="btn btn-primary"
onClick={() => runNow(r.id)}
onClick={() => { fireAutomation('run_now'); runNow(r.id); }}
disabled={isBusy}
>
{t('routines.runNow')}
@ -798,6 +807,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
type="button"
className="btn"
onClick={() => {
fireAutomation('edit');
setForm(formFromRoutine(r));
setEditingId(r.id);
setShowForm(true);
@ -809,7 +819,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
<button
type="button"
className="btn"
onClick={() => toggleEnabled(r)}
onClick={() => { fireAutomation(r.enabled ? 'pause' : 'resume'); toggleEnabled(r); }}
disabled={isBusy}
>
{r.enabled ? t('routines.pause') : t('routines.resume')}
@ -817,7 +827,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
<button
type="button"
className="btn btn-ghost"
onClick={() => setExpandedId(isExpanded ? null : r.id)}
onClick={() => { fireAutomation('history'); setExpandedId(isExpanded ? null : r.id); }}
aria-expanded={isExpanded}
>
{isExpanded ? t('routines.hideHistory') : t('routines.history')}
@ -825,7 +835,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
<button
type="button"
className="btn btn-ghost btn-danger"
onClick={() => remove(r.id)}
onClick={() => { fireAutomation('delete'); remove(r.id); }}
disabled={isBusy}
title={t('routines.deleteTitle')}
>

View file

@ -1016,7 +1016,15 @@ export interface AutomationsClickProps {
| 'run_now'
| 'open_artifact'
| 'type_card'
| 'filter_tab';
| 'filter_tab'
| 'edit'
| 'pause'
| 'resume'
| 'delete'
| 'history'
| 'cancel'
| 'create'
| 'save';
type_id?: 'orbit' | 'routines' | 'schedules' | 'live_artifacts';
filter_id?: 'all' | 'scheduled' | 'running' | 'done';
}
@ -1078,6 +1086,20 @@ export interface PluginsSourcesTabClickProps {
plugin_type?: string;
}
export interface PluginDetailClickProps {
page_name: 'plugins';
area: 'plugin_detail';
element: 'back' | 'use_plugin';
plugin_id?: string;
}
export interface PluginLoopClickProps {
page_name: 'plugins';
area: 'plugin_loop';
element: 'clear_active' | 'submit' | 'card_details' | 'card_use';
plugin_id?: string;
}
// DESIGN SYSTEMS
export interface DesignSystemsTopClickProps {
page_name: 'design_systems';
@ -1490,6 +1512,8 @@ export type UiClickProps =
| PluginsTemplatesDropdownClickProps
| PluginsAvailableTabClickProps
| PluginsSourcesTabClickProps
| PluginDetailClickProps
| PluginLoopClickProps
| DesignSystemsTopClickProps
| DesignSystemsTemplateCardClickProps
| DesignSystemsTemplatesModalClickProps