Skip to content
Open
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## Unreleased

### Features

#### Apple

- feat(apple): Dynamically fetch latest Sentry Cocoa SDK version for SPM instead of hardcoding
- feat(apple): Add `--fix` flag for diagnosing and repairing Apple integrations
- fix(apple): Remove `.experimental` namespace from `enableLogs` option in code snippets
- ref(apple): Add CI/CD guidance and `SENTRY_SKIP_DSYM_UPLOAD` option for dSYM upload build phase
- ref(apple): Add info message about expected Sentry.framework dSYM warning in App Store uploads

## 6.12.0

### Features
Expand Down
6 changes: 6 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ const argv = yargs(hideBin(process.argv), process.cwd())
describe: 'Ignore git changes in the project',
type: 'boolean',
},
fix: {
default: false,
describe:
'Run diagnostic checks and fix issues with your Sentry integration',
type: 'boolean',
},
spotlight: {
default: false,
describe:
Expand Down
13 changes: 13 additions & 0 deletions src/apple/apple-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
printWelcome,
} from '../utils/clack';
import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config';
import { fetchSdkVersion } from '../utils/release-registry';
import { checkInstalledCLI } from './check-installed-cli';
import { configureFastlane } from './configure-fastlane';
import { configurePackageManager } from './configure-package-manager';
Expand Down Expand Up @@ -81,12 +82,18 @@ async function runAppleWizardWithTelementry(
projectDir,
});

// Step - Fetch latest SDK version for SPM
const sdkVersion = shouldUseSPM
? await fetchSdkVersion('sentry.cocoa')
: undefined;

// Step - Configure Xcode Project
configureXcodeProject({
xcProject,
project: selectedProject,
target,
shouldUseSPM,
sdkVersion,
});

// Step - Feature Selection
Expand Down Expand Up @@ -121,6 +128,12 @@ async function runAppleWizardWithTelementry(
selectedProject.slug,
);

clack.log.info(
`When uploading to the App Store, you may see a warning about missing dSYMs for ${chalk.cyan(
'Sentry.framework',
)}. This is expected for pre-compiled SPM frameworks and does not affect Sentry's crash reporting.`,
);

clack.log.success(
'Sentry was successfully added to your project! Run your project to send your first event to Sentry. Go to Sentry.io to see whether everything is working fine.',
);
Expand Down
8 changes: 6 additions & 2 deletions src/apple/configure-sentry-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ export function configureSentryCLI({
`Created a ${chalk.cyan(
'.sentryclirc',
)} file in your project directory to provide an auth token for Sentry CLI.

It was also added to your ${chalk.cyan('.gitignore')} file.
Set the ${chalk.cyan(
'SENTRY_AUTH_TOKEN',
)} environment variable in your CI environment. See https://docs.sentry.io/cli/configuration/#auth-token for more information.`,
)} environment variable in your CI environment to upload debug symbols during App Store builds. See https://docs.sentry.io/cli/configuration/#auth-token for more information.

Set ${chalk.cyan(
'SENTRY_SKIP_DSYM_UPLOAD=1',
)} in Xcode build settings to skip uploads for specific configurations (e.g., Debug).`,
);
Sentry.setTag('sentry-cli-configured', true);
debug(`Sentry CLI configured: ${chalk.cyan(true.toString())}`);
Expand Down
10 changes: 9 additions & 1 deletion src/apple/configure-xcode-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ export function configureXcodeProject({
project,
target,
shouldUseSPM,
sdkVersion,
}: {
xcProject: XcodeProject;
project: SentryProjectData;
target: string;
shouldUseSPM: boolean;
sdkVersion?: string;
}) {
traceStep('Update Xcode project', () => {
xcProject.updateXcodeProject(project, target, shouldUseSPM, true);
xcProject.updateXcodeProject(
project,
target,
shouldUseSPM,
true,
sdkVersion,
);
});
}
95 changes: 95 additions & 0 deletions src/apple/doctor/apple-doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';

import { withTelemetry } from '../../telemetry';
import { abortIfCancelled, printWelcome } from '../../utils/clack';
import { lookupXcodeProject } from '../lookup-xcode-project';
import type { AppleWizardOptions } from '../options';
import { checkBuildPhase } from './checks/check-build-phase';
import { checkCodeInit } from './checks/check-code-init';
import { checkSdkVersion } from './checks/check-sdk-version';
import { checkSentryCli } from './checks/check-sentry-cli';
import { checkSentryCliRc } from './checks/check-sentryclirc';
import type { DiagnosticResult } from './types';

export async function runAppleDoctorWizard(
options: AppleWizardOptions,
): Promise<void> {
return withTelemetry(
{
enabled: options.telemetryEnabled,
integration: 'ios',
wizardOptions: options,
},
() => runAppleDoctorWithTelemetry(options),
);
}

async function runAppleDoctorWithTelemetry(
options: AppleWizardOptions,
): Promise<void> {
const projectDir = options.projectDir ?? process.cwd();

printWelcome({ wizardName: 'Sentry Apple Doctor' });

const { xcProject, target } = await lookupXcodeProject({ projectDir });

clack.log.info('Running diagnostic checks...\n');

const results: DiagnosticResult[] = [
checkSentryCli(),
checkSentryCliRc({ projectDir }),
await checkSdkVersion({ xcProject }),
checkBuildPhase({ xcProject, target }),
checkCodeInit({ xcProject, target }),
];

let hasFailures = false;
let hasFixable = false;

for (const result of results) {
if (result.status === 'pass') {
clack.log.success(
`${chalk.green('PASS')} ${result.name}: ${result.message}`,
);
} else if (result.status === 'warn') {
clack.log.warn(
`${chalk.yellow('WARN')} ${result.name}: ${result.message}`,
);
} else {
clack.log.error(`${chalk.red('FAIL')} ${result.name}: ${result.message}`);
hasFailures = true;
}

if (result.fixAvailable && result.status !== 'pass') {
hasFixable = true;
}
}

if (hasFixable) {
const shouldFix = await abortIfCancelled(
clack.confirm({
message: 'Would you like to attempt to fix the issues found?',
}),
);

if (shouldFix) {
for (const result of results) {
if (result.fixAvailable && result.status !== 'pass' && result.fix) {
clack.log.step(`Fixing: ${result.name}...`);
const fixed = await result.fix();
if (fixed) {
clack.log.success(`Fixed: ${result.name}`);
} else {
clack.log.warn(`Could not automatically fix: ${result.name}`);
}
}
}
}
} else if (!hasFailures) {
clack.log.success(
'All checks passed! Your Sentry integration looks healthy.',
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warnings still reported as fully healthy

Medium Severity

The final summary prints All checks passed whenever there are no fail results, even if earlier checks returned warn. This makes runAppleDoctorWizard report a healthy integration after warning findings, which can hide real configuration problems from users.

Fix in Cursor Fix in Web

}
}
80 changes: 80 additions & 0 deletions src/apple/doctor/checks/check-build-phase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { PBXNativeTarget, PBXShellScriptBuildPhase } from 'xcode';
import type { XcodeProject } from '../../xcode-manager';
import type { DiagnosticResult } from '../types';

export function checkBuildPhase({
xcProject,
target,
}: {
xcProject: XcodeProject;
target: string;
}): DiagnosticResult {
const xcObjects = xcProject.objects;

const targetKey = Object.keys(xcObjects.PBXNativeTarget ?? {}).find((key) => {
const value = xcObjects.PBXNativeTarget?.[key];
return (
!key.endsWith('_comment') &&
typeof value !== 'string' &&
value?.name === target
);
});

if (!targetKey) {
return {
name: 'dSYM Upload Build Phase',
status: 'fail',
message: `Target "${target}" not found in project.`,
fixAvailable: false,
};
}

const nativeTarget = xcObjects.PBXNativeTarget?.[
targetKey
] as PBXNativeTarget;

let sentryBuildPhase: PBXShellScriptBuildPhase | undefined;
for (const phase of nativeTarget.buildPhases ?? []) {
const bp = xcObjects.PBXShellScriptBuildPhase?.[phase.value];
if (typeof bp !== 'string' && bp?.shellScript?.includes('sentry-cli')) {
sentryBuildPhase = bp;
break;
}
}

if (!sentryBuildPhase) {
return {
name: 'dSYM Upload Build Phase',
status: 'fail',
message:
'No Sentry dSYM upload build phase found in target. Re-run the wizard to add it.',
fixAvailable: false,
};
}

const issues: string[] = [];
const script = sentryBuildPhase.shellScript ?? '';

if (!script.includes('SENTRY_ORG')) {
issues.push('Missing SENTRY_ORG');
}
if (!script.includes('SENTRY_PROJECT')) {
issues.push('Missing SENTRY_PROJECT');
}

if (issues.length === 0) {
return {
name: 'dSYM Upload Build Phase',
status: 'pass',
message: 'Sentry dSYM upload build phase is correctly configured.',
fixAvailable: false,
};
}

return {
name: 'dSYM Upload Build Phase',
status: 'warn',
message: `Issues found: ${issues.join('; ')}`,
fixAvailable: false,
};
}
60 changes: 60 additions & 0 deletions src/apple/doctor/checks/check-code-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as fs from 'node:fs';
import type { XcodeProject } from '../../xcode-manager';
import type { DiagnosticResult } from '../types';

export function checkCodeInit({
xcProject,
target,
}: {
xcProject: XcodeProject;
target: string;
}): DiagnosticResult {
const files = xcProject.getSourceFilesForTarget(target);

if (!files || files.length === 0) {
return {
name: 'Sentry Initialization Code',
status: 'warn',
message:
'Could not resolve source files for the target to check for initialization code.',
fixAvailable: false,
};
}

for (const filePath of files) {
if (!fs.existsSync(filePath)) continue;

let content: string;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch {
continue;
}

// Check for active (non-commented) Sentry initialization
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
if (
trimmed.includes('SentrySDK.start') ||
trimmed.includes('[SentrySDK start')
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Commented init code can be misdetected

Low Severity

checkCodeInit only skips lines starting with // or /*, so lines inside block comments (for example starting with *) are still scanned. A commented SentrySDK.start can be treated as active code, causing a false pass result.

Fix in Cursor Fix in Web

return {
name: 'Sentry Initialization Code',
status: 'pass',
message: `Sentry initialization found in ${filePath}.`,
fixAvailable: false,
};
}
}
}

return {
name: 'Sentry Initialization Code',
status: 'fail',
message:
'No active Sentry initialization code found in source files. SDK will not start.',
fixAvailable: false,
};
}
Loading
Loading