fix: harden legacy release channel fallback
This commit is contained in:
@@ -71,6 +71,20 @@ describe('SettingsPanel', () => {
|
||||
expect(screen.queryByText(/Beta\/Stable/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('treats a legacy beta release channel as stable', () => {
|
||||
render(
|
||||
<SettingsPanel
|
||||
open={true}
|
||||
settings={{ ...emptySettings, release_channel: 'beta' }}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('settings-release-channel')).toHaveAttribute('data-value', 'stable')
|
||||
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('preserves alpha when alpha is already selected', () => {
|
||||
const alphaSettings: Settings = {
|
||||
...emptySettings,
|
||||
|
||||
@@ -68,6 +68,21 @@ describe('useSettings', () => {
|
||||
expect(mockInvokeFn).toHaveBeenCalledWith('get_settings', {})
|
||||
})
|
||||
|
||||
it('normalizes a legacy beta release channel back to stable on load', async () => {
|
||||
mockSettingsStore = {
|
||||
...savedSettings,
|
||||
release_channel: 'beta',
|
||||
}
|
||||
|
||||
const { result } = renderHook(() => useSettings())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loaded).toBe(true)
|
||||
})
|
||||
|
||||
expect(result.current.settings.release_channel).toBeNull()
|
||||
})
|
||||
|
||||
it('saves settings via backend', async () => {
|
||||
const { result } = renderHook(() => useSettings())
|
||||
|
||||
|
||||
@@ -7,14 +7,16 @@ const mockInitSentry = vi.fn()
|
||||
const mockTeardownSentry = vi.fn()
|
||||
const mockInitPostHog = vi.fn()
|
||||
const mockTeardownPostHog = vi.fn()
|
||||
const mockUpdatePostHogIdentify = vi.fn()
|
||||
const mockSetReleaseChannel = vi.fn()
|
||||
|
||||
vi.mock('../lib/telemetry', () => ({
|
||||
initSentry: (...args: unknown[]) => mockInitSentry(...args),
|
||||
teardownSentry: () => mockTeardownSentry(),
|
||||
initPostHog: (...args: unknown[]) => mockInitPostHog(...args),
|
||||
teardownPostHog: () => mockTeardownPostHog(),
|
||||
updatePostHogIdentify: vi.fn(),
|
||||
setReleaseChannel: vi.fn(),
|
||||
updatePostHogIdentify: (...args: unknown[]) => mockUpdatePostHogIdentify(...args),
|
||||
setReleaseChannel: (...args: unknown[]) => mockSetReleaseChannel(...args),
|
||||
}))
|
||||
|
||||
const baseSettings: Settings = {
|
||||
@@ -23,35 +25,49 @@ const baseSettings: Settings = {
|
||||
analytics_enabled: null, anonymous_id: null, release_channel: null,
|
||||
}
|
||||
|
||||
function renderTelemetry(settings: Settings, loaded = true) {
|
||||
return renderHook(() => useTelemetry(settings, loaded))
|
||||
}
|
||||
|
||||
function analyticsSettings(releaseChannel: string | null = null): Settings {
|
||||
return {
|
||||
...baseSettings,
|
||||
analytics_enabled: true,
|
||||
anonymous_id: 'test-uuid',
|
||||
release_channel: releaseChannel,
|
||||
}
|
||||
}
|
||||
|
||||
describe('useTelemetry', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does nothing when settings are not loaded', () => {
|
||||
renderHook(() => useTelemetry(baseSettings, false))
|
||||
renderTelemetry(baseSettings, false)
|
||||
expect(mockInitSentry).not.toHaveBeenCalled()
|
||||
expect(mockInitPostHog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when consent is not granted', () => {
|
||||
renderHook(() => useTelemetry({ ...baseSettings, telemetry_consent: false }, true))
|
||||
renderTelemetry({ ...baseSettings, telemetry_consent: false })
|
||||
expect(mockInitSentry).not.toHaveBeenCalled()
|
||||
expect(mockInitPostHog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('initializes Sentry when crash reporting is enabled', () => {
|
||||
renderHook(() =>
|
||||
useTelemetry({ ...baseSettings, crash_reporting_enabled: true, anonymous_id: 'test-uuid' }, true)
|
||||
)
|
||||
renderTelemetry({ ...baseSettings, crash_reporting_enabled: true, anonymous_id: 'test-uuid' })
|
||||
expect(mockInitSentry).toHaveBeenCalledWith('test-uuid')
|
||||
})
|
||||
|
||||
it('initializes PostHog when analytics is enabled', () => {
|
||||
renderHook(() =>
|
||||
useTelemetry({ ...baseSettings, analytics_enabled: true, anonymous_id: 'test-uuid' }, true)
|
||||
)
|
||||
expect(mockInitPostHog).toHaveBeenCalledWith('test-uuid', 'stable')
|
||||
it.each([
|
||||
{ name: 'stable defaults', releaseChannel: null, expectedChannel: 'stable' },
|
||||
{ name: 'legacy beta release channels', releaseChannel: 'beta', expectedChannel: 'stable' },
|
||||
])('initializes PostHog for $name', ({ releaseChannel, expectedChannel }) => {
|
||||
renderTelemetry(analyticsSettings(releaseChannel))
|
||||
|
||||
expect(mockInitPostHog).toHaveBeenCalledWith('test-uuid', expectedChannel)
|
||||
expect(mockSetReleaseChannel).toHaveBeenCalledWith(expectedChannel)
|
||||
})
|
||||
|
||||
it('tears down Sentry when crash reporting is disabled after being enabled', () => {
|
||||
@@ -68,9 +84,36 @@ describe('useTelemetry', () => {
|
||||
})
|
||||
|
||||
it('does not initialize without anonymous_id', () => {
|
||||
renderHook(() =>
|
||||
useTelemetry({ ...baseSettings, crash_reporting_enabled: true, anonymous_id: null }, true)
|
||||
)
|
||||
renderTelemetry({ ...baseSettings, crash_reporting_enabled: true, anonymous_id: null })
|
||||
expect(mockInitSentry).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates the analytics identity when the normalized channel changes', () => {
|
||||
const { rerender } = renderHook(
|
||||
({ settings, loaded }) => useTelemetry(settings, loaded),
|
||||
{
|
||||
initialProps: {
|
||||
settings: {
|
||||
...baseSettings,
|
||||
analytics_enabled: true,
|
||||
anonymous_id: 'test-uuid',
|
||||
release_channel: 'alpha',
|
||||
},
|
||||
loaded: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
rerender({
|
||||
settings: {
|
||||
...baseSettings,
|
||||
analytics_enabled: true,
|
||||
anonymous_id: 'test-uuid',
|
||||
release_channel: 'beta',
|
||||
},
|
||||
loaded: true,
|
||||
})
|
||||
|
||||
expect(mockUpdatePostHogIdentify).toHaveBeenCalledWith('stable')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,12 +2,54 @@ import { useEffect, useRef } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import { initSentry, teardownSentry, initPostHog, teardownPostHog, updatePostHogIdentify, setReleaseChannel } from '../lib/telemetry'
|
||||
import { normalizeReleaseChannel, type ReleaseChannel } from '../lib/releaseChannel'
|
||||
import type { Settings } from '../types'
|
||||
|
||||
function tauriCall(command: string): Promise<void> {
|
||||
return isTauri() ? invoke<void>(command) : mockInvoke<void>(command)
|
||||
}
|
||||
|
||||
function resolveReleaseChannel(settings: Settings): ReleaseChannel {
|
||||
return normalizeReleaseChannel(settings.release_channel)
|
||||
}
|
||||
|
||||
function syncCrashReporting(
|
||||
crashEnabled: boolean,
|
||||
anonymousId: string | null,
|
||||
wasEnabled: boolean,
|
||||
): void {
|
||||
if (crashEnabled && anonymousId) {
|
||||
if (!wasEnabled) initSentry(anonymousId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!wasEnabled) return
|
||||
|
||||
teardownSentry()
|
||||
tauriCall('reinit_telemetry').catch(() => {})
|
||||
}
|
||||
|
||||
function syncAnalytics(
|
||||
analyticsEnabled: boolean,
|
||||
anonymousId: string | null,
|
||||
releaseChannel: ReleaseChannel,
|
||||
wasEnabled: boolean,
|
||||
): void {
|
||||
if (!analyticsEnabled) {
|
||||
if (wasEnabled) teardownPostHog()
|
||||
return
|
||||
}
|
||||
|
||||
if (!anonymousId) return
|
||||
|
||||
if (wasEnabled) {
|
||||
updatePostHogIdentify(releaseChannel)
|
||||
return
|
||||
}
|
||||
|
||||
initPostHog(anonymousId, releaseChannel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes / tears down Sentry and PostHog reactively based on user settings.
|
||||
* Call once in App after settings are loaded.
|
||||
@@ -20,25 +62,12 @@ export function useTelemetry(settings: Settings, loaded: boolean): void {
|
||||
if (!loaded) return
|
||||
const crashEnabled = settings.crash_reporting_enabled === true
|
||||
const analyticsEnabled = settings.analytics_enabled === true
|
||||
const id = settings.anonymous_id
|
||||
const anonymousId = settings.anonymous_id
|
||||
const releaseChannel = resolveReleaseChannel(settings)
|
||||
|
||||
if (crashEnabled && id && !prevCrash.current) {
|
||||
initSentry(id)
|
||||
} else if (!crashEnabled && prevCrash.current) {
|
||||
teardownSentry()
|
||||
tauriCall('reinit_telemetry').catch(() => {})
|
||||
}
|
||||
|
||||
const channel = settings.release_channel ?? 'stable'
|
||||
setReleaseChannel(channel)
|
||||
|
||||
if (analyticsEnabled && id && !prevAnalytics.current) {
|
||||
initPostHog(id, channel)
|
||||
} else if (!analyticsEnabled && prevAnalytics.current) {
|
||||
teardownPostHog()
|
||||
} else if (analyticsEnabled && id) {
|
||||
updatePostHogIdentify(channel)
|
||||
}
|
||||
syncCrashReporting(crashEnabled, anonymousId, prevCrash.current)
|
||||
setReleaseChannel(releaseChannel)
|
||||
syncAnalytics(analyticsEnabled, anonymousId, releaseChannel, prevAnalytics.current)
|
||||
|
||||
prevCrash.current = crashEnabled
|
||||
prevAnalytics.current = analyticsEnabled
|
||||
|
||||
Reference in New Issue
Block a user