From 2315a1d6bb79686dc191df01169375491914c491 Mon Sep 17 00:00:00 2001 From: Filipe Alvarenga Date: Tue, 31 Mar 2026 18:10:24 -0300 Subject: [PATCH] fix(apple): Dynamic SDK version, enableLogs fix, dSYM guidance, and doctor command The Apple wizard had several issues affecting iOS users: - SPM dependency was hardcoded to minimumVersion 8.0.0 with upToNextMajorVersion, meaning it would never resolve to 9.x. Now fetches the latest version from the Sentry release registry (sentry.cocoa) and falls back to 8.0.0 if the fetch fails. - The enableLogs code snippet used options.experimental.enableLogs which no longer exists in sentry-cocoa v9. Removed the .experimental namespace so the generated code compiles correctly. - The dSYM upload build phase script had no way to skip uploads and lacked CI/CD auth guidance. Added SENTRY_SKIP_DSYM_UPLOAD env var support and a note about setting SENTRY_AUTH_TOKEN in CI. - Added an info message about the expected Sentry.framework dSYM warning in App Store uploads (normal for pre-compiled SPM frameworks). - There was no way to diagnose or fix an existing integration. Added a --fix flag that runs five diagnostic checks (sentry-cli, auth token, SDK version, build phase, init code) and offers to auto-fix what it can. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 12 +++ bin.ts | 6 ++ src/apple/apple-wizard.ts | 13 +++ src/apple/configure-sentry-cli.ts | 8 +- src/apple/configure-xcode-project.ts | 10 ++- src/apple/doctor/apple-doctor.ts | 95 ++++++++++++++++++++ src/apple/doctor/checks/check-build-phase.ts | 80 +++++++++++++++++ src/apple/doctor/checks/check-code-init.ts | 60 +++++++++++++ src/apple/doctor/checks/check-sdk-version.ts | 82 +++++++++++++++++ src/apple/doctor/checks/check-sentry-cli.ts | 30 +++++++ src/apple/doctor/checks/check-sentryclirc.ts | 53 +++++++++++ src/apple/doctor/types.ts | 9 ++ src/apple/templates.ts | 12 ++- src/apple/xcode-manager.ts | 21 ++++- src/run.ts | 18 +++- src/utils/types.ts | 6 ++ test/apple/code-tools.test.ts | 8 +- test/apple/templates.test.ts | 24 ++++- test/apple/xcode-manager.test.ts | 79 ++++++++++++++++ 19 files changed, 604 insertions(+), 22 deletions(-) create mode 100644 src/apple/doctor/apple-doctor.ts create mode 100644 src/apple/doctor/checks/check-build-phase.ts create mode 100644 src/apple/doctor/checks/check-code-init.ts create mode 100644 src/apple/doctor/checks/check-sdk-version.ts create mode 100644 src/apple/doctor/checks/check-sentry-cli.ts create mode 100644 src/apple/doctor/checks/check-sentryclirc.ts create mode 100644 src/apple/doctor/types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e3d8415..c465df6a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bin.ts b/bin.ts index 6692c1e47..d66763fb6 100644 --- a/bin.ts +++ b/bin.ts @@ -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: diff --git a/src/apple/apple-wizard.ts b/src/apple/apple-wizard.ts index 69e5c6ba6..0a599feae 100644 --- a/src/apple/apple-wizard.ts +++ b/src/apple/apple-wizard.ts @@ -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'; @@ -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 @@ -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.', ); diff --git a/src/apple/configure-sentry-cli.ts b/src/apple/configure-sentry-cli.ts index a5e5af747..75326bf71 100644 --- a/src/apple/configure-sentry-cli.ts +++ b/src/apple/configure-sentry-cli.ts @@ -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())}`); diff --git a/src/apple/configure-xcode-project.ts b/src/apple/configure-xcode-project.ts index ee596aebc..f99685b70 100644 --- a/src/apple/configure-xcode-project.ts +++ b/src/apple/configure-xcode-project.ts @@ -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, + ); }); } diff --git a/src/apple/doctor/apple-doctor.ts b/src/apple/doctor/apple-doctor.ts new file mode 100644 index 000000000..8b593e2fc --- /dev/null +++ b/src/apple/doctor/apple-doctor.ts @@ -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 { + return withTelemetry( + { + enabled: options.telemetryEnabled, + integration: 'ios', + wizardOptions: options, + }, + () => runAppleDoctorWithTelemetry(options), + ); +} + +async function runAppleDoctorWithTelemetry( + options: AppleWizardOptions, +): Promise { + 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.', + ); + } +} diff --git a/src/apple/doctor/checks/check-build-phase.ts b/src/apple/doctor/checks/check-build-phase.ts new file mode 100644 index 000000000..08d312e62 --- /dev/null +++ b/src/apple/doctor/checks/check-build-phase.ts @@ -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, + }; +} diff --git a/src/apple/doctor/checks/check-code-init.ts b/src/apple/doctor/checks/check-code-init.ts new file mode 100644 index 000000000..873d12492 --- /dev/null +++ b/src/apple/doctor/checks/check-code-init.ts @@ -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') + ) { + 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, + }; +} diff --git a/src/apple/doctor/checks/check-sdk-version.ts b/src/apple/doctor/checks/check-sdk-version.ts new file mode 100644 index 000000000..5f8d4d990 --- /dev/null +++ b/src/apple/doctor/checks/check-sdk-version.ts @@ -0,0 +1,82 @@ +import * as fs from 'node:fs'; +import { fetchSdkVersion } from '../../../utils/release-registry'; +import type { XcodeProject } from '../../xcode-manager'; +import type { DiagnosticResult } from '../types'; + +interface SpmPackageRef { + repositoryURL?: string; + requirement?: { minimumVersion: string }; +} + +export async function checkSdkVersion({ + xcProject, +}: { + xcProject: XcodeProject; +}): Promise { + const refs = xcProject.objects.XCRemoteSwiftPackageReference ?? {}; + let currentMinVersion: string | undefined; + let packageRefKey: string | undefined; + + for (const [key, value] of Object.entries(refs)) { + if (key.endsWith('_comment') || typeof value === 'string') continue; + const ref = value as SpmPackageRef; + if (ref.repositoryURL?.includes('sentry-cocoa')) { + currentMinVersion = ref.requirement?.minimumVersion; + packageRefKey = key; + break; + } + } + + if (!currentMinVersion) { + return { + name: 'SDK Version (SPM)', + status: 'warn', + message: + 'No Sentry SPM package reference found. SDK may be installed via CocoaPods or not installed.', + fixAvailable: false, + }; + } + + const latestVersion = await fetchSdkVersion('sentry.cocoa'); + if (!latestVersion) { + return { + name: 'SDK Version (SPM)', + status: 'warn', + message: `Current minimum version: ${currentMinVersion}. Could not fetch latest version to compare.`, + fixAvailable: false, + }; + } + + const currentMajor = parseInt(currentMinVersion.split('.')[0], 10); + const latestMajor = parseInt(latestVersion.split('.')[0], 10); + + if (currentMajor >= latestMajor) { + return { + name: 'SDK Version (SPM)', + status: 'pass', + message: `Minimum version ${currentMinVersion} is on the latest major (${latestMajor}.x).`, + fixAvailable: false, + }; + } + + return { + name: 'SDK Version (SPM)', + status: 'fail', + message: `Minimum version is ${currentMinVersion} but latest major is ${latestMajor}.x. Consider updating to ${latestMajor}.0.0.`, + fixAvailable: true, + fix: () => { + if (packageRefKey && xcProject.objects.XCRemoteSwiftPackageReference) { + const ref = xcProject.objects.XCRemoteSwiftPackageReference[ + packageRefKey + ] as SpmPackageRef | string | undefined; + if (typeof ref !== 'string' && ref?.requirement) { + ref.requirement.minimumVersion = `${latestMajor}.0.0`; + const newContent = xcProject.project.writeSync(); + fs.writeFileSync(xcProject.pbxprojPath, newContent); + return Promise.resolve(true); + } + } + return Promise.resolve(false); + }, + }; +} diff --git a/src/apple/doctor/checks/check-sentry-cli.ts b/src/apple/doctor/checks/check-sentry-cli.ts new file mode 100644 index 000000000..22b9d3dea --- /dev/null +++ b/src/apple/doctor/checks/check-sentry-cli.ts @@ -0,0 +1,30 @@ +import * as bash from '../../../utils/bash'; +import type { DiagnosticResult } from '../types'; + +export function checkSentryCli(): DiagnosticResult { + const hasCli = bash.hasSentryCLI(); + + if (hasCli) { + return { + name: 'sentry-cli', + status: 'pass', + message: 'sentry-cli is installed and available.', + fixAvailable: false, + }; + } + + return { + name: 'sentry-cli', + status: 'fail', + message: 'sentry-cli is not installed. dSYM uploads will fail.', + fixAvailable: true, + fix: async () => { + try { + await bash.installSentryCLI(); + return true; + } catch { + return false; + } + }, + }; +} diff --git a/src/apple/doctor/checks/check-sentryclirc.ts b/src/apple/doctor/checks/check-sentryclirc.ts new file mode 100644 index 000000000..31c841bdc --- /dev/null +++ b/src/apple/doctor/checks/check-sentryclirc.ts @@ -0,0 +1,53 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { DiagnosticResult } from '../types'; + +export function checkSentryCliRc({ + projectDir, +}: { + projectDir: string; +}): DiagnosticResult { + const rcPath = path.join(projectDir, '.sentryclirc'); + + if (!fs.existsSync(rcPath)) { + if (process.env.SENTRY_AUTH_TOKEN) { + return { + name: '.sentryclirc / Auth Token', + status: 'pass', + message: + 'No .sentryclirc found, but SENTRY_AUTH_TOKEN environment variable is set.', + fixAvailable: false, + }; + } + + return { + name: '.sentryclirc / Auth Token', + status: 'fail', + message: + 'No .sentryclirc found and SENTRY_AUTH_TOKEN is not set. dSYM uploads will fail.', + fixAvailable: false, + }; + } + + const content = fs.readFileSync(rcPath, 'utf8'); + if ( + !content.includes('token=') || + content.includes('token=\n') || + content.includes('token=_YOUR') + ) { + return { + name: '.sentryclirc / Auth Token', + status: 'warn', + message: + '.sentryclirc exists but appears to have an invalid or placeholder auth token.', + fixAvailable: false, + }; + } + + return { + name: '.sentryclirc / Auth Token', + status: 'pass', + message: '.sentryclirc exists and contains an auth token.', + fixAvailable: false, + }; +} diff --git a/src/apple/doctor/types.ts b/src/apple/doctor/types.ts new file mode 100644 index 000000000..c62890c94 --- /dev/null +++ b/src/apple/doctor/types.ts @@ -0,0 +1,9 @@ +export type DiagnosticStatus = 'pass' | 'warn' | 'fail'; + +export interface DiagnosticResult { + name: string; + status: DiagnosticStatus; + message: string; + fixAvailable: boolean; + fix?: () => Promise; +} diff --git a/src/apple/templates.ts b/src/apple/templates.ts index 7c22e5b58..b181586dc 100644 --- a/src/apple/templates.ts +++ b/src/apple/templates.ts @@ -13,6 +13,10 @@ fi ` : ''; return `# This script is responsible for uploading debug symbols and source context for Sentry.${includeHomebrew} +if [ "$SENTRY_SKIP_DSYM_UPLOAD" = "1" ]; then + echo "SENTRY_SKIP_DSYM_UPLOAD is set, skipping dSYM upload." + exit 0 +fi if which sentry-cli >/dev/null; then export SENTRY_ORG=${orgSlug} export SENTRY_PROJECT=${projectSlug} @@ -56,8 +60,8 @@ export function getSwiftSnippet(dsn: string, enableLogs: boolean): string { if (enableLogs) { snippet += ` - // Enable experimental logging features - options.experimental.enableLogs = true`; + // Enable logging features + options.enableLogs = true`; } snippet += ` @@ -93,8 +97,8 @@ export function getObjcSnippet(dsn: string, enableLogs: boolean): string { if (enableLogs) { snippet += ` - // Enable experimental logging features - options.experimental.enableLogs = YES;`; + // Enable logging features + options.enableLogs = YES;`; } snippet += ` diff --git a/src/apple/xcode-manager.ts b/src/apple/xcode-manager.ts index 996413e81..6ce8bce5c 100644 --- a/src/apple/xcode-manager.ts +++ b/src/apple/xcode-manager.ts @@ -84,7 +84,13 @@ function setDebugInformationFormatAndSandbox( } } -function addSentrySPM(proj: Project, targetName: string): void { +const FALLBACK_SENTRY_COCOA_VERSION = '8.0.0'; + +function addSentrySPM( + proj: Project, + targetName: string, + sdkVersion?: string, +): void { const xcObjects = proj.hash.project.objects; const sentryFrameworkUUID = proj.generateUuid(); @@ -176,12 +182,20 @@ function addSentrySPM(proj: Project, targetName: string): void { xcObjects.XCRemoteSwiftPackageReference = {}; } + let minimumVersion = FALLBACK_SENTRY_COCOA_VERSION; + if (sdkVersion) { + const majorMatch = sdkVersion.match(/^(\d+)\./); + if (majorMatch) { + minimumVersion = `${majorMatch[1]}.0.0`; + } + } + xcObjects.XCRemoteSwiftPackageReference[sentrySwiftPackageUUID] = { isa: 'XCRemoteSwiftPackageReference', repositoryURL: '"https://github.com/getsentry/sentry-cocoa/"', requirement: { kind: 'upToNextMajorVersion', - minimumVersion: '8.0.0', + minimumVersion, }, }; xcObjects.XCRemoteSwiftPackageReference[`${sentrySwiftPackageUUID}_comment`] = @@ -261,6 +275,7 @@ export class XcodeProject { target: string, addSPMReference: boolean, uploadSource = true, + sdkVersion?: string, ): void { this.addUploadSymbolsScript({ sentryProject, @@ -271,7 +286,7 @@ export class XcodeProject { setDebugInformationFormatAndSandbox(this.project, target); } if (addSPMReference) { - addSentrySPM(this.project, target); + addSentrySPM(this.project, target, sdkVersion); } const newContent = this.project.writeSync(); fs.writeFileSync(this.pbxprojPath, newContent); diff --git a/src/run.ts b/src/run.ts index c3304b08e..e3af84a28 100644 --- a/src/run.ts +++ b/src/run.ts @@ -9,6 +9,7 @@ import { run as legacyRun } from '../lib/Setup'; import { runAndroidWizard } from './android/android-wizard'; import { runAngularWizard } from './angular/angular-wizard'; import { runAppleWizard } from './apple/apple-wizard'; +import { runAppleDoctorWizard } from './apple/doctor/apple-doctor'; import { runFlutterWizard } from './flutter/flutter-wizard'; import { runNextjsWizard } from './nextjs/nextjs-wizard'; import { runNuxtWizard } from './nuxt/nuxt-wizard'; @@ -68,6 +69,7 @@ type Args = { comingFrom?: string; ignoreGitChanges?: boolean; xcodeProjectDir?: string; + fix?: boolean; }; function preSelectedProjectArgsToObject( @@ -156,6 +158,7 @@ export async function run(argv: Args) { comingFrom: finalArgs.comingFrom, ignoreGitChanges: finalArgs.ignoreGitChanges, spotlight: finalArgs.spotlight, + fix: finalArgs.fix, }; switch (integration) { @@ -168,10 +171,17 @@ export async function run(argv: Args) { break; case 'ios': - await runAppleWizard({ - ...wizardOptions, - projectDir: finalArgs.xcodeProjectDir, - }); + if (finalArgs.fix) { + await runAppleDoctorWizard({ + ...wizardOptions, + projectDir: finalArgs.xcodeProjectDir, + }); + } else { + await runAppleWizard({ + ...wizardOptions, + projectDir: finalArgs.xcodeProjectDir, + }); + } break; case 'android': diff --git a/src/utils/types.ts b/src/utils/types.ts index 9f4e2dd49..669344239 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -89,6 +89,12 @@ export type WizardOptions = { * This can be passed via the `--spotlight` arg. */ spotlight?: boolean; + + /** + * Run diagnostic checks and fix issues with an existing Sentry integration. + * This can be passed via the `--fix` arg. + */ + fix?: boolean; }; export interface Feature { diff --git a/test/apple/code-tools.test.ts b/test/apple/code-tools.test.ts index 36b2f0a6b..78290c825 100644 --- a/test/apple/code-tools.test.ts +++ b/test/apple/code-tools.test.ts @@ -1194,7 +1194,7 @@ describe('code-tools', () => { // -- Assert -- expect(result).toBeTruthy(); const fileContent = fs.readFileSync(filePath, 'utf8'); - expect(fileContent).toContain('options.experimental.enableLogs = true'); + expect(fileContent).toContain('options.enableLogs = true'); }); it('should add logs option to Objective-C file', () => { @@ -1212,7 +1212,7 @@ describe('code-tools', () => { // -- Assert -- expect(result).toBeTruthy(); const fileContent = fs.readFileSync(filePath, 'utf8'); - expect(fileContent).toContain('options.experimental.enableLogs = YES;'); + expect(fileContent).toContain('options.enableLogs = YES;'); }); }); @@ -1233,7 +1233,7 @@ describe('code-tools', () => { expect(result).toBeTruthy(); const fileContent = fs.readFileSync(filePath, 'utf8'); expect(fileContent).not.toContain( - 'options.experimental.enableLogs = true', + 'options.enableLogs = true', ); }); @@ -1253,7 +1253,7 @@ describe('code-tools', () => { expect(result).toBeTruthy(); const fileContent = fs.readFileSync(filePath, 'utf8'); expect(fileContent).not.toContain( - 'options.experimental.enableLogs = YES;', + 'options.enableLogs = YES;', ); }); }); diff --git a/test/apple/templates.test.ts b/test/apple/templates.test.ts index d496d604a..7dd09c2dd 100644 --- a/test/apple/templates.test.ts +++ b/test/apple/templates.test.ts @@ -26,6 +26,10 @@ if [[ "$(uname -m)" == arm64 ]]; then export PATH="/opt/homebrew/bin:$PATH" fi +if [ "$SENTRY_SKIP_DSYM_UPLOAD" = "1" ]; then + echo "SENTRY_SKIP_DSYM_UPLOAD is set, skipping dSYM upload." + exit 0 +fi if which sentry-cli >/dev/null; then export SENTRY_ORG=test-org export SENTRY_PROJECT=test-project @@ -42,6 +46,10 @@ fi uploadSource: true, includeHomebrewPath: false, expectedScript: `# This script is responsible for uploading debug symbols and source context for Sentry. +if [ "$SENTRY_SKIP_DSYM_UPLOAD" = "1" ]; then + echo "SENTRY_SKIP_DSYM_UPLOAD is set, skipping dSYM upload." + exit 0 +fi if which sentry-cli >/dev/null; then export SENTRY_ORG=test-org export SENTRY_PROJECT=test-project @@ -62,6 +70,10 @@ if [[ "$(uname -m)" == arm64 ]]; then export PATH="/opt/homebrew/bin:$PATH" fi +if [ "$SENTRY_SKIP_DSYM_UPLOAD" = "1" ]; then + echo "SENTRY_SKIP_DSYM_UPLOAD is set, skipping dSYM upload." + exit 0 +fi if which sentry-cli >/dev/null; then export SENTRY_ORG=test-org export SENTRY_PROJECT=test-project @@ -78,6 +90,10 @@ fi uploadSource: false, includeHomebrewPath: false, expectedScript: `# This script is responsible for uploading debug symbols and source context for Sentry. +if [ "$SENTRY_SKIP_DSYM_UPLOAD" = "1" ]; then + echo "SENTRY_SKIP_DSYM_UPLOAD is set, skipping dSYM upload." + exit 0 +fi if which sentry-cli >/dev/null; then export SENTRY_ORG=test-org export SENTRY_PROJECT=test-project @@ -179,8 +195,8 @@ fi // options.attachScreenshot = true // This adds a screenshot to the error events // options.attachViewHierarchy = true // This adds the view hierarchy to the error events - // Enable experimental logging features - options.experimental.enableLogs = true + // Enable logging features + options.enableLogs = true } // Remove the next line after confirming that your Sentry integration is working. SentrySDK.capture(message: "This app uses Sentry! :)") @@ -250,8 +266,8 @@ fi //options.attachScreenshot = YES; //This will add a screenshot to the error events //options.attachViewHierarchy = YES; //This will add the view hierarchy to the error events - // Enable experimental logging features - options.experimental.enableLogs = YES; + // Enable logging features + options.enableLogs = YES; }]; //Remove the next line after confirming that your Sentry integration is working. [SentrySDK captureMessage:@"This app uses Sentry!"]; diff --git a/test/apple/xcode-manager.test.ts b/test/apple/xcode-manager.test.ts index 7b9c50ab2..98022c664 100644 --- a/test/apple/xcode-manager.test.ts +++ b/test/apple/xcode-manager.test.ts @@ -594,6 +594,85 @@ describe('XcodeManager', () => { }), ]); }); + + it('should use dynamic SDK version when provided', () => { + // -- Act -- + xcodeProject.updateXcodeProject( + projectData, + 'Project', + addSPMReference, + true, + '9.8.0', + ); + + // -- Assert -- + const remoteSwiftPackageReferences = + xcodeProject.objects.XCRemoteSwiftPackageReference; + expect(remoteSwiftPackageReferences).toBeDefined(); + const rspRefKeys = Object.keys( + remoteSwiftPackageReferences ?? {}, + ).filter((k) => !k.endsWith('_comment')); + expect(remoteSwiftPackageReferences?.[rspRefKeys[0]]).toEqual({ + isa: 'XCRemoteSwiftPackageReference', + repositoryURL: '"https://github.com/getsentry/sentry-cocoa/"', + requirement: { + kind: 'upToNextMajorVersion', + minimumVersion: '9.0.0', + }, + }); + }); + + it('should fall back to 8.0.0 when sdkVersion is undefined', () => { + // -- Act -- + xcodeProject.updateXcodeProject( + projectData, + 'Project', + addSPMReference, + true, + undefined, + ); + + // -- Assert -- + const remoteSwiftPackageReferences = + xcodeProject.objects.XCRemoteSwiftPackageReference; + const rspRefKeys = Object.keys( + remoteSwiftPackageReferences ?? {}, + ).filter((k) => !k.endsWith('_comment')); + expect(remoteSwiftPackageReferences?.[rspRefKeys[0]]).toEqual( + expect.objectContaining({ + requirement: { + kind: 'upToNextMajorVersion', + minimumVersion: '8.0.0', + }, + }), + ); + }); + + it('should fall back to 8.0.0 when sdkVersion is malformed', () => { + // -- Act -- + xcodeProject.updateXcodeProject( + projectData, + 'Project', + addSPMReference, + true, + 'not-a-version', + ); + + // -- Assert -- + const remoteSwiftPackageReferences = + xcodeProject.objects.XCRemoteSwiftPackageReference; + const rspRefKeys = Object.keys( + remoteSwiftPackageReferences ?? {}, + ).filter((k) => !k.endsWith('_comment')); + expect(remoteSwiftPackageReferences?.[rspRefKeys[0]]).toEqual( + expect.objectContaining({ + requirement: { + kind: 'upToNextMajorVersion', + minimumVersion: '8.0.0', + }, + }), + ); + }); }); });