more (modular local offline not selling data) analytics!

This commit is contained in:
legop3
2026-06-19 01:03:47 -04:00
parent c7e811a957
commit ea8e027be9
12 changed files with 547 additions and 139 deletions
@@ -4,6 +4,7 @@
import { useEffect, useMemo, useState } from 'react';
import useBarcodeGameState from '../../barcodeGames/useBarcodeGameState.js';
import CardFrame from '../CardFrame/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
function clampRgbChannel(value) {
if (!Number.isFinite(value)) return null;
@@ -189,7 +190,7 @@ function Leaderboard({ players, ownPlayer }) {
}
export default function BarcodeGamesPanel() {
const { state, voteForGame } = useBarcodeGameState();
const { state, connectionState, voteForGame } = useBarcodeGameState();
const [pendingGameId, setPendingGameId] = useState(null);
const activeGame = state.activeGame;
const display = activeGame?.display || {};
@@ -204,10 +205,41 @@ export default function BarcodeGamesPanel() {
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]);
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);
}
@@ -4,6 +4,7 @@
import { useEffect, useState } from 'react';
import { useSessionActions } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
export default function NicknameForm({ compact = false }) {
const { setNickname } = useSessionActions();
@@ -26,6 +27,14 @@ export default function NicknameForm({ compact = false }) {
// the driver UI so chat/user-list labels stay consistent across routes.
await setNickname(trimmed);
save({ nickname: trimmed });
/*
The event records that a nickname was saved, while the shared analytics
session reporter owns the actual nickname field. Keeping that split
prevents every form call site from needing to know privacy/config rules.
*/
trackAnalyticsEvent('nickname_set', {
length: trimmed.length,
});
} catch (err) {
alert(err.message);
} finally {
+15 -2
View File
@@ -29,9 +29,10 @@ import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import BarcodeGamesPanel from '../BarcodeGamesPanel/index.jsx';
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { useLayoutEffect, useRef, useState } from 'react';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { themeGapClass, themeStackClass } from '../../themeFlags.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
const CHAT_DOCK_INITIAL_HEIGHT = 224;
const CHAT_DOCK_MIN_HEIGHT = 144;
@@ -173,6 +174,18 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
// unavailable, including disabled service and direct-address mode.
return Boolean(vote?.votingEnabled);
});
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.
*/
setActiveTab(tab);
trackAnalyticsEvent('tab_change', { tab, layout, surface: 'desktop_right_pane' });
},
[layout],
);
useLayoutEffect(() => {
const chatDock = chatDockRef.current;
@@ -259,7 +272,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
return (
<section className="text-base">
<Tabs defaultTab="telemetry" currentTab={activeTab} onTabChange={setActiveTab}>
<Tabs defaultTab="telemetry" currentTab={activeTab} onTabChange={handleTabChange}>
<TabList>
<Tab id="telemetry">Controls</Tab>
<Tab id="activities">Activities</Tab>