mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +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,
|
PluginsTemplatesDropdownClickProps,
|
||||||
PluginsAvailableTabClickProps,
|
PluginsAvailableTabClickProps,
|
||||||
PluginsSourcesTabClickProps,
|
PluginsSourcesTabClickProps,
|
||||||
|
PluginDetailClickProps,
|
||||||
|
PluginLoopClickProps,
|
||||||
DesignSystemsTopClickProps,
|
DesignSystemsTopClickProps,
|
||||||
DesignSystemsTemplateCardClickProps,
|
DesignSystemsTemplateCardClickProps,
|
||||||
DesignSystemsTemplatesModalClickProps,
|
DesignSystemsTemplatesModalClickProps,
|
||||||
|
|
@ -326,6 +328,20 @@ export function trackPluginsSourcesTabClick(
|
||||||
send(track, 'ui_click', props);
|
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(
|
export function trackDesignSystemsTopClick(
|
||||||
track: Track,
|
track: Track,
|
||||||
props: DesignSystemsTopClickProps,
|
props: DesignSystemsTopClickProps,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import type { ApplyResult, InstalledPluginRecord } from '@open-design/contracts'
|
||||||
import { applyPlugin } from '../state/projects';
|
import { applyPlugin } from '../state/projects';
|
||||||
import { navigate } from '../router';
|
import { navigate } from '../router';
|
||||||
import { useI18n } from '../i18n';
|
import { useI18n } from '../i18n';
|
||||||
|
import { useAnalytics } from '../analytics/provider';
|
||||||
|
import { trackPluginDetailClick } from '../analytics/events';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
|
|
@ -19,11 +21,17 @@ interface Props {
|
||||||
|
|
||||||
export function PluginDetailView(props: Props) {
|
export function PluginDetailView(props: Props) {
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
const analytics = useAnalytics();
|
||||||
const [plugin, setPlugin] = useState<InstalledPluginRecord | null>(null);
|
const [plugin, setPlugin] = useState<InstalledPluginRecord | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [applying, setApplying] = useState(false);
|
const [applying, setApplying] = useState(false);
|
||||||
const [applied, setApplied] = useState<ApplyResult | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void fetch(`/api/plugins/${encodeURIComponent(props.pluginId)}`)
|
void fetch(`/api/plugins/${encodeURIComponent(props.pluginId)}`)
|
||||||
|
|
@ -47,7 +55,7 @@ export function PluginDetailView(props: Props) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="plugin-detail" data-testid="plugin-detail">
|
<div className="plugin-detail" data-testid="plugin-detail">
|
||||||
<button type="button" onClick={() => navigate({ kind: 'marketplace' })}>
|
<button type="button" onClick={onBack}>
|
||||||
← Marketplace
|
← Marketplace
|
||||||
</button>
|
</button>
|
||||||
<div role="alert">Failed to load plugin: {error}</div>
|
<div role="alert">Failed to load plugin: {error}</div>
|
||||||
|
|
@ -80,6 +88,7 @@ export function PluginDetailView(props: Props) {
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const onUse = async () => {
|
const onUse = async () => {
|
||||||
|
trackPluginDetailClick(analytics.track, { page_name: 'plugins', area: 'plugin_detail', element: 'use_plugin', plugin_id: plugin.id });
|
||||||
setApplying(true);
|
setApplying(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const result = await applyPlugin(plugin.id, { locale });
|
const result = await applyPlugin(plugin.id, { locale });
|
||||||
|
|
@ -100,7 +109,7 @@ export function PluginDetailView(props: Props) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="plugin-detail__back"
|
className="plugin-detail__back"
|
||||||
onClick={() => navigate({ kind: 'marketplace' })}
|
onClick={onBack}
|
||||||
>
|
>
|
||||||
← Marketplace
|
← Marketplace
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import { Icon } from './Icon';
|
||||||
import { PluginDetailsModal } from './PluginDetailsModal';
|
import { PluginDetailsModal } from './PluginDetailsModal';
|
||||||
import { TrustBadge } from './TrustBadge';
|
import { TrustBadge } from './TrustBadge';
|
||||||
import { authorInitials, derivePluginSourceLinks } from '../runtime/plugin-source';
|
import { authorInitials, derivePluginSourceLinks } from '../runtime/plugin-source';
|
||||||
|
import { useAnalytics } from '../analytics/provider';
|
||||||
|
import { trackPluginLoopClick } from '../analytics/events';
|
||||||
|
|
||||||
export interface PluginLoopSubmit {
|
export interface PluginLoopSubmit {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|
@ -56,6 +58,7 @@ interface ActivePlugin {
|
||||||
|
|
||||||
export function PluginLoopHome({ onSubmit }: Props) {
|
export function PluginLoopHome({ onSubmit }: Props) {
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
const analytics = useAnalytics();
|
||||||
const [plugins, setPlugins] = useState<InstalledPluginRecord[]>([]);
|
const [plugins, setPlugins] = useState<InstalledPluginRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [pendingApplyId, setPendingApplyId] = useState<string | null>(null);
|
const [pendingApplyId, setPendingApplyId] = useState<string | null>(null);
|
||||||
|
|
@ -128,6 +131,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
|
||||||
function submit() {
|
function submit() {
|
||||||
const trimmed = prompt.trim();
|
const trimmed = prompt.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
|
trackPluginLoopClick(analytics.track, { page_name: 'plugins', area: 'plugin_loop', element: 'submit', plugin_id: active?.record.id });
|
||||||
onSubmit({
|
onSubmit({
|
||||||
prompt: trimmed,
|
prompt: trimmed,
|
||||||
pluginId: active?.record.id ?? null,
|
pluginId: active?.record.id ?? null,
|
||||||
|
|
@ -168,7 +172,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="plugin-loop-home__active-clear"
|
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"
|
aria-label="Clear active plugin"
|
||||||
title="Clear active plugin"
|
title="Clear active plugin"
|
||||||
>
|
>
|
||||||
|
|
@ -293,7 +297,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="plugin-loop-home__card-details"
|
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}`}
|
aria-label={`View details for ${p.title}`}
|
||||||
data-testid={`view-details-${p.id}`}
|
data-testid={`view-details-${p.id}`}
|
||||||
title="View plugin details"
|
title="View plugin details"
|
||||||
|
|
@ -304,7 +308,7 @@ export function PluginLoopHome({ onSubmit }: Props) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="plugin-loop-home__card-action"
|
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}
|
disabled={isPending || pendingApplyId !== null}
|
||||||
aria-busy={isPending ? 'true' : undefined}
|
aria-busy={isPending ? 'true' : undefined}
|
||||||
data-testid={`use-example-${p.id}`}
|
data-testid={`use-example-${p.id}`}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import { Icon } from './Icon';
|
||||||
import { navigate } from '../router';
|
import { navigate } from '../router';
|
||||||
import { useT } from '../i18n';
|
import { useT } from '../i18n';
|
||||||
import type { Dict } from '../i18n/types';
|
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,
|
// 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.
|
// 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) {
|
export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
const t = useT();
|
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 [routines, setRoutines] = useState<Routine[]>([]);
|
||||||
const [projects, setProjects] = useState<ProjectSummary[]>([]);
|
const [projects, setProjects] = useState<ProjectSummary[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -515,6 +521,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
|
|
||||||
const submit = async (e: FormEvent) => {
|
const submit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
fireAutomation(editingId ? 'save' : 'create');
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
|
@ -625,6 +632,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
fireAutomation('new_automation');
|
||||||
setForm(emptyForm());
|
setForm(emptyForm());
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
}}
|
}}
|
||||||
|
|
@ -715,6 +723,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
type="button"
|
type="button"
|
||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
fireAutomation('cancel');
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setForm(emptyForm());
|
setForm(emptyForm());
|
||||||
|
|
@ -789,7 +798,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => runNow(r.id)}
|
onClick={() => { fireAutomation('run_now'); runNow(r.id); }}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
>
|
>
|
||||||
{t('routines.runNow')}
|
{t('routines.runNow')}
|
||||||
|
|
@ -798,6 +807,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
type="button"
|
type="button"
|
||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
fireAutomation('edit');
|
||||||
setForm(formFromRoutine(r));
|
setForm(formFromRoutine(r));
|
||||||
setEditingId(r.id);
|
setEditingId(r.id);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
|
|
@ -809,7 +819,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => toggleEnabled(r)}
|
onClick={() => { fireAutomation(r.enabled ? 'pause' : 'resume'); toggleEnabled(r); }}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
>
|
>
|
||||||
{r.enabled ? t('routines.pause') : t('routines.resume')}
|
{r.enabled ? t('routines.pause') : t('routines.resume')}
|
||||||
|
|
@ -817,7 +827,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost"
|
className="btn btn-ghost"
|
||||||
onClick={() => setExpandedId(isExpanded ? null : r.id)}
|
onClick={() => { fireAutomation('history'); setExpandedId(isExpanded ? null : r.id); }}
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
>
|
>
|
||||||
{isExpanded ? t('routines.hideHistory') : t('routines.history')}
|
{isExpanded ? t('routines.hideHistory') : t('routines.history')}
|
||||||
|
|
@ -825,7 +835,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost btn-danger"
|
className="btn btn-ghost btn-danger"
|
||||||
onClick={() => remove(r.id)}
|
onClick={() => { fireAutomation('delete'); remove(r.id); }}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
title={t('routines.deleteTitle')}
|
title={t('routines.deleteTitle')}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1016,7 +1016,15 @@ export interface AutomationsClickProps {
|
||||||
| 'run_now'
|
| 'run_now'
|
||||||
| 'open_artifact'
|
| 'open_artifact'
|
||||||
| 'type_card'
|
| 'type_card'
|
||||||
| 'filter_tab';
|
| 'filter_tab'
|
||||||
|
| 'edit'
|
||||||
|
| 'pause'
|
||||||
|
| 'resume'
|
||||||
|
| 'delete'
|
||||||
|
| 'history'
|
||||||
|
| 'cancel'
|
||||||
|
| 'create'
|
||||||
|
| 'save';
|
||||||
type_id?: 'orbit' | 'routines' | 'schedules' | 'live_artifacts';
|
type_id?: 'orbit' | 'routines' | 'schedules' | 'live_artifacts';
|
||||||
filter_id?: 'all' | 'scheduled' | 'running' | 'done';
|
filter_id?: 'all' | 'scheduled' | 'running' | 'done';
|
||||||
}
|
}
|
||||||
|
|
@ -1078,6 +1086,20 @@ export interface PluginsSourcesTabClickProps {
|
||||||
plugin_type?: string;
|
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
|
// DESIGN SYSTEMS
|
||||||
export interface DesignSystemsTopClickProps {
|
export interface DesignSystemsTopClickProps {
|
||||||
page_name: 'design_systems';
|
page_name: 'design_systems';
|
||||||
|
|
@ -1490,6 +1512,8 @@ export type UiClickProps =
|
||||||
| PluginsTemplatesDropdownClickProps
|
| PluginsTemplatesDropdownClickProps
|
||||||
| PluginsAvailableTabClickProps
|
| PluginsAvailableTabClickProps
|
||||||
| PluginsSourcesTabClickProps
|
| PluginsSourcesTabClickProps
|
||||||
|
| PluginDetailClickProps
|
||||||
|
| PluginLoopClickProps
|
||||||
| DesignSystemsTopClickProps
|
| DesignSystemsTopClickProps
|
||||||
| DesignSystemsTemplateCardClickProps
|
| DesignSystemsTemplateCardClickProps
|
||||||
| DesignSystemsTemplatesModalClickProps
|
| DesignSystemsTemplatesModalClickProps
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue