feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
47
dexto/packages/webui/lib/analytics/capture.ts
Normal file
47
dexto/packages/webui/lib/analytics/capture.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Non-React capture helpers for use in event handlers.
|
||||
// These functions work outside of React context (e.g., in handlers.ts).
|
||||
|
||||
import posthog from 'posthog-js';
|
||||
import type { LLMTokensConsumedEvent } from '@dexto/analytics';
|
||||
|
||||
/**
|
||||
* Check if analytics is enabled by looking for the injected config.
|
||||
*/
|
||||
function isAnalyticsEnabled(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return !!(window as any).__DEXTO_ANALYTICS__;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base context properties included with every event.
|
||||
*/
|
||||
function getBaseContext() {
|
||||
if (typeof window === 'undefined') {
|
||||
return { app: 'dexto-webui' };
|
||||
}
|
||||
const config = (window as any).__DEXTO_ANALYTICS__;
|
||||
return {
|
||||
app: 'dexto-webui',
|
||||
app_version: config?.appVersion ?? 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture LLM token usage event.
|
||||
* Called from event handlers (non-React context).
|
||||
*
|
||||
* @param params - Token usage data (source is automatically set to 'webui')
|
||||
*/
|
||||
export function captureTokenUsage(params: Omit<LLMTokensConsumedEvent, 'source'>): void {
|
||||
if (!isAnalyticsEnabled()) return;
|
||||
|
||||
try {
|
||||
posthog.capture('dexto_llm_tokens_consumed', {
|
||||
...getBaseContext(),
|
||||
source: 'webui',
|
||||
...params,
|
||||
});
|
||||
} catch {
|
||||
// Analytics should never break the app
|
||||
}
|
||||
}
|
||||
38
dexto/packages/webui/lib/analytics/events.ts
Normal file
38
dexto/packages/webui/lib/analytics/events.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// WebUI analytics event definitions for PostHog.
|
||||
// These events track user activation, retention, and feature usage.
|
||||
//
|
||||
// All events use unified names (dexto_*) with a `source` property to
|
||||
// distinguish CLI vs WebUI. This enables simpler PostHog dashboards.
|
||||
|
||||
import type { SharedAnalyticsEventMap, FileAttachedEvent } from '@dexto/analytics';
|
||||
|
||||
/**
|
||||
* Base context automatically included with every WebUI event.
|
||||
* Populated by the AnalyticsProvider.
|
||||
*/
|
||||
export interface BaseEventContext {
|
||||
app?: 'dexto-webui';
|
||||
app_version?: string;
|
||||
browser?: string;
|
||||
browser_version?: string;
|
||||
os?: string;
|
||||
screen_width?: number;
|
||||
screen_height?: number;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebUI analytics event map extending shared events with WebUI-specific events.
|
||||
*
|
||||
* IMPORTANT: If an event is also tracked by CLI, move it to SharedAnalyticsEventMap
|
||||
* in @dexto/analytics to avoid duplication.
|
||||
*/
|
||||
export interface WebUIAnalyticsEventMap extends SharedAnalyticsEventMap {
|
||||
// WebUI-specific events (not supported by CLI)
|
||||
dexto_file_attached: FileAttachedEvent;
|
||||
}
|
||||
|
||||
export type WebUIAnalyticsEventName = keyof WebUIAnalyticsEventMap;
|
||||
|
||||
export type WebUIAnalyticsEventPayload<Name extends WebUIAnalyticsEventName> =
|
||||
WebUIAnalyticsEventMap[Name];
|
||||
210
dexto/packages/webui/lib/analytics/hook.ts
Normal file
210
dexto/packages/webui/lib/analytics/hook.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
// Convenience hook for tracking analytics events in WebUI components.
|
||||
//
|
||||
// Uses shared event names (dexto_*) with source: 'webui' for unified dashboards.
|
||||
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useAnalyticsContext } from './provider.js';
|
||||
import type {
|
||||
MessageSentEvent,
|
||||
SessionCreatedEvent,
|
||||
SessionResetEvent,
|
||||
ToolCalledEvent,
|
||||
ToolResultEvent,
|
||||
LLMSwitchedEvent,
|
||||
SessionSwitchedEvent,
|
||||
AgentSwitchedEvent,
|
||||
FileAttachedEvent,
|
||||
ImageAttachedEvent,
|
||||
MCPServerConnectedEvent,
|
||||
} from '@dexto/analytics';
|
||||
|
||||
export interface UseAnalyticsReturn {
|
||||
capture: ReturnType<typeof useAnalyticsContext>['capture'];
|
||||
enabled: boolean;
|
||||
isReady: boolean;
|
||||
|
||||
// Convenience tracking methods (source: 'webui' added automatically)
|
||||
trackMessageSent: (params: Omit<MessageSentEvent, 'messageCount' | 'source'>) => void;
|
||||
trackSessionCreated: (params: Omit<SessionCreatedEvent, 'source'>) => void;
|
||||
trackSessionSwitched: (params: Omit<SessionSwitchedEvent, 'source'>) => void;
|
||||
trackSessionReset: (params: Omit<SessionResetEvent, 'source'>) => void;
|
||||
trackAgentSwitched: (params: Omit<AgentSwitchedEvent, 'source'>) => void;
|
||||
trackToolCalled: (params: Omit<ToolCalledEvent, 'source'>) => void;
|
||||
trackToolResult: (params: Omit<ToolResultEvent, 'source'>) => void;
|
||||
trackLLMSwitched: (params: Omit<LLMSwitchedEvent, 'source'>) => void;
|
||||
trackFileAttached: (params: Omit<FileAttachedEvent, 'source'>) => void;
|
||||
trackImageAttached: (params: Omit<ImageAttachedEvent, 'source'>) => void;
|
||||
trackMCPServerConnected: (params: Omit<MCPServerConnectedEvent, 'source'>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for tracking analytics events with convenience methods.
|
||||
* Automatically handles message counting per session.
|
||||
*/
|
||||
export function useAnalytics(): UseAnalyticsReturn {
|
||||
const { capture, enabled, isReady } = useAnalyticsContext();
|
||||
|
||||
// Track message count per session
|
||||
const messageCountRef = useRef<Record<string, number>>({});
|
||||
|
||||
const trackMessageSent = useCallback(
|
||||
(params: Omit<MessageSentEvent, 'messageCount' | 'source'>) => {
|
||||
if (!enabled) return;
|
||||
|
||||
try {
|
||||
// Increment message count for this session
|
||||
const sessionId = params.sessionId;
|
||||
messageCountRef.current[sessionId] = (messageCountRef.current[sessionId] || 0) + 1;
|
||||
|
||||
capture('dexto_message_sent', {
|
||||
source: 'webui',
|
||||
...params,
|
||||
messageCount: messageCountRef.current[sessionId],
|
||||
});
|
||||
} catch (error) {
|
||||
// Analytics should never break the app - fail silently
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackSessionCreated = useCallback(
|
||||
(params: Omit<SessionCreatedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_session_created', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackSessionSwitched = useCallback(
|
||||
(params: Omit<SessionSwitchedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_session_switched', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackSessionReset = useCallback(
|
||||
(params: Omit<SessionResetEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
// Reset message count for this session
|
||||
messageCountRef.current[params.sessionId] = 0;
|
||||
capture('dexto_session_reset', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackAgentSwitched = useCallback(
|
||||
(params: Omit<AgentSwitchedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_agent_switched', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackToolCalled = useCallback(
|
||||
(params: Omit<ToolCalledEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_tool_called', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackToolResult = useCallback(
|
||||
(params: Omit<ToolResultEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_tool_result', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackLLMSwitched = useCallback(
|
||||
(params: Omit<LLMSwitchedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_llm_switched', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackFileAttached = useCallback(
|
||||
(params: Omit<FileAttachedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_file_attached', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackImageAttached = useCallback(
|
||||
(params: Omit<ImageAttachedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_image_attached', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
const trackMCPServerConnected = useCallback(
|
||||
(params: Omit<MCPServerConnectedEvent, 'source'>) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
capture('dexto_mcp_server_connected', { source: 'webui', ...params });
|
||||
} catch (error) {
|
||||
console.warn('Analytics tracking failed:', error);
|
||||
}
|
||||
},
|
||||
[capture, enabled]
|
||||
);
|
||||
|
||||
return {
|
||||
capture,
|
||||
enabled,
|
||||
isReady,
|
||||
trackMessageSent,
|
||||
trackSessionCreated,
|
||||
trackSessionSwitched,
|
||||
trackSessionReset,
|
||||
trackAgentSwitched,
|
||||
trackToolCalled,
|
||||
trackToolResult,
|
||||
trackLLMSwitched,
|
||||
trackFileAttached,
|
||||
trackImageAttached,
|
||||
trackMCPServerConnected,
|
||||
};
|
||||
}
|
||||
15
dexto/packages/webui/lib/analytics/index.ts
Normal file
15
dexto/packages/webui/lib/analytics/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Main exports for WebUI analytics library.
|
||||
//
|
||||
// All event types should be imported directly from @dexto/analytics.
|
||||
|
||||
export { AnalyticsProvider, useAnalyticsContext } from './provider.js';
|
||||
export { useAnalytics } from './hook.js';
|
||||
export { captureTokenUsage } from './capture.js';
|
||||
|
||||
// WebUI-specific type maps (for type-safe capture)
|
||||
export type {
|
||||
WebUIAnalyticsEventName,
|
||||
WebUIAnalyticsEventPayload,
|
||||
WebUIAnalyticsEventMap,
|
||||
BaseEventContext,
|
||||
} from './events.js';
|
||||
145
dexto/packages/webui/lib/analytics/provider.tsx
Normal file
145
dexto/packages/webui/lib/analytics/provider.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
// packages/webui/lib/analytics/provider.tsx
|
||||
// React Context Provider for WebUI analytics using PostHog JS SDK.
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||
import posthog from 'posthog-js';
|
||||
import type {
|
||||
WebUIAnalyticsEventName,
|
||||
WebUIAnalyticsEventPayload,
|
||||
BaseEventContext,
|
||||
} from './events.js';
|
||||
|
||||
interface AnalyticsConfig {
|
||||
distinctId: string;
|
||||
posthogKey: string;
|
||||
posthogHost: string;
|
||||
appVersion: string;
|
||||
}
|
||||
|
||||
interface AnalyticsContextType {
|
||||
capture: <Name extends WebUIAnalyticsEventName>(
|
||||
event: Name,
|
||||
properties?: WebUIAnalyticsEventPayload<Name>
|
||||
) => void;
|
||||
enabled: boolean;
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
const AnalyticsContext = createContext<AnalyticsContextType | undefined>(undefined);
|
||||
|
||||
interface AnalyticsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics config injected during app initialization.
|
||||
* Returns null if analytics disabled or config not available.
|
||||
*/
|
||||
function getAnalyticsConfig(): AnalyticsConfig | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return (window as any).__DEXTO_ANALYTICS__ ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base context properties included with every event.
|
||||
*/
|
||||
function getBaseContext(): BaseEventContext {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
app: 'dexto-webui',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
app: 'dexto-webui',
|
||||
app_version: getAnalyticsConfig()?.appVersion ?? 'unknown',
|
||||
browser: navigator.userAgent.split(' ').pop()?.split('/')[0],
|
||||
browser_version: navigator.userAgent.split(' ').pop()?.split('/')[1],
|
||||
os: navigator.platform,
|
||||
screen_width: window.screen.width,
|
||||
screen_height: window.screen.height,
|
||||
// session_id will be managed by PostHog automatically
|
||||
};
|
||||
}
|
||||
|
||||
export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const config = getAnalyticsConfig();
|
||||
|
||||
if (!config) {
|
||||
// Analytics disabled or config not available
|
||||
setEnabled(false);
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize PostHog
|
||||
posthog.init(config.posthogKey, {
|
||||
api_host: config.posthogHost,
|
||||
person_profiles: 'identified_only', // Only create profiles for identified users
|
||||
loaded: (posthogInstance) => {
|
||||
// Use the distinct ID from CLI (unified tracking)
|
||||
posthogInstance.identify(config.distinctId);
|
||||
setEnabled(true);
|
||||
setIsReady(true);
|
||||
},
|
||||
autocapture: false, // Disable automatic event capture
|
||||
capture_pageview: false, // We'll manually track page views for better control
|
||||
disable_session_recording: true, // Disable session replay (privacy)
|
||||
disable_surveys: true, // Disable surveys
|
||||
opt_out_capturing_by_default: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize analytics:', error);
|
||||
setEnabled(false);
|
||||
setIsReady(true);
|
||||
}
|
||||
|
||||
// Cleanup on unmount - always reset to clear identity
|
||||
return () => {
|
||||
try {
|
||||
posthog.reset(); // Always clear identity on unmount
|
||||
} catch {
|
||||
// Ignore errors if PostHog wasn't initialized
|
||||
}
|
||||
};
|
||||
}, []); // Run once on mount
|
||||
|
||||
const capture = <Name extends WebUIAnalyticsEventName>(
|
||||
event: Name,
|
||||
properties?: WebUIAnalyticsEventPayload<Name>
|
||||
) => {
|
||||
if (!enabled || !isReady) return;
|
||||
|
||||
try {
|
||||
posthog.capture(event, {
|
||||
...getBaseContext(),
|
||||
...properties,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to capture analytics event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnalyticsContext.Provider value={{ capture, enabled, isReady }}>
|
||||
{children}
|
||||
</AnalyticsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access analytics from any component.
|
||||
* Must be used within an AnalyticsProvider.
|
||||
*/
|
||||
export function useAnalyticsContext(): AnalyticsContextType {
|
||||
const context = useContext(AnalyticsContext);
|
||||
if (!context) {
|
||||
throw new Error('useAnalyticsContext must be used within an AnalyticsProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user