analytics and embed meta update!!!

This commit is contained in:
legop3
2026-07-31 22:12:04 -04:00
parent 1aecab66e7
commit 8c5d98bed6
45 changed files with 395 additions and 699 deletions
@@ -9,7 +9,6 @@ import OverseerControlPanel from './OverseerControlPanel.jsx';
import ReplaySnapshotHealth from './ReplaySnapshotHealth.jsx';
import AdminIpLogPanel from './AdminIpLogPanel.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
const MODES = [
{ key: 'open', label: 'Open' },
@@ -161,23 +160,17 @@ export default function AdminPanelContent() {
const handleModeChange = async (event) => {
const mode = event.target.value;
trackAnalyticsEvent('admin_mode_change', { mode, status: 'started' });
try {
await setMode(mode);
trackAnalyticsEvent('admin_mode_change', { mode, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('admin_mode_change', { mode, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
}
};
const handleForceControl = async (roverId) => {
trackAnalyticsEvent('admin_force_control', { roverId, status: 'started' });
try {
await requestControl(roverId, { force: true });
trackAnalyticsEvent('admin_force_control', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('admin_force_control', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
}
};
@@ -187,12 +180,9 @@ export default function AdminPanelContent() {
const ok = window.confirm(`Reboot rover "${rover.name || rover.id}" now?`);
if (!ok) return;
setRebootStates((prev) => ({ ...prev, [rover.id]: true }));
trackAnalyticsEvent('rover_reboot_click', { roverId: rover.id, scope: 'admin' });
try {
await rebootRover(rover.id);
trackAnalyticsEvent('rover_reboot_result', { roverId: rover.id, scope: 'admin', status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('rover_reboot_result', { roverId: rover.id, scope: 'admin', status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setRebootStates((prev) => ({ ...prev, [rover.id]: false }));
@@ -206,7 +196,6 @@ export default function AdminPanelContent() {
);
if (!ok) return;
setUpdateStates((prev) => ({ ...prev, [rover.id]: true }));
trackAnalyticsEvent('rover_update_click', { roverId: rover.id });
try {
// The rover acknowledges once the privileged self-update helper has been
// launched, not when the full install finishes. That is deliberate: a
@@ -214,9 +203,7 @@ export default function AdminPanelContent() {
// the same websocket would make the button look failed even when the
// update is doing exactly what it should.
await updateRover(rover.id);
trackAnalyticsEvent('rover_update_result', { roverId: rover.id, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('rover_update_result', { roverId: rover.id, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setUpdateStates((prev) => ({ ...prev, [rover.id]: false }));
@@ -232,22 +219,15 @@ export default function AdminPanelContent() {
);
if (!ok) return;
trackAnalyticsEvent('rover_update_all_click', { roverCount });
try {
// The server owns the fan-out because it has the authoritative online
// rover map and can enforce admin privileges once before issuing the
// existing per-rover update command to every connected rover.
const result = await updateAllRovers();
trackAnalyticsEvent('rover_update_all_result', {
status: 'accepted',
updated: result?.updated?.length || 0,
failed: result?.failed?.length || 0,
});
if (result?.failed?.length) {
alert(`Update requested for ${result.updated?.length || 0} rover(s). ${result.failed.length} rover(s) failed to queue.`);
}
} catch (err) {
trackAnalyticsEvent('rover_update_all_result', { status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
}
};
@@ -256,12 +236,9 @@ export default function AdminPanelContent() {
const ok = window.confirm('Reboot the server host now? This will disconnect all users.');
if (!ok) return;
setServerRebooting(true);
trackAnalyticsEvent('server_reboot_click', { status: 'started' });
try {
await rebootServer();
trackAnalyticsEvent('server_reboot_click', { status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('server_reboot_click', { status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
setServerRebooting(false);
}
@@ -1,10 +1,9 @@
// Barcode Games Panel
// Purpose: Shows barcode game selection, current game status, and player points in the Activities tab.
// Scope: Keeps the driver-side UI compact but informative; game-specific rules stay in server game modules.
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import useBarcodeGameState from '../../barcodeGames/useBarcodeGameState.js';
import CardFrame from '../CardFrame/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { isFeatureEnabled } from '../../lib/features.js';
@@ -261,10 +260,8 @@ export default function BarcodeGamesPanel() {
}
function BarcodeGamesPanelContent() {
const { state, connectionState, voteForGame } = useBarcodeGameState();
const { state, voteForGame } = useBarcodeGameState();
const [pendingGameId, setPendingGameId] = useState(null);
const activeSignatureRef = useRef('');
const participantCountRef = useRef(0);
const activeGame = state.activeGame;
const display = activeGame?.display || {};
// Voting should stop once the shared game system has moved past selection.
@@ -278,77 +275,10 @@ function BarcodeGamesPanelContent() {
const participants = Array.isArray(state.participants) ? state.participants : [];
const activeTheme = getGameTheme(activeGame?.themeColor);
useEffect(() => {
if (!connectionState.stale || !connectionState.lastReceivedAt) return;
/*
Stale barcode-game state is worth tracking because it points to a real
interaction problem: users can be looking at old game choices or scores
even though the rest of the page appears loaded.
*/
trackAnalyticsEvent('barcode_game_state_stale', {
connected: connectionState.connected,
phase: state.phase || 'unknown',
});
}, [connectionState.connected, connectionState.lastReceivedAt, connectionState.stale, state.phase]);
useEffect(() => {
const signature = `${activeGame?.id || 'none'}:${activeGame?.status || state.phase || 'unknown'}`;
if (activeSignatureRef.current === signature) return;
activeSignatureRef.current = signature;
/*
Game lifecycle events are derived from server state instead of button
clicks so they still report games started by another user or by a scanner
workflow outside this panel.
*/
trackAnalyticsEvent('barcode_game_active_change', {
gameId: activeGame?.id || '',
status: activeGame?.status || '',
phase: state.phase || 'unknown',
});
if (activeGame?.status === 'ending' || state.phase === 'ending') {
trackAnalyticsEvent('barcode_game_round_end', {
gameId: activeGame?.id || '',
participantCount: participants.length,
});
}
}, [activeGame?.id, activeGame?.status, participants.length, state.phase]);
useEffect(() => {
if (participants.length <= participantCountRef.current) {
participantCountRef.current = participants.length;
return;
}
participantCountRef.current = participants.length;
trackAnalyticsEvent('barcode_game_player_join', {
gameId: activeGame?.id || '',
participantCount: participants.length,
});
}, [activeGame?.id, participants.length]);
const handleVote = async (gameId) => {
setPendingGameId(gameId);
trackAnalyticsEvent('barcode_game_vote', {
gameId,
phase: state.phase || 'unknown',
status: 'started',
});
try {
await voteForGame(gameId);
trackAnalyticsEvent('barcode_game_vote', {
gameId,
phase: state.phase || 'unknown',
status: 'accepted',
});
} catch (error) {
trackAnalyticsEvent('barcode_game_vote', {
gameId,
phase: state.phase || 'unknown',
status: 'failed',
reason: error?.message || 'unknown',
});
throw error;
} finally {
setPendingGameId(null);
}
@@ -8,7 +8,6 @@ import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetry
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { deriveDriveDockStateFromTelemetry } from './driveDockState.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
function StatusRow({ value, tone = 'neutral' }) {
@@ -129,14 +128,11 @@ export default function DriveDockAction({
if (!roverId || pending) return;
if (isMobile) triggerTouchHaptic('button');
setPending('drive');
trackAnalyticsEvent('drive_return', { roverId, source: 'drive_dock_action' });
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
trackAnalyticsEvent('drive_return_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('drive_return_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setPending(null);
@@ -149,14 +145,11 @@ export default function DriveDockAction({
setConfirmOpen(false);
setShowModal(false);
setPending('drive');
trackAnalyticsEvent('drive_start', { roverId, source: 'drive_dock_action' });
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
trackAnalyticsEvent('drive_start_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('drive_start_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setPending(null);
@@ -167,12 +160,9 @@ export default function DriveDockAction({
if (!roverId || pending) return;
if (isMobile) triggerTouchHaptic('button');
setPending('dock');
trackAnalyticsEvent('dock_assist_start', { roverId });
try {
dockAssist.enterAssist();
trackAnalyticsEvent('dock_assist_start_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('dock_assist_start_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setPending(null);
@@ -186,12 +176,10 @@ export default function DriveDockAction({
if (isMobile) triggerTouchHaptic('button');
if (manualAssistActive) {
dockAssist.exitAssist();
trackAnalyticsEvent('dock_assist_exit', { roverId });
return;
}
setShowModal(true);
setConfirmOpen(true);
trackAnalyticsEvent('dock_assist_open', { roverId });
};
// The drive/dock card appears inside the mobile controls and can be held or
@@ -9,7 +9,6 @@ import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
import { AUX_ZERO } from './constants.js';
import VacuumControls from './VacuumControls.jsx';
import VerticalCameraTilt from './VerticalCameraTilt.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
import { useSessionSelector } from '../../context/SessionContext.jsx';
const CAMERA_TILT_STEP_DEGREES = 0.5;
@@ -56,25 +55,22 @@ function AuxColumnContent() {
const handleHeadlightToggle = useCallback(
(nextOn) => {
if (!headlightAvailable) return;
trackAnalyticsEvent('headlight_toggle', { roverId, source: 'mobile_control', enabled: Boolean(nextOn) });
setHeadlight(nextOn);
},
[headlightAvailable, roverId, setHeadlight],
[headlightAvailable, setHeadlight],
);
const handleLaserToggle = useCallback(
(nextOn) => {
if (!laserAvailable) return;
trackAnalyticsEvent('laser_toggle', { roverId, source: 'mobile_control', enabled: Boolean(nextOn) });
setLaser(nextOn);
},
[laserAvailable, roverId, setLaser],
[laserAvailable, setLaser],
);
const handleHornStart = useCallback(() => {
trackAnalyticsEvent('horn_start', { roverId, source: 'mobile_control' });
return startHorn();
}, [roverId, startHorn]);
}, [startHorn]);
const handleAuxPress = useCallback(
(id, values) => {
+1 -9
View File
@@ -22,7 +22,6 @@ import { useSessionActions, useSessionSelector } from '../../context/SessionCont
import { usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js';
import { useSharedClock } from '../../hooks/useSharedClock.js';
import { isFeatureEnabled } from '../../lib/features.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
import { useSettingsNamespace } from '../../settings/index.js';
import { DEFAULT_PAGE_THEME_KEY, getPageThemeClass } from '../../themes/index.js';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
@@ -861,7 +860,7 @@ export function PtzControllerPage({ layout = 'desktop' }) {
);
}
export default function PtzQueueCard({ layout = 'desktop' }) {
export default function PtzQueueCard() {
const featureEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera'));
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
@@ -886,7 +885,6 @@ export default function PtzQueueCard({ layout = 'desktop' }) {
return;
}
setPending(true);
trackAnalyticsEvent('ptz_queue_join', { layout });
try {
const response = await ptzClaim();
/*
@@ -897,13 +895,7 @@ export default function PtzQueueCard({ layout = 'desktop' }) {
if (response?.state?.isOperator || response?.state?.queuedPosition) {
navigate('/ptz');
}
trackAnalyticsEvent('ptz_queue_join_result', { layout, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('ptz_queue_join_result', {
layout,
status: 'failed',
reason: err?.message || 'unknown',
});
alert(err.message || 'PTZ request failed.');
} finally {
setPending(false);
+9 -15
View File
@@ -36,7 +36,6 @@ import CardFrame from '../CardFrame/index.jsx';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { themeGapClass, themeStackClass } from '../../themes/index.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
const CHAT_DOCK_INITIAL_HEIGHT = 224;
const CHAT_DOCK_MIN_HEIGHT = 144;
@@ -103,23 +102,20 @@ function DriveDockPanel() {
const cameraTiltStep = camera?.precisionMode
? CAMERA_TILT_PRECISION_STEP_DEGREES
: CAMERA_TILT_STEP_DEGREES;
const trackedControls = useMemo(
const auxControls = useMemo(
() => ({
setHeadlight: (nextOn) => {
trackAnalyticsEvent('headlight_toggle', { roverId, source: 'desktop_control', enabled: Boolean(nextOn) });
setHeadlight(nextOn);
},
setLaser: (nextOn) => {
trackAnalyticsEvent('laser_toggle', { roverId, source: 'desktop_control', enabled: Boolean(nextOn) });
setLaser(nextOn);
},
startHorn: () => {
trackAnalyticsEvent('horn_start', { roverId, source: 'desktop_control' });
return startHorn();
},
stopHorn,
}),
[roverId, setHeadlight, setLaser, startHorn, stopHorn],
[setHeadlight, setLaser, startHorn, stopHorn],
);
return (
@@ -139,7 +135,7 @@ function DriveDockPanel() {
label="Headlight"
on={headlightState?.headlightOn}
disabled={!roverId}
onToggle={trackedControls.setHeadlight}
onToggle={auxControls.setHeadlight}
keyLabel={headlightLabel}
/>
)}
@@ -148,7 +144,7 @@ function DriveDockPanel() {
label="Laser"
on={laserState?.laserOn}
disabled={!roverId || roomLightsLockedOn}
onToggle={trackedControls.setLaser}
onToggle={auxControls.setLaser}
keyLabel={laserLabel}
/>
)}
@@ -157,8 +153,8 @@ function DriveDockPanel() {
{hornAvailable && (
<HornControl
disabled={!roverId || hornBlocked}
onStart={trackedControls.startHorn}
onStop={trackedControls.stopHorn}
onStart={auxControls.startHorn}
onStop={auxControls.stopHorn}
keyLabel={hornLabel}
active={horn?.active}
heat={horn?.heat}
@@ -259,14 +255,12 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const handleTabChange = useCallback(
(tab) => {
/*
Desktop users spend most of their time on this one route, so panel
changes are the cleanest way to understand feature usage without adding
analytics calls to every nested control in the rover dashboard.
Keep the selected desktop panel controlled here so the tab strip and
panel content always move together.
*/
setActiveTab(tab);
trackAnalyticsEvent('tab_change', { tab, layout, surface: 'desktop_right_pane' });
},
[layout],
[],
);
useLayoutEffect(() => {
@@ -10,7 +10,6 @@ import { useVideoRequests } from '../../hooks/useVideoRequests.js';
import { useRoverSnapshots } from '../../hooks/useRoverSnapshots.js';
import { useSettingsNamespace } from '../../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { trackAnalyticsEventThrottled } from '../../analytics/index.js';
import {
RESTART_DELAY_MS,
UNMUTE_RETRY_MS,
@@ -439,20 +438,6 @@ export default function RoverMediaPlayer({
ensurePlayback();
}
if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) {
/*
WHEP status changes can repeat during automatic restart attempts. The
throttled event keeps media reliability visible in analytics without
reporting every retry loop as a separate user action.
*/
trackAnalyticsEventThrottled(
'whep_player_error',
{
roverId: effectiveRoverId,
status: nextStatus,
detail: info || '',
},
{ key: `${effectiveRoverId || 'unknown'}:${nextStatus}:${info || ''}`, throttleMs: 60 * 1000 },
);
scheduleRestart();
}
};
@@ -469,15 +454,6 @@ export default function RoverMediaPlayer({
if (!active) return;
setStatus('error');
setDetail(err.message);
trackAnalyticsEventThrottled(
'whep_player_error',
{
roverId: effectiveRoverId,
status: 'start_failed',
detail: err.message,
},
{ key: `${effectiveRoverId || 'unknown'}:start_failed:${err.message}`, throttleMs: 60 * 1000 },
);
scheduleRestart();
});
@@ -168,22 +168,13 @@ export default function RoverQueuesPanel({
return;
}
setPending((prev) => ({ ...prev, [targetRoverId]: true }));
trackAnalyticsEvent('rover_queue_join', {
trackAnalyticsEvent('rover_selected', {
roverId: targetRoverId,
alreadyAssigned: assignedRoverId === String(targetRoverId),
alreadySelected: assignedRoverId === String(targetRoverId),
});
try {
await requestControl(targetRoverId);
trackAnalyticsEvent('rover_queue_join_result', {
roverId: targetRoverId,
status: 'accepted',
});
} catch (err) {
trackAnalyticsEvent('rover_queue_join_result', {
roverId: targetRoverId,
status: 'failed',
reason: err?.message || 'unknown',
});
alert(err.message);
} finally {
setPending((prev) => ({ ...prev, [targetRoverId]: false }));
@@ -198,18 +189,10 @@ export default function RoverQueuesPanel({
const ok = window.confirm(`Reboot your rover "${assignedRoverName}" now?`);
if (!ok) return;
setRebootPending(true);
trackAnalyticsEvent('rover_reboot_click', { roverId: assignedRoverId, scope: 'own_rover' });
try {
await rebootOwnRover();
trackAnalyticsEvent('rover_reboot_result', { roverId: assignedRoverId, scope: 'own_rover', status: 'accepted' });
alert('Reboot command sent.');
} catch (err) {
trackAnalyticsEvent('rover_reboot_result', {
roverId: assignedRoverId,
scope: 'own_rover',
status: 'failed',
reason: err?.message || 'unknown',
});
alert(err.message);
} finally {
setRebootPending(false);
@@ -1,38 +0,0 @@
// Session Document Title
// Purpose: Uses the configured local inter-instance profile name as the browser tab title.
// Scope: Owns only the document title lifecycle; the static HTML title remains the fallback.
import { useEffect } from 'react';
import { useSessionSelector } from '../../context/SessionContext.jsx';
const DEFAULT_DOCUMENT_TITLE = document.title;
export default function SessionDocumentTitle() {
const interInstanceEnabled = useSessionSelector((state) => Boolean(state.session?.interInstances?.enabled));
const interInstanceName = useSessionSelector((state) =>
String(state.session?.interInstances?.profile?.name || '').trim(),
);
useEffect(() => {
/*
The static title from index.html remains the source of truth until the
server confirms that inter-instance sharing is enabled and supplies a
usable profile name. This prevents the profile's loading state from
replacing the familiar fallback title with an empty or temporary value.
*/
if (!interInstanceEnabled || !interInstanceName) return undefined;
document.title = interInstanceName;
/*
Restore the static title when the synchronized profile disappears or the
feature is disabled. React also runs this cleanup during development's
StrictMode effect check, so the component never leaves a stale server name
behind when its session-derived conditions stop being true.
*/
return () => {
document.title = DEFAULT_DOCUMENT_TITLE;
};
}, [interInstanceEnabled, interInstanceName]);
return null;
}
@@ -19,7 +19,6 @@ import { useSettingsNamespace } from '../../settings/index.js';
import { useSocket } from '../../context/SocketContext.jsx';
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../analytics/index.js';
import {
DEFAULT_PAGE_THEME_KEY,
PAGE_THEME_OPTIONS,
@@ -196,7 +195,6 @@ export default function SettingsPanel() {
const handleTransportChange = (event) => {
const next = event.target.value;
savePageSettings((current) => ({ ...(current ?? {}), connectionTransport: next }));
trackAnalyticsEvent('settings_change', { setting: 'connection_transport', value: next });
reconnectSocketWithTransport(socket, next);
};
@@ -204,30 +202,21 @@ export default function SettingsPanel() {
const raw = Number(event.target.value);
const next = Number.isFinite(raw) ? Math.max(0, Math.min(1, raw)) : 0;
saveAudioSettings((current) => ({ ...(current ?? {}), [key]: next }));
trackAnalyticsEventThrottled(
'settings_change',
{ setting: key, value: next },
{ key: `audio:${key}`, throttleMs: 3 * 1000 },
);
};
const handleMainBrushDuckEnabled = (event) => {
const checked = Boolean(event.target.checked);
saveAudioSettings((current) => ({ ...(current ?? {}), mainBrushDuckEnabled: checked }));
trackAnalyticsEvent('settings_change', { setting: 'mainBrushDuckEnabled', value: checked });
};
const handleSwapMobileControlColumns = (event) => {
const checked = Boolean(event.target.checked);
savePageSettings((current) => ({ ...(current ?? {}), swapMobileControlColumns: checked }));
trackAnalyticsEvent('mobile_controls_swap', { enabled: checked });
trackAnalyticsEvent('settings_change', { setting: 'swapMobileControlColumns', value: checked });
};
const handleDriveMacroBackoffEnabled = (event) => {
const checked = Boolean(event.target.checked);
savePageSettings((current) => ({ ...(current ?? {}), driveMacroBackoffEnabled: checked }));
trackAnalyticsEvent('settings_change', { setting: 'driveMacroBackoffEnabled', value: checked });
};
const handleInterInstanceTransferSettings = (event) => {
@@ -238,7 +227,6 @@ export default function SettingsPanel() {
letting users opt out of sending their current settings cookie.
*/
savePageSettings((current) => ({ ...(current ?? {}), interInstanceTransferSettings: checked }));
trackAnalyticsEvent('settings_change', { setting: 'interInstanceTransferSettings', value: checked });
};
const movePageThemePreview = (direction) => {
@@ -261,10 +249,6 @@ export default function SettingsPanel() {
...(current ?? {}),
backgroundTheme: previewPageThemeKey,
}));
trackAnalyticsEvent('settings_change', {
setting: 'backgroundTheme',
value: previewPageThemeKey,
});
};
const handleVideoFilterChange = (event) => {
@@ -276,7 +260,6 @@ export default function SettingsPanel() {
...(current ?? {}),
colorFilter: nextFilter,
}));
trackAnalyticsEvent('settings_change', { setting: 'videoColorFilter', value: nextFilter });
};
return (
+2 -5
View File
@@ -40,14 +40,11 @@ export default function SocialButton({ id = null, label, url, icon, color, layou
rel="noopener noreferrer"
aria-label={text}
onClick={() => {
trackAnalyticsEvent('social_link_click', {
id: id || '',
trackAnalyticsEvent('social_link_clicked', {
destination: id || 'custom',
label: text,
layout,
});
if (id === 'discord') {
trackAnalyticsEvent('discord_click', { layout });
}
}}
className={`grid h-full min-h-0 w-full place-items-center rounded-md bg-slate-700 px-0.5 ${isInline ? 'py-0.5' : 'pb-3'} text-center text-sm font-medium text-white transition hover:opacity-90 ${className}`}
style={bgColor ? { backgroundColor: bgColor } : undefined}
@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useSocket } from '../../context/SocketContext.jsx';
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../analytics/index.js';
const CONNECTED_FADE_DELAY_MS = 4000;
@@ -42,39 +41,23 @@ export default function SocketConnectionPill() {
setLastReason('');
show();
scheduleFade();
trackAnalyticsEvent('socket_connect', { connected: true });
};
const onDisconnect = (reason) => {
setConnected(false);
setLastReason(typeof reason === 'string' ? reason : 'disconnected');
show();
trackAnalyticsEventThrottled(
'socket_disconnect',
{ reason: typeof reason === 'string' ? reason : 'disconnected' },
{ key: `disconnect:${reason || 'unknown'}`, throttleMs: 30 * 1000 },
);
};
const onConnectError = (err) => {
setConnected(false);
setLastReason(err?.message || 'connect error');
show();
trackAnalyticsEventThrottled(
'socket_connect_error',
{ message: err?.message || 'connect error' },
{ key: `connect_error:${err?.message || 'unknown'}`, throttleMs: 30 * 1000 },
);
};
const onReconnectAttempt = () => {
setConnected(false);
show();
trackAnalyticsEventThrottled(
'socket_reconnect_attempt',
{ connected: false },
{ key: 'socket_reconnect_attempt', throttleMs: 30 * 1000 },
);
};
socket.on('connect', onConnect);
@@ -4,7 +4,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
import { useSession } from '../../context/SessionContext.jsx';
import { trackAnalyticsEventThrottled } from '../../analytics/index.js';
/*
Sliders are a 0-1 fraction of whichever ceiling the server resolved for this
@@ -87,11 +86,6 @@ export default function VolumeSettingsCard() {
setDraft(nextDraft);
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
commitTimerRef.current = setTimeout(() => commit(nextDraft), COMMIT_DEBOUNCE_MS);
trackAnalyticsEventThrottled(
'settings_change',
{ setting: key, value: next },
{ key: `volume:${key}`, throttleMs: 3 * 1000 },
);
};
// Sliders would be misleading before the first session sync lands.
@@ -18,7 +18,6 @@ import { mergeFloatChunks, encodeWavMono16 } from './audioCodec.js';
import StatusIndicator from './StatusIndicator.jsx';
import KeyPill from './KeyPill.jsx';
import CardFrame from '../../CardFrame/index.jsx';
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../../analytics/index.js';
export default function VipAudioUploadCard({
ownRoverId = '',
@@ -88,9 +87,8 @@ export default function VipAudioUploadCard({
(nextMode) => {
const mode = nextMode === 'clip' ? 'clip' : 'live';
saveVipAudio((current) => ({ ...(current || {}), pttMode: mode }));
trackAnalyticsEvent('vip_ptt_mode_change', { mode, roverId });
},
[roverId, saveVipAudio],
[saveVipAudio],
);
const handleUploadPlay = async () => {
@@ -109,11 +107,6 @@ export default function VipAudioUploadCard({
setWorking(true);
setMessage('');
trackAnalyticsEvent('vip_upload_play', {
roverId,
size: selectedUpload.size,
mime: selectedUpload.type || '',
});
try {
const buffer = await selectedUpload.arrayBuffer();
const dataBase64 = bytesToBase64(new Uint8Array(buffer));
@@ -124,9 +117,7 @@ export default function VipAudioUploadCard({
dataBase64,
});
setMessage('Upload playback started.');
trackAnalyticsEvent('vip_upload_play_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('vip_upload_play_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
setMessage(err?.message || 'Failed to play upload.');
} finally {
setWorking(false);
@@ -141,13 +132,10 @@ export default function VipAudioUploadCard({
setWorking(true);
setMessage('');
trackAnalyticsEvent('vip_upload_stop', { roverId });
try {
await stopUploadedAudio?.(roverId);
setMessage('Upload playback stopped.');
trackAnalyticsEvent('vip_upload_stop_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('vip_upload_stop_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
setMessage(err?.message || 'Failed to stop upload.');
} finally {
setWorking(false);
@@ -378,24 +366,20 @@ export default function VipAudioUploadCard({
if (!send) {
setClipState('idle');
trackAnalyticsEvent('vip_clip_record_cancel', { roverId });
return;
}
if (!roverId) {
setClipState('idle');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'skipped_no_rover' });
return;
}
if (!chunks.length) {
setClipState('idle');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'skipped_empty' });
return;
}
setClipState('sending');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'started' });
try {
const merged = mergeFloatChunks(chunks);
const wavBytes = encodeWavMono16(merged, clipSampleRateRef.current || TARGET_SAMPLE_RATE);
@@ -407,10 +391,8 @@ export default function VipAudioUploadCard({
dataBase64,
});
setClipState('idle');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'accepted' });
} catch (err) {
setClipState('error');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'failed', reason: err?.message || 'unknown' });
setMessage(err?.message || 'Failed to send PTT clip.');
}
},
@@ -426,7 +408,6 @@ export default function VipAudioUploadCard({
clipChunksRef.current = [];
clipRecordingRef.current = true;
setClipState('recording');
trackAnalyticsEvent('vip_clip_record_start', { roverId });
}, [ensureClipPipeline, roverId]);
useEffect(() => {
@@ -453,17 +434,11 @@ export default function VipAudioUploadCard({
await startWhipMic(roverId);
if (!cancelled) {
setMicState('live');
trackAnalyticsEvent('vip_live_mic_start', { roverId });
}
} catch (err) {
if (!cancelled) {
setMicState('error');
setMessage(err?.message || 'Failed to start mic forwarding.');
trackAnalyticsEventThrottled(
'vip_live_mic_error',
{ roverId, reason: err?.message || 'unknown' },
{ key: `${roverId}:${err?.message || 'unknown'}`, throttleMs: 60 * 1000 },
);
}
await stopMicCapture(roverId);
}
@@ -651,7 +626,6 @@ export default function VipAudioUploadCard({
onChange={(event) => {
const enabled = Boolean(event.target.checked);
saveVipAudio((current) => ({ ...(current || {}), openMicEnabled: enabled }));
trackAnalyticsEvent('vip_open_mic_change', { roverId, enabled });
}}
/>
<span>{clipMode ? 'Open mic (live mode only)' : 'Open mic'}</span>
@@ -5,7 +5,6 @@ import { useMemo, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
import { flowWrapClass, innerFlowClass } from './constants.js';
import RoverLabel from '../RoverLabel/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
export default function VipPrivateRoverAccessCard({
requestableRovers = [],
@@ -30,24 +29,14 @@ export default function VipPrivateRoverAccessCard({
if (!roverId) return;
setPendingByRover((prev) => ({ ...prev, [roverId]: true }));
onMessage?.('');
trackAnalyticsEvent('vip_private_rover_request', { roverId, status: 'started' });
try {
const response = await requestPrivateRoverAccess?.(roverId);
trackAnalyticsEvent('vip_private_rover_request', {
roverId,
status: response?.existing ? 'existing' : 'accepted',
});
if (response?.existing) {
onMessage?.('You already have a pending request for that rover.');
} else {
onMessage?.('Private rover access request sent to lockdown admins.');
}
} catch (err) {
trackAnalyticsEvent('vip_private_rover_request', {
roverId,
status: 'failed',
reason: err?.message || 'unknown',
});
onMessage?.(err.message || 'Failed to send request.');
} finally {
setPendingByRover((prev) => ({ ...prev, [roverId]: false }));
@@ -5,7 +5,6 @@ import { useState } from 'react';
import NicknameForm from '../NicknameForm/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { flowWrapClass, innerFlowClass, fieldClass } from './constants.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
export default function VipVerificationCard({
pendingRequestId,
@@ -26,7 +25,6 @@ export default function VipVerificationCard({
setRequestKeyInput(currentStoredKey);
setConfirmNickname(false);
onMessage?.('');
trackAnalyticsEvent('vip_verify_flow_start', { hasNickname: Boolean(nickname) });
};
const cancelRequestFlow = () => {
@@ -46,10 +44,6 @@ export default function VipVerificationCard({
}
setWorking(true);
onMessage?.('');
trackAnalyticsEvent('vip_verify_attempt', {
hasNickname: Boolean(nickname),
hasIdentityKey: Boolean(requestKeyInput),
});
try {
const applied = await applyIdentityKey(requestKeyInput);
await requestVerification?.();
@@ -57,9 +51,7 @@ export default function VipVerificationCard({
setRequestFlowStep(0);
setConfirmNickname(false);
onMessage?.('Verification request sent to lockdown admins.');
trackAnalyticsEvent('vip_verify_result', { status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('vip_verify_result', { status: 'failed', reason: err?.message || 'unknown' });
onMessage?.(err.message || 'Failed to submit request.');
} finally {
setWorking(false);