Skip to content

Error handling improvements, ITERATION 2#459

Merged
mykhailodanilenko merged 5 commits intomainfrom
feature/refresh-token-fix
Oct 10, 2025
Merged

Error handling improvements, ITERATION 2#459
mykhailodanilenko merged 5 commits intomainfrom
feature/refresh-token-fix

Conversation

@ar9708
Copy link
Copy Markdown
Contributor

@ar9708 ar9708 commented Oct 7, 2025

Replaces #457, to get nice AI-assisted reviews from Claude. (As shown in #458, it does not work with cross-repo PRs yet, so we need to pause using forks as the source.)

🚀 Description

This PR improves the error handling mechanisms and logging across the application, while fixing some of the errors related to refresh token handling. Also, a small test for error handling is added to the "copy diagnostic info" button, press it 5 times in a row and observe the result.

📄 Motivation and Context

#451

🧪 How Has This Been Tested?

  • Web (desktop)
  • Web (mobile)
  • Mobile (iOS)
  • Mobile (Android)
  • TV (Android)
  • TV (Apple)

📦 Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

✅ Checklist (copied from README)

  • Squash your changes into a single clear and thoroughly descriptive commit, split changes into multiple commits only when it contributes to readability
  • Reference the GitHub issue that you are contributing on in your commit title or body
  • Sign your commits, as this is required by the automated GitHub PR checks
  • Ensure that the changes adhere to the project code style and formatting rules by running npx eslint . and npx prettier --check ../ from the ./OwnTube.tv/ directory (without errors/warnings)
  • Include links and illustrations in your pull request to make it easy to review
  • Request a review by @mykhailodanilenko, @ar9708 and @mblomdahl

@ar9708 ar9708 added bug Something isn't working diagnostics Diagnostics collection and analysis labels Oct 7, 2025
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Improves robustness and observability of error handling, session/auth refresh logic, and diagnostics, plus adds localized strings and styling flexibility for error actions.

  • Adds richer error propagation (collection errors, Axios diagnostics) and passes error states to UI components.
  • Enhances auth token refresh and 401 handling plus diagnostic event payloads.
  • Introduces a test-oriented diagnostics trigger and customizable ErrorPage button styling.

Reviewed Changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
screens/Playlists/index.tsx Incorporates collection error into displayed error details.
screens/Playlists/components/PlaylistVideosView.tsx Suppresses null return when an actual error state exists.
screens/Playlist/index.tsx Propagates video query error state to VideoGrid.
screens/ChannelsScreen/index.tsx Centralizes error rendering and includes collection errors.
screens/ChannelCategoryScreen/index.tsx Adds error flags and forwards them to VideoGrid.
screens/CategoryScreen/index.tsx Adds category/video error handling to grid.
screens/CategoriesScreen/index.tsx Includes collection error in error key resolution.
public/featured-instances.json5 Enables username/password login for specific instance.
locales/*.json Adds new translation keys for copy feedback and press limit error.
hooks/useAuthSessionSync.tsx Awaits session selection before marking load complete.
hooks/useAppStateDiagnostics.ts Safely optional-chains subscription removal.
diagnostics/useCustomDiagnosticEvents.ts Removes unsafe any cast on capture.
components/shared/Button.tsx Adds configurable text color and refactors internal color calc.
components/ErrorPage.tsx Supports custom button colors.
components/ErrorBoundary.tsx Supplies explicit styling for reload button.
components/DeviceCapabilities/DeviceCapabilities.tsx Adds copy feedback, press limit test trigger, and improved cleanup.
components/ChannelView.tsx Avoids hiding content when an error (not emptiness) occurred.
components/CategoryView.tsx Same error-aware visibility + variant prop.
app/_layout.tsx Moves ErrorBoundary higher in tree for broader coverage.
api/helpers.ts Adds aggregated error array and Axios diagnostics parser.
api/errorHandler.ts Captures structured Axios diagnostics with error type.
api/axiosInstance.ts Hardens token refresh logic, adds 401 interceptor, and refined expiry calculations.
Files not reviewed (1)
  • OwnTube.tv/package-lock.json: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@ar9708 ar9708 mentioned this pull request Oct 7, 2025
15 tasks
@ar9708 ar9708 requested a review from Copilot October 7, 2025 12:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • OwnTube.tv/package-lock.json: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@mblomdahl
Copy link
Copy Markdown
Contributor

@claude Please review!

@claude
Copy link
Copy Markdown

claude bot commented Oct 7, 2025

Claude finished @mblomdahl's task —— View job


Code Review Complete ✅

I've reviewed the error handling improvements in this PR. Overall, this is a solid improvement to the application's error handling and diagnostics infrastructure. Below are my findings:


🎯 Strengths

  1. Comprehensive token refresh logic - The improvements to axiosInstance.ts add robust handling for token expiration with proper lock mechanisms to prevent race conditions
  2. Better error diagnostics - The parseAxiosErrorDiagnosticsData function properly sanitizes sensitive data while capturing useful debugging info
  3. Improved error propagation - Errors are now properly bubbled up through collection queries to the UI
  4. Nice test feature - The 5-press test in DeviceCapabilities is a clever way to test error boundaries
  5. Proper cleanup - Good use of cleanup functions in hooks (timeouts, event listeners)

🐛 Issues Found

1. Typo in ErrorBoundary (OwnTube.tv/components/ErrorBoundary.tsx:30)

There's a typo: "occurred" is misspelled as "occured"

// Line 30 - Should be "occurred" not "occured" error occurred${...`}

2. Potential null pointer in collection error handling

Several screens access collectionError[0] without checking if collectionError is null first. While the code checks isError before this block, TypeScript doesn't guarantee collectionError is non-null:

  • OwnTube.tv/screens/ChannelsScreen/index.tsx:67

    const { title, description } = getErrorTextKeys(channelsError || channelsCollectionError[0]);
  • OwnTube.tv/screens/Playlists/index.tsx:54

    const { title, description } = getErrorTextKeys(playlistsError || collectionError[0] || null);

    (This one already has the fix)

  • OwnTube.tv/screens/CategoriesScreen/index.tsx:43

Recommendation: Use optional chaining consistently:

const { title, description } = getErrorTextKeys(channelsError || channelsCollectionError?.[0] || null);

3. Token refresh timing issue (OwnTube.tv/api/axiosInstance.ts:103-104)

The token refresh is triggered when now > halfway, but this could cause unnecessary refresh attempts. Consider the logic:

const halfway = accessIssued && accessExpiresInNum ? accessIssued + accessExpiresInNum * 0.5 : 0;
if ((now > halfway && refreshToken && shouldAttachAccessToken) || (!accessTokenValid && refreshTokenValid)) {

Issue: If we're past halfway but the access token is still valid (accessTokenValid === true), we'll attempt a refresh on every request until the token actually expires. This could lead to rate limiting.

Recommendation: Only refresh if we're past 80-90% of the token's lifetime OR if the token has expired:

const refreshThreshold = accessIssued && accessExpiresInNum ? accessIssued + accessExpiresInNum * 0.8 : 0;
if ((now > refreshThreshold && refreshToken && shouldAttachAccessToken && accessTokenValid) || 
    (!accessTokenValid && refreshTokenValid)) {

4. Missing dependency in useEffect (OwnTube.tv/hooks/useAuthSessionSync.tsx:40)

The useEffect calls handleCheckSessionExistence() but doesn't include it in the dependency array. This could cause stale closure issues.

Recommendation: Either include the function in dependencies or use useCallback:

const handleCheckSessionExistence = useCallback(async () => {
  if (backend) {
    await selectSession(backend);
  } else {
    clearSession();
  }
}, [backend, selectSession, clearSession]);

💡 Suggestions for Improvement

5. Consider debouncing the token refresh (OwnTube.tv/api/axiosInstance.ts)

While you have a lock mechanism (REFRESH_LOCKS), multiple requests hitting the API simultaneously could all check the lock before it's set. Consider adding a small delay or promise-based waiting:

const REFRESH_PROMISES: Record<string, Promise<any> | null> = {};

const refreshAccessToken = async (backend: string, refreshToken: string) => {
  if (REFRESH_PROMISES[backend]) {
    return REFRESH_PROMISES[backend]; // Wait for existing refresh
  }
  
  const promise = (async () => {
    try {
      // ... existing refresh logic
    } finally {
      REFRESH_PROMISES[backend] = null;
    }
  })();
  
  REFRESH_PROMISES[backend] = promise;
  return promise;
};

6. Button text color calculation (OwnTube.tv/components/shared/Button.tsx:57-58)

The text color logic is a bit complex. Consider extracting it to a separate function:

const getTextColor = useCallback(() => {
  if (textColor) return textColor;
  if (disabled) return colors.themeDesaturated500;
  return contrast === "high" ? colors.white94 : colors.theme900;
}, [textColor, disabled, contrast, colors]);

7. Error boundary placement (OwnTube.tv/app/_layout.tsx:192)

Good move placing ErrorBoundary higher in the tree! This ensures errors in providers are caught. Consider adding error logging to track which component caused the error.


🔒 Security Notes

Good job on sanitizing error data - The parseAxiosErrorDiagnosticsData function properly removes sensitive headers and config data before sending to PostHog

Token handling looks secure - Tokens are never logged or exposed in error messages


📝 Code Style

  • ✅ All code follows TypeScript best practices
  • ✅ Proper use of i18n for all user-facing strings
  • ✅ Good separation of concerns between API, UI, and diagnostics layers
  • ⚠️ Missing JSDoc comments on complex functions like refreshAccessToken - would help future maintainers

🧪 Testing Recommendations

Since this PR touches critical authentication and error handling:

  1. Test token refresh edge cases (expired refresh token, network failure during refresh)
  2. Verify the 5-press test works across all platforms
  3. Test error boundaries with various error types (render errors, async errors, etc.)
  4. Verify collection errors display correctly when some queries succeed and others fail

Verdict

Approve with minor fixes recommended. The core improvements are solid and will significantly improve error handling. The issues identified are mostly minor (except the token refresh timing, which could cause performance issues under heavy load).

Priority fixes:

  1. Fix the typo in ErrorBoundary
  2. Add optional chaining to collection error access
  3. Adjust token refresh timing to avoid excessive refresh attempts

Nice-to-have:
4. Add missing useEffect dependency
5. Consider promise-based refresh locking

Great work on improving the diagnostics infrastructure! 🎉


Job run: https://github.com/OwnTube-tv/web-client/actions/runs/
Branch: feature/refresh-token-fix

@mblomdahl
Copy link
Copy Markdown
Contributor

@claude I think the issue no. 1 about the "occurred" typo is already fixed. Is it not?

@claude
Copy link
Copy Markdown

claude bot commented Oct 7, 2025

Claude finished @mblomdahl's task —— View job


Verification Complete ✅

You're absolutely right, @mblomdahl! I apologize for the confusion.

I just checked OwnTube.tv/components/ErrorBoundary.tsx:30 and the typo is already fixed. The line correctly reads:

```typescript error occurred${...`}


The word "occurred" is spelled correctly with double 'r'. I can see from the commit history that this was fixed in commit `3b41173 - fix typo in ErrorBoundary component title string`, which I should have caught when reviewing.

**Status**: Issue #1 from my previous review is already resolved. ✅

---
*Job run: https://github.com/OwnTube-tv/web-client/actions/runs/18314848619*
*Branch: feature/refresh-token-fix*

Copy link
Copy Markdown
Contributor

@mblomdahl mblomdahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mykhailodanilenko Please assess the PR review from Claude and correct the things you think are valid issues + nice-to-haves. Tag me for re-review after!

@mblomdahl
Copy link
Copy Markdown
Contributor

@claude About issue 3 that you mentioned, "token refresh timing issue", the idea is to minimize the risk of intermittent users finding themselves with expired refresh tokens. We don't control the token expiration time, but it's usually 2 weeks, and if a user is visiting randomly a few times per month we want to maximize for them still being logged in on next visit. Does that make sense? How can this code be made more understandable to humans and AI agents alike?

@claude
Copy link
Copy Markdown

claude bot commented Oct 8, 2025

Claude finished @mblomdahl's task —— View job


Response to Token Refresh Timing Question

  • Read current implementation
  • Analyze the use case
  • Provide recommendations for clarity

Understanding the Use Case ✅

You're absolutely right, and I apologize for misunderstanding the intent! Your use case makes perfect sense:

Goal: Maximize the chance that intermittent users (visiting every few weeks) stay logged in by refreshing tokens proactively.

Strategy: Refresh at 50% of token lifetime to ensure the refresh token itself gets renewed before it expires.

This is actually a smart approach for the use case! My concern about "excessive refresh attempts" doesn't apply here because:

  1. The refresh only happens once per token (after crossing the 50% threshold)
  2. The REFRESH_LOCKS mechanism prevents concurrent refreshes
  3. For a 2-week token, this means refreshing after ~1 week, which is very reasonable

Suggestions for Code Clarity

Here's how to make the logic more understandable to both humans and AI agents:

1. Add explanatory comments (OwnTube.tv/api/axiosInstance.ts:103-104)

// Proactively refresh tokens at 50% of their lifetime to maximize login
// persistence for intermittent users. For a typical 2-week token lifetime,
// this refreshes after ~1 week, ensuring the refresh token itself gets
// renewed before it expires.
const halfway = accessIssued && accessExpiresInNum ? accessIssued + accessExpiresInNum * 0.5 : 0;
if ((now > halfway && refreshToken && shouldAttachAccessToken) || (!accessTokenValid && refreshTokenValid)) {

2. Use more descriptive variable names

Consider renaming halfway to something more intention-revealing:

// Option A: Describe the timing strategy
const proactiveRefreshThreshold = accessIssued && accessExpiresInNum 
  ? accessIssued + accessExpiresInNum * 0.5 
  : 0;

// Option B: Describe what it represents
const tokenRefreshTimestamp = accessIssued && accessExpiresInNum 
  ? accessIssued + accessExpiresInNum * 0.5 
  : 0;

// Option C: Include the percentage in the name
const fiftyPercentLifetimeThreshold = accessIssued && accessExpiresInNum 
  ? accessIssued + accessExpiresInNum * 0.5 
  : 0;

I'd recommend Option A (proactiveRefreshThreshold) as it best communicates the intent.

3. Extract the magic number to a constant

// At the top of the file, near other constants
const TOKEN_REFRESH_LIFETIME_PERCENTAGE = 0.5; // Refresh at 50% of token lifetime

// Then in the code:
const proactiveRefreshThreshold = accessIssued && accessExpiresInNum 
  ? accessIssued + accessExpiresInNum * TOKEN_REFRESH_LIFETIME_PERCENTAGE 
  : 0;

This makes the 50% configurable and documents why that specific percentage was chosen.

4. Add JSDoc to the refresh function

/**
 * Refreshes an access token using a refresh token.
 * 
 * Uses a backend-specific lock to prevent concurrent refresh attempts.
 * Typical use case: Proactively refresh tokens before they expire to keep
 * intermittent users logged in (users who visit every few weeks).
 * 
 * @param backend - The backend server hostname
 * @param refreshToken - The refresh token to use
 * @returns The new login response with fresh tokens, or null if locked
 */
const refreshAccessToken = async (backend: string, refreshToken: string) => {

Summary

Your implementation is actually correct for the use case! The key is documenting the intent so that future maintainers (human or AI) understand:

  1. Why 50% was chosen (maximize login persistence)
  2. What problem it solves (intermittent users with ~2 week tokens)
  3. How it prevents issues (lock mechanism prevents concurrent refreshes)

Would you like me to implement these documentation improvements?


Job run: https://github.com/OwnTube-tv/web-client/actions/runs/18315185531
Branch: feature/refresh-token-fix

@mblomdahl
Copy link
Copy Markdown
Contributor

@claude About issue 7 from your initial review, please elaborate. How can we do this, "Consider adding error logging to track which component caused the error."? Propose solution.

@claude
Copy link
Copy Markdown

claude bot commented Oct 8, 2025

Claude finished @mblomdahl's task —— View job


Elaborating on Error Boundary Component Tracking ✅

  • Read current ErrorBoundary implementation
  • Review diagnostics infrastructure
  • Research component tracking approaches
  • Propose detailed solution with code examples

Solution: Enhanced Error Boundary with Component Tracking

Great question! Currently, the ErrorBoundary at OwnTube.tv/components/ErrorBoundary.tsx:23 calls postHogInstance.captureException(error) but doesn't capture metadata about which component caused the error. This makes debugging production issues harder.

React's componentDidCatch provides info.componentStack, which contains the component tree. We should capture this along with additional context.


Proposed Implementation

Here's how to enhance the ErrorBoundary to track which component caused errors:

1. Add a new exception type to diagnostics constants

Add to OwnTube.tv/diagnostics/constants.ts:

export enum CustomPostHogExceptions {
  OutOfMemory = "out_of_memory",
  VideoPlayerError = "video_player_error",
  HttpRequestError = "http_request_error",
  ChromecastError = "chromecast_error",
  TokenError = "token_error",
  RateLimitError = "rate_limit_error",
  ComponentRenderError = "component_render_error", // NEW
}

2. Parse the component stack to extract meaningful info

Add a helper function to extract the most relevant component from the stack:

// OwnTube.tv/components/ErrorBoundary.tsx

/**
 * Extracts the component name that likely caused the error from React's component stack.
 * Returns the first custom component (non-React internal) in the stack.
 * 
 * @example
 * Input: "    at VideoGrid (src/components/VideoGrid.tsx:45)\n    at CategoryScreen..."
 * Output: { componentName: "VideoGrid", filePath: "src/components/VideoGrid.tsx", lineNumber: 45 }
 */
const parseComponentStack = (componentStack?: string): {
  componentName?: string;
  filePath?: string;
  lineNumber?: number;
  fullStack?: string;
} => {
  if (!componentStack) {
    return {};
  }

  // Split by lines and find the first non-React internal component
  const lines = componentStack.split('\n').filter(line => line.trim());
  
  for (const line of lines) {
    // Match patterns like: "at ComponentName (path/to/file.tsx:123)"
    const match = line.match(/at\s+(\w+)\s+\((.+?):(\d+)(?::\d+)?\)/);
    
    if (match) {
      const [, componentName, filePath, lineNumber] = match;
      
      // Skip React internals and common wrappers
      if (!['ErrorBoundary', 'Suspense', 'Fragment'].includes(componentName)) {
        return {
          componentName,
          filePath,
          lineNumber: parseInt(lineNumber, 10),
          fullStack: componentStack,
        };
      }
    }
  }

  // Fallback: return the full stack if we couldn't parse it
  return { fullStack: componentStack };
};

3. Update componentDidCatch to capture enriched data

Replace the current implementation in OwnTube.tv/components/ErrorBoundary.tsx:20-24:

componentDidCatch(error: unknown, info: { componentStack?: string }) {
  this.setState({ ...this.state, error, info });
  
  // Parse component stack to identify the failing component
  const componentInfo = parseComponentStack(info.componentStack);
  
  // Log to console for development debugging
  console.error('ErrorBoundary caught error:', {
    error,
    component: componentInfo.componentName,
    location: componentInfo.filePath 
      ? `${componentInfo.filePath}:${componentInfo.lineNumber}` 
      : 'unknown',
  });
  console.error('Full component stack:', info.componentStack);
  
  // Capture to PostHog with structured metadata
  postHogInstance.captureException(error, {
    errorType: CustomPostHogExceptions.ComponentRenderError,
    componentName: componentInfo.componentName,
    filePath: componentInfo.filePath,
    lineNumber: componentInfo.lineNumber,
    componentStack: componentInfo.fullStack,
    errorMessage: (error as Error)?.message,
    errorName: (error as Error)?.name,
  });
}

4. Add TypeScript type for the error metadata (optional but recommended)

Create a type for consistency:

interface ComponentErrorMetadata {
  errorType: CustomPostHogExceptions.ComponentRenderError;
  componentName?: string;
  filePath?: string;
  lineNumber?: number;
  componentStack?: string;
  errorMessage?: string;
  errorName?: string;
}

Benefits of This Approach

  1. Production debugging: PostHog will show exactly which component is causing crashes
  2. Frequency analysis: You can identify if specific components crash more often
  3. Version tracking: Combined with PostHog's built-in version tracking, you can see if crashes correlate with specific releases
  4. File path tracking: Helps you quickly navigate to the problematic code
  5. Maintains security: Component names and file paths are not sensitive data

Example PostHog Event

After implementation, when a component crashes, PostHog will capture:

{
  "event": "$exception",
  "properties": {
    "errorType": "component_render_error",
    "componentName": "VideoGrid",
    "filePath": "OwnTube.tv/components/VideoGrid/index.tsx",
    "lineNumber": 67,
    "errorMessage": "Cannot read property 'map' of undefined",
    "errorName": "TypeError",
    "componentStack": "    at VideoGrid (src/components/VideoGrid/index.tsx:67)\n    at CategoryScreen..."
  }
}

Testing the Implementation

You can test this by intentionally throwing an error in a component:

// In any component for testing
useEffect(() => {
  if (testErrorBoundary) {
    throw new Error('Test error boundary tracking');
  }
}, [testErrorBoundary]);

Then check PostHog to verify the component name, file path, and line number are captured correctly.


Alternative: Use Error Boundary per Route

For even more granular tracking, you could wrap individual routes/screens with named error boundaries:

const NamedErrorBoundary = ({ name, children }: { name: string; children: ReactNode }) => {
  // Similar implementation but includes `name` in PostHog capture
};

// Then in your routes:
<NamedErrorBoundary name="VideoScreen">
  <VideoScreen />
</NamedErrorBoundary>

This approach would tell you exactly which screen failed, even if the component stack is unclear.


Summary

The key improvements are:

  1. ✅ Add ComponentRenderError to exception types
  2. ✅ Parse componentStack to extract meaningful component info
  3. ✅ Capture structured metadata to PostHog
  4. ✅ Improve console logging for development debugging

Would you like me to implement these changes, or do you have questions about the approach?


Job run: https://github.com/OwnTube-tv/web-client/actions/runs/18336375395
Branch: feature/refresh-token-fix

@mykhailodanilenko mykhailodanilenko merged commit 364c5fc into main Oct 10, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working diagnostics Diagnostics collection and analysis

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants