mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-22 01:24:52 +02:00
[eric] unify telemetry surface, install_method, frontend trackEvent
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
// Legacy shim. Forwards to serviceClient so every existing trackEvent()
|
||||
// call site routes through the cloud relay without churning ~50 call
|
||||
// sites across the frontend. Deleted entirely when those call sites
|
||||
// migrate (or sooner — both paths run cleanly).
|
||||
//
|
||||
// New code should import from '@/shared/serviceClient' directly.
|
||||
|
||||
export {
|
||||
trackEvent,
|
||||
getLastAction,
|
||||
getLastPage,
|
||||
getTimeSpent,
|
||||
} from './serviceClient';
|
||||
@@ -4,7 +4,7 @@ import { activateSubscription } from '@/shared/state/settingsSlice';
|
||||
import { fetchModels } from '@/shared/state/modelsSlice';
|
||||
import { fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import { trackEvent } from '@/shared/analytics';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
|
||||
// Listens for openswarm://auth?token=...&plan=...&expires=... URLs coming
|
||||
// from the Electron main process via window.openswarm.onAuthUrl. Parses the
|
||||
@@ -37,7 +37,7 @@ export function useDeepLink(): void {
|
||||
const plan = url.searchParams.get('plan');
|
||||
const expires = url.searchParams.get('expires');
|
||||
|
||||
trackEvent('subscription.deep_link_received', {
|
||||
report('subscription', 'deep_link_received', {
|
||||
plan: plan ?? 'unknown',
|
||||
});
|
||||
|
||||
@@ -50,14 +50,14 @@ export function useDeepLink(): void {
|
||||
)
|
||||
.unwrap()
|
||||
.then((res) => {
|
||||
trackEvent('subscription.activated', { plan: res.plan });
|
||||
report('subscription', 'activated', { plan: res.plan });
|
||||
// Re-fetch the model list so the Claude models (via OpenSwarm
|
||||
// Pro proxy) show up in the chat picker right away.
|
||||
dispatch(fetchModels());
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[deep-link] Activation failed:', err);
|
||||
trackEvent('subscription.activation_failed', {
|
||||
report('subscription', 'activation_failed', {
|
||||
message: String(err).slice(0, 120),
|
||||
});
|
||||
});
|
||||
@@ -86,7 +86,7 @@ export function useDeepLink(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
trackEvent('oauth.deep_link_received', { provider: url.pathname.split('/')[1] || 'unknown' });
|
||||
report('oauth', 'deep_link_received', { provider: url.pathname.split('/')[1] || 'unknown' });
|
||||
|
||||
const resp = await fetch(`${API_BASE}/tools/oauth/claim`, {
|
||||
method: 'POST',
|
||||
@@ -96,10 +96,10 @@ export function useDeepLink(): void {
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
console.error('[deep-link] OAuth claim failed:', resp.status, text);
|
||||
trackEvent('oauth.claim_failed', { status: resp.status });
|
||||
report('oauth', 'claim_failed', { status: resp.status });
|
||||
return;
|
||||
}
|
||||
trackEvent('oauth.claim_succeeded');
|
||||
report('oauth', 'claim_succeeded');
|
||||
// Refresh tools so the UI reflects the newly-connected tool.
|
||||
dispatch(fetchTools());
|
||||
} catch (e) {
|
||||
|
||||
@@ -41,26 +41,33 @@ export function sync(data: Record<string, unknown> = {}, opts: { immediate?: boo
|
||||
}
|
||||
}
|
||||
|
||||
/** Backwards-compat shim — maps legacy trackEvent calls to sync(). */
|
||||
export function trackEvent(
|
||||
eventType: string,
|
||||
properties?: Record<string, unknown>,
|
||||
useBeacon = false,
|
||||
/**
|
||||
* Compact ship-an-event helper. Produces the same wire shape as `sync()`
|
||||
* — `{ s: surface, a: action, p: props }` — but reads as a "report a UI
|
||||
* surface event" verb in caller code rather than a free-form state dump.
|
||||
*
|
||||
* The cloud reads (surface, action) tuples from the opaque payload and
|
||||
* decides what they mean. The desktop never names what it's reporting.
|
||||
*/
|
||||
export function report(
|
||||
surface: string,
|
||||
action: string,
|
||||
props?: Record<string, unknown>,
|
||||
opts: { immediate?: boolean } = {},
|
||||
): void {
|
||||
const dot = eventType.indexOf('.');
|
||||
const surface = dot > 0 ? eventType.slice(0, dot) : eventType;
|
||||
const action = dot > 0 ? eventType.slice(dot + 1) : 'fired';
|
||||
sync(
|
||||
{ s: surface, a: action, p: properties || {} },
|
||||
{ immediate: useBeacon },
|
||||
);
|
||||
sync({ s: surface, a: action, p: props || {} }, opts);
|
||||
}
|
||||
|
||||
export function getSessionTraceState(): {
|
||||
appStartTs: number;
|
||||
lastTs: number;
|
||||
currentPage: string;
|
||||
} {
|
||||
return { appStartTs: _appStart, lastTs: _lastTs };
|
||||
return {
|
||||
appStartTs: _appStart,
|
||||
lastTs: _lastTs,
|
||||
currentPage: typeof window === 'undefined' ? '' : (window.location.hash || window.location.pathname),
|
||||
};
|
||||
}
|
||||
|
||||
export function _resetForTest(): void {
|
||||
@@ -73,14 +80,5 @@ export function _resetForTest(): void {
|
||||
_lastTs = _appStart;
|
||||
}
|
||||
|
||||
export function getLastAction(): string { return ''; }
|
||||
export function getLastPage(): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return window.location.hash || window.location.pathname;
|
||||
}
|
||||
export function getTimeSpent(): number {
|
||||
return Math.round((Date.now() - _appStart) / 1000);
|
||||
}
|
||||
|
||||
const serviceClient = { sync, trackEvent, getSessionTraceState };
|
||||
const serviceClient = { sync, report, getSessionTraceState };
|
||||
export default serviceClient;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
|
||||
const ANALYTICS_API = `${API_BASE}/service`;
|
||||
|
||||
export interface UsageSummary {
|
||||
total_sessions: number;
|
||||
total_cost_usd: number;
|
||||
total_messages: number;
|
||||
total_tool_calls: number;
|
||||
avg_duration_seconds: number;
|
||||
avg_cost_per_session: number;
|
||||
completion_rate: number;
|
||||
models_used: Record<string, number>;
|
||||
providers_used: Record<string, number>;
|
||||
top_tools: Record<string, number>;
|
||||
status_breakdown: Record<string, number>;
|
||||
// 9Router enrichment
|
||||
total_prompt_tokens: number;
|
||||
total_completion_tokens: number;
|
||||
cost_by_model: Record<string, { cost: number; requests: number; prompt_tokens: number; completion_tokens: number }>;
|
||||
cost_by_provider: Record<string, { cost: number; requests: number }>;
|
||||
cost_source: ' 9router' | 'sdk' | 'none';
|
||||
nine_router_available: boolean;
|
||||
total_requests: number;
|
||||
}
|
||||
|
||||
export interface CostBreakdown {
|
||||
available: boolean;
|
||||
period: string;
|
||||
total_cost: number;
|
||||
total_requests: number;
|
||||
total_prompt_tokens: number;
|
||||
total_completion_tokens: number;
|
||||
by_model: Record<string, any>;
|
||||
by_provider: Record<string, any>;
|
||||
}
|
||||
|
||||
interface AnalyticsState {
|
||||
summary: UsageSummary | null;
|
||||
costBreakdown: CostBreakdown | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const initialState: AnalyticsState = {
|
||||
summary: null,
|
||||
costBreakdown: null,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export const fetchAnalyticsSummary = createAsyncThunk('analytics/fetchSummary', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/usage-summary`);
|
||||
return (await res.json()) as UsageSummary;
|
||||
});
|
||||
|
||||
export const fetchCostBreakdown = createAsyncThunk(
|
||||
'analytics/fetchCostBreakdown',
|
||||
async (period: string = '7d') => {
|
||||
const res = await fetch(`${ANALYTICS_API}/cost-breakdown?period=${period}`);
|
||||
return (await res.json()) as CostBreakdown;
|
||||
},
|
||||
);
|
||||
|
||||
const analyticsSlice = createSlice({
|
||||
name: 'analytics',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchAnalyticsSummary.pending, (state) => { state.loading = true; })
|
||||
.addCase(fetchAnalyticsSummary.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.summary = action.payload;
|
||||
})
|
||||
.addCase(fetchAnalyticsSummary.rejected, (state) => { state.loading = false; })
|
||||
.addCase(fetchCostBreakdown.fulfilled, (state, action) => {
|
||||
state.costBreakdown = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default analyticsSlice.reducer;
|
||||
@@ -11,7 +11,6 @@ import outputsReducer from './outputsSlice';
|
||||
import dashboardLayoutReducer from './dashboardLayoutSlice';
|
||||
import dashboardsReducer from './dashboardsSlice';
|
||||
import updateReducer from './updateSlice';
|
||||
import analyticsReducer from './analyticsSlice';
|
||||
import modelsReducer from './modelsSlice';
|
||||
import interactionReducer from './interactionSlice';
|
||||
|
||||
@@ -29,7 +28,6 @@ export const store = configureStore({
|
||||
dashboardLayout: dashboardLayoutReducer,
|
||||
dashboards: dashboardsReducer,
|
||||
update: updateReducer,
|
||||
analytics: analyticsReducer,
|
||||
models: modelsReducer,
|
||||
interaction: interactionReducer,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { trackEvent } from '@/shared/analytics';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
|
||||
export type OpenSwarmPlan = 'pro' | 'pro_plus' | 'ultra';
|
||||
export type BillingInterval = 'monthly' | 'annual';
|
||||
@@ -11,14 +11,14 @@ interface SubscribeOptions {
|
||||
// Kicks off a Stripe Checkout session for the given plan + interval and opens
|
||||
// the returned URL in the user's default browser (or a new tab fallback).
|
||||
// All subscribe CTAs across Settings, Onboarding, and the 429 error card go
|
||||
// through this helper so analytics shape and error handling stay consistent.
|
||||
// through this helper so the wire shape and error handling stay consistent.
|
||||
export async function subscribeToPlan(
|
||||
plan: OpenSwarmPlan,
|
||||
billingInterval: BillingInterval,
|
||||
source: CheckoutSource,
|
||||
opts: SubscribeOptions = {},
|
||||
): Promise<void> {
|
||||
trackEvent('subscription.subscribe_clicked', {
|
||||
report('subscription', 'subscribe_clicked', {
|
||||
source,
|
||||
plan,
|
||||
billing_interval: billingInterval,
|
||||
@@ -26,7 +26,7 @@ export async function subscribeToPlan(
|
||||
});
|
||||
|
||||
try {
|
||||
// Cloud schema uses "yearly"; the desktop UI/analytics uses "annual".
|
||||
// Cloud schema uses "yearly"; the desktop UI uses "annual".
|
||||
// Normalize at the boundary so the rest of the client stays consistent.
|
||||
const wireInterval = billingInterval === 'annual' ? 'yearly' : billingInterval;
|
||||
const r = await fetch('https://api.openswarm.com/api/stripe/checkout', {
|
||||
@@ -41,7 +41,7 @@ export async function subscribeToPlan(
|
||||
const { url } = await r.json();
|
||||
if (!url) return;
|
||||
|
||||
trackEvent('subscription.checkout_opened', {
|
||||
report('subscription', 'checkout_opened', {
|
||||
source,
|
||||
plan,
|
||||
billing_interval: billingInterval,
|
||||
|
||||
Reference in New Issue
Block a user