From 34d597b5118d83568d2863201670f07dc4d6d26a Mon Sep 17 00:00:00 2001 From: oabrivard Date: Fri, 6 Feb 2026 10:49:52 +0100 Subject: [PATCH] Updated to documentation to align with most recent technical choices (SolidJS, Tauri) --- README.md | 2 +- .../application-architecture.md | 8 +- docs/2_architecture/security-architecture.md | 2 +- docs/3_guidelines/design-guidelines.md | 7 +- docs/3_guidelines/development-guidelines.md | 638 ++++-------------- docs/3_guidelines/observability-guidelines.md | 67 +- 6 files changed, 160 insertions(+), 564 deletions(-) diff --git a/README.md b/README.md index dea2bff..61dc833 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Know Foolery is a quiz game inspired by the French game "Déconnaissance" (https - Production-ready deployment ### Phase 3: Mobile Expansion (Weeks 13-18) -- React Native mobile applications +- Cross-platform mobile applications - Cross-platform component optimization - Mobile app store deployment diff --git a/docs/2_architecture/application-architecture.md b/docs/2_architecture/application-architecture.md index f2fd691..515d0c3 100644 --- a/docs/2_architecture/application-architecture.md +++ b/docs/2_architecture/application-architecture.md @@ -70,7 +70,7 @@ Web Application: Language: TypeScript 5.0+ Build Tool: Vite 4.0+ UI Library: SUID (suid.io) - Testing: Jest + Playwright + Testing: Vitest + Playwright Mobile Applications: Framework: Tauri 2.9.5+ @@ -171,8 +171,8 @@ Circuit Breaker: ### Cross-Platform Deployment - **Web**: Standard web application deployment -- **Mobile**: iOS App Store and Google Play Store distribution -- **Desktop**: Wails applications for major operating systems +- **Mobile**: iOS App Store and Google Play Store distribution via Tauri +- **Desktop**: Tauri applications for major operating systems ### Infrastructure Technologies ```yaml @@ -293,7 +293,7 @@ knowfoolery/ │ │ │ └── styles/ │ │ │ └── global.css │ │ └── tsconfig.json -│ └── cross-platform/ # Tauri app (embeds the SolidJS web app) +│ └── cross-platform/ # Tauri app │ ├── package.json │ ├── vite.config.ts │ ├── src/ # Inherits most from web app diff --git a/docs/2_architecture/security-architecture.md b/docs/2_architecture/security-architecture.md index f7711cd..481a4fa 100644 --- a/docs/2_architecture/security-architecture.md +++ b/docs/2_architecture/security-architecture.md @@ -77,7 +77,7 @@ Zitadel serves as the self-hosted OAuth 2.0/OpenID Connect authentication provid ┌─────────────────────────────────────────────────────────────────────┐ │ Client Applications │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Web App │ │ Mobile iOS │ │Mobile Android│ │Desktop Wails│ │ +│ │ Web App │ │ Mobile iOS │ │Mobile Android│ │Desktop Tauri│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ diff --git a/docs/3_guidelines/design-guidelines.md b/docs/3_guidelines/design-guidelines.md index fe313d3..458a11c 100644 --- a/docs/3_guidelines/design-guidelines.md +++ b/docs/3_guidelines/design-guidelines.md @@ -10,11 +10,11 @@ - Custom middleware for JWT authentication and rate limiting - Comprehensive testing with testcontainers for integration tests -### TypeScript/React Patterns +### TypeScript/SolidJS Patterns - Components use Gluestack UI with proper TypeScript typing - Custom hooks pattern for business logic (e.g., `useGameSession`) - Context + useReducer for state management -- Comprehensive testing with Jest + React Testing Library +- Comprehensive testing with Jest + Recommended UI Testing Library ### Authentication Flow - Zitadel provides OAuth 2.0/OIDC authentication @@ -187,8 +187,7 @@ service LeaderboardService { ### Frontend Testing - **Unit tests** with Jest -- **Component tests** with Jest + React Testing Library -- **Hook tests** with @testing-library/react-hooks +- **Component tests** with Jest + vitest + Recoummended Testing Library ### End to End Testing - **E2E Testing** with Playwright for complete user journeys diff --git a/docs/3_guidelines/development-guidelines.md b/docs/3_guidelines/development-guidelines.md index ab4833f..3d2464c 100644 --- a/docs/3_guidelines/development-guidelines.md +++ b/docs/3_guidelines/development-guidelines.md @@ -63,18 +63,17 @@ issues: // .eslintrc.json { "extends": [ - "@react-native", + "plugin:solid/typescript", "@typescript-eslint/recommended", "prettier" ], "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "react-hooks"], + "plugins": ["@typescript-eslint", "solid"], "rules": { "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/prefer-const": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", + "solid/reactivity": "warn", "prefer-const": "error", "no-var": "error", "object-shorthand": "error", @@ -129,14 +128,14 @@ type ScoreCalculator interface {} // Files: kebab-case game-card.tsx user-service.ts -auth-context.tsx +auth-store.ts // Components: PascalCase -export const GameCard: React.FC = () => {} -export const LeaderboardList: React.FC = () => {} +export const GameCard: Component = () => {} +export const LeaderboardList: Component = () => {} -// Hooks: camelCase with "use" prefix -export const useGameSession = () => {} +// Composables/stores: camelCase with "create"/"use" prefix +export const createGameSessionStore = () => {} export const useAuthentication = () => {} // Functions: camelCase @@ -608,24 +607,21 @@ func (s *GameService) calculateTimeRemaining(startTime time.Time) int { ## TypeScript Development Standards -### React Component Patterns +### SolidJS Component Patterns #### Component Structure ```typescript -// Component with proper TypeScript types and patterns -import React, { useState, useEffect, useCallback } from 'react' +// Component with proper TypeScript types and SolidJS patterns +import type { Component } from 'solid-js' +import { createMemo, createSignal, Show } from 'solid-js' import { - Card, - VStack, - HStack, - Text, - Input, + Box, Button, - Badge, - Progress -} from '@gluestack-ui/themed' -import { useGameSession } from '../hooks/useGameSession' -import { useTimer } from '../hooks/useTimer' + Input, + LinearProgress, + Typography, + Chip, +} from '@suid/material' // Props interface with proper documentation export interface GameCardProps { @@ -637,346 +633,99 @@ export interface GameCardProps { timeRemaining: number /** Number of attempts left */ attemptsLeft: number - /** Current player score */ - currentScore: number - /** Loading state */ - isLoading?: boolean /** Callback when answer is submitted */ onSubmitAnswer: (answer: string) => Promise - /** Callback when hint is requested */ - onRequestHint: () => Promise - /** Callback when time expires */ - onTimeExpire?: () => void + /** Loading state from async operations */ + loading?: boolean } /** * GameCard component displays a quiz question with input and controls - * + * * @param props - GameCard props - * @returns React component + * @returns SolidJS component */ -export const GameCard: React.FC = ({ - question, - theme, - timeRemaining, - attemptsLeft, - currentScore, - isLoading = false, - onSubmitAnswer, - onRequestHint, - onTimeExpire -}) => { - // Local state with proper typing - const [answer, setAnswer] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - - // Custom hooks - const { formatTime } = useTimer() - - // Memoized handlers - const handleSubmit = useCallback(async () => { - if (!answer.trim() || isSubmitting) return - +export const GameCard: Component = (props) => { + const [answer, setAnswer] = createSignal('') + const [isSubmitting, setIsSubmitting] = createSignal(false) + + const disabled = createMemo(() => props.loading || isSubmitting()) + const progressValue = createMemo(() => ((30 * 60 - props.timeRemaining) / (30 * 60)) * 100) + + const handleSubmit = async () => { + const value = answer().trim() + if (!value || disabled()) return + setIsSubmitting(true) try { - await onSubmitAnswer(answer.trim()) - setAnswer('') // Clear input on successful submission + await props.onSubmitAnswer(value) + setAnswer('') } catch (error) { console.error('Failed to submit answer:', error) - // Error handling would be managed by parent component } finally { setIsSubmitting(false) } - }, [answer, isSubmitting, onSubmitAnswer]) - - const handleHintRequest = useCallback(async () => { - if (isLoading) return - - try { - await onRequestHint() - } catch (error) { - console.error('Failed to request hint:', error) - } - }, [isLoading, onRequestHint]) - - // Effects - useEffect(() => { - if (timeRemaining === 0 && onTimeExpire) { - onTimeExpire() - } - }, [timeRemaining, onTimeExpire]) - - // Computed values - const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100 - const isTimeWarning = timeRemaining <= 300 // 5 minutes - const isTimeCritical = timeRemaining <= 60 // 1 minute - + } + return ( - - - {/* Header with theme and timer */} - - - - {theme} - - - - - ⏱️ {formatTime(timeRemaining)} - - - - - {/* Progress indicator */} - - - - - {/* Question */} - - {question} - - - {/* Answer input */} - - - - - {/* Game stats */} - - - Attempts left: {attemptsLeft}/3 - - - Score: {currentScore} points - - - - {/* Action buttons */} - - - - - - + + + + {props.timeRemaining}s + + + {props.question} + Attempts left: {props.attemptsLeft}/3 + setAnswer(e.currentTarget.value)} + disabled={disabled()} + placeholder="Enter your answer" + /> + + + Submitting... + + ) } - -// Default export for dynamic imports -export default GameCard ``` -#### Custom Hooks Pattern +#### Solid Composable Pattern ```typescript -// Custom hook with proper TypeScript types -import { useState, useEffect, useCallback, useRef } from 'react' +import { createSignal } from 'solid-js' import { GameSessionService } from '../services/game-session-service' -interface UseGameSessionOptions { - playerName: string - onSessionEnd?: (finalScore: number) => void - onError?: (error: Error) => void -} - -interface GameSessionState { - sessionId: string | null - currentQuestion: Question | null - score: number - attemptsLeft: number - timeRemaining: number - isLoading: boolean - error: string | null -} - -interface GameSessionActions { - startGame: () => Promise - submitAnswer: (answer: string) => Promise - requestHint: () => Promise - endGame: () => Promise -} +export const createGameSessionStore = (playerName: () => string) => { + const service = new GameSessionService() + const [isLoading, setIsLoading] = createSignal(false) + const [error, setError] = createSignal(null) + const [sessionId, setSessionId] = createSignal(null) -export const useGameSession = ( - options: UseGameSessionOptions -): [GameSessionState, GameSessionActions] => { - const [state, setState] = useState({ - sessionId: null, - currentQuestion: null, - score: 0, - attemptsLeft: 3, - timeRemaining: 30 * 60, // 30 minutes - isLoading: false, - error: null, - }) - - const timerRef = useRef(null) - const gameService = useRef(new GameSessionService()) - - // Timer effect - useEffect(() => { - if (state.sessionId && state.timeRemaining > 0) { - timerRef.current = setInterval(() => { - setState(prev => { - const newTimeRemaining = prev.timeRemaining - 1 - if (newTimeRemaining <= 0 && options.onSessionEnd) { - options.onSessionEnd(prev.score) - } - return { ...prev, timeRemaining: Math.max(0, newTimeRemaining) } - }) - }, 1000) - } - - return () => { - if (timerRef.current) { - clearInterval(timerRef.current) - } - } - }, [state.sessionId, state.timeRemaining, options.onSessionEnd]) - - // Actions - const startGame = useCallback(async () => { - setState(prev => ({ ...prev, isLoading: true, error: null })) - + const startGame = async () => { + setIsLoading(true) + setError(null) try { - const session = await gameService.current.startGame(options.playerName) - setState(prev => ({ - ...prev, - sessionId: session.sessionId, - currentQuestion: session.firstQuestion, - score: 0, - attemptsLeft: 3, - timeRemaining: session.timeRemaining, - isLoading: false, - })) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to start game' - setState(prev => ({ ...prev, error: errorMessage, isLoading: false })) - options.onError?.(error instanceof Error ? error : new Error(errorMessage)) - } - }, [options.playerName, options.onError]) - - const submitAnswer = useCallback(async (answer: string): Promise => { - if (!state.sessionId || !state.currentQuestion) { - throw new Error('No active game session') - } - - setState(prev => ({ ...prev, isLoading: true })) - - try { - const result = await gameService.current.submitAnswer( - state.sessionId, - state.currentQuestion.id, - answer - ) - - setState(prev => ({ - ...prev, - score: result.newScore, - attemptsLeft: result.attemptsLeft, - currentQuestion: result.nextQuestion, - isLoading: false, - })) - - return result.isCorrect - } catch (error) { - setState(prev => ({ ...prev, isLoading: false })) - throw error - } - }, [state.sessionId, state.currentQuestion]) - - const requestHint = useCallback(async (): Promise => { - if (!state.sessionId || !state.currentQuestion) { - throw new Error('No active game session') - } - - const hint = await gameService.current.requestHint( - state.sessionId, - state.currentQuestion.id - ) - - return hint - }, [state.sessionId, state.currentQuestion]) - - const endGame = useCallback(async () => { - if (!state.sessionId) return - - await gameService.current.endGame(state.sessionId) - - setState({ - sessionId: null, - currentQuestion: null, - score: 0, - attemptsLeft: 3, - timeRemaining: 30 * 60, - isLoading: false, - error: null, - }) - - if (timerRef.current) { - clearInterval(timerRef.current) + const session = await service.startGame(playerName()) + setSessionId(session.sessionId) + return session + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to start game' + setError(message) + throw err + } finally { + setIsLoading(false) } - }, [state.sessionId]) - - return [ - state, - { - startGame, - submitAnswer, - requestHint, - endGame, - }, - ] + } + + return { + isLoading, + error, + sessionId, + startGame, + } } ``` @@ -1148,196 +897,50 @@ func BenchmarkGameService_StartGame(b *testing.B) { ### TypeScript Testing Patterns ```typescript -// React component testing with Jest and Testing Library -import React from 'react' -import { render, screen, fireEvent, waitFor } from '@testing-library/react' -import { GluestackUIProvider } from '@gluestack-ui/themed' +// SolidJS component testing with Vitest and Testing Library +import { render, screen, fireEvent } from '@solidjs/testing-library' +import { describe, expect, it, vi } from 'vitest' import { GameCard, GameCardProps } from '../GameCard' -// Test wrapper for Gluestack UI -const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - {children} -) - // Mock props factory const createMockProps = (overrides: Partial = {}): GameCardProps => ({ question: "What is the capital of France?", theme: "Geography", timeRemaining: 1800, // 30 minutes attemptsLeft: 3, - currentScore: 0, - onSubmitAnswer: jest.fn(), - onRequestHint: jest.fn(), + onSubmitAnswer: vi.fn(), + loading: false, ...overrides, }) describe('GameCard', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - describe('Rendering', () => { it('should render question and theme correctly', () => { const props = createMockProps() - - render(, { wrapper: TestWrapper }) - - expect(screen.getByTestId('question-text')).toHaveTextContent(props.question) - expect(screen.getByTestId('theme-badge')).toHaveTextContent(props.theme) - }) - - it('should display timer in correct format', () => { - const props = createMockProps({ timeRemaining: 125 }) // 2:05 - - render(, { wrapper: TestWrapper }) - - expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05') - }) - - it('should show attempts and score correctly', () => { - const props = createMockProps({ attemptsLeft: 2, currentScore: 5 }) - - render(, { wrapper: TestWrapper }) - - expect(screen.getByTestId('attempts-counter')).toHaveTextContent('Attempts left: 2/3') - expect(screen.getByTestId('score-display')).toHaveTextContent('Score: 5 points') + + render(() => ) + + expect(screen.getByText(props.question)).toBeInTheDocument() + expect(screen.getByText(props.theme)).toBeInTheDocument() }) }) - + describe('User Interactions', () => { it('should call onSubmitAnswer when submit button is clicked', async () => { const props = createMockProps() - const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined) + const mockOnSubmitAnswer = vi.fn().mockResolvedValue(undefined) props.onSubmitAnswer = mockOnSubmitAnswer - - render(, { wrapper: TestWrapper }) - - // Type an answer - const input = screen.getByTestId('answer-input') - fireEvent.changeText(input, 'Paris') - - // Click submit - const submitButton = screen.getByTestId('submit-button') - fireEvent.press(submitButton) - - await waitFor(() => { - expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris') - }) - }) - - it('should not submit empty answer', () => { - const props = createMockProps() - - render(, { wrapper: TestWrapper }) - - const submitButton = screen.getByTestId('submit-button') - expect(submitButton).toBeDisabled() - }) - - it('should call onRequestHint when hint button is clicked', async () => { - const props = createMockProps() - const mockOnRequestHint = jest.fn().mockResolvedValue('City of Light') - props.onRequestHint = mockOnRequestHint - - render(, { wrapper: TestWrapper }) - - const hintButton = screen.getByTestId('hint-button') - fireEvent.press(hintButton) - - await waitFor(() => { - expect(mockOnRequestHint).toHaveBeenCalled() - }) - }) - }) - - describe('Loading States', () => { - it('should disable inputs when loading', () => { - const props = createMockProps({ isLoading: true }) - - render(, { wrapper: TestWrapper }) - - expect(screen.getByTestId('answer-input')).toBeDisabled() - expect(screen.getByTestId('submit-button')).toBeDisabled() - expect(screen.getByTestId('hint-button')).toBeDisabled() - }) - }) - - describe('Timer States', () => { - it('should show warning color when time is low', () => { - const props = createMockProps({ timeRemaining: 300 }) // 5 minutes - - render(, { wrapper: TestWrapper }) - - const timer = screen.getByTestId('timer-display') - expect(timer).toHaveStyle({ color: expect.stringContaining('warning') }) - }) - - it('should show critical color when time is very low', () => { - const props = createMockProps({ timeRemaining: 30 }) // 30 seconds - - render(, { wrapper: TestWrapper }) - - const timer = screen.getByTestId('timer-display') - expect(timer).toHaveStyle({ color: expect.stringContaining('error') }) - }) - }) -}) -// Hook testing -import { renderHook, act } from '@testing-library/react' -import { useGameSession } from '../hooks/useGameSession' + render(() => ) -// Mock the service -jest.mock('../services/game-session-service') + const input = screen.getByPlaceholderText('Enter your answer') + fireEvent.input(input, { target: { value: 'Paris' } }) -describe('useGameSession', () => { - const mockGameService = { - startGame: jest.fn(), - submitAnswer: jest.fn(), - requestHint: jest.fn(), - endGame: jest.fn(), - } - - beforeEach(() => { - jest.clearAllMocks() - // Set up service mock - ;(GameSessionService as jest.Mock).mockImplementation(() => mockGameService) - }) - - it('should start game successfully', async () => { - mockGameService.startGame.mockResolvedValue({ - sessionId: 'session123', - firstQuestion: { id: 'q1', text: 'Test question' }, - timeRemaining: 1800, - }) - - const { result } = renderHook(() => - useGameSession({ playerName: 'TestPlayer' }) - ) - - await act(async () => { - await result.current[1].startGame() - }) - - expect(result.current[0].sessionId).toBe('session123') - expect(result.current[0].currentQuestion).toBeDefined() - }) - - it('should handle errors during game start', async () => { - const mockError = new Error('Failed to start game') - mockGameService.startGame.mockRejectedValue(mockError) - - const onError = jest.fn() - const { result } = renderHook(() => - useGameSession({ playerName: 'TestPlayer', onError }) - ) - - await act(async () => { - await result.current[1].startGame() + const submitButton = screen.getByRole('button', { name: 'Submit Answer' }) + fireEvent.click(submitButton) + + expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris') }) - - expect(result.current[0].error).toBe('Failed to start game') - expect(onError).toHaveBeenCalledWith(mockError) }) }) ``` @@ -1404,26 +1007,19 @@ docker-compose up -d ### Frontend Development Note: This project uses Yarn with `nodeLinker: node-modules`. PnP files like `.pnp.cjs` and `.pnp.loader.mjs` are not used. +Status: `Web` commands are implemented. `Tauri cross-platform` commands are planned and require a future `frontend/apps/cross-platform` workspace. ```bash # Web application -cd frontend/apps/web -npm install -npm run dev # Start development server -npm run build # Production build -npm run test # Run Jest tests -npm run lint # ESLint -npm run type-check # TypeScript checking - -# Mobile application -cd frontend/apps/mobile -npm install -npx expo start # Start Expo development -npx expo run:ios # Run on iOS simulator -npx expo run:android # Run on Android emulator - -# Desktop application -cd frontend/apps/desktop -npm install -npm run dev # Start Wails development -npm run build # Build desktop app +cd frontend +yarn install +yarn dev # Start web development server (SolidJS) +yarn build # Build all frontend workspaces +yarn test # Run Vitest suites +yarn lint # ESLint +yarn format:check # Prettier check + +# Cross-platform packaging with Tauri (desktop/mobile) +cd frontend/apps/cross-platform +yarn tauri dev # Run Tauri app with embedded web UI +yarn tauri build # Build release bundles ``` diff --git a/docs/3_guidelines/observability-guidelines.md b/docs/3_guidelines/observability-guidelines.md index 8cb3d0d..8916919 100644 --- a/docs/3_guidelines/observability-guidelines.md +++ b/docs/3_guidelines/observability-guidelines.md @@ -565,24 +565,24 @@ export class GameMetricsTracker { // Detect platform if (/Android/i.test(navigator.userAgent)) return 'android' if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) return 'ios' - if (window.wails) return 'desktop' // For Wails apps + if ('__TAURI__' in window) return 'desktop' // For Tauri apps return 'web' } } -// Usage in React components -export const useGameMetrics = () => { - const collector = useRef(new MetricsCollector('/api/v1/metrics')) - const gameTracker = useRef(new GameMetricsTracker(collector.current)) +// Usage in SolidJS modules/components +export const createGameMetrics = () => { + const collector = new MetricsCollector('/api/v1/metrics') + const gameTracker = new GameMetricsTracker(collector) return { - trackGameStart: gameTracker.current.trackGameStart.bind(gameTracker.current), - trackQuestionDisplayed: gameTracker.current.trackQuestionDisplayed.bind(gameTracker.current), - trackAnswerSubmitted: gameTracker.current.trackAnswerSubmitted.bind(gameTracker.current), - trackHintRequested: gameTracker.current.trackHintRequested.bind(gameTracker.current), - trackGameCompleted: gameTracker.current.trackGameCompleted.bind(gameTracker.current), - trackUserAction: collector.current.trackUserAction.bind(collector.current), - trackError: collector.current.trackError.bind(collector.current), + trackGameStart: gameTracker.trackGameStart.bind(gameTracker), + trackQuestionDisplayed: gameTracker.trackQuestionDisplayed.bind(gameTracker), + trackAnswerSubmitted: gameTracker.trackAnswerSubmitted.bind(gameTracker), + trackHintRequested: gameTracker.trackHintRequested.bind(gameTracker), + trackGameCompleted: gameTracker.trackGameCompleted.bind(gameTracker), + trackUserAction: collector.trackUserAction.bind(collector), + trackError: collector.trackError.bind(collector), } } ``` @@ -797,26 +797,27 @@ export class FrontendTracing { } } -// React hook for tracing -export const useTracing = () => { - const tracer = trace.getTracer('react-components') - - const traceUserAction = useCallback( - async (action: string, properties: Record, fn: () => Promise) => { - return tracer.startActiveSpan(`user.${action}`, async (span) => { - try { - span.setAttributes(properties) - await fn() - } catch (error) { - span.recordException(error as Error) - throw error - } finally { - span.end() - } - }) - }, - [tracer] - ) +// SolidJS composable for tracing +export const createTracing = () => { + const tracer = trace.getTracer('solid-components') + + const traceUserAction = async ( + action: string, + properties: Record, + fn: () => Promise + ) => { + return tracer.startActiveSpan(`user.${action}`, async (span) => { + try { + span.setAttributes(properties) + await fn() + } catch (error) { + span.recordException(error as Error) + throw error + } finally { + span.end() + } + }) + } return { traceUserAction } } @@ -1443,4 +1444,4 @@ receivers: text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' ``` -This comprehensive observability strategy ensures that Know Foolery has full visibility into its performance, user behavior, and system health, enabling proactive issue resolution and data-driven product improvements. \ No newline at end of file +This comprehensive observability strategy ensures that Know Foolery has full visibility into its performance, user behavior, and system health, enabling proactive issue resolution and data-driven product improvements.