diff --git a/src/lib/programs/__tests__/source-maps-detect-agentic.test.ts b/src/lib/programs/__tests__/source-maps-detect-agentic.test.ts index a13a16dd..9e4238e9 100644 --- a/src/lib/programs/__tests__/source-maps-detect-agentic.test.ts +++ b/src/lib/programs/__tests__/source-maps-detect-agentic.test.ts @@ -1,6 +1,8 @@ import { coerceReport, + isNativePlatform, SOURCE_MAPS_TARGETS, + type DetectedProject, } from '@lib/programs/error-tracking-upload-source-maps/detect-agentic'; import { AUTOMATABLE_VARIANTS } from '@lib/programs/error-tracking-upload-source-maps/detect'; @@ -34,6 +36,40 @@ describe('SOURCE_MAPS_TARGETS precedence', () => { }); }); +describe('isNativePlatform', () => { + const project = (over: Partial): DetectedProject => ({ + path: '.', + framework: 'Unknown', + variant: null, + hasPostHog: true, + instrumentable: false, + ...over, + }); + + it('flags native/mobile stacks the agent reports', () => { + for (const framework of [ + 'React Native', + 'react-native', + 'Expo', + 'Flutter', + 'iOS', + 'Android (Kotlin)', + 'Swift', + ]) { + expect(isNativePlatform(project({ framework }))).toBe(true); + } + }); + + it('does not flag a supported web stack', () => { + // A variant means it's automatable — never route it to the manual path. + expect( + isNativePlatform(project({ framework: 'Next.js', variant: 'nextjs' })), + ).toBe(false); + // Unsupported but non-native (e.g. a backend language) stays generic. + expect(isNativePlatform(project({ framework: 'Rust' }))).toBe(false); + }); +}); + describe('coerceReport', () => { it('marks a supported project with a PostHog SDK as instrumentable', () => { const report = coerceReport({ diff --git a/src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts b/src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts index e5aff10e..9c5e3685 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts @@ -98,6 +98,25 @@ function classify( return { instrumentable: true }; } +/** + * Native / mobile stacks the wizard can't automate yet (no automatable skill + * variant, so `variant` is null) but which *do* have a documented manual + * source-map upload path. Used only to tailor the dead-end guidance on the + * detect screen — matched against the agent's free-text `framework` label. + */ +const NATIVE_FRAMEWORK_RE = + /react[\s-]?native|expo|flutter|\bios\b|android|swift|kotlin|hermes/i; + +/** + * True when a blocked project is a native/mobile stack (React Native, iOS, + * Android, Flutter). These have no automatable variant but the docs cover + * manual source-map / symbol upload, so the screen points the user there + * instead of treating it as a flat dead end. + */ +export function isNativePlatform(project: DetectedProject): boolean { + return project.variant == null && NATIVE_FRAMEWORK_RE.test(project.framework); +} + /** Map a generic detection report into source-maps projects. */ function toSourceMapsReport(report: AgenticDetectionReport): DetectionReport { return { diff --git a/src/lib/programs/error-tracking-upload-source-maps/index.ts b/src/lib/programs/error-tracking-upload-source-maps/index.ts index 57c657bc..6253e6b1 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/index.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/index.ts @@ -17,7 +17,12 @@ import { getUiHostFromHost } from '@utils/urls'; import { getUI } from '@ui'; const REPORT_FILE = 'posthog-source-maps-report.md'; -const DOCS_URL = 'https://posthog.com/docs/error-tracking/upload-source-maps'; +/** Manual source-map upload guide. Shown in the outro and on the + * detect-screen dead ends so an unsupported/native stack still has a path + * forward instead of an exit. */ +export const SOURCE_MAPS_DOCS_URL = + 'https://posthog.com/docs/error-tracking/upload-source-maps'; +const DOCS_URL = SOURCE_MAPS_DOCS_URL; export const errorTrackingUploadSourceMapsConfig: ProgramConfig = { command: 'upload-source-maps', diff --git a/src/ui/tui/screens/SourceMapsDetectScreen.tsx b/src/ui/tui/screens/SourceMapsDetectScreen.tsx index 8dd0e3e6..96e6c730 100644 --- a/src/ui/tui/screens/SourceMapsDetectScreen.tsx +++ b/src/ui/tui/screens/SourceMapsDetectScreen.tsx @@ -14,10 +14,12 @@ import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; import { SOURCE_MAPS_CONTEXT_KEYS, + SOURCE_MAPS_DOCS_URL, VARIANT_DISPLAY_NAME, } from '@lib/programs/error-tracking-upload-source-maps/index'; import { detectSourceMapsProjects, + isNativePlatform, type DetectedProject, type DetectionReport, } from '@lib/programs/error-tracking-upload-source-maps/detect-agentic'; @@ -123,6 +125,13 @@ export const SourceMapsDetectScreen = ({ {state.message} + + + This is usually transient — re-run the command to try again. To set + up source-map upload by hand, follow: + + {SOURCE_MAPS_DOCS_URL} + process.exit(1)} @@ -136,18 +145,31 @@ export const SourceMapsDetectScreen = ({ const blocked = report.projects.filter((p) => !p.instrumentable); if (instrumentable.length === 0) { + const hasNative = blocked.some(isNativePlatform); return ( - - {Icons.squareFilled} Nothing to instrument yet + + {Icons.warning} Nothing to wire up automatically - None of the {report.projects.length} projects found can have - source-map upload set up. + The wizard couldn't set up source-map upload for any of the{' '} + {report.projects.length} project + {report.projects.length === 1 ? '' : 's'} it found + {hasNative ? " — native apps aren't automated yet" : ''}. + + + You can still upload source maps manually + {hasNative + ? ' — the docs cover React Native, iOS, Android and Flutter too' + : ''} + : + + {SOURCE_MAPS_DOCS_URL} +