Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/lib/programs/__tests__/source-maps-detect-agentic.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -34,6 +36,40 @@ describe('SOURCE_MAPS_TARGETS precedence', () => {
});
});

describe('isNativePlatform', () => {
const project = (over: Partial<DetectedProject>): 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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion src/lib/programs/error-tracking-upload-source-maps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
30 changes: 26 additions & 4 deletions src/ui/tui/screens/SourceMapsDetectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -123,6 +125,13 @@ export const SourceMapsDetectScreen = ({
</Text>
<Text dimColor>{state.message}</Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text>
This is usually transient — re-run the command to try again. To set
up source-map upload by hand, follow:
</Text>
<Text color={Colors.primary}>{SOURCE_MAPS_DOCS_URL}</Text>
</Box>
<PickerMenu
options={[{ label: 'Exit', value: EXIT }]}
onSelect={() => process.exit(1)}
Expand All @@ -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 (
<Box flexDirection="column">
<Box flexDirection="column" marginBottom={1}>
<Text color={Colors.error} bold>
{Icons.squareFilled} Nothing to instrument yet
<Text color={Colors.accent} bold>
{Icons.warning} Nothing to wire up automatically
</Text>
<Text dimColor>
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" : ''}.
</Text>
</Box>
<BlockedSummary blocked={blocked} />
<Box flexDirection="column" marginTop={1}>
<Text>
You can still upload source maps manually
{hasNative
? ' — the docs cover React Native, iOS, Android and Flutter too'
: ''}
:
</Text>
<Text color={Colors.primary}>{SOURCE_MAPS_DOCS_URL}</Text>
</Box>
<Box marginTop={1}>
<PickerMenu
options={[{ label: 'Exit', value: EXIT }]}
Expand Down
Loading