add global error handling and enhance error tracking capabilities
This commit is contained in:
@@ -7,7 +7,10 @@ import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
|
||||
import NavBar from '@/views/components/NavBar'
|
||||
|
||||
import { initialize as initializeAnalytics } from './analytics'
|
||||
import {
|
||||
initialize as initializeAnalytics,
|
||||
installGlobalErrorHandlers,
|
||||
} from './analytics'
|
||||
import useAnalyticsIdentity from './analytics/useAnalyticsIdentity'
|
||||
import { registerCapacitorListeners } from './CapacitorListener'
|
||||
import PageTransition from './components/animations/PageTransition'
|
||||
@@ -147,6 +150,7 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
initializeAnalytics()
|
||||
installGlobalErrorHandlers()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
@@ -85,6 +85,12 @@ export const ERROR_SCHEMAS = {
|
||||
error_code: 'string',
|
||||
operation: 'string',
|
||||
},
|
||||
// No message/stack field here by design — those come from the real Error
|
||||
// object passed to posthog.captureException() itself, not from this
|
||||
// sanitized properties bag. This schema only classifies how it was caught.
|
||||
frontend_error: {
|
||||
source: 'enum:window_error,unhandled_rejection',
|
||||
},
|
||||
}
|
||||
|
||||
const MAX_STRING_LENGTH = 200
|
||||
|
||||
@@ -142,6 +142,9 @@ export const track = (eventName, properties = {}) => {
|
||||
posthog.capture(eventName, sanitized)
|
||||
}
|
||||
|
||||
/** Backend/API failures: a normal sanitized event, same as track() — not
|
||||
* PostHog's Error Tracking product. There's no real Error object here (just
|
||||
* an HTTP response), so there's no stack trace to gain from captureException. */
|
||||
export const captureError = (errorType, properties = {}) => {
|
||||
if (!canSend('crash')) return
|
||||
const posthog = getClientSync()
|
||||
@@ -150,15 +153,56 @@ export const captureError = (errorType, properties = {}) => {
|
||||
const sanitized = sanitizeErrorProperties(errorType, properties)
|
||||
if (!sanitized) return
|
||||
|
||||
// captureException (not capture) so this lands on PostHog's Error Tracking
|
||||
// page, grouped by errorType — the message is deliberately generic, since
|
||||
// any per-instance detail must go through the sanitized allowlist above,
|
||||
// never straight into the exception message.
|
||||
const error = new Error(errorType)
|
||||
error.name = errorType
|
||||
posthog.capture(errorType, sanitized)
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend crashes only. Uses captureException (not capture) so these land
|
||||
* on PostHog's Error Tracking page with a genuine message + stack trace —
|
||||
* that text is NOT filtered by the sanitized allowlist below, since it comes
|
||||
* from the exception object itself, not from `properties`.
|
||||
*/
|
||||
export const captureException = (error, properties = {}) => {
|
||||
if (!canSend('crash')) return
|
||||
const posthog = getClientSync()
|
||||
if (!posthog) return
|
||||
|
||||
const sanitized = sanitizeErrorProperties('frontend_error', properties)
|
||||
if (!sanitized) return
|
||||
|
||||
posthog.captureException(error, sanitized)
|
||||
}
|
||||
|
||||
let globalHandlersInstalled = false
|
||||
|
||||
/** Reports uncaught exceptions and unhandled promise rejections to
|
||||
* PostHog's Error Tracking, gated by the same crash consent as api_error.
|
||||
* Complements, doesn't overlap with, src/views/Error.jsx: that's a React
|
||||
* Router error-boundary screen for render/loader errors, which React catches
|
||||
* before they ever reach window.onerror — a different class of failure, with
|
||||
* its own user-initiated "Report this problem" flow via ErrorReportService.
|
||||
* These listeners only see what bypasses React's boundaries entirely (event
|
||||
* handlers, timers, unhandled promise rejections). Safe to call multiple
|
||||
* times; only installs once. */
|
||||
export const installGlobalErrorHandlers = () => {
|
||||
if (globalHandlersInstalled || typeof window === 'undefined') return
|
||||
globalHandlersInstalled = true
|
||||
|
||||
window.addEventListener('error', event => {
|
||||
const error =
|
||||
event?.error instanceof Error
|
||||
? event.error
|
||||
: new Error(event?.message || 'Unknown window error')
|
||||
captureException(error, { source: 'window_error' })
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
const reason = event?.reason
|
||||
const error = reason instanceof Error ? reason : new Error(String(reason))
|
||||
captureException(error, { source: 'unhandled_rejection' })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* kind: 'analytics' | 'crash'. Enabling analytics (re-)initializes PostHog
|
||||
* if needed and sends analytics_enabled; enabling crash-only never talks to
|
||||
|
||||
@@ -56,11 +56,9 @@ const SignupView = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// The "how did you hear about us" step (/heard-about) is
|
||||
// temporarily skipped; new accounts go straight to circle setup.
|
||||
// Re-enable by navigating to '/heard-about' again — that view
|
||||
// already forwards to '/circle-setup' when done.
|
||||
Navigate('/circle-setup', { replace: true })
|
||||
// "How did you hear about us" (cloud) / privacy preferences (self-hosted)
|
||||
// that view forwards to '/circle-setup' once the user answers.
|
||||
Navigate('/heard-about', { replace: true })
|
||||
}
|
||||
const handleSignUpValidation = () => {
|
||||
// Reset errors before validation
|
||||
|
||||
@@ -353,6 +353,7 @@ const MyChores = () => {
|
||||
membersData?.res &&
|
||||
choresData?.res
|
||||
) {
|
||||
// throw new Error('FAKE ERROR') // For testing Sentry error tracking
|
||||
const processEffectAsync = async () => {
|
||||
// Sync local state with query data to ensure updates are reflected
|
||||
setChores(processedChores)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Button, Input, Link, Switch, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { track } from '../../analytics'
|
||||
import { isOfficialDonetickInstance } from '../../utils/FeatureToggle'
|
||||
import {
|
||||
haptic,
|
||||
@@ -79,7 +80,13 @@ const AcquisitionSurvey = ({ onDone }) => {
|
||||
|
||||
const finish = () => {
|
||||
const answer = selected === OTHER ? detail.trim() || null : selected
|
||||
if (answer) recordAcquisitionSource(answer)
|
||||
if (answer) {
|
||||
recordAcquisitionSource(answer)
|
||||
track('onboarding_option_selected', {
|
||||
step: 'heard_about',
|
||||
option: answer,
|
||||
})
|
||||
}
|
||||
onDone()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Logo from '../../Logo'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { haptic } from '../../utils/Onboarding'
|
||||
import { authButtonSx } from '../Authorization/authStyles'
|
||||
import { isOfficialDonetickInstance } from '../../utils/FeatureToggle'
|
||||
|
||||
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'
|
||||
|
||||
@@ -39,7 +40,8 @@ const WorkspaceReadyView = () => {
|
||||
* and a missing offering or a store hiccup must never trap a new user here.
|
||||
*/
|
||||
const showPaywall = async () => {
|
||||
if (!Capacitor.isNativePlatform() || !userProfile?.id) return
|
||||
const isDonetickDotCom = await isOfficialDonetickInstance()
|
||||
if (!isDonetickDotCom || !Capacitor.isNativePlatform() || !userProfile?.id) return
|
||||
|
||||
const { Purchases } = await import('@revenuecat/purchases-capacitor')
|
||||
const { RevenueCatUI } = await import('@revenuecat/purchases-capacitor-ui')
|
||||
|
||||
Reference in New Issue
Block a user