mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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:
parent
279da4f3b6
commit
dadf8a5bc3
5 changed files with 73 additions and 10 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue