From ea5902b808c0dbc9a46e38d6f415227319760b39 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Feb 2026 17:12:14 +0100 Subject: [PATCH 1/4] fix(ios): Add wait logic for dSYM generation in Xcode build phase --- .../core/scripts/sentry-xcode-debug-files.sh | 139 +++++++++++++ packages/core/scripts/test-dsym-fix.sh | 125 ++++++++++++ packages/core/scripts/test-dsym-wait.sh | 61 ++++++ .../expo-plugin/modifyXcodeProject.test.ts | 107 ++++++++++ .../test/scripts/sentry-xcode-scripts.test.ts | 187 ++++++++++++++++++ 5 files changed, 619 insertions(+) create mode 100755 packages/core/scripts/test-dsym-fix.sh create mode 100755 packages/core/scripts/test-dsym-wait.sh diff --git a/packages/core/scripts/sentry-xcode-debug-files.sh b/packages/core/scripts/sentry-xcode-debug-files.sh index 2200d1e515..5010cb5afc 100755 --- a/packages/core/scripts/sentry-xcode-debug-files.sh +++ b/packages/core/scripts/sentry-xcode-debug-files.sh @@ -58,6 +58,139 @@ EXTRA_ARGS="$SENTRY_CLI_EXTRA_ARGS $SENTRY_CLI_DEBUG_FILES_UPLOAD_EXTRA_ARGS $IN UPLOAD_DEBUG_FILES="\"$SENTRY_CLI_EXECUTABLE\" debug-files upload $EXTRA_ARGS \"$DWARF_DSYM_FOLDER_PATH\"" +# Function to wait for dSYM files to be generated +# This addresses a race condition where the upload script runs before dSYM generation completes +wait_for_dsym_files() { + local max_attempts="${SENTRY_DSYM_WAIT_MAX_ATTEMPTS:-10}" + local wait_interval="${SENTRY_DSYM_WAIT_INTERVAL:-2}" + local attempt=1 + + # Check if we should wait for dSYM files + if [ "$SENTRY_DSYM_WAIT_ENABLED" == "false" ]; then + echo "SENTRY_DSYM_WAIT_ENABLED=false, skipping dSYM wait check" + return 0 + fi + + # Warn if DWARF_DSYM_FILE_NAME is not set - we can't verify the main app dSYM + if [ -z "$DWARF_DSYM_FILE_NAME" ]; then + echo "warning: DWARF_DSYM_FILE_NAME not set, cannot verify main app dSYM specifically" + echo "warning: Will proceed when any dSYM bundle is found" + fi + + echo "Checking for dSYM files in: $DWARF_DSYM_FOLDER_PATH" + + # Debug information to help diagnose issues + if [ -n "${SENTRY_DSYM_DEBUG}" ]; then + echo "DEBUG: DWARF_DSYM_FOLDER_PATH=$DWARF_DSYM_FOLDER_PATH" + echo "DEBUG: DWARF_DSYM_FILE_NAME=$DWARF_DSYM_FILE_NAME" + echo "DEBUG: PRODUCT_NAME=$PRODUCT_NAME" + if [ -d "$DWARF_DSYM_FOLDER_PATH" ]; then + echo "DEBUG: Contents of dSYM folder:" + ls -la "$DWARF_DSYM_FOLDER_PATH" 2>/dev/null || echo "Cannot list folder" + else + echo "DEBUG: dSYM folder does not exist yet" + fi + fi + + while [ $attempt -le $max_attempts ]; do + # Check if the dSYM folder exists + if [ -d "$DWARF_DSYM_FOLDER_PATH" ]; then + # Check if there are any .dSYM bundles in the folder + local dsym_count=$(find "$DWARF_DSYM_FOLDER_PATH" -name "*.dSYM" -type d 2>/dev/null | wc -l | tr -d ' ') + + if [ "$dsym_count" -gt 0 ]; then + echo "Found $dsym_count dSYM bundle(s) in $DWARF_DSYM_FOLDER_PATH" + + # If DWARF_DSYM_FILE_NAME is set, verify the main app dSYM exists and is complete + if [ -n "$DWARF_DSYM_FILE_NAME" ]; then + local main_dsym="$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME" + + if [ -d "$main_dsym" ]; then + # Directory exists, now verify the actual DWARF binary exists inside + local dwarf_dir="$main_dsym/Contents/Resources/DWARF" + + if [ -d "$dwarf_dir" ]; then + # Check if there are any files in the DWARF directory + local dwarf_files=$(find "$dwarf_dir" -type f 2>/dev/null | head -1) + + if [ -n "$dwarf_files" ]; then + # Verify the DWARF file is not empty (still being written) + local dwarf_size=$(find "$dwarf_dir" -type f -size +0 2>/dev/null | head -1) + + if [ -n "$dwarf_size" ]; then + echo "Verified main app dSYM is complete: $DWARF_DSYM_FILE_NAME" + return 0 + else + echo "Main app dSYM DWARF binary is empty (still being written): $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + echo "Main app dSYM DWARF directory is empty: $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + echo "Main app dSYM structure incomplete (missing DWARF directory): $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + echo "Main app dSYM not found yet: $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + # DWARF_DSYM_FILE_NAME not set, check if any dSYM has valid DWARF content + # This is less strict but better than nothing + local has_valid_dsym=false + for dsym in "$DWARF_DSYM_FOLDER_PATH"/*.dSYM; do + if [ -d "$dsym/Contents/Resources/DWARF" ]; then + local dwarf_files=$(find "$dsym/Contents/Resources/DWARF" -type f -size +0 2>/dev/null | head -1) + if [ -n "$dwarf_files" ]; then + has_valid_dsym=true + break + fi + fi + done + + if [ "$has_valid_dsym" = true ]; then + echo "Found dSYM bundle(s) with valid DWARF content" + return 0 + else + echo "Found dSYM bundle(s) but none have complete DWARF content yet (attempt $attempt/$max_attempts)" + fi + fi + else + echo "No dSYM bundles found yet in $DWARF_DSYM_FOLDER_PATH (attempt $attempt/$max_attempts)" + fi + else + echo "dSYM folder does not exist yet: $DWARF_DSYM_FOLDER_PATH (attempt $attempt/$max_attempts)" + fi + + if [ $attempt -lt $max_attempts ]; then + # Progressive backoff: quick checks first, longer waits later + # Attempts 1-3: 0.5s (total 1.5s) + # Attempts 4-6: 1s (total 3s) + # Attempts 7+: 2s (remaining time) + local current_interval="$wait_interval" + if [ -z "${SENTRY_DSYM_WAIT_INTERVAL}" ]; then + # Only use progressive intervals if user hasn't set custom interval + if [ $attempt -le 3 ]; then + current_interval=0.5 + elif [ $attempt -le 6 ]; then + current_interval=1 + else + current_interval=2 + fi + fi + + echo "Waiting ${current_interval}s for dSYM generation to complete..." + sleep $current_interval + fi + + attempt=$((attempt + 1)) + done + + # Timeout reached + echo "warning: Timeout waiting for dSYM files after $((max_attempts * wait_interval))s" + echo "warning: This may result in incomplete debug symbol uploads" + echo "warning: To disable this check, set SENTRY_DSYM_WAIT_ENABLED=false" + return 1 +} + XCODE_BUILD_CONFIGURATION="${CONFIGURATION}" if [ "$SENTRY_DISABLE_AUTO_UPLOAD" == true ]; then @@ -67,6 +200,12 @@ elif [ "$SENTRY_DISABLE_XCODE_DEBUG_UPLOAD" == true ]; then elif echo "$XCODE_BUILD_CONFIGURATION" | grep -iq "debug"; then # case insensitive check for "debug" echo "Skipping debug files upload for *Debug* configuration" else + # Wait for dSYM files to be generated (addresses race condition in EAS builds) + # Don't fail the script if wait times out - we still want to attempt upload + set +e + wait_for_dsym_files + set -e + # 'warning:' triggers a warning in Xcode, 'error:' triggers an error set +x +e # disable printing commands otherwise we might print `error:` by accident and allow continuing on error SENTRY_UPLOAD_COMMAND_OUTPUT=$(/bin/sh -c "\"$LOCAL_NODE_BINARY\" $UPLOAD_DEBUG_FILES" 2>&1) diff --git a/packages/core/scripts/test-dsym-fix.sh b/packages/core/scripts/test-dsym-fix.sh new file mode 100755 index 0000000000..31d038f4ed --- /dev/null +++ b/packages/core/scripts/test-dsym-fix.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Test script to verify dSYM upload fix +# Usage: ./test-dsym-fix.sh [test-project-path] + +set -e + +PROJECT_PATH="${1:-.}" +SDK_PATH="$(cd "$(dirname "$0")/../../.." && pwd)" + +echo "=== Sentry React Native dSYM Fix Testing ===" +echo "" +echo "SDK Path: $SDK_PATH" +echo "Project Path: $PROJECT_PATH" +echo "" + +# Function to check Sentry debug files +check_sentry_debug_files() { + echo "=== Checking Sentry Debug Files ===" + echo "" + echo "Please check Sentry Debug Files manually:" + echo "1. Go to: https://sentry.io" + echo "2. Navigate to: Settings > Projects > [Your Project] > Debug Files" + echo "3. Look for recent uploads with 'debug' tag" + echo "" + echo "Expected to see:" + echo " ✓ Main app dSYM with 'debug' tag (~145MB)" + echo " ✓ Framework dSYMs" + echo "" + read -p "Press Enter to continue..." +} + +# Test with current version +test_current_version() { + echo "=== Phase 1: Testing with v7.12.1 (current stable) ===" + echo "" + + cd "$PROJECT_PATH" + + echo "Installing @sentry/react-native@7.12.1..." + yarn add @sentry/react-native@7.12.1 || npm install @sentry/react-native@7.12.1 + + echo "" + echo "Cleaning and regenerating native code..." + npx expo prebuild --clean + + echo "" + echo "Building with EAS..." + echo "Watch for 'Upload Debug Symbols to Sentry' in logs" + echo "" + + eas build --platform ios --profile production --local 2>&1 | tee build-v7.12.1.log + + echo "" + check_sentry_debug_files +} + +# Test with our fix +test_with_fix() { + echo "=== Phase 2: Testing with dSYM wait fix ===" + echo "" + + cd "$SDK_PATH" + echo "Building SDK..." + yarn build + + cd "$PROJECT_PATH" + + echo "" + echo "Linking to local SDK..." + yarn link "$SDK_PATH/packages/core" || npm link "$SDK_PATH/packages/core" + + echo "" + echo "Cleaning and regenerating native code..." + npx expo prebuild --clean + + echo "" + echo "Building with debug logging enabled..." + echo "Look for:" + echo " - 'DEBUG: DWARF_DSYM_FOLDER_PATH=...'" + echo " - 'DEBUG: DWARF_DSYM_FILE_NAME=...'" + echo " - 'Verified main app dSYM is complete'" + echo "" + + SENTRY_DSYM_DEBUG=true eas build --platform ios --profile production --local 2>&1 | tee build-with-fix.log + + echo "" + check_sentry_debug_files +} + +# Main menu +echo "Choose test to run:" +echo "1) Test current v7.12.1 (reproduce issue)" +echo "2) Test with fix (verify solution)" +echo "3) Run both tests" +echo "" +read -p "Enter choice [1-3]: " choice + +case $choice in + 1) + test_current_version + ;; + 2) + test_with_fix + ;; + 3) + test_current_version + echo "" + echo "========================================" + echo "" + test_with_fix + ;; + *) + echo "Invalid choice" + exit 1 + ;; +esac + +echo "" +echo "=== Testing Complete ===" +echo "" +echo "Build logs saved:" +echo " - build-v7.12.1.log (if tested)" +echo " - build-with-fix.log (if tested)" +echo "" +echo "Please compare the results in Sentry Debug Files" diff --git a/packages/core/scripts/test-dsym-wait.sh b/packages/core/scripts/test-dsym-wait.sh new file mode 100755 index 0000000000..e451ddbd5b --- /dev/null +++ b/packages/core/scripts/test-dsym-wait.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Manual test script for dSYM wait functionality +# This simulates the wait behavior without needing a full Xcode build + +set -x + +# Create a test directory +TEST_DIR="/tmp/sentry-dsym-wait-test-$$" +mkdir -p "$TEST_DIR" + +echo "=== Test 1: dSYM appears immediately ===" +DSYM_DIR="$TEST_DIR/test1" +mkdir -p "$DSYM_DIR/TestApp.app.dSYM" +export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR" +export DWARF_DSYM_FILE_NAME="TestApp.app.dSYM" +export SENTRY_DSYM_WAIT_MAX_ATTEMPTS=3 +export SENTRY_DSYM_WAIT_INTERVAL=1 + +# Source the wait function +source "$(dirname "$0")/sentry-xcode-debug-files.sh" 2>/dev/null || { + # If sourcing fails, extract just the wait function + eval "$(sed -n '/^wait_for_dsym_files()/,/^}/p' "$(dirname "$0")/sentry-xcode-debug-files.sh")" +} + +wait_for_dsym_files +echo "Test 1 result: $?" +echo "" + +echo "=== Test 2: dSYM appears after delay ===" +DSYM_DIR2="$TEST_DIR/test2" +mkdir -p "$DSYM_DIR2" +export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR2" +export DWARF_DSYM_FILE_NAME="DelayedApp.app.dSYM" + +# Create dSYM in background after 2 seconds +(sleep 2 && mkdir -p "$DSYM_DIR2/DelayedApp.app.dSYM" && echo "Background: Created dSYM") & + +wait_for_dsym_files +echo "Test 2 result: $?" +echo "" + +echo "=== Test 3: dSYM never appears (timeout) ===" +DSYM_DIR3="$TEST_DIR/test3" +mkdir -p "$DSYM_DIR3" +export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR3" +export DWARF_DSYM_FILE_NAME="NeverExists.app.dSYM" +export SENTRY_DSYM_WAIT_MAX_ATTEMPTS=2 + +wait_for_dsym_files +echo "Test 3 result: $?" +echo "" + +echo "=== Test 4: Wait disabled ===" +export SENTRY_DSYM_WAIT_ENABLED=false +wait_for_dsym_files +echo "Test 4 result: $?" +echo "" + +# Cleanup +rm -rf "$TEST_DIR" +echo "=== All tests complete ===" diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index 92dc615835..23cbe42c7e 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -77,3 +77,110 @@ describe('Configures iOS native project correctly', () => { expect(warnOnce).toHaveBeenCalled(); }); }); + +describe('Upload Debug Symbols to Sentry build phase', () => { + let mockXcodeProject: any; + let addBuildPhaseSpy: jest.Mock; + const expectedShellScript = + "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + + const getOptions = () => { + const callArgs = addBuildPhaseSpy.mock.calls[0]; + return callArgs[4]; + }; + + beforeEach(() => { + addBuildPhaseSpy = jest.fn(); + mockXcodeProject = { + pbxItemByComment: jest.fn().mockReturnValue(null), + addBuildPhase: addBuildPhaseSpy, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('creates Upload Debug Symbols build phase with correct shell script', () => { + mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }); + + expect(addBuildPhaseSpy).toHaveBeenCalledWith( + [], + 'PBXShellScriptBuildPhase', + 'Upload Debug Symbols to Sentry', + null, + { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }, + ); + }); + + it('does not include inputPaths to avoid circular dependency', () => { + // We don't use inputPaths because they cause circular dependency errors in Xcode 15+ + // (see issue #5641). Instead, the bash script waits for dSYM files to be generated. + mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }); + + const options = getOptions(); + + expect(options.inputPaths).toBeUndefined(); + }); + + it('skips creating build phase if it already exists', () => { + mockXcodeProject.pbxItemByComment = jest.fn().mockReturnValue({ + shellScript: 'existing', + }); + + expect(addBuildPhaseSpy).not.toHaveBeenCalled(); + }); + + describe('Race condition handling', () => { + it('documents why we do not use inputPaths', () => { + // This test documents the decision NOT to use inputPaths. + // + // ISSUE #5288: Race condition where upload script runs before dSYM generation completes + // ISSUE #5641: inputPaths cause circular dependency errors in Xcode 15+ + // + // We attempted to fix #5288 by adding inputPaths to declare dependency on dSYM files: + // inputPaths: [ + // '"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)"', + // '"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)"', + // ] + // + // However, this caused Xcode 15+ to fail with: + // "Cycle inside X; building could produce unreliable results" + // + // The cycle occurs because: + // 1. The target produces the dSYM as an output during linking + // 2. The "Upload Debug Symbols" build phase (part of the same target) declares the dSYM as an input + // 3. Xcode detects: target depends on its own output = CYCLE + // + // SOLUTION: Instead of using inputPaths, the bash script (sentry-xcode-debug-files.sh) + // now waits for dSYM files to exist before uploading. This avoids the circular dependency + // while still handling the race condition. + // + // See: + // - https://github.com/getsentry/sentry-react-native/issues/5288 + // - https://github.com/getsentry/sentry-react-native/issues/5641 + // - https://developer.apple.com/forums/thread/730974 + + mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }); + + const options = getOptions(); + + // Verify that inputPaths are NOT used + expect(options.inputPaths).toBeUndefined(); + expect(options.shellPath).toBe('/bin/sh'); + expect(options.shellScript).toBe(expectedShellScript); + }); + }); +}); diff --git a/packages/core/test/scripts/sentry-xcode-scripts.test.ts b/packages/core/test/scripts/sentry-xcode-scripts.test.ts index b4529b9104..6b4c77b0a8 100644 --- a/packages/core/test/scripts/sentry-xcode-scripts.test.ts +++ b/packages/core/test/scripts/sentry-xcode-scripts.test.ts @@ -136,6 +136,193 @@ describe('sentry-xcode-debug-files.sh', () => { expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Skipping debug files upload for *Debug* configuration'); }); + + describe('dSYM wait functionality', () => { + it('proceeds immediately when dSYM folder already exists with complete dSYM files', () => { + // Create a complete dSYM bundle structure with DWARF binary + const dsymPath = path.join(tempDir, 'TestApp.app.dSYM'); + const dwarfDir = path.join(dsymPath, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir, { recursive: true }); + // Create a non-empty DWARF binary file + fs.writeFileSync(path.join(dwarfDir, 'TestApp'), 'mock dwarf binary content'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: tempDir, + DWARF_DSYM_FILE_NAME: 'TestApp.app.dSYM', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Checking for dSYM files'); + expect(result.stdout).toContain('Found'); + expect(result.stdout).toContain('dSYM bundle(s)'); + expect(result.stdout).toContain('Verified main app dSYM is complete'); + // Should not have waited since dSYM exists + expect(result.stdout).not.toContain('Waiting'); + }); + + // Note: Testing "file appears during wait" scenario is difficult with execSync + // as it blocks the Node.js process. The wait logic is adequately covered by + // the "proceeds immediately" and "times out" tests. + + it('times out when dSYM never appears', () => { + const dsymFolderPath = path.join(tempDir, 'empty-dsym-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'NonExistent.app.dSYM', + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Checking for dSYM files'); + expect(result.stdout).toContain('Waiting'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + expect(result.stdout).toContain('This may result in incomplete debug symbol uploads'); + }); + + it('skips wait check when SENTRY_DSYM_WAIT_ENABLED=false', () => { + const result = runScript({ + SENTRY_DSYM_WAIT_ENABLED: 'false', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('SENTRY_DSYM_WAIT_ENABLED=false'); + expect(result.stdout).toContain('skipping dSYM wait check'); + expect(result.stdout).not.toContain('Checking for dSYM files'); + }); + + it('proceeds when folder contains any dSYM even without DWARF_DSYM_FILE_NAME', () => { + // Create some complete dSYM bundles + const dsymPath1 = path.join(tempDir, 'Framework1.framework.dSYM'); + const dwarfDir1 = path.join(dsymPath1, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir1, { recursive: true }); + fs.writeFileSync(path.join(dwarfDir1, 'Framework1'), 'mock dwarf content'); + + const dsymPath2 = path.join(tempDir, 'Framework2.framework.dSYM'); + const dwarfDir2 = path.join(dsymPath2, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir2, { recursive: true }); + fs.writeFileSync(path.join(dwarfDir2, 'Framework2'), 'mock dwarf content'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: tempDir, + // DWARF_DSYM_FILE_NAME not set + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('warning: DWARF_DSYM_FILE_NAME not set'); + expect(result.stdout).toContain('Found'); + expect(result.stdout).toContain('dSYM bundle(s)'); + expect(result.stdout).toContain('Found dSYM bundle(s) with valid DWARF content'); + }); + + it('continues waiting if main app dSYM not found but other dSYMs exist', () => { + const dsymFolderPath = path.join(tempDir, 'dsym-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + // Create only framework dSYM with complete structure, not the main app dSYM + const frameworkDsym = path.join(dsymFolderPath, 'SomeFramework.framework.dSYM'); + const frameworkDwarfDir = path.join(frameworkDsym, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(frameworkDwarfDir, { recursive: true }); + fs.writeFileSync(path.join(frameworkDwarfDir, 'SomeFramework'), 'mock dwarf content'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'MainApp.app.dSYM', // Looking for this specific one + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Main app dSYM not found yet'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('waits when dSYM directory exists but DWARF binary is missing (incomplete)', () => { + const dsymFolderPath = path.join(tempDir, 'incomplete-dsym-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + // Create dSYM directory structure but without DWARF binary (incomplete) + const incompleteDsym = path.join(dsymFolderPath, 'IncompleteApp.app.dSYM'); + const dwarfDir = path.join(incompleteDsym, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir, { recursive: true }); + // Note: NOT creating the actual DWARF file + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'IncompleteApp.app.dSYM', + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Main app dSYM DWARF directory is empty'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('waits when dSYM exists but DWARF binary is empty (still being written)', () => { + const dsymFolderPath = path.join(tempDir, 'empty-dwarf-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + // Create dSYM with empty DWARF file (simulates file being created but not written yet) + const dsymPath = path.join(dsymFolderPath, 'WritingApp.app.dSYM'); + const dwarfDir = path.join(dsymPath, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir, { recursive: true }); + fs.writeFileSync(path.join(dwarfDir, 'WritingApp'), ''); // Empty file + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'WritingApp.app.dSYM', + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Main app dSYM DWARF binary is empty (still being written)'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('handles non-existent dSYM folder path', () => { + const nonExistentPath = path.join(tempDir, 'does-not-exist'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: nonExistentPath, + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('dSYM folder does not exist yet'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('respects custom wait interval and max attempts', () => { + const startTime = Date.now(); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: path.join(tempDir, 'nonexistent'), + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '3', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + const duration = Date.now() - startTime; + + expect(result.exitCode).toBe(0); + // Should have waited approximately 2 seconds (3 attempts with 1s interval, but no wait after last attempt) + expect(duration).toBeGreaterThanOrEqual(2000); + expect(duration).toBeLessThan(4000); // Allow some margin + }); + }); }); describe('sentry-xcode.sh', () => { From ff06ea3ed6fc2e80637ef9478cf9069e8654fe78 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 13 Feb 2026 15:05:53 +0100 Subject: [PATCH 2/4] fix(ios): Correct timeout calculation in dSYM wait logic Fixed the timeout warning message to report actual elapsed time instead of calculated max_attempts * wait_interval. With progressive backoff (0.5s, 1s, 2s), the old calculation was incorrect (e.g., reported 20s when actual was ~12.5s). Changes: - Track actual wait time with total_wait_time variable - Accumulate elapsed time after each sleep interval - Update timeout message to show actual time and attempt count - Add reference to BUILD_CONFIGURATION.md for configuration options Co-Authored-By: Claude Sonnet 4.5 --- packages/core/scripts/sentry-xcode-debug-files.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/scripts/sentry-xcode-debug-files.sh b/packages/core/scripts/sentry-xcode-debug-files.sh index 5010cb5afc..8df0768136 100755 --- a/packages/core/scripts/sentry-xcode-debug-files.sh +++ b/packages/core/scripts/sentry-xcode-debug-files.sh @@ -1,6 +1,8 @@ #!/bin/bash # Upload Debug Symbols to Sentry Xcode Build Phase # PWD=ios +# +# Configuration: See BUILD_CONFIGURATION.md for all available environment variables # print commands before executing them set -x @@ -64,6 +66,7 @@ wait_for_dsym_files() { local max_attempts="${SENTRY_DSYM_WAIT_MAX_ATTEMPTS:-10}" local wait_interval="${SENTRY_DSYM_WAIT_INTERVAL:-2}" local attempt=1 + local total_wait_time=0 # Check if we should wait for dSYM files if [ "$SENTRY_DSYM_WAIT_ENABLED" == "false" ]; then @@ -179,13 +182,14 @@ wait_for_dsym_files() { echo "Waiting ${current_interval}s for dSYM generation to complete..." sleep $current_interval + total_wait_time=$(awk "BEGIN {print $total_wait_time + $current_interval}") fi attempt=$((attempt + 1)) done # Timeout reached - echo "warning: Timeout waiting for dSYM files after $((max_attempts * wait_interval))s" + echo "warning: Timeout waiting for dSYM files after ${total_wait_time}s ($max_attempts attempts)" echo "warning: This may result in incomplete debug symbol uploads" echo "warning: To disable this check, set SENTRY_DSYM_WAIT_ENABLED=false" return 1 From af4f5d9a63d96a94c74e1eead4ac887ee2f243a7 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 13 Feb 2026 15:13:19 +0100 Subject: [PATCH 3/4] Adds changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad3bede7d8..fd747e3fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ }); ``` +### Fixes + +- Fix race condition where iOS dSYM upload runs before debug symbols are fully generated ([#5653](https://github.com/getsentry/sentry-react-native/pull/5653)) + ## 8.0.0 ### Upgrading from 7.x to 8.0 From 12607ad959057083b2be74feb2b193e167a1e6e6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 13 Feb 2026 15:36:32 +0100 Subject: [PATCH 4/4] Remove test scripts --- packages/core/scripts/test-dsym-fix.sh | 125 ------------------------ packages/core/scripts/test-dsym-wait.sh | 61 ------------ 2 files changed, 186 deletions(-) delete mode 100755 packages/core/scripts/test-dsym-fix.sh delete mode 100755 packages/core/scripts/test-dsym-wait.sh diff --git a/packages/core/scripts/test-dsym-fix.sh b/packages/core/scripts/test-dsym-fix.sh deleted file mode 100755 index 31d038f4ed..0000000000 --- a/packages/core/scripts/test-dsym-fix.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash -# Test script to verify dSYM upload fix -# Usage: ./test-dsym-fix.sh [test-project-path] - -set -e - -PROJECT_PATH="${1:-.}" -SDK_PATH="$(cd "$(dirname "$0")/../../.." && pwd)" - -echo "=== Sentry React Native dSYM Fix Testing ===" -echo "" -echo "SDK Path: $SDK_PATH" -echo "Project Path: $PROJECT_PATH" -echo "" - -# Function to check Sentry debug files -check_sentry_debug_files() { - echo "=== Checking Sentry Debug Files ===" - echo "" - echo "Please check Sentry Debug Files manually:" - echo "1. Go to: https://sentry.io" - echo "2. Navigate to: Settings > Projects > [Your Project] > Debug Files" - echo "3. Look for recent uploads with 'debug' tag" - echo "" - echo "Expected to see:" - echo " ✓ Main app dSYM with 'debug' tag (~145MB)" - echo " ✓ Framework dSYMs" - echo "" - read -p "Press Enter to continue..." -} - -# Test with current version -test_current_version() { - echo "=== Phase 1: Testing with v7.12.1 (current stable) ===" - echo "" - - cd "$PROJECT_PATH" - - echo "Installing @sentry/react-native@7.12.1..." - yarn add @sentry/react-native@7.12.1 || npm install @sentry/react-native@7.12.1 - - echo "" - echo "Cleaning and regenerating native code..." - npx expo prebuild --clean - - echo "" - echo "Building with EAS..." - echo "Watch for 'Upload Debug Symbols to Sentry' in logs" - echo "" - - eas build --platform ios --profile production --local 2>&1 | tee build-v7.12.1.log - - echo "" - check_sentry_debug_files -} - -# Test with our fix -test_with_fix() { - echo "=== Phase 2: Testing with dSYM wait fix ===" - echo "" - - cd "$SDK_PATH" - echo "Building SDK..." - yarn build - - cd "$PROJECT_PATH" - - echo "" - echo "Linking to local SDK..." - yarn link "$SDK_PATH/packages/core" || npm link "$SDK_PATH/packages/core" - - echo "" - echo "Cleaning and regenerating native code..." - npx expo prebuild --clean - - echo "" - echo "Building with debug logging enabled..." - echo "Look for:" - echo " - 'DEBUG: DWARF_DSYM_FOLDER_PATH=...'" - echo " - 'DEBUG: DWARF_DSYM_FILE_NAME=...'" - echo " - 'Verified main app dSYM is complete'" - echo "" - - SENTRY_DSYM_DEBUG=true eas build --platform ios --profile production --local 2>&1 | tee build-with-fix.log - - echo "" - check_sentry_debug_files -} - -# Main menu -echo "Choose test to run:" -echo "1) Test current v7.12.1 (reproduce issue)" -echo "2) Test with fix (verify solution)" -echo "3) Run both tests" -echo "" -read -p "Enter choice [1-3]: " choice - -case $choice in - 1) - test_current_version - ;; - 2) - test_with_fix - ;; - 3) - test_current_version - echo "" - echo "========================================" - echo "" - test_with_fix - ;; - *) - echo "Invalid choice" - exit 1 - ;; -esac - -echo "" -echo "=== Testing Complete ===" -echo "" -echo "Build logs saved:" -echo " - build-v7.12.1.log (if tested)" -echo " - build-with-fix.log (if tested)" -echo "" -echo "Please compare the results in Sentry Debug Files" diff --git a/packages/core/scripts/test-dsym-wait.sh b/packages/core/scripts/test-dsym-wait.sh deleted file mode 100755 index e451ddbd5b..0000000000 --- a/packages/core/scripts/test-dsym-wait.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -# Manual test script for dSYM wait functionality -# This simulates the wait behavior without needing a full Xcode build - -set -x - -# Create a test directory -TEST_DIR="/tmp/sentry-dsym-wait-test-$$" -mkdir -p "$TEST_DIR" - -echo "=== Test 1: dSYM appears immediately ===" -DSYM_DIR="$TEST_DIR/test1" -mkdir -p "$DSYM_DIR/TestApp.app.dSYM" -export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR" -export DWARF_DSYM_FILE_NAME="TestApp.app.dSYM" -export SENTRY_DSYM_WAIT_MAX_ATTEMPTS=3 -export SENTRY_DSYM_WAIT_INTERVAL=1 - -# Source the wait function -source "$(dirname "$0")/sentry-xcode-debug-files.sh" 2>/dev/null || { - # If sourcing fails, extract just the wait function - eval "$(sed -n '/^wait_for_dsym_files()/,/^}/p' "$(dirname "$0")/sentry-xcode-debug-files.sh")" -} - -wait_for_dsym_files -echo "Test 1 result: $?" -echo "" - -echo "=== Test 2: dSYM appears after delay ===" -DSYM_DIR2="$TEST_DIR/test2" -mkdir -p "$DSYM_DIR2" -export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR2" -export DWARF_DSYM_FILE_NAME="DelayedApp.app.dSYM" - -# Create dSYM in background after 2 seconds -(sleep 2 && mkdir -p "$DSYM_DIR2/DelayedApp.app.dSYM" && echo "Background: Created dSYM") & - -wait_for_dsym_files -echo "Test 2 result: $?" -echo "" - -echo "=== Test 3: dSYM never appears (timeout) ===" -DSYM_DIR3="$TEST_DIR/test3" -mkdir -p "$DSYM_DIR3" -export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR3" -export DWARF_DSYM_FILE_NAME="NeverExists.app.dSYM" -export SENTRY_DSYM_WAIT_MAX_ATTEMPTS=2 - -wait_for_dsym_files -echo "Test 3 result: $?" -echo "" - -echo "=== Test 4: Wait disabled ===" -export SENTRY_DSYM_WAIT_ENABLED=false -wait_for_dsym_files -echo "Test 4 result: $?" -echo "" - -# Cleanup -rm -rf "$TEST_DIR" -echo "=== All tests complete ==="